5 Commits
Author SHA1 Message Date
melekhin fa77b2ce39 666 2026-08-10 15:23:45 +07:00
melekhin b023d2755e 555+ 2026-08-10 14:59:18 +07:00
melekhin 2811a40c6d Rotates 555 2026-08-10 14:48:49 +07:00
melekhin 9c81a084f1 ROTATES 2 2026-08-10 13:58:11 +07:00
melekhin 265bfc7419 optimize 2026-08-03 16:31:54 +07:00
3 changed files with 408 additions and 269 deletions
+374 -204
View File
@@ -1,18 +1,8 @@
namespace QWERTYkez.WordProcessor; namespace QWERTYkez.WordProcessor;
#if DEBUG
public static class Debugger
{
public static StringBuilder Builder { get; } = new();
}
#endif
/// <summary> /// <summary>
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений. /// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение /// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem"/>.
/// остаётся в текущем параграфе, а последующие создают новые.
/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах.
/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem.SplitValue"/>.
/// </summary> /// </summary>
internal static class MultiReplaceExt internal static class MultiReplaceExt
{ {
@@ -236,7 +226,7 @@ internal static class MultiReplaceExt
return newPara; 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); var seg = structure.Segments.FirstOrDefault(s => position >= s.Start && position < s.End);
if (seg is null) return; if (seg is null) return;
@@ -244,19 +234,19 @@ internal static class MultiReplaceExt
var textRun = new Run(); var textRun = new Run();
if (seg.Run.RunProperties is not null) if (seg.Run.RunProperties is not null)
textRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true); textRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
textRun.AppendChild(new Text(item.Text ?? string.Empty)); textRun.AppendChild(new Text(text));
para.AppendChild(textRun); para.AppendChild(textRun);
} }
/// <summary> private static void AddSectionProperties(
/// Добавляет SectionProperties к параграфу. Все значения (PageSize, PageMargin) берутся из документа. Paragraph para,
/// Для книжных секций (addPageSize=false) Orient не устанавливается (not set). BreakType splitValue,
/// </summary> bool addPageSize,
private static void AddSectionProperties(Paragraph para, PageBreakType splitValue, bool addPageSize, SectionProperties sourceSection, SectionProperties portraitSection) SectionProperties sourceSection,
SectionProperties portraitSection)
{ {
if (para is null) return; if (para is null) return;
// Удаляем существующие секции
if (para.ParagraphProperties is not null) if (para.ParagraphProperties is not null)
{ {
var sections = para.ParagraphProperties.Elements<SectionProperties>().ToList(); var sections = para.ParagraphProperties.Elements<SectionProperties>().ToList();
@@ -268,65 +258,85 @@ internal static class MultiReplaceExt
para.ParagraphProperties ??= new ParagraphProperties(); para.ParagraphProperties ??= new ParagraphProperties();
var sectionProps = new SectionProperties(); var sectionProps = new SectionProperties();
// Добавляем PageSize var pageSizeClone = CreatePageSizeClone(addPageSize, splitValue, sourceSection, portraitSection);
SectionProperties? sizeSource = addPageSize ? sourceSection : portraitSection; if (pageSizeClone is not null)
var sourcePageSize = sizeSource?.GetFirstChild<PageSize>();
if (sourcePageSize is null && portraitSection is not null)
sourcePageSize = portraitSection.GetFirstChild<PageSize>();
if (sourcePageSize is null && sourceSection is not null)
sourcePageSize = sourceSection.GetFirstChild<PageSize>();
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); sectionProps.AppendChild(pageSizeClone);
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<PageSize>()
?? portraitSection.GetFirstChild<PageSize>()
?? sourceSection.GetFirstChild<PageSize>();
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;
} }
// Копируем PageMargin return clone;
}
private static PageMargin? CreateMarginClone(
bool addPageSize,
BreakType splitValue,
SectionProperties sourceSection,
SectionProperties portraitSection)
{
PageMargin? marginToUse = null; PageMargin? marginToUse = null;
if (addPageSize) if (addPageSize)
{ {
var sourceMargin = sourceSection?.GetFirstChild<PageMargin>() var sourceMargin = sourceSection?.GetFirstChild<PageMargin>()
?? portraitSection?.GetFirstChild<PageMargin>(); ?? portraitSection?.GetFirstChild<PageMargin>();
if (sourceMargin is not null) if (sourceMargin is not null)
{ {
var marginClone = (PageMargin)sourceMargin.CloneNode(true); var clone = (PageMargin)sourceMargin.CloneNode(true);
var sourceOrient = sourceSection?.GetFirstChild<PageSize>()?.Orient; var sourceOrient = sourceSection?.GetFirstChild<PageSize>()?.Orient;
bool sourceIsLandscape = sourceOrient?.Value == PageOrientationValues.Landscape || bool sourceIsLandscape = sourceOrient?.Value == PageOrientationValues.Landscape ||
(sourceSection?.GetFirstChild<PageSize>()?.Width?.Value > sourceSection?.GetFirstChild<PageSize>()?.Height?.Value); (sourceSection?.GetFirstChild<PageSize>()?.Width?.Value > sourceSection?.GetFirstChild<PageSize>()?.Height?.Value);
if ((splitValue == PageBreakType.NewLandscapeSection && !sourceIsLandscape) ||
(splitValue == PageBreakType.NewPortraitSection && sourceIsLandscape)) if ((splitValue == BreakType.NewLandscapeSection && !sourceIsLandscape) ||
(splitValue == BreakType.NewPortraitSection && sourceIsLandscape))
{ {
marginClone.SwapBottomRight(); clone.SwapBottomRight();
} }
marginToUse = marginClone; marginToUse = clone;
} }
} }
else else
@@ -336,11 +346,54 @@ internal static class MultiReplaceExt
marginToUse = (PageMargin)portraitMargin.CloneNode(true); marginToUse = (PageMargin)portraitMargin.CloneNode(true);
} }
if (marginToUse is not null) return marginToUse;
sectionProps.AppendChild(marginToUse); }
sectionProps.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); private static void EnsureBodyLandscapeSection(
para.ParagraphProperties.AppendChild(sectionProps); Body body,
SectionProperties sourceSection,
bool sectionChangeInsideGroup,
BreakType? lastOrientation)
{
if (body is null || sourceSection is null) return;
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
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<SectionProperties>())
{
var ps = bodySec.GetFirstChild<PageSize>();
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<PageMargin>();
if (sourceMargin is not null)
{
var marginClone = (PageMargin)sourceMargin.CloneNode(true);
if (sourceSection.GetFirstChild<PageSize>()?.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) private static void MergeParagraph(Paragraph target, Paragraph source)
@@ -349,55 +402,7 @@ internal static class MultiReplaceExt
target.AppendChild(child.CloneNode(true)); target.AppendChild(child.CloneNode(true));
} }
/// <summary>Выводит структуру документа: для каждого параграфа показывает текст, наличие секции, ориентацию, размеры и поля.</summary> // ---------- ОСНОВНОЙ АЛГОРИТМ ----------
private static void LogDocumentStructure(Body body, string title)
{
#if DEBUG
Debugger.Builder.AppendLine($"=== {title} ===");
var paragraphs = body.Descendants<Paragraph>().ToList();
int index = 0;
foreach (var para in paragraphs)
{
var text = para.InnerText.Replace("\n", "\\n").Replace("\r", "\\r");
var section = para.ParagraphProperties?.GetFirstChild<SectionProperties>();
string sectionInfo = "None";
if (section is not null)
{
var pageSize = section.GetFirstChild<PageSize>();
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<PageMargin>();
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<SectionProperties>().ToList();
if (bodySections.Any())
{
Debugger.Builder.AppendLine(" Body SectionProperties:");
foreach (var sec in bodySections)
{
var ps = sec.GetFirstChild<PageSize>();
var pm = sec.GetFirstChild<PageMargin>();
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
}
/// <summary>Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации.</summary> /// <summary>Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации.</summary>
private static List<Paragraph>? ProcessMultiReplacements( private static List<Paragraph>? ProcessMultiReplacements(
@@ -408,6 +413,17 @@ internal static class MultiReplaceExt
{ {
var body = original.Ancestors<Body>().FirstOrDefault(); var body = original.Ancestors<Body>().FirstOrDefault();
// Определяем, есть ли пустые параграфы перед original
bool hasEmptyParagraphsBefore = false;
var prevPara = original.PreviousSibling<Paragraph>();
while (prevPara is not null)
{
if (!string.IsNullOrEmpty(prevPara.InnerText))
break;
hasEmptyParagraphsBefore = true;
prevPara = prevPara.PreviousSibling<Paragraph>();
}
// 1. Сбор определений // 1. Сбор определений
var definitions = new List<MatchDefinition>(); var definitions = new List<MatchDefinition>();
if (stringReplacements is not null) if (stringReplacements is not null)
@@ -415,7 +431,7 @@ internal static class MultiReplaceExt
foreach (var kvp in stringReplacements) foreach (var kvp in stringReplacements)
{ {
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue; if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
var items = kvp.Value.Select(v => new ReplaceItem(v, PageBreakType.None)); var items = kvp.Value.Select(v => new ReplaceItem(v));
definitions.Add(new MatchDefinition(kvp.Key, items)); definitions.Add(new MatchDefinition(kvp.Key, items));
} }
} }
@@ -463,7 +479,6 @@ internal static class MultiReplaceExt
} }
sourceSection ??= new SectionProperties(); sourceSection ??= new SectionProperties();
// Получаем книжную секцию для копирования полей (из документа)
SectionProperties? portraitSection = null; SectionProperties? portraitSection = null;
if (body is not null) if (body is not null)
{ {
@@ -476,13 +491,21 @@ internal static class MultiReplaceExt
Paragraph? currentPara = null; Paragraph? currentPara = null;
int currentPos = 0; int currentPos = 0;
bool sectionChangeInsideGroup = false; bool sectionChangeInsideGroup = false;
PageBreakType? lastOrientation = null; BreakType? lastOrientation = null;
// Состояние обработки группы
bool pageBreakBeforeNext = false;
BreakType? pendingOrientation = null;
bool firstSplitMarkerHandled = false;
int textCount = 0;
Paragraph? lastTextPara = null;
bool emptyParagraphCreated = false;
for (int i = 0; i < matches.Count; i++) for (int i = 0; i < matches.Count; i++)
{ {
var match = matches[i]; var match = matches[i];
// Текст перед совпадением (если есть) // Текст перед совпадением
if (currentPos < match.Start) if (currentPos < match.Start)
{ {
var beforePara = CloneParagraphWithoutSection(original); var beforePara = CloneParagraphWithoutSection(original);
@@ -509,126 +532,124 @@ internal static class MultiReplaceExt
{ {
resultParas.Add(beforePara); resultParas.Add(beforePara);
currentPara = beforePara; currentPara = beforePara;
lastTextPara = beforePara;
} }
} }
var values = match.Definition.Values.ToList(); var values = match.Definition.Values.ToList();
if (values.Count == 0) continue; if (values.Count == 0) continue;
for (int vIdx = 0; vIdx < values.Count; vIdx++) // Сбрасываем флаги для новой группы
pageBreakBeforeNext = false;
pendingOrientation = null;
firstSplitMarkerHandled = false;
textCount = 0;
lastTextPara = null;
emptyParagraphCreated = false;
foreach (var item in values)
{ {
var item = values[vIdx]; if (!string.IsNullOrEmpty(item.Text))
// Создаём новый параграф для каждого элемента
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
InsertFormattedRun(currentPara, item, structure, match.Start);
// Обработка смены ориентации
if (item.SplitValue == PageBreakType.NewLandscapeSection || item.SplitValue == PageBreakType.NewPortraitSection)
{ {
bool addPageSize = (vIdx != 0); ProcessTextItem(
PageBreakType orientation = item.SplitValue; item.Text,
if (vIdx != 0) ref pendingOrientation,
{ ref currentPara,
orientation = PageBreakType.NewLandscapeSection; ref lastTextPara,
} ref textCount,
AddSectionProperties(currentPara, orientation, addPageSize, sourceSection, portraitSection); ref pageBreakBeforeNext,
lastOrientation = item.SplitValue; ref sectionChangeInsideGroup,
sectionChangeInsideGroup = true; ref lastOrientation,
original,
structure,
match.Start,
sourceSection,
portraitSection,
resultParas);
} }
else if (item.SplitValue == PageBreakType.PageBreak) else if (item.BreakValue.HasValue)
{ {
var seg = structure.Segments.FirstOrDefault(s => match.Start >= s.Start && match.Start < s.End); ProcessBreakItem(
var breakRun = new Run(new Break { Type = BreakValues.Page }); item.BreakValue.Value,
if (seg is not null && seg.Run.RunProperties is not null) ref pendingOrientation,
breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true); ref currentPara,
currentPara.AppendChild(breakRun); ref lastTextPara,
ref pageBreakBeforeNext,
ref firstSplitMarkerHandled,
ref sectionChangeInsideGroup,
ref lastOrientation,
ref textCount,
ref emptyParagraphCreated,
hasEmptyParagraphsBefore,
original,
sourceSection,
portraitSection,
resultParas);
} }
} }
// Закрываем секцию, если была смена и последний параграф не имеет секции // Закрываем секцию, если была смена
if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null) if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null)
{ {
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null) if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{ {
bool addPageSize = (lastOrientation.Value == PageBreakType.NewLandscapeSection); bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection); AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection);
} }
} }
// Если исходная секция была landscape, внутри группы была смена, и последняя ориентация — книжная, // Если внутри группы не было смены, но есть sourceSection с PageSize,
// добавляем landscape секцию в Body, чтобы остаток остался landscape. // копируем её в последний параграф группы (если он не имеет секции)
if (body is not null) if (!sectionChangeInsideGroup && currentPara is not null)
{ {
var sourcePageSize = sourceSection.GetFirstChild<PageSize>(); var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null) if (sourcePageSize is not null)
{ {
bool sourceIsLandscape = sourcePageSize.Orient?.Value == PageOrientationValues.Landscape || if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
(sourcePageSize.Width?.Value > sourcePageSize.Height?.Value);
bool lastIsPortrait = lastOrientation.HasValue && lastOrientation.Value == PageBreakType.NewPortraitSection;
if (sourceIsLandscape && sectionChangeInsideGroup && lastIsPortrait)
{ {
// Проверяем, есть ли уже секция landscape на Body currentPara.ParagraphProperties ??= new ParagraphProperties();
bool hasBodyLandscape = false; currentPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true));
foreach (var bodySec in body.Elements<SectionProperties>())
{
var ps = bodySec.GetFirstChild<PageSize>();
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<PageMargin>();
if (sourceMargin is not null)
{
var marginClone = (PageMargin)sourceMargin.CloneNode(true);
// Если исходная секция не landscape, но мы делаем landscape, меняем поля
if (sourceSection.GetFirstChild<PageSize>()?.Orient?.Value != PageOrientationValues.Landscape)
{
marginClone.SwapBottomRight();
}
bodySection.AppendChild(marginClone);
}
bodySection.AppendChild(new SectionType { Val = SectionMarkValues.NextPage });
body.AppendChild(bodySection);
}
} }
} }
} }
EnsureBodyLandscapeSection(body!, sourceSection, sectionChangeInsideGroup, lastOrientation);
currentPos = match.End; currentPos = match.End;
} }
// 6. Обработка остатка текста (если есть) // 6. Обработка остатка текста
if (currentPos < fullText.Length) if (currentPos < fullText.Length)
{ {
var remainderPara = BuildRemainderParagraph(original, structure, currentPos); var remainderPara = BuildRemainderParagraph(original, structure, currentPos);
if (remainderPara is not null) if (remainderPara is not null)
{ {
// Если внутри группы была смена, применяем последнюю ориентацию к остатку // Если есть отложенная ориентация — применяем её
if (sectionChangeInsideGroup && lastOrientation.HasValue) if (pendingOrientation.HasValue)
{ {
var breakRun = new Run(new Break { Type = BreakValues.Page }); var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0); remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (lastOrientation.Value == PageBreakType.NewLandscapeSection); 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); AddSectionProperties(remainderPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection);
} }
else
{
// Если нет смены, но есть sourceSection с PageSize, копируем её в остаток
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
remainderPara.ParagraphProperties ??= new ParagraphProperties();
remainderPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true));
}
}
resultParas.Add(remainderPara); resultParas.Add(remainderPara);
} }
} }
@@ -641,12 +662,161 @@ internal static class MultiReplaceExt
resultParas.RemoveAt(i); resultParas.RemoveAt(i);
} }
// Логирование
#if DEBUG
if (body is not null)
LogDocumentStructure(body, "FINAL DOCUMENT STRUCTURE");
#endif
return resultParas.Count > 0 ? resultParas : null; return resultParas.Count > 0 ? resultParas : null;
} }
// ---------- ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ОБРАБОТКИ ГРУПП ----------
private static void ProcessTextItem(
string text,
ref BreakType? pendingOrientation,
ref Paragraph? currentPara,
ref Paragraph? lastTextPara,
ref int textCount,
ref bool pageBreakBeforeNext,
ref bool sectionChangeInsideGroup,
ref BreakType? lastOrientation,
Paragraph original,
ParagraphStructure structure,
int position,
SectionProperties sourceSection,
SectionProperties portraitSection,
List<Paragraph> resultParas)
{
if (pendingOrientation.HasValue)
{
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(currentPara, pendingOrientation.Value, addPageSize, sourceSection, portraitSection);
lastOrientation = pendingOrientation;
sectionChangeInsideGroup = true;
pendingOrientation = null;
textCount++;
lastTextPara = currentPara;
if (pageBreakBeforeNext)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
currentPara.InsertAt(breakRun, 0);
pageBreakBeforeNext = false;
}
InsertFormattedRun(currentPara, text, structure, position);
}
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, text, structure, position);
}
}
private static void ProcessBreakItem(
BreakType breakType,
ref BreakType? pendingOrientation,
ref Paragraph? currentPara,
ref Paragraph? lastTextPara,
ref bool pageBreakBeforeNext,
ref bool firstSplitMarkerHandled,
ref bool sectionChangeInsideGroup,
ref BreakType? lastOrientation,
ref int textCount,
ref bool emptyParagraphCreated,
bool hasEmptyParagraphsBefore,
Paragraph original,
SectionProperties sourceSection,
SectionProperties portraitSection,
List<Paragraph> resultParas)
{
if (breakType == BreakType.PageBreak)
{
if (currentPara is not null)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
var lastRun = currentPara.Descendants<Run>().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 && hasEmptyParagraphsBefore && !firstSplitMarkerHandled)
{
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
AddSectionProperties(currentPara, breakType, false, sourceSection, portraitSection);
lastOrientation = breakType;
sectionChangeInsideGroup = true;
firstSplitMarkerHandled = true;
emptyParagraphCreated = true;
pendingOrientation = null;
textCount = 0;
lastTextPara = null;
}
else if (currentPara is null && !hasEmptyParagraphsBefore && !firstSplitMarkerHandled)
{
pendingOrientation = breakType;
firstSplitMarkerHandled = true;
}
else if (currentPara is not null)
{
if (!firstSplitMarkerHandled)
{
if (lastTextPara is not null)
{
if (lastTextPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
lastTextPara.ParagraphProperties ??= new ParagraphProperties();
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
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;
}
}
}
}
} }
+22 -53
View File
@@ -1,70 +1,39 @@
namespace QWERTYkez.WordProcessor; namespace QWERTYkez.WordProcessor;
/// <summary> /// <summary> Определяет тип разрыва или смены ориентации страницы </summary>
/// Определяет тип разрыва или смены ориентации страницы, применяется к элементам следующим после замены public enum BreakType
/// </summary>
public enum PageBreakType
{ {
/// <summary>Без разрыва или смены ориентации.</summary>
None,
/// <summary>Обычный разрыв страницы (новый лист).</summary> /// <summary>Обычный разрыв страницы (новый лист).</summary>
PageBreak, PageBreak,
/// <summary>Начать новую секцию с альбомной ориентацией страницы.</summary> /// <summary>Начать новую секцию с альбомной ориентацией страницы.</summary>
NewLandscapeSection, NewLandscapeSection,
/// <summary>Начать новую секцию с книжной ориентацией страницы.</summary> /// <summary>Начать новую секцию с книжной ориентацией страницы.</summary>
NewPortraitSection, NewPortraitSection,
} }
/// <summary> public class ReplaceItem
/// Представляет элемент замены текста, содержащий сам текст и указание
/// на тип разрыва или смены ориентации, который должен быть применён
/// после вставки этого текста.
/// </summary>
/// <remarks>
/// Используется в методах множественной замены, например,
/// <see cref="IWordWriter.ReplaceItem(string, IEnumerable{ReplaceItem})"/>.
/// </remarks>
public readonly struct ReplaceItem
{ {
/// <summary> private ReplaceItem() { }
/// Инициализирует новый экземпляр <see cref="ReplaceItem"/> с пустым текстом public ReplaceItem(string text) => _Text = text;
/// и типом разрыва <see cref="PageBreakType.None"/>. public ReplaceItem(BreakType item) => _BreakValue = item;
/// </summary>
public ReplaceItem() { }
/// <summary>
/// Инициализирует новый экземпляр <see cref="ReplaceItem"/> с указанным текстом
/// и типом разрыва/смены ориентации.
/// </summary>
/// <param name="text">Текст, который будет вставлен вместо плейсхолдера.</param>
/// <param name="splitValue">
/// Тип разрыва или смены ориентации, который будет применён после вставки текста.
/// По умолчанию <see cref="PageBreakType.None"/>.
/// </param>
public ReplaceItem(string text, PageBreakType splitValue = PageBreakType.None)
{
Text = text;
SplitValue = splitValue;
}
/// <summary> public static implicit operator ReplaceItem(string text) => new(text);
/// Получает текст, который будет вставлен вместо плейсхолдера. public static implicit operator ReplaceItem(BreakType item) => item switch
/// </summary> {
public string Text { get; init; } = string.Empty; BreakType.PageBreak => PageBreak,
BreakType.NewLandscapeSection => NewLandscapeSection,
BreakType.NewPortraitSection => NewPortraitSection,
_ => throw new NotImplementedException()
};
/// <summary> 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 };
/// </summary>
public PageBreakType SplitValue { get; init; } = PageBreakType.None;
/// <summary> public string Text => _Text;
/// Определяет явное преобразование из строки в <see cref="ReplaceItem"/>. public string _Text = string.Empty;
/// </summary>
/// <param name="text">Строка текста.</param> public BreakType? BreakValue => _BreakValue;
/// <returns>Новый экземпляр <see cref="ReplaceItem"/> с указанным текстом и <see cref="PageBreakType.None"/>.</returns> public BreakType? _BreakValue;
public static explicit operator ReplaceItem(string text) => new() { Text = text };
} }
+12 -12
View File
@@ -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)) if (paragraph is null || string.IsNullOrEmpty(oldValue))
return false; 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) 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<KeyValuePair<string, string>> replacements, StringComparison comparisonType) internal static void Replace(this Paragraph paragraph, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
{ {
@@ -139,7 +139,7 @@ internal static class SimplyReplaceExt
OldValue = kvp.Key, OldValue = kvp.Key,
NewValue = kvp.Value ?? string.Empty, NewValue = kvp.Value ?? string.Empty,
Index = pos, Index = pos,
SplitValue = PageBreakType.None BreakValue = null
}); });
pos += kvp.Key.Length; pos += kvp.Key.Length;
} }
@@ -159,7 +159,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0) 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, OldValue = kvp.Key,
NewValue = kvp.Value.Text ?? string.Empty, NewValue = kvp.Value.Text ?? string.Empty,
Index = pos, Index = pos,
SplitValue = kvp.Value.SplitValue BreakValue = kvp.Value.BreakValue
}); });
pos += kvp.Key.Length; pos += kvp.Key.Length;
} }
@@ -213,7 +213,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0) 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 OldValue { get; set; } = null!;
internal string NewValue { get; set; } = null!; internal string NewValue { get; set; } = null!;
internal int Index { get; set; } internal int Index { get; set; }
internal PageBreakType SplitValue { get; set; } = PageBreakType.None; internal BreakType? BreakValue { get; set; } = null;
} }
private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable<Run> runs) private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable<Run> runs)
@@ -287,7 +287,7 @@ internal static class SimplyReplaceExt
int matchStart, int matchStart,
int matchEnd, int matchEnd,
string newValue, string newValue,
PageBreakType splitValue) BreakType? splitValue)
{ {
if (nodesToReplace.Count == 0) return; if (nodesToReplace.Count == 0) return;
@@ -312,7 +312,7 @@ internal static class SimplyReplaceExt
nodesToReplace[i].Text.Text = string.Empty; 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) if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para)
{ {
@@ -322,7 +322,7 @@ internal static class SimplyReplaceExt
para.AppendChild(breakRun); para.AppendChild(breakRun);
} }
} }
else if (splitValue == PageBreakType.NewLandscapeSection || splitValue == PageBreakType.NewPortraitSection) else if (splitValue == BreakType.NewLandscapeSection || splitValue == BreakType.NewPortraitSection)
{ {
var firstText = nodesToReplace[0].Text; var firstText = nodesToReplace[0].Text;
if (firstText.Parent is Run run && run.Parent is Paragraph para) 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; if (para is null) return;
PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection PageOrientationValues orientation = splitValue == BreakType.NewLandscapeSection
? PageOrientationValues.Landscape ? PageOrientationValues.Landscape
: PageOrientationValues.Portrait; : PageOrientationValues.Portrait;