[RT]
This commit is contained in:
@@ -5,6 +5,7 @@ public readonly struct CellProps(double? width = null!)
|
|||||||
public double? Width { get; init; } = width;
|
public double? Width { get; init; } = width;
|
||||||
public TableVerticalAlignmentValues? VerticalAlignment { get; init; } = default;
|
public TableVerticalAlignmentValues? VerticalAlignment { get; init; } = default;
|
||||||
public MergedCell? Merge { get; init; } = default;
|
public MergedCell? Merge { get; init; } = default;
|
||||||
|
public TextDirection TextDirection { get; init; } = TextDirection.LeftToRight;
|
||||||
|
|
||||||
|
|
||||||
#pragma warning disable IDE1006 // Стили именования
|
#pragma warning disable IDE1006 // Стили именования
|
||||||
@@ -40,6 +41,19 @@ public readonly struct CellProps(double? width = null!)
|
|||||||
Width = ((uint)(Width.Value * 567d)).ToString() // 1 см = 567 DXA
|
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)
|
if (Merge.HasValue)
|
||||||
{
|
{
|
||||||
switch (Merge.Value)
|
switch (Merge.Value)
|
||||||
@@ -87,3 +101,13 @@ public enum MergedCell
|
|||||||
_VH_,
|
_VH_,
|
||||||
V__H,
|
V__H,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TextDirection
|
||||||
|
{
|
||||||
|
/// <summary> ⇒ ⇒ ⇒ Стандартный свева направо </summary>
|
||||||
|
LeftToRight,
|
||||||
|
/// <summary> ⇑ ⇑ ⇑ Снизу вверх </summary>
|
||||||
|
BottomToTop,
|
||||||
|
/// <summary> ⇓ ⇓ ⇓ Сверху вниз </summary>
|
||||||
|
TopToBottom,
|
||||||
|
}
|
||||||
@@ -54,6 +54,10 @@ public interface IRow
|
|||||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||||
IRow Cell(CellProps cellProps);
|
IRow Cell(CellProps cellProps);
|
||||||
|
|
||||||
|
/// <summary>Добавляет ячейку (без текста).</summary>
|
||||||
|
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||||
|
IRow Cell();
|
||||||
|
|
||||||
/// <summary>Добавляет ячейку с заданными свойствами и текстом (используется базовый шрифт).</summary>
|
/// <summary>Добавляет ячейку с заданными свойствами и текстом (используется базовый шрифт).</summary>
|
||||||
/// <param name="cellProps">Свойства ячейки.</param>
|
/// <param name="cellProps">Свойства ячейки.</param>
|
||||||
/// <param name="text">Текст ячейки.</param>
|
/// <param name="text">Текст ячейки.</param>
|
||||||
|
|||||||
@@ -237,6 +237,14 @@ internal sealed class TableBuilder : ParagraphBuilderBase, ITable, IRow, ICell
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IRow Cell()
|
||||||
|
{
|
||||||
|
var cellBuilder = CellBuilder;
|
||||||
|
cellBuilder.Paragraph(p => p.Run(""));
|
||||||
|
_row.AppendChild(cellBuilder.Build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// Метод для установки произвольных свойств строки
|
// Метод для установки произвольных свойств строки
|
||||||
public IRow SetProperties(Action<TableRowProperties> configure)
|
public IRow SetProperties(Action<TableRowProperties> configure)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ internal static class DynamicReplaceToTableExt
|
|||||||
string matchedText = paraText.Substring(pStart, pEnd + suffix.Length - pStart);
|
string matchedText = paraText.Substring(pStart, pEnd + suffix.Length - pStart);
|
||||||
|
|
||||||
// Извлекаем свойства шрифта из Run-а, содержащего совпадение
|
// Извлекаем свойства шрифта из Run-а, содержащего совпадение
|
||||||
var fontProps = ExtractFontPropsFromParagraph(paragraph, matchedText);
|
var fontProps = paragraph.ExtractFontProps(matchedText);
|
||||||
|
|
||||||
if (fontProps is { } props)
|
if (fontProps is { } props)
|
||||||
fontProps = props.Override(baseFont);
|
fontProps = props.Override(baseFont);
|
||||||
@@ -95,122 +95,4 @@ internal static class DynamicReplaceToTableExt
|
|||||||
parent.RemoveChild(paragraph);
|
parent.RemoveChild(paragraph);
|
||||||
parent.InsertAt(table, paraIndex);
|
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<Run>().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<RunFonts>();
|
|
||||||
if (runFonts is not null)
|
|
||||||
{
|
|
||||||
fontProps = fontProps with
|
|
||||||
{
|
|
||||||
FontFamily = runFonts.Ascii ?? runFonts.HighAnsi ?? runFonts.ComplexScript
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var fontSize = runProperties.GetFirstChild<FontSize>();
|
|
||||||
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<Bold>();
|
|
||||||
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<Italic>();
|
|
||||||
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<Underline>();
|
|
||||||
if (underline is not null && underline.Val is not null)
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { Underline = underline.Val.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
var color = runProperties.GetFirstChild<Color>();
|
|
||||||
if (color is not null && !string.IsNullOrEmpty(color.Val))
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { Color = color.Val };
|
|
||||||
}
|
|
||||||
|
|
||||||
var verticalAlignment = runProperties.GetFirstChild<VerticalTextAlignment>();
|
|
||||||
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<Text>())
|
|
||||||
{
|
|
||||||
sb.Append(text.Text ?? string.Empty);
|
|
||||||
}
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -48,4 +48,72 @@ internal static class Extension
|
|||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Извлекает свойства шрифта (FontProps) из Run, содержащего указанный текст в параграфе.
|
||||||
|
/// </summary>
|
||||||
|
public static FontProps? ExtractFontProps(this Paragraph paragraph, string searchText)
|
||||||
|
{
|
||||||
|
if (paragraph is null || string.IsNullOrEmpty(searchText))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var runs = paragraph.Descendants<Run>().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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вспомогательный метод для получения текста из Run.
|
||||||
|
/// </summary>
|
||||||
|
static string GetRunText(Run run)
|
||||||
|
{
|
||||||
|
if (run is null) return string.Empty;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var text in run.Elements<Text>())
|
||||||
|
{
|
||||||
|
sb.Append(text.Text ?? string.Empty);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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<RunFonts>();
|
||||||
|
var ff = ExtractFontFamily(runFonts, paragraph);
|
||||||
|
if (!string.IsNullOrEmpty(ff))
|
||||||
|
fontProps = fontProps with { FontFamily = ff };
|
||||||
|
|
||||||
|
var fontSize = runProperties.GetFirstChild<FontSize>();
|
||||||
|
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<Bold>();
|
||||||
|
if (bold is not null)
|
||||||
|
{
|
||||||
|
bool isBold = bold.Val?.Value ?? true;
|
||||||
|
fontProps = fontProps with { IsBold = isBold };
|
||||||
|
}
|
||||||
|
|
||||||
|
var italic = runProperties.GetFirstChild<Italic>();
|
||||||
|
if (italic is not null)
|
||||||
|
{
|
||||||
|
bool isItalic = italic.Val?.Value ?? true;
|
||||||
|
fontProps = fontProps with { IsItalic = isItalic };
|
||||||
|
}
|
||||||
|
|
||||||
|
var underline = runProperties.GetFirstChild<Underline>();
|
||||||
|
if (underline?.Val is not null)
|
||||||
|
fontProps = fontProps with { Underline = underline.Val.Value };
|
||||||
|
|
||||||
|
var color = runProperties.GetFirstChild<Color>();
|
||||||
|
if (color is not null && !string.IsNullOrEmpty(color.Val))
|
||||||
|
fontProps = fontProps with { Color = color.Val };
|
||||||
|
|
||||||
|
var verticalAlignment = runProperties.GetFirstChild<VerticalTextAlignment>();
|
||||||
|
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<Document>().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<Style>()
|
||||||
|
.FirstOrDefault(s => s.Default?.Value == true && s.Type?.Value == StyleValues.Paragraph);
|
||||||
|
var defId = defaultParaStyle?.StyleId?.Value;
|
||||||
|
if (!string.IsNullOrEmpty(defId))
|
||||||
|
{
|
||||||
|
var ff = ResolveFromStyleChain(styles, defId, mainPart);
|
||||||
|
if (!string.IsNullOrEmpty(ff)) return ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. DocDefaults
|
||||||
|
var docDefaultsFonts = styles?.DocDefaults?
|
||||||
|
.RunPropertiesDefault?
|
||||||
|
.RunPropertiesBaseStyle?
|
||||||
|
.RunFonts;
|
||||||
|
var docDefault = ExtractFontFamily(docDefaultsFonts, paragraph);
|
||||||
|
if (!string.IsNullOrEmpty(docDefault)) return docDefault;
|
||||||
|
|
||||||
|
// 4. Theme fallback (minor)
|
||||||
|
var themeFont = TryResolveThemeFont(mainPart, isMajor: false);
|
||||||
|
if (!string.IsNullOrEmpty(themeFont)) return themeFont;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveFromStyleChain(Styles styles, string styleId, MainDocumentPart? mainPart)
|
||||||
|
{
|
||||||
|
var visited = new HashSet<string>();
|
||||||
|
var currentId = styleId;
|
||||||
|
|
||||||
|
while (!string.IsNullOrEmpty(currentId) && visited.Add(currentId))
|
||||||
|
{
|
||||||
|
var style = styles.Elements<Style>()
|
||||||
|
.FirstOrDefault(s => s.StyleId?.Value == currentId);
|
||||||
|
if (style is null) break;
|
||||||
|
|
||||||
|
var fonts = style.StyleRunProperties?.RunFonts;
|
||||||
|
var ff = ExtractFontFamily(fonts, mainPart);
|
||||||
|
if (!string.IsNullOrEmpty(ff)) return ff;
|
||||||
|
|
||||||
|
// linked style (w:link) — иногда шрифт лежит в парном character-style
|
||||||
|
var linkedId = style.LinkedStyle?.Val?.Value;
|
||||||
|
if (!string.IsNullOrEmpty(linkedId) && visited.Add(linkedId))
|
||||||
|
{
|
||||||
|
var linked = styles.Elements<Style>()
|
||||||
|
.FirstOrDefault(s => s.StyleId?.Value == linkedId);
|
||||||
|
var linkedFf = ExtractFontFamily(linked?.StyleRunProperties?.RunFonts, mainPart);
|
||||||
|
if (!string.IsNullOrEmpty(linkedFf)) return linkedFf;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentId = style.BasedOn?.Val?.Value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// RunFonts → строка. Разрешает темы.
|
||||||
|
// ============================================================
|
||||||
|
internal static string? ExtractFontFamily(RunFonts? fonts, Paragraph? paragraph)
|
||||||
|
=> ExtractFontFamily(fonts, paragraph?.Ancestors<Document>().FirstOrDefault()?.OpenXmlPart as MainDocumentPart);
|
||||||
|
|
||||||
|
internal static string? ExtractFontFamily(RunFonts? fonts, MainDocumentPart? mainPart)
|
||||||
|
{
|
||||||
|
if (fonts is null) return null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(fonts.Ascii)) return fonts.Ascii;
|
||||||
|
if (!string.IsNullOrEmpty(fonts.HighAnsi)) return fonts.HighAnsi;
|
||||||
|
if (!string.IsNullOrEmpty(fonts.ComplexScript)) return fonts.ComplexScript;
|
||||||
|
|
||||||
|
// Атрибуты темы
|
||||||
|
bool isMajor =
|
||||||
|
fonts.AsciiTheme?.Value == ThemeFontValues.MajorAscii ||
|
||||||
|
fonts.AsciiTheme?.Value == ThemeFontValues.MajorHighAnsi ||
|
||||||
|
fonts.HighAnsiTheme?.Value == ThemeFontValues.MajorAscii ||
|
||||||
|
fonts.HighAnsiTheme?.Value == ThemeFontValues.MajorHighAnsi;
|
||||||
|
|
||||||
|
bool hasTheme =
|
||||||
|
fonts.AsciiTheme?.Value is not null ||
|
||||||
|
fonts.HighAnsiTheme?.Value is not null;
|
||||||
|
|
||||||
|
if (hasTheme)
|
||||||
|
{
|
||||||
|
var t = TryResolveThemeFont(mainPart, isMajor);
|
||||||
|
if (!string.IsNullOrEmpty(t)) return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryResolveThemeFont(MainDocumentPart? mainPart, bool isMajor)
|
||||||
|
{
|
||||||
|
var fontScheme = mainPart?.ThemePart?.Theme?.ThemeElements?.FontScheme;
|
||||||
|
if (fontScheme is null) return null;
|
||||||
|
|
||||||
|
var latin = isMajor
|
||||||
|
? fontScheme.MajorFont?.LatinFont
|
||||||
|
: fontScheme.MinorFont?.LatinFont;
|
||||||
|
|
||||||
|
return latin?.Typeface?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,30 +12,81 @@ internal static class ReplaceToTableExt
|
|||||||
|
|
||||||
var parent = paragraph.Parent;
|
var parent = paragraph.Parent;
|
||||||
|
|
||||||
// Создаем TableBuilder с базовым шрифтом
|
// 1. Строим таблицу
|
||||||
var builder = TableBuilder.Create(baseFont);
|
var builder = TableBuilder.Create(baseFont);
|
||||||
buildTable(builder);
|
buildTable(builder);
|
||||||
var table = builder.Build();
|
var table = builder.Build();
|
||||||
|
|
||||||
// Находим индекс параграфа среди детей родителя
|
// 2. Находим индекс параграфа
|
||||||
int paraIndex = -1;
|
int paraIndex = -1;
|
||||||
var children = parent.ChildElements;
|
var children = parent.ChildElements;
|
||||||
|
|
||||||
for (int i = 0; i < children.Count; i++)
|
for (int i = 0; i < children.Count; i++)
|
||||||
{
|
{
|
||||||
if (children[i] == paragraph)
|
if (children[i] == paragraph) { paraIndex = i; break; }
|
||||||
|
}
|
||||||
|
if (paraIndex == -1) return;
|
||||||
|
|
||||||
|
// 3. PageBreakBefore должен сработать ДО таблицы, поэтому снимаем
|
||||||
|
// его с параграфа и выносим в отдельный пустой параграф-маркер.
|
||||||
|
bool hasPageBreakBefore = false;
|
||||||
|
if (paragraph.ParagraphProperties?.GetFirstChild<PageBreakBefore>() is { } pbb)
|
||||||
{
|
{
|
||||||
paraIndex = i;
|
hasPageBreakBefore = true;
|
||||||
break;
|
pbb.Remove();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paraIndex == -1)
|
int insertAt = paraIndex;
|
||||||
return;
|
if (hasPageBreakBefore)
|
||||||
|
{
|
||||||
|
var pbbPara = new Paragraph(new ParagraphProperties(new PageBreakBefore()));
|
||||||
|
parent.InsertAt(pbbPara, insertAt);
|
||||||
|
insertAt++;
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем старый параграф и вставляем таблицу на его место
|
// 4. Вставляем таблицу — параграф при этом сдвигается на +1.
|
||||||
parent.RemoveChild(paragraph);
|
// Ссылка на paragraph остаётся валидной.
|
||||||
parent.InsertAt(table, paraIndex);
|
parent.InsertAt(table, insertAt);
|
||||||
|
|
||||||
|
// 5. Чистим содержимое параграфа: pPr остаётся, текст — удаляется,
|
||||||
|
// run-ы с <w:br/> — сохраняются (с отрезанным текстом).
|
||||||
|
ClearParagraphContent(paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Очистка содержимого параграфа с сохранением «полезных» элементов.
|
||||||
|
// ============================================================
|
||||||
|
private static void ClearParagraphContent(Paragraph paragraph)
|
||||||
|
{
|
||||||
|
var toRemove = new List<OpenXmlElement>();
|
||||||
|
foreach (var child in paragraph.ChildElements)
|
||||||
|
{
|
||||||
|
if (child is ParagraphProperties) continue;
|
||||||
|
toRemove.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var child in toRemove)
|
||||||
|
{
|
||||||
|
// Run, содержащий page/column break — оставляем, но убираем из него весь текст.
|
||||||
|
if (child is Run run && run.Elements<Break>().Any())
|
||||||
|
{
|
||||||
|
foreach (var t in run.Elements<Text>().ToList()) t.Remove();
|
||||||
|
foreach (var other in run.ChildElements.ToList())
|
||||||
|
{
|
||||||
|
if (other is Break || other is RunProperties) continue;
|
||||||
|
other.Remove();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
child.Remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если pPr стал пустым — тоже удаляем, чтобы не оставлять мусор.
|
||||||
|
if (paragraph.ParagraphProperties is { } ppr && !ppr.ChildElements.Any())
|
||||||
|
ppr.Remove();
|
||||||
|
|
||||||
|
// Если параграф теперь полностью пуст (нет ни Runs, ни pPr) — убираем его.
|
||||||
|
if (!paragraph.ChildElements.Any())
|
||||||
|
paragraph.Remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,7 +113,7 @@ internal static class ReplaceToTableExt
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Извлекаем свойства шрифта для передачи в TableBuilder
|
// Извлекаем свойства шрифта для передачи в TableBuilder
|
||||||
var fontProps = ExtractFontPropsFromParagraph(paragraph, oldValue);
|
var fontProps = paragraph.ExtractFontProps(oldValue);
|
||||||
|
|
||||||
if (fontProps is { } props)
|
if (fontProps is { } props)
|
||||||
fontProps = props.Override(baseFont);
|
fontProps = props.Override(baseFont);
|
||||||
@@ -71,151 +122,4 @@ internal static class ReplaceToTableExt
|
|||||||
ReplaceWithTableBuilder(paragraph, buildTable, fontProps);
|
ReplaceWithTableBuilder(paragraph, buildTable, fontProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Извлекает свойства шрифта (FontProps) из Run, содержащего указанный текст в параграфе.
|
|
||||||
/// </summary>
|
|
||||||
private static FontProps? ExtractFontPropsFromParagraph(Paragraph paragraph, string searchText)
|
|
||||||
{
|
|
||||||
if (paragraph is null || string.IsNullOrEmpty(searchText))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var runs = paragraph.Descendants<Run>().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 CreateFontPropsFromRun(run);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если точное вхождение не найдено, ищем пересечение текста
|
|
||||||
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 CreateFontPropsFromRun(intersectingRun.Run);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Не удалось найти подходящий Run
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт FontProps на основе свойств указанного Run.
|
|
||||||
/// </summary>
|
|
||||||
private static FontProps CreateFontPropsFromRun(Run run)
|
|
||||||
{
|
|
||||||
var runProperties = run.RunProperties;
|
|
||||||
if (runProperties is null)
|
|
||||||
return new FontProps();
|
|
||||||
|
|
||||||
var fontProps = new FontProps();
|
|
||||||
|
|
||||||
// Font Family
|
|
||||||
var runFonts = runProperties.GetFirstChild<RunFonts>();
|
|
||||||
if (runFonts is not null)
|
|
||||||
{
|
|
||||||
fontProps = fontProps with
|
|
||||||
{
|
|
||||||
FontFamily = runFonts.Ascii ?? runFonts.HighAnsi ?? runFonts.ComplexScript
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font Size
|
|
||||||
var fontSize = runProperties.GetFirstChild<FontSize>();
|
|
||||||
if (fontSize is not null && !string.IsNullOrEmpty(fontSize.Val) &&
|
|
||||||
uint.TryParse(fontSize.Val, out uint halfPoints))
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { Size = halfPoints / 2.0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bold
|
|
||||||
var bold = runProperties.GetFirstChild<Bold>();
|
|
||||||
if (bold is not null)
|
|
||||||
{
|
|
||||||
bool isBold = true;
|
|
||||||
if (bold.Val is not null)
|
|
||||||
{
|
|
||||||
isBold = bold.Val.Value;
|
|
||||||
}
|
|
||||||
fontProps = fontProps with { IsBold = isBold };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Italic
|
|
||||||
var italic = runProperties.GetFirstChild<Italic>();
|
|
||||||
if (italic is not null)
|
|
||||||
{
|
|
||||||
bool isItalic = true;
|
|
||||||
if (italic.Val is not null)
|
|
||||||
{
|
|
||||||
isItalic = italic.Val.Value;
|
|
||||||
}
|
|
||||||
fontProps = fontProps with { IsItalic = isItalic };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Underline
|
|
||||||
var underline = runProperties.GetFirstChild<Underline>();
|
|
||||||
if (underline is not null && underline.Val is not null)
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { Underline = underline.Val.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color
|
|
||||||
var color = runProperties.GetFirstChild<Color>();
|
|
||||||
if (color is not null && !string.IsNullOrEmpty(color.Val))
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { Color = color.Val };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscript/Superscript
|
|
||||||
var verticalAlignment = runProperties.GetFirstChild<VerticalTextAlignment>();
|
|
||||||
if (verticalAlignment is not null && verticalAlignment.Val is not null)
|
|
||||||
{
|
|
||||||
fontProps = fontProps with { SubSup = verticalAlignment.Val.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return fontProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вспомогательный метод для получения текста из Run.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetRunText(Run run)
|
|
||||||
{
|
|
||||||
if (run is null) return string.Empty;
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (var text in run.Elements<Text>())
|
|
||||||
{
|
|
||||||
sb.Append(text.Text ?? string.Empty);
|
|
||||||
}
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user