diff --git a/QWERTYkez.WordProcessor/Builders/CellProps.cs b/QWERTYkez.WordProcessor/Builders/CellProps.cs index adf788b..4da50ba 100644 --- a/QWERTYkez.WordProcessor/Builders/CellProps.cs +++ b/QWERTYkez.WordProcessor/Builders/CellProps.cs @@ -5,6 +5,7 @@ public readonly struct CellProps(double? width = null!) public double? Width { get; init; } = width; public TableVerticalAlignmentValues? VerticalAlignment { get; init; } = default; public MergedCell? Merge { get; init; } = default; + public TextDirection TextDirection { get; init; } = TextDirection.LeftToRight; #pragma warning disable IDE1006 // Стили именования @@ -40,6 +41,19 @@ public readonly struct CellProps(double? width = null!) Width = ((uint)(Width.Value * 567d)).ToString() // 1 см = 567 DXA }); + if (TextDirection is not TextDirection.LeftToRight) + { + list.Add(new DocumentFormat.OpenXml.Wordprocessing.TextDirection + { + Val = TextDirection switch + { + TextDirection.BottomToTop => TextDirectionValues.BottomToTopLeftToRight, + TextDirection.TopToBottom => TextDirectionValues.TopToBottomRightToLeft, + _ => throw new ArgumentOutOfRangeException(nameof(TextDirection), TextDirection, "Unsupported text direction"), + } + }); + } + if (Merge.HasValue) { switch (Merge.Value) @@ -86,4 +100,14 @@ public enum MergedCell _VH_, V__H, +} + +public enum TextDirection +{ + /// ⇒ ⇒ ⇒ Стандартный свева направо + LeftToRight, + /// ⇑ ⇑ ⇑ Снизу вверх + BottomToTop, + /// ⇓ ⇓ ⇓ Сверху вниз + TopToBottom, } \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/Builders/Interfaces.cs b/QWERTYkez.WordProcessor/Builders/Interfaces.cs index 790ab3a..a5f7258 100644 --- a/QWERTYkez.WordProcessor/Builders/Interfaces.cs +++ b/QWERTYkez.WordProcessor/Builders/Interfaces.cs @@ -54,6 +54,10 @@ public interface IRow /// Тот же экземпляр для цепочки вызовов. IRow Cell(CellProps cellProps); + /// Добавляет ячейку (без текста). + /// Тот же экземпляр для цепочки вызовов. + IRow Cell(); + /// Добавляет ячейку с заданными свойствами и текстом (используется базовый шрифт). /// Свойства ячейки. /// Текст ячейки. diff --git a/QWERTYkez.WordProcessor/Builders/TableBuilder.cs b/QWERTYkez.WordProcessor/Builders/TableBuilder.cs index d22552b..f2343d7 100644 --- a/QWERTYkez.WordProcessor/Builders/TableBuilder.cs +++ b/QWERTYkez.WordProcessor/Builders/TableBuilder.cs @@ -237,6 +237,14 @@ internal sealed class TableBuilder : ParagraphBuilderBase, ITable, IRow, ICell return this; } + public IRow Cell() + { + var cellBuilder = CellBuilder; + cellBuilder.Paragraph(p => p.Run("")); + _row.AppendChild(cellBuilder.Build()); + return this; + } + // Метод для установки произвольных свойств строки public IRow SetProperties(Action configure) { diff --git a/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs b/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs index 217be94..8f535d0 100644 --- a/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs +++ b/QWERTYkez.WordProcessor/DynamicReplaceToTableExt.cs @@ -47,7 +47,7 @@ internal static class DynamicReplaceToTableExt string matchedText = paraText.Substring(pStart, pEnd + suffix.Length - pStart); // Извлекаем свойства шрифта из Run-а, содержащего совпадение - var fontProps = ExtractFontPropsFromParagraph(paragraph, matchedText); + var fontProps = paragraph.ExtractFontProps(matchedText); if (fontProps is { } props) fontProps = props.Override(baseFont); @@ -95,122 +95,4 @@ internal static class DynamicReplaceToTableExt 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/Extension.cs b/QWERTYkez.WordProcessor/Extension.cs index 3b602e2..3878906 100644 --- a/QWERTYkez.WordProcessor/Extension.cs +++ b/QWERTYkez.WordProcessor/Extension.cs @@ -48,4 +48,72 @@ internal static class Extension } return -1; } + + + + /// + /// Извлекает свойства шрифта (FontProps) из Run, содержащего указанный текст в параграфе. + /// + public static FontProps? ExtractFontProps(this Paragraph paragraph, string searchText) + { + if (paragraph is null || string.IsNullOrEmpty(searchText)) + return null; + + var runs = paragraph.Descendants().ToList(); + if (runs.Count == 0) + return null; + + // Собираем полный текст параграфа и позиции каждого Run для анализа + 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; + } + } + + // Ищем Run, который содержит искомый текст (регистронезависимо) + foreach (var (run, start, end, text) in runInfos) + { + // Проверяем, содержится ли искомый текст в этом Run + if (text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) + return FontPropsExtractor.CreateFontPropsFromRun(run, paragraph); + } + + // Если точное вхождение не найдено, ищем пересечение текста + var fullText = string.Concat(runInfos.Select(r => r.Text)); + int textIndex = fullText.IndexOf(searchText, StringComparison.OrdinalIgnoreCase); + + if (textIndex >= 0) + { + // Находим первый Run, который пересекается с найденным текстом + var intersectingRun = runInfos.FirstOrDefault(r => + r.Start <= textIndex && r.End > textIndex); + + if (intersectingRun.Run is not null) + return FontPropsExtractor.CreateFontPropsFromRun(intersectingRun.Run, paragraph); + } + + return null; // Не удалось найти подходящий Run + } + + /// + /// Вспомогательный метод для получения текста из Run. + /// + 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/FontPropsExtractor.cs b/QWERTYkez.WordProcessor/FontPropsExtractor.cs new file mode 100644 index 0000000..425c18f --- /dev/null +++ b/QWERTYkez.WordProcessor/FontPropsExtractor.cs @@ -0,0 +1,187 @@ +namespace QWERTYkez.WordProcessor; + +internal static class FontPropsExtractor +{ + // ============================================================ + // Создаёт FontProps на основе Run + разрешает унаследованный шрифт. + // ============================================================ + internal static FontProps CreateFontPropsFromRun(Run run, Paragraph paragraph) + { + var runProperties = run?.RunProperties; + var fontProps = new FontProps(); + + if (runProperties is not null) + { + var runFonts = runProperties.GetFirstChild(); + var ff = ExtractFontFamily(runFonts, paragraph); + if (!string.IsNullOrEmpty(ff)) + fontProps = fontProps with { FontFamily = ff }; + + 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 = bold.Val?.Value ?? true; + fontProps = fontProps with { IsBold = isBold }; + } + + var italic = runProperties.GetFirstChild(); + if (italic is not null) + { + bool isItalic = italic.Val?.Value ?? true; + fontProps = fontProps with { IsItalic = isItalic }; + } + + var underline = runProperties.GetFirstChild(); + if (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?.Val is not null) + fontProps = fontProps with { SubSup = verticalAlignment.Val.Value }; + } + + // Фолбэк: если в run шрифт не задан — идём по цепочке стилей + if (string.IsNullOrEmpty(fontProps.FontFamily)) + { + var fallback = ResolveInheritedFontFamily(paragraph); + if (!string.IsNullOrEmpty(fallback)) + fontProps = fontProps with { FontFamily = fallback }; + } + + return fontProps; + } + + // ============================================================ + // Разрешение шрифта по цепочке: + // pStyle → BasedOn → default paragraph style → DocDefaults + // ============================================================ + private static string? ResolveInheritedFontFamily(Paragraph paragraph) + { + if (paragraph is null) return null; + + var mainPart = paragraph.Ancestors().FirstOrDefault()?.OpenXmlPart as MainDocumentPart; + var styles = mainPart?.StyleDefinitionsPart?.Styles; + + // 1. Стиль абзаца + var pStyleId = paragraph.ParagraphProperties?.ParagraphStyleId?.Val?.Value; + if (!string.IsNullOrEmpty(pStyleId) && styles is not null) + { + var ff = ResolveFromStyleChain(styles, pStyleId, mainPart); + if (!string.IsNullOrEmpty(ff)) return ff; + } + + // 2. Дефолтный стиль абзаца + if (styles is not null) + { + var defaultParaStyle = styles.Elements