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