множественные замены с параметром

This commit is contained in:
melekhin
2026-09-10 15:53:51 +07:00
parent eb3055a2e8
commit 70a19ad738
3 changed files with 621 additions and 25 deletions
+349 -1
View File
@@ -402,7 +402,7 @@ internal static class MultiReplaceExt
// ---------- ОСНОВНОЙ АЛГОРИТМ ----------
private static List<Paragraph>? ProcessMultiReplacements(
internal static List<Paragraph>? ProcessMultiReplacements(
Paragraph original,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
@@ -848,4 +848,352 @@ internal static class MultiReplaceExt
}
}
}
// ============================================================
// НОВЫЕ ПУБЛИЧНЫЕ ТОЧКИ ВХОДА (mergeBoundaryText: true)
// ============================================================
internal static List<Paragraph>? ProcessParagraphWithMergedBoundaries(
Paragraph paragraph,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements,
StringComparison comparisonType)
{
return ProcessMultiReplacements(paragraph, replacements, null, comparisonType, mergeBoundaryText: true);
}
internal static List<Paragraph>? ProcessParagraphWithMergedBoundaries(
Paragraph paragraph,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements,
StringComparison comparisonType)
{
return ProcessMultiReplacements(paragraph, null, replacements, comparisonType, mergeBoundaryText: true);
}
// ============================================================
// ОСНОВНОЙ АЛГОРИТМ (изменена только сигнатура + 4 места)
// ============================================================
private static List<Paragraph>? ProcessMultiReplacements(
Paragraph original,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
StringComparison comparisonType,
bool mergeBoundaryText = false) // <-- новый параметр
{
var body = original.Ancestors<Body>().FirstOrDefault();
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>();
}
SectionProperties? followingSection = null;
if (original.ParagraphProperties?.GetFirstChild<SectionProperties>()?.CloneNode(true) is not SectionProperties sourceSection)
{
var nextPara = original.NextSibling<Paragraph>();
if (nextPara is not null)
{
followingSection = nextPara.ParagraphProperties?.GetFirstChild<SectionProperties>()?.CloneNode(true) as SectionProperties;
}
}
else
{
followingSection = sourceSection;
}
followingSection ??= new SectionProperties();
// 1. Сбор определений
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;
var items = kvp.Value.Select(v => new ReplaceItem(v));
definitions.Add(new MatchDefinition(kvp.Key, items));
}
}
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;
// 3. Поиск всех вхождений
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));
// 4. Книжная секция для копирования полей
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;
bool sectionChangeInsideGroup = false;
BreakType? lastOrientation = null;
bool pageBreakBeforeNext = false;
BreakType? pendingOrientation = null;
bool firstSplitMarkerHandled = false;
int textCount = 0;
Paragraph? lastTextPara = null;
bool emptyParagraphCreated = false;
// НОВОЕ: буфер runs для склейки "before"-текста с первым value-параграфом
List<Run>? pendingBeforeRuns = null;
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
// ---- Текст перед совпадением ----
if (currentPos < match.Start)
{
var beforePara = CloneParagraphWithoutSection(original);
foreach (var seg in structure.Segments)
{
if (seg.End <= currentPos) continue;
if (seg.Start >= match.Start) break;
if (seg.Start >= currentPos && seg.End <= match.Start)
{
beforePara.AppendChild(seg.Run.CloneNode(true));
}
else if (seg.Start < match.Start && seg.End > currentPos)
{
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())
{
if (!mergeBoundaryText)
{
resultParas.Add(beforePara);
currentPara = beforePara;
lastTextPara = beforePara;
}
else
{
// Копим runs, склеим их с первым value-параграфом
pendingBeforeRuns = new List<Run>();
foreach (var r in beforePara.Elements<Run>())
pendingBeforeRuns.Add((Run)r.CloneNode(true));
}
}
}
// ---- /Текст перед совпадением ----
var values = match.Definition.Values.ToList();
if (values.Count == 0) continue;
// Fast-path (одиночный простой текст) — оставлен как есть.
// В merge-режиме пропускаем, чтобы before/remainder могли склеиться.
if (!mergeBoundaryText
&& values.Count == 1
&& !string.IsNullOrEmpty(values[0].Text)
&& !values[0].BreakValue.HasValue)
{
var newText = values[0].Text;
var matchSeg = structure.Segments.FirstOrDefault(s => s.Start >= match.Start && s.End <= match.End);
if (matchSeg is not null)
{
var run = matchSeg.Run;
foreach (var textElem in run.Elements<Text>().ToList())
textElem.Remove();
run.AppendChild(new Text(newText));
return null;
}
}
// Сброс флагов для новой группы
pageBreakBeforeNext = false;
pendingOrientation = null;
firstSplitMarkerHandled = false;
textCount = 0;
lastTextPara = null;
emptyParagraphCreated = false;
foreach (var item in values)
{
if (!string.IsNullOrEmpty(item.Text))
{
ProcessTextItem(
item.Text,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref textCount,
ref pageBreakBeforeNext,
ref sectionChangeInsideGroup,
ref lastOrientation,
original,
structure,
match.Start,
followingSection,
portraitSection,
resultParas);
// НОВОЕ: вклеиваем before-runs в только что созданный параграф
if (mergeBoundaryText && pendingBeforeRuns is not null && currentPara is not null)
{
for (int j = pendingBeforeRuns.Count - 1; j >= 0; j--)
currentPara.InsertAt((Run)pendingBeforeRuns[j].CloneNode(true), 0);
pendingBeforeRuns = null;
}
}
else if (item.BreakValue.HasValue)
{
ProcessBreakItem(
item.BreakValue.Value,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref pageBreakBeforeNext,
ref firstSplitMarkerHandled,
ref sectionChangeInsideGroup,
ref lastOrientation,
ref textCount,
ref emptyParagraphCreated,
hasEmptyParagraphsBefore,
original,
followingSection,
portraitSection,
resultParas);
// НОВОЕ: если создался параграф под break (landscape/portrait), тоже вклеим
if (mergeBoundaryText && pendingBeforeRuns is not null && currentPara is not null)
{
// вставляем before-runs ПЕРЕД любыми break-runs
for (int j = pendingBeforeRuns.Count - 1; j >= 0; j--)
currentPara.InsertAt((Run)pendingBeforeRuns[j].CloneNode(true), 0);
pendingBeforeRuns = null;
}
}
}
// Закрытие секции (без изменений)
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, followingSection, portraitSection);
}
}
if (!sectionChangeInsideGroup && currentPara is not null)
{
var sourcePageSize = followingSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
currentPara.ParagraphProperties ??= new ParagraphProperties();
currentPara.ParagraphProperties.AppendChild(followingSection.CloneNode(true));
}
}
}
EnsureBodyLandscapeSection(body!, followingSection, sectionChangeInsideGroup, lastOrientation);
currentPos = match.End;
}
// ---- Обработка остатка текста ----
if (currentPos < fullText.Length)
{
var remainderPara = BuildRemainderParagraph(original, structure, currentPos);
if (remainderPara is not null)
{
bool canMerge = mergeBoundaryText
&& resultParas.Count > 0
&& !pendingOrientation.HasValue
&& !sectionChangeInsideGroup;
if (canMerge)
{
// Вклеиваем runs остатка в последний параграф
var lastPara = resultParas[resultParas.Count - 1];
foreach (var r in remainderPara.Elements<Run>().ToList())
lastPara.AppendChild(r.CloneNode(true));
}
else
{
// Оригинальное поведение
if (pendingOrientation.HasValue)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(remainderPara, pendingOrientation.Value, addPageSize, followingSection, 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, followingSection, portraitSection);
}
else
{
var sourcePageSize = followingSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
remainderPara.ParagraphProperties ??= new ParagraphProperties();
remainderPara.ParagraphProperties.AppendChild(followingSection.CloneNode(true));
}
}
resultParas.Add(remainderPara);
}
}
}
// ---- /Обработка остатка текста ----
// 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);
}
return resultParas.Count > 0 ? resultParas : null;
}
}