diff --git a/QWERTYkez.WordProcessor/DynamicMultiReplaceExt.cs b/QWERTYkez.WordProcessor/DynamicMultiReplaceExt.cs
index 865a0de..44d6e62 100644
--- a/QWERTYkez.WordProcessor/DynamicMultiReplaceExt.cs
+++ b/QWERTYkez.WordProcessor/DynamicMultiReplaceExt.cs
@@ -2,8 +2,7 @@
///
/// Множественные динамические замены с извлечением параметра из "$key|param$".
-/// Переиспользует движок (форматирование Run-ов,
-/// разрывы страниц, смена ориентации, разбиение на параграфы — всё как в оригинале).
+/// Переиспользует движок
///
internal static class DynamicMultiReplaceExt
{
@@ -101,7 +100,7 @@ internal static class DynamicMultiReplaceExt
catch { continue; }
if (values is null) continue;
- var list = values as IList ?? values.ToList();
+ var list = values as IList ?? [.. values];
if (list.Count == 0) continue;
dict[matchedText] = list;
@@ -206,7 +205,7 @@ internal static class DynamicMultiReplaceExt
catch { continue; }
if (values is null) continue;
- var list = values as IList ?? values.ToList();
+ var list = values as IList ?? [.. values];
if (list.Count == 0) continue;
dict[matchedText] = list;
diff --git a/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs b/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs
new file mode 100644
index 0000000..217be94
--- /dev/null
+++ b/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs
@@ -0,0 +1,216 @@
+namespace QWERTYkez.WordProcessor;
+
+///
+/// Динамическая замена параграфа, содержащего "$key|param$", на таблицу.
+/// Логика построения и подмены — 1:1 копия из ,
+/// плюс извлечение параметра.
+///
+internal static class DynamicReplaceToTableExt
+{
+ ///
+ /// Заменяет все параграфы, содержащие шаблон "$key|...$", на таблицы.
+ ///
+ /// Делегат (param, tableBuilder) — параметр из "$key|param$" и билдер таблицы.
+ internal static void ReplaceParagraphsContainingTextToTableDynamic(
+ Body body,
+ string oldValue,
+ Action buildTable,
+ FontProps? baseFont = null)
+ {
+ if (body is null || string.IsNullOrEmpty(oldValue) || buildTable is null)
+ return;
+
+ DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
+
+ var paragraphs = body.Elements().ToList();
+ if (paragraphs.Count == 0)
+ return;
+
+ for (int i = paragraphs.Count - 1; i >= 0; i--)
+ {
+ var paragraph = paragraphs[i];
+ if (paragraph is null) continue;
+
+ var paraText = paragraph.InnerText;
+ if (string.IsNullOrEmpty(paraText)) continue;
+
+ int pStart = paraText.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
+ if (pStart < 0) continue;
+
+ int paramStart = pStart + prefix.Length;
+ int pEnd = string.IsNullOrEmpty(suffix)
+ ? paraText.Length
+ : paraText.IndexOf(suffix, paramStart, StringComparison.OrdinalIgnoreCase);
+ if (pEnd < 0) continue;
+
+ string param = paraText.Substring(paramStart, pEnd - paramStart);
+ string matchedText = paraText.Substring(pStart, pEnd + suffix.Length - pStart);
+
+ // Извлекаем свойства шрифта из Run-а, содержащего совпадение
+ var fontProps = ExtractFontPropsFromParagraph(paragraph, matchedText);
+
+ if (fontProps is { } props)
+ fontProps = props.Override(baseFont);
+ else if (baseFont is not null)
+ fontProps = baseFont;
+
+ ReplaceWithTableBuilderDynamic(paragraph, param, buildTable, fontProps);
+ }
+ }
+
+ ///
+ /// Создаёт TableBuilder, вызывает пользовательский делегат, заменяет параграф таблицей.
+ /// (Копия с добавленным param.)
+ ///
+ private static void ReplaceWithTableBuilderDynamic(
+ Paragraph paragraph,
+ string param,
+ Action buildTable,
+ FontProps? baseFont)
+ {
+ if (paragraph is null || paragraph.Parent is null || buildTable is null)
+ return;
+
+ var parent = paragraph.Parent;
+
+ var builder = TableBuilder.Create(baseFont);
+ buildTable(param, builder);
+ var table = builder.Build();
+
+ int paraIndex = -1;
+ var children = parent.ChildElements;
+
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (children[i] == paragraph)
+ {
+ paraIndex = i;
+ break;
+ }
+ }
+
+ if (paraIndex == -1)
+ return;
+
+ parent.RemoveChild(paragraph);
+ parent.InsertAt(table, paraIndex);
+ }
+
+ private static FontProps? ExtractFontPropsFromParagraph(Paragraph paragraph, string searchText)
+ {
+ if (paragraph is null || string.IsNullOrEmpty(searchText))
+ return null;
+
+ var runs = paragraph.Descendants().ToList();
+ if (runs.Count == 0)
+ return null;
+
+ var runInfos = new List<(Run Run, int Start, int End, string Text)>();
+ int currentPosition = 0;
+
+ foreach (var run in runs)
+ {
+ var runText = GetRunText(run);
+ if (!string.IsNullOrEmpty(runText))
+ {
+ runInfos.Add((run, currentPosition, currentPosition + runText.Length, runText));
+ currentPosition += runText.Length;
+ }
+ }
+
+ foreach (var (run, start, end, text) in runInfos)
+ {
+ if (text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
+ return CreateFontPropsFromRun(run);
+ }
+
+ var fullText = string.Concat(runInfos.Select(r => r.Text));
+ int textIndex = fullText.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
+
+ if (textIndex >= 0)
+ {
+ var intersectingRun = runInfos.FirstOrDefault(r =>
+ r.Start <= textIndex && r.End > textIndex);
+
+ if (intersectingRun.Run is not null)
+ return CreateFontPropsFromRun(intersectingRun.Run);
+ }
+
+ return null;
+ }
+
+ private static FontProps CreateFontPropsFromRun(Run run)
+ {
+ var runProperties = run.RunProperties;
+ if (runProperties is null)
+ return new FontProps();
+
+ var fontProps = new FontProps();
+
+ var runFonts = runProperties.GetFirstChild();
+ if (runFonts is not null)
+ {
+ fontProps = fontProps with
+ {
+ FontFamily = runFonts.Ascii ?? runFonts.HighAnsi ?? runFonts.ComplexScript
+ };
+ }
+
+ var fontSize = runProperties.GetFirstChild();
+ if (fontSize is not null && !string.IsNullOrEmpty(fontSize.Val) &&
+ uint.TryParse(fontSize.Val, out uint halfPoints))
+ {
+ fontProps = fontProps with { Size = halfPoints / 2.0 };
+ }
+
+ var bold = runProperties.GetFirstChild();
+ if (bold is not null)
+ {
+ bool isBold = true;
+ if (bold.Val is not null)
+ isBold = bold.Val.Value;
+ fontProps = fontProps with { IsBold = isBold };
+ }
+
+ var italic = runProperties.GetFirstChild();
+ if (italic is not null)
+ {
+ bool isItalic = true;
+ if (italic.Val is not null)
+ isItalic = italic.Val.Value;
+ fontProps = fontProps with { IsItalic = isItalic };
+ }
+
+ var underline = runProperties.GetFirstChild();
+ if (underline is not null && underline.Val is not null)
+ {
+ fontProps = fontProps with { Underline = underline.Val.Value };
+ }
+
+ var color = runProperties.GetFirstChild();
+ if (color is not null && !string.IsNullOrEmpty(color.Val))
+ {
+ fontProps = fontProps with { Color = color.Val };
+ }
+
+ var verticalAlignment = runProperties.GetFirstChild();
+ if (verticalAlignment is not null && verticalAlignment.Val is not null)
+ {
+ fontProps = fontProps with { SubSup = verticalAlignment.Val.Value };
+ }
+
+ return fontProps;
+ }
+
+ private static string GetRunText(Run run)
+ {
+ if (run is null) return string.Empty;
+
+ var sb = new StringBuilder();
+ foreach (var text in run.Elements())
+ {
+ sb.Append(text.Text ?? string.Empty);
+ }
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.WordProcessor/MultiReplace.cs b/QWERTYkez.WordProcessor/MultiReplace.cs
index c1cb152..2166cfa 100644
--- a/QWERTYkez.WordProcessor/MultiReplace.cs
+++ b/QWERTYkez.WordProcessor/MultiReplace.cs
@@ -1011,7 +1011,7 @@ internal static class MultiReplaceExt
else
{
// Копим runs, склеим их с первым value-параграфом
- pendingBeforeRuns = new List();
+ pendingBeforeRuns = [];
foreach (var r in beforePara.Elements())
pendingBeforeRuns.Add((Run)r.CloneNode(true));
}
diff --git a/QWERTYkez.WordProcessor/WordWriter.cs b/QWERTYkez.WordProcessor/WordWriter.cs
index f5dbf9c..197ba3a 100644
--- a/QWERTYkez.WordProcessor/WordWriter.cs
+++ b/QWERTYkez.WordProcessor/WordWriter.cs
@@ -479,13 +479,48 @@ internal sealed class WordWriter : WordReader, IWordWriter
}
}
+ // ============================================================
+ // Динамические замены на таблицу с извлечением параметра из "$key|$"
+ // ============================================================
+
public void ReplaceToTable(string oldValue, Action buildTable)
{
- throw new NotImplementedException();
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ if (buildTable is null) return;
+
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceToTable(dynamic)] START: '{oldValue}'");
+#endif
+
+ lock (_syncLock)
+ {
+ DynamicReplaceToTableExt.ReplaceParagraphsContainingTextToTableDynamic(_body, oldValue, buildTable);
+ _isModified = true;
+ }
}
+
public void ReplaceToTable(string oldValue, FontProps baseFont, Action buildTable)
{
- throw new NotImplementedException();
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ if (buildTable is null) return;
+
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceToTable(dynamic,font)] START: '{oldValue}'");
+#endif
+
+ lock (_syncLock)
+ {
+ DynamicReplaceToTableExt.ReplaceParagraphsContainingTextToTableDynamic(_body, oldValue, buildTable, baseFont);
+ _isModified = true;
+ }
}
#endregion