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