This commit is contained in:
melekhin
2026-09-24 14:09:37 +07:00
parent ebec7f724c
commit fab3528e57
7 changed files with 357 additions and 280 deletions
+65 -161
View File
@@ -12,30 +12,81 @@ internal static class ReplaceToTableExt
var parent = paragraph.Parent;
// Создаем TableBuilder с базовым шрифтом
// 1. Строим таблицу
var builder = TableBuilder.Create(baseFont);
buildTable(builder);
var table = builder.Build();
// Находим индекс параграфа среди детей родителя
// 2. Находим индекс параграфа
int paraIndex = -1;
var children = parent.ChildElements;
for (int i = 0; i < children.Count; i++)
{
if (children[i] == paragraph)
{
paraIndex = i;
break;
}
if (children[i] == paragraph) { paraIndex = i; break; }
}
if (paraIndex == -1) return;
// 3. PageBreakBefore должен сработать ДО таблицы, поэтому снимаем
// его с параграфа и выносим в отдельный пустой параграф-маркер.
bool hasPageBreakBefore = false;
if (paragraph.ParagraphProperties?.GetFirstChild<PageBreakBefore>() is { } pbb)
{
hasPageBreakBefore = true;
pbb.Remove();
}
if (paraIndex == -1)
return;
int insertAt = paraIndex;
if (hasPageBreakBefore)
{
var pbbPara = new Paragraph(new ParagraphProperties(new PageBreakBefore()));
parent.InsertAt(pbbPara, insertAt);
insertAt++;
}
// Удаляем старый параграф и вставляем таблицу на его место
parent.RemoveChild(paragraph);
parent.InsertAt(table, paraIndex);
// 4. Вставляем таблицу — параграф при этом сдвигается на +1.
// Ссылка на paragraph остаётся валидной.
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>
@@ -62,7 +113,7 @@ internal static class ReplaceToTableExt
continue;
// Извлекаем свойства шрифта для передачи в TableBuilder
var fontProps = ExtractFontPropsFromParagraph(paragraph, oldValue);
var fontProps = paragraph.ExtractFontProps(oldValue);
if (fontProps is { } props)
fontProps = props.Override(baseFont);
@@ -71,151 +122,4 @@ internal static class ReplaceToTableExt
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();
}
}