2026-06-05 15:58:03 +07:00
namespace QWERTYkez.WordProcessor ;
/// <summary>
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
2026-08-10 13:58:11 +07:00
/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem"/>.
2026-06-05 15:58:03 +07:00
/// </summary>
internal static class MultiReplaceExt
{
2026-07-21 16:43:59 +07:00
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (для Body) ----------
2026-06-05 15:58:03 +07:00
#region Body . Replace с одним ключом
internal static void Replace ( this Body body , string oldValue , IEnumerable < string > newValues , StringComparison comparisonType )
{
if ( body is null || string . IsNullOrEmpty ( oldValue ) || newValues is null ) return ;
2026-08-10 13:58:11 +07:00
var dict = new Dictionary < string , IEnumerable < string >> { { oldValue , newValues } };
body . Replace ( dict , comparisonType );
2026-06-05 15:58:03 +07:00
}
internal static void Replace ( this Body body , string oldValue , IEnumerable < ReplaceItem > newValues , StringComparison comparisonType )
{
if ( body is null || string . IsNullOrEmpty ( oldValue ) || newValues is null ) return ;
2026-08-10 13:58:11 +07:00
var dict = new Dictionary < string , IEnumerable < ReplaceItem >> { { oldValue , newValues } };
body . Replace ( dict , comparisonType );
2026-06-05 15:58:03 +07:00
}
#endregion
#region Body . Replace со словарём массивов
internal static void Replace ( this Body body , IEnumerable < KeyValuePair < string , IEnumerable < string >>> replacements , StringComparison comparisonType )
{
if ( body is null || replacements is null ) return ;
2026-08-10 13:58:11 +07:00
var paragraphs = body . Elements < Paragraph >(). 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 );
}
2026-06-05 15:58:03 +07:00
}
internal static void Replace ( this Body body , IEnumerable < KeyValuePair < string , IEnumerable < ReplaceItem >>> replacements , StringComparison comparisonType )
{
if ( body is null || replacements is null ) return ;
var paragraphs = body . Elements < Paragraph >(). ToList ();
for ( int i = paragraphs . Count - 1 ; i >= 0 ; i --)
{
var p = paragraphs [ i ];
if ( p ?. Parent is null ) continue ;
2026-08-10 13:58:11 +07:00
var newParas = ProcessMultiReplacements ( p , null , replacements , comparisonType );
if ( newParas is not null && newParas . Count > 0 )
2026-06-05 15:58:03 +07:00
ParagraphReplacer . ReplaceParagraph ( p , newParas );
}
}
#endregion
#region Paragraph . ReplaceWithMultiple ( один ключ )
internal static bool ReplaceWithMultiple ( this Paragraph ? paragraph , string oldValue , IEnumerable < string > newValues , StringComparison comparisonType )
{
2026-08-10 13:58:11 +07:00
if ( paragraph is null || string . IsNullOrEmpty ( oldValue ) || newValues is null || newValues . Count () == 0 )
2026-06-05 15:58:03 +07:00
return false ;
var dict = new Dictionary < string , IEnumerable < string >> { { oldValue , newValues } };
var newParas = ProcessMultiReplacements ( paragraph , dict , null , comparisonType );
2026-08-10 13:58:11 +07:00
if ( newParas is null || newParas . Count == 0 ) return false ;
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
if ( paragraph . Parent is not null )
2026-06-05 15:58:03 +07:00
{
ParagraphReplacer . ReplaceParagraph ( paragraph , newParas );
return true ;
}
return false ;
}
internal static bool ReplaceWithMultiple ( this Paragraph ? paragraph , string oldValue , IEnumerable < ReplaceItem > newValues , StringComparison comparisonType )
{
2026-08-10 13:58:11 +07:00
if ( paragraph is null || string . IsNullOrEmpty ( oldValue ) || newValues is null || newValues . Count () == 0 )
2026-06-05 15:58:03 +07:00
return false ;
var dict = new Dictionary < string , IEnumerable < ReplaceItem >> { { oldValue , newValues } };
var newParas = ProcessMultiReplacements ( paragraph , null , dict , comparisonType );
2026-08-10 13:58:11 +07:00
if ( newParas is null || newParas . Count == 0 ) return false ;
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
if ( paragraph . Parent is not null )
2026-06-05 15:58:03 +07:00
{
ParagraphReplacer . ReplaceParagraph ( paragraph , newParas );
return true ;
}
return false ;
}
#endregion
#region ProcessParagraphWithAllReplacements ( для обратной совместимости )
internal static List < Paragraph >? ProcessParagraphWithAllReplacements (
Paragraph paragraph ,
IEnumerable < KeyValuePair < string , IEnumerable < string >>> replacements ,
StringComparison comparisonType )
2026-08-10 13:58:11 +07:00
{
return ProcessMultiReplacements ( paragraph , replacements , null , comparisonType );
}
2026-06-05 15:58:03 +07:00
internal static List < Paragraph >? ProcessParagraphWithAllReplacements (
Paragraph paragraph ,
IEnumerable < KeyValuePair < string , IEnumerable < ReplaceItem >>> replacements ,
StringComparison comparisonType )
2026-08-10 13:58:11 +07:00
{
return ProcessMultiReplacements ( paragraph , null , replacements , comparisonType );
}
2026-06-05 15:58:03 +07:00
internal static List < Paragraph >? ProcessParagraphWithAllReplacements (
Paragraph paragraph ,
IEnumerable < KeyValuePair < string , string >> replacements ,
StringComparison comparisonType )
{
2026-08-10 13:58:11 +07:00
Dictionary < string , IEnumerable < string >> dict = replacements
2026-06-05 15:58:03 +07:00
. Where ( kvp => ! string . IsNullOrEmpty ( kvp . Key ))
. ToDictionary ( kvp => kvp . Key , kvp => ( IEnumerable < string >)[ kvp . Value ]);
return ProcessMultiReplacements ( paragraph , dict , null , comparisonType );
}
#endregion
// ---------- ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ ----------
private class MatchDefinition ( string key , IEnumerable < ReplaceItem > values )
{
public string Key { get ; } = key ;
public IEnumerable < ReplaceItem > Values { get ; } = values ;
}
private class Match
{
2026-08-10 13:58:11 +07:00
public MatchDefinition Definition { get ; set ; } = null !;
public int Start { get ; set ; }
public int End { get ; set ; }
2026-06-05 15:58:03 +07:00
}
private class RunSegment ( Run run , string text , int start , int end )
{
public Run Run { get ; } = run ;
public string Text { get ; } = text ;
public int Start { get ; } = start ;
public int End { get ; } = end ;
}
2026-07-21 16:43:59 +07:00
private class ParagraphStructure ( string fullText , List < RunSegment > segments )
2026-06-05 15:58:03 +07:00
{
public string FullText { get ; } = fullText ;
public List < RunSegment > Segments { get ; } = segments ;
}
private static ParagraphStructure AnalyzeParagraphStructure ( List < Run > runs )
{
2026-08-10 13:58:11 +07:00
var segments = new List < RunSegment >();
2026-06-05 15:58:03 +07:00
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 ;
}
return new ParagraphStructure ( sb . ToString (), segments );
}
private static string GetRunText ( Run run )
{
var sb = new StringBuilder ();
foreach ( var text in run . Elements < Text >())
sb . Append ( text . Text );
return sb . ToString ();
}
2026-08-03 16:11:54 +07:00
/// <summary>Клонирует параграф, удаляя все SectionProperties и PageBreakBefore.</summary>
2026-07-23 16:42:26 +07:00
private static Paragraph CloneParagraphWithoutSection ( Paragraph original )
2026-06-05 15:58:03 +07:00
{
var newPara = new Paragraph ();
2026-08-10 13:58:11 +07:00
if ( original . ParagraphProperties is not null )
2026-07-21 16:43:59 +07:00
{
2026-08-10 13:58:11 +07:00
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 ));
}
2026-08-03 16:31:54 +07:00
newPara . ParagraphProperties = newProps ;
2026-08-10 13:58:11 +07:00
}
2026-06-05 15:58:03 +07:00
return newPara ;
}
2026-08-03 16:11:54 +07:00
private static Paragraph ? BuildRemainderParagraph ( Paragraph original , ParagraphStructure structure , int position )
2026-06-05 15:58:03 +07:00
{
if ( position >= structure . FullText . Length ) return null ;
2026-07-23 16:42:26 +07:00
var newPara = CloneParagraphWithoutSection ( original );
2026-08-03 16:11:54 +07:00
foreach ( var seg in structure . Segments )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
if ( seg . End <= position ) continue ;
if ( seg . Start >= position )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
newPara . AppendChild ( seg . Run . CloneNode ( true ));
2026-06-05 15:58:03 +07:00
}
2026-08-10 13:58:11 +07:00
else if ( seg . End > position )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
var runClone = ( Run ) seg . Run . CloneNode ( true );
2026-08-10 13:58:11 +07:00
foreach ( var t in runClone . Elements < Text >(). ToList ()) t . Remove ();
2026-08-03 16:11:54 +07:00
int offset = position - seg . Start ;
2026-08-10 13:58:11 +07:00
string newText = seg . Text . Substring ( offset );
runClone . AppendChild ( new Text ( newText ));
2026-08-03 16:11:54 +07:00
newPara . AppendChild ( runClone );
2026-06-05 15:58:03 +07:00
}
}
2026-08-10 13:58:11 +07:00
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 ;
2026-06-05 15:58:03 +07:00
}
2026-08-10 13:58:11 +07:00
private static void InsertFormattedRun ( Paragraph para , string text , ParagraphStructure structure , int position )
2026-06-05 15:58:03 +07:00
{
var seg = structure . Segments . FirstOrDefault ( s => position >= s . Start && position < s . End );
if ( seg is null ) return ;
var textRun = new Run ();
if ( seg . Run . RunProperties is not null )
textRun . RunProperties = ( RunProperties ) seg . Run . RunProperties . CloneNode ( true );
2026-08-10 13:58:11 +07:00
textRun . AppendChild ( new Text ( text ));
2026-06-05 15:58:03 +07:00
para . AppendChild ( textRun );
2026-07-21 16:43:59 +07:00
}
2026-08-03 16:11:54 +07:00
/// <summary>
/// Добавляет SectionProperties к параграфу. Все значения (PageSize, PageMargin) берутся из документа.
2026-08-10 13:58:11 +07:00
/// Для книжных секций (addPageSize=false) Orient не устанавливается (not set).
2026-08-03 16:11:54 +07:00
/// </summary>
2026-08-10 13:58:11 +07:00
private static void AddSectionProperties (
Paragraph para ,
BreakType splitValue ,
bool addPageSize ,
SectionProperties sourceSection ,
SectionProperties portraitSection )
2026-07-21 16:43:59 +07:00
{
if ( para is null ) return ;
2026-06-05 15:58:03 +07:00
2026-08-03 16:11:54 +07:00
if ( para . ParagraphProperties is not null )
2026-06-05 15:58:03 +07:00
{
2026-08-10 13:58:11 +07:00
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 ();
2026-06-05 15:58:03 +07:00
}
2026-08-03 16:11:54 +07:00
para . ParagraphProperties ??= new ParagraphProperties ();
var sectionProps = new SectionProperties ();
2026-08-10 13:58:11 +07:00
var pageSizeClone = CreatePageSizeClone ( addPageSize , splitValue , sourceSection , portraitSection );
if ( pageSizeClone is not null )
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 ;
2026-08-03 16:31:54 +07:00
var sourcePageSize = sizeSource ?. GetFirstChild < PageSize >()
2026-08-10 13:58:11 +07:00
?? portraitSection . GetFirstChild < PageSize >()
?? sourceSection . GetFirstChild < PageSize >();
2026-08-03 16:11:54 +07:00
2026-08-10 13:58:11 +07:00
if ( sourcePageSize is null )
return null ;
2026-08-03 16:11:54 +07:00
2026-08-10 13:58:11 +07:00
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 ;
2026-07-21 16:43:59 +07:00
}
2026-08-10 13:58:11 +07:00
else if (! targetIsLandscape && sourceIsLandscape )
{
clone . SwapValues ();
clone . Orient = addPageSize ? PageOrientationValues . Portrait : null ;
}
else
{
clone . Orient = targetIsLandscape ? PageOrientationValues . Landscape :
addPageSize ? PageOrientationValues . Portrait : null ;
}
return clone ;
}
2026-07-21 16:43:59 +07:00
2026-08-10 13:58:11 +07:00
private static PageMargin ? CreateMarginClone (
bool addPageSize ,
BreakType splitValue ,
SectionProperties sourceSection ,
SectionProperties portraitSection )
{
2026-08-03 16:11:54 +07:00
PageMargin ? marginToUse = null ;
2026-08-10 13:58:11 +07:00
2026-08-03 16:11:54 +07:00
if ( addPageSize )
{
2026-08-10 13:58:11 +07:00
var sourceMargin = sourceSection ?. GetFirstChild < PageMargin >()
?? portraitSection ?. GetFirstChild < PageMargin >();
2026-08-03 16:11:54 +07:00
if ( sourceMargin is not null )
2026-07-21 16:43:59 +07:00
{
2026-08-10 13:58:11 +07:00
var clone = ( 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 );
2026-08-03 16:31:54 +07:00
2026-08-10 13:58:11 +07:00
if (( splitValue == BreakType . NewLandscapeSection && ! sourceIsLandscape ) ||
( splitValue == BreakType . NewPortraitSection && sourceIsLandscape ))
2026-08-03 16:11:54 +07:00
{
2026-08-10 13:58:11 +07:00
clone . SwapBottomRight ();
2026-08-03 16:11:54 +07:00
}
2026-08-10 13:58:11 +07:00
marginToUse = clone ;
2026-08-03 16:11:54 +07:00
}
}
2026-08-10 13:58:11 +07:00
else
2026-08-03 16:11:54 +07:00
{
2026-08-10 13:58:11 +07:00
var portraitMargin = portraitSection ?. GetFirstChild < PageMargin >();
if ( portraitMargin is not null )
marginToUse = ( PageMargin ) portraitMargin . CloneNode ( true );
2026-08-03 16:11:54 +07:00
}
2026-07-21 16:43:59 +07:00
2026-08-10 13:58:11 +07:00
return marginToUse ;
}
2026-07-23 16:42:26 +07:00
2026-08-10 13:58:11 +07:00
private static void EnsureBodyLandscapeSection (
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 )
{
foreach ( var child in source . ChildElements )
target . AppendChild ( child . CloneNode ( true ));
2026-06-05 15:58:03 +07:00
}
2026-07-21 16:43:59 +07:00
/// <summary>Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации.</summary>
2026-06-05 15:58:03 +07:00
private static List < Paragraph >? ProcessMultiReplacements (
Paragraph original ,
IEnumerable < KeyValuePair < string , IEnumerable < string >>>? stringReplacements ,
IEnumerable < KeyValuePair < string , IEnumerable < ReplaceItem >>>? itemReplacements ,
StringComparison comparisonType )
{
2026-08-10 13:58:11 +07:00
var body = original . Ancestors < Body >(). FirstOrDefault ();
2026-08-03 16:31:54 +07:00
2026-08-10 14:59:18 +07:00
// Определяем, есть ли пустые параграфы перед 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 >();
}
2026-08-10 13:58:11 +07:00
// 1. Сбор определений
var definitions = new List < MatchDefinition >();
2026-06-05 15:58:03 +07:00
if ( stringReplacements is not null )
{
2026-08-10 13:58:11 +07:00
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 ));
}
2026-06-05 15:58:03 +07:00
}
if ( itemReplacements is not null )
{
2026-08-10 13:58:11 +07:00
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 ));
}
2026-06-05 15:58:03 +07:00
}
if ( definitions . Count == 0 ) return null ;
2026-08-10 13:58:11 +07:00
// 2. Анализ структуры параграфа
var runs = original . Descendants < Run >(). ToList ();
2026-06-05 15:58:03 +07:00
if ( runs . Count == 0 ) return null ;
var structure = AnalyzeParagraphStructure ( runs );
string fullText = structure . FullText ;
if ( fullText . Length == 0 ) return null ;
2026-08-10 13:58:11 +07:00
// 3. Поиск всех вхождений
2026-06-05 15:58:03 +07:00
var matches = new List < Match >();
foreach ( var def in definitions )
{
int pos = 0 ;
while (( pos = fullText . IndexOf ( def . Key , pos , comparisonType )) != - 1 )
{
2026-08-10 13:58:11 +07:00
matches . Add ( new Match
{
Definition = def ,
Start = pos ,
End = pos + def . Key . Length
});
2026-06-05 15:58:03 +07:00
pos += def . Key . Length ;
}
}
if ( matches . Count == 0 ) return null ;
2026-08-03 16:11:54 +07:00
matches . Sort (( a , b ) => a . Start . CompareTo ( b . Start ));
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
// 4. Получаем исходную секцию для копирования (из original или из документа)
SectionProperties ? sourceSection = original . ParagraphProperties ?. GetFirstChild < SectionProperties >()?. CloneNode ( true ) as SectionProperties ;
if ( sourceSection is null && body is not null )
{
sourceSection = body . Elements < SectionProperties >(). LastOrDefault ()?. CloneNode ( true ) as SectionProperties ;
}
sourceSection ??= new SectionProperties ();
2026-07-23 16:42:26 +07:00
2026-08-10 13:58:11 +07:00
SectionProperties ? portraitSection = null ;
if ( body is not null )
{
portraitSection = body . Elements < SectionProperties >(). FirstOrDefault ()?. CloneNode ( true ) as SectionProperties ;
}
portraitSection ??= new SectionProperties ();
2026-07-23 16:42:26 +07:00
2026-08-10 13:58:11 +07:00
// 5. Построение результата
2026-06-05 15:58:03 +07:00
var resultParas = new List < Paragraph >();
Paragraph ? currentPara = null ;
int currentPos = 0 ;
2026-07-23 16:42:26 +07:00
bool sectionChangeInsideGroup = false ;
2026-08-10 13:58:11 +07:00
BreakType ? lastOrientation = null ;
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
// Состояние обработки группы
bool pageBreakBeforeNext = false ;
BreakType ? pendingOrientation = null ;
bool firstSplitMarkerHandled = false ;
int textCount = 0 ;
Paragraph ? lastTextPara = null ;
2026-08-10 14:59:18 +07:00
bool emptyParagraphCreated = false ; // был ли создан пустой параграф для not set
2026-08-10 13:58:11 +07:00
for ( int i = 0 ; i < matches . Count ; i ++)
2026-06-05 15:58:03 +07:00
{
2026-08-10 13:58:11 +07:00
var match = matches [ i ];
// Текст перед совпадением
2026-06-05 15:58:03 +07:00
if ( currentPos < match . Start )
{
2026-08-03 16:11:54 +07:00
var beforePara = CloneParagraphWithoutSection ( original );
foreach ( var seg in structure . Segments )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
if ( seg . End <= currentPos ) continue ;
if ( seg . Start >= match . Start ) break ;
if ( seg . Start >= currentPos && seg . End <= match . Start )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
beforePara . AppendChild ( seg . Run . CloneNode ( true ));
2026-06-05 15:58:03 +07:00
}
2026-08-03 16:11:54 +07:00
else if ( seg . Start < match . Start && seg . End > currentPos )
2026-06-05 15:58:03 +07:00
{
2026-08-03 16:11:54 +07:00
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 );
2026-08-10 13:58:11 +07:00
foreach ( var t in runClone . Elements < Text >(). ToList ()) t . Remove ();
string textPart = seg . Text . Substring ( startOffset , endOffset - startOffset );
runClone . AppendChild ( new Text ( textPart ));
2026-08-03 16:11:54 +07:00
beforePara . AppendChild ( runClone );
2026-06-05 15:58:03 +07:00
}
}
2026-08-10 13:58:11 +07:00
if ( beforePara . ChildElements . OfType < Run >(). Any ())
2026-08-03 16:11:54 +07:00
{
resultParas . Add ( beforePara );
currentPara = beforePara ;
2026-08-10 13:58:11 +07:00
lastTextPara = beforePara ;
2026-08-03 16:11:54 +07:00
}
2026-06-05 15:58:03 +07:00
}
2026-07-21 16:43:59 +07:00
var values = match . Definition . Values . ToList ();
if ( values . Count == 0 ) continue ;
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
// Сбрасываем флаги для новой группы
pageBreakBeforeNext = false ;
pendingOrientation = null ;
firstSplitMarkerHandled = false ;
textCount = 0 ;
lastTextPara = null ;
2026-08-10 14:59:18 +07:00
emptyParagraphCreated = false ;
2026-07-23 16:42:26 +07:00
2026-08-10 13:58:11 +07:00
foreach ( var item in values )
{
if (! string . IsNullOrEmpty ( item . Text ))
2026-07-21 16:43:59 +07:00
{
2026-08-10 14:59:18 +07:00
// Если есть отложенная ориентация — применяем её
2026-08-10 13:58:11 +07:00
if ( pendingOrientation . HasValue )
{
var newPara = CloneParagraphWithoutSection ( original );
resultParas . Add ( newPara );
currentPara = newPara ;
2026-08-10 14:59:18 +07:00
// Определяем addPageSize:
// - если был создан пустой параграф для not set (emptyParagraphCreated == true), то первый текст получает addPageSize = true для landscape, false для portrait
// - иначе addPageSize = (pendingOrientation == BreakType.NewLandscapeSection)
bool addPageSize ;
if ( emptyParagraphCreated )
{
addPageSize = ( pendingOrientation . Value == BreakType . NewLandscapeSection );
}
else
{
addPageSize = ( pendingOrientation . Value == BreakType . NewLandscapeSection );
}
2026-08-10 13:58:11 +07:00
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 ;
}
2026-06-05 15:58:03 +07:00
2026-08-10 13:58:11 +07:00
InsertFormattedRun ( currentPara , item . Text , structure , match . Start );
}
else
{
var newPara = CloneParagraphWithoutSection ( original );
resultParas . Add ( newPara );
currentPara = newPara ;
textCount ++;
lastTextPara = currentPara ;
2026-08-03 16:11:54 +07:00
2026-08-10 13:58:11 +07:00
if ( pageBreakBeforeNext )
{
var breakRun = new Run ( new Break { Type = BreakValues . Page });
currentPara . InsertAt ( breakRun , 0 );
pageBreakBeforeNext = false ;
}
2026-08-03 16:31:54 +07:00
2026-08-10 13:58:11 +07:00
InsertFormattedRun ( currentPara , item . Text , structure , match . Start );
}
}
else if ( item . BreakValue . HasValue )
2026-06-05 15:58:03 +07:00
{
2026-08-10 13:58:11 +07:00
var breakType = item . BreakValue . Value ;
if ( breakType == BreakType . PageBreak )
2026-07-21 16:43:59 +07:00
{
2026-08-10 13:58:11 +07:00
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 ;
}
2026-08-03 16:31:54 +07:00
}
2026-08-10 13:58:11 +07:00
else if ( breakType == BreakType . NewLandscapeSection || breakType == BreakType . NewPortraitSection )
2026-08-03 16:31:54 +07:00
{
2026-08-10 14:59:18 +07:00
// Если это первый маркер в группе и перед original есть пустые параграфы,
// создаём пустой параграф с not set
if ( currentPara is null && hasEmptyParagraphsBefore && ! firstSplitMarkerHandled )
2026-08-10 13:58:11 +07:00
{
2026-08-10 14:59:18 +07:00
var newPara = CloneParagraphWithoutSection ( original );
resultParas . Add ( newPara );
currentPara = newPara ;
// Добавляем секцию not set (без PageSize)
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 )
{
// Нет пустых параграфов, нет текущего параграфа — запоминаем ориентацию для первого текста
2026-08-10 14:48:49 +07:00
pendingOrientation = breakType ;
2026-08-10 14:59:18 +07:00
firstSplitMarkerHandled = true ;
// emptyParagraphCreated остаётся false
2026-08-10 13:58:11 +07:00
}
2026-08-10 14:59:18 +07:00
else if ( currentPara is not null )
2026-07-23 16:42:26 +07:00
{
2026-08-10 13:58:11 +07:00
if (! firstSplitMarkerHandled )
{
2026-08-10 14:59:18 +07:00
// Это первый маркер смены в группе (после текста)
2026-08-10 13:58:11 +07:00
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 );
}
2026-08-10 14:59:18 +07:00
lastOrientation = breakType ;
sectionChangeInsideGroup = true ;
2026-08-10 13:58:11 +07:00
}
firstSplitMarkerHandled = true ;
pendingOrientation = null ;
}
else
{
AddSectionProperties ( currentPara , breakType , false , sourceSection , portraitSection );
lastOrientation = breakType ;
sectionChangeInsideGroup = true ;
firstSplitMarkerHandled = true ;
pendingOrientation = null ;
}
}
else
{
2026-08-10 14:48:49 +07:00
// Это не первый маркер — устанавливаем отложенную ориентацию для следующего текста
2026-08-10 13:58:11 +07:00
pendingOrientation = breakType ;
}
2026-07-23 16:42:26 +07:00
}
2026-07-21 16:43:59 +07:00
}
2026-06-05 15:58:03 +07:00
}
}
2026-08-10 13:58:11 +07:00
// Закрываем секцию, если была смена
if ( sectionChangeInsideGroup && lastOrientation . HasValue && currentPara is not null )
{
if ( currentPara . ParagraphProperties ?. GetFirstChild < SectionProperties >() is null )
{
bool addPageSize = ( lastOrientation . Value == BreakType . NewLandscapeSection );
AddSectionProperties ( currentPara , lastOrientation . Value , addPageSize , sourceSection , portraitSection );
}
}
EnsureBodyLandscapeSection ( body !, sourceSection , sectionChangeInsideGroup , lastOrientation );
2026-08-03 16:11:54 +07:00
currentPos = match . End ;
2026-07-23 16:42:26 +07:00
}
2026-08-10 13:58:11 +07:00
// 6. Обработка остатка текста
if ( currentPos < fullText . Length )
2026-07-23 16:42:26 +07:00
{
2026-08-10 13:58:11 +07:00
var remainderPara = BuildRemainderParagraph ( original , structure , currentPos );
if ( remainderPara is not null )
2026-08-03 16:11:54 +07:00
{
2026-08-10 13:58:11 +07:00
if ( pendingOrientation . HasValue )
{
var breakRun = new Run ( new Break { Type = BreakValues . Page });
remainderPara . InsertAt ( breakRun , 0 );
2026-08-10 14:59:18 +07:00
bool addPageSize = ( pendingOrientation . Value == BreakType . NewLandscapeSection );
2026-08-10 13:58:11 +07:00
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 );
2026-08-03 16:11:54 +07:00
}
2026-07-23 16:42:26 +07:00
}
2026-07-21 16:43:59 +07:00
2026-08-10 13:58:11 +07:00
// 7. Очистка пустых параграфов
for ( int i = resultParas . Count - 1 ; i >= 0 ; i --)
{
var p = resultParas [ i ];
if (! p . ChildElements . OfType < Run >(). Any () && p . ParagraphProperties is null )
resultParas . RemoveAt ( i );
}
2026-08-03 16:11:54 +07:00
return resultParas . Count > 0 ? resultParas : null ;
2026-07-23 16:42:26 +07:00
}
2026-06-05 15:58:03 +07:00
}