множественные замены с параметром
This commit is contained in:
@@ -0,0 +1,220 @@
|
|||||||
|
namespace QWERTYkez.WordProcessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Множественные динамические замены с извлечением параметра из "$key|param$".
|
||||||
|
/// Переиспользует движок <see cref="MultiReplaceExt"/> (форматирование Run-ов,
|
||||||
|
/// разрывы страниц, смена ориентации, разбиение на параграфы — всё как в оригинале).
|
||||||
|
/// </summary>
|
||||||
|
internal static class DynamicMultiReplaceExt
|
||||||
|
{
|
||||||
|
// ============================================================
|
||||||
|
// ReplaceString (Func<string, IEnumerable<string>>)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
internal static void ReplaceStringMultiInBody(
|
||||||
|
Body body,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<string>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (body is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
var paragraphs = body.Elements<Paragraph>().ToList();
|
||||||
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicStringMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReplaceStringMultiInTables(
|
||||||
|
OpenXmlElement element,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<string>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (element is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
foreach (var table in TableTextProcessor.GetAllTables(element))
|
||||||
|
{
|
||||||
|
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
|
||||||
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicStringMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReplaceStringMultiInHeadersFooters(
|
||||||
|
WordprocessingDocument doc,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<string>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (doc is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
|
||||||
|
ProcessStringMultiInElement(header, prefix, suffix, factory, comparisonType);
|
||||||
|
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
|
||||||
|
ProcessStringMultiInElement(footer, prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessStringMultiInElement(
|
||||||
|
OpenXmlElement element,
|
||||||
|
string prefix,
|
||||||
|
string suffix,
|
||||||
|
Func<string, IEnumerable<string>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||||||
|
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||||||
|
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||||||
|
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||||||
|
|
||||||
|
for (int i = allParagraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicStringMulti(allParagraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessDynamicStringMulti(
|
||||||
|
Paragraph paragraph,
|
||||||
|
string prefix,
|
||||||
|
string suffix,
|
||||||
|
Func<string, IEnumerable<string>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (paragraph is null || paragraph.Parent is null) return;
|
||||||
|
|
||||||
|
var text = paragraph.InnerText;
|
||||||
|
if (string.IsNullOrEmpty(text)) return;
|
||||||
|
|
||||||
|
var matches = DynamicReplaceExt.FindMatches(text, prefix, suffix, comparisonType);
|
||||||
|
if (matches.Count == 0) return;
|
||||||
|
|
||||||
|
// Строим словарь уникальных matchedText -> значения для этого параграфа
|
||||||
|
var dict = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
for (int i = 0; i < matches.Count; i++)
|
||||||
|
{
|
||||||
|
var m = matches[i];
|
||||||
|
string matchedText = text.Substring(m.Start, m.End - m.Start);
|
||||||
|
if (dict.ContainsKey(matchedText)) continue;
|
||||||
|
|
||||||
|
IEnumerable<string> values;
|
||||||
|
try { values = factory(m.Param); }
|
||||||
|
catch { continue; }
|
||||||
|
if (values is null) continue;
|
||||||
|
|
||||||
|
var list = values as IList<string> ?? values.ToList();
|
||||||
|
if (list.Count == 0) continue;
|
||||||
|
|
||||||
|
dict[matchedText] = list;
|
||||||
|
}
|
||||||
|
if (dict.Count == 0) return;
|
||||||
|
|
||||||
|
var newParas = MultiReplaceExt.ProcessParagraphWithMergedBoundaries(paragraph, dict, comparisonType);
|
||||||
|
if (newParas is not null && newParas.Count > 0)
|
||||||
|
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ReplaceItem (Func<string, IEnumerable<ReplaceItem>>)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
internal static void ReplaceItemMultiInBody(
|
||||||
|
Body body,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<ReplaceItem>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (body is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
var paragraphs = body.Elements<Paragraph>().ToList();
|
||||||
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicItemMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReplaceItemMultiInTables(
|
||||||
|
OpenXmlElement element,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<ReplaceItem>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (element is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
foreach (var table in TableTextProcessor.GetAllTables(element))
|
||||||
|
{
|
||||||
|
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
|
||||||
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicItemMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReplaceItemMultiInHeadersFooters(
|
||||||
|
WordprocessingDocument doc,
|
||||||
|
string oldValue,
|
||||||
|
Func<string, IEnumerable<ReplaceItem>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (doc is null || factory is null) return;
|
||||||
|
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
|
||||||
|
|
||||||
|
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
|
||||||
|
ProcessItemMultiInElement(header, prefix, suffix, factory, comparisonType);
|
||||||
|
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
|
||||||
|
ProcessItemMultiInElement(footer, prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessItemMultiInElement(
|
||||||
|
OpenXmlElement element,
|
||||||
|
string prefix,
|
||||||
|
string suffix,
|
||||||
|
Func<string, IEnumerable<ReplaceItem>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||||||
|
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||||||
|
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||||||
|
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||||||
|
|
||||||
|
for (int i = allParagraphs.Count - 1; i >= 0; i--)
|
||||||
|
ProcessDynamicItemMulti(allParagraphs[i], prefix, suffix, factory, comparisonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessDynamicItemMulti(
|
||||||
|
Paragraph paragraph,
|
||||||
|
string prefix,
|
||||||
|
string suffix,
|
||||||
|
Func<string, IEnumerable<ReplaceItem>> factory,
|
||||||
|
StringComparison comparisonType)
|
||||||
|
{
|
||||||
|
if (paragraph is null || paragraph.Parent is null) return;
|
||||||
|
|
||||||
|
var text = paragraph.InnerText;
|
||||||
|
if (string.IsNullOrEmpty(text)) return;
|
||||||
|
|
||||||
|
var matches = DynamicReplaceExt.FindMatches(text, prefix, suffix, comparisonType);
|
||||||
|
if (matches.Count == 0) return;
|
||||||
|
|
||||||
|
var dict = new Dictionary<string, IEnumerable<ReplaceItem>>();
|
||||||
|
for (int i = 0; i < matches.Count; i++)
|
||||||
|
{
|
||||||
|
var m = matches[i];
|
||||||
|
string matchedText = text.Substring(m.Start, m.End - m.Start);
|
||||||
|
if (dict.ContainsKey(matchedText)) continue;
|
||||||
|
|
||||||
|
IEnumerable<ReplaceItem> values;
|
||||||
|
try { values = factory(m.Param); }
|
||||||
|
catch { continue; }
|
||||||
|
if (values is null) continue;
|
||||||
|
|
||||||
|
var list = values as IList<ReplaceItem> ?? values.ToList();
|
||||||
|
if (list.Count == 0) continue;
|
||||||
|
|
||||||
|
dict[matchedText] = list;
|
||||||
|
}
|
||||||
|
if (dict.Count == 0) return;
|
||||||
|
|
||||||
|
var newParas = MultiReplaceExt.ProcessParagraphWithMergedBoundaries(paragraph, dict, comparisonType);
|
||||||
|
if (newParas is not null && newParas.Count > 0)
|
||||||
|
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -402,7 +402,7 @@ internal static class MultiReplaceExt
|
|||||||
|
|
||||||
// ---------- ОСНОВНОЙ АЛГОРИТМ ----------
|
// ---------- ОСНОВНОЙ АЛГОРИТМ ----------
|
||||||
|
|
||||||
private static List<Paragraph>? ProcessMultiReplacements(
|
internal static List<Paragraph>? ProcessMultiReplacements(
|
||||||
Paragraph original,
|
Paragraph original,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +344,7 @@ internal sealed class WordWriter : WordReader, IWordWriter
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region динамические змены с параметром
|
||||||
|
|
||||||
public void ReplaceString(string oldValue, Func<string, string> newValues)
|
public void ReplaceString(string oldValue, Func<string, string> newValues)
|
||||||
{
|
{
|
||||||
@@ -373,7 +373,6 @@ internal sealed class WordWriter : WordReader, IWordWriter
|
|||||||
_isModified = true;
|
_isModified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReplaceItem(string oldValue, Func<string, ReplaceItem> newValues)
|
public void ReplaceItem(string oldValue, Func<string, ReplaceItem> newValues)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
@@ -401,7 +400,6 @@ internal sealed class WordWriter : WordReader, IWordWriter
|
|||||||
_isModified = true;
|
_isModified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReplaceToText(string oldValue, Action<string, IText> buildText)
|
public void ReplaceToText(string oldValue, Action<string, IText> buildText)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
@@ -422,45 +420,75 @@ internal sealed class WordWriter : WordReader, IWordWriter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ReplaceString(string oldValue, Func<string, string[]> newValues)
|
||||||
|
=> ReplaceString(oldValue, (Func<string, IEnumerable<string>>)newValues);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void ReplaceString(string oldValue, Func<string, IEnumerable<string>> newValues)
|
public void ReplaceString(string oldValue, Func<string, IEnumerable<string>> newValues)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
ThrowIfDisposed();
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceString(string oldValue, Func<string, string[]> newValues)
|
if (string.IsNullOrEmpty(oldValue))
|
||||||
{
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (newValues is null) return;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceString(dynamic-multi)] START: '{oldValue}'");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
// 1. Основной текст
|
||||||
|
DynamicMultiReplaceExt.ReplaceStringMultiInBody(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// 2. Таблицы в основном тексте
|
||||||
|
DynamicMultiReplaceExt.ReplaceStringMultiInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// 3. Колонтитулы
|
||||||
|
DynamicMultiReplaceExt.ReplaceStringMultiInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_isModified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void ReplaceItem(string oldValue, Func<string, ReplaceItem[]> newValues)
|
||||||
|
=> ReplaceItem(oldValue, (Func<string, IEnumerable<ReplaceItem>>)newValues);
|
||||||
public void ReplaceItem(string oldValue, Func<string, IEnumerable<ReplaceItem>> newValues)
|
public void ReplaceItem(string oldValue, Func<string, IEnumerable<ReplaceItem>> newValues)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
ThrowIfDisposed();
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceItem(string oldValue, Func<string, ReplaceItem[]> newValues)
|
if (string.IsNullOrEmpty(oldValue))
|
||||||
{
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||||||
throw new NotImplementedException();
|
|
||||||
|
if (newValues is null) return;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceItem(dynamic-multi)] START: '{oldValue}'");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
// 1. Основной текст
|
||||||
|
DynamicMultiReplaceExt.ReplaceItemMultiInBody(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// 2. Таблицы в основном тексте
|
||||||
|
DynamicMultiReplaceExt.ReplaceItemMultiInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// 3. Колонтитулы
|
||||||
|
DynamicMultiReplaceExt.ReplaceItemMultiInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_isModified = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReplaceToTable(string oldValue, Action<string, ITable> buildTable)
|
public void ReplaceToTable(string oldValue, Action<string, ITable> buildTable)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReplaceToTable(string oldValue, FontProps baseFont, Action<string, ITable> buildTable)
|
public void ReplaceToTable(string oldValue, FontProps baseFont, Action<string, ITable> buildTable)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user