2026-06-05 15:58:03 +07:00
namespace QWERTYkez.WordProcessor ;
/// <summary>
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение
/// остаётся в текущем параграфе, а последующие создают новые.
/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах.
2026-07-21 16:43:59 +07:00
/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem.SplitValue"/>.
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 с одним ключом
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения oldValue в теле документа на массив строк (каждая строка в отдельном параграфе).</summary>
2026-06-05 15:58:03 +07:00
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 ;
var dict = new Dictionary < string , IEnumerable < string >> { { oldValue , newValues } };
body . Replace ( dict , comparisonType );
}
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения oldValue в теле документа на массив ReplaceItem (каждый элемент в отдельном параграфе с учётом разрывов).</summary>
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 ;
var dict = new Dictionary < string , IEnumerable < ReplaceItem >> { { oldValue , newValues } };
body . Replace ( dict , comparisonType );
}
#endregion
#region Body . Replace со словарём массивов
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения из словаря (ключ -> массив строк) в теле документа.</summary>
2026-06-05 15:58:03 +07:00
internal static void Replace ( this Body body , IEnumerable < KeyValuePair < string , IEnumerable < string >>> 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 ;
var newParas = ProcessMultiReplacements ( p , replacements , null , comparisonType );
if ( newParas is not null && newParas . Count > 0 )
ParagraphReplacer . ReplaceParagraph ( p , newParas );
}
}
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения из словаря (ключ -> массив ReplaceItem) в теле документа.</summary>
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 ;
var newParas = ProcessMultiReplacements ( p , null , replacements , comparisonType );
if ( newParas is not null && newParas . Count > 0 )
ParagraphReplacer . ReplaceParagraph ( p , newParas );
}
}
#endregion
#region Paragraph . ReplaceWithMultiple ( один ключ )
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения oldValue в параграфе на массив строк (каждая строка в новом параграфе).</summary>
2026-06-05 15:58:03 +07:00
internal static bool ReplaceWithMultiple ( this Paragraph ? paragraph , string oldValue , IEnumerable < string > newValues , StringComparison comparisonType )
{
if ( paragraph is null || string . IsNullOrEmpty ( oldValue ) || newValues is null || newValues . Count () == 0 )
return false ;
var dict = new Dictionary < string , IEnumerable < string >> { { oldValue , newValues } };
var newParas = ProcessMultiReplacements ( paragraph , dict , null , comparisonType );
if ( newParas is null || newParas . Count == 0 ) return false ;
if ( paragraph . Parent is not null )
{
ParagraphReplacer . ReplaceParagraph ( paragraph , newParas );
return true ;
}
return false ;
}
2026-07-21 16:43:59 +07:00
/// <summary>Заменяет все вхождения oldValue в параграфе на массив ReplaceItem (каждый элемент в новом параграфе с учётом разрывов).</summary>
2026-06-05 15:58:03 +07:00
internal static bool ReplaceWithMultiple ( this Paragraph ? paragraph , string oldValue , IEnumerable < ReplaceItem > newValues , StringComparison comparisonType )
{
if ( paragraph is null || string . IsNullOrEmpty ( oldValue ) || newValues is null || newValues . Count () == 0 )
return false ;
var dict = new Dictionary < string , IEnumerable < ReplaceItem >> { { oldValue , newValues } };
var newParas = ProcessMultiReplacements ( paragraph , null , dict , comparisonType );
if ( newParas is null || newParas . Count == 0 ) return false ;
if ( paragraph . Parent is not null )
{
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 )
{
return ProcessMultiReplacements ( paragraph , replacements , null , comparisonType );
}
internal static List < Paragraph >? ProcessParagraphWithAllReplacements (
Paragraph paragraph ,
IEnumerable < KeyValuePair < string , IEnumerable < ReplaceItem >>> replacements ,
StringComparison comparisonType )
{
return ProcessMultiReplacements ( paragraph , null , replacements , comparisonType );
}
internal static List < Paragraph >? ProcessParagraphWithAllReplacements (
Paragraph paragraph ,
IEnumerable < KeyValuePair < string , string >> replacements ,
StringComparison comparisonType )
{
Dictionary < string , IEnumerable < string >> dict = replacements
. 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
{
public MatchDefinition Definition { get ; set ; } = null !;
public int Start { get ; set ; }
public int End { get ; set ; }
}
private class RunSegment ( Run run , string text , int start , int end )
{
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 )
{
var segments = new List < RunSegment >();
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-07-21 16:43:59 +07:00
/// <summary>Клонирует свойства параграфа, но не копирует SectionProperties.</summary>
2026-06-05 15:58:03 +07:00
private static Paragraph CloneParagraphProperties ( Paragraph original )
{
var newPara = new Paragraph ();
if ( original . ParagraphProperties is not null )
2026-07-21 16:43:59 +07:00
{
var props = new ParagraphProperties ();
foreach ( var child in original . ParagraphProperties . ChildElements )
{
if ( child is not SectionProperties )
props . AppendChild ( child . CloneNode ( true ));
}
newPara . ParagraphProperties = props ;
}
2026-06-05 15:58:03 +07:00
return newPara ;
}
2026-07-21 16:43:59 +07:00
/// <summary>Строит параграф из текстового диапазона [start, end) исходного параграфа.</summary>
2026-06-05 15:58:03 +07:00
private static Paragraph ? BuildRangeParagraph ( Paragraph original , ParagraphStructure structure , int start , int end )
{
if ( start >= end ) return null ;
var newPara = CloneParagraphProperties ( 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 ();
2026-07-21 16:43:59 +07:00
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 );
}
2026-06-05 15:58:03 +07:00
}
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 ;
}
2026-07-21 16:43:59 +07:00
/// <summary>Строит параграф из текста после позиции position, пропуская нетекстовые элементы до первого текстового сегмента.</summary>
2026-06-05 15:58:03 +07:00
private static Paragraph ? BuildAfterParagraph ( Paragraph original , ParagraphStructure structure , int position )
{
if ( position >= structure . FullText . Length ) return null ;
var newPara = CloneParagraphProperties ( original );
var firstTextSeg = structure . Segments . FirstOrDefault ( s => s . Start >= position );
bool passedFirstText = false ;
foreach ( var child in original . ChildElements )
{
if ( child is Run run )
{
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 ;
}
}
else
{
if ( passedFirstText )
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 ;
}
2026-07-21 16:43:59 +07:00
/// <summary>Вставляет Run с текстом из ReplaceItem, копируя форматирование из сегмента по позиции.</summary>
2026-06-05 15:58:03 +07:00
private static void InsertFormattedRun ( Paragraph para , ReplaceItem item , ParagraphStructure structure , int position )
{
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 );
textRun . AppendChild ( new Text ( item . Text ?? string . Empty ));
para . AppendChild ( textRun );
2026-07-21 16:43:59 +07:00
}
/// <summary>Добавляет SectionProperties для смены ориентации, включая явный разрыв раздела.</summary>
private static void AddSectionProperties ( Paragraph para , PageBreakType splitValue )
{
if ( para is null ) return ;
PageOrientationValues orientation = splitValue == PageBreakType . NewLandscapeSection
? PageOrientationValues . Landscape
: PageOrientationValues . Portrait ;
2026-06-05 15:58:03 +07:00
2026-07-21 16:43:59 +07:00
uint width , height ;
if ( orientation == PageOrientationValues . Landscape )
2026-06-05 15:58:03 +07:00
{
2026-07-21 16:43:59 +07:00
width = 16838 ; // A4 landscape
height = 11906 ;
2026-06-05 15:58:03 +07:00
}
2026-07-21 16:43:59 +07:00
else
{
width = 11906 ; // A4 portrait
height = 16838 ;
}
var sectionProps = new SectionProperties (
new PageSize
{
Width = width ,
Height = height ,
Orient = orientation
},
new SectionType { Val = SectionMarkValues . NextPage } // явный разрыв раздела
);
para . ParagraphProperties ??= new ParagraphProperties ();
para . ParagraphProperties . AppendChild ( sectionProps );
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 void MergeParagraph ( Paragraph target , Paragraph source )
{
foreach ( var child in source . ChildElements )
target . AppendChild ( child . CloneNode ( true ));
}
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-07-21 16:43:59 +07:00
// 1. Сбор определений
2026-06-05 15:58:03 +07:00
var definitions = new List < MatchDefinition >();
if ( stringReplacements is not null )
{
foreach ( var kvp in stringReplacements )
{
if ( string . IsNullOrEmpty ( kvp . Key ) || kvp . Value is null || kvp . Value . Count () == 0 ) continue ;
2026-07-21 16:43:59 +07:00
var items = kvp . Value . Select ( v => new ReplaceItem ( v , PageBreakType . None ));
definitions . Add ( new MatchDefinition ( kvp . Key , items ));
2026-06-05 15:58:03 +07:00
}
}
if ( itemReplacements is not null )
{
foreach ( var kvp in itemReplacements )
{
if ( string . IsNullOrEmpty ( kvp . Key ) || kvp . Value is null || kvp . Value . Count () == 0 ) continue ;
definitions . Add ( new MatchDefinition ( kvp . Key , kvp . Value ));
}
}
if ( definitions . Count == 0 ) return null ;
// 2. Анализ структуры параграфа
var runs = original . Descendants < Run >(). ToList ();
if ( runs . Count == 0 ) return null ;
var structure = AnalyzeParagraphStructure ( runs );
string fullText = structure . FullText ;
if ( fullText . Length == 0 ) return null ;
2026-07-21 16:43:59 +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 )
{
matches . Add ( new Match
{
Definition = def ,
Start = pos ,
End = pos + def . Key . Length
});
pos += def . Key . Length ;
}
}
if ( matches . Count == 0 ) return null ;
matches . Sort (( a , b ) => a . Start . CompareTo ( b . Start ));
2026-07-21 16:43:59 +07:00
// 4. Построение результата
2026-06-05 15:58:03 +07:00
var resultParas = new List < Paragraph >();
Paragraph ? currentPara = null ;
int currentPos = 0 ;
2026-07-21 16:43:59 +07:00
PageBreakType ? pendingOrientation = null ; // отложенная смена ориентации для следующего параграфа
2026-06-05 15:58:03 +07:00
for ( int i = 0 ; i < matches . Count ; i ++)
{
var match = matches [ i ];
2026-07-21 16:43:59 +07:00
// Текст перед совпадением
2026-06-05 15:58:03 +07:00
if ( currentPos < match . Start )
{
var textPart = BuildRangeParagraph ( original , structure , currentPos , match . Start );
if ( textPart is not null )
{
if ( currentPara is null )
{
currentPara = textPart ;
resultParas . Add ( currentPara );
}
else
{
MergeParagraph ( currentPara , textPart );
}
}
}
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-07-21 16:43:59 +07:00
for ( int vIdx = 0 ; vIdx < values . Count ; vIdx ++)
2026-06-05 15:58:03 +07:00
{
2026-07-21 16:43:59 +07:00
var item = values [ vIdx ];
2026-06-05 15:58:03 +07:00
2026-07-21 16:43:59 +07:00
bool createNew = false ;
if ( vIdx == 0 )
{
// Создаём новый параграф, если у первого элемента есть разрыв/смена ориентации
if ( currentPara is null || item . SplitValue != PageBreakType . None )
createNew = true ;
}
else
{
createNew = true ;
}
if ( createNew )
{
var newPara = CloneParagraphProperties ( original );
resultParas . Add ( newPara );
currentPara = newPara ;
// Если есть отложенная ориентация, применяем её к этому новому параграфу и сбрасываем
if ( pendingOrientation . HasValue )
{
AddSectionProperties ( currentPara , pendingOrientation . Value );
pendingOrientation = null ;
}
// Если это первый созданный параграф и у него есть разрыв/смена ориентации,
// явно задаём книжную ориентацию, чтобы избежать наследования альбомной.
if ( resultParas . Count == 1 && vIdx == 0 && item . SplitValue != PageBreakType . None )
{
if ( currentPara . ParagraphProperties ?. GetFirstChild < SectionProperties >() is null )
{
AddSectionProperties ( currentPara , PageBreakType . NewPortraitSection );
}
}
}
// Вставляем текст
InsertFormattedRun ( currentPara , item , structure , match . Start );
// Обработка разрывов страниц (обычный PageBreak)
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 );
}
// Смена ориентации – откладываем для следующего параграфа
else if ( item . SplitValue == PageBreakType . NewLandscapeSection ||
item . SplitValue == PageBreakType . NewPortraitSection )
{
pendingOrientation = item . SplitValue ;
}
}
2026-06-05 15:58:03 +07:00
currentPos = match . End ;
}
2026-07-21 16:43:59 +07:00
// Текст после последнего совпадения
2026-06-05 15:58:03 +07:00
if ( currentPos < fullText . Length )
{
var textPart = BuildAfterParagraph ( original , structure , currentPos );
if ( textPart is not null )
{
2026-07-21 16:43:59 +07:00
if ( pendingOrientation . HasValue )
2026-06-05 15:58:03 +07:00
{
2026-07-21 16:43:59 +07:00
// Создаём новый параграф для остатка и применяем ориентацию
var newPara = CloneParagraphProperties ( original );
MergeParagraph ( newPara , textPart );
AddSectionProperties ( newPara , pendingOrientation . Value );
resultParas . Add ( newPara );
pendingOrientation = null ;
2026-06-05 15:58:03 +07:00
}
else
{
2026-07-21 16:43:59 +07:00
if ( currentPara is null )
{
currentPara = textPart ;
resultParas . Add ( currentPara );
}
else
{
MergeParagraph ( currentPara , textPart );
}
2026-06-05 15:58:03 +07:00
}
}
}
2026-07-21 16:43:59 +07:00
// Если осталась отложенная ориентация (например, замена в конце документа, и не было создано новых параграфов),
// создаём новый параграф с этой ориентацией (пустой, чтобы начать новую секцию)
if ( pendingOrientation . HasValue )
{
var newPara = CloneParagraphProperties ( original );
AddSectionProperties ( newPara , pendingOrientation . Value );
resultParas . Add ( newPara );
pendingOrientation = null ;
}
// Очистка пустых параграфов
2026-06-05 15:58:03 +07:00
for ( int i = resultParas . Count - 1 ; i >= 0 ; i --)
{
if (! resultParas [ i ]. ChildElements . OfType < Run >(). Any () && resultParas [ i ]. ParagraphProperties is null )
resultParas . RemoveAt ( i );
}
return resultParas . Count > 0 ? resultParas : null ;
}
}