@@ -1,5 +1,12 @@
namespace QWERTYkez.WordProcessor ;
#if DEBUG
public static class Debugger
{
public static StringBuilder Builder { get ; } = new ( ) ;
}
#endif
/// <summary>
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение
@@ -183,7 +190,7 @@ internal static class MultiReplaceExt
return sb . ToString ( ) ;
}
/// <summary>Клонирует параграф, удаляя все SectionProperties.</summary>
/// <summary>Клонирует параграф, удаляя все SectionProperties и PageBreakBefore .</summary>
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 < Text > ( ) . 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 < Run > ( ) . Where ( r = > ! r . HasChildren ) . ToList ( ) )
run . Remove ( ) ;
if ( ! newPara . ChildElements . OfType < Run > ( ) . 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 < Text > ( ) . 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 < Text > ( ) . 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 < Run > ( ) . Where ( r = > ! r . HasChildren ) . ToList ( ) )
run . Remove ( ) ;
if ( ! newPara . ChildElements . OfType < Run > ( ) . 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 )
/// <summary>
/// Добавляет SectionProperties к параграфу. Все значения (PageSize, PageMargin) берутся из документа.
/// Для книжных секций (addPageSize=false) Orient не устанавливается (not set).
/// </summary>
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 < SectionProperties > ( ) . ToList ( ) ;
foreach ( var sec in sections ) sec . Remove ( ) ;
var pageBreaks = para . ParagraphProperties . Elements < PageBreakBefore > ( ) . 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 < 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 ) ;
}
// Копируем PageMargin
PageMargin ? marginToUse = null ;
if ( addPageSize )
{
var sourceMargin = sourceSection ? . GetFirstChild < PageMargin > ( )
? ? portraitSection ? . GetFirstChild < PageMargin > ( ) ;
if ( sourceMargin is not null )
{
var marginClone = ( PageMargin ) sourceMargin . CloneNode ( true ) ;
var sourceOrient = sourceSection ? . GetFirstChild < PageSize > ( ) ? . Orient ;
bool sourceIsLandscape = sourceOrient ? . Value = = PageOrientationValues . Landscape | |
( sourceSection ? . GetFirstChild < PageSize > ( ) ? . Width ? . Value > sourceSection ? . GetFirstChild < PageSize > ( ) ? . 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 < PageMargin > ( ) ;
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 < SectionProperties > ( ) . 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 ) ) ;
}
/// <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>
private static List < Paragraph > ? ProcessMultiReplacements (
Paragraph original ,
@@ -371,9 +406,9 @@ internal static class MultiReplaceExt
IEnumerable < KeyValuePair < string , IEnumerable < ReplaceItem > > > ? itemReplacements ,
StringComparison comparisonType )
{
// Логируем начало
Log ( $"=== ProcessMultiReplacements START ===" ) ;
Log ( $"Original text: '{original.InnerText}'" ) ;
var body = original . Ancestors < Body > ( ) . FirstOrDefault ( ) ;
// 1. Сбор определений
var definitions = new List < MatchDefinition > ( ) ;
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 < SectionProperties > ( ) ? . CloneNode ( true ) as SectionProperties ;
if ( followingSection is null )
// 4. Получаем исходную секцию для копирования (из original или из документа)
SectionProperties ? sourceSection = original . ParagraphProperties ? . GetFirstChild < SectionProperties > ( ) ? . CloneNode ( true ) as SectionProperties ;
if ( sourceSection is null & & body is not null )
{
var nextPara = original . NextSibling < Paragraph > ( ) ;
if ( nextPara is not null )
{
followingSection = nextPara . ParagraphProperties ? . GetFirstChild < SectionProperties > ( ) ? . CloneNode ( true ) as SectionProperties ;
if ( followingSection is not null )
{
var pageSize = followingSection . GetFirstChild < PageSize > ( ) ;
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 < SectionProperties > ( ) . 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 < SectionProperties > ( ) . FirstOrDefault ( ) ? . CloneNode ( true ) as SectionProperties ;
}
portraitSection ? ? = new SectionProperties ( ) ;
// 5. Построение результата
var resultParas = new List < Paragraph > ( ) ;
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 < Text > ( ) . ToList ( ) ) t . Remove ( ) ;
string textPart = seg . Text . Substring ( startOffset , endOffset - startOffset ) ;
runClone . AppendChild ( new Text ( textPart ) ) ;
beforePara . AppendChild ( runClone ) ;
}
}
if ( beforePara . ChildElements . OfType < Run > ( ) . 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 < SectionProperties > ( ) 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 < SectionProperties > ( ) 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 < PageSize > ( ) ;
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 < 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 ) ;
}
}
}
}
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 < SectionProperties > ( ) . 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 < Run > ( ) . Any ( ) & & resultParas [ i ] . ParagraphProperties is null )
var p = resultParas [ i ] ;
if ( ! p . ChildElements . OfType < Run > ( ) . 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 < SectionProperties > ( ) 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
}
}