Добавьте файлы проекта.
This commit is contained in:
48
QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs
Normal file
48
QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
internal static class CellAddressHelper
|
||||
{
|
||||
private const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
private static readonly uint[] _powers = [1, 26, 676];
|
||||
|
||||
public static uint ColumnLetterToIndex(string col)
|
||||
{
|
||||
uint index = 0;
|
||||
foreach (char ch in col)
|
||||
{
|
||||
char c = char.ToUpperInvariant(ch);
|
||||
if (c < 'A' || c > 'Z') return 0;
|
||||
index = index * 26 + (uint)(c - 'A' + 1);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public static string ColumnIndexToLetter(uint col)
|
||||
{
|
||||
if (col == 0) return "";
|
||||
uint n = col;
|
||||
char[] buf = new char[3];
|
||||
int idx = 0;
|
||||
while (n > 0)
|
||||
{
|
||||
n--;
|
||||
buf[idx++] = (char)('A' + (n % 26));
|
||||
n /= 26;
|
||||
}
|
||||
return new string(buf, 0, idx);
|
||||
}
|
||||
|
||||
public static bool TryParseCellReference(string reference, out uint row, out uint col)
|
||||
{
|
||||
row = 0; col = 0;
|
||||
if (string.IsNullOrEmpty(reference)) return false;
|
||||
int i = 0;
|
||||
while (i < reference.Length && char.IsLetter(reference[i])) i++;
|
||||
if (i == 0) return false;
|
||||
string colPart = reference.Substring(0, i);
|
||||
string rowPart = reference.Substring(i);
|
||||
if (!uint.TryParse(rowPart, out row)) return false;
|
||||
col = CellAddressHelper.ColumnLetterToIndex(colPart);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
223
QWERTYkez.ExcelProcessor/Editors/CellAlign.cs
Normal file
223
QWERTYkez.ExcelProcessor/Editors/CellAlign.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет выравнивание содержимого ячейки: горизонтальное, вертикальное, перенос текста и уменьшение по размеру.
|
||||
/// Все свойства опциональны. Если свойство не задано, соответствующий аспект не изменяется.
|
||||
/// </summary>
|
||||
public readonly struct CellAlign : IEquatable<CellAlign>
|
||||
{
|
||||
/// <summary>Горизонтальное выравнивание.</summary>
|
||||
public CellAlignHorizontal? Horizontal { get; init; }
|
||||
|
||||
/// <summary>Вертикальное выравнивание.</summary>
|
||||
public CellAlignVertical? Vertical { get; init; }
|
||||
|
||||
/// <summary>Переносить ли текст по словам (многострочный режим).</summary>
|
||||
public bool? WrapText { get; init; }
|
||||
|
||||
/// <summary>Уменьшать размер шрифта, чтобы текст поместился в ячейку.</summary>
|
||||
public bool? ShrinkToFit { get; init; }
|
||||
|
||||
/// <summary>Преобразует горизонтальное выравнивание в тип Open XML.</summary>
|
||||
public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value)
|
||||
{
|
||||
if (Horizontal.HasValue)
|
||||
{
|
||||
value = Horizontal.Value switch
|
||||
{
|
||||
CellAlignHorizontal.Left => HorizontalAlignmentValues.Left,
|
||||
CellAlignHorizontal.Center => HorizontalAlignmentValues.Center,
|
||||
CellAlignHorizontal.Right => HorizontalAlignmentValues.Right,
|
||||
CellAlignHorizontal.Fill => HorizontalAlignmentValues.Fill,
|
||||
CellAlignHorizontal.Justify => HorizontalAlignmentValues.Justify,
|
||||
CellAlignHorizontal.CenterContinuous => HorizontalAlignmentValues.CenterContinuous,
|
||||
CellAlignHorizontal.Distributed => HorizontalAlignmentValues.Distributed,
|
||||
_ => throw new NotSupportedException($"Unsupported horizontal alignment: {Horizontal.Value}")
|
||||
};
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Преобразует вертикальное выравнивание в тип Open XML.</summary>
|
||||
public bool TryGetExcelVerticalAlignment(out VerticalAlignmentValues value)
|
||||
{
|
||||
if (Vertical.HasValue)
|
||||
{
|
||||
value = Vertical.Value switch
|
||||
{
|
||||
CellAlignVertical.Top => VerticalAlignmentValues.Top,
|
||||
CellAlignVertical.Center => VerticalAlignmentValues.Center,
|
||||
CellAlignVertical.Bottom => VerticalAlignmentValues.Bottom,
|
||||
CellAlignVertical.Justify => VerticalAlignmentValues.Justify,
|
||||
CellAlignVertical.Distributed => VerticalAlignmentValues.Distributed,
|
||||
_ => throw new NotSupportedException($"Unsupported vertical alignment: {Vertical.Value}")
|
||||
};
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт элемент Alignment для Open XML.</summary>
|
||||
public Alignment? ToAlignment()
|
||||
{
|
||||
if (!Horizontal.HasValue && !Vertical.HasValue && !WrapText.HasValue && !ShrinkToFit.HasValue)
|
||||
return null;
|
||||
|
||||
var align = new Alignment();
|
||||
if (TryGetExcelHorizontalAlignment(out var hAlign))
|
||||
align.Horizontal = hAlign;
|
||||
if (TryGetExcelVerticalAlignment(out var vAlign))
|
||||
align.Vertical = vAlign;
|
||||
if (WrapText.HasValue)
|
||||
align.WrapText = WrapText.Value;
|
||||
if (ShrinkToFit.HasValue)
|
||||
align.ShrinkToFit = ShrinkToFit.Value;
|
||||
return align;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт CellAlign из элемента Alignment Open XML.</summary>
|
||||
public static CellAlign FromAlignment(Alignment? alignment)
|
||||
{
|
||||
if (alignment == null)
|
||||
return default;
|
||||
|
||||
var result = new CellAlign();
|
||||
if (alignment.Horizontal?.Value is { } horizontal)
|
||||
{
|
||||
result = result with { Horizontal = MapHorizontalFromExcel(horizontal) };
|
||||
}
|
||||
if (alignment.Vertical?.Value is { } vertical)
|
||||
{
|
||||
result = result with { Vertical = MapVerticalFromExcel(vertical) };
|
||||
}
|
||||
if (alignment.WrapText?.Value is { } wrapText)
|
||||
{
|
||||
result = result with { WrapText = wrapText };
|
||||
}
|
||||
if (alignment.ShrinkToFit?.Value is { } shrinkToFit)
|
||||
{
|
||||
result = result with { ShrinkToFit = shrinkToFit };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value)
|
||||
{
|
||||
if (value == HorizontalAlignmentValues.Left)
|
||||
{
|
||||
return CellAlignHorizontal.Left;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.Center)
|
||||
{
|
||||
return CellAlignHorizontal.Center;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.Right)
|
||||
{
|
||||
return CellAlignHorizontal.Right;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.Fill)
|
||||
{
|
||||
return CellAlignHorizontal.Fill;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.Justify)
|
||||
{
|
||||
return CellAlignHorizontal.Justify;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.CenterContinuous)
|
||||
{
|
||||
return CellAlignHorizontal.CenterContinuous;
|
||||
}
|
||||
else if (value == HorizontalAlignmentValues.Distributed)
|
||||
{
|
||||
return CellAlignHorizontal.Distributed;
|
||||
}
|
||||
else throw new NotSupportedException($"Unsupported horizontal alignment: {value}");
|
||||
}
|
||||
|
||||
private static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value)
|
||||
{
|
||||
if (value == VerticalAlignmentValues.Top)
|
||||
{
|
||||
return CellAlignVertical.Top;
|
||||
}
|
||||
else if (value == VerticalAlignmentValues.Center)
|
||||
{
|
||||
return CellAlignVertical.Center;
|
||||
}
|
||||
else if (value == VerticalAlignmentValues.Bottom)
|
||||
{
|
||||
return CellAlignVertical.Bottom;
|
||||
}
|
||||
else if (value == VerticalAlignmentValues.Justify)
|
||||
{
|
||||
return CellAlignVertical.Justify;
|
||||
}
|
||||
else if (value == VerticalAlignmentValues.Distributed)
|
||||
{
|
||||
return CellAlignVertical.Distributed;
|
||||
}
|
||||
else throw new NotSupportedException($"Unsupported vertical alignment: {value}");
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CellAlign other && Equals(other);
|
||||
public bool Equals(CellAlign other) => this == other;
|
||||
|
||||
public static bool operator ==(CellAlign left, CellAlign right) =>
|
||||
left.Horizontal == right.Horizontal &&
|
||||
left.Vertical == right.Vertical &&
|
||||
left.WrapText == right.WrapText &&
|
||||
left.ShrinkToFit == right.ShrinkToFit;
|
||||
|
||||
public static bool operator !=(CellAlign left, CellAlign right) => !(left == right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 31 + (Horizontal?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (Vertical?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (WrapText?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (ShrinkToFit?.GetHashCode() ?? 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Горизонтальное выравнивание содержимого ячейки.</summary>
|
||||
public enum CellAlignHorizontal
|
||||
{
|
||||
/// <summary>По левому краю.</summary>
|
||||
Left,
|
||||
/// <summary>По центру.</summary>
|
||||
Center,
|
||||
/// <summary>По правому краю.</summary>
|
||||
Right,
|
||||
/// <summary>Заполнение (повтор содержимого для заполнения ширины).</summary>
|
||||
Fill,
|
||||
/// <summary>По ширине (для многострочного текста).</summary>
|
||||
Justify,
|
||||
/// <summary>Центрирование по выделенным ячейкам (визуально, без объединения).</summary>
|
||||
CenterContinuous,
|
||||
/// <summary>Распределённый (выравнивание по ширине с пробелами).</summary>
|
||||
Distributed,
|
||||
}
|
||||
|
||||
/// <summary>Вертикальное выравнивание содержимого ячейки.</summary>
|
||||
public enum CellAlignVertical
|
||||
{
|
||||
/// <summary>По верхнему краю.</summary>
|
||||
Top,
|
||||
/// <summary>По центру.</summary>
|
||||
Center,
|
||||
/// <summary>По нижнему краю (значение по умолчанию).</summary>
|
||||
Bottom,
|
||||
/// <summary>По высоте (для многострочного текста).</summary>
|
||||
Justify,
|
||||
/// <summary>Распределённый по вертикали.</summary>
|
||||
Distributed,
|
||||
}
|
||||
281
QWERTYkez.ExcelProcessor/Editors/CellBorder.cs
Normal file
281
QWERTYkez.ExcelProcessor/Editors/CellBorder.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
|
||||
/// Каждая граница может иметь стиль и цвет.
|
||||
/// </summary>
|
||||
public readonly struct CellBorder : IEquatable<CellBorder>
|
||||
{
|
||||
/// <summary>Верхняя граница.</summary>
|
||||
public BorderSide? TopBorder { get; init; }
|
||||
|
||||
/// <summary>Нижняя граница.</summary>
|
||||
public BorderSide? BottomBorder { get; init; }
|
||||
|
||||
/// <summary>Левая граница.</summary>
|
||||
public BorderSide? LeftBorder { get; init; }
|
||||
|
||||
/// <summary>Правая граница.</summary>
|
||||
public BorderSide? RightBorder { get; init; }
|
||||
|
||||
/// <summary>Диагональная граница «из левого верхнего в правый нижний» (\\).</summary>
|
||||
public BorderSide? DiagonalLeft { get; init; }
|
||||
|
||||
/// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary>
|
||||
public BorderSide? DiagonalRight { get; init; }
|
||||
|
||||
/// <summary>Создаёт элемент Border для Open XML.</summary>
|
||||
public Border? ToBorder()
|
||||
{
|
||||
bool hasAny = TopBorder.HasValue || BottomBorder.HasValue || LeftBorder.HasValue ||
|
||||
RightBorder.HasValue || DiagonalLeft.HasValue || DiagonalRight.HasValue;
|
||||
if (!hasAny) return null;
|
||||
|
||||
var border = new Border();
|
||||
if (TopBorder.HasValue)
|
||||
border.TopBorder = TopBorder.Value.ToBorderElement<TopBorder>();
|
||||
if (BottomBorder.HasValue)
|
||||
border.BottomBorder = BottomBorder.Value.ToBorderElement<BottomBorder>();
|
||||
if (LeftBorder.HasValue)
|
||||
border.LeftBorder = LeftBorder.Value.ToBorderElement<LeftBorder>();
|
||||
if (RightBorder.HasValue)
|
||||
border.RightBorder = RightBorder.Value.ToBorderElement<RightBorder>();
|
||||
|
||||
// Обработка диагональных границ
|
||||
if (DiagonalLeft.HasValue || DiagonalRight.HasValue)
|
||||
{
|
||||
// Диагональ \\ (из левого верхнего в правый нижний) = DiagonalDown
|
||||
border.DiagonalDown = DiagonalLeft.HasValue;
|
||||
// Диагональ // (из левого нижнего в правый верхний) = DiagonalUp
|
||||
border.DiagonalUp = DiagonalRight.HasValue;
|
||||
|
||||
// Стиль и цвет берём из первой заданной диагонали (например, DiagonalLeft)
|
||||
var diagSide = (DiagonalLeft ?? DiagonalRight)!.Value;
|
||||
border.DiagonalBorder = diagSide.ToBorderElement<DiagonalBorder>();
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт CellBorder из элемента Border Open XML.</summary>
|
||||
public static CellBorder FromBorder(Border? border)
|
||||
{
|
||||
if (border == null)
|
||||
return default;
|
||||
|
||||
var result = new CellBorder
|
||||
{
|
||||
TopBorder = BorderSide.FromBorderProperties(border.TopBorder),
|
||||
BottomBorder = BorderSide.FromBorderProperties(border.BottomBorder),
|
||||
LeftBorder = BorderSide.FromBorderProperties(border.LeftBorder),
|
||||
RightBorder = BorderSide.FromBorderProperties(border.RightBorder)
|
||||
};
|
||||
|
||||
// Диагональные границы
|
||||
if (border.DiagonalDown?.Value == true)
|
||||
result = result with { DiagonalLeft = BorderSide.FromBorderProperties(border.DiagonalBorder) };
|
||||
if (border.DiagonalUp?.Value == true)
|
||||
result = result with { DiagonalRight = BorderSide.FromBorderProperties(border.DiagonalBorder) };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CellBorder other && Equals(other);
|
||||
public bool Equals(CellBorder other) => this == other;
|
||||
|
||||
public static bool operator ==(CellBorder left, CellBorder right) =>
|
||||
Equals(left.TopBorder, right.TopBorder) &&
|
||||
Equals(left.BottomBorder, right.BottomBorder) &&
|
||||
Equals(left.LeftBorder, right.LeftBorder) &&
|
||||
Equals(left.RightBorder, right.RightBorder) &&
|
||||
Equals(left.DiagonalLeft, right.DiagonalLeft) &&
|
||||
Equals(left.DiagonalRight, right.DiagonalRight);
|
||||
|
||||
public static bool operator !=(CellBorder left, CellBorder right) => !(left == right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 31 + (TopBorder?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (BottomBorder?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (LeftBorder?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (RightBorder?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (DiagonalLeft?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (DiagonalRight?.GetHashCode() ?? 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Стиль и цвет границы.</summary>
|
||||
public readonly struct BorderSide : IEquatable<BorderSide>
|
||||
{
|
||||
/// <summary>Стиль линии границы.</summary>
|
||||
public BorderStyle? Style { get; init; }
|
||||
|
||||
/// <summary>Цвет границы.</summary>
|
||||
public ExColor? Color { get; init; }
|
||||
|
||||
internal T ToBorderElement<T>() where T : BorderPropertiesType, new()
|
||||
{
|
||||
var element = new T();
|
||||
if (Style.HasValue)
|
||||
{
|
||||
element.Style = Style.Value switch
|
||||
{
|
||||
BorderStyle.Thin => BorderStyleValues.Thin,
|
||||
BorderStyle.Medium => BorderStyleValues.Medium,
|
||||
BorderStyle.Dashed => BorderStyleValues.Dashed,
|
||||
BorderStyle.Dotted => BorderStyleValues.Dotted,
|
||||
BorderStyle.Thick => BorderStyleValues.Thick,
|
||||
BorderStyle.Double => BorderStyleValues.Double,
|
||||
BorderStyle.Hair => BorderStyleValues.Hair,
|
||||
BorderStyle.MediumDashed => BorderStyleValues.MediumDashed,
|
||||
BorderStyle.DashDot => BorderStyleValues.DashDot,
|
||||
BorderStyle.MediumDashDot => BorderStyleValues.MediumDashDot,
|
||||
BorderStyle.DashDotDot => BorderStyleValues.DashDotDot,
|
||||
BorderStyle.MediumDashDotDot => BorderStyleValues.MediumDashDotDot,
|
||||
BorderStyle.SlantDashDot => BorderStyleValues.SlantDashDot,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
if (Color.HasValue && Color.Value.Color.HasValue)
|
||||
{
|
||||
var c = Color.Value.Color.Value;
|
||||
element.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт BorderSide из элемента границы Open XML (TopBorder, BottomBorder, LeftBorder, RightBorder, DiagonalBorder).</summary>
|
||||
public static BorderSide FromBorderProperties(BorderPropertiesType? borderElement)
|
||||
{
|
||||
if (borderElement is null)
|
||||
return default;
|
||||
|
||||
var result = new BorderSide();
|
||||
if (borderElement.Style is not null)
|
||||
{
|
||||
result = result with { Style = MapBorderStyleFromExcel(borderElement.Style.Value) };
|
||||
}
|
||||
if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
|
||||
{
|
||||
var color = System.Drawing.Color.FromArgb(
|
||||
Convert.ToByte(rgb.Substring(0, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(2, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(4, 2), 16)
|
||||
);
|
||||
result = result with { Color = new ExColor(color) };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value)
|
||||
{
|
||||
if (value == BorderStyleValues.Thin)
|
||||
{
|
||||
return BorderStyle.Thin;
|
||||
}
|
||||
else if (value == BorderStyleValues.Medium)
|
||||
{
|
||||
return BorderStyle.Medium;
|
||||
}
|
||||
else if (value == BorderStyleValues.Dashed)
|
||||
{
|
||||
return BorderStyle.Dashed;
|
||||
}
|
||||
else if (value == BorderStyleValues.Dotted)
|
||||
{
|
||||
return BorderStyle.Dotted;
|
||||
}
|
||||
else if (value == BorderStyleValues.Thick)
|
||||
{
|
||||
return BorderStyle.Thick;
|
||||
}
|
||||
else if (value == BorderStyleValues.Double)
|
||||
{
|
||||
return BorderStyle.Double;
|
||||
}
|
||||
else if (value == BorderStyleValues.Hair)
|
||||
{
|
||||
return BorderStyle.Hair;
|
||||
}
|
||||
else if (value == BorderStyleValues.MediumDashed)
|
||||
{
|
||||
return BorderStyle.MediumDashed;
|
||||
}
|
||||
else if (value == BorderStyleValues.DashDot)
|
||||
{
|
||||
return BorderStyle.DashDot;
|
||||
}
|
||||
else if (value == BorderStyleValues.MediumDashDot)
|
||||
{
|
||||
return BorderStyle.MediumDashDot;
|
||||
}
|
||||
else if (value == BorderStyleValues.DashDotDot)
|
||||
{
|
||||
return BorderStyle.DashDotDot;
|
||||
}
|
||||
else if (value == BorderStyleValues.MediumDashDotDot)
|
||||
{
|
||||
return BorderStyle.MediumDashDotDot;
|
||||
}
|
||||
else if (value == BorderStyleValues.SlantDashDot)
|
||||
{
|
||||
return BorderStyle.SlantDashDot;
|
||||
}
|
||||
else throw new NotSupportedException($"Unsupported border style: {value}");
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is BorderSide other && Equals(other);
|
||||
public bool Equals(BorderSide other) => this == other;
|
||||
|
||||
public static bool operator ==(BorderSide left, BorderSide right) =>
|
||||
left.Style == right.Style && Equals(left.Color, right.Color);
|
||||
|
||||
public static bool operator !=(BorderSide left, BorderSide right) => !(left == right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 31 + Style.GetHashCode();
|
||||
hash = hash * 31 + (Color?.GetHashCode() ?? 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Тип линии границы</summary>
|
||||
public enum BorderStyle
|
||||
{
|
||||
/// <summary> Тонкая линия </summary>
|
||||
Thin,
|
||||
/// <summary> Средняя линия </summary>
|
||||
Medium,
|
||||
/// <summary> Штриховая </summary>
|
||||
Dashed,
|
||||
/// <summary> Точечная </summary>
|
||||
Dotted,
|
||||
/// <summary> Толстая линия </summary>
|
||||
Thick,
|
||||
/// <summary> Двойная линия </summary>
|
||||
Double,
|
||||
/// <summary> Волосяная (очень тонкая) </summary>
|
||||
Hair,
|
||||
/// <summary> Средняя штриховая </summary>
|
||||
MediumDashed,
|
||||
/// <summary> Штрих-пунктирная </summary>
|
||||
DashDot,
|
||||
/// <summary> Средняя штрих-пунктирная </summary>
|
||||
MediumDashDot,
|
||||
/// <summary> Штрих-пунктир-пунктир </summary>
|
||||
DashDotDot,
|
||||
/// <summary> Средняя штрих-пунктир-пунктир </summary>
|
||||
MediumDashDotDot,
|
||||
/// <summary> Наклонная штрих-пунктирная (для диагональных) </summary>
|
||||
SlantDashDot,
|
||||
}
|
||||
53
QWERTYkez.ExcelProcessor/Editors/CellFill.cs
Normal file
53
QWERTYkez.ExcelProcessor/Editors/CellFill.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет заливку (фон) ячейки.
|
||||
/// </summary>
|
||||
public readonly struct CellFill : IEquatable<CellFill>
|
||||
{
|
||||
/// <summary>Цвет фона.</summary>
|
||||
public ExColor? BackgroundColor { get; init; }
|
||||
|
||||
/// <summary>Создаёт элемент Fill для Open XML.</summary>
|
||||
public Fill? ToFill()
|
||||
{
|
||||
if (!BackgroundColor.HasValue || !BackgroundColor.Value.Color.HasValue)
|
||||
return null;
|
||||
|
||||
var c = BackgroundColor.Value.Color.Value;
|
||||
var fill = new Fill
|
||||
{
|
||||
PatternFill = new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.Solid,
|
||||
ForegroundColor = new ForegroundColor { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" }
|
||||
}
|
||||
};
|
||||
return fill;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт CellFill из элемента Fill Open XML.</summary>
|
||||
public static CellFill FromFill(Fill? fill)
|
||||
{
|
||||
if (fill?.PatternFill?.ForegroundColor?.Rgb?.Value is not string rgb || rgb.Length < 6)
|
||||
return default;
|
||||
|
||||
var color = System.Drawing.Color.FromArgb(
|
||||
Convert.ToByte(rgb.Substring(0, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(2, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(4, 2), 16)
|
||||
);
|
||||
return new CellFill { BackgroundColor = new ExColor(color) };
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CellFill other && Equals(other);
|
||||
public bool Equals(CellFill other) => this == other;
|
||||
|
||||
public static bool operator ==(CellFill left, CellFill right) =>
|
||||
Equals(left.BackgroundColor, right.BackgroundColor);
|
||||
|
||||
public static bool operator !=(CellFill left, CellFill right) => !(left == right);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
BackgroundColor?.GetHashCode() ?? 0;
|
||||
}
|
||||
112
QWERTYkez.ExcelProcessor/Editors/CellFont.cs
Normal file
112
QWERTYkez.ExcelProcessor/Editors/CellFont.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет шрифтовое оформление всей ячейки: размер, семейство, цвет, начертание.
|
||||
/// Все свойства опциональны.
|
||||
/// </summary>
|
||||
public readonly struct CellFont : IEquatable<CellFont>
|
||||
{
|
||||
/// <summary>Размер шрифта в пунктах (например, 11).</summary>
|
||||
public double? FontSize { get; init; }
|
||||
|
||||
/// <summary>Имя семейства шрифта (например, "Calibri", "Arial").</summary>
|
||||
public string? FontFamily { get; init; }
|
||||
|
||||
/// <summary>Цвет текста.</summary>
|
||||
public ExColor? FontColor { get; init; }
|
||||
|
||||
/// <summary>Жирное начертание.</summary>
|
||||
public bool? IsBold { get; init; }
|
||||
|
||||
/// <summary>Курсив.</summary>
|
||||
public bool? IsItalic { get; init; }
|
||||
|
||||
/// <summary>Подчёркивание (одинарное).</summary>
|
||||
public bool? IsUnderline { get; init; }
|
||||
|
||||
/// <summary>Зачёркивание.</summary>
|
||||
public bool? IsStrike { get; init; }
|
||||
|
||||
/// <summary>Создаёт элемент Font для Open XML.</summary>
|
||||
public Font? ToFont()
|
||||
{
|
||||
if (!FontSize.HasValue && FontFamily is null && FontColor is null &&
|
||||
!IsBold.HasValue && !IsItalic.HasValue && !IsUnderline.HasValue && !IsStrike.HasValue)
|
||||
return null;
|
||||
|
||||
var font = new Font();
|
||||
if (FontSize.HasValue)
|
||||
font.FontSize = new FontSize { Val = FontSize.Value };
|
||||
if (FontFamily is not null)
|
||||
font.FontName = new FontName { Val = FontFamily };
|
||||
if (FontColor.HasValue && FontColor.Value.Color.HasValue)
|
||||
{
|
||||
var c = FontColor.Value.Color.Value;
|
||||
font.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
|
||||
}
|
||||
if (IsBold == true) font.Bold = new Bold();
|
||||
if (IsItalic == true) font.Italic = new Italic();
|
||||
if (IsUnderline == true) font.Underline = new Underline();
|
||||
if (IsStrike == true) font.Strike = new Strike();
|
||||
return font;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт CellFont из элемента Font Open XML.</summary>
|
||||
public static CellFont FromFont(Font? font)
|
||||
{
|
||||
if (font == null)
|
||||
return default;
|
||||
|
||||
var result = new CellFont
|
||||
{
|
||||
IsBold = font.Bold != null,
|
||||
IsItalic = font.Italic != null,
|
||||
IsUnderline = font.Underline != null,
|
||||
IsStrike = font.Strike != null
|
||||
};
|
||||
if (font.FontSize?.Val?.Value is { } size)
|
||||
result = result with { FontSize = size };
|
||||
if (font.FontName?.Val?.Value is { } name)
|
||||
result = result with { FontFamily = name };
|
||||
if (font.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
|
||||
{
|
||||
var color = System.Drawing.Color.FromArgb(
|
||||
Convert.ToByte(rgb.Substring(0, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(2, 2), 16),
|
||||
Convert.ToByte(rgb.Substring(4, 2), 16)
|
||||
);
|
||||
result = result with { FontColor = new ExColor(color) };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CellFont other && Equals(other);
|
||||
public bool Equals(CellFont other) => this == other;
|
||||
|
||||
public static bool operator ==(CellFont left, CellFont right) =>
|
||||
left.FontSize == right.FontSize &&
|
||||
left.FontFamily == right.FontFamily &&
|
||||
Equals(left.FontColor, right.FontColor) &&
|
||||
left.IsBold == right.IsBold &&
|
||||
left.IsItalic == right.IsItalic &&
|
||||
left.IsUnderline == right.IsUnderline &&
|
||||
left.IsStrike == right.IsStrike;
|
||||
|
||||
public static bool operator !=(CellFont left, CellFont right) => !(left == right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 31 + (FontSize?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (FontFamily?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (FontColor?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (IsBold?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (IsItalic?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (IsUnderline?.GetHashCode() ?? 0);
|
||||
hash = hash * 31 + (IsStrike?.GetHashCode() ?? 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs
Normal file
63
QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors
|
||||
{
|
||||
/// <summary>
|
||||
/// Представляет ширину столбца в Excel. Поддерживает задание в символах, пунктах, сантиметрах и миллиметрах.
|
||||
/// Для методов FromCM/FromMM/FromPoints требуется калибровка через <see cref="ExcelWriter"/>, иначе точность не гарантирована.
|
||||
/// </summary>
|
||||
public readonly struct ColumnWidth
|
||||
{
|
||||
private readonly double _rawValue;
|
||||
private readonly UnitType _unit;
|
||||
|
||||
private enum UnitType { Characters, Points, Centimeters, Millimeters }
|
||||
|
||||
/// <summary>Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).</summary>
|
||||
public static double DefaultPointsPerChar { get; set; } = 5.65;
|
||||
|
||||
private ColumnWidth(double value, UnitType unit)
|
||||
{
|
||||
_rawValue = value;
|
||||
_unit = unit;
|
||||
}
|
||||
|
||||
/// <summary>Задаёт ширину в character units (символах стандартного шрифта).</summary>
|
||||
public static ColumnWidth FromCharacters(double characters) => new(characters, UnitType.Characters);
|
||||
|
||||
/// <summary>Задаёт ширину в пунктах.</summary>
|
||||
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
|
||||
public static ColumnWidth FromPoints(double points) => new(points, UnitType.Points);
|
||||
|
||||
/// <summary>Задаёт ширину в сантиметрах.</summary>
|
||||
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
|
||||
public static ColumnWidth FromCM(double cm) => new(cm, UnitType.Centimeters);
|
||||
|
||||
/// <summary>Задаёт ширину в миллиметрах.</summary>
|
||||
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
|
||||
public static ColumnWidth FromMM(double mm) => new(mm, UnitType.Millimeters);
|
||||
|
||||
/// <summary>Возвращает целевую ширину в пунктах (если ширина задана в пунктах, см или мм).</summary>
|
||||
internal double GetTargetPoints()
|
||||
{
|
||||
return _unit switch
|
||||
{
|
||||
UnitType.Points => _rawValue,
|
||||
UnitType.Centimeters => _rawValue * 28.3464566929,
|
||||
UnitType.Millimeters => (_rawValue / 10.0) * 28.3464566929,
|
||||
_ => double.NaN
|
||||
};
|
||||
}
|
||||
internal double GetTargetCentimeters()
|
||||
{
|
||||
return _unit switch
|
||||
{
|
||||
UnitType.Centimeters => _rawValue,
|
||||
UnitType.Millimeters => _rawValue / 10.0,
|
||||
UnitType.Points => _rawValue / 28.3464566929,
|
||||
_ => double.NaN
|
||||
};
|
||||
}
|
||||
|
||||
internal bool IsCharacterUnit => _unit == UnitType.Characters;
|
||||
internal double CharacterValue => IsCharacterUnit ? _rawValue : double.NaN;
|
||||
}
|
||||
}
|
||||
62
QWERTYkez.ExcelProcessor/Editors/ExColor.cs
Normal file
62
QWERTYkez.ExcelProcessor/Editors/ExColor.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
public readonly struct ExColor(System.Drawing.Color? Color)
|
||||
{
|
||||
private readonly System.Drawing.Color? color = Color;
|
||||
|
||||
/// <summary>Проверяет, является ли цвет автоматическим (т.е. Color == null).</summary>
|
||||
public bool IsAuto => color is null;
|
||||
|
||||
public System.Drawing.Color? Color => color;
|
||||
|
||||
public static ExColor FromArgb(byte r, byte g, byte b) => new(System.Drawing.Color.FromArgb(r, g, b));
|
||||
public static ExColor FromName(string knownColor) => new(System.Drawing.Color.FromName(knownColor));
|
||||
|
||||
|
||||
|
||||
public static implicit operator ExColor(Color exColor) => FromExcel(exColor);
|
||||
public static implicit operator Color(ExColor color) => color.ToExcel();
|
||||
public static ExColor FromExcel(Color excelColor)
|
||||
{
|
||||
if (excelColor == null)
|
||||
return new ExColor(null);
|
||||
|
||||
// Если цвет автоматический
|
||||
if (excelColor.Auto != null && excelColor.Auto.Value)
|
||||
return new ExColor(null);
|
||||
|
||||
// Если задан RGB
|
||||
if (excelColor.Rgb?.Value is { } rgb && rgb.Length > 0)
|
||||
{
|
||||
if (rgb.Length == 6) // RRGGBB
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
else if (rgb.Length == 8) // AARRGGBB (альфа игнорируется)
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
// По умолчанию — автоматический цвет
|
||||
return new ExColor(null);
|
||||
}
|
||||
public Color ToExcel()
|
||||
{
|
||||
var excelColor = new Color();
|
||||
{
|
||||
if (color.HasValue)
|
||||
{
|
||||
excelColor.Rgb = $"{color.Value.R:X2}{color.Value.G:X2}{color.Value.B:X2}";
|
||||
}
|
||||
else excelColor.Auto = true;
|
||||
}
|
||||
return excelColor;
|
||||
}
|
||||
}
|
||||
43
QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs
Normal file
43
QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="IBook"/>, которая делегирует вызовы к <see cref="ExcelWriter"/>.
|
||||
/// Используется в ситуациях, когда требуется отдельный объект книги (например, в <see cref="IExcelWriter.Edit"/>).
|
||||
/// </summary>
|
||||
internal sealed class ExcelBook : IBook
|
||||
{
|
||||
internal readonly ExcelWriter Writer;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует обёртку книги.
|
||||
/// </summary>
|
||||
/// <param name="writer">Реальный процессор Excel (реализует IBook).</param>
|
||||
internal ExcelBook(ExcelWriter writer)
|
||||
{
|
||||
Writer = writer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ISheet> GetSheets() => Writer.GetSheets();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISheet? Sheet(string name) => Writer.Sheet(name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name, out sheet);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name, edit);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<NumberFormatPattern> GetNumberFormats() => Writer.GetNumberFormats();
|
||||
|
||||
/// <inheritdoc />
|
||||
public NumberFormatPattern CreateNumberFormat(string format) => Writer.CreateNumberFormat(format);
|
||||
}
|
||||
742
QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
Normal file
742
QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
Normal file
@@ -0,0 +1,742 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="ICell"/>.
|
||||
/// </summary>
|
||||
internal sealed class ExcelCell : ICell
|
||||
{
|
||||
private readonly ExcelWriter _writer;
|
||||
private readonly ExcelSheet _sheet;
|
||||
private uint _row;
|
||||
private uint _col;
|
||||
|
||||
internal ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col)
|
||||
{
|
||||
_writer = writer;
|
||||
_sheet = sheet;
|
||||
_row = row;
|
||||
_col = col;
|
||||
}
|
||||
|
||||
public bool IsMerged
|
||||
{
|
||||
get
|
||||
{
|
||||
var range = GetMergedRange();
|
||||
return range != null;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange? GetMergedRange()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var mergeCells = GetMergeCells();
|
||||
if (mergeCells == null) return null;
|
||||
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
|
||||
{
|
||||
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var range))
|
||||
{
|
||||
if (_row >= range.RowStart && _row <= range.RowEnd &&
|
||||
_col >= range.ColStart && _col <= range.ColEnd)
|
||||
return range;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetMergedRange(IRange range)
|
||||
{
|
||||
var merged = GetMergedRange();
|
||||
if (merged == null) return false;
|
||||
return merged.Equals(range);
|
||||
}
|
||||
|
||||
public uint Row => _row;
|
||||
public uint Col => _col;
|
||||
public string ColLetter => CellAddressHelper.ColumnIndexToLetter(_col);
|
||||
|
||||
public ICell MoveTo(uint rowIndex, uint colIndex)
|
||||
{
|
||||
if (rowIndex == _row && colIndex == _col) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
// Вырезать-вставить: копируем данные в новую позицию, очищаем старую
|
||||
var srcCell = GetCellElement();
|
||||
if (srcCell != null)
|
||||
{
|
||||
var cloned = (Cell)srcCell.CloneNode(true);
|
||||
InsertCellAt(rowIndex, colIndex, cloned);
|
||||
srcCell.Remove();
|
||||
}
|
||||
_row = rowIndex;
|
||||
_col = colIndex;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell MoveTo(uint rowIndex, string colIndex) => MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
|
||||
|
||||
public RowHeight Height
|
||||
{
|
||||
get => ExcelRow.GetHeight(_writer, _sheet, _row);
|
||||
set => ExcelRow.SetHeight(_writer, _sheet, _row, value);
|
||||
}
|
||||
|
||||
public ColumnWidth Width
|
||||
{
|
||||
get => ExcelColumn.GetWidth(_writer, _sheet, _col);
|
||||
set => ExcelColumn.SetWidth(_writer, _sheet, _col, value);
|
||||
}
|
||||
|
||||
public bool IsNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell == null) return false;
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.Number) return true;
|
||||
// Если тип не указан, но значение число – Excel считает числом
|
||||
if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, out _)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBoolean
|
||||
{
|
||||
get
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Boolean;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsError
|
||||
{
|
||||
get
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsNumber) return false;
|
||||
var format = GetNumberFormat();
|
||||
if (format == null) return false;
|
||||
|
||||
// Встроенный формат: проверяем по ID
|
||||
if (format.Id.HasValue && format.Id.Value < 164)
|
||||
{
|
||||
int id = format.Id.Value;
|
||||
return (id >= 14 && id <= 22) || // даты
|
||||
(id >= 27 && id <= 36) || // время и дата-время
|
||||
(id >= 45 && id <= 47); // другие форматы времени
|
||||
}
|
||||
else
|
||||
{
|
||||
// Пользовательский формат: ищем символы даты/времени
|
||||
string code = format.Format?.ToLowerInvariant() ?? string.Empty;
|
||||
bool isDate = code.Contains('d') && code.Contains('m') && code.Contains('y');
|
||||
bool isTime = code.Contains('h') && code.Contains('m') && code.Contains('s');
|
||||
return isDate || isTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLocked
|
||||
{
|
||||
get
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return false;
|
||||
return _writer.IsCellLocked(styleIndex);
|
||||
}
|
||||
}
|
||||
public bool HasFormula => GetCellElement()?.CellFormula != null;
|
||||
|
||||
public string GetString()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell == null) return string.Empty;
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
|
||||
{
|
||||
// Общая таблица строк
|
||||
if (uint.TryParse(cell.CellValue?.Text, out uint idx))
|
||||
return GetSharedString(idx);
|
||||
return string.Empty;
|
||||
}
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.InlineString)
|
||||
{
|
||||
// Rich text – извлекаем весь текст
|
||||
return ExtractTextFromInlineString(cell.InlineString);
|
||||
}
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
|
||||
return cell.CellValue?.Text ?? "FALSE";
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.Error)
|
||||
return cell.CellValue?.Text ?? "#NULL!";
|
||||
// Число или общий тип
|
||||
return cell.CellValue?.Text ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NumberFormatPattern? GetNumberFormat()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return null;
|
||||
return _writer.GetNumberFormat(styleIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CellAlign GetCellAlign()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return default;
|
||||
return _writer.GetCellAlign(styleIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CellBorder GetCellBorder()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return default;
|
||||
return _writer.GetCellBorder(styleIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CellFill GetCellFill()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return default;
|
||||
return _writer.GetCellFill(styleIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CellFont GetCellFont()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
||||
return default;
|
||||
return _writer.GetCellFont(styleIndex);
|
||||
}
|
||||
|
||||
public bool TryGetBoolean(out bool value)
|
||||
{
|
||||
value = false;
|
||||
var cell = GetCellElement();
|
||||
if (cell == null) return false;
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
|
||||
{
|
||||
string txt = cell.CellValue?.Text ?? "0";
|
||||
value = txt == "1" || txt.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool? GetBoolean() => TryGetBoolean(out bool v) ? v : null;
|
||||
|
||||
public bool TryGetDate(out DateTime value)
|
||||
{
|
||||
value = default;
|
||||
if (!IsNumber) return false;
|
||||
if (TryGetNumber(out double num))
|
||||
{
|
||||
// Excel даты: 1 января 1900 = 1, 1 января 1904 = 0 в 1904 системе
|
||||
// Предполагаем 1900 систему
|
||||
value = DateTime.FromOADate(num);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public DateTime? TryGetDate() => TryGetDate(out DateTime d) ? d : null;
|
||||
|
||||
public bool TryGetNumber(out double value)
|
||||
{
|
||||
value = 0;
|
||||
var cell = GetCellElement();
|
||||
if (cell == null) return false;
|
||||
if (cell.DataType != null && cell.DataType.Value == CellValues.Number)
|
||||
{
|
||||
if (cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
|
||||
return true;
|
||||
}
|
||||
else if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public double? TryGetNumber() => TryGetNumber(out double v) ? v : null;
|
||||
|
||||
public bool TrySet(string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(formula)) return false;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
cell.CellFormula = new CellFormula(formula);
|
||||
cell.DataType = null; // формула сама определяет тип
|
||||
if (format != null)
|
||||
SetNumberFormatInternal(cell, format);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ICell Set(string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (!TrySet(formula, format))
|
||||
throw new InvalidOperationException("Failed to set formula");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Set(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
SetNumberFormatInternal(cell, format);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: align
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: border,
|
||||
align: null
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: fill,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: font,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public ICell Set(Action<ICellText> value)
|
||||
{
|
||||
if (value == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
var textObj = new ExcelCellText();
|
||||
value(textObj);
|
||||
cell.InlineString = BuildInlineString(textObj);
|
||||
cell.DataType = CellValues.InlineString;
|
||||
cell.CellValue = null;
|
||||
cell.CellFormula = null;
|
||||
|
||||
// Если есть разрыв строки (символ \n), включаем перенос текста для ячейки
|
||||
bool hasNewline = textObj.GetRuns().Any(r => r.Text != null && r.Text.Contains('\n'));
|
||||
if (hasNewline)
|
||||
{
|
||||
var currentAlign = GetCellAlign();
|
||||
if (currentAlign.WrapText != true)
|
||||
{
|
||||
var newAlign = new CellAlign
|
||||
{
|
||||
Horizontal = currentAlign.Horizontal,
|
||||
Vertical = currentAlign.Vertical,
|
||||
WrapText = true,
|
||||
ShrinkToFit = currentAlign.ShrinkToFit
|
||||
};
|
||||
Set(newAlign);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Set(string value)
|
||||
{
|
||||
if (value == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
// Проверяем, можно ли сохранить как число?
|
||||
if (double.TryParse(value, out double num))
|
||||
{
|
||||
cell.DataType = CellValues.Number;
|
||||
cell.CellValue = new CellValue(num.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Используем общую таблицу строк
|
||||
int idx = GetOrAddSharedString(value);
|
||||
cell.DataType = CellValues.SharedString;
|
||||
cell.CellValue = new CellValue(idx.ToString());
|
||||
}
|
||||
cell.InlineString = null;
|
||||
cell.CellFormula = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Set(bool value)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
cell.DataType = CellValues.Boolean;
|
||||
cell.CellValue = new CellValue(value ? "1" : "0");
|
||||
cell.InlineString = null;
|
||||
cell.CellFormula = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Set(DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
double oa = value.ToOADate();
|
||||
Set(oa, format);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Set(decimal value, NumberFormatPattern? format = null) => Set((double)value, format);
|
||||
public ICell Set(double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
cell.DataType = CellValues.Number;
|
||||
cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture));
|
||||
if (format != null)
|
||||
SetNumberFormatInternal(cell, format);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public ICell Set(float value, NumberFormatPattern? format = null) => Set((double)value, format);
|
||||
public ICell Set(int value, NumberFormatPattern? format = null) => Set((double)value, format);
|
||||
public ICell Set(long value, NumberFormatPattern? format = null) => Set((double)value, format);
|
||||
|
||||
public void ClearContent()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
if (cell != null)
|
||||
{
|
||||
cell.CellValue = null;
|
||||
cell.CellFormula = null;
|
||||
cell.InlineString = null;
|
||||
cell.DataType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearFormat()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
cell?.StyleIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearContent();
|
||||
ClearFormat();
|
||||
}
|
||||
|
||||
public ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var srcCell = GetCellElement();
|
||||
if (srcCell != null)
|
||||
{
|
||||
var cloned = (Cell)srcCell.CloneNode(true);
|
||||
InsertCellAt(rowIndex, colIndex, cloned);
|
||||
copiedCell = new ExcelCell(_writer, _sheet, rowIndex, colIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
copiedCell = new ExcelCell(_writer, _sheet, rowIndex, colIndex);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) =>
|
||||
CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell);
|
||||
|
||||
// Private helpers
|
||||
|
||||
private Cell? GetCellElement()
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var row = FindRowElement(sheetData, _row);
|
||||
if (row == null) return null;
|
||||
return FindCellInRow(row, _col);
|
||||
}
|
||||
|
||||
private Cell GetOrCreateCellElement()
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var row = GetOrCreateRowElement(sheetData, _row);
|
||||
var cell = FindCellInRow(row, _col);
|
||||
if (cell != null) return cell;
|
||||
cell = new Cell();
|
||||
string cellRef = CellAddressHelper.ColumnIndexToLetter(_col) + _row.ToString();
|
||||
cell.CellReference = cellRef;
|
||||
InsertCellInRow(row, cell, _col);
|
||||
return cell;
|
||||
}
|
||||
|
||||
private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
|
||||
{
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
if (row.RowIndex?.Value == rowIndex) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
|
||||
{
|
||||
var existing = FindRowElement(sheetData, rowIndex);
|
||||
if (existing != null) return existing;
|
||||
var newRow = new Row { RowIndex = rowIndex };
|
||||
InsertRowElement(sheetData, newRow, rowIndex);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
|
||||
{
|
||||
bool inserted = false;
|
||||
foreach (var existing in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
if (existing.RowIndex?.Value > rowIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(row);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) sheetData.Append(row);
|
||||
}
|
||||
|
||||
private static Cell? FindCellInRow(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
||||
{
|
||||
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
|
||||
cell.CellReference = newRef;
|
||||
bool inserted = false;
|
||||
foreach (var existing in row.Elements<Cell>().ToList())
|
||||
{
|
||||
if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) && existingCol > colIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(cell);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) row.Append(cell);
|
||||
}
|
||||
|
||||
private void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var row = GetOrCreateRowElement(sheetData, rowIndex);
|
||||
InsertCellInRow(row, cell, colIndex);
|
||||
}
|
||||
|
||||
private void SetNumberFormatInternal(Cell cell, NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return;
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: format,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
}
|
||||
|
||||
private string GetSharedString(uint index)
|
||||
{
|
||||
return _writer.GetSharedString(index);
|
||||
}
|
||||
|
||||
private int GetOrAddSharedString(string value)
|
||||
{
|
||||
return _writer.GetOrAddSharedString(value);
|
||||
}
|
||||
|
||||
private string ExtractTextFromInlineString(InlineString? inlineString)
|
||||
{
|
||||
if (inlineString == null) return string.Empty;
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var run in inlineString.Elements<Run>())
|
||||
{
|
||||
var text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
|
||||
sb.Append(text);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private InlineString BuildInlineString(ExcelCellText textObj)
|
||||
{
|
||||
var inline = new InlineString();
|
||||
foreach (var run in textObj.GetRuns())
|
||||
{
|
||||
var runElement = new Run();
|
||||
// Всегда создаём элемент Text, даже если строка состоит из "\n"
|
||||
runElement.Append(new Text(run.Text));
|
||||
|
||||
if (run.Format is { } fmt)
|
||||
{
|
||||
var rPr = new RunProperties();
|
||||
if (fmt.IsBold == true) rPr.Append(new Bold());
|
||||
if (fmt.IsItalic == true) rPr.Append(new Italic());
|
||||
if (fmt.Underline.HasValue)
|
||||
{
|
||||
rPr.Append(new Underline
|
||||
{
|
||||
Val = fmt.Underline.Value switch
|
||||
{
|
||||
UnderlineStyle.Single => UnderlineValues.Single,
|
||||
UnderlineStyle.Double => UnderlineValues.Double,
|
||||
UnderlineStyle.SingleAccounting => UnderlineValues.SingleAccounting,
|
||||
UnderlineStyle.DoubleAccounting => UnderlineValues.DoubleAccounting,
|
||||
_ => throw new NotImplementedException(),
|
||||
}
|
||||
});
|
||||
}
|
||||
if (fmt.IsStrike == true) rPr.Append(new Strike());
|
||||
if (fmt.Color.HasValue)
|
||||
{
|
||||
var excelColor = fmt.Color.Value.ToExcel();
|
||||
rPr.Append(excelColor);
|
||||
}
|
||||
if (fmt.FontSize.HasValue)
|
||||
rPr.Append(new FontSize { Val = fmt.FontSize.Value });
|
||||
if (!string.IsNullOrEmpty(fmt.FontFamily))
|
||||
rPr.Append(new RunFont { Val = fmt.FontFamily });
|
||||
if (fmt.Vertical.HasValue)
|
||||
{
|
||||
var vertAlign = new VerticalTextAlignment
|
||||
{
|
||||
Val = fmt.Vertical.Value == VerticalTextRunAlignment.Superscript
|
||||
? VerticalAlignmentRunValues.Superscript
|
||||
: VerticalAlignmentRunValues.Subscript
|
||||
};
|
||||
rPr.Append(vertAlign);
|
||||
}
|
||||
runElement.RunProperties = rPr;
|
||||
}
|
||||
inline.Append(runElement);
|
||||
}
|
||||
return inline;
|
||||
}
|
||||
|
||||
private MergeCells? GetMergeCells() =>
|
||||
_sheet.Worksheet.GetFirstChild<MergeCells>();
|
||||
|
||||
private bool TryParseRangeReference(string reference, out ExcelRange range)
|
||||
{
|
||||
range = null!;
|
||||
if (string.IsNullOrEmpty(reference)) return false;
|
||||
string[] parts = reference.Split(':');
|
||||
if (parts.Length != 2) return false;
|
||||
if (!CellAddressHelper.TryParseCellReference(parts[0] + "1", out uint row1, out uint col1)) return false;
|
||||
if (!CellAddressHelper.TryParseCellReference(parts[1] + "1", out uint row2, out uint col2)) return false;
|
||||
uint rowStart = Math.Min(row1, row2), rowEnd = Math.Max(row1, row2);
|
||||
uint colStart = Math.Min(col1, col2), colEnd = Math.Max(col1, col2);
|
||||
range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
270
QWERTYkez.ExcelProcessor/Editors/ExcelCellText.cs
Normal file
270
QWERTYkez.ExcelProcessor/Editors/ExcelCellText.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="ICellText"/> для работы с богатым текстом ячейки.
|
||||
/// Хранит коллекцию фрагментов <see cref="ExcelRun"/>.
|
||||
/// Минимизирует аллокации, не использует рефлексию.
|
||||
/// </summary>
|
||||
internal sealed class ExcelCellText : ICellText
|
||||
{
|
||||
private List<IRun>? _runs;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _runs?.Count ?? 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IRun> GetRuns()
|
||||
{
|
||||
if (_runs is null)
|
||||
return [];
|
||||
// Возвращаем сам список, чтобы избежать копирования.
|
||||
// Вызывающий не должен модифицировать коллекцию.
|
||||
return _runs;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRun? GetRunAt(int index)
|
||||
{
|
||||
if (_runs is null || index < 0 || index >= _runs.Count)
|
||||
return null;
|
||||
return _runs[index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetRunAt(int index, out IRun run)
|
||||
{
|
||||
run = GetRunAt(index)!;
|
||||
return run != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRun? First()
|
||||
{
|
||||
if (_runs is null || _runs.Count == 0)
|
||||
return null;
|
||||
return _runs[0];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetFirst(out IRun run)
|
||||
{
|
||||
run = First()!;
|
||||
return run != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRun? Last()
|
||||
{
|
||||
if (_runs is null || _runs.Count == 0)
|
||||
return null;
|
||||
return _runs[_runs.Count - 1];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetLast(out IRun run)
|
||||
{
|
||||
run = Last()!;
|
||||
return run != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRemoveRun(IRun run)
|
||||
{
|
||||
if (run is null || _runs is null)
|
||||
return false;
|
||||
return _runs.Remove(run);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRemoveRun(int index)
|
||||
{
|
||||
if (_runs is null || index < 0 || index >= _runs.Count)
|
||||
return false;
|
||||
_runs.RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRemoveRun(int index, out IRun? removed)
|
||||
{
|
||||
removed = GetRunAt(index);
|
||||
if (removed is null)
|
||||
return false;
|
||||
return TryRemoveRun(index);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICellText AddBreak()
|
||||
{
|
||||
// Добавляем символ переноса строки в последний существующий Run
|
||||
if (_runs != null && _runs.Count > 0)
|
||||
{
|
||||
var lastRun = _runs[_runs.Count - 1];
|
||||
lastRun.Text += "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Если нет ни одного Run, создаём новый с символом переноса
|
||||
AddRun("\n", null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICellText AddRun(string text, RunFormat? format = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return this;
|
||||
_runs ??= [];
|
||||
var run = new ExcelRun { Text = text, Format = format };
|
||||
_runs.Add(run);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICellText AddRunBreak(string text, RunFormat? format = null)
|
||||
{
|
||||
AddRun(text, format);
|
||||
AddBreak();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICellText AddSubRun(string text, RunFormat? format = null)
|
||||
{
|
||||
var subFormat = format is { } fmt
|
||||
? new RunFormat
|
||||
{
|
||||
IsBold = fmt.IsBold,
|
||||
IsItalic = fmt.IsItalic,
|
||||
Underline = fmt.Underline,
|
||||
IsStrike = fmt.IsStrike,
|
||||
Color = fmt.Color,
|
||||
FontSize = fmt.FontSize,
|
||||
FontFamily = fmt.FontFamily,
|
||||
Vertical = VerticalTextRunAlignment.Subscript
|
||||
}
|
||||
: new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
|
||||
return AddRun(text, subFormat);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICellText AddSupRun(string text, RunFormat? format = null)
|
||||
{
|
||||
var supFormat = format is { } fmt
|
||||
? new RunFormat
|
||||
{
|
||||
IsBold = fmt.IsBold,
|
||||
IsItalic = fmt.IsItalic,
|
||||
Underline = fmt.Underline,
|
||||
IsStrike = fmt.IsStrike,
|
||||
Color = fmt.Color,
|
||||
FontSize = fmt.FontSize,
|
||||
FontFamily = fmt.FontFamily,
|
||||
Vertical = VerticalTextRunAlignment.Subscript
|
||||
}
|
||||
: new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
|
||||
return AddRun(text, supFormat);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryInsertRun(int index, string text, RunFormat? format = null)
|
||||
{
|
||||
if (index < 0 || string.IsNullOrEmpty(text))
|
||||
return false;
|
||||
_runs ??= [];
|
||||
if (index > _runs.Count)
|
||||
return false;
|
||||
var run = new ExcelRun { Text = text, Format = format };
|
||||
_runs.Insert(index, run);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryInsertRunBreak(int index, string text, RunFormat? format = null)
|
||||
{
|
||||
if (!TryInsertRun(index, text, format))
|
||||
return false;
|
||||
// После вставленного run добавляем break на следующей позиции
|
||||
AddBreak();
|
||||
// Сдвигаем? Просто добавляем break в конец – неверно. Break должен быть сразу после вставленного.
|
||||
// Но AddBreak добавляет в конец. Нужно вставить break на index+1.
|
||||
return TryInsertRun(index + 1, "\n", null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryInsertSubRun(int index, string text, RunFormat? format = null)
|
||||
{
|
||||
var subFormat = format is { } fmt
|
||||
? new RunFormat
|
||||
{
|
||||
IsBold = fmt.IsBold,
|
||||
IsItalic = fmt.IsItalic,
|
||||
Underline = fmt.Underline,
|
||||
IsStrike = fmt.IsStrike,
|
||||
Color = fmt.Color,
|
||||
FontSize = fmt.FontSize,
|
||||
FontFamily = fmt.FontFamily,
|
||||
Vertical = VerticalTextRunAlignment.Subscript
|
||||
}
|
||||
: new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
|
||||
return TryInsertRun(index, text, subFormat);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryInsertSupRun(int index, string text, RunFormat? format = null)
|
||||
{
|
||||
var supFormat = format is { } fmt
|
||||
? new RunFormat
|
||||
{
|
||||
IsBold = fmt.IsBold,
|
||||
IsItalic = fmt.IsItalic,
|
||||
Underline = fmt.Underline,
|
||||
IsStrike = fmt.IsStrike,
|
||||
Color = fmt.Color,
|
||||
FontSize = fmt.FontSize,
|
||||
FontFamily = fmt.FontFamily,
|
||||
Vertical = VerticalTextRunAlignment.Subscript
|
||||
}
|
||||
: new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
|
||||
return TryInsertRun(index, text, supFormat);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyFormatToAllRuns(RunFormat format)
|
||||
{
|
||||
if (_runs is null || _runs.Count == 0)
|
||||
return;
|
||||
foreach (var run in _runs)
|
||||
{
|
||||
if (run is ExcelRun xRun)
|
||||
{
|
||||
// Объединение форматов: ненулевые свойства overlay заменяют значения в base.
|
||||
var baseFmt = xRun.Format ?? new RunFormat();
|
||||
xRun.Format = MergeRunFormat(baseFmt, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
_runs?.Clear();
|
||||
_runs = null;
|
||||
}
|
||||
|
||||
private static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay)
|
||||
{
|
||||
return new RunFormat
|
||||
{
|
||||
IsBold = overlay.IsBold ?? baseFmt.IsBold,
|
||||
IsItalic = overlay.IsItalic ?? baseFmt.IsItalic,
|
||||
Underline = overlay.Underline ?? baseFmt.Underline,
|
||||
IsStrike = overlay.IsStrike ?? baseFmt.IsStrike,
|
||||
Color = overlay.Color ?? baseFmt.Color,
|
||||
FontSize = overlay.FontSize ?? baseFmt.FontSize,
|
||||
FontFamily = overlay.FontFamily ?? baseFmt.FontFamily,
|
||||
Vertical = overlay.Vertical ?? baseFmt.Vertical
|
||||
};
|
||||
}
|
||||
}
|
||||
502
QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs
Normal file
502
QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="IColumn"/>.
|
||||
/// </summary>
|
||||
internal sealed class ExcelColumn : IColumn
|
||||
{
|
||||
internal readonly ExcelWriter _writer;
|
||||
internal readonly ExcelSheet _sheet;
|
||||
internal uint _colIndex;
|
||||
|
||||
internal ExcelColumn(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
|
||||
{
|
||||
_writer = writer;
|
||||
_sheet = sheet;
|
||||
_colIndex = colIndex;
|
||||
}
|
||||
|
||||
public uint Index => _colIndex;
|
||||
public string IndexLetter => NumberToColumnLetter(_colIndex);
|
||||
|
||||
|
||||
|
||||
/// <summary>Устанавливает ширину столбца без создания объекта IColumn.</summary>
|
||||
internal static void SetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex, ColumnWidth width)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var col = GetOrCreateColumnElementInternal(sheet, colIndex);
|
||||
|
||||
double widthInChars;
|
||||
|
||||
if (width.IsCharacterUnit)
|
||||
{
|
||||
// Если ширина задана в символах, используем напрямую
|
||||
widthInChars = width.CharacterValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
double targetPoints = width.GetTargetPoints();
|
||||
widthInChars = writer.TryGetCalibrateCoeff(targetPoints, out var closestCw)
|
||||
? closestCw : targetPoints / ColumnWidth.DefaultPointsPerChar;
|
||||
}
|
||||
|
||||
|
||||
col.Width = widthInChars;
|
||||
col.CustomWidth = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Возвращает ширину столбца.</summary>
|
||||
internal static ColumnWidth GetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var col = GetColumnElementInternal(sheet, colIndex);
|
||||
if (col is Column column
|
||||
&& column.CustomWidth?.Value == true
|
||||
&& column.Width?.Value is { } wid)
|
||||
return ColumnWidth.FromCharacters(wid);
|
||||
return ColumnWidth.FromCharacters(8.43); // стандартная ширина
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные внутренние методы (перенести существующую логику)
|
||||
private static Column? GetColumnElementInternal(ExcelSheet sheet, uint colIndex)
|
||||
{
|
||||
var cols = sheet.Worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column col in cols.Elements<Column>())
|
||||
{
|
||||
if (col?.Min?.Value is { } min
|
||||
&& col?.Max?.Value is { } max
|
||||
&& min <= colIndex
|
||||
&& max >= colIndex)
|
||||
return col;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
|
||||
{
|
||||
var existing = GetColumnElementInternal(sheet, colIndex);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var worksheet = sheet.Worksheet;
|
||||
var cols = worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null)
|
||||
{
|
||||
cols = new Columns();
|
||||
worksheet.InsertAt(cols, 0);
|
||||
}
|
||||
var newCol = new Column
|
||||
{
|
||||
Min = colIndex,
|
||||
Max = colIndex,
|
||||
Width = 8.43,
|
||||
CustomWidth = true
|
||||
};
|
||||
cols.Append(newCol);
|
||||
return newCol;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public IColumn MoveTo(uint index)
|
||||
{
|
||||
if (index == _colIndex) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
// Копируем данные в новую позицию
|
||||
CopyColumnData(_colIndex, index);
|
||||
// Очищаем исходный столбец
|
||||
ClearColumnData(_colIndex);
|
||||
_colIndex = index;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public IColumn MoveTo(string index)
|
||||
{
|
||||
uint idx = CellAddressHelper.ColumnLetterToIndex(index);
|
||||
return MoveTo(idx);
|
||||
}
|
||||
|
||||
public ColumnWidth Width
|
||||
{
|
||||
get
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var column = GetColumnElement();
|
||||
if (column is Column col && col.Width?.Value is { } val && col.CustomWidth?.Value == true)
|
||||
return ColumnWidth.FromCharacters(val);
|
||||
return ColumnWidth.FromCharacters(8.43); // стандартная ширина
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var col = GetOrCreateColumnElement();
|
||||
double widthInChars;
|
||||
|
||||
if (value.IsCharacterUnit)
|
||||
{
|
||||
// Если ширина задана в символах, используем напрямую
|
||||
widthInChars = value.CharacterValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
double targetCm = value.GetTargetCentimeters();
|
||||
widthInChars = _writer.TryGetCalibrateCoeff(targetCm, out var closestCw)
|
||||
? closestCw : targetCm * (ColumnWidth.DefaultPointsPerChar / 28.3464566929);
|
||||
}
|
||||
|
||||
col.Width = widthInChars;
|
||||
col.CustomWidth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IColumn SetNumberFormat(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int fmtId = GetOrCreateNumberFormatId(format);
|
||||
// Применяем ко всем ячейкам столбца
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
var cell = FindCellInRow(row, _colIndex);
|
||||
cell?.StyleIndex = (uint)fmtId;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Cell(uint row)
|
||||
{
|
||||
return new ExcelCell(_writer, _sheet, row, _colIndex);
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, Action<ICell> edit)
|
||||
{
|
||||
var cell = Cell(row);
|
||||
edit(cell);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, string value)
|
||||
{
|
||||
Cell(row).Set(value); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, bool value)
|
||||
{
|
||||
Cell(row).Set(value); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, string value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IColumn Cell(uint row, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row).Set(value, format); return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void ClearContents()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ClearColumnData(_colIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearFormats()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
var cell = FindCellInRow(row, _colIndex);
|
||||
cell?.StyleIndex = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearContents();
|
||||
ClearFormats();
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
// Очищаем данные
|
||||
ClearColumnData(_colIndex);
|
||||
// Удаляем элемент Column, если он существует
|
||||
DeleteColumnElement();
|
||||
}
|
||||
}
|
||||
|
||||
public IColumn CopyTo(uint index, out IColumn copiedColumn)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
CopyColumnData(_colIndex, index);
|
||||
copiedColumn = new ExcelColumn(_writer, _sheet, index);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IColumn CopyTo(string index, out IColumn copiedColumn)
|
||||
{
|
||||
uint idx = CellAddressHelper.ColumnLetterToIndex(index);
|
||||
return CopyTo(idx, out copiedColumn);
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
|
||||
private Column? GetColumnElement()
|
||||
{
|
||||
var cols = _sheet.Worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column col in cols.Elements<Column>())
|
||||
{
|
||||
if (col?.Min?.Value is { } min
|
||||
&& col?.Max?.Value is { } max
|
||||
&& min <= _colIndex
|
||||
&& max >= _colIndex)
|
||||
return col;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Column GetOrCreateColumnElement()
|
||||
{
|
||||
var existing = GetColumnElement();
|
||||
if (existing != null) return existing;
|
||||
|
||||
var worksheet = _sheet.Worksheet;
|
||||
var cols = worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null)
|
||||
{
|
||||
cols = new Columns();
|
||||
worksheet.InsertAt(cols, 0);
|
||||
}
|
||||
|
||||
var newCol = new Column
|
||||
{
|
||||
Min = _colIndex,
|
||||
Max = _colIndex,
|
||||
Width = 8.43,
|
||||
CustomWidth = true
|
||||
};
|
||||
cols.Append(newCol);
|
||||
return newCol;
|
||||
}
|
||||
|
||||
private void DeleteColumnElement()
|
||||
{
|
||||
var col = GetColumnElement();
|
||||
col?.Remove();
|
||||
}
|
||||
|
||||
private void CopyColumnData(uint sourceCol, uint targetCol)
|
||||
{
|
||||
if (sourceCol == targetCol) return;
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
// Сначала удаляем существующие данные в целевом столбце (если нужно перезаписать)
|
||||
ClearColumnData(targetCol);
|
||||
// Копируем значения и форматы из sourceCol в targetCol
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
var sourceCell = FindCellInRow(row, sourceCol);
|
||||
if (sourceCell != null)
|
||||
{
|
||||
var targetCell = FindCellInRow(row, targetCol);
|
||||
if (targetCell == null)
|
||||
{
|
||||
targetCell = new Cell();
|
||||
// Вставляем в нужное место (по порядку столбцов)
|
||||
InsertCellInRow(row, targetCell, targetCol);
|
||||
}
|
||||
// Копируем содержимое и стиль
|
||||
targetCell.DataType = sourceCell.DataType;
|
||||
targetCell.CellValue = sourceCell.CellValue?.CloneNode(true) as CellValue;
|
||||
targetCell.CellFormula = sourceCell.CellFormula?.CloneNode(true) as CellFormula;
|
||||
targetCell.StyleIndex = sourceCell.StyleIndex;
|
||||
// Если есть InlineString, клонируем
|
||||
if (sourceCell.InlineString != null)
|
||||
targetCell.InlineString = (InlineString)sourceCell.InlineString.CloneNode(true);
|
||||
}
|
||||
}
|
||||
// Копируем ширину столбца
|
||||
var sourceColElem = GetColumnElementForIndex(sourceCol);
|
||||
if (sourceColElem?.Width is { } width)
|
||||
{
|
||||
var targetColElem = GetOrCreateColumnElementForIndex(targetCol);
|
||||
targetColElem.Width = width;
|
||||
targetColElem.CustomWidth = sourceColElem.CustomWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearColumnData(uint col)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
var cell = FindCellInRow(row, col);
|
||||
cell?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
private Cell? FindCellInRow(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
string cellRef = cell.CellReference?.Value ?? string.Empty;
|
||||
if (TryParseCellReference(cellRef, out uint _, out uint colIdx) && colIdx == colIndex)
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
||||
{
|
||||
string newRef = NumberToColumnLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
|
||||
cell.CellReference = newRef;
|
||||
// Вставляем в правильном порядке (по возрастанию столбцов)
|
||||
bool inserted = false;
|
||||
foreach (var existing in row.Elements<Cell>().ToList())
|
||||
{
|
||||
if (TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) &&
|
||||
existingCol > colIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(cell);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted)
|
||||
row.Append(cell);
|
||||
}
|
||||
|
||||
private Column? GetColumnElementForIndex(uint col)
|
||||
{
|
||||
var cols = _sheet.Worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column c in cols.Elements<Column>())
|
||||
{
|
||||
if (c.Min?.Value is { } min
|
||||
&& c.Max?.Value is { } max
|
||||
&& min <= col
|
||||
&& max >= col)
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Column GetOrCreateColumnElementForIndex(uint col)
|
||||
{
|
||||
var existing = GetColumnElementForIndex(col);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var worksheet = _sheet.Worksheet;
|
||||
var cols = worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null)
|
||||
{
|
||||
cols = new Columns();
|
||||
worksheet.InsertAt(cols, 0);
|
||||
}
|
||||
var newCol = new Column
|
||||
{
|
||||
Min = col,
|
||||
Max = col,
|
||||
Width = 8.43,
|
||||
CustomWidth = true
|
||||
};
|
||||
cols.Append(newCol);
|
||||
return newCol;
|
||||
}
|
||||
|
||||
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
|
||||
{
|
||||
return _writer.GetOrCreateCellFormatId(numberFormat: format);
|
||||
}
|
||||
|
||||
private static bool TryParseCellReference(string reference, out uint row, out uint col)
|
||||
{
|
||||
row = 0; col = 0;
|
||||
if (string.IsNullOrEmpty(reference)) return false;
|
||||
int i = 0;
|
||||
while (i < reference.Length && char.IsLetter(reference[i])) i++;
|
||||
if (i == 0) return false;
|
||||
string colPart = reference.Substring(0, i);
|
||||
string rowPart = reference.Substring(i);
|
||||
if (!uint.TryParse(rowPart, out row)) return false;
|
||||
col = CellAddressHelper.ColumnLetterToIndex(colPart);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NumberToColumnLetter(uint col)
|
||||
{
|
||||
if (col == 0) throw new ArgumentException("Column number must be > 0");
|
||||
string result = "";
|
||||
while (col > 0)
|
||||
{
|
||||
col--;
|
||||
result = (char)('A' + (col % 26)) + result;
|
||||
col /= 26;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
886
QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs
Normal file
886
QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs
Normal file
@@ -0,0 +1,886 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="IRange"/>.
|
||||
/// </summary>
|
||||
internal sealed class ExcelRange : IRange
|
||||
{
|
||||
private readonly ExcelWriter _writer;
|
||||
private readonly ExcelSheet _sheet;
|
||||
private uint _rowStart, _rowEnd;
|
||||
private uint _colStart, _colEnd;
|
||||
|
||||
internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
||||
{
|
||||
_writer = writer;
|
||||
_sheet = sheet;
|
||||
_rowStart = rowStart;
|
||||
_rowEnd = rowEnd;
|
||||
_colStart = colStart;
|
||||
_colEnd = colEnd;
|
||||
}
|
||||
|
||||
public uint RowStart => _rowStart;
|
||||
public uint RowEnd => _rowEnd;
|
||||
public uint ColStart => _colStart;
|
||||
public uint ColEnd => _colEnd;
|
||||
public string ColStartLetter => CellAddressHelper.ColumnIndexToLetter(_colStart);
|
||||
public string ColEndLetter => CellAddressHelper.ColumnIndexToLetter(_colEnd);
|
||||
public uint Rows => _rowEnd - _rowStart + 1;
|
||||
public uint Cols => _colEnd - _colStart + 1;
|
||||
|
||||
public bool IsMerged
|
||||
{
|
||||
get
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var mergeCells = GetMergeCells();
|
||||
if (mergeCells == null) return false;
|
||||
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
|
||||
{
|
||||
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange))
|
||||
{
|
||||
if (refRange._rowStart == _rowStart && refRange._rowEnd == _rowEnd &&
|
||||
refRange._colStart == _colStart && refRange._colEnd == _colEnd)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
|
||||
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
||||
/// </summary>
|
||||
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
|
||||
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
|
||||
/// <returns>Тот же объект IRange с новыми координатами.</returns>
|
||||
public IRange MoveTo(uint newRow, uint newCol)
|
||||
{
|
||||
if (newRow == _rowStart && newCol == _colStart) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
uint newRowEnd = newRow + (_rowEnd - _rowStart);
|
||||
uint newColEnd = newCol + (_colEnd - _colStart);
|
||||
bool overlap = RangesOverlap(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, newRowEnd, newColEnd);
|
||||
bool horizontalShift = (newRow == _rowStart);
|
||||
bool verticalShift = (newCol == _colStart);
|
||||
|
||||
if (!overlap)
|
||||
{
|
||||
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, CopyOrder.Any);
|
||||
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
||||
}
|
||||
else if (horizontalShift)
|
||||
{
|
||||
CopyOrder order = (newCol > _colStart) ? CopyOrder.RightToLeft : CopyOrder.LeftToRight;
|
||||
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
|
||||
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
||||
}
|
||||
else if (verticalShift)
|
||||
{
|
||||
CopyOrder order = (newRow > _rowStart) ? CopyOrder.BottomToTop : CopyOrder.TopToBottom;
|
||||
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
|
||||
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Диагональное перемещение – два последовательных сдвига
|
||||
MoveTo(_rowStart, newCol);
|
||||
MoveTo(newRow, _colStart);
|
||||
return this;
|
||||
}
|
||||
|
||||
_rowStart = newRow;
|
||||
_rowEnd = newRowEnd;
|
||||
_colStart = newCol;
|
||||
_colEnd = newColEnd;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange MoveTo(uint rowIndex, string colIndex)
|
||||
{
|
||||
return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
|
||||
}
|
||||
|
||||
private enum CopyOrder
|
||||
{
|
||||
Any,
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
TopToBottom,
|
||||
BottomToTop
|
||||
}
|
||||
|
||||
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
|
||||
private void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
|
||||
uint dstRowStart, uint dstColStart, CopyOrder order)
|
||||
{
|
||||
// Определяем все строки, которые могут понадобиться (исходные и целевые)
|
||||
var rowIndices = new HashSet<uint>();
|
||||
for (uint r = srcRowStart; r <= srcRowEnd; r++) rowIndices.Add(r);
|
||||
uint dstRowEnd = dstRowStart + (srcRowEnd - srcRowStart);
|
||||
for (uint r = dstRowStart; r <= dstRowEnd; r++) rowIndices.Add(r);
|
||||
var rowsDict = GetRowDictionary(rowIndices);
|
||||
|
||||
int rowStep = 1, colStep = 1;
|
||||
int rowStart = (int)srcRowStart, rowEnd = (int)srcRowEnd;
|
||||
int colStart = (int)srcColStart, colEnd = (int)srcColEnd;
|
||||
|
||||
if (order == CopyOrder.BottomToTop)
|
||||
{
|
||||
rowStep = -1;
|
||||
rowStart = (int)srcRowEnd;
|
||||
rowEnd = (int)srcRowStart;
|
||||
}
|
||||
if (order == CopyOrder.RightToLeft)
|
||||
{
|
||||
colStep = -1;
|
||||
colStart = (int)srcColEnd;
|
||||
colEnd = (int)srcColStart;
|
||||
}
|
||||
|
||||
for (int r = rowStart; (rowStep > 0 ? r <= rowEnd : r >= rowEnd); r += rowStep)
|
||||
{
|
||||
if (!rowsDict.TryGetValue((uint)r, out var srcRow))
|
||||
continue;
|
||||
|
||||
for (int c = colStart; (colStep > 0 ? c <= colEnd : c >= colEnd); c += colStep)
|
||||
{
|
||||
Cell? srcCell = FindCellInRowFast(srcRow, (uint)c);
|
||||
if (srcCell == null) continue;
|
||||
|
||||
Cell cloned = (Cell)srcCell.CloneNode(true);
|
||||
uint dstRow = dstRowStart + (uint)(r - srcRowStart);
|
||||
uint dstCol = dstColStart + (uint)(c - srcColStart);
|
||||
cloned.CellReference = $"{CellAddressHelper.ColumnIndexToLetter(dstCol)}{dstRow}";
|
||||
InsertCellAt(dstRow, dstCol, cloned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Строит словарь строк для указанных индексов строк.</summary>
|
||||
private Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
|
||||
{
|
||||
var dict = new Dictionary<uint, Row>();
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
if (row.RowIndex?.Value is uint idx && rowIndices.Contains(idx))
|
||||
dict[idx] = row;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
|
||||
private Cell? FindCellInRowFast(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
|
||||
private static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e,
|
||||
uint r2s, uint c2s, uint r2e, uint c2e)
|
||||
{
|
||||
return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s);
|
||||
}
|
||||
|
||||
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
|
||||
private void InsertCellAt(uint row, uint col, Cell cell)
|
||||
{
|
||||
DeleteCellAt(row, col);
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var rowElement = GetOrCreateRowElement(sheetData, row);
|
||||
InsertCellInRow(rowElement, cell, col);
|
||||
}
|
||||
|
||||
/// <summary>Удаляет ячейку, если она существует.</summary>
|
||||
private void DeleteCellAt(uint row, uint col)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var rowElement = FindRowElement(sheetData, row);
|
||||
if (rowElement == null) return;
|
||||
var cell = FindCellInRow(rowElement, col);
|
||||
cell?.Remove();
|
||||
}
|
||||
|
||||
/// <summary>Получает или создаёт строку с указанным индексом.</summary>
|
||||
private Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
|
||||
{
|
||||
var existing = FindRowElement(sheetData, rowIndex);
|
||||
if (existing != null) return existing;
|
||||
var newRow = new Row { RowIndex = rowIndex };
|
||||
InsertRowElement(sheetData, newRow, rowIndex);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
|
||||
private void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
||||
{
|
||||
string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}";
|
||||
cell.CellReference = newRef;
|
||||
bool inserted = false;
|
||||
foreach (var existing in row.Elements<Cell>().ToList())
|
||||
{
|
||||
if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) &&
|
||||
existingCol > colIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(cell);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) row.Append(cell);
|
||||
}
|
||||
|
||||
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
|
||||
private void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
||||
{
|
||||
for (uint r = rowStart; r <= rowEnd; r++)
|
||||
{
|
||||
var row = FindRowElement(_sheet.GetSheetData(), r);
|
||||
if (row == null) continue;
|
||||
for (uint c = colStart; c <= colEnd; c++)
|
||||
{
|
||||
var cell = FindCellInRow(row, c);
|
||||
cell?.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public bool TryMerge()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
if (IsMerged) return false;
|
||||
var worksheet = _sheet.Worksheet;
|
||||
|
||||
var mergeCells = worksheet.GetFirstChild<MergeCells>();
|
||||
if (mergeCells == null)
|
||||
{
|
||||
mergeCells = new MergeCells();
|
||||
worksheet.Append(mergeCells);
|
||||
}
|
||||
string refStr = $"{ColStartLetter}{RowStart}:{ColEndLetter}{RowEnd}";
|
||||
mergeCells.Append(new MergeCell { Reference = refStr });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Unmerge()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var mergeCells = GetMergeCells();
|
||||
if (mergeCells == null) return;
|
||||
var toRemove = mergeCells.Elements<MergeCell>()
|
||||
.FirstOrDefault(mc => TryParseRangeReference(mc.Reference?.Value ?? string.Empty, out var rng) &&
|
||||
rng._rowStart == _rowStart && rng._rowEnd == _rowEnd &&
|
||||
rng._colStart == _colStart && rng._colEnd == _colEnd);
|
||||
toRemove?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public IRange? GetMergedRange()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var mergeCells = GetMergeCells();
|
||||
if (mergeCells == null) return null;
|
||||
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
|
||||
{
|
||||
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange))
|
||||
{
|
||||
if (refRange._rowStart >= _rowStart && refRange._rowEnd <= _rowEnd &&
|
||||
refRange._colStart >= _colStart && refRange._colEnd <= _colEnd)
|
||||
return refRange;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetMergedRange(IRange range)
|
||||
{
|
||||
var merged = GetMergedRange();
|
||||
if (merged == null) return false;
|
||||
return merged.Equals(range);
|
||||
}
|
||||
|
||||
public IRange SetNumberFormat(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int fmtId = GetOrCreateNumberFormatId(format);
|
||||
for (uint r = _rowStart; r <= _rowEnd; r++)
|
||||
{
|
||||
for (uint c = _colStart; c <= _colEnd; c++)
|
||||
{
|
||||
var cell = GetCellInternal(r, c);
|
||||
cell?.StyleIndex = (uint)fmtId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellAlign(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: align
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellBorder(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: border,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellFill(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: fill,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellFont(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: font,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Применяет стиль ко всем ячейкам диапазона.
|
||||
/// </summary>
|
||||
/// <param name="styleIndex">Индекс стиля для применения.</param>
|
||||
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
|
||||
private void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true)
|
||||
{
|
||||
for (uint row = _rowStart; row <= _rowEnd; row++)
|
||||
{
|
||||
for (uint col = _colStart; col <= _colEnd; col++)
|
||||
{
|
||||
var cell = GetCellInternal(row, col);
|
||||
if (cell != null)
|
||||
{
|
||||
cell.StyleIndex = styleIndex;
|
||||
}
|
||||
else if (createIfMissing)
|
||||
{
|
||||
var newCell = new Cell
|
||||
{
|
||||
CellReference = $"{CellAddressHelper.ColumnIndexToLetter(col)}{row}",
|
||||
StyleIndex = styleIndex
|
||||
};
|
||||
InsertCellAt(row, col, newCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ICell> Cells
|
||||
{
|
||||
get
|
||||
{
|
||||
for (uint r = _rowStart; r <= _rowEnd; r++)
|
||||
for (uint c = _colStart; c <= _colEnd; c++)
|
||||
yield return new ExcelCell(_writer, _sheet, r, c);
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetSubCell(uint row, uint col, out ICell cell)
|
||||
{
|
||||
uint globalRow = _rowStart + row - 1;
|
||||
uint globalCol = _colStart + col - 1;
|
||||
if (globalRow > _rowEnd || globalCol > _colEnd)
|
||||
{
|
||||
cell = default!;
|
||||
return false;
|
||||
}
|
||||
cell = new ExcelCell(_writer, _sheet, globalRow, globalCol);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GetSubCell(uint row, string col, out ICell cell)
|
||||
{
|
||||
uint colIndex = CellAddressHelper.ColumnLetterToIndex(col);
|
||||
return GetSubCell(row, colIndex, out cell);
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, Action<ICell> edit)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
edit(cell);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, string value)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, bool value)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(formula, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, Action<ICell> edit)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
edit(cell);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, string value)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, bool value)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(formula, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
if (GetSubCell(row, col, out var cell))
|
||||
{
|
||||
cell.Set(value, format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void ClearContents()
|
||||
{
|
||||
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
||||
}
|
||||
|
||||
public void ClearFormats()
|
||||
{
|
||||
for (uint r = _rowStart; r <= _rowEnd; r++)
|
||||
for (uint c = _colStart; c <= _colEnd; c++)
|
||||
{
|
||||
var cell = GetCellInternal(r, c);
|
||||
cell?.StyleIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearContents();
|
||||
ClearFormats();
|
||||
}
|
||||
|
||||
public IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
CopyData(_rowStart, _colStart, _rowEnd, _colEnd, rowIndex, colIndex);
|
||||
copiedRange = new ExcelRange(_writer, _sheet, rowIndex, colIndex, rowIndex + Rows - 1, colIndex + Cols - 1);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange)
|
||||
{
|
||||
uint col = CellAddressHelper.ColumnLetterToIndex(colIndex);
|
||||
return CopyTo(rowIndex, col, out copiedRange);
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
|
||||
private Cell? GetCellInternal(uint row, uint col)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var rowElement = FindRowElement(sheetData, row);
|
||||
if (rowElement == null) return null;
|
||||
return FindCellInRow(rowElement, col);
|
||||
}
|
||||
|
||||
private void CopyData(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart)
|
||||
{
|
||||
// Сохраняем все значения и форматы из исходного диапазона
|
||||
var cellsData = new List<(uint row, uint col, Cell cell)>();
|
||||
for (uint r = srcRowStart; r <= srcRowEnd; r++)
|
||||
{
|
||||
for (uint c = srcColStart; c <= srcColEnd; c++)
|
||||
{
|
||||
var cell = GetCellInternal(r, c);
|
||||
if (cell != null)
|
||||
{
|
||||
var cloned = (Cell)cell.CloneNode(true);
|
||||
cellsData.Add((r - srcRowStart + dstRowStart, c - srcColStart + dstColStart, cloned));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Вставляем в новый диапазон
|
||||
foreach (var (row, col, clonedCell) in cellsData)
|
||||
{
|
||||
InsertCellAt(row, col, clonedCell);
|
||||
}
|
||||
// Копируем ширины столбцов
|
||||
for (uint c = srcColStart; c <= srcColEnd; c++)
|
||||
{
|
||||
var srcCol = GetColumnElementForIndex(c);
|
||||
if (srcCol?.Width is { } width)
|
||||
{
|
||||
uint targetCol = c - srcColStart + dstColStart;
|
||||
var dstCol = GetOrCreateColumnElementForIndex(targetCol);
|
||||
dstCol.Width = width;
|
||||
dstCol.CustomWidth = srcCol.CustomWidth;
|
||||
}
|
||||
}
|
||||
// Копируем высоты строк
|
||||
for (uint r = srcRowStart; r <= srcRowEnd; r++)
|
||||
{
|
||||
var srcRow = FindRowElement(_sheet.GetSheetData(), r);
|
||||
if (srcRow is { } row
|
||||
&& row.Height?.Value is { } hei
|
||||
&& row.CustomHeight?.Value == true)
|
||||
{
|
||||
uint targetRow = r - srcRowStart + dstRowStart;
|
||||
var dstRow = GetOrCreateRowElement(targetRow);
|
||||
dstRow.Height = hei;
|
||||
dstRow.CustomHeight = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Row GetOrCreateRowElement(uint rowIndex)
|
||||
{
|
||||
var existing = FindRowElement(_sheet.GetSheetData(), rowIndex);
|
||||
if (existing != null) return existing;
|
||||
var newRow = new Row { RowIndex = rowIndex };
|
||||
InsertRowElement(_sheet.GetSheetData(), newRow, rowIndex);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
|
||||
{
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
if (row.RowIndex?.Value == rowIndex) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
|
||||
{
|
||||
bool inserted = false;
|
||||
foreach (var existing in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
if (existing.RowIndex?.Value > rowIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(row);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) sheetData.Append(row);
|
||||
}
|
||||
|
||||
private Cell? FindCellInRow(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Column? GetColumnElementForIndex(uint col)
|
||||
{
|
||||
var worksheet = _sheet.Worksheet;
|
||||
var cols = worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column c in cols.Elements<Column>())
|
||||
if (c.Min?.Value is { } min
|
||||
&& c.Max?.Value is { } max
|
||||
&& min <= col
|
||||
&& max >= col)
|
||||
return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
private Column GetOrCreateColumnElementForIndex(uint col)
|
||||
{
|
||||
var existing = GetColumnElementForIndex(col);
|
||||
if (existing != null) return existing;
|
||||
var worksheet = _sheet.Worksheet;
|
||||
var cols = worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null)
|
||||
{
|
||||
cols = new Columns();
|
||||
worksheet.InsertAt(cols, 0);
|
||||
}
|
||||
var newCol = new Column
|
||||
{
|
||||
Min = col,
|
||||
Max = col,
|
||||
Width = 8.43,
|
||||
CustomWidth = true
|
||||
};
|
||||
cols.Append(newCol);
|
||||
return newCol;
|
||||
}
|
||||
|
||||
private MergeCells? GetMergeCells() =>
|
||||
_sheet.Worksheet.GetFirstChild<MergeCells>();
|
||||
|
||||
private bool TryParseRangeReference(string reference, out ExcelRange range)
|
||||
{
|
||||
range = null!;
|
||||
if (string.IsNullOrEmpty(reference)) return false;
|
||||
string[] parts = reference.Split(':');
|
||||
if (parts.Length != 2) return false;
|
||||
if (!CellAddressHelper.TryParseCellReference(parts[0] + "1", out uint row1, out uint col1)) return false;
|
||||
if (!CellAddressHelper.TryParseCellReference(parts[1] + "1", out uint row2, out uint col2)) return false;
|
||||
uint rowStart = Math.Min(row1, row2), rowEnd = Math.Max(row1, row2);
|
||||
uint colStart = Math.Min(col1, col2), colEnd = Math.Max(col1, col2);
|
||||
range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// создаёт стиль только с числовым форматом
|
||||
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
|
||||
{
|
||||
return _writer.GetOrCreateCellFormatId(numberFormat: format);
|
||||
}
|
||||
}
|
||||
393
QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs
Normal file
393
QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="IRow"/>.
|
||||
/// </summary>
|
||||
internal sealed class ExcelRow : IRow
|
||||
{
|
||||
private readonly ExcelWriter _writer;
|
||||
private readonly ExcelSheet _sheet;
|
||||
private uint _rowIndex;
|
||||
|
||||
internal ExcelRow(ExcelWriter writer, ExcelSheet sheet, uint rowIndex)
|
||||
{
|
||||
_writer = writer;
|
||||
_sheet = sheet;
|
||||
_rowIndex = rowIndex;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Index => _rowIndex;
|
||||
|
||||
|
||||
|
||||
/// <summary>Устанавливает высоту строки без создания объекта IRow.</summary>
|
||||
internal static void SetHeight(ExcelWriter writer, ExcelSheet sheet, uint rowIndex, RowHeight height)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var row = GetOrCreateRowElementInternal(sheet, rowIndex);
|
||||
row.Height = height.Points;
|
||||
row.CustomHeight = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Возвращает высоту строки.</summary>
|
||||
internal static RowHeight GetHeight(ExcelWriter writer, ExcelSheet sheet, uint rowIndex)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var row = GetRowElementInternal(sheet, rowIndex);
|
||||
if (row is { } r && r.CustomHeight?.HasValue == true && r.Height?.Value is { } hei)
|
||||
return RowHeight.FromPoints(hei);
|
||||
return RowHeight.FromPoints(15.0); // стандартная высота
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
private static Row? GetRowElementInternal(ExcelSheet sheet, uint rowIndex)
|
||||
{
|
||||
var sheetData = sheet.GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
if (row.RowIndex?.Value == rowIndex) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex)
|
||||
{
|
||||
var existing = GetRowElementInternal(sheet, rowIndex);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var sheetData = sheet.GetSheetData();
|
||||
var newRow = new Row { RowIndex = rowIndex };
|
||||
InsertRowElementInternal(sheetData, newRow, rowIndex);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
private static void InsertRowElementInternal(SheetData sheetData, Row row, uint rowIndex)
|
||||
{
|
||||
bool inserted = false;
|
||||
foreach (var existing in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
if (existing.RowIndex?.Value > rowIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(row);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) sheetData.Append(row);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow MoveTo(uint index)
|
||||
{
|
||||
if (index == _rowIndex) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
// Находим элемент строки
|
||||
var rowElement = FindRowElement(sheetData, _rowIndex);
|
||||
if (rowElement == null)
|
||||
{
|
||||
// Пустая строка — просто меняем индекс
|
||||
_rowIndex = index;
|
||||
return this;
|
||||
}
|
||||
// Удаляем из текущей позиции
|
||||
rowElement.Remove();
|
||||
// Вставляем в новую позицию
|
||||
InsertRowElement(sheetData, rowElement, index);
|
||||
// Обновляем RowIndex в элементе
|
||||
rowElement.RowIndex = index;
|
||||
_rowIndex = index;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowHeight Height
|
||||
{
|
||||
get
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
if (rowElement.Height?.Value is double h && rowElement.CustomHeight?.Value == true)
|
||||
return RowHeight.FromPoints(h);
|
||||
// Стандартная высота, если не задана (обычно 15 пунктов)
|
||||
return RowHeight.FromPoints(15.0);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
rowElement.Height = value.Points;
|
||||
rowElement.CustomHeight = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow SetNumberFormat(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
// Находим все ячейки в этой строке и устанавливаем формат
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var rowElement = FindRowElement(sheetData, _rowIndex);
|
||||
if (rowElement == null) return this;
|
||||
int formatIndex = GetOrCreateNumberFormatId(format);
|
||||
foreach (var cell in rowElement.Elements<Cell>())
|
||||
cell.StyleIndex = (uint)formatIndex;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Cell(uint col) => new ExcelCell(_writer, _sheet, _rowIndex, col);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Cell(uint col, Action<ICell> edit)
|
||||
{
|
||||
edit(Cell(col)); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, string value)
|
||||
{
|
||||
Cell(col).Set(value); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, bool value)
|
||||
{
|
||||
Cell(col).Set(value); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, string value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(uint col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, Action<ICell> edit)
|
||||
{
|
||||
edit(Cell(col)); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, string value)
|
||||
{
|
||||
Cell(col).Set(value); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, bool value)
|
||||
{
|
||||
Cell(col).Set(value); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, string value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IRow Cell(string col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public IRow Cell(string col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Cell(string col) => Cell(CellAddressHelper.ColumnLetterToIndex(col));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow EditCell(string col, Action<ICell> edit)
|
||||
{
|
||||
edit(Cell(col)); return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearContents()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = FindRowElement(_sheet.GetSheetData(), _rowIndex);
|
||||
if (rowElement == null) return;
|
||||
foreach (var cell in rowElement.Elements<Cell>().ToList())
|
||||
{
|
||||
cell.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearFormats()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = FindRowElement(_sheet.GetSheetData(), _rowIndex);
|
||||
if (rowElement == null) return;
|
||||
foreach (var cell in rowElement.Elements<Cell>())
|
||||
{
|
||||
cell.StyleIndex = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
ClearContents();
|
||||
ClearFormats();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove()
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var rowElement = FindRowElement(sheetData, _rowIndex);
|
||||
rowElement?.Remove();
|
||||
// Сдвиг индексов последующих строк вниз в Open XML не требуется, т.к. они имеют свои RowIndex.
|
||||
// Но при последующем доступе по индексам нужно будет корректировать.
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow CopyTo(uint index, out IRow copiedRow)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var sourceRow = FindRowElement(_sheet.GetSheetData(), _rowIndex);
|
||||
if (sourceRow == null)
|
||||
{
|
||||
copiedRow = new ExcelRow(_writer, _sheet, index);
|
||||
return this;
|
||||
}
|
||||
// Клонируем элемент строки
|
||||
var clonedRow = (Row)sourceRow.CloneNode(true);
|
||||
clonedRow.RowIndex = index;
|
||||
// Вставляем в новую позицию
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
InsertRowElement(sheetData, clonedRow, index);
|
||||
copiedRow = new ExcelRow(_writer, _sheet, index);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
|
||||
private Row GetOrCreateRowElement()
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
var existing = FindRowElement(sheetData, _rowIndex);
|
||||
if (existing != null) return existing;
|
||||
var newRow = new Row { RowIndex = _rowIndex };
|
||||
InsertRowElement(sheetData, newRow, _rowIndex);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
|
||||
{
|
||||
// Поиск по атрибуту RowIndex. В Open XML строки могут идти не по порядку.
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
if (row.RowIndex?.Value == rowIndex)
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
|
||||
{
|
||||
// Вставка с сохранением сортировки по RowIndex
|
||||
bool inserted = false;
|
||||
foreach (var existing in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
if (existing.RowIndex?.Value > rowIndex)
|
||||
{
|
||||
existing.InsertBeforeSelf(row);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted)
|
||||
sheetData.Append(row);
|
||||
}
|
||||
|
||||
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
|
||||
{
|
||||
// Создаём стиль, содержащий только числовой формат, и возвращаем его индекс
|
||||
return _writer.GetOrCreateCellFormatId(numberFormat: format);
|
||||
}
|
||||
}
|
||||
25
QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs
Normal file
25
QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="IRun"/> для хранения форматированного фрагмента текста.
|
||||
/// Минимизирует аллокации, не использует рефлексию, все поля доступны напрямую внутри сборки.
|
||||
/// </summary>
|
||||
internal sealed class ExcelRun : IRun
|
||||
{
|
||||
private string _text = string.Empty;
|
||||
private RunFormat? _format;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RunFormat? Format
|
||||
{
|
||||
get => _format;
|
||||
set => _format = value;
|
||||
}
|
||||
}
|
||||
277
QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs
Normal file
277
QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя реализация <see cref="ISheet"/>.
|
||||
/// </summary>
|
||||
internal sealed class ExcelSheet : ISheet
|
||||
{
|
||||
internal readonly ExcelWriter Book;
|
||||
internal readonly Sheet SheetElement;
|
||||
internal readonly Worksheet Worksheet;
|
||||
internal readonly uint SheetId;
|
||||
|
||||
internal ExcelSheet(ExcelWriter book, Sheet sheetElement, uint sheetId)
|
||||
{
|
||||
Book = book;
|
||||
SheetElement = sheetElement;
|
||||
SheetId = sheetId;
|
||||
|
||||
string partId = SheetElement.Id?.Value ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(partId))
|
||||
throw new InvalidOperationException("Sheet has no relationship ID");
|
||||
if (Book._doc.WorkbookPart?.GetPartById(partId) is not WorksheetPart part)
|
||||
throw new InvalidOperationException("WorksheetPart not found");
|
||||
if (part.Worksheet is not Worksheet worksheet)
|
||||
throw new InvalidOperationException("WorksheetPart not found");
|
||||
Worksheet = worksheet;
|
||||
}
|
||||
|
||||
public int Index => (int)SheetId;
|
||||
public string Name => SheetElement.Name?.Value ?? string.Empty;
|
||||
|
||||
public bool TrySetName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return false;
|
||||
Book.ThrowIfDisposed();
|
||||
SheetElement.Name = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IRow Row(uint row) => new ExcelRow(Book, this, row);
|
||||
public ISheet Row(uint row, Action<IRow> edit)
|
||||
{
|
||||
edit(Row(row));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IColumn Col(uint col) => new ExcelColumn(Book, this, col);
|
||||
public ISheet Col(uint col, Action<IColumn> edit)
|
||||
{
|
||||
edit(Col(col));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IColumn Col(string col) => Col(CellAddressHelper.ColumnLetterToIndex(col));
|
||||
public ISheet Col(string col, Action<IColumn> edit)
|
||||
{
|
||||
edit(Col(col));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell Cell(uint row, uint col) => new ExcelCell(Book, this, row, col);
|
||||
public ISheet Cell(uint row, uint col, Action<ICell> edit)
|
||||
{
|
||||
edit(Cell(row, col));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, string value)
|
||||
{
|
||||
Cell(row, col).Set(value); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, bool value)
|
||||
{
|
||||
Cell(row, col).Set(value); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(formula, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, uint col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ICell Cell(uint row, string col) => Cell(row, CellAddressHelper.ColumnLetterToIndex(col));
|
||||
public ISheet Cell(uint row, string col, Action<ICell> edit)
|
||||
{
|
||||
edit(Cell(row, col));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, string value)
|
||||
{
|
||||
Cell(row, col).Set(value); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, bool value)
|
||||
{
|
||||
Cell(row, col).Set(value); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(formula, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, decimal value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, double value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, float value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, int value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public ISheet Cell(uint row, string col, long value, NumberFormatPattern? format = null)
|
||||
{
|
||||
Cell(row, col).Set(value, format); return this;
|
||||
}
|
||||
|
||||
public void ClearContents()
|
||||
{
|
||||
var sheetData = GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>().ToList())
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>().ToList())
|
||||
cell.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearFormats()
|
||||
{
|
||||
var sheetData = GetSheetData();
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
cell.StyleIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearContents();
|
||||
ClearFormats();
|
||||
}
|
||||
|
||||
public void Remove() => Book.TryRemoveSheet(this);
|
||||
|
||||
// Вспомогательные методы
|
||||
|
||||
internal SheetData GetSheetData()
|
||||
{
|
||||
var worksheet = Worksheet;
|
||||
var sheetData = worksheet.GetFirstChild<SheetData>();
|
||||
if (sheetData == null)
|
||||
{
|
||||
sheetData = new SheetData();
|
||||
worksheet.Append(sheetData);
|
||||
}
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#region Range Operations
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol)
|
||||
{
|
||||
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol)
|
||||
{
|
||||
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
|
||||
uint endColIdx = CellAddressHelper.ColumnLetterToIndex(endCol);
|
||||
return new ExcelRange(Book, this, startRow, startColIdx, endRow, endColIdx);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit)
|
||||
{
|
||||
var range = RangeByIndexes(startRow, startCol, endRow, endCol);
|
||||
edit(range);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit)
|
||||
{
|
||||
var range = RangeByIndexes(startRow, startCol, endRow, endCol);
|
||||
edit(range);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols)
|
||||
{
|
||||
if (rows == 0 || cols == 0)
|
||||
throw new ArgumentException("Rows and columns must be greater than 0");
|
||||
uint endRow = startRow + rows - 1;
|
||||
uint endCol = startCol + cols - 1;
|
||||
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols)
|
||||
{
|
||||
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
|
||||
return RangeByLength(startRow, startColIdx, rows, cols);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit)
|
||||
{
|
||||
var range = RangeByLength(startRow, startCol, rows, cols);
|
||||
edit(range);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit)
|
||||
{
|
||||
var range = RangeByLength(startRow, startCol, rows, cols);
|
||||
edit(range);
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
817
QWERTYkez.ExcelProcessor/Editors/Interfaces.cs
Normal file
817
QWERTYkez.ExcelProcessor/Editors/Interfaces.cs
Normal file
@@ -0,0 +1,817 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>Представляет книгу Excel</summary>
|
||||
public interface IBook
|
||||
{
|
||||
/// <summary>Возвращает список всех листов в книге.</summary>
|
||||
IReadOnlyList<ISheet> GetSheets();
|
||||
|
||||
/// <summary>Возвращает лист по имени или null, если лист не найден.</summary>
|
||||
/// <param name="name">Имя листа (регистр учитывается).</param>
|
||||
ISheet? Sheet(string name);
|
||||
|
||||
/// <summary>Пытается получить лист по имени.</summary>
|
||||
/// <param name="name">Имя листа.</param>
|
||||
/// <param name="sheet">Найденный лист или null.</param>
|
||||
/// <returns>true, если лист найден, иначе false.</returns>
|
||||
bool TryGetSheet(string name, out ISheet sheet);
|
||||
|
||||
/// <summary>Пытается добавить новый лист с указанным именем и выполняет его настройку.</summary>
|
||||
/// <param name="name">Имя нового листа (должно быть уникальным в книге).</param>
|
||||
/// <param name="edit">Действие для редактирования листа (может быть null).</param>
|
||||
/// <returns>true, если лист успешно создан, иначе false (например, имя уже существует).</returns>
|
||||
bool TryAddSheet(string name, Action<ISheet>? edit = null);
|
||||
|
||||
/// <summary>Удаляет лист по имени.</summary>
|
||||
/// <param name="name">Имя удаляемого листа.</param>
|
||||
/// <returns>true, если лист существовал и был удалён, иначе false.</returns>
|
||||
bool TryRemoveSheet(string name);
|
||||
|
||||
/// <summary>Удаляет лист.</summary>
|
||||
/// <param name="sheet">Удаляемый лист.</param>
|
||||
/// <returns>true, если лист был удалён, иначе false.</returns>
|
||||
bool TryRemoveSheet(ISheet sheet);
|
||||
|
||||
/// <summary>Возвращает список всех числовых форматов, определённых в книге (встроенные и пользовательские).</summary>
|
||||
IReadOnlyList<NumberFormatPattern> GetNumberFormats();
|
||||
|
||||
/// <summary>Создаёт новый пользовательский числовой формат (если формат с таким кодом уже существует, может вернуть существующий).</summary>
|
||||
/// <param name="format">Код формата (например, "# ##0,00").</param>
|
||||
/// <returns>Объект формата, привязанный к книге.</returns>
|
||||
NumberFormatPattern CreateNumberFormat(string format);
|
||||
}
|
||||
|
||||
/// <summary>Представляет лист Excel</summary>
|
||||
public interface ISheet
|
||||
{
|
||||
/// <summary>Индекс листа в книге (начиная с 1).</summary>
|
||||
int Index { get; }
|
||||
|
||||
/// <summary>Имя листа (уникальное в книге).</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>Пытается изменить имя листа.</summary>
|
||||
/// <param name="name">Новое имя (не должно совпадать с существующими).</param>
|
||||
/// <returns>true, если переименование выполнено, иначе false.</returns>
|
||||
bool TrySetName(string name);
|
||||
|
||||
/// <summary>Возвращает строку по её индексу (начиная с 1). Если строка отсутствует, создаёт пустую.</summary>
|
||||
IRow Row(uint row);
|
||||
|
||||
/// <summary>Редактирует строку, применяя делегат, и возвращает текущий лист (fluent).</summary>
|
||||
/// <param name="row">Индекс строки (начиная с 1).</param>
|
||||
/// <param name="edit">Действие над строкой.</param>
|
||||
ISheet Row(uint row, Action<IRow> edit);
|
||||
|
||||
/// <summary>Возвращает столбец по его индексу (начиная с 1). Если столбец отсутствует, создаёт пустой.</summary>
|
||||
IColumn Col(uint col);
|
||||
|
||||
/// <summary>Редактирует столбец по индексу.</summary>
|
||||
ISheet Col(uint col, Action<IColumn> edit);
|
||||
|
||||
/// <summary>Возвращает столбец по буквенному обозначению (например, "A", "AB").</summary>
|
||||
IColumn Col(string col);
|
||||
|
||||
/// <summary>Редактирует столбец по буквенному обозначению.</summary>
|
||||
ISheet Col(string col, Action<IColumn> edit);
|
||||
|
||||
/// <summary>Возвращает диапазон ячеек по начальным и конечным индексам строк и столбцов.</summary>
|
||||
IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol);
|
||||
|
||||
/// <summary>Возвращает диапазон ячеек по начальным и конечным координатам с буквенным обозначением столбцов.</summary>
|
||||
IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol);
|
||||
|
||||
/// <summary>Редактирует диапазон, заданный начальными и конечными индексами.</summary>
|
||||
ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit);
|
||||
|
||||
/// <summary>Редактирует диапазон, заданный начальными и конечными координатами с буквенными столбцами.</summary>
|
||||
ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit);
|
||||
|
||||
/// <summary>Возвращает диапазон, начиная с указанной ячейки, заданной размером (строки x столбцы).</summary>
|
||||
IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols);
|
||||
|
||||
/// <summary>Возвращает диапазон по начальной ячейке и размеру с буквенным обозначением столбца.</summary>
|
||||
IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols);
|
||||
|
||||
/// <summary>Редактирует диапазон, заданный начальной ячейкой и размером.</summary>
|
||||
ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit);
|
||||
|
||||
/// <summary>Редактирует диапазон, заданный начальной ячейкой и размером (буква столбца).</summary>
|
||||
ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit);
|
||||
|
||||
/// <summary>Возвращает ячейку по номеру строки и столбца (оба начиная с 1).</summary>
|
||||
ICell Cell(uint row, uint col);
|
||||
|
||||
/// <summary>Редактирует ячейку по строке и столбцу.</summary>
|
||||
ISheet Cell(uint row, uint col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку.</summary>
|
||||
ISheet Cell(uint row, uint col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку.</summary>
|
||||
ISheet Cell(uint row, uint col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку с указанным числовым форматом.</summary>
|
||||
ISheet Cell(uint row, uint col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Возвращает ячейку по строке и букве столбца (например, 1, "A").</summary>
|
||||
ICell Cell(uint row, string col);
|
||||
|
||||
/// <summary>Редактирует ячейку по строке и букве столбца.</summary>
|
||||
ISheet Cell(uint row, string col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку с числовым форматом по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку по адресу (строка, буква столбца).</summary>
|
||||
ISheet Cell(uint row, string col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Очищает всё содержимое листа (значения, формулы, но не форматирование).</summary>
|
||||
void ClearContents();
|
||||
|
||||
/// <summary>Очищает всё форматирование на листе (стили, шрифты, границы, но оставляет значения).</summary>
|
||||
void ClearFormats();
|
||||
|
||||
/// <summary>Очищает и содержимое, и форматирование листа.</summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>Удаляет лист из книги</summary>
|
||||
void Remove();
|
||||
}
|
||||
|
||||
/// <summary>Представляет строку на листе.</summary>
|
||||
public interface IRow
|
||||
{
|
||||
/// <summary>Индекс строки (начиная с 1).</summary>
|
||||
uint Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущую строку в указанную позицию (как "копировать-вставить" в Excel).
|
||||
/// Исходная строка остаётся без изменений.
|
||||
/// </summary>
|
||||
/// <param name="index">Индекс строки (начиная с 1), в которую будет вставлена копия.
|
||||
/// Существующие строки, начиная с этой позиции, сдвигаются вниз.</param>
|
||||
/// <param name="copiedRow">Возвращает новую строку-копию, расположенную по указанному индексу.</param>
|
||||
/// <returns>Текущий объект IRow для цепочки вызовов (fluent).</returns>
|
||||
IRow CopyTo(uint index, out IRow copiedRow);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущую строку в новую позицию (как "вырезать-вставить" в Excel).
|
||||
/// Исходная строка удаляется, а перемещённая строка сохраняет все свои данные и форматирование.
|
||||
/// </summary>
|
||||
/// <param name="index">Новый индекс строки (начиная с 1).
|
||||
/// Другие строки сдвигаются, освобождая место для перемещённой строки.</param>
|
||||
/// <returns>Тот же объект IRow, но уже с новым индексом (fluent).</returns>
|
||||
IRow MoveTo(uint index);
|
||||
|
||||
/// <summary>Высота строки</summary>
|
||||
RowHeight Height { get; set; }
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек строки.</summary>
|
||||
IRow SetNumberFormat(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Возвращает ячейку в заданном столбце (индекс с 1).</summary>
|
||||
ICell Cell(uint col);
|
||||
|
||||
/// <summary>Редактирует ячейку в заданном столбце.</summary>
|
||||
IRow Cell(uint col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку столбца.</summary>
|
||||
IRow Cell(uint col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку столбца.</summary>
|
||||
IRow Cell(uint col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку столбца с числовым форматом.</summary>
|
||||
IRow Cell(uint col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Возвращает ячейку по букве столбца.</summary>
|
||||
ICell Cell(string col);
|
||||
|
||||
/// <summary>Редактирует ячейку по букве столбца.</summary>
|
||||
IRow Cell(string col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку по букве столбца.</summary>
|
||||
IRow Cell(string col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку по букве столбца.</summary>
|
||||
IRow Cell(string col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку по букве столбца с числовым форматом.</summary>
|
||||
IRow Cell(string col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Очищает содержимое всех ячеек строки (значения, формулы).</summary>
|
||||
void ClearContents();
|
||||
|
||||
/// <summary>Очищает форматирование всех ячеек строки.</summary>
|
||||
void ClearFormats();
|
||||
|
||||
/// <summary>Очищает содержимое и форматирование строки.</summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>Удаляет строку с листа (сдвигая нижние строки вверх).</summary>
|
||||
void Remove();
|
||||
}
|
||||
|
||||
/// <summary>Представляет столбец на листе.</summary>
|
||||
public interface IColumn
|
||||
{
|
||||
/// <summary>Индекс столбца (начиная с 1).</summary>
|
||||
uint Index { get; }
|
||||
|
||||
/// <summary>Буквенное обозначение столбца (например, "A", "Z", "AA").</summary>
|
||||
string IndexLetter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущий столбец в указанную позицию по числовому индексу (как "копировать-вставить").
|
||||
/// Исходный столбец остаётся без изменений.
|
||||
/// </summary>
|
||||
/// <param name="index">Числовой индекс столбца (начиная с 1), куда будет вставлена копия.
|
||||
/// Существующие столбцы, начиная с этой позиции, сдвигаются вправо.</param>
|
||||
/// <param name="copiedColumn">Возвращает новый столбец-копию, расположенный по указанному индексу.</param>
|
||||
/// <returns>Текущий объект IColumn для цепочки вызовов.</returns>
|
||||
IColumn CopyTo(uint index, out IColumn copiedColumn);
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущий столбец в указанную позицию по буквенному обозначению (как "копировать-вставить").
|
||||
/// </summary>
|
||||
/// <param name="index">Буквенное обозначение столбца (например, "D", "AA"), куда будет вставлена копия.</param>
|
||||
/// <param name="copiedColumn">Возвращает новый столбец-копию.</param>
|
||||
/// <returns>Текущий объект IColumn.</returns>
|
||||
IColumn CopyTo(string index, out IColumn copiedColumn);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий столбец в новую позицию по числовому индексу (как "вырезать-вставить").
|
||||
/// Исходный столбец удаляется, а перемещённый столбец сохраняет свои данные и форматирование.
|
||||
/// </summary>
|
||||
/// <param name="index">Новый числовой индекс столбца (начиная с 1).</param>
|
||||
/// <returns>Тот же объект IColumn с новым индексом (fluent).</returns>
|
||||
IColumn MoveTo(uint index);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий столбец в новую позицию по буквенному обозначению (как "вырезать-вставить").
|
||||
/// </summary>
|
||||
/// <param name="index">Новое буквенное обозначение столбца (например, "E").</param>
|
||||
/// <returns>Тот же объект IColumn с новым индексом.</returns>
|
||||
IColumn MoveTo(string index);
|
||||
|
||||
/// <summary>Ширина столбца (чтение и запись).</summary>
|
||||
ColumnWidth Width { get; set; }
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек столбца.</summary>
|
||||
IColumn SetNumberFormat(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Возвращает ячейку в заданной строке (индекс с 1).</summary>
|
||||
ICell Cell(uint row);
|
||||
|
||||
/// <summary>Редактирует ячейку в заданной строке.</summary>
|
||||
IColumn Cell(uint row, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку строки.</summary>
|
||||
IColumn Cell(uint row, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку строки.</summary>
|
||||
IColumn Cell(uint row, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку строки с числовым форматом.</summary>
|
||||
IColumn Cell(uint row, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Очищает содержимое всех ячеек столбца.</summary>
|
||||
void ClearContents();
|
||||
|
||||
/// <summary>Очищает форматирование всех ячеек столбца.</summary>
|
||||
void ClearFormats();
|
||||
|
||||
/// <summary>Очищает содержимое и форматирование столбца.</summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>Удаляет столбец с листа (сдвигая правые столбцы влево).</summary>
|
||||
void Remove();
|
||||
}
|
||||
|
||||
/// <summary>Представляет прямоугольный диапазон ячеек на листе.</summary>
|
||||
public interface IRange
|
||||
{
|
||||
/// <summary>Объединяет ячейки диапазона в одну (содержимое левой верхней ячейки сохраняется).</summary>
|
||||
/// <returns>true, если объединение выполнено (диапазон не был объединён ранее).</returns>
|
||||
bool TryMerge();
|
||||
|
||||
/// <summary>Разъединяет ранее объединённый диапазон (восстанавливает исходные ячейки, содержимое остаётся в левой верхней).</summary>
|
||||
void Unmerge();
|
||||
|
||||
/// <summary>Проверяет, объединён ли диапазон (как единое целое).</summary>
|
||||
bool IsMerged { get; }
|
||||
|
||||
/// <summary>Возвращает диапазон, представляющий объединённую область, если текущий диапазон является частью объединения.</summary>
|
||||
IRange? GetMergedRange();
|
||||
|
||||
/// <summary>Пытается получить объединённый диапазон, содержащий текущий.</summary>
|
||||
bool TryGetMergedRange(IRange range);
|
||||
|
||||
/// <summary>Номер начальной строки диапазона (1-based).</summary>
|
||||
uint RowStart { get; }
|
||||
|
||||
/// <summary>Номер конечной строки.</summary>
|
||||
uint RowEnd { get; }
|
||||
|
||||
/// <summary>Номер начального столбца.</summary>
|
||||
uint ColStart { get; }
|
||||
|
||||
/// <summary>Номер конечного столбца.</summary>
|
||||
uint ColEnd { get; }
|
||||
|
||||
/// <summary>Буквенное обозначение начального столбца.</summary>
|
||||
string ColStartLetter { get; }
|
||||
|
||||
/// <summary>Буквенное обозначение конечного столбца.</summary>
|
||||
string ColEndLetter { get; }
|
||||
|
||||
/// <summary>Количество строк в диапазоне.</summary>
|
||||
uint Rows { get; }
|
||||
|
||||
/// <summary>Количество столбцов в диапазоне.</summary>
|
||||
uint Cols { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущий диапазон в новую позицию (верхний левый угол) – как "копировать-вставить".
|
||||
/// Исходный диапазон остаётся неизменным.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки (начиная с 1) для верхнего левого угла вставляемого диапазона.</param>
|
||||
/// <param name="colIndex">Номер столбца (начиная с 1) для верхнего левого угла.</param>
|
||||
/// <param name="copiedRange">Возвращает новый диапазон-копию, расположенный по указанным координатам.</param>
|
||||
/// <returns>Текущий объект IRange (fluent).</returns>
|
||||
IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange);
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой – как "копировать-вставить".
|
||||
/// Исходный диапазон остаётся неизменным.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки (начиная с 1).</param>
|
||||
/// <param name="colIndex">Буквенное обозначение столбца (например, "C").</param>
|
||||
/// <param name="copiedRange">Возвращает новый диапазон-копию.</param>
|
||||
/// <returns>Текущий объект IRange.</returns>
|
||||
IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий диапазон в новую позицию (верхний левый угол) – как "вырезать-вставить".
|
||||
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки для нового положения верхнего левого угла.</param>
|
||||
/// <param name="colIndex">Номер столбца для нового положения.</param>
|
||||
/// <returns>Тот же объект IRange, но уже с новыми координатами (fluent).</returns>
|
||||
IRange MoveTo(uint rowIndex, uint colIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой – как "вырезать-вставить".
|
||||
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки.</param>
|
||||
/// <param name="colIndex">Буквенное обозначение столбца.</param>
|
||||
/// <returns>Тот же объект IRange с новыми координатами.</returns>
|
||||
IRange MoveTo(uint rowIndex, string colIndex);
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек диапазона.</summary>
|
||||
IRange SetNumberFormat(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
|
||||
IRange SetCellAlign(CellAlign format);
|
||||
|
||||
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
|
||||
IRange SetCellBorder(CellBorder format);
|
||||
|
||||
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
|
||||
IRange SetCellFill(CellFill format);
|
||||
|
||||
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
|
||||
IRange SetCellFont(CellFont format);
|
||||
|
||||
/// <summary>Перечисляет все ячейки диапазона (по строкам).</summary>
|
||||
IEnumerable<ICell> Cells { get; }
|
||||
|
||||
/// <summary>Получает ячейку внутри диапазона по относительным координатам (начиная с 1).</summary>
|
||||
bool GetSubCell(uint row, uint col, out ICell cell);
|
||||
|
||||
/// <summary>Редактирует ячейку внутри диапазона по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку по относительным координатам.</summary>
|
||||
bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Получает ячейку по относительной строке и букве столбца.</summary>
|
||||
bool GetSubCell(uint row, string col, out ICell cell);
|
||||
|
||||
/// <summary>Редактирует ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, Action<ICell> edit);
|
||||
|
||||
/// <summary>Устанавливает строковое значение в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, bool value);
|
||||
|
||||
/// <summary>Устанавливает формулу в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает дату в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число в ячейку по относительной строке и букве столбца.</summary>
|
||||
bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Очищает содержимое всех ячеек диапазона.</summary>
|
||||
void ClearContents();
|
||||
|
||||
/// <summary>Очищает форматирование всех ячеек диапазона.</summary>
|
||||
void ClearFormats();
|
||||
|
||||
/// <summary>Очищает и содержимое, и форматирование.</summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>Представляет одну ячейку на листе.</summary>
|
||||
public interface ICell
|
||||
{
|
||||
/// <summary>Проверяет, входит ли ячейка в объединённый диапазон.</summary>
|
||||
bool IsMerged { get; }
|
||||
|
||||
/// <summary>Возвращает объединённый диапазон, если ячейка объединена, иначе null.</summary>
|
||||
IRange? GetMergedRange();
|
||||
|
||||
/// <summary>Пытается получить объединённый диапазон, содержащий эту ячейку.</summary>
|
||||
bool TryGetMergedRange(IRange range);
|
||||
|
||||
/// <summary>Номер строки (1-based).</summary>
|
||||
uint Row { get; }
|
||||
|
||||
/// <summary>Номер столбца (1-based).</summary>
|
||||
uint Col { get; }
|
||||
|
||||
/// <summary>Буква столбца.</summary>
|
||||
string ColLetter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущую ячейку в указанную позицию (как "копировать-вставить").
|
||||
/// Исходная ячейка остаётся без изменений.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки (начиная с 1), куда будет вставлена копия.</param>
|
||||
/// <param name="colIndex">Номер столбца (начиная с 1).</param>
|
||||
/// <param name="copiedCell">Возвращает новую ячейку-копию.</param>
|
||||
/// <returns>Текущий объект ICell для цепочки вызовов.</returns>
|
||||
ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell);
|
||||
|
||||
/// <summary>
|
||||
/// Копирует текущую ячейку в указанную позицию с буквенным обозначением столбца (как "копировать-вставить").
|
||||
/// Исходная ячейка остаётся без изменений.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Номер строки.</param>
|
||||
/// <param name="colIndex">Буквенное обозначение столбца (например, "B").</param>
|
||||
/// <param name="copiedCell">Возвращает новую ячейку-копию.</param>
|
||||
/// <returns>Текущий объект ICell.</returns>
|
||||
ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущую ячейку в новую позицию (как "вырезать-вставить").
|
||||
/// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Новый номер строки.</param>
|
||||
/// <param name="colIndex">Новый номер столбца.</param>
|
||||
/// <returns>Тот же объект ICell с новыми координатами (fluent).</returns>
|
||||
ICell MoveTo(uint rowIndex, uint colIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущую ячейку в новую позицию с буквенным обозначением столбца (как "вырезать-вставить").
|
||||
/// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">Новый номер строки.</param>
|
||||
/// <param name="colIndex">Буквенное обозначение столбца.</param>
|
||||
/// <returns>Тот же объект ICell с новыми координатами.</returns>
|
||||
ICell MoveTo(uint rowIndex, string colIndex);
|
||||
|
||||
/// <summary>Высота строки, содержащей ячейку (чтение и запись).</summary>
|
||||
RowHeight Height { get; set; }
|
||||
|
||||
/// <summary>Ширина столбца, содержащего ячейку.</summary>
|
||||
ColumnWidth Width { get; set; }
|
||||
|
||||
/// <summary>Проверяет, содержит ли ячейка числовое значение (целое или с плавающей точкой).</summary>
|
||||
bool IsNumber { get; }
|
||||
|
||||
/// <summary>Проверяет, является ли содержимое логическим (TRUE/FALSE).</summary>
|
||||
bool IsBoolean { get; }
|
||||
|
||||
/// <summary>Проверяет, содержит ли ячейка код ошибки (например, #DIV/0!).</summary>
|
||||
bool IsError { get; }
|
||||
|
||||
/// <summary>Проверяет, интерпретируется ли значение как дата (по числовому формату).</summary>
|
||||
bool IsDate { get; }
|
||||
|
||||
/// <summary>Проверяет, заблокирована ли ячейка (атрибут защиты).</summary>
|
||||
bool IsLocked { get; }
|
||||
|
||||
/// <summary>Проверяет, содержит ли ячейка формулу.</summary>
|
||||
bool HasFormula { get; }
|
||||
|
||||
/// <summary>Возвращает текстовое представление содержимого ячейки (как оно отображается).</summary>
|
||||
string GetString();
|
||||
|
||||
/// <summary>Возвращает числовой формат ячейки.</summary>
|
||||
NumberFormatPattern? GetNumberFormat();
|
||||
|
||||
/// <summary>Возвращает выравнивание текста ячейки.</summary>
|
||||
CellAlign GetCellAlign();
|
||||
|
||||
/// <summary>Возвращает границы ячейки.</summary>
|
||||
CellBorder GetCellBorder();
|
||||
|
||||
/// <summary>Возвращает заливку ячейки.</summary>
|
||||
CellFill GetCellFill();
|
||||
|
||||
/// <summary>Возвращает шрифт ячейки.</summary>
|
||||
CellFont GetCellFont();
|
||||
|
||||
/// <summary>Пытается извлечь логическое значение.</summary>
|
||||
bool TryGetBoolean(out bool value);
|
||||
|
||||
/// <summary>Возвращает логическое значение или null, если тип не соответствует.</summary>
|
||||
bool? GetBoolean();
|
||||
|
||||
/// <summary>Пытается извлечь дату и время.</summary>
|
||||
bool TryGetDate(out DateTime value);
|
||||
|
||||
/// <summary>Возвращает дату или null.</summary>
|
||||
DateTime? TryGetDate();
|
||||
|
||||
/// <summary>Пытается извлечь число (double).</summary>
|
||||
bool TryGetNumber(out double value);
|
||||
|
||||
/// <summary>Возвращает число или null.</summary>
|
||||
double? TryGetNumber();
|
||||
|
||||
/// <summary>Пытается установить формулу (без вычисленного значения).</summary>
|
||||
/// <param name="formula">Текст формулы (например, "SUM(A1:A5)").</param>
|
||||
/// <param name="format">Необязательный числовой формат для результата.</param>
|
||||
bool TrySet(string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает формулу (выбрасывает исключение при ошибке).</summary>
|
||||
ICell Set(string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает числовой формат ячейки (не меняя значение).</summary>
|
||||
ICell Set(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание текста ячейки.</summary>
|
||||
ICell Set(CellAlign format);
|
||||
|
||||
/// <summary>Устанавливает границы ячейки.</summary>
|
||||
ICell Set(CellBorder format);
|
||||
|
||||
/// <summary>Устанавливает заливку ячейки.</summary>
|
||||
ICell Set(CellFill format);
|
||||
|
||||
/// <summary>Устанавливает шрифт ячейки.</summary>
|
||||
ICell Set(CellFont format);
|
||||
|
||||
/// <summary>Устанавливает богатый текст (форматированный) с помощью делегата.</summary>
|
||||
ICell Set(Action<ICellText> value);
|
||||
|
||||
/// <summary>Устанавливает простое текстовое значение (без форматирования).</summary>
|
||||
ICell Set(string value);
|
||||
|
||||
/// <summary>Устанавливает логическое значение.</summary>
|
||||
ICell Set(bool value);
|
||||
|
||||
/// <summary>Устанавливает дату, опционально с числовым форматом.</summary>
|
||||
ICell Set(DateTime value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает десятичное число (сохраняется как double).</summary>
|
||||
ICell Set(decimal value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число двойной точности.</summary>
|
||||
ICell Set(double value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает число с плавающей точкой.</summary>
|
||||
ICell Set(float value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает целое число.</summary>
|
||||
ICell Set(int value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает длинное целое число.</summary>
|
||||
ICell Set(long value, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Очищает содержимое ячейки (значение/формулу, но не форматирование).</summary>
|
||||
void ClearContent();
|
||||
|
||||
/// <summary>Очищает форматирование ячейки (сбрасывает стиль).</summary>
|
||||
void ClearFormat();
|
||||
|
||||
/// <summary>Очищает и содержимое, и форматирование.</summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>Представляет богатый текст внутри ячейки (несколько форматированных фрагментов).</summary>
|
||||
public interface ICellText
|
||||
{
|
||||
/// <summary>Количество фрагментов (Run) в тексте.</summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>Возвращает все фрагменты.</summary>
|
||||
IEnumerable<IRun> GetRuns();
|
||||
|
||||
/// <summary>Возвращает фрагмент по индексу или null.</summary>
|
||||
IRun? GetRunAt(int index);
|
||||
|
||||
/// <summary>Пытается получить фрагмент по индексу.</summary>
|
||||
bool TryGetRunAt(int index, out IRun run);
|
||||
|
||||
/// <summary>Первый фрагмент или null.</summary>
|
||||
IRun? First();
|
||||
|
||||
/// <summary>Пытается получить первый фрагмент.</summary>
|
||||
bool TryGetFirst(out IRun run);
|
||||
|
||||
/// <summary>Последний фрагмент или null.</summary>
|
||||
IRun? Last();
|
||||
|
||||
/// <summary>Пытается получить последний фрагмент.</summary>
|
||||
bool TryGetLast(out IRun run);
|
||||
|
||||
/// <summary>Пытается удалить фрагмент.</summary>
|
||||
bool TryRemoveRun(IRun run);
|
||||
|
||||
/// <summary>Удаляет фрагмент по индексу.</summary>
|
||||
bool TryRemoveRun(int index);
|
||||
|
||||
/// <summary>Удаляет фрагмент по индексу и возвращает удалённый.</summary>
|
||||
bool TryRemoveRun(int index, out IRun? removed);
|
||||
|
||||
/// <summary>Добавляет разрыв строки (перенос внутри ячейки).</summary>
|
||||
ICellText AddBreak();
|
||||
|
||||
/// <summary>Добавляет обычный текстовый фрагмент.</summary>
|
||||
ICellText AddRun(string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Добавляет фрагмент с последующим разрывом строки.</summary>
|
||||
ICellText AddRunBreak(string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Добавляет подстрочный фрагмент (эквивалентно AddRun с Vertical = Subscript).</summary>
|
||||
ICellText AddSubRun(string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Добавляет надстрочный фрагмент.</summary>
|
||||
ICellText AddSupRun(string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Вставляет фрагмент по индексу.</summary>
|
||||
bool TryInsertRun(int index, string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Вставляет фрагмент с последующим разрывом строки по индексу.</summary>
|
||||
bool TryInsertRunBreak(int index, string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Вставляет подстрочный фрагмент по индексу.</summary>
|
||||
bool TryInsertSubRun(int index, string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Вставляет надстрочный фрагмент по индексу.</summary>
|
||||
bool TryInsertSupRun(int index, string text, RunFormat? format = null);
|
||||
|
||||
/// <summary>Применяет заданный формат ко всем существующим фрагментам (поверх их текущего форматирования, заменяя неуказанные свойства).</summary>
|
||||
void ApplyFormatToAllRuns(RunFormat format);
|
||||
|
||||
/// <summary>Удаляет все фрагменты, очищая текст ячейки.</summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>Представляет один форматированный фрагмент текста внутри ячейки.</summary>
|
||||
public interface IRun
|
||||
{
|
||||
/// <summary>Текст фрагмента.</summary>
|
||||
string Text { get; set; }
|
||||
|
||||
/// <summary>Форматирование фрагмента (может быть null, что означает отсутствие явного форматирования).</summary>
|
||||
RunFormat? Format { get; set; }
|
||||
}
|
||||
20
QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs
Normal file
20
QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
public class NumberFormatPattern
|
||||
{
|
||||
public string Format { get; }
|
||||
internal int? Id { get; private set; }
|
||||
|
||||
public NumberFormatPattern(string format, ushort id = 0)
|
||||
{
|
||||
Format = format ?? throw new ArgumentNullException(nameof(format));
|
||||
if (id != 0) Id = id;
|
||||
}
|
||||
|
||||
internal void Attach(ushort id) => Id = id;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is NumberFormatPattern other && Format == other.Format;
|
||||
|
||||
public override int GetHashCode() => Format?.GetHashCode() ?? 0;
|
||||
}
|
||||
29
QWERTYkez.ExcelProcessor/Editors/RowHeight.cs
Normal file
29
QWERTYkez.ExcelProcessor/Editors/RowHeight.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет типобезопасное представление высоты строки в Excel с поддержкой различных единиц измерения.
|
||||
/// </summary>
|
||||
public readonly struct RowHeight(double points)
|
||||
{
|
||||
private const double POINTS_PER_INCH = 72.0;
|
||||
private const double INCH_PER_CM = 1.0 / 2.54;
|
||||
private const double POINTS_PER_CM = POINTS_PER_INCH * INCH_PER_CM; // ≈ 28.3464566929
|
||||
private const double POINTS_PER_MM = POINTS_PER_CM / 10.0; // ≈ 2.83464566929
|
||||
|
||||
private const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa
|
||||
private const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT;
|
||||
|
||||
/// <summary>Высота в пунктах (points).</summary>
|
||||
public double Points { get; } = points;
|
||||
|
||||
public static RowHeight FromPoints(double points) => new(points);
|
||||
public static RowHeight FromInch(double inch) => new(inch * POINTS_PER_INCH);
|
||||
public static RowHeight FromDXA(double dxa) => new(dxa * POINTS_PER_DXA);
|
||||
public static RowHeight FromCM(double cm) => new(cm * POINTS_PER_CM);
|
||||
public static RowHeight FromMM(double mm) => new(mm * POINTS_PER_MM);
|
||||
|
||||
public double Inch => Points / POINTS_PER_INCH;
|
||||
public double DXA => Points / POINTS_PER_DXA;
|
||||
public double CM => Points / POINTS_PER_CM;
|
||||
public double MM => Points / POINTS_PER_MM;
|
||||
}
|
||||
70
QWERTYkez.ExcelProcessor/Editors/RunFormat.cs
Normal file
70
QWERTYkez.ExcelProcessor/Editors/RunFormat.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
namespace QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет форматирование отдельного фрагмента (Run) внутри ячейки с богатым текстом.
|
||||
/// Применяется только к тексту внутри <see cref="ICellText"/>.
|
||||
/// </summary>
|
||||
public readonly struct RunFormat
|
||||
{
|
||||
/// <summary>Жирное начертание фрагмента.</summary>
|
||||
public bool? IsBold { get; init; }
|
||||
|
||||
/// <summary>Курсив фрагмента.</summary>
|
||||
public bool? IsItalic { get; init; }
|
||||
|
||||
/// <summary>Стиль подчёркивания (одинарное, двойное, волнистое и т.д.).</summary>
|
||||
public UnderlineStyle? Underline { get; init; }
|
||||
|
||||
/// <summary>Одинарное зачёркивание.</summary>
|
||||
public bool? IsStrike { get; init; }
|
||||
|
||||
/// <summary>Цвет текста фрагмента.</summary>
|
||||
public ExColor? Color { get; init; }
|
||||
|
||||
/// <summary>Размер шрифта фрагмента в пунктах.</summary>
|
||||
public double? FontSize { get; init; }
|
||||
|
||||
/// <summary>Имя шрифта фрагмента (например, "Calibri").</summary>
|
||||
public string? FontFamily { get; init; }
|
||||
|
||||
/// <summary>Вертикальное смещение (надстрочный или подстрочный).</summary>
|
||||
public VerticalTextRunAlignment? Vertical { get; init; }
|
||||
|
||||
/*
|
||||
|
||||
методы для извлечения OpenXmlElement или других более удобных типов
|
||||
|
||||
public bool TryExtract(out List<...> elements);
|
||||
|
||||
или
|
||||
|
||||
public bool TrySetFor(InlineString str)
|
||||
|
||||
или
|
||||
|
||||
public bool TrySetFor(ExcelRun str)
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>Определяет стиль подчёркивания текста в ячейке или в части текста (Run).</summary>
|
||||
public enum UnderlineStyle
|
||||
{
|
||||
/// <summary>Одинарное сплошное подчёркивание.</summary>
|
||||
Single,
|
||||
/// <summary>Двойное сплошное подчёркивание.</summary>
|
||||
Double,
|
||||
/// <summary>Одинарное подчёркивание, используемое для бухгалтерских форматов (нижняя граница ячейки).</summary>
|
||||
SingleAccounting,
|
||||
/// <summary>Двойное подчёркивание для бухгалтерских форматов.</summary>
|
||||
DoubleAccounting
|
||||
}
|
||||
|
||||
/// <summary>Вертикальное смещение текста внутри прогона (Run) – надстрочный или подстрочный.</summary>
|
||||
public enum VerticalTextRunAlignment
|
||||
{
|
||||
/// <summary>Надстрочный текст (верхний индекс).</summary>
|
||||
Superscript,
|
||||
/// <summary>Подстрочный текст (нижний индекс).</summary>
|
||||
Subscript
|
||||
}
|
||||
320
QWERTYkez.ExcelProcessor/ExcelProcessor.cs
Normal file
320
QWERTYkez.ExcelProcessor/ExcelProcessor.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Статический класс для работы с документами Excel через процессоры чтения/записи.
|
||||
/// </summary>
|
||||
public static class ExcelProcessor
|
||||
{
|
||||
#region Read Operations
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(string sourcePath, Action<IExcelReader> read)
|
||||
{
|
||||
return TryRead(new FileInfo(sourcePath), read);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(FileInfo sourceFile, Action<IExcelReader> read)
|
||||
{
|
||||
if (sourceFile is null || read is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] SourceFile or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = ExcelReader.CreateInternal(sourceFile);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
read(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in read action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read Operations from Memory
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из массива байт только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="data">Массив байт, содержащий документ .xlsx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(byte[] data, Action<IExcelReader> read)
|
||||
=> TryRead(new ReadOnlyMemory<byte>(data), read);
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="data">Буфер с документом .xlsx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(ReadOnlyMemory<byte> data, Action<IExcelReader> read)
|
||||
{
|
||||
if (data.IsEmpty || read is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Data is empty or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = ExcelReader.CreateFromData(data);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
read(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in read action from memory: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(string sourcePath, Action<IExcelWriter> write)
|
||||
{
|
||||
return TryWrite(sourcePath, sourcePath, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(FileInfo sourceFile, Action<IExcelWriter> write)
|
||||
{
|
||||
return TryWrite(sourceFile, sourceFile.FullName, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(string sourcePath, string? destinationPath, Action<IExcelWriter> write)
|
||||
{
|
||||
return TryWrite(new FileInfo(sourcePath), destinationPath, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(FileInfo sourceFile, string? destinationPath, Action<IExcelWriter> write)
|
||||
{
|
||||
if (sourceFile is null || write is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] SourceFile or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = ExcelWriter.CreateInternal(sourceFile, destinationPath);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
write(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in write action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations from Memory
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из массива байт для записи, выполнить действия и сохранить результат по указанному пути.
|
||||
/// </summary>
|
||||
/// <param name="data">Массив байт, содержащий документ .xlsx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(byte[] data, string destinationPath, Action<IExcelWriter> write)
|
||||
=> TryWrite(new ReadOnlyMemory<byte>(data), destinationPath, write);
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> для записи, выполнить действия и сохранить результат по указанному пути.
|
||||
/// </summary>
|
||||
/// <param name="data">Буфер с документом .xlsx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(ReadOnlyMemory<byte> data, string destinationPath, Action<IExcelWriter> write)
|
||||
{
|
||||
if (data.IsEmpty || write is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Data is empty or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = ExcelWriter.CreateFromData(data, destinationPath);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
write(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in write action from memory: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Excel, отредактировать его и вернуть результат в виде массива байт.
|
||||
/// </summary>
|
||||
/// <param name="result">Результирующий массив байт (если операция успешна).</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(out byte[] result, Action<IExcelWriter> write)
|
||||
{
|
||||
result = null!;
|
||||
if (write is null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = ExcelWriter.CreateNew();
|
||||
write(writer);
|
||||
result = writer.GetDocumentBytes(); // добавим этот метод в ExcelWriter
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Excel, отредактировать его и записать в указанный поток.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">Поток, в который будет записан документ.</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(Stream outputStream, Action<IExcelWriter> write)
|
||||
{
|
||||
if (write is null || outputStream is null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = ExcelWriter.CreateNew();
|
||||
write(writer);
|
||||
writer.SaveTo(outputStream);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Excel, отредактировать его и сохранить на диск.
|
||||
/// </summary>
|
||||
/// <param name="destinationPath">Путь для сохранения результата.</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(string destinationPath, Action<IExcelWriter> write)
|
||||
{
|
||||
if (string.IsNullOrEmpty(destinationPath) || write is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = ExcelWriter.CreateNew(destinationPath);
|
||||
write(writer);
|
||||
writer.Save();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Excel, отредактировать его и прочитать результат в памяти.
|
||||
/// </summary>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <param name="read">Действия для чтения полученного документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryRead(Action<IExcelWriter> write, Action<IExcelReader> read)
|
||||
{
|
||||
if (write is null || read is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = ExcelWriter.CreateNew();
|
||||
write(writer);
|
||||
using var reader = writer.ToReader();
|
||||
read(reader);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
276
QWERTYkez.ExcelProcessor/ExcelReader.cs
Normal file
276
QWERTYkez.ExcelProcessor/ExcelReader.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет потокобезопасный процессор только для чтения документов Excel (xlsx / xlsm) формата.
|
||||
/// <para>Не поддерживает операции изменения документа.</para>
|
||||
/// </summary>
|
||||
internal class ExcelReader : IDisposable, IExcelReader
|
||||
{
|
||||
internal MemoryStream _ms = null!;
|
||||
internal SpreadsheetDocument _doc = null!;
|
||||
internal bool _disposed;
|
||||
internal readonly object _syncLock = new();
|
||||
internal string? _originalSourcePath;
|
||||
|
||||
internal ExcelReader() { }
|
||||
|
||||
#region Factory Methods
|
||||
|
||||
internal static ExcelReader? CreateInternal(FileInfo sourceFile)
|
||||
{
|
||||
if (sourceFile is null || !sourceFile.Exists)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
MemoryStream? ms = null;
|
||||
SpreadsheetDocument? doc = null;
|
||||
|
||||
try
|
||||
{
|
||||
ms = new MemoryStream();
|
||||
using (var file = new FileStream(sourceFile.FullName,
|
||||
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
file.CopyTo(ms);
|
||||
}
|
||||
ms.Position = 0;
|
||||
|
||||
doc = SpreadsheetDocument.Open(ms, isEditable: false,
|
||||
new OpenSettings { AutoSave = false });
|
||||
|
||||
if (doc is not null)
|
||||
{
|
||||
var processor = new ExcelReader
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_originalSourcePath = sourceFile.FullName,
|
||||
FilePath = sourceFile.FullName
|
||||
};
|
||||
|
||||
return processor;
|
||||
}
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Document body is null or empty");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
|
||||
#endif
|
||||
}
|
||||
|
||||
doc?.Dispose();
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static ExcelReader? CreateFromData(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
if (data.IsEmpty)
|
||||
return null;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
ms.Write(data.ToArray(), 0, data.Length);
|
||||
ms.Position = 0;
|
||||
|
||||
var doc = SpreadsheetDocument.Open(ms, false,
|
||||
new OpenSettings { AutoSave = false });
|
||||
|
||||
if (doc is not null)
|
||||
{
|
||||
var processor = new ExcelReader
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
FilePath = null // из памяти – нет файла
|
||||
};
|
||||
|
||||
return processor;
|
||||
}
|
||||
doc?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Writers
|
||||
|
||||
public bool TryWrite(string destinationPath, Action<IExcelWriter> action)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(destinationPath))
|
||||
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
|
||||
if (action is null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
// Копируем данные из текущего потока
|
||||
byte[] data;
|
||||
lock (_syncLock)
|
||||
{
|
||||
_ms.Position = 0;
|
||||
data = _ms.ToArray();
|
||||
}
|
||||
|
||||
using var writable = ExcelWriter.CreateFromData(new ReadOnlyMemory<byte>(data), destinationPath);
|
||||
if (writable is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
action(writable);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(Action<IExcelWriter> write, Action<IExcelReader> read)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (write is null) throw new ArgumentNullException(nameof(write));
|
||||
if (read is null) throw new ArgumentNullException(nameof(read));
|
||||
|
||||
// Копируем текущие данные
|
||||
byte[] data;
|
||||
lock (_syncLock)
|
||||
{
|
||||
_ms.Position = 0;
|
||||
data = _ms.ToArray();
|
||||
}
|
||||
|
||||
ExcelWriter? writable = null;
|
||||
ExcelReader? resultReader = null;
|
||||
try
|
||||
{
|
||||
// Создаём редактируемую копию (без привязки к файлу)
|
||||
writable = ExcelWriter.CreateFromData(data);
|
||||
if (writable is null)
|
||||
return false;
|
||||
|
||||
// Применяем изменения
|
||||
write(writable);
|
||||
|
||||
// Сохраняем изменения в поток и создаём read-only процессор
|
||||
resultReader = writable.ToReader();
|
||||
|
||||
if (resultReader is null)
|
||||
return false;
|
||||
|
||||
// Работаем с изменённой копией
|
||||
read(resultReader);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Гарантированно освобождаем созданные процессоры
|
||||
resultReader?.Dispose();
|
||||
writable?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Получает путь к исходному файлу.
|
||||
/// </summary>
|
||||
public string? FilePath { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
|
||||
/// </summary>
|
||||
public bool IsValid => !_disposed && _doc is not null && _ms is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Получает текущий размер документа в байтах.
|
||||
/// </summary>
|
||||
public long DocumentSize => _ms.Length;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read Operations
|
||||
|
||||
/// <summary>
|
||||
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
|
||||
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
|
||||
/// </summary>
|
||||
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
|
||||
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
|
||||
public ISet<string> FindPlaceholders()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
return PlaceholderFinder.FindInDocument(_doc);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Pattern
|
||||
|
||||
internal void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_doc.Dispose();
|
||||
_ms.Dispose();
|
||||
}
|
||||
|
||||
_doc = null!;
|
||||
_ms = null!;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~ExcelReader()
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
1293
QWERTYkez.ExcelProcessor/ExcelWriter.cs
Normal file
1293
QWERTYkez.ExcelProcessor/ExcelWriter.cs
Normal file
File diff suppressed because it is too large
Load Diff
14
QWERTYkez.ExcelProcessor/IExcelReader.cs
Normal file
14
QWERTYkez.ExcelProcessor/IExcelReader.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
public interface IExcelReader
|
||||
{
|
||||
long DocumentSize { get; }
|
||||
string? FilePath { get; }
|
||||
bool IsValid { get; }
|
||||
|
||||
ISet<string> FindPlaceholders();
|
||||
|
||||
|
||||
bool TryWrite(string destinationPath, Action<IExcelWriter> action);
|
||||
bool TryWrite(Action<IExcelWriter> write, Action<IExcelReader> read);
|
||||
}
|
||||
29
QWERTYkez.ExcelProcessor/IExcelWriter.cs
Normal file
29
QWERTYkez.ExcelProcessor/IExcelWriter.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using QWERTYkez.ExcelProcessor.Editors;
|
||||
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
public interface IExcelWriter : IBook
|
||||
{
|
||||
void Save();
|
||||
void SaveTo(string path);
|
||||
Task SaveToAsync(string path, CancellationToken cancellationToken = default);
|
||||
bool TrySaveTo(string path, out Exception? error);
|
||||
|
||||
void Replace(string oldValue, string newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(string oldValue, double newValue, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(string oldValue, float newValue, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(string oldValue, int newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(string oldValue, long newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IDictionary<string, string> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IDictionary<string, double> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IDictionary<string, float> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IDictionary<string, int> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IDictionary<string, long> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IEnumerable<KeyValuePair<string, double>> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IEnumerable<KeyValuePair<string, float>> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IEnumerable<KeyValuePair<string, int>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
void Replace(IEnumerable<KeyValuePair<string, long>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
void Edit(Action<IBook> edit);
|
||||
}
|
||||
135
QWERTYkez.ExcelProcessor/NormalizedSet.cs
Normal file
135
QWERTYkez.ExcelProcessor/NormalizedSet.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Множество строк, которое автоматически приводит все добавляемые элементы
|
||||
/// к верхнему регистру и удаляет диакритические знаки (например, 'ё' -> 'Е').
|
||||
/// Реализует ISet<string>, поэтому может использоваться там, где ожидается этот интерфейс.
|
||||
/// </summary>
|
||||
public class NormalizedSet : ISet<string>
|
||||
{
|
||||
private readonly HashSet<string> _inner;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт пустое нормализованное множество.
|
||||
/// </summary>
|
||||
public NormalizedSet()
|
||||
{
|
||||
_inner = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт нормализованное множество, заполненное элементами из указанной коллекции.
|
||||
/// </summary>
|
||||
/// <param name="collection">Коллекция, элементы которой будут нормализованы и добавлены.</param>
|
||||
public NormalizedSet(IEnumerable<string> collection)
|
||||
{
|
||||
_inner = [.. collection.Select(Normalize)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Нормализует строку: верхний регистр и удаление диакритики.
|
||||
/// </summary>
|
||||
private static string Normalize(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return s;
|
||||
|
||||
var normalized = s.Normalize(NormalizationForm.FormD);
|
||||
var sb = new StringBuilder();
|
||||
foreach (char c in normalized)
|
||||
{
|
||||
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString().Normalize(NormalizationForm.FormC).ToUpperInvariant();
|
||||
}
|
||||
|
||||
// ---------- Реализация ISet<string> ----------
|
||||
|
||||
public bool Add(string item) => _inner.Add(Normalize(item));
|
||||
|
||||
void ICollection<string>.Add(string item) => Add(item);
|
||||
|
||||
public void UnionWith(IEnumerable<string> other)
|
||||
{
|
||||
foreach (var item in other)
|
||||
Add(item);
|
||||
}
|
||||
|
||||
public void IntersectWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.IntersectWith(normalizedOther);
|
||||
}
|
||||
|
||||
public void ExceptWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.ExceptWith(normalizedOther);
|
||||
}
|
||||
|
||||
public void SymmetricExceptWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.SymmetricExceptWith(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsSubsetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsSupersetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsSupersetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsProperSupersetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsProperSupersetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsProperSubsetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsProperSubsetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool Overlaps(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.Overlaps(normalizedOther);
|
||||
}
|
||||
|
||||
public bool SetEquals(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.SetEquals(normalizedOther);
|
||||
}
|
||||
|
||||
// ---------- Реализация ICollection<string> ----------
|
||||
|
||||
public int Count => _inner.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Clear() => _inner.Clear();
|
||||
|
||||
public bool Contains(string item) => _inner.Contains(Normalize(item));
|
||||
|
||||
public void CopyTo(string[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
|
||||
|
||||
public bool Remove(string item) => _inner.Remove(Normalize(item));
|
||||
|
||||
// ---------- Реализация IEnumerable<string> и IEnumerable ----------
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => _inner.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
143
QWERTYkez.ExcelProcessor/PlaceholderFinder.cs
Normal file
143
QWERTYkez.ExcelProcessor/PlaceholderFinder.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
internal static class PlaceholderFinder
|
||||
{
|
||||
/// <summary>Находит плейсхолдеры $...$ в указанных листах.</summary>
|
||||
public static ISet<string> FindInDocument(SpreadsheetDocument doc, params WorksheetPart[] worksheets)
|
||||
{
|
||||
var result = new NormalizedSet();
|
||||
if (doc.WorkbookPart is null) return result;
|
||||
|
||||
var sharedStringTable = doc.WorkbookPart.SharedStringTablePart?.SharedStringTable;
|
||||
foreach (var ws in worksheets)
|
||||
ProcessWorksheet(ws, sharedStringTable, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Находит плейсхолдеры во всех листах документа.</summary>
|
||||
public static ISet<string> FindInDocument(SpreadsheetDocument doc)
|
||||
{
|
||||
if (doc.WorkbookPart is null) return new NormalizedSet();
|
||||
return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]);
|
||||
}
|
||||
|
||||
private static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet<string> result)
|
||||
{
|
||||
var worksheet = wsPart.Worksheet;
|
||||
if (worksheet is null) return;
|
||||
|
||||
ProcessCells(worksheet, sharedStrings, result);
|
||||
ProcessHeaderFooter(worksheet, result);
|
||||
}
|
||||
|
||||
private static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet<string> result)
|
||||
{
|
||||
var sheetData = worksheet.GetFirstChild<SheetData>();
|
||||
if (sheetData is null) return;
|
||||
|
||||
List<string>? tempList = null; // буфер для плейсхолдеров из одного текста
|
||||
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
string text = GetCellValue(cell, sharedStrings);
|
||||
if (string.IsNullOrEmpty(text)) continue;
|
||||
|
||||
#if DEBUG
|
||||
if (text.Contains('$'))
|
||||
Debug.WriteLine($"[PlaceholderFinder] Cell {cell.CellReference}: '{text}'");
|
||||
#endif
|
||||
if (FindPlaceholdersInText(text, ref tempList))
|
||||
{
|
||||
foreach (var ph in tempList!)
|
||||
result.Add(ph);
|
||||
tempList.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessHeaderFooter(Worksheet worksheet, ISet<string> result)
|
||||
{
|
||||
var hf = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
|
||||
if (hf is null) return;
|
||||
|
||||
var texts = new[]
|
||||
{
|
||||
hf.OddHeader?.Text, hf.OddFooter?.Text,
|
||||
hf.EvenHeader?.Text, hf.EvenFooter?.Text,
|
||||
hf.FirstHeader?.Text, hf.FirstFooter?.Text
|
||||
};
|
||||
|
||||
List<string>? tempList = null;
|
||||
foreach (var text in texts)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) continue;
|
||||
if (FindPlaceholdersInText(text!, ref tempList))
|
||||
{
|
||||
foreach (var ph in tempList!)
|
||||
result.Add(ph);
|
||||
tempList.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCellValue(Cell cell, SharedStringTable? sharedStrings)
|
||||
{
|
||||
if (cell?.CellValue is null && cell?.InlineString is null)
|
||||
return string.Empty;
|
||||
|
||||
// Встроенная строка
|
||||
if (cell.InlineString is not null)
|
||||
return string.Concat(cell.InlineString.Descendants<Text>().Select(t => t.Text));
|
||||
|
||||
// Обычное значение или общая строка
|
||||
string raw = cell.CellValue!.InnerText;
|
||||
if (cell.DataType?.Value == CellValues.SharedString)
|
||||
{
|
||||
if (sharedStrings is not null && int.TryParse(raw, out int idx) && idx >= 0 && idx < sharedStrings.Count!)
|
||||
{
|
||||
var si = sharedStrings.ElementAt(idx);
|
||||
return string.Concat(si.Descendants<Text>().Select(t => t.Text));
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static unsafe bool FindPlaceholdersInText(string text, ref List<string>? output)
|
||||
{
|
||||
fixed (char* pText = text)
|
||||
{
|
||||
char* start = pText;
|
||||
char* end = pText + text.Length;
|
||||
bool found = false;
|
||||
|
||||
while (start < end)
|
||||
{
|
||||
// Ищем '$'
|
||||
while (start < end && *start != '$') start++;
|
||||
if (start >= end) break;
|
||||
char* open = start++;
|
||||
if (start >= end) break;
|
||||
|
||||
// Ищем закрывающий '$'
|
||||
char* contentStart = start;
|
||||
while (start < end && *start != '$') start++;
|
||||
if (start >= end) break;
|
||||
char* close = start++;
|
||||
|
||||
int len = (int)(close - contentStart);
|
||||
if (len > 0)
|
||||
{
|
||||
found = true;
|
||||
output ??= [];
|
||||
output.Add(new string(contentStart, 0, len));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj
Normal file
18
QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="IsExternalInit" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
524
QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs
Normal file
524
QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs
Normal file
@@ -0,0 +1,524 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
internal static class ReplaceNumericExtensions
|
||||
{
|
||||
// =========================== МНОЖЕСТВЕННЫЕ ЗАМЕНЫ ===========================
|
||||
|
||||
// --- Double ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
IEnumerable<KeyValuePair<string, double>> replacements, string? format = null,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (replacements is null) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var coll = replacements as ICollection<KeyValuePair<string, double>>;
|
||||
int count = coll?.Count ?? replacements.Count();
|
||||
if (count == 0) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
var stringReplacements = new Dictionary<string, string>(count);
|
||||
foreach (var kvp in replacements)
|
||||
stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture);
|
||||
|
||||
ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType,
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// --- Float ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
IEnumerable<KeyValuePair<string, float>> replacements, string? format = null,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (replacements is null) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var coll = replacements as ICollection<KeyValuePair<string, float>>;
|
||||
int count = coll?.Count ?? replacements.Count();
|
||||
if (count == 0) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
var stringReplacements = new Dictionary<string, string>(count);
|
||||
foreach (var kvp in replacements)
|
||||
stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture);
|
||||
|
||||
ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType,
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// --- Int ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
IEnumerable<KeyValuePair<string, int>> replacements,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (replacements is null) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var coll = replacements as ICollection<KeyValuePair<string, int>>;
|
||||
int count = coll?.Count ?? replacements.Count();
|
||||
if (count == 0) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
var stringReplacements = new Dictionary<string, string>(count);
|
||||
foreach (var kvp in replacements)
|
||||
stringReplacements[kvp.Key] = kvp.Value.ToString(CultureInfo.CurrentCulture);
|
||||
|
||||
ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, null, comparisonType,
|
||||
(val, _) => val.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// --- Long ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
IEnumerable<KeyValuePair<string, long>> replacements,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (replacements is null) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var coll = replacements as ICollection<KeyValuePair<string, long>>;
|
||||
int count = coll?.Count ?? replacements.Count();
|
||||
if (count == 0) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
var stringReplacements = new Dictionary<string, string>(count);
|
||||
foreach (var kvp in replacements)
|
||||
stringReplacements[kvp.Key] = kvp.Value.ToString(CultureInfo.CurrentCulture);
|
||||
|
||||
ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, null, comparisonType,
|
||||
(val, _) => val.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// =========================== ОДИНОЧНЫЕ ЗАМЕНЫ ===========================
|
||||
|
||||
// --- Double ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
string oldValue, double newValue, string? format = null,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(oldValue)) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, format, comparisonType,
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture),
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
// --- Float ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
string oldValue, float newValue, string? format = null,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(oldValue)) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, format, comparisonType,
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture),
|
||||
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
// --- Int ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
string oldValue, int newValue,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(oldValue)) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, null, comparisonType,
|
||||
(val, _) => val.ToString(CultureInfo.InvariantCulture),
|
||||
(val, _) => val.ToString(CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
// --- Long ---
|
||||
internal static void Replace(this SpreadsheetDocument doc,
|
||||
string oldValue, long newValue,
|
||||
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(oldValue)) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, null, comparisonType,
|
||||
(val, _) => val.ToString(CultureInfo.InvariantCulture),
|
||||
(val, _) => val.ToString(CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
// =========================== ОБЩАЯ ЛОГИКА ===========================
|
||||
|
||||
private static void ReplaceNumericCore<T>(
|
||||
WorkbookPart workbookPart,
|
||||
WorksheetPart[] worksheets,
|
||||
IEnumerable<KeyValuePair<string, T>> numericReplacements,
|
||||
Dictionary<string, string> stringReplacements,
|
||||
string? format,
|
||||
StringComparison comparisonType,
|
||||
Func<T, string?, string> numberToStringForNumberCell)
|
||||
{
|
||||
var formatCache = new Dictionary<string, uint>();
|
||||
|
||||
uint? GetOrCreateStyleIndex(string fmt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fmt)) return null;
|
||||
if (formatCache.TryGetValue(fmt, out var idx)) return idx;
|
||||
var newIdx = CreateNumberFormat(workbookPart, fmt);
|
||||
formatCache[fmt] = newIdx;
|
||||
return newIdx;
|
||||
}
|
||||
|
||||
// Инициализация SharedStringTable (один раз)
|
||||
var allSharedStrings = new List<string>();
|
||||
var sharedStringIndexMap = new Dictionary<string, int>();
|
||||
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
|
||||
if (sharedStringTable != null)
|
||||
{
|
||||
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
|
||||
{
|
||||
var text = ConcatTexts(item.Descendants<Text>());
|
||||
sharedStringIndexMap[text] = allSharedStrings.Count;
|
||||
allSharedStrings.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var worksheetPart in worksheets)
|
||||
{
|
||||
var worksheet = worksheetPart.Worksheet;
|
||||
if (worksheet == null) continue;
|
||||
var sheetData = worksheet.GetFirstChild<SheetData>();
|
||||
if (sheetData == null) continue;
|
||||
|
||||
// Обработка ячеек
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
string originalText = GetCellTextForNumeric(cell, allSharedStrings);
|
||||
if (string.IsNullOrEmpty(originalText)) continue;
|
||||
|
||||
string? matchedKey = null;
|
||||
T matchedVal = default!;
|
||||
int matchStart = -1, matchLength = 0;
|
||||
|
||||
foreach (var kvp in numericReplacements)
|
||||
{
|
||||
int idx = originalText.IndexOf(kvp.Key, comparisonType);
|
||||
if (idx >= 0 && kvp.Key.Length > matchLength)
|
||||
{
|
||||
matchedKey = kvp.Key;
|
||||
matchedVal = kvp.Value;
|
||||
matchStart = idx;
|
||||
matchLength = kvp.Key.Length;
|
||||
}
|
||||
}
|
||||
if (matchedKey == null) continue;
|
||||
|
||||
bool isFullCell = (matchStart == 0 && matchLength == originalText.Length);
|
||||
if (isFullCell)
|
||||
{
|
||||
cell.DataType = CellValues.Number;
|
||||
string numStr = numberToStringForNumberCell(matchedVal, format);
|
||||
cell.CellValue = new CellValue(numStr);
|
||||
if (!string.IsNullOrEmpty(format))
|
||||
{
|
||||
var styleIdx = GetOrCreateStyleIndex(format!);
|
||||
if (styleIdx.HasValue)
|
||||
cell.StyleIndex = styleIdx.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string replacementStr = stringReplacements[matchedKey];
|
||||
string newText = ReplaceSubstring(originalText, matchStart, matchLength, replacementStr);
|
||||
SetCellText(cell, newText, allSharedStrings, sharedStringIndexMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Колонтитулы и комментарии
|
||||
ReplaceInHeadersFooters(worksheetPart, stringReplacements, comparisonType);
|
||||
ReplaceInComments(worksheetPart, stringReplacements, comparisonType);
|
||||
}
|
||||
|
||||
// Сохраняем SharedStringTable
|
||||
UpdateSharedStringTable(workbookPart, allSharedStrings);
|
||||
}
|
||||
|
||||
private static void ReplaceSingleCore<T>(
|
||||
WorkbookPart workbookPart,
|
||||
WorksheetPart[] worksheets,
|
||||
string oldValue,
|
||||
T newValue,
|
||||
string? format,
|
||||
StringComparison comparisonType,
|
||||
Func<T, string?, string> numberToStringForNumberCell,
|
||||
Func<T, string?, string> numberToStringForTextCell)
|
||||
{
|
||||
var formatCache = new Dictionary<string, uint>();
|
||||
|
||||
uint? GetOrCreateStyleIndex(string fmt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fmt)) return null;
|
||||
if (formatCache.TryGetValue(fmt, out var idx)) return idx;
|
||||
var newIdx = CreateNumberFormat(workbookPart, fmt);
|
||||
formatCache[fmt] = newIdx;
|
||||
return newIdx;
|
||||
}
|
||||
|
||||
var singleStringReplacement = new Dictionary<string, string>
|
||||
{
|
||||
{ oldValue, numberToStringForTextCell(newValue, format) }
|
||||
};
|
||||
|
||||
// Инициализация SharedStringTable
|
||||
var allSharedStrings = new List<string>();
|
||||
var sharedStringIndexMap = new Dictionary<string, int>();
|
||||
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
|
||||
if (sharedStringTable != null)
|
||||
{
|
||||
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
|
||||
{
|
||||
var text = ConcatTexts(item.Descendants<Text>());
|
||||
sharedStringIndexMap[text] = allSharedStrings.Count;
|
||||
allSharedStrings.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var worksheetPart in worksheets)
|
||||
{
|
||||
var worksheet = worksheetPart.Worksheet;
|
||||
if (worksheet == null) continue;
|
||||
var sheetData = worksheet.GetFirstChild<SheetData>();
|
||||
if (sheetData == null) continue;
|
||||
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
string originalText = GetCellTextForNumeric(cell, allSharedStrings);
|
||||
if (string.IsNullOrEmpty(originalText)) continue;
|
||||
|
||||
int idx = originalText.IndexOf(oldValue, comparisonType);
|
||||
if (idx < 0) continue;
|
||||
|
||||
bool isFullCell = (idx == 0 && oldValue.Length == originalText.Length);
|
||||
if (isFullCell)
|
||||
{
|
||||
cell.DataType = CellValues.Number;
|
||||
string numStr = numberToStringForNumberCell(newValue, format);
|
||||
cell.CellValue = new CellValue(numStr);
|
||||
if (!string.IsNullOrEmpty(format))
|
||||
{
|
||||
var styleIdx = GetOrCreateStyleIndex(format!);
|
||||
if (styleIdx.HasValue)
|
||||
cell.StyleIndex = styleIdx.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string replacementStr = numberToStringForTextCell(newValue, format);
|
||||
string newText = ReplaceSubstring(originalText, idx, oldValue.Length, replacementStr);
|
||||
SetCellText(cell, newText, allSharedStrings, sharedStringIndexMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceInHeadersFooters(worksheetPart, singleStringReplacement, comparisonType);
|
||||
ReplaceInComments(worksheetPart, singleStringReplacement, comparisonType);
|
||||
}
|
||||
|
||||
UpdateSharedStringTable(workbookPart, allSharedStrings);
|
||||
}
|
||||
|
||||
// =========================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ===========================
|
||||
|
||||
private static string GetCellTextForNumeric(Cell cell, List<string> allSharedStrings)
|
||||
{
|
||||
if (cell?.CellValue == null) return string.Empty;
|
||||
|
||||
// InlineString – без LINQ
|
||||
if (cell.InlineString != null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var t in cell.InlineString.Descendants<Text>())
|
||||
sb.Append(t.Text);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
string val = cell.CellValue.InnerText;
|
||||
if (cell.DataType?.Value == CellValues.SharedString)
|
||||
{
|
||||
if (int.TryParse(val, out int idx) && idx >= 0 && idx < allSharedStrings.Count)
|
||||
return allSharedStrings[idx];
|
||||
return string.Empty;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private static void SetCellText(Cell cell, string newText,
|
||||
List<string> allSharedStrings, Dictionary<string, int> sharedStringIndexMap)
|
||||
{
|
||||
if (cell.InlineString != null)
|
||||
{
|
||||
// Очищаем старые тексты
|
||||
foreach (var t in cell.InlineString.Descendants<Text>().ToList())
|
||||
t.Remove();
|
||||
cell.InlineString.AppendChild(new Text(newText));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sharedStringIndexMap.TryGetValue(newText, out int index))
|
||||
{
|
||||
index = allSharedStrings.Count;
|
||||
allSharedStrings.Add(newText);
|
||||
sharedStringIndexMap[newText] = index;
|
||||
}
|
||||
cell.DataType = CellValues.SharedString;
|
||||
cell.CellValue = new CellValue(index.ToString());
|
||||
}
|
||||
|
||||
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
|
||||
{
|
||||
var ssPart = workbookPart.SharedStringTablePart;
|
||||
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
|
||||
var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable();
|
||||
sharedStringTable.RemoveAllChildren<SharedStringItem>();
|
||||
foreach (var str in allSharedStrings)
|
||||
sharedStringTable.AppendChild(new SharedStringItem(new Text(str)));
|
||||
sharedStringTable.Save();
|
||||
}
|
||||
|
||||
private static string ConcatTexts(IEnumerable<Text> texts)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var t in texts)
|
||||
sb.Append(t.Text);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Оптимизированная замена подстроки через string.Create (без unsafe)
|
||||
private static unsafe string ReplaceSubstring(string original, int start, int length, string replacement)
|
||||
{
|
||||
if (length == 0) return original;
|
||||
int newLen = original.Length - length + replacement.Length;
|
||||
if (newLen <= 0) return replacement;
|
||||
|
||||
fixed (char* pOrig = original, pRep = replacement)
|
||||
{
|
||||
char* result = stackalloc char[newLen];
|
||||
int pos = 0;
|
||||
for (int i = 0; i < start; i++)
|
||||
result[pos++] = pOrig[i];
|
||||
for (int i = 0; i < replacement.Length; i++)
|
||||
result[pos++] = pRep[i];
|
||||
for (int i = start + length; i < original.Length; i++)
|
||||
result[pos++] = pOrig[i];
|
||||
return new string(result, 0, newLen);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint CreateNumberFormat(WorkbookPart workbookPart, string format)
|
||||
{
|
||||
var stylesPart = workbookPart.WorkbookStylesPart;
|
||||
if (stylesPart == null)
|
||||
{
|
||||
stylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
|
||||
stylesPart.Stylesheet = new Stylesheet();
|
||||
}
|
||||
var ss = stylesPart.Stylesheet!;
|
||||
ss.NumberingFormats ??= new NumberingFormats();
|
||||
uint nextId = 164;
|
||||
if (ss.NumberingFormats.Elements<NumberingFormat>().Any())
|
||||
nextId = ss.NumberingFormats.Elements<NumberingFormat>().Max(nf => nf.NumberFormatId!.Value) + 1;
|
||||
var nf = new NumberingFormat { NumberFormatId = nextId, FormatCode = format };
|
||||
ss.NumberingFormats.AppendChild(nf);
|
||||
ss.CellFormats ??= new CellFormats();
|
||||
var cf = new DocumentFormat.OpenXml.Spreadsheet.CellFormat
|
||||
{
|
||||
NumberFormatId = nextId,
|
||||
FormatId = 0,
|
||||
ApplyNumberFormat = true
|
||||
};
|
||||
ss.CellFormats.AppendChild(cf);
|
||||
ss.Save();
|
||||
return ss.CellFormats.Count!.Value - 1;
|
||||
}
|
||||
|
||||
// =========================== КОЛОНТИТУЛЫ И КОММЕНТАРИИ ===========================
|
||||
|
||||
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
var worksheet = worksheetPart.Worksheet;
|
||||
if (worksheet is null) return;
|
||||
var headerFooter = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
|
||||
if (headerFooter is null) return;
|
||||
|
||||
foreach (var elem in new OpenXmlLeafTextElement?[] { headerFooter.OddHeader, headerFooter.OddFooter, headerFooter.EvenHeader, headerFooter.EvenFooter, headerFooter.FirstHeader, headerFooter.FirstFooter })
|
||||
ReplaceHeaderFooter(elem, replacementDict, comparisonType);
|
||||
}
|
||||
|
||||
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
if (element?.Text is null) return;
|
||||
string original = element.Text;
|
||||
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
||||
if (processed != original)
|
||||
element.Text = processed;
|
||||
}
|
||||
|
||||
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
var commentsPart = worksheetPart.WorksheetCommentsPart;
|
||||
if (commentsPart?.Comments is null) return;
|
||||
foreach (var comment in commentsPart.Comments.Elements<Comment>())
|
||||
{
|
||||
var textElement = comment.Descendants<CommentText>().FirstOrDefault();
|
||||
if (textElement?.Text is null) continue;
|
||||
string original = textElement.Text.Text;
|
||||
if (string.IsNullOrEmpty(original)) continue;
|
||||
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
||||
if (processed != original)
|
||||
textElement.Text.Text = processed;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
|
||||
string result = input;
|
||||
foreach (string key in replacementDict.Keys.OrderByDescending(k => k.Length))
|
||||
{
|
||||
string value = replacementDict[key];
|
||||
result = ReplaceInString(result, key, value, comparisonType);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
|
||||
{
|
||||
int idx = original.IndexOf(oldValue, comparisonType);
|
||||
if (idx < 0) return original;
|
||||
var sb = new StringBuilder(original.Length + newValue.Length - oldValue.Length);
|
||||
int last = 0;
|
||||
while (idx >= 0)
|
||||
{
|
||||
sb.Append(original, last, idx - last);
|
||||
sb.Append(newValue);
|
||||
last = idx + oldValue.Length;
|
||||
idx = original.IndexOf(oldValue, last, comparisonType);
|
||||
}
|
||||
sb.Append(original, last, original.Length - last);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
217
QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs
Normal file
217
QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
internal static class ReplaceStringExtensions
|
||||
{
|
||||
// --- Публичный метод для одиночной замены ---
|
||||
internal static void Replace(this SpreadsheetDocument doc, string oldValue, string newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(oldValue)) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var replacement = new Dictionary<string, string> { [oldValue] = newValue };
|
||||
ReplaceCore(doc, worksheets, replacement, comparisonType);
|
||||
}
|
||||
|
||||
// --- Публичный метод для множественной замены ---
|
||||
internal static void Replace(this SpreadsheetDocument doc, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
if (replacements is null) return;
|
||||
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
||||
if (worksheets.Length < 1) return;
|
||||
|
||||
var comparer = GetComparerForStringComparison(comparisonType);
|
||||
var replacementDict = new Dictionary<string, string>(comparer);
|
||||
foreach (var kvp in replacements)
|
||||
if (!string.IsNullOrEmpty(kvp.Key))
|
||||
replacementDict[kvp.Key] = kvp.Value;
|
||||
if (replacementDict.Count == 0) return;
|
||||
ReplaceCore(doc, worksheets, replacementDict, comparisonType);
|
||||
}
|
||||
|
||||
// --- Общий приватный метод, содержащий всю логику замены ---
|
||||
private static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
var workbookPart = doc.WorkbookPart!;
|
||||
|
||||
// 1. Собрать все изменения для ячеек
|
||||
var cellChanges = new Dictionary<Cell, string>();
|
||||
var allSharedStrings = new List<string>();
|
||||
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
|
||||
if (sharedStringTable is not null)
|
||||
{
|
||||
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
|
||||
allSharedStrings.Add(string.Concat(item.Descendants<Text>().Select(t => t.Text)));
|
||||
}
|
||||
|
||||
foreach (var worksheetPart in worksheets)
|
||||
CollectCellChanges(worksheetPart, replacementDict, comparisonType, cellChanges, allSharedStrings);
|
||||
|
||||
// 2. Применить изменения к ячейкам
|
||||
ApplyCellChanges(cellChanges, allSharedStrings);
|
||||
|
||||
// 3. Обновить SharedStringTable
|
||||
UpdateSharedStringTable(workbookPart, allSharedStrings);
|
||||
|
||||
// 4. Обработать колонтитулы
|
||||
foreach (var worksheetPart in worksheets)
|
||||
ReplaceInHeadersFooters(worksheetPart, replacementDict, comparisonType);
|
||||
|
||||
// 5. Обработать комментарии
|
||||
foreach (var worksheetPart in worksheets)
|
||||
ReplaceInComments(worksheetPart, replacementDict, comparisonType);
|
||||
}
|
||||
|
||||
// --- Остальные вспомогательные методы (без изменений) ---
|
||||
private static IEqualityComparer<string> GetComparerForStringComparison(StringComparison comparisonType) =>
|
||||
comparisonType switch
|
||||
{
|
||||
StringComparison.Ordinal => StringComparer.Ordinal,
|
||||
StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase,
|
||||
StringComparison.InvariantCulture => StringComparer.InvariantCulture,
|
||||
StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase,
|
||||
StringComparison.CurrentCulture => StringComparer.CurrentCulture,
|
||||
StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase,
|
||||
_ => StringComparer.OrdinalIgnoreCase,
|
||||
};
|
||||
|
||||
private static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType, Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
|
||||
{
|
||||
var worksheet = worksheetPart.Worksheet;
|
||||
if (worksheet is null) return;
|
||||
var sheetData = worksheet.GetFirstChild<SheetData>();
|
||||
if (sheetData is null) return;
|
||||
|
||||
foreach (var row in sheetData.Elements<Row>())
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
string originalText = GetCellText(cell, allSharedStrings);
|
||||
if (string.IsNullOrEmpty(originalText)) continue;
|
||||
string newText = ProcessReplacements(originalText, replacementDict, comparisonType);
|
||||
if (newText != originalText)
|
||||
cellChanges[cell] = newText;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyCellChanges(Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
|
||||
{
|
||||
foreach (var kvp in cellChanges)
|
||||
{
|
||||
var cell = kvp.Key;
|
||||
var newText = kvp.Value;
|
||||
if (cell.InlineString is not null)
|
||||
{
|
||||
var texts = cell.InlineString.Descendants<Text>().ToList();
|
||||
foreach (var t in texts) t.Remove();
|
||||
cell.InlineString.AppendChild(new Text(newText));
|
||||
}
|
||||
else
|
||||
{
|
||||
int newIndex = AddOrFindStringIndex(allSharedStrings, newText);
|
||||
cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
|
||||
cell.CellValue = new CellValue(newIndex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
|
||||
{
|
||||
var ssPart = workbookPart.SharedStringTablePart;
|
||||
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
|
||||
var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable();
|
||||
sharedStringTable.RemoveAllChildren<SharedStringItem>();
|
||||
foreach (var str in allSharedStrings)
|
||||
sharedStringTable.AppendChild(new SharedStringItem(new Text(str)));
|
||||
sharedStringTable.Save();
|
||||
}
|
||||
|
||||
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
|
||||
string result = input;
|
||||
foreach (string key in replacementDict.Keys.OrderByDescending(k => k.Length))
|
||||
{
|
||||
string value = replacementDict[key];
|
||||
result = ReplaceInString(result, key, value, comparisonType);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
|
||||
{
|
||||
int idx = original.IndexOf(oldValue, comparisonType);
|
||||
if (idx < 0) return original;
|
||||
var sb = new StringBuilder(original.Length + newValue.Length - oldValue.Length);
|
||||
int last = 0;
|
||||
while (idx >= 0)
|
||||
{
|
||||
sb.Append(original, last, idx - last);
|
||||
sb.Append(newValue);
|
||||
last = idx + oldValue.Length;
|
||||
idx = original.IndexOf(oldValue, last, comparisonType);
|
||||
}
|
||||
sb.Append(original, last, original.Length - last);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetCellText(Cell cell, List<string> allSharedStrings)
|
||||
{
|
||||
if (cell?.CellValue is null) return string.Empty;
|
||||
if (cell.InlineString is not null)
|
||||
return string.Concat(cell.InlineString.Descendants<Text>().Select(t => t.Text));
|
||||
string value = cell.CellValue.InnerText;
|
||||
if (cell.DataType?.Value == CellValues.SharedString)
|
||||
{
|
||||
if (int.TryParse(value, out int idx) && idx >= 0 && idx < allSharedStrings.Count)
|
||||
return allSharedStrings[idx];
|
||||
return string.Empty;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int AddOrFindStringIndex(List<string> allSharedStrings, string text)
|
||||
{
|
||||
int idx = allSharedStrings.IndexOf(text);
|
||||
if (idx >= 0) return idx;
|
||||
allSharedStrings.Add(text);
|
||||
return allSharedStrings.Count - 1;
|
||||
}
|
||||
|
||||
// --- Колонтитулы (обобщённый метод) ---
|
||||
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
var worksheet = worksheetPart.Worksheet;
|
||||
if (worksheet is null) return;
|
||||
var headerFooter = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
|
||||
if (headerFooter is null) return;
|
||||
|
||||
foreach (var elem in new OpenXmlLeafTextElement?[] { headerFooter.OddHeader, headerFooter.OddFooter, headerFooter.EvenHeader, headerFooter.EvenFooter, headerFooter.FirstHeader, headerFooter.FirstFooter })
|
||||
ReplaceHeaderFooter(elem, replacementDict, comparisonType);
|
||||
}
|
||||
|
||||
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
if (element?.Text is null) return;
|
||||
string original = element.Text;
|
||||
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
||||
if (processed != original)
|
||||
element.Text = processed;
|
||||
}
|
||||
|
||||
// --- Комментарии ---
|
||||
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
||||
{
|
||||
var commentsPart = worksheetPart.WorksheetCommentsPart;
|
||||
if (commentsPart?.Comments is null) return;
|
||||
foreach (var comment in commentsPart.Comments.Elements<Comment>())
|
||||
{
|
||||
var textElement = comment.Descendants<CommentText>().FirstOrDefault();
|
||||
if (textElement?.Text is null) continue;
|
||||
string original = textElement.Text.Text;
|
||||
if (string.IsNullOrEmpty(original)) continue;
|
||||
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
||||
if (processed != original)
|
||||
textElement.Text.Text = processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
QWERTYkez.ExcelProcessor/globals.cs
Normal file
12
QWERTYkez.ExcelProcessor/globals.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
global using DocumentFormat.OpenXml;
|
||||
global using DocumentFormat.OpenXml.Packaging;
|
||||
global using DocumentFormat.OpenXml.Spreadsheet;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Diagnostics;
|
||||
global using System.Globalization;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
4
QWERTYkez.OpenXmlProcessors.slnx
Normal file
4
QWERTYkez.OpenXmlProcessors.slnx
Normal file
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj" />
|
||||
<Project Path="QWERTYkez.WordProcessor/QWERTYkez.WordProcessor.csproj" />
|
||||
</Solution>
|
||||
89
QWERTYkez.WordProcessor/Builders/CellProps.cs
Normal file
89
QWERTYkez.WordProcessor/Builders/CellProps.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
#pragma warning disable IDE1006 // Стили именования
|
||||
public static CellProps V_H_ { get; } = new() { Merge = MergedCell.V_H_ };
|
||||
public static CellProps H_ { get; } = new() { Merge = MergedCell.H_ };
|
||||
public static CellProps V_ { get; } = new() { Merge = MergedCell.V_ };
|
||||
public static CellProps _V_H { get; } = new() { Merge = MergedCell._V_H };
|
||||
public static CellProps _H { get; } = new() { Merge = MergedCell._H };
|
||||
public static CellProps _V { get; } = new() { Merge = MergedCell._V };
|
||||
public static CellProps _VH_ { get; } = new() { Merge = MergedCell._VH_ };
|
||||
public static CellProps V__H { get; } = new() { Merge = MergedCell.V__H };
|
||||
#pragma warning restore IDE1006 // Стили именования
|
||||
|
||||
|
||||
internal bool TryExtract(out List<OpenXmlElement> list)
|
||||
{
|
||||
list =
|
||||
[
|
||||
new TableCellVerticalAlignment() { Val = VerticalAlignment ?? TableVerticalAlignmentValues.Center },
|
||||
new TableCellMargin()
|
||||
{
|
||||
TopMargin = new TopMargin() { Width = "0" },
|
||||
BottomMargin = new BottomMargin() { Width = "0" },
|
||||
LeftMargin = new LeftMargin() { Width = "0" },
|
||||
RightMargin = new RightMargin() { Width = "0" }
|
||||
}
|
||||
];
|
||||
|
||||
if (Width.HasValue)
|
||||
list.Add(new TableCellWidth()
|
||||
{
|
||||
Type = TableWidthUnitValues.Dxa,
|
||||
Width = ((uint)(Width.Value * 567d)).ToString() // 1 см = 567 DXA
|
||||
});
|
||||
|
||||
if (Merge.HasValue)
|
||||
{
|
||||
switch (Merge.Value)
|
||||
{
|
||||
case MergedCell.V_H_:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Restart });
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Restart });
|
||||
break;
|
||||
case MergedCell.H_:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Restart }); break;
|
||||
case MergedCell.V_:
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Restart }); break;
|
||||
case MergedCell._V_H:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Continue });
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Continue });
|
||||
break;
|
||||
case MergedCell._H:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Continue }); break;
|
||||
case MergedCell._V:
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Continue }); break;
|
||||
case MergedCell._VH_:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Restart });
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Continue });
|
||||
break;
|
||||
case MergedCell.V__H:
|
||||
list.Add(new HorizontalMerge() { Val = MergedCellValues.Continue });
|
||||
list.Add(new VerticalMerge() { Val = MergedCellValues.Restart });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
}
|
||||
public enum MergedCell
|
||||
{
|
||||
V_H_,
|
||||
H_,
|
||||
V_,
|
||||
|
||||
_V_H,
|
||||
_H,
|
||||
_V,
|
||||
|
||||
_VH_,
|
||||
V__H,
|
||||
}
|
||||
162
QWERTYkez.WordProcessor/Builders/FontProps.cs
Normal file
162
QWERTYkez.WordProcessor/Builders/FontProps.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using MathStyle = DocumentFormat.OpenXml.Math.Style;
|
||||
using MathStyleValues = DocumentFormat.OpenXml.Math.StyleValues;
|
||||
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
public record FontProps
|
||||
{
|
||||
public FontProps(double? size = null) => Size = size ?? 12;
|
||||
|
||||
public string? FontFamily { get; init; }
|
||||
|
||||
public double? Size
|
||||
{
|
||||
get => _SizeD.HasValue ? _SizeD.Value / 2d : null;
|
||||
init => _SizeD = value.HasValue ? (uint?)(value.Value * 2) : null;
|
||||
}
|
||||
uint? _SizeD;
|
||||
|
||||
public bool IsItalic { get; init; } = false;
|
||||
|
||||
public bool IsBold { get; init; } = false;
|
||||
|
||||
public UnderlineValues? Underline { get; init; }
|
||||
|
||||
public VerticalPositionValues? SubSup { get; init; }
|
||||
|
||||
public string? Color { get; init; }
|
||||
|
||||
|
||||
public bool TryExtract(out List<OpenXmlElement> list)
|
||||
{
|
||||
list = [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FontFamily))
|
||||
list.Add(new RunFonts { Ascii = FontFamily, HighAnsi = FontFamily });
|
||||
|
||||
if (_SizeD is not null)
|
||||
list.Add(new FontSize { Val = _SizeD.Value.ToString() });
|
||||
|
||||
if (IsBold)
|
||||
list.Add(new Bold());
|
||||
|
||||
if (IsItalic)
|
||||
list.Add(new Italic());
|
||||
|
||||
if (Underline is not null)
|
||||
list.Add(new Underline { Val = Underline });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Color))
|
||||
list.Add(new Color { Val = Color });
|
||||
|
||||
if (SubSup is not null)
|
||||
list.Add(new VerticalTextAlignment { Val = SubSup });
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
public bool TryExtractWithoutFamily(out List<OpenXmlElement> list)
|
||||
{
|
||||
list = [];
|
||||
|
||||
if (_SizeD is not null)
|
||||
list.Add(new FontSize { Val = _SizeD.Value.ToString() });
|
||||
|
||||
if (IsBold)
|
||||
list.Add(new Bold());
|
||||
|
||||
if (IsItalic)
|
||||
list.Add(new Italic());
|
||||
|
||||
if (Underline is not null)
|
||||
list.Add(new Underline { Val = Underline });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Color))
|
||||
list.Add(new Color { Val = Color });
|
||||
|
||||
if (SubSup is not null)
|
||||
list.Add(new VerticalTextAlignment { Val = SubSup });
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
public bool TrySupExtract(out List<OpenXmlElement> list)
|
||||
{
|
||||
list = [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FontFamily))
|
||||
list.Add(new RunFonts { Ascii = FontFamily, HighAnsi = FontFamily });
|
||||
|
||||
if (_SizeD is not null)
|
||||
list.Add(new FontSize { Val = _SizeD.Value.ToString() });
|
||||
|
||||
if (IsBold)
|
||||
list.Add(new Bold());
|
||||
|
||||
if (IsItalic)
|
||||
list.Add(new Italic());
|
||||
|
||||
if (Underline is not null)
|
||||
list.Add(new Underline { Val = Underline });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Color))
|
||||
list.Add(new Color { Val = Color });
|
||||
|
||||
list.Add(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript });
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
public bool TrySubExtract(out List<OpenXmlElement> list)
|
||||
{
|
||||
list = [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FontFamily))
|
||||
list.Add(new RunFonts { Ascii = FontFamily, HighAnsi = FontFamily });
|
||||
|
||||
if (_SizeD is not null)
|
||||
list.Add(new FontSize { Val = _SizeD.Value.ToString() });
|
||||
|
||||
if (IsBold)
|
||||
list.Add(new Bold());
|
||||
|
||||
if (IsItalic)
|
||||
list.Add(new Italic());
|
||||
|
||||
if (Underline is not null)
|
||||
list.Add(new Underline { Val = Underline });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Color))
|
||||
list.Add(new Color { Val = Color });
|
||||
|
||||
list.Add(new VerticalTextAlignment { Val = VerticalPositionValues.Subscript });
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
|
||||
public bool TryExtractForMath(out List<OpenXmlElement> list)
|
||||
{
|
||||
list = [];
|
||||
|
||||
// Для математики используем только элемент Style
|
||||
if (IsBold)
|
||||
list.Add(new MathStyle { Val = MathStyleValues.BoldItalic });
|
||||
else list.Add(new MathStyle { Val = MathStyleValues.Italic });
|
||||
// Обычное начертание можно не добавлять, так как по умолчанию и так обычное
|
||||
// (но если нужно явно сбросить, можно добавить Style { Val = StyleValues.Plain })
|
||||
|
||||
return list.Count > 0;
|
||||
}
|
||||
|
||||
// Фабричные методы для создания FontProps
|
||||
public static FontProps Default => new();
|
||||
|
||||
public static FontProps WithFont(string fontFamily, double? size = null) =>
|
||||
new(size) { FontFamily = fontFamily };
|
||||
|
||||
public static FontProps WithSize(double size) =>
|
||||
new(size);
|
||||
|
||||
public static FontProps WithStyle(bool bold = false, bool italic = false) =>
|
||||
new() { IsBold = bold, IsItalic = italic };
|
||||
|
||||
public static FontProps WithColor(string hexColor) =>
|
||||
new() { Color = hexColor };
|
||||
}
|
||||
357
QWERTYkez.WordProcessor/Builders/FormulaHelper.cs
Normal file
357
QWERTYkez.WordProcessor/Builders/FormulaHelper.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
using Base = DocumentFormat.OpenXml.Math.Base;
|
||||
using ControlProperties = DocumentFormat.OpenXml.Math.ControlProperties;
|
||||
using Degree = DocumentFormat.OpenXml.Math.Degree;
|
||||
using Denominator = DocumentFormat.OpenXml.Math.Denominator;
|
||||
using Fraction = DocumentFormat.OpenXml.Math.Fraction;
|
||||
using FractionProperties = DocumentFormat.OpenXml.Math.FractionProperties;
|
||||
using HideDegree = DocumentFormat.OpenXml.Math.HideDegree;
|
||||
using MathRun = DocumentFormat.OpenXml.Math.Run;
|
||||
using MathRunProperties = DocumentFormat.OpenXml.Math.RunProperties;
|
||||
using MathText = DocumentFormat.OpenXml.Math.Text;
|
||||
using Numerator = DocumentFormat.OpenXml.Math.Numerator;
|
||||
using Radical = DocumentFormat.OpenXml.Math.Radical;
|
||||
using RadicalProperties = DocumentFormat.OpenXml.Math.RadicalProperties;
|
||||
using SubArgument = DocumentFormat.OpenXml.Math.SubArgument;
|
||||
using Subscript = DocumentFormat.OpenXml.Math.Subscript;
|
||||
using SubSuperscript = DocumentFormat.OpenXml.Math.SubSuperscript;
|
||||
using SuperArgument = DocumentFormat.OpenXml.Math.SuperArgument;
|
||||
using Superscript = DocumentFormat.OpenXml.Math.Superscript;
|
||||
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
internal static class FormulaHelper
|
||||
{
|
||||
// Вспомогательный метод для создания MathRun с форматированием
|
||||
public static MathRun CreateMathRun(FontProps? font)
|
||||
{
|
||||
var mathRun = new MathRun();
|
||||
|
||||
// 1. Математический стиль (полужирный/курсив) – добавляем первым
|
||||
if (font is not null && font.TryExtractForMath(out var mathStyleElements))
|
||||
{
|
||||
var mathPr = new MathRunProperties(mathStyleElements);
|
||||
mathRun.AppendChild(mathPr);
|
||||
}
|
||||
|
||||
// 2. Wordprocessing: цвет, размер, подчёркивание (без семейства шрифта)
|
||||
var wordPr = new RunProperties();
|
||||
if (font is not null && font.TryExtractWithoutFamily(out var wordElements))
|
||||
{
|
||||
foreach (var elem in wordElements)
|
||||
wordPr.AppendChild(elem.CloneNode(true));
|
||||
}
|
||||
mathRun.AppendChild(wordPr);
|
||||
|
||||
return mathRun;
|
||||
}
|
||||
|
||||
public static void AddText(OpenXmlElement parent, string text, FontProps? font)
|
||||
{
|
||||
if (parent is MathRun mathRun)
|
||||
{
|
||||
mathRun.AppendChild(new MathText(text));
|
||||
}
|
||||
else
|
||||
{
|
||||
var run = CreateMathRun(font);
|
||||
run.AppendChild(new MathText(text));
|
||||
parent.AppendChild(run);
|
||||
}
|
||||
}
|
||||
|
||||
public static Fraction CreateFraction(
|
||||
Action<IFormula> numeratorBuilder,
|
||||
Action<IFormula> denominatorBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var fraction = new Fraction();
|
||||
var font = builder.BaseFont;
|
||||
|
||||
// Добавляем свойства дроби с форматированием (для черты дроби)
|
||||
var fPr = new FractionProperties();
|
||||
if (font.TryExtractWithoutFamily(out var wordElements))
|
||||
{
|
||||
var ctrlPr = new ControlProperties();
|
||||
var rPr = new RunProperties();
|
||||
foreach (var elem in wordElements)
|
||||
rPr.AppendChild(elem.CloneNode(true));
|
||||
rPr.AppendChild(new RunFonts { Ascii = "Cambria Math", HighAnsi = "Cambria Math" });
|
||||
ctrlPr.AppendChild(rPr);
|
||||
fPr.AppendChild(ctrlPr);
|
||||
}
|
||||
fraction.AppendChild(fPr);
|
||||
|
||||
// Числитель
|
||||
var numeratorElem = new Numerator();
|
||||
fraction.AppendChild(numeratorElem);
|
||||
builder.PushContext(numeratorElem);
|
||||
numeratorBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
// Знаменатель
|
||||
var denominatorElem = new Denominator();
|
||||
fraction.AppendChild(denominatorElem);
|
||||
builder.PushContext(denominatorElem);
|
||||
denominatorBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
return fraction;
|
||||
}
|
||||
|
||||
public static Radical CreateRadical(
|
||||
Action<IFormula> radicandBuilder,
|
||||
Action<IFormula>? degreeBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var radical = new Radical();
|
||||
var font = builder.BaseFont;
|
||||
|
||||
var radPr = new RadicalProperties();
|
||||
|
||||
// Цвет, размер и подчёркивание для знака корня (ControlProperties)
|
||||
if (font is not null && font.TryExtractWithoutFamily(out var wordElements))
|
||||
{
|
||||
var ctrlPr = new ControlProperties();
|
||||
var rPr = new RunProperties();
|
||||
foreach (var elem in wordElements)
|
||||
rPr.AppendChild(elem.CloneNode(true));
|
||||
rPr.AppendChild(new RunFonts { Ascii = "Cambria Math", HighAnsi = "Cambria Math" });
|
||||
ctrlPr.AppendChild(rPr);
|
||||
radPr.AppendChild(ctrlPr);
|
||||
}
|
||||
|
||||
// Если степень не задана, скрываем её
|
||||
if (degreeBuilder is null)
|
||||
{
|
||||
radPr.HideDegree = new HideDegree { Val = DocumentFormat.OpenXml.Math.BooleanValues.One };
|
||||
}
|
||||
|
||||
// Добавляем свойства радикала (с цветом)
|
||||
radical.AppendChild(radPr);
|
||||
|
||||
// Степень (если есть)
|
||||
if (degreeBuilder is not null)
|
||||
{
|
||||
var degree = new Degree();
|
||||
radical.AppendChild(degree);
|
||||
builder.PushContext(degree);
|
||||
degreeBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
|
||||
// Подкоренное выражение
|
||||
var radicand = new Base();
|
||||
radical.AppendChild(radicand);
|
||||
builder.PushContext(radicand);
|
||||
radicandBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
return radical;
|
||||
}
|
||||
|
||||
public static void AddIntegral(
|
||||
OpenXmlElement currentContext,
|
||||
Action<IFormula> functionBuilder,
|
||||
Action<IFormula> differentialBuilder,
|
||||
Action<IFormula>? lowerLimitBuilder,
|
||||
Action<IFormula>? upperLimitBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var font = builder.BaseFont;
|
||||
|
||||
if (lowerLimitBuilder is null && upperLimitBuilder is null)
|
||||
{
|
||||
var integralRun = CreateMathRun(font);
|
||||
integralRun.AppendChild(new MathText("∫"));
|
||||
currentContext.AppendChild(integralRun);
|
||||
|
||||
var funcRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(funcRun);
|
||||
builder.PushContext(funcRun);
|
||||
functionBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
var diffRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(diffRun);
|
||||
builder.PushContext(diffRun);
|
||||
differentialBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
else
|
||||
{
|
||||
var integralWithLimits = new SubSuperscript();
|
||||
currentContext.AppendChild(integralWithLimits);
|
||||
|
||||
var baseElem = new Base();
|
||||
var integralRun = CreateMathRun(font);
|
||||
integralRun.AppendChild(new MathText("∫"));
|
||||
baseElem.AppendChild(integralRun);
|
||||
integralWithLimits.AppendChild(baseElem);
|
||||
|
||||
if (lowerLimitBuilder is not null)
|
||||
{
|
||||
var subArg = new SubArgument();
|
||||
integralWithLimits.AppendChild(subArg);
|
||||
builder.PushContext(subArg);
|
||||
lowerLimitBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
if (upperLimitBuilder is not null)
|
||||
{
|
||||
var superArg = new SuperArgument();
|
||||
integralWithLimits.AppendChild(superArg);
|
||||
builder.PushContext(superArg);
|
||||
upperLimitBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
|
||||
var funcRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(funcRun);
|
||||
builder.PushContext(funcRun);
|
||||
functionBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
var diffRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(diffRun);
|
||||
builder.PushContext(diffRun);
|
||||
differentialBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddSum(
|
||||
OpenXmlElement currentContext,
|
||||
Action<IFormula> expressionBuilder,
|
||||
Action<IFormula>? lowerLimitBuilder,
|
||||
Action<IFormula>? upperLimitBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var font = builder.BaseFont;
|
||||
|
||||
if (lowerLimitBuilder is null && upperLimitBuilder is null)
|
||||
{
|
||||
var sumRun = CreateMathRun(font);
|
||||
sumRun.AppendChild(new MathText("∑"));
|
||||
currentContext.AppendChild(sumRun);
|
||||
|
||||
var exprRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(exprRun);
|
||||
builder.PushContext(exprRun);
|
||||
expressionBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
else
|
||||
{
|
||||
var sumWithLimits = new SubSuperscript();
|
||||
currentContext.AppendChild(sumWithLimits);
|
||||
|
||||
var baseElem = new Base();
|
||||
var sumRun = CreateMathRun(font);
|
||||
sumRun.AppendChild(new MathText("∑"));
|
||||
baseElem.AppendChild(sumRun);
|
||||
sumWithLimits.AppendChild(baseElem);
|
||||
|
||||
if (lowerLimitBuilder is not null)
|
||||
{
|
||||
var subArg = new SubArgument();
|
||||
sumWithLimits.AppendChild(subArg);
|
||||
builder.PushContext(subArg);
|
||||
lowerLimitBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
if (upperLimitBuilder is not null)
|
||||
{
|
||||
var superArg = new SuperArgument();
|
||||
sumWithLimits.AppendChild(superArg);
|
||||
builder.PushContext(superArg);
|
||||
upperLimitBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
|
||||
var exprRun = CreateMathRun(font);
|
||||
currentContext.AppendChild(exprRun);
|
||||
builder.PushContext(exprRun);
|
||||
expressionBuilder(builder);
|
||||
builder.PopContext();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Создаёт степень.</summary>
|
||||
public static Superscript CreateSuperscript(
|
||||
Action<IFormula> baseBuilder,
|
||||
Action<IFormula> supBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var superscript = new Superscript();
|
||||
|
||||
// Основание
|
||||
var baseElem = new Base();
|
||||
superscript.AppendChild(baseElem);
|
||||
builder.PushContext(baseElem);
|
||||
baseBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
// Показатель
|
||||
var superArg = new SuperArgument();
|
||||
superscript.AppendChild(superArg);
|
||||
builder.PushContext(superArg);
|
||||
supBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
return superscript;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт нижний индекс.</summary>
|
||||
public static Subscript CreateSubscript(
|
||||
Action<IFormula> baseBuilder,
|
||||
Action<IFormula> subBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var subscript = new Subscript();
|
||||
|
||||
var baseElem = new Base();
|
||||
subscript.AppendChild(baseElem);
|
||||
builder.PushContext(baseElem);
|
||||
baseBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
var subArg = new SubArgument();
|
||||
subscript.AppendChild(subArg);
|
||||
builder.PushContext(subArg);
|
||||
subBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
return subscript;
|
||||
}
|
||||
|
||||
/// <summary>Создаёт одновременные нижний и верхний индексы.</summary>
|
||||
public static SubSuperscript CreateSubSuperscript(
|
||||
Action<IFormula> baseBuilder,
|
||||
Action<IFormula> subBuilder,
|
||||
Action<IFormula> supBuilder,
|
||||
IFormula builder)
|
||||
{
|
||||
var subSup = new SubSuperscript();
|
||||
|
||||
var baseElem = new Base();
|
||||
subSup.AppendChild(baseElem);
|
||||
builder.PushContext(baseElem);
|
||||
baseBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
var subArg = new SubArgument();
|
||||
subSup.AppendChild(subArg);
|
||||
builder.PushContext(subArg);
|
||||
subBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
var superArg = new SuperArgument();
|
||||
subSup.AppendChild(superArg);
|
||||
builder.PushContext(superArg);
|
||||
supBuilder(builder);
|
||||
builder.PopContext();
|
||||
|
||||
return subSup;
|
||||
}
|
||||
}
|
||||
315
QWERTYkez.WordProcessor/Builders/Interfaces.cs
Normal file
315
QWERTYkez.WordProcessor/Builders/Interfaces.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
public interface ITable
|
||||
{
|
||||
/// <summary>Базовый шрифт, используемый по умолчанию для содержимого таблицы.</summary>
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
/// <summary>Добавляет строку в таблицу, настраиваемую с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации строки через <see cref="IRow"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ITable"/> для цепочки вызовов.</returns>
|
||||
ITable AddRow(Action<IRow> configure);
|
||||
|
||||
/// <summary>Добавляет строку с указанной высотой, настраиваемую с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="height">Высота строки в сантиметрах.</param>
|
||||
/// <param name="configure">Делегат для конфигурации строки через <see cref="IRow"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ITable"/> для цепочки вызовов.</returns>
|
||||
ITable AddRow(double height, Action<IRow> configure);
|
||||
|
||||
/// <summary>Добавляет строку с ячейками, содержащими заданные тексты, используя указанный шрифт.</summary>
|
||||
/// <param name="font">Шрифт для всех ячеек строки.</param>
|
||||
/// <param name="cellTexts">Массив текстов для ячеек.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ITable"/> для цепочки вызовов.</returns>
|
||||
ITable AddRowWithCells(FontProps font, params string[] cellTexts);
|
||||
|
||||
/// <summary>Добавляет строку с ячейками, содержащими заданные тексты, используя базовый шрифт таблицы.</summary>
|
||||
/// <param name="cellTexts">Массив текстов для ячеек.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ITable"/> для цепочки вызовов.</returns>
|
||||
ITable AddRowWithCells(params string[] cellTexts);
|
||||
|
||||
/// <summary>Задаёт свойства таблицы: ширину и стиль границ, выравнивание.</summary>
|
||||
/// <param name="borderWidth">Ширина границ в пунктах (1/8 pt). По умолчанию 8.</param>
|
||||
/// <param name="borderValues">Стиль границ. По умолчанию <see cref="BorderValues.Single"/>.</param>
|
||||
/// <param name="tableAlignment">Выравнивание таблицы на странице. По умолчанию <see cref="TableRowAlignmentValues.Center"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ITable"/> для цепочки вызовов.</returns>
|
||||
ITable Properties(uint borderWidth = 8, BorderValues? borderValues = null, TableRowAlignmentValues? tableAlignment = null);
|
||||
|
||||
/// <summary>Создаёт объект <see cref="Table"/> на основе выполненных настроек.</summary>
|
||||
/// <returns>Готовый объект <see cref="Table"/> для вставки в документ.</returns>
|
||||
internal Table Build();
|
||||
}
|
||||
|
||||
public interface IRow
|
||||
{
|
||||
/// <summary>Базовый шрифт, используемый по умолчанию для содержимого строки.</summary>
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
/// <summary>Добавляет ячейку, настраиваемую с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации ячейки через <see cref="ICell"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(Action<ICell> configure);
|
||||
|
||||
/// <summary>Добавляет ячейку с заданными свойствами (без текста).</summary>
|
||||
/// <param name="cellProps">Свойства ячейки (ширина, объединение, вертикальное выравнивание).</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(CellProps cellProps);
|
||||
|
||||
/// <summary>Добавляет ячейку с заданными свойствами и текстом (используется базовый шрифт).</summary>
|
||||
/// <param name="cellProps">Свойства ячейки.</param>
|
||||
/// <param name="text">Текст ячейки.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(CellProps cellProps, string text);
|
||||
|
||||
/// <summary>Добавляет ячейку с заданными свойствами, текстом и указанным шрифтом.</summary>
|
||||
/// <param name="cellProps">Свойства ячейки.</param>
|
||||
/// <param name="text">Текст ячейки.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(CellProps cellProps, string text, FontProps font);
|
||||
|
||||
/// <summary>Добавляет ячейку с простым текстом (используется базовый шрифт).</summary>
|
||||
/// <param name="text">Текст ячейки.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(string text);
|
||||
|
||||
/// <summary>Добавляет ячейку с простым текстом и указанным шрифтом.</summary>
|
||||
/// <param name="text">Текст ячейки.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCell(string text, FontProps font);
|
||||
|
||||
/// <summary>Добавляет ячейку, содержащую один абзац, построенный с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации абзаца через <see cref="IParagraph"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCellWithPara(Action<IParagraph> configure);
|
||||
|
||||
/// <summary>Добавляет ячейку с указанными свойствами, содержащую один абзац, построенный с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="cellProps">Свойства ячейки.</param>
|
||||
/// <param name="configure">Делегат для конфигурации абзаца через <see cref="IParagraph"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow AddCellWithPara(CellProps cellProps, Action<IParagraph> configure);
|
||||
|
||||
/// <summary>Устанавливает точную высоту строки (свойство Exact).</summary>
|
||||
/// <param name="heightInCm">Высота в сантиметрах.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow SetExactHeight(double heightInCm);
|
||||
|
||||
/// <summary>Устанавливает минимальную высоту строки (свойство AtLeast).</summary>
|
||||
/// <param name="heightInCm">Высота в сантиметрах.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow SetHeight(double heightInCm);
|
||||
|
||||
/// <summary>Позволяет напрямую настроить свойства строки <see cref="TableRowProperties"/>.</summary>
|
||||
/// <param name="configure">Делегат для настройки свойств.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IRow"/> для цепочки вызовов.</returns>
|
||||
IRow SetProperties(Action<TableRowProperties> configure);
|
||||
|
||||
/// <summary>Создаёт объект <see cref="TableRow"/> на основе выполненных настроек.</summary>
|
||||
/// <returns>Готовый объект <see cref="TableRow"/> для вставки в таблицу.</returns>
|
||||
internal TableRow Build();
|
||||
}
|
||||
|
||||
public interface ICell
|
||||
{
|
||||
/// <summary>Базовый шрифт, используемый по умолчанию для содержимого ячейки.</summary>
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
/// <summary>Добавляет в ячейку абзац, содержащий математическую формулу.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации формулы через <see cref="IFormula"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ICell"/> для цепочки вызовов.</returns>
|
||||
ICell AddFormula(Action<IFormula> configure);
|
||||
|
||||
/// <summary>Добавляет в ячейку абзац, построенный с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации абзаца через <see cref="IParagraph"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ICell"/> для цепочки вызовов.</returns>
|
||||
ICell AddParagraph(Action<IParagraph> configure);
|
||||
|
||||
/// <summary>Добавляет в ячейку абзац, состоящий из нескольких строк текста (разделённых переносами).</summary>
|
||||
/// <param name="lines">Массив строк, каждая строка будет размещена на новой строке абзаца.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ICell"/> для цепочки вызовов.</returns>
|
||||
ICell AddParagraph(params string[] lines);
|
||||
|
||||
/// <summary>Добавляет в ячейку абзац с указанным текстом и опциональным шрифтом.</summary>
|
||||
/// <param name="text">Текст абзаца.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ICell"/> для цепочки вызовов.</returns>
|
||||
ICell AddParagraph(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Устанавливает свойства ячейки (ширина, объединение, вертикальное выравнивание).</summary>
|
||||
/// <param name="props">Свойства ячейки.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="ICell"/> для цепочки вызовов.</returns>
|
||||
ICell SetCellProps(CellProps props);
|
||||
|
||||
/// <summary>Создаёт объект <see cref="TableCell"/> на основе выполненных настроек.</summary>
|
||||
/// <returns>Готовый объект <see cref="TableCell"/> для вставки в строку.</returns>
|
||||
internal TableCell Build();
|
||||
}
|
||||
|
||||
public interface IText
|
||||
{
|
||||
/// <summary>Базовый шрифт, используемый по умолчанию для содержимого текстового блока.</summary>
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
/// <summary>Добавляет в документ абзац, содержащий математическую формулу.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации формулы через <see cref="IFormula"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IText"/> для цепочки вызовов.</returns>
|
||||
IText AddFormula(Action<IFormula> configure);
|
||||
|
||||
/// <summary>Добавляет абзац, построенный с помощью <paramref name="configure"/>.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации абзаца через <see cref="IParagraph"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IText"/> для цепочки вызовов.</returns>
|
||||
IText AddParagraph(Action<IParagraph> configure);
|
||||
|
||||
/// <summary>Добавляет абзац, состоящий из нескольких строк текста (разделённых переносами).</summary>
|
||||
/// <param name="lines">Массив строк, каждая строка будет размещена на новой строке абзаца.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IText"/> для цепочки вызовов.</returns>
|
||||
IText AddParagraph(params string[] lines);
|
||||
|
||||
/// <summary>Добавляет абзац с указанным текстом и опциональным шрифтом.</summary>
|
||||
/// <param name="text">Текст абзаца.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IText"/> для цепочки вызовов.</returns>
|
||||
IText AddParagraph(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Создаёт список абзацев <see cref="Paragraph"/> на основе выполненных настроек.</summary>
|
||||
/// <returns>Список готовых абзацев для вставки в документ.</returns>
|
||||
internal List<Paragraph> Build();
|
||||
}
|
||||
|
||||
public interface IParagraph
|
||||
{
|
||||
/// <summary>Базовый шрифт, используемый по умолчанию для содержимого абзаца.</summary>
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
/// <summary>Добавляет в абзац математическую формулу (внутри отдельного <see cref="Run"/>).</summary>
|
||||
/// <param name="configure">Делегат для конфигурации формулы через <see cref="IFormula"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddFormula(Action<IFormula> configure);
|
||||
|
||||
/// <summary>Добавляет в абзац математическую формулу (внутри отдельного <see cref="Run"/>) и разрыв строки (<see cref="DocumentFormat.OpenXml.Wordprocessing.Break"/>) в текущий абзац.</summary>
|
||||
/// <param name="configure">Делегат для конфигурации формулы через <see cref="IFormula"/>.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddFormulaBreak(Action<IFormula> configure);
|
||||
|
||||
/// <summary>Добавляет разрыв строки (<see cref="DocumentFormat.OpenXml.Wordprocessing.Break"/>) в текущий абзац.</summary>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph Break();
|
||||
|
||||
/// <summary>Добавляет разрыв строки (<see cref="DocumentFormat.OpenXml.Wordprocessing.Break"/>) в текущий абзац.</summary>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph BreakPage();
|
||||
|
||||
/// <summary>Добавляет текстовый фрагмент (<see cref="Run"/>) с указанным текстом и опциональным шрифтом.</summary>
|
||||
/// <param name="text">Текст фрагмента.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddRun(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Добавляет текстовый фрагмент (<see cref="Run"/>) с указанным текстом и опциональным шрифтом и разрыв строки (<see cref="DocumentFormat.OpenXml.Wordprocessing.Break"/>) в текущий абзац.</summary>
|
||||
/// <param name="text">Текст фрагмента.</param>
|
||||
/// <param name="font">Шрифт для текста. Если <see langword="null"/>, используется базовый шрифт.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddRunBreak(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Добавляет текстовый фрагмент с полным контролем над свойствами <see cref="RunProperties"/>.</summary>
|
||||
/// <param name="text">Текст фрагмента.</param>
|
||||
/// <param name="configure">Делегат для настройки свойств фрагмента.</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddRunWithCustomProps(string text, Action<RunProperties> configure);
|
||||
|
||||
/// <summary>Добавляет текстовый фрагмент, отформатированный как нижний индекс (<see cref="VerticalPositionValues.Subscript"/>).</summary>
|
||||
/// <param name="text">Текст фрагмента.</param>
|
||||
/// <param name="font">Базовый шрифт (свойства <see cref="SubSup"/> будут переопределены).</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddSubRun(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Добавляет текстовый фрагмент, отформатированный как верхний индекс (<see cref="VerticalPositionValues.Superscript"/>).</summary>
|
||||
/// <param name="text">Текст фрагмента.</param>
|
||||
/// <param name="font">Базовый шрифт (свойства <see cref="SubSup"/> будут переопределены).</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph AddSupRun(string text, FontProps? font = null);
|
||||
|
||||
/// <summary>Устанавливает выравнивание абзаца.</summary>
|
||||
/// <param name="alignment">Тип выравнивания (лево, право, центр, по ширине).</param>
|
||||
/// <returns>Тот же экземпляр <see cref="IParagraph"/> для цепочки вызовов.</returns>
|
||||
IParagraph SetAlignment(JustificationValues alignment);
|
||||
|
||||
/// <summary>Создаёт объект <see cref="Paragraph"/> на основе выполненных настроек.</summary>
|
||||
/// <returns>Готовый объект <see cref="Paragraph"/>.</returns>
|
||||
internal Paragraph Build();
|
||||
}
|
||||
|
||||
public interface IFormula
|
||||
{
|
||||
FontProps BaseFont { get; }
|
||||
|
||||
IFormula Text(string text);
|
||||
|
||||
|
||||
IFormula Division(string numerator, string denominator);
|
||||
IFormula Division(string numerator, Action<IFormula> denominator);
|
||||
IFormula Division(Action<IFormula> numerator, string denominator);
|
||||
IFormula Division(Action<IFormula> numerator, Action<IFormula> denominator);
|
||||
|
||||
|
||||
IFormula Sup(string baseText, string supText);
|
||||
IFormula Sup(string baseText, Action<IFormula> supText);
|
||||
IFormula Sup(Action<IFormula> baseText, string supText);
|
||||
IFormula Sup(Action<IFormula> baseText, Action<IFormula> supText);
|
||||
|
||||
|
||||
IFormula Sub(string baseText, string subText);
|
||||
IFormula Sub(string baseText, Action<IFormula> subText);
|
||||
IFormula Sub(Action<IFormula> baseText, string subText);
|
||||
IFormula Sub(Action<IFormula> baseText, Action<IFormula> subText);
|
||||
|
||||
|
||||
IFormula SubSup(string baseText, string subText, string supText);
|
||||
IFormula SubSup(string baseText, string subText, Action<IFormula> supText);
|
||||
IFormula SubSup(string baseText, Action<IFormula> subText, string supText);
|
||||
IFormula SubSup(string baseText, Action<IFormula> subText, Action<IFormula> supText);
|
||||
IFormula SubSup(Action<IFormula> baseText, string subText, string supText);
|
||||
IFormula SubSup(Action<IFormula> baseText, string subText, Action<IFormula> supText);
|
||||
IFormula SubSup(Action<IFormula> baseText, Action<IFormula> subText, string supText);
|
||||
IFormula SubSup(Action<IFormula> baseText, Action<IFormula> subText, Action<IFormula> supText);
|
||||
|
||||
|
||||
IFormula Root(string radicand);
|
||||
IFormula Root(Action<IFormula> radicand);
|
||||
IFormula Root(string radicand, string degree);
|
||||
IFormula Root(string radicand, Action<IFormula> degree);
|
||||
IFormula Root(Action<IFormula> radicand, string degree);
|
||||
IFormula Root(Action<IFormula> radicand, Action<IFormula> degree);
|
||||
|
||||
|
||||
IFormula Integral(string function, string differential, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(string function, string differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(string function, string differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(string function, string differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(string function, Action<IFormula> differential, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(string function, Action<IFormula> differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(string function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(string function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, string differential, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, string differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, string differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, string differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, Action<IFormula> differential, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, Action<IFormula> differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Integral(Action<IFormula> function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
|
||||
|
||||
IFormula Sum(string expression, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Sum(string expression, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Sum(string expression, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Sum(string expression, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Sum(Action<IFormula> expression, string? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Sum(Action<IFormula> expression, string? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
IFormula Sum(Action<IFormula> expression, Action<IFormula>? lowerLimit = null, string? upperLimit = null);
|
||||
IFormula Sum(Action<IFormula> expression, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null);
|
||||
|
||||
|
||||
internal void PushContext(OpenXmlElement element);
|
||||
internal void PopContext();
|
||||
}
|
||||
431
QWERTYkez.WordProcessor/Builders/ParagraphBuilderBase.cs
Normal file
431
QWERTYkez.WordProcessor/Builders/ParagraphBuilderBase.cs
Normal file
@@ -0,0 +1,431 @@
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
abstract class ParagraphBuilderBase : IParagraph, IFormula
|
||||
{
|
||||
public FontProps BaseFont { get; internal set; }
|
||||
|
||||
// Конструктор для установки базового шрифта
|
||||
protected ParagraphBuilderBase(FontProps? baseFont = null)
|
||||
{
|
||||
BaseFont = baseFont is not null && baseFont.SubSup.HasValue
|
||||
? baseFont with { SubSup = null }
|
||||
: baseFont ?? new FontProps();
|
||||
}
|
||||
|
||||
|
||||
internal FontProps GetEffectiveFont(FontProps? overrideFont) => overrideFont ?? BaseFont;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
protected Paragraph _paragraph = null!;
|
||||
protected List<Run> _runs = null!;
|
||||
|
||||
internal IParagraph ParagraphBuilder => CreateParagraphBuilder();
|
||||
protected virtual IParagraph CreateParagraphBuilder()
|
||||
{
|
||||
_paragraph = new Paragraph
|
||||
{
|
||||
ParagraphProperties = new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
After = "0",
|
||||
Before = "0",
|
||||
LineRule = LineSpacingRuleValues.Auto
|
||||
},
|
||||
new Indentation
|
||||
{
|
||||
Left = "0",
|
||||
Right = "0",
|
||||
FirstLine = "0",
|
||||
Hanging = "0"
|
||||
}
|
||||
)
|
||||
};
|
||||
_runs = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Paragraph IParagraph.Build()
|
||||
{
|
||||
foreach (var run in _runs)
|
||||
{
|
||||
_paragraph.AppendChild(run);
|
||||
}
|
||||
return _paragraph;
|
||||
}
|
||||
|
||||
// Настройка выравнивания
|
||||
public IParagraph SetAlignment(JustificationValues alignment)
|
||||
{
|
||||
var props = _paragraph.ParagraphProperties;
|
||||
props?.Justification = new Justification { Val = alignment };
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление текста
|
||||
public IParagraph AddRun(string text, FontProps? font = null)
|
||||
{
|
||||
// Заменяем обычные пробелы на неразрывные для сохранения видимости
|
||||
string processedText = text.Replace(' ', '\u00A0');
|
||||
|
||||
var effectiveFont = GetEffectiveFont(font);
|
||||
Run run;
|
||||
|
||||
if (effectiveFont.TryExtract(out var elements))
|
||||
{
|
||||
run = new Run(new RunProperties(elements), new Text(processedText));
|
||||
}
|
||||
else
|
||||
{
|
||||
run = new Run(new Text(processedText));
|
||||
}
|
||||
|
||||
_runs.Add(run);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление текста
|
||||
public IParagraph AddRunBreak(string text, FontProps? font = null)
|
||||
{
|
||||
// Заменяем обычные пробелы на неразрывные для сохранения видимости
|
||||
string processedText = text.Replace(' ', '\u00A0');
|
||||
|
||||
var effectiveFont = GetEffectiveFont(font);
|
||||
Run run;
|
||||
|
||||
if (effectiveFont.TryExtract(out var elements))
|
||||
{
|
||||
run = new Run(new RunProperties(elements), new Text(processedText));
|
||||
}
|
||||
else
|
||||
{
|
||||
run = new Run(new Text(processedText));
|
||||
}
|
||||
|
||||
_runs.Add(run);
|
||||
|
||||
_runs.Add(new Run(new Break()));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление текста
|
||||
public IParagraph AddSupRun(string text, FontProps? font = null)
|
||||
{
|
||||
var effectiveFont = GetEffectiveFont(font);
|
||||
Run run;
|
||||
|
||||
if (effectiveFont.TrySupExtract(out var elements))
|
||||
{
|
||||
run = new Run(new RunProperties(elements), new Text(text));
|
||||
}
|
||||
else
|
||||
{
|
||||
run = new Run(new Text(text));
|
||||
}
|
||||
|
||||
_runs.Add(run);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление текста
|
||||
public IParagraph AddSubRun(string text, FontProps? font = null)
|
||||
{
|
||||
var effectiveFont = GetEffectiveFont(font);
|
||||
Run run;
|
||||
|
||||
if (effectiveFont.TrySubExtract(out var elements))
|
||||
{
|
||||
run = new Run(new RunProperties(elements), new Text(text));
|
||||
}
|
||||
else
|
||||
{
|
||||
run = new Run(new Text(text));
|
||||
}
|
||||
|
||||
_runs.Add(run);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IParagraph AddRunWithCustomProps(string text, Action<RunProperties> configure)
|
||||
{
|
||||
var props = new RunProperties();
|
||||
configure(props);
|
||||
_runs.Add(new Run(props, new Text(text)));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление разрыва строки
|
||||
public IParagraph Break()
|
||||
{
|
||||
_runs.Add(new Run(new Break()));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление разрыва строки
|
||||
public IParagraph BreakPage()
|
||||
{
|
||||
_runs.Add(new Run(new Break() { Type = BreakValues.Page }));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Метод AddFormula для IParagraphBuilder
|
||||
IParagraph IParagraph.AddFormula(Action<IFormula> configure)
|
||||
{
|
||||
var math = new OfficeMath();
|
||||
try
|
||||
{
|
||||
_mathContextStack.Push(math);
|
||||
configure(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mathContextStack.Pop();
|
||||
}
|
||||
|
||||
var run = new Run(); // без RunProperties
|
||||
run.AppendChild(math);
|
||||
_runs.Add(run);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Метод AddFormula для IParagraphBuilder
|
||||
IParagraph IParagraph.AddFormulaBreak(Action<IFormula> configure)
|
||||
{
|
||||
var math = new OfficeMath();
|
||||
try
|
||||
{
|
||||
_mathContextStack.Push(math);
|
||||
configure(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mathContextStack.Pop();
|
||||
}
|
||||
|
||||
var run = new Run(); // без RunProperties
|
||||
run.AppendChild(math);
|
||||
_runs.Add(run);
|
||||
_runs.Add(new Run(new Break()));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Стек для вложенных математических конструкций
|
||||
private readonly Stack<OpenXmlElement> _mathContextStack = new();
|
||||
bool TryGetCurrentMathContext(out OpenXmlElement element)
|
||||
{
|
||||
if (_mathContextStack.Count > 0)
|
||||
{
|
||||
element = _mathContextStack.Peek();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
element = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Вспомогательные методы для стека (используются FormulaHelper)
|
||||
void IFormula.PushContext(OpenXmlElement element) => _mathContextStack.Push(element);
|
||||
void IFormula.PopContext() => _mathContextStack.Pop();
|
||||
|
||||
|
||||
// Реализация IFormulaBuilder
|
||||
public IFormula Text(string text)
|
||||
{
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
FormulaHelper.AddText(element, text, BaseFont);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// Division
|
||||
public IFormula Division(string numerator, string denominator) =>
|
||||
Division(ToAction(numerator), ToAction(denominator));
|
||||
public IFormula Division(string numerator, Action<IFormula> denominator) =>
|
||||
Division(ToAction(numerator), denominator);
|
||||
public IFormula Division(Action<IFormula> numerator, string denominator) =>
|
||||
Division(numerator, ToAction(denominator));
|
||||
public IFormula Division(Action<IFormula> numerator, Action<IFormula> denominator)
|
||||
{
|
||||
var fraction = FormulaHelper.CreateFraction(numerator, denominator, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(fraction);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// Sup
|
||||
public IFormula Sup(string baseText, string supText) =>
|
||||
Sup(ToAction(baseText), ToAction(supText));
|
||||
public IFormula Sup(string baseText, Action<IFormula> supText) =>
|
||||
Sup(ToAction(baseText), supText);
|
||||
public IFormula Sup(Action<IFormula> baseText, string supText) =>
|
||||
Sup(baseText, ToAction(supText));
|
||||
public IFormula Sup(Action<IFormula> baseText, Action<IFormula> supText)
|
||||
{
|
||||
var superscript = FormulaHelper.CreateSuperscript(baseText, supText, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(superscript);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// Sub
|
||||
public IFormula Sub(string baseText, string subText) =>
|
||||
Sub(ToAction(baseText), ToAction(subText));
|
||||
public IFormula Sub(string baseText, Action<IFormula> subText) =>
|
||||
Sub(ToAction(baseText), subText);
|
||||
public IFormula Sub(Action<IFormula> baseText, string subText) =>
|
||||
Sub(baseText, ToAction(subText));
|
||||
public IFormula Sub(Action<IFormula> baseText, Action<IFormula> subText)
|
||||
{
|
||||
var subscript = FormulaHelper.CreateSubscript(baseText, subText, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(subscript);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// SubSup
|
||||
public IFormula SubSup(string baseText, string subText, string supText) =>
|
||||
SubSup(ToAction(baseText), ToAction(subText), ToAction(supText));
|
||||
public IFormula SubSup(string baseText, string subText, Action<IFormula> supText) =>
|
||||
SubSup(ToAction(baseText), ToAction(subText), supText);
|
||||
public IFormula SubSup(string baseText, Action<IFormula> subText, string supText) =>
|
||||
SubSup(ToAction(baseText), subText, ToAction(supText));
|
||||
public IFormula SubSup(string baseText, Action<IFormula> subText, Action<IFormula> supText) =>
|
||||
SubSup(ToAction(baseText), subText, supText);
|
||||
public IFormula SubSup(Action<IFormula> baseText, string subText, string supText) =>
|
||||
SubSup(baseText, ToAction(subText), ToAction(supText));
|
||||
public IFormula SubSup(Action<IFormula> baseText, string subText, Action<IFormula> supText) =>
|
||||
SubSup(baseText, ToAction(subText), supText);
|
||||
public IFormula SubSup(Action<IFormula> baseText, Action<IFormula> subText, string supText) =>
|
||||
SubSup(baseText, subText, ToAction(supText));
|
||||
public IFormula SubSup(Action<IFormula> baseText, Action<IFormula> subText, Action<IFormula> supText)
|
||||
{
|
||||
var subSup = FormulaHelper.CreateSubSuperscript(baseText, subText, supText, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(subSup);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// Root
|
||||
public IFormula Root(string radicand) => Root(ToAction(radicand));
|
||||
public IFormula Root(Action<IFormula> radicand)
|
||||
{
|
||||
var radical = FormulaHelper.CreateRadical(radicand, null, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(radical);
|
||||
return this;
|
||||
}
|
||||
public IFormula Root(string radicand, string degree) =>
|
||||
Root(ToAction(radicand), ToAction(degree));
|
||||
public IFormula Root(string radicand, Action<IFormula> degree) =>
|
||||
Root(ToAction(radicand), degree);
|
||||
public IFormula Root(Action<IFormula> radicand, string degree) =>
|
||||
Root(radicand, ToAction(degree));
|
||||
public IFormula Root(Action<IFormula> radicand, Action<IFormula> degree)
|
||||
{
|
||||
var radical = FormulaHelper.CreateRadical(radicand, degree, this);
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
element.AppendChild(radical);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
private void AddIntegralCore(
|
||||
Action<IFormula> function,
|
||||
Action<IFormula> differential,
|
||||
Action<IFormula>? lowerLimit,
|
||||
Action<IFormula>? upperLimit)
|
||||
{
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
FormulaHelper.AddIntegral(element, function, differential, lowerLimit, upperLimit, this);
|
||||
}
|
||||
|
||||
public IFormula Integral(string function, string differential, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(ToAction(function), ToAction(differential), ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Integral(string function, string differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(ToAction(function), ToAction(differential), ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Integral(string function, string differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(ToAction(function), ToAction(differential), lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Integral(string function, string differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(ToAction(function), ToAction(differential), lowerLimit, upperLimit);
|
||||
public IFormula Integral(string function, Action<IFormula> differential, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(ToAction(function), differential, ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Integral(string function, Action<IFormula> differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(ToAction(function), differential, ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Integral(string function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(ToAction(function), differential, lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Integral(string function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(ToAction(function), differential, lowerLimit, upperLimit);
|
||||
public IFormula Integral(Action<IFormula> function, string differential, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(function, ToAction(differential), ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Integral(Action<IFormula> function, string differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(function, ToAction(differential), ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Integral(Action<IFormula> function, string differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(function, ToAction(differential), lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Integral(Action<IFormula> function, string differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(function, ToAction(differential), lowerLimit, upperLimit);
|
||||
public IFormula Integral(Action<IFormula> function, Action<IFormula> differential, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(function, differential, ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Integral(Action<IFormula> function, Action<IFormula> differential, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Integral(function, differential, ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Integral(Action<IFormula> function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Integral(function, differential, lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Integral(Action<IFormula> function, Action<IFormula> differential, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null)
|
||||
{
|
||||
AddIntegralCore(function, differential, lowerLimit, upperLimit);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// Sum
|
||||
private void AddSumCore(
|
||||
Action<IFormula> expression,
|
||||
Action<IFormula>? lowerLimit,
|
||||
Action<IFormula>? upperLimit)
|
||||
{
|
||||
if (TryGetCurrentMathContext(out var element))
|
||||
FormulaHelper.AddSum(element, expression, lowerLimit, upperLimit, this);
|
||||
}
|
||||
|
||||
public IFormula Sum(string expression, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Sum(ToAction(expression), ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Sum(string expression, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Sum(ToAction(expression), ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Sum(string expression, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Sum(ToAction(expression), lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Sum(string expression, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Sum(ToAction(expression), lowerLimit, upperLimit);
|
||||
public IFormula Sum(Action<IFormula> expression, string? lowerLimit = null, string? upperLimit = null) =>
|
||||
Sum(expression, ToNullableAction(lowerLimit), ToNullableAction(upperLimit));
|
||||
public IFormula Sum(Action<IFormula> expression, string? lowerLimit = null, Action<IFormula>? upperLimit = null) =>
|
||||
Sum(expression, ToNullableAction(lowerLimit), upperLimit);
|
||||
public IFormula Sum(Action<IFormula> expression, Action<IFormula>? lowerLimit = null, string? upperLimit = null) =>
|
||||
Sum(expression, lowerLimit, ToNullableAction(upperLimit));
|
||||
public IFormula Sum(Action<IFormula> expression, Action<IFormula>? lowerLimit = null, Action<IFormula>? upperLimit = null)
|
||||
{
|
||||
AddSumCore(expression, lowerLimit, upperLimit);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Вспомогательный метод для преобразования строки в делегат
|
||||
private static Action<IFormula>? ToNullableAction(string? text) => text is null ? null : (b => b.Text(text));
|
||||
private static Action<IFormula> ToAction(string text) => b => b.Text(text);
|
||||
}
|
||||
328
QWERTYkez.WordProcessor/Builders/TableBuilder.cs
Normal file
328
QWERTYkez.WordProcessor/Builders/TableBuilder.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
internal sealed class TableBuilder : ParagraphBuilderBase, ITable, IRow, ICell
|
||||
{
|
||||
private readonly Table _table = new();
|
||||
|
||||
internal TableBuilder(FontProps? baseFont) : base(baseFont)
|
||||
{
|
||||
}
|
||||
|
||||
internal static TableBuilder Create(FontProps? baseFont = null) => new(baseFont);
|
||||
|
||||
|
||||
// Метод завершения сборки
|
||||
public Table Build() => _table;
|
||||
|
||||
// Публичные методы настройки таблицы
|
||||
public ITable Properties(
|
||||
uint borderWidth = 8,
|
||||
BorderValues? borderValues = null,
|
||||
TableRowAlignmentValues? tableAlignment = null)
|
||||
{
|
||||
var borderType = borderValues ?? BorderValues.Single;
|
||||
var alignment = tableAlignment ?? TableRowAlignmentValues.Center;
|
||||
|
||||
_table.AppendChild(new TableProperties(
|
||||
new TableBorders(
|
||||
new TopBorder() { Val = borderType, Size = borderWidth },
|
||||
new BottomBorder() { Val = borderType, Size = borderWidth },
|
||||
new LeftBorder() { Val = borderType, Size = borderWidth },
|
||||
new RightBorder() { Val = borderType, Size = borderWidth },
|
||||
new InsideHorizontalBorder() { Val = borderType, Size = borderWidth },
|
||||
new InsideVerticalBorder() { Val = borderType, Size = borderWidth }
|
||||
),
|
||||
new TableJustification { Val = alignment },
|
||||
new TableLayout() { Type = TableLayoutValues.Autofit }
|
||||
));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление строк с использованием IRowBuilder
|
||||
public ITable AddRow(Action<IRow> configure)
|
||||
{
|
||||
var IRowBuilder = RowBuilder;
|
||||
configure(this);
|
||||
_table.AppendChild(IRowBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ITable AddRow(double height, Action<IRow> configure)
|
||||
{
|
||||
var IRowBuilder = RowBuilder.SetHeight(height);
|
||||
configure(this);
|
||||
_table.AppendChild(IRowBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
// Быстрые методы для добавления строк с ячейками
|
||||
public ITable AddRowWithCells(params string[] cellTexts)
|
||||
{
|
||||
AddRow(row =>
|
||||
{
|
||||
foreach (var text in cellTexts)
|
||||
{
|
||||
row.AddCell(text);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ITable AddRowWithCells(FontProps font, params string[] cellTexts)
|
||||
{
|
||||
AddRow(row =>
|
||||
{
|
||||
foreach (var text in cellTexts)
|
||||
{
|
||||
row.AddCell(text, font);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private TableRow _row = null!;
|
||||
internal IRow RowBuilder
|
||||
{
|
||||
get
|
||||
{
|
||||
_row = new();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
TableRow IRow.Build() => _row;
|
||||
|
||||
|
||||
// Настройка высоты строки (исправленный метод)
|
||||
public IRow SetHeight(double heightInCm)
|
||||
{
|
||||
var height = new TableRowHeight
|
||||
{
|
||||
Val = (UInt32Value)(uint)(heightInCm * 567), // 1 см = 567 DXA
|
||||
HeightType = HeightRuleValues.AtLeast
|
||||
};
|
||||
|
||||
// Убедимся, что TableRowProperties существует
|
||||
if (_row.TableRowProperties is null)
|
||||
{
|
||||
_row.TableRowProperties = new TableRowProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Удалим существующий TableRowHeight, если он есть
|
||||
var existingHeight = _row.TableRowProperties.Elements<TableRowHeight>().FirstOrDefault();
|
||||
existingHeight?.Remove();
|
||||
}
|
||||
|
||||
// Добавляем TableRowHeight как дочерний элемент
|
||||
_row.TableRowProperties.AppendChild(height);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow SetExactHeight(double heightInCm)
|
||||
{
|
||||
var height = new TableRowHeight
|
||||
{
|
||||
Val = (UInt32Value)(uint)(heightInCm * 567),
|
||||
HeightType = HeightRuleValues.Exact
|
||||
};
|
||||
|
||||
// Убедимся, что TableRowProperties существует
|
||||
if (_row.TableRowProperties is null)
|
||||
{
|
||||
_row.TableRowProperties = new TableRowProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Удалим существующий TableRowHeight, если он есть
|
||||
var existingHeight = _row.TableRowProperties.Elements<TableRowHeight>().FirstOrDefault();
|
||||
existingHeight?.Remove();
|
||||
}
|
||||
|
||||
// Добавляем TableRowHeight как дочерний элемент
|
||||
_row.TableRowProperties.AppendChild(height);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление ячеек
|
||||
public IRow AddCell(Action<ICell> configure)
|
||||
{
|
||||
var cellBuilder = CellBuilder;
|
||||
configure(this);
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow AddCell(string text)
|
||||
{
|
||||
var cellBuilder = CellBuilder;
|
||||
cellBuilder.AddParagraph(p => p.AddRun(text));
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow AddCell(string text, FontProps font)
|
||||
{
|
||||
var cellBuilder = CellBuilder;
|
||||
|
||||
if (font is not null)
|
||||
cellBuilder.AddParagraph(p => p.AddRun(text, font));
|
||||
else cellBuilder.AddParagraph(p => p.AddRun(text));
|
||||
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow AddCell(CellProps cellProps, string text, FontProps font)
|
||||
{
|
||||
var cellBuilder = CellBuilder.SetCellProps(cellProps);
|
||||
|
||||
if (font is not null)
|
||||
cellBuilder.AddParagraph(p => p.AddRun(text, font));
|
||||
else cellBuilder.AddParagraph(p => p.AddRun(text));
|
||||
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow AddCell(CellProps cellProps, string text)
|
||||
{
|
||||
var cellBuilder = CellBuilder.SetCellProps(cellProps);
|
||||
cellBuilder.AddParagraph(p => p.AddRun(text));
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRow AddCell(CellProps cellProps)
|
||||
{
|
||||
var cellBuilder = CellBuilder.SetCellProps(cellProps);
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
// Метод для установки произвольных свойств строки
|
||||
public IRow SetProperties(Action<TableRowProperties> configure)
|
||||
{
|
||||
_row.TableRowProperties ??= new TableRowProperties();
|
||||
configure(_row.TableRowProperties);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public IRow AddCellWithPara(Action<IParagraph> configure)
|
||||
{
|
||||
var cellBuilder = CellBuilder;
|
||||
cellBuilder.AddParagraph(configure);
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
public IRow AddCellWithPara(CellProps cellProps, Action<IParagraph> configure)
|
||||
{
|
||||
var cellBuilder = CellBuilder.SetCellProps(cellProps);
|
||||
cellBuilder.AddParagraph(configure);
|
||||
_row.AppendChild(cellBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private TableCell _cell = null!;
|
||||
private List<Paragraph> _paragraphs = null!;
|
||||
internal ICell CellBuilder
|
||||
{
|
||||
get
|
||||
{
|
||||
_cell = new();
|
||||
_paragraphs = [];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
TableCell ICell.Build()
|
||||
{
|
||||
foreach (var paragraph in _paragraphs)
|
||||
{
|
||||
_cell.AppendChild(paragraph);
|
||||
}
|
||||
return _cell;
|
||||
}
|
||||
|
||||
// Установка свойств ячейки
|
||||
public ICell SetCellProps(CellProps props)
|
||||
{
|
||||
if (props.TryExtract(out var elements))
|
||||
{
|
||||
_cell.TableCellProperties = new TableCellProperties(elements);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление параграфов
|
||||
public ICell AddParagraph(Action<IParagraph> configure)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
configure(paraBuilder);
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell AddParagraph(string text, FontProps? font = null)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
|
||||
if (font is not null)
|
||||
{
|
||||
paraBuilder.AddRun(text, font);
|
||||
}
|
||||
else
|
||||
{
|
||||
paraBuilder.AddRun(text);
|
||||
}
|
||||
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICell AddParagraph(params string[] lines)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
paraBuilder.AddRun(lines[0]);
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
paraBuilder.Break();
|
||||
paraBuilder.AddRun(lines[i]);
|
||||
}
|
||||
}
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
// Метод AddFormula для ICellBuilder
|
||||
ICell ICell.AddFormula(Action<IFormula> configure)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
paraBuilder.AddFormula(configure);
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
110
QWERTYkez.WordProcessor/Builders/TextBuilder.cs
Normal file
110
QWERTYkez.WordProcessor/Builders/TextBuilder.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
internal sealed class TextBuilder : ParagraphBuilderBase, IText
|
||||
{
|
||||
private readonly ParagraphProperties? _baseParagraphProperties;
|
||||
|
||||
internal TextBuilder(FontProps? baseFont, ParagraphProperties? baseParagraphProperties = null) : base(baseFont)
|
||||
{
|
||||
_baseParagraphProperties = baseParagraphProperties?.CloneNode(true) as ParagraphProperties;
|
||||
}
|
||||
|
||||
internal static TextBuilder Create(FontProps? baseFont = null, ParagraphProperties? baseParagraphProperties = null) =>
|
||||
new(baseFont, baseParagraphProperties);
|
||||
|
||||
|
||||
|
||||
|
||||
// Переопределяем создание параграфа
|
||||
protected override IParagraph CreateParagraphBuilder()
|
||||
{
|
||||
_paragraph = new Paragraph();
|
||||
|
||||
if (_baseParagraphProperties is not null)
|
||||
{
|
||||
_paragraph.ParagraphProperties = _baseParagraphProperties.CloneNode(true) as ParagraphProperties;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Используем стандартные свойства (как в базовом классе)
|
||||
_paragraph.ParagraphProperties = new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
After = "0",
|
||||
Before = "0",
|
||||
LineRule = LineSpacingRuleValues.Auto
|
||||
},
|
||||
new Indentation
|
||||
{
|
||||
Left = "0",
|
||||
Right = "0",
|
||||
FirstLine = "0",
|
||||
Hanging = "0"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_runs = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private readonly List<Paragraph> _paragraphs = [];
|
||||
public List<Paragraph> Build() => _paragraphs;
|
||||
|
||||
IText IText.AddFormula(Action<IFormula> configure)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
paraBuilder.AddFormula(configure);
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
// Добавление параграфов
|
||||
public IText AddParagraph(Action<IParagraph> configure)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
configure(paraBuilder);
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IText AddParagraph(string text, FontProps? font = null)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
|
||||
if (font is not null)
|
||||
{
|
||||
paraBuilder.AddRun(text, font);
|
||||
}
|
||||
else
|
||||
{
|
||||
paraBuilder.AddRun(text);
|
||||
}
|
||||
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IText AddParagraph(params string[] lines)
|
||||
{
|
||||
var paraBuilder = ParagraphBuilder;
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
paraBuilder.AddRun(lines[0]);
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
paraBuilder.Break();
|
||||
paraBuilder.AddRun(lines[i]);
|
||||
}
|
||||
}
|
||||
_paragraphs.Add(paraBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
371
QWERTYkez.WordProcessor/HeaderFooterProcessor.cs
Normal file
371
QWERTYkez.WordProcessor/HeaderFooterProcessor.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик текста в верхних и нижних колонтитулах DOCX документов
|
||||
/// </summary>
|
||||
internal static class HeaderFooterProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Заменяет текст во всех колонтитулах документа
|
||||
/// </summary>
|
||||
internal static void ReplaceInHeadersFooters(WordprocessingDocument document, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (document is null || string.IsNullOrEmpty(oldValue) || newValues is null)
|
||||
return;
|
||||
|
||||
var headers = GetAllHeaders(document).ToList();
|
||||
var footers = GetAllFooters(document).ToList();
|
||||
|
||||
if (headers.Count == 0 && footers.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int headerMatches = 0;
|
||||
int footerMatches = 0;
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (ContainsText(header, oldValue, comparisonType))
|
||||
{
|
||||
headerMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
if (ContainsText(footer, oldValue, comparisonType))
|
||||
{
|
||||
footerMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerMatches > 0 || footerMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters] Looking for '{oldValue}'");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters] Headers: {headers.Count} total, {headerMatches} with matches");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters] Footers: {footers.Count} total, {footerMatches} with matches");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
ReplaceInElement(header, oldValue, newValues, comparisonType);
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
ReplaceInElement(footer, oldValue, newValues, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену во всех колонтитулах
|
||||
/// </summary>
|
||||
internal static void ReplaceInHeadersFooters(WordprocessingDocument document, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (document is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var headers = GetAllHeaders(document).ToList();
|
||||
var footers = GetAllFooters(document).ToList();
|
||||
|
||||
if (headers.Count == 0 && footers.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int headerMatches = 0;
|
||||
int footerMatches = 0;
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && ContainsText(header, kvp.Key, comparisonType))
|
||||
{
|
||||
headerMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && ContainsText(footer, kvp.Key, comparisonType))
|
||||
{
|
||||
footerMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (headerMatches > 0 || footerMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] START with {replacements.Count()} items");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] Headers: {headers.Count} total, {headerMatches} with matches");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] Footers: {footers.Count} total, {footerMatches} with matches");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
ReplaceInElement(header, replacements, comparisonType);
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
ReplaceInElement(footer, replacements, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену во всех колонтитулах
|
||||
/// </summary>
|
||||
internal static void ReplaceInHeadersFooters(WordprocessingDocument document, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (document is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var headers = GetAllHeaders(document).ToList();
|
||||
var footers = GetAllFooters(document).ToList();
|
||||
|
||||
if (headers.Count == 0 && footers.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int headerMatches = 0;
|
||||
int footerMatches = 0;
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && ContainsText(header, kvp.Key, comparisonType))
|
||||
{
|
||||
headerMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && ContainsText(footer, kvp.Key, comparisonType))
|
||||
{
|
||||
footerMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (headerMatches > 0 || footerMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] START with {replacements.Count()} items");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] Headers: {headers.Count} total, {headerMatches} with matches");
|
||||
Debug.WriteLine($"[DEBUG] [HeaderFooterProcessor.ReplaceInHeadersFooters(dict)] Footers: {footers.Count} total, {footerMatches} with matches");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
ReplaceInElement(header, replacements, comparisonType);
|
||||
}
|
||||
|
||||
foreach (var footer in footers)
|
||||
{
|
||||
ReplaceInElement(footer, replacements, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет текст в указанном элементе (Header или Footer)
|
||||
/// </summary>
|
||||
private static void ReplaceInElement(OpenXmlElement element, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null)
|
||||
return;
|
||||
|
||||
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||||
if (paragraphs.Count == 0)
|
||||
return;
|
||||
|
||||
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||||
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||||
|
||||
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||||
|
||||
if (newValues.Count() == 1)
|
||||
{
|
||||
foreach (var paragraph in allParagraphs)
|
||||
{
|
||||
paragraph?.SimpleReplace(oldValue, newValues.First(), comparisonType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var paragraph in allParagraphs)
|
||||
{
|
||||
if (paragraph is not null && paragraph.InnerText.IndexOf(oldValue, comparisonType) >= 0)
|
||||
{
|
||||
paragraph.ReplaceWithMultiple(oldValue, newValues, comparisonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену в указанном элементе
|
||||
/// </summary>
|
||||
private static void ReplaceInElement(OpenXmlElement element, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||||
if (paragraphs.Count == 0)
|
||||
return;
|
||||
|
||||
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||||
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||||
|
||||
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||||
|
||||
foreach (var paragraph in allParagraphs)
|
||||
{
|
||||
if (paragraph is null)
|
||||
continue;
|
||||
|
||||
var paraText = paragraph.InnerText;
|
||||
bool hasMatch = false;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && paraText.IndexOf(kvp.Key, comparisonType) >= 0)
|
||||
{
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMatch)
|
||||
continue;
|
||||
|
||||
var newParagraphs = MultiReplaceExt.ProcessParagraphWithAllReplacements(paragraph, replacements, comparisonType);
|
||||
if (newParagraphs is not null && newParagraphs.Count > 0)
|
||||
{
|
||||
ReplaceParagraphInParent(paragraph, newParagraphs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену в указанном элементе
|
||||
/// </summary>
|
||||
private static void ReplaceInElement(OpenXmlElement element, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||||
if (paragraphs.Count == 0)
|
||||
return;
|
||||
|
||||
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||||
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||||
|
||||
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||||
|
||||
foreach (var paragraph in allParagraphs)
|
||||
{
|
||||
if (paragraph is null)
|
||||
continue;
|
||||
|
||||
var paraText = paragraph.InnerText;
|
||||
bool hasMatch = false;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && paraText.IndexOf(kvp.Key, comparisonType) >= 0)
|
||||
{
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMatch)
|
||||
continue;
|
||||
|
||||
var newParagraphs = MultiReplaceExt.ProcessParagraphWithAllReplacements(paragraph, replacements, comparisonType);
|
||||
if (newParagraphs is not null && newParagraphs.Count > 0)
|
||||
{
|
||||
ReplaceParagraphInParent(paragraph, newParagraphs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static bool ContainsText(OpenXmlElement element, string searchText, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || string.IsNullOrEmpty(searchText))
|
||||
return false;
|
||||
|
||||
return element.InnerText?.IndexOf(searchText, comparisonType) >= 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Остальные методы остаются без изменений
|
||||
|
||||
/// <summary>
|
||||
/// Находит и возвращает все верхние колонтитулы в документе
|
||||
/// </summary>
|
||||
internal static IEnumerable<OpenXmlElement> GetAllHeaders(WordprocessingDocument document)
|
||||
{
|
||||
if (document?.MainDocumentPart is null)
|
||||
yield break;
|
||||
|
||||
foreach (var headerPart in document.MainDocumentPart.HeaderParts)
|
||||
{
|
||||
if (headerPart?.Header is not null)
|
||||
{
|
||||
yield return headerPart.Header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит и возвращает все нижние колонтитулы в документе
|
||||
/// </summary>
|
||||
internal static IEnumerable<OpenXmlElement> GetAllFooters(WordprocessingDocument document)
|
||||
{
|
||||
if (document?.MainDocumentPart is null)
|
||||
yield break;
|
||||
|
||||
foreach (var footerPart in document.MainDocumentPart.FooterParts)
|
||||
{
|
||||
if (footerPart?.Footer is not null)
|
||||
{
|
||||
yield return footerPart.Footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, содержит ли документ колонтитулы
|
||||
/// </summary>
|
||||
internal static bool HasHeadersOrFooters(WordprocessingDocument document)
|
||||
{
|
||||
if (document?.MainDocumentPart is null)
|
||||
return false;
|
||||
|
||||
return document.MainDocumentPart.HeaderParts.Any() || document.MainDocumentPart.FooterParts.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет параграф на новые параграфы в родительском элементе
|
||||
/// </summary>
|
||||
private static void ReplaceParagraphInParent(Paragraph oldParagraph, List<Paragraph> newParagraphs)
|
||||
{
|
||||
ParagraphReplacer.ReplaceParagraph(oldParagraph, newParagraphs);
|
||||
}
|
||||
}
|
||||
14
QWERTYkez.WordProcessor/IWordReader.cs
Normal file
14
QWERTYkez.WordProcessor/IWordReader.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
public interface IWordReader
|
||||
{
|
||||
long DocumentSize { get; }
|
||||
string? FilePath { get; }
|
||||
bool IsValid { get; }
|
||||
|
||||
ISet<string> FindPlaceholders();
|
||||
|
||||
|
||||
bool TryWrite(string destinationPath, Action<IWordWriter> action);
|
||||
bool TryWrite(Action<IWordWriter> write, Action<IWordReader> read);
|
||||
}
|
||||
29
QWERTYkez.WordProcessor/IWordWriter.cs
Normal file
29
QWERTYkez.WordProcessor/IWordWriter.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
public interface IWordWriter : IWordReader
|
||||
{
|
||||
/// <summary> Добавляет новый параграф с указанным текстом в конец документа. </summary>
|
||||
void AddParagraph(string text, bool preserveFormatting = true);
|
||||
|
||||
|
||||
void ReplaceItem(IDictionary<string, IEnumerable<ReplaceItem>> replacements);
|
||||
void ReplaceItem(IDictionary<string, ReplaceItem> replacements);
|
||||
void ReplaceItem(IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements);
|
||||
void ReplaceItem(IEnumerable<KeyValuePair<string, ReplaceItem>> replacements);
|
||||
void ReplaceItem(string oldValue, IEnumerable<ReplaceItem> newValues);
|
||||
void ReplaceItem(string oldValue, params ReplaceItem[] newValues);
|
||||
void ReplaceString(IDictionary<string, IEnumerable<string>> replacements);
|
||||
void ReplaceString(IDictionary<string, string> replacements);
|
||||
void ReplaceString(IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements);
|
||||
void ReplaceString(IEnumerable<KeyValuePair<string, string>> replacements);
|
||||
void ReplaceString(string oldValue, IEnumerable<string> newValues);
|
||||
void ReplaceString(string oldValue, params string[] newValues);
|
||||
void ReplaceToTable(string oldValue, Action<ITable> buildTable);
|
||||
void ReplaceToText(string oldValue, Action<IText> buildText);
|
||||
void Save();
|
||||
void SaveTo(string path);
|
||||
Task SaveToAsync(string path, CancellationToken cancellationToken = default);
|
||||
bool TrySaveTo(string path, out Exception? error);
|
||||
}
|
||||
106
QWERTYkez.WordProcessor/IeExtension.cs
Normal file
106
QWERTYkez.WordProcessor/IeExtension.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
internal static class IeExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Выполняет указанные действия для первого и последующих элементов последовательности.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип элементов последовательности.</typeparam>
|
||||
/// <param name="items">Последовательность элементов.</param>
|
||||
/// <param name="first">Действие над первым элементом (если есть).</param>
|
||||
/// <param name="next">Действие над каждым последующим элементом, начиная со второго.</param>
|
||||
/// <exception cref="ArgumentNullException">Возникает, если items или любой из делегатов равен null.</exception>
|
||||
public static void ForFirstNext<T>(this IEnumerable<T> items, Action<T> first, Action<T> next)
|
||||
{
|
||||
if (items is null) throw new ArgumentNullException(nameof(items));
|
||||
if (first is null) throw new ArgumentNullException(nameof(first));
|
||||
if (next is null) throw new ArgumentNullException(nameof(next));
|
||||
|
||||
using var enumerator = items.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
return;
|
||||
|
||||
first(enumerator.Current);
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
next(enumerator.Current);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет указанные действия для первого, промежуточных и последнего элементов последовательности.
|
||||
/// Если последовательность содержит только один элемент, то для него вызываются и first, и last.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип элементов последовательности.</typeparam>
|
||||
/// <param name="items">Последовательность элементов.</param>
|
||||
/// <param name="first">Действие над первым элементом.</param>
|
||||
/// <param name="next">Действие над элементами, которые не являются ни первыми, ни последними.</param>
|
||||
/// <param name="last">Действие над последним элементом.</param>
|
||||
/// <exception cref="ArgumentNullException">Возникает, если items или любой из делегатов равен null.</exception>
|
||||
public static void ForFirstNextLast<T>(this IEnumerable<T> items, Action<T> first, Action<T> next, Action<T> last)
|
||||
{
|
||||
if (items is null) throw new ArgumentNullException(nameof(items));
|
||||
if (first is null) throw new ArgumentNullException(nameof(first));
|
||||
if (next is null) throw new ArgumentNullException(nameof(next));
|
||||
if (last is null) throw new ArgumentNullException(nameof(last));
|
||||
|
||||
using var enumerator = items.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
return;
|
||||
|
||||
T firstItem = enumerator.Current;
|
||||
|
||||
// Если только один элемент
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
first(firstItem);
|
||||
last(firstItem);
|
||||
return;
|
||||
}
|
||||
|
||||
// Есть как минимум два элемента
|
||||
first(firstItem);
|
||||
|
||||
T prev = enumerator.Current; // второй элемент
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
next(prev); // предыдущий элемент точно не последний
|
||||
prev = enumerator.Current;
|
||||
}
|
||||
|
||||
last(prev); // последний элемент
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет указанные действия для всех элементов, кроме последнего, и для последнего элемента.
|
||||
/// Если последовательность содержит только один элемент, то вызывается только last.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип элементов последовательности.</typeparam>
|
||||
/// <param name="items">Последовательность элементов.</param>
|
||||
/// <param name="next">Действие над элементами, не являющимися последними.</param>
|
||||
/// <param name="last">Действие над последним элементом.</param>
|
||||
/// <exception cref="ArgumentNullException">Возникает, если items или любой из делегатов равен null.</exception>
|
||||
public static void ForNextLast<T>(this IEnumerable<T> items, Action<T> next, Action<T> last)
|
||||
{
|
||||
if (items is null) throw new ArgumentNullException(nameof(items));
|
||||
if (next is null) throw new ArgumentNullException(nameof(next));
|
||||
if (last is null) throw new ArgumentNullException(nameof(last));
|
||||
|
||||
using var enumerator = items.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
return;
|
||||
|
||||
T prev = enumerator.Current;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
next(prev);
|
||||
prev = enumerator.Current;
|
||||
}
|
||||
|
||||
last(prev);
|
||||
}
|
||||
}
|
||||
494
QWERTYkez.WordProcessor/MultiReplace.cs
Normal file
494
QWERTYkez.WordProcessor/MultiReplace.cs
Normal file
@@ -0,0 +1,494 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
|
||||
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение
|
||||
/// остаётся в текущем параграфе, а последующие создают новые.
|
||||
/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах.
|
||||
/// </summary>
|
||||
internal static class MultiReplaceExt
|
||||
{
|
||||
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (СИГНАТУРЫ НЕИЗМЕННЫ) ----------
|
||||
|
||||
#region Body.Replace с одним ключом
|
||||
|
||||
internal static void Replace(this Body body, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return;
|
||||
var dict = new Dictionary<string, IEnumerable<string>> { { oldValue, newValues } };
|
||||
body.Replace(dict, comparisonType);
|
||||
}
|
||||
|
||||
internal static void Replace(this Body body, string oldValue, IEnumerable<ReplaceItem> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return;
|
||||
var dict = new Dictionary<string, IEnumerable<ReplaceItem>> { { oldValue, newValues } };
|
||||
body.Replace(dict, comparisonType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Body.Replace со словарём массивов
|
||||
|
||||
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || replacements is null) return;
|
||||
var paragraphs = body.Elements<Paragraph>().ToList();
|
||||
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var p = paragraphs[i];
|
||||
if (p?.Parent is null) continue;
|
||||
var newParas = ProcessMultiReplacements(p, replacements, null, comparisonType);
|
||||
if (newParas is not null && newParas.Count > 0)
|
||||
ParagraphReplacer.ReplaceParagraph(p, newParas);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || replacements is null) return;
|
||||
var paragraphs = body.Elements<Paragraph>().ToList();
|
||||
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var p = paragraphs[i];
|
||||
if (p?.Parent is null) continue;
|
||||
var newParas = ProcessMultiReplacements(p, null, replacements, comparisonType);
|
||||
if (newParas is not null && newParas.Count > 0)
|
||||
ParagraphReplacer.ReplaceParagraph(p, newParas);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Paragraph.ReplaceWithMultiple (один ключ)
|
||||
|
||||
internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0)
|
||||
return false;
|
||||
|
||||
var dict = new Dictionary<string, IEnumerable<string>> { { oldValue, newValues } };
|
||||
var newParas = ProcessMultiReplacements(paragraph, dict, null, comparisonType);
|
||||
if (newParas is null || newParas.Count == 0) return false;
|
||||
|
||||
if (paragraph.Parent is not null)
|
||||
{
|
||||
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable<ReplaceItem> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0)
|
||||
return false;
|
||||
|
||||
var dict = new Dictionary<string, IEnumerable<ReplaceItem>> { { oldValue, newValues } };
|
||||
var newParas = ProcessMultiReplacements(paragraph, null, dict, comparisonType);
|
||||
if (newParas is null || newParas.Count == 0) return false;
|
||||
|
||||
if (paragraph.Parent is not null)
|
||||
{
|
||||
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProcessParagraphWithAllReplacements (для обратной совместимости)
|
||||
|
||||
internal static List<Paragraph>? ProcessParagraphWithAllReplacements(
|
||||
Paragraph paragraph,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements,
|
||||
StringComparison comparisonType)
|
||||
{
|
||||
return ProcessMultiReplacements(paragraph, replacements, null, comparisonType);
|
||||
}
|
||||
|
||||
internal static List<Paragraph>? ProcessParagraphWithAllReplacements(
|
||||
Paragraph paragraph,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements,
|
||||
StringComparison comparisonType)
|
||||
{
|
||||
return ProcessMultiReplacements(paragraph, null, replacements, comparisonType);
|
||||
}
|
||||
|
||||
internal static List<Paragraph>? ProcessParagraphWithAllReplacements(
|
||||
Paragraph paragraph,
|
||||
IEnumerable<KeyValuePair<string, string>> replacements,
|
||||
StringComparison comparisonType)
|
||||
{
|
||||
Dictionary<string, IEnumerable<string>> dict = replacements
|
||||
.Where(kvp => !string.IsNullOrEmpty(kvp.Key))
|
||||
.ToDictionary(kvp => kvp.Key, kvp => (IEnumerable<string>)[kvp.Value]);
|
||||
return ProcessMultiReplacements(paragraph, dict, null, comparisonType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// ---------- ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ ----------
|
||||
|
||||
private class MatchDefinition(string key, IEnumerable<ReplaceItem> values)
|
||||
{
|
||||
public string Key { get; } = key;
|
||||
public IEnumerable<ReplaceItem> Values { get; } = values;
|
||||
}
|
||||
|
||||
private class Match
|
||||
{
|
||||
public MatchDefinition Definition { get; set; } = null!;
|
||||
public int Start { get; set; }
|
||||
public int End { get; set; }
|
||||
}
|
||||
|
||||
private class RunSegment(Run run, string text, int start, int end)
|
||||
{
|
||||
public Run Run { get; } = run;
|
||||
public string Text { get; } = text;
|
||||
public int Start { get; } = start;
|
||||
public int End { get; } = end;
|
||||
}
|
||||
|
||||
private class ParagraphStructure(string fullText, List<MultiReplaceExt.RunSegment> segments)
|
||||
{
|
||||
public string FullText { get; } = fullText;
|
||||
public List<RunSegment> Segments { get; } = segments;
|
||||
}
|
||||
|
||||
private static ParagraphStructure AnalyzeParagraphStructure(List<Run> runs)
|
||||
{
|
||||
var segments = new List<RunSegment>();
|
||||
var sb = new StringBuilder();
|
||||
int pos = 0;
|
||||
foreach (var run in runs)
|
||||
{
|
||||
string text = GetRunText(run);
|
||||
if (string.IsNullOrEmpty(text)) continue;
|
||||
segments.Add(new RunSegment(run, text, pos, pos + text.Length));
|
||||
sb.Append(text);
|
||||
pos += text.Length;
|
||||
}
|
||||
return new ParagraphStructure(sb.ToString(), segments);
|
||||
}
|
||||
|
||||
private static string GetRunText(Run run)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var text in run.Elements<Text>())
|
||||
sb.Append(text.Text);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static Paragraph CloneParagraphProperties(Paragraph original)
|
||||
{
|
||||
var newPara = new Paragraph();
|
||||
if (original.ParagraphProperties is not null)
|
||||
newPara.ParagraphProperties = (ParagraphProperties)original.ParagraphProperties.CloneNode(true);
|
||||
return newPara;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Строит параграф, содержащий копии всех элементов исходного параграфа,
|
||||
/// попадающих в текстовый диапазон [start, end).
|
||||
/// </summary>
|
||||
private static Paragraph? BuildRangeParagraph(Paragraph original, ParagraphStructure structure, int start, int end)
|
||||
{
|
||||
if (start >= end) return null;
|
||||
|
||||
var newPara = CloneParagraphProperties(original);
|
||||
|
||||
foreach (var child in original.ChildElements)
|
||||
{
|
||||
if (child is Run run)
|
||||
{
|
||||
var seg = structure.Segments.FirstOrDefault(s => s.Run == run);
|
||||
if (seg is null)
|
||||
{
|
||||
// Run без текста (разрыв, поле) – копируем целиком, т.к. не можем привязать к позиции
|
||||
newPara.AppendChild(run.CloneNode(true));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seg.End <= start || seg.Start >= end)
|
||||
continue;
|
||||
|
||||
if (seg.Start >= start && seg.End <= end)
|
||||
{
|
||||
// Полностью внутри диапазона
|
||||
newPara.AppendChild(run.CloneNode(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Частичное пересечение – обрезаем текст
|
||||
var runClone = (Run)run.CloneNode(true);
|
||||
foreach (var t in runClone.Elements<Text>().ToList())
|
||||
t.Remove();
|
||||
|
||||
int cutStart = Math.Max(start, seg.Start) - seg.Start;
|
||||
int cutEnd = Math.Min(end, seg.End) - seg.Start;
|
||||
string newText = seg.Text.Substring(cutStart, cutEnd - cutStart);
|
||||
runClone.AppendChild(new Text(newText));
|
||||
newPara.AppendChild(runClone);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Не Run – копируем всегда (закладки, поля и т.п.), т.к. не можем определить позицию
|
||||
newPara.AppendChild(child.CloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем пустые Run
|
||||
foreach (var run in newPara.Descendants<Run>().Where(r => !r.HasChildren).ToList())
|
||||
run.Remove();
|
||||
|
||||
if (!newPara.ChildElements.OfType<Run>().Any() && newPara.ParagraphProperties is null)
|
||||
return null;
|
||||
|
||||
return newPara;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Строит параграф, содержащий все элементы исходного параграфа,
|
||||
/// которые находятся строго после указанной текстовой позиции,
|
||||
/// пропуская нетекстовые элементы до первого текстового сегмента.
|
||||
/// </summary>
|
||||
private static Paragraph? BuildAfterParagraph(Paragraph original, ParagraphStructure structure, int position)
|
||||
{
|
||||
if (position >= structure.FullText.Length) return null;
|
||||
|
||||
var newPara = CloneParagraphProperties(original);
|
||||
|
||||
// Находим первый текстовый сегмент, который начинается на или после position
|
||||
var firstTextSeg = structure.Segments.FirstOrDefault(s => s.Start >= position);
|
||||
bool passedFirstText = false;
|
||||
|
||||
foreach (var child in original.ChildElements)
|
||||
{
|
||||
if (child is Run run)
|
||||
{
|
||||
var seg = structure.Segments.FirstOrDefault(s => s.Run == run);
|
||||
if (seg is null)
|
||||
{
|
||||
// Run без текста – добавляем только если уже прошли первый текстовый сегмент
|
||||
if (passedFirstText)
|
||||
newPara.AppendChild(run.CloneNode(true));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seg.Start >= position)
|
||||
{
|
||||
// Полностью после позиции
|
||||
newPara.AppendChild(run.CloneNode(true));
|
||||
if (seg == firstTextSeg)
|
||||
passedFirstText = true;
|
||||
}
|
||||
else if (seg.End > position)
|
||||
{
|
||||
// Частично пересекает – обрезаем текст
|
||||
var runClone = (Run)run.CloneNode(true);
|
||||
foreach (var t in runClone.Elements<Text>().ToList())
|
||||
t.Remove();
|
||||
|
||||
int offset = position - seg.Start;
|
||||
string newText = seg.Text.Substring(offset);
|
||||
runClone.AppendChild(new Text(newText));
|
||||
newPara.AppendChild(runClone);
|
||||
passedFirstText = true;
|
||||
}
|
||||
// seg.End <= position – игнорируем
|
||||
}
|
||||
else
|
||||
{
|
||||
// Не Run – добавляем только если уже прошли первый текстовый сегмент
|
||||
if (passedFirstText)
|
||||
newPara.AppendChild(child.CloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var run in newPara.Descendants<Run>().Where(r => !r.HasChildren).ToList())
|
||||
run.Remove();
|
||||
|
||||
if (!newPara.ChildElements.OfType<Run>().Any() && newPara.ParagraphProperties is null)
|
||||
return null;
|
||||
|
||||
return newPara;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вставляет в параграф новый Run с текстом из ReplaceItem,
|
||||
/// копируя форматирование из сегмента, содержащего указанную позицию.
|
||||
/// Если BreakPage == true, добавляет отдельный Run с разрывом страницы.
|
||||
/// </summary>
|
||||
private static void InsertFormattedRun(Paragraph para, ReplaceItem item, ParagraphStructure structure, int position)
|
||||
{
|
||||
var seg = structure.Segments.FirstOrDefault(s => position >= s.Start && position < s.End);
|
||||
if (seg is null) return;
|
||||
|
||||
var textRun = new Run();
|
||||
if (seg.Run.RunProperties is not null)
|
||||
textRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
|
||||
textRun.AppendChild(new Text(item.Text ?? string.Empty));
|
||||
para.AppendChild(textRun);
|
||||
|
||||
if (item.BreakPage)
|
||||
{
|
||||
var breakRun = new Run(new Break() { Type = BreakValues.Page });
|
||||
if (seg.Run.RunProperties is not null)
|
||||
breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
|
||||
para.AppendChild(breakRun);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет содержимое одного параграфа в другой (клонируя элементы).
|
||||
/// </summary>
|
||||
private static void MergeParagraph(Paragraph target, Paragraph source)
|
||||
{
|
||||
foreach (var child in source.ChildElements)
|
||||
target.AppendChild(child.CloneNode(true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Основной алгоритм: обрабатывает все вхождения всех ключей из предоставленных словарей.
|
||||
/// </summary>
|
||||
private static List<Paragraph>? ProcessMultiReplacements(
|
||||
Paragraph original,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
|
||||
StringComparison comparisonType)
|
||||
{
|
||||
// 1. Собираем определения замен
|
||||
var definitions = new List<MatchDefinition>();
|
||||
if (stringReplacements is not null)
|
||||
{
|
||||
foreach (var kvp in stringReplacements)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
|
||||
definitions.Add(new MatchDefinition(kvp.Key, [.. kvp.Value.Select(v => new ReplaceItem(v, false))]));
|
||||
}
|
||||
}
|
||||
if (itemReplacements is not null)
|
||||
{
|
||||
foreach (var kvp in itemReplacements)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
|
||||
definitions.Add(new MatchDefinition(kvp.Key, kvp.Value));
|
||||
}
|
||||
}
|
||||
|
||||
if (definitions.Count == 0) return null;
|
||||
|
||||
// 2. Анализ структуры параграфа
|
||||
var runs = original.Descendants<Run>().ToList();
|
||||
if (runs.Count == 0) return null;
|
||||
var structure = AnalyzeParagraphStructure(runs);
|
||||
string fullText = structure.FullText;
|
||||
if (fullText.Length == 0) return null;
|
||||
|
||||
// 3. Находим все вхождения всех ключей
|
||||
var matches = new List<Match>();
|
||||
foreach (var def in definitions)
|
||||
{
|
||||
int pos = 0;
|
||||
while ((pos = fullText.IndexOf(def.Key, pos, comparisonType)) != -1)
|
||||
{
|
||||
matches.Add(new Match
|
||||
{
|
||||
Definition = def,
|
||||
Start = pos,
|
||||
End = pos + def.Key.Length
|
||||
});
|
||||
pos += def.Key.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.Count == 0) return null;
|
||||
|
||||
// 4. Сортируем по позиции
|
||||
matches.Sort((a, b) => a.Start.CompareTo(b.Start));
|
||||
|
||||
// 5. Построение результирующих параграфов
|
||||
var resultParas = new List<Paragraph>();
|
||||
Paragraph? currentPara = null;
|
||||
int currentPos = 0;
|
||||
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
var match = matches[i];
|
||||
|
||||
// Текст перед текущим совпадением (от currentPos до match.Start)
|
||||
if (currentPos < match.Start)
|
||||
{
|
||||
var textPart = BuildRangeParagraph(original, structure, currentPos, match.Start);
|
||||
if (textPart is not null)
|
||||
{
|
||||
if (currentPara is null)
|
||||
{
|
||||
currentPara = textPart;
|
||||
resultParas.Add(currentPara);
|
||||
}
|
||||
else
|
||||
{
|
||||
MergeParagraph(currentPara, textPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем значения замены для этого совпадения
|
||||
var values = match.Definition.Values;
|
||||
|
||||
// Первое значение – в текущий параграф (или создаём новый)
|
||||
if (currentPara is null)
|
||||
{
|
||||
currentPara = CloneParagraphProperties(original);
|
||||
resultParas.Add(currentPara);
|
||||
}
|
||||
|
||||
values.ForFirstNext(first =>
|
||||
{
|
||||
InsertFormattedRun(currentPara, first, structure, match.Start);
|
||||
},
|
||||
next =>
|
||||
{
|
||||
// Остальные значения – в новые параграфы
|
||||
var newPara = CloneParagraphProperties(original);
|
||||
InsertFormattedRun(newPara, next, structure, match.Start);
|
||||
resultParas.Add(newPara);
|
||||
currentPara = newPara; // теперь текущий параграф – последний созданный
|
||||
});
|
||||
|
||||
currentPos = match.End;
|
||||
}
|
||||
|
||||
// Текст после последнего совпадения – используем BuildAfterParagraph, чтобы пропустить лишние разрывы
|
||||
if (currentPos < fullText.Length)
|
||||
{
|
||||
var textPart = BuildAfterParagraph(original, structure, currentPos);
|
||||
if (textPart is not null)
|
||||
{
|
||||
if (currentPara is null)
|
||||
{
|
||||
currentPara = textPart;
|
||||
resultParas.Add(currentPara);
|
||||
}
|
||||
else
|
||||
{
|
||||
MergeParagraph(currentPara, textPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем пустые параграфы
|
||||
for (int i = resultParas.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!resultParas[i].ChildElements.OfType<Run>().Any() && resultParas[i].ParagraphProperties is null)
|
||||
resultParas.RemoveAt(i);
|
||||
}
|
||||
|
||||
return resultParas.Count > 0 ? resultParas : null;
|
||||
}
|
||||
}
|
||||
135
QWERTYkez.WordProcessor/NormalizedSet.cs
Normal file
135
QWERTYkez.WordProcessor/NormalizedSet.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Множество строк, которое автоматически приводит все добавляемые элементы
|
||||
/// к верхнему регистру и удаляет диакритические знаки (например, 'ё' -> 'Е').
|
||||
/// Реализует ISet<string>, поэтому может использоваться там, где ожидается этот интерфейс.
|
||||
/// </summary>
|
||||
public class NormalizedSet : ISet<string>
|
||||
{
|
||||
private readonly HashSet<string> _inner;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт пустое нормализованное множество.
|
||||
/// </summary>
|
||||
public NormalizedSet()
|
||||
{
|
||||
_inner = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт нормализованное множество, заполненное элементами из указанной коллекции.
|
||||
/// </summary>
|
||||
/// <param name="collection">Коллекция, элементы которой будут нормализованы и добавлены.</param>
|
||||
public NormalizedSet(IEnumerable<string> collection)
|
||||
{
|
||||
_inner = [.. collection.Select(Normalize)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Нормализует строку: верхний регистр и удаление диакритики.
|
||||
/// </summary>
|
||||
private static string Normalize(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return s;
|
||||
|
||||
var normalized = s.Normalize(NormalizationForm.FormD);
|
||||
var sb = new StringBuilder();
|
||||
foreach (char c in normalized)
|
||||
{
|
||||
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString().Normalize(NormalizationForm.FormC).ToUpperInvariant();
|
||||
}
|
||||
|
||||
// ---------- Реализация ISet<string> ----------
|
||||
|
||||
public bool Add(string item) => _inner.Add(Normalize(item));
|
||||
|
||||
void ICollection<string>.Add(string item) => Add(item);
|
||||
|
||||
public void UnionWith(IEnumerable<string> other)
|
||||
{
|
||||
foreach (var item in other)
|
||||
Add(item);
|
||||
}
|
||||
|
||||
public void IntersectWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.IntersectWith(normalizedOther);
|
||||
}
|
||||
|
||||
public void ExceptWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.ExceptWith(normalizedOther);
|
||||
}
|
||||
|
||||
public void SymmetricExceptWith(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
_inner.SymmetricExceptWith(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsSubsetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsSupersetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsSupersetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsProperSupersetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsProperSupersetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool IsProperSubsetOf(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.IsProperSubsetOf(normalizedOther);
|
||||
}
|
||||
|
||||
public bool Overlaps(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.Overlaps(normalizedOther);
|
||||
}
|
||||
|
||||
public bool SetEquals(IEnumerable<string> other)
|
||||
{
|
||||
var normalizedOther = new HashSet<string>(other.Select(Normalize));
|
||||
return _inner.SetEquals(normalizedOther);
|
||||
}
|
||||
|
||||
// ---------- Реализация ICollection<string> ----------
|
||||
|
||||
public int Count => _inner.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Clear() => _inner.Clear();
|
||||
|
||||
public bool Contains(string item) => _inner.Contains(Normalize(item));
|
||||
|
||||
public void CopyTo(string[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
|
||||
|
||||
public bool Remove(string item) => _inner.Remove(Normalize(item));
|
||||
|
||||
// ---------- Реализация IEnumerable<string> и IEnumerable ----------
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => _inner.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
47
QWERTYkez.WordProcessor/ParagraphReplacer.cs
Normal file
47
QWERTYkez.WordProcessor/ParagraphReplacer.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Вспомогательный класс для замены параграфов в различных контейнерах
|
||||
/// </summary>
|
||||
internal static class ParagraphReplacer
|
||||
{
|
||||
/// <summary>
|
||||
/// Заменяет параграф на новые параграфы в родительском элементе
|
||||
/// </summary>
|
||||
/// <param name="oldParagraph">Старый параграф для замены</param>
|
||||
/// <param name="newParagraphs">Новые параграфы</param>
|
||||
internal static void ReplaceParagraph(Paragraph oldParagraph, List<Paragraph> newParagraphs)
|
||||
{
|
||||
if (oldParagraph is null || newParagraphs is null || newParagraphs.Count == 0)
|
||||
return;
|
||||
|
||||
var parent = oldParagraph.Parent;
|
||||
if (parent is null)
|
||||
return;
|
||||
|
||||
int paraIndex = -1;
|
||||
var children = parent.ChildElements;
|
||||
|
||||
// Находим индекс параграфа
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
if (children[i] == oldParagraph)
|
||||
{
|
||||
paraIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (paraIndex == -1)
|
||||
return;
|
||||
|
||||
// Удаляем старый параграф
|
||||
parent.RemoveChild(oldParagraph);
|
||||
|
||||
// Вставляем новые параграфы
|
||||
for (int i = 0; i < newParagraphs.Count; i++)
|
||||
{
|
||||
parent.InsertAt(newParagraphs[i], paraIndex + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
175
QWERTYkez.WordProcessor/PlaceholderFinder.cs
Normal file
175
QWERTYkez.WordProcessor/PlaceholderFinder.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
internal static class PlaceholderFinder
|
||||
{
|
||||
public static ISet<string> FindInDocument(WordprocessingDocument doc, Body body)
|
||||
{
|
||||
ISet<string> result = new NormalizedSet();
|
||||
|
||||
// 1. Основной текст
|
||||
FindInParagraphs(body.Elements<Paragraph>(), result);
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
foreach (var table in body.Descendants<Table>())
|
||||
{
|
||||
FindInParagraphs(table.Descendants<Paragraph>(), result);
|
||||
}
|
||||
|
||||
// 3. Колонтитулы
|
||||
FindInHeadersAndFooters(doc, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void FindInParagraphs(IEnumerable<Paragraph> paragraphs, ISet<string> result)
|
||||
{
|
||||
// Локальная коллекция для каждого вызова - оптимизация для уменьшения аллокаций
|
||||
List<string>? tempList = null;
|
||||
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
var text = GetParagraphText(paragraph);
|
||||
if (string.IsNullOrEmpty(text)) continue;
|
||||
|
||||
// Откладываем создание списка до первого найденного плейсхолдера
|
||||
bool hasPlaceholders = FindPlaceholdersInText(text, ref tempList);
|
||||
|
||||
if (hasPlaceholders && tempList is not null)
|
||||
{
|
||||
// Добавляем найденные плейсхолдеры с учетом регистра
|
||||
foreach (var placeholder in tempList)
|
||||
{
|
||||
result.Add(placeholder);
|
||||
}
|
||||
tempList.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool FindPlaceholdersInText(string text, ref List<string>? output)
|
||||
{
|
||||
fixed (char* pText = text)
|
||||
{
|
||||
char* start = pText;
|
||||
char* end = pText + text.Length;
|
||||
bool foundAny = false;
|
||||
|
||||
while (start < end)
|
||||
{
|
||||
// Ищем начало плейсхолдера
|
||||
char* dollarStart = null;
|
||||
while (start < end)
|
||||
{
|
||||
if (*start == '$')
|
||||
{
|
||||
dollarStart = start;
|
||||
start++;
|
||||
break;
|
||||
}
|
||||
start++;
|
||||
}
|
||||
|
||||
if (dollarStart is null || start >= end) break;
|
||||
|
||||
// Ищем конец плейсхолдера
|
||||
char* dollarEnd = null;
|
||||
char* contentStart = start; // Начало содержимого (после первого $)
|
||||
|
||||
while (start < end)
|
||||
{
|
||||
if (*start == '$')
|
||||
{
|
||||
dollarEnd = start;
|
||||
start++;
|
||||
break;
|
||||
}
|
||||
start++;
|
||||
}
|
||||
|
||||
if (dollarEnd is null) break;
|
||||
|
||||
// Извлекаем содержимое между долларами
|
||||
int contentLength = (int)(dollarEnd - contentStart);
|
||||
if (contentLength > 0) // Игнорируем пустые "$$"
|
||||
{
|
||||
foundAny = true;
|
||||
output ??= [];
|
||||
|
||||
string content = new(contentStart, 0, contentLength);
|
||||
output.Add(content);
|
||||
}
|
||||
}
|
||||
|
||||
return foundAny;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FindInHeadersAndFooters(WordprocessingDocument doc, ISet<string> result)
|
||||
{
|
||||
if (doc.MainDocumentPart is null) return;
|
||||
|
||||
// Верхние колонтитулы
|
||||
foreach (var headerPart in doc.MainDocumentPart.HeaderParts)
|
||||
{
|
||||
if (headerPart?.Header is not null)
|
||||
{
|
||||
FindInParagraphs(headerPart.Header.Descendants<Paragraph>(), result);
|
||||
}
|
||||
}
|
||||
|
||||
// Нижние колонтитулы
|
||||
foreach (var footerPart in doc.MainDocumentPart.FooterParts)
|
||||
{
|
||||
if (footerPart?.Footer is not null)
|
||||
{
|
||||
FindInParagraphs(footerPart.Footer.Descendants<Paragraph>(), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetParagraphText(Paragraph paragraph)
|
||||
{
|
||||
if (paragraph is null) return string.Empty;
|
||||
|
||||
// Используем Span для минимальных аллокаций
|
||||
var texts = paragraph.Descendants<Text>();
|
||||
|
||||
// Быстрая проверка: если всего один WordText элемент
|
||||
if (texts is ICollection<Text> collection && collection.Count == 1)
|
||||
{
|
||||
foreach (var text in collection)
|
||||
{
|
||||
return text.Text ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// Для нескольких WordText элементов
|
||||
int totalLength = 0;
|
||||
|
||||
// Первый проход: подсчет общей длины
|
||||
foreach (var text in texts)
|
||||
{
|
||||
if (text.Text is not null)
|
||||
{
|
||||
totalLength += text.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalLength == 0) return string.Empty;
|
||||
|
||||
// Второй проход: копирование
|
||||
var chars = new char[totalLength];
|
||||
int position = 0;
|
||||
|
||||
foreach (var text in texts)
|
||||
{
|
||||
if (text.Text is not null)
|
||||
{
|
||||
text.Text.CopyTo(0, chars, position, text.Text.Length);
|
||||
position += text.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
15
QWERTYkez.WordProcessor/QWERTYkez.WordProcessor.csproj
Normal file
15
QWERTYkez.WordProcessor/QWERTYkez.WordProcessor.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="IsExternalInit" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
24
QWERTYkez.WordProcessor/ReplaceItem.cs
Normal file
24
QWERTYkez.WordProcessor/ReplaceItem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
public readonly struct ReplaceItem
|
||||
{
|
||||
public ReplaceItem() { }
|
||||
public ReplaceItem(string text)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
public ReplaceItem(string text, bool breakPage)
|
||||
{
|
||||
Text = text;
|
||||
BreakPage = breakPage;
|
||||
}
|
||||
|
||||
public string Text { get; init; } = string.Empty;
|
||||
public bool BreakPage { get; init; } = false;
|
||||
|
||||
|
||||
// Неявное преобразование из ReplaceItem в string
|
||||
//public static implicit operator string(ReplaceItem item) => item.Text;
|
||||
// Явное преобразование из string в ReplaceItem
|
||||
public static explicit operator ReplaceItem(string text) => new() { Text = text };
|
||||
}
|
||||
220
QWERTYkez.WordProcessor/ReplaceToTableExt.cs
Normal file
220
QWERTYkez.WordProcessor/ReplaceToTableExt.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
internal static class ReplaceToTableExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Заменяет параграф на таблицу, созданную с помощью TableBuilder
|
||||
/// </summary>
|
||||
internal static void ReplaceWithTableBuilder(Paragraph paragraph, Action<TableBuilder> buildTable, FontProps? baseFont)
|
||||
{
|
||||
if (paragraph is null || paragraph.Parent is null || buildTable is null)
|
||||
return;
|
||||
|
||||
var parent = paragraph.Parent;
|
||||
|
||||
// Создаем TableBuilder с базовым шрифтом
|
||||
var builder = TableBuilder.Create(baseFont);
|
||||
buildTable(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет все параграфы, содержащие указанный текст, на таблицы
|
||||
/// </summary>
|
||||
internal static void ReplaceParagraphsContainingTextToTable(Body body,
|
||||
string oldValue, Action<TableBuilder> buildTable)
|
||||
{
|
||||
if (body is null || string.IsNullOrEmpty(oldValue) || buildTable is null)
|
||||
return;
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>().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) ||
|
||||
paraText.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
|
||||
// Извлекаем свойства шрифта для передачи в TableBuilder
|
||||
var fontProps = ExtractFontPropsFromParagraph(paragraph, oldValue);
|
||||
|
||||
// Заменяем параграф на таблицу
|
||||
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();
|
||||
}
|
||||
}
|
||||
231
QWERTYkez.WordProcessor/ReplaceToTextExt.cs
Normal file
231
QWERTYkez.WordProcessor/ReplaceToTextExt.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Вспомогательный класс для замены параграфов, содержащих указанный текст, на текст, построенный с помощью TextBuilder.
|
||||
/// </summary>
|
||||
internal static class ReplaceToTextExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Заменяет все параграфы, содержащие указанный текст, на текст, построенный с помощью TextBuilder.
|
||||
/// </summary>
|
||||
internal static void ReplaceParagraphsContainingTextToText(
|
||||
Body body,
|
||||
string oldValue,
|
||||
Action<IText> buildText)
|
||||
{
|
||||
if (body is null || string.IsNullOrEmpty(oldValue) || buildText is null)
|
||||
return;
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>().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) ||
|
||||
paraText.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
|
||||
// Извлекаем свойства шрифта для передачи в TextBuilder
|
||||
var fontProps = ExtractFontPropsFromParagraph(paragraph, oldValue);
|
||||
// Извлекаем свойства параграфа для наследования
|
||||
var paragraphProps = paragraph.ParagraphProperties?.CloneNode(true) as ParagraphProperties;
|
||||
|
||||
// Заменяем параграф на текст с наследованием форматирования
|
||||
ReplaceWithTextBuilder(paragraph, buildText, fontProps, paragraphProps);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceWithTextBuilder(
|
||||
Paragraph paragraph,
|
||||
Action<IText> buildText,
|
||||
FontProps? baseFont,
|
||||
ParagraphProperties? baseParagraphProps)
|
||||
{
|
||||
if (paragraph is null || paragraph.Parent is null || buildText is null)
|
||||
return;
|
||||
|
||||
var parent = paragraph.Parent;
|
||||
|
||||
// Создаем TextBuilder с базовым шрифтом и свойствами параграфа
|
||||
var builder = TextBuilder.Create(baseFont, baseParagraphProps);
|
||||
buildText(builder);
|
||||
var newParagraphs = 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);
|
||||
|
||||
for (int i = 0; i < newParagraphs.Count; i++)
|
||||
{
|
||||
parent.InsertAt(newParagraphs[i], paraIndex + i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
374
QWERTYkez.WordProcessor/SimplyReplace.cs
Normal file
374
QWERTYkez.WordProcessor/SimplyReplace.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
internal static class SimplyReplaceExt
|
||||
{
|
||||
private readonly struct TextNodeInfo(Text text, int startIndex, int length)
|
||||
{
|
||||
internal readonly Text Text = text;
|
||||
internal readonly int StartIndex = startIndex;
|
||||
internal readonly int Length = length;
|
||||
}
|
||||
|
||||
private sealed class ParagraphStructure(string fullText, SimplyReplaceExt.TextNodeInfo[] textNodes)
|
||||
{
|
||||
internal readonly string FullText = fullText;
|
||||
internal readonly TextNodeInfo[] TextNodes = textNodes;
|
||||
|
||||
internal int FindFirstNodeIndexAtPosition(int position)
|
||||
{
|
||||
if (position < 0 || position >= FullText.Length)
|
||||
return -1;
|
||||
|
||||
int left = 0;
|
||||
int right = TextNodes.Length - 1;
|
||||
int result = -1;
|
||||
|
||||
while (left <= right)
|
||||
{
|
||||
int mid = left + ((right - left) >> 1);
|
||||
if (TextNodes[mid].StartIndex <= position)
|
||||
{
|
||||
result = mid;
|
||||
left = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result >= 0 && TextNodes[result].StartIndex + TextNodes[result].Length > position
|
||||
? result : -1;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Replace(this Body body, string oldValue, string newValue, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || string.IsNullOrEmpty(oldValue)) return;
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>();
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
paragraph?.SimpleReplace(oldValue, newValue, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || replacements is null || replacements.Count() == 0)
|
||||
return;
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>();
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
paragraph?.Replace(replacements, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, ReplaceItem>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (body is null || replacements is null || replacements.Count() == 0)
|
||||
return;
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>();
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
paragraph?.Replace(replacements, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage = false)
|
||||
{
|
||||
if (paragraph is null || string.IsNullOrEmpty(oldValue))
|
||||
return false;
|
||||
|
||||
var paragraphText = paragraph.InnerText;
|
||||
if (string.IsNullOrEmpty(paragraphText))
|
||||
return false;
|
||||
|
||||
int matchIndex = paragraphText.IndexOf(oldValue, comparisonType);
|
||||
if (matchIndex == -1)
|
||||
return false;
|
||||
|
||||
var runs = paragraph.Elements<Run>();
|
||||
if (!runs.Any())
|
||||
return false;
|
||||
|
||||
var structure = AnalyzeParagraphStructure(runs);
|
||||
if (structure.FullText.Length == 0)
|
||||
return false;
|
||||
|
||||
int matchEnd = matchIndex + oldValue.Length;
|
||||
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
|
||||
if (nodesToReplace.Count == 0)
|
||||
return false;
|
||||
|
||||
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, breakPage);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void Replace(this Paragraph paragraph, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (paragraph is null || replacements is null || replacements.Count() == 0)
|
||||
return;
|
||||
|
||||
var runs = paragraph.Elements<Run>().ToList();
|
||||
if (runs.Count == 0)
|
||||
return;
|
||||
|
||||
var structure = AnalyzeParagraphStructure(runs);
|
||||
if (structure.FullText.Length == 0)
|
||||
return;
|
||||
|
||||
// Используем List с предопределенной емкостью
|
||||
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
|
||||
|
||||
// Сначала находим все вхождения
|
||||
var fullText = structure.FullText;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key))
|
||||
continue;
|
||||
|
||||
int pos = 0;
|
||||
while ((pos = fullText.IndexOf(kvp.Key, pos, comparisonType)) != -1)
|
||||
{
|
||||
replacementsInParagraph.Add(new ReplacementInfo
|
||||
{
|
||||
OldValue = kvp.Key,
|
||||
NewValue = kvp.Value ?? string.Empty,
|
||||
Index = pos
|
||||
});
|
||||
pos += kvp.Key.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacementsInParagraph.Count == 0)
|
||||
return;
|
||||
|
||||
// Сортируем по убыванию позиции
|
||||
replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index));
|
||||
|
||||
// Выполняем замены
|
||||
for (int i = 0; i < replacementsInParagraph.Count; i++)
|
||||
{
|
||||
var replacement = replacementsInParagraph[i];
|
||||
int matchIndex = replacement.Index;
|
||||
int matchEnd = matchIndex + replacement.OldValue.Length;
|
||||
|
||||
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
|
||||
if (nodesToReplace.Count > 0)
|
||||
{
|
||||
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Replace(this Paragraph paragraph, IEnumerable<KeyValuePair<string, ReplaceItem>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (paragraph is null || replacements is null || replacements.Count() == 0)
|
||||
return;
|
||||
|
||||
var runs = paragraph.Elements<Run>().ToList();
|
||||
if (runs.Count == 0)
|
||||
return;
|
||||
|
||||
var structure = AnalyzeParagraphStructure(runs);
|
||||
if (structure.FullText.Length == 0)
|
||||
return;
|
||||
|
||||
// Используем List с предопределенной емкостью
|
||||
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
|
||||
|
||||
// Сначала находим все вхождения
|
||||
var fullText = structure.FullText;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key))
|
||||
continue;
|
||||
|
||||
int pos = 0;
|
||||
while ((pos = fullText.IndexOf(kvp.Key, pos, comparisonType)) != -1)
|
||||
{
|
||||
replacementsInParagraph.Add(new ReplacementInfo
|
||||
{
|
||||
OldValue = kvp.Key,
|
||||
NewValue = kvp.Value.Text ?? string.Empty,
|
||||
BreakPage = kvp.Value.BreakPage,
|
||||
Index = pos
|
||||
});
|
||||
pos += kvp.Key.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacementsInParagraph.Count == 0)
|
||||
return;
|
||||
|
||||
// Сортируем по убыванию позиции
|
||||
replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index));
|
||||
|
||||
// Выполняем замены
|
||||
for (int i = 0; i < replacementsInParagraph.Count; i++)
|
||||
{
|
||||
var replacement = replacementsInParagraph[i];
|
||||
int matchIndex = replacement.Index;
|
||||
int matchEnd = matchIndex + replacement.OldValue.Length;
|
||||
|
||||
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
|
||||
if (nodesToReplace.Count > 0)
|
||||
{
|
||||
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ReplacementInfo
|
||||
{
|
||||
internal string OldValue { get; set; } = null!;
|
||||
internal string NewValue { get; set; } = null!;
|
||||
internal int Index { get; set; }
|
||||
internal bool BreakPage { get; set; }
|
||||
}
|
||||
|
||||
private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable<Run> runs)
|
||||
{
|
||||
var textNodesList = new List<TextNodeInfo>(32);
|
||||
var sb = new StringBuilder(256);
|
||||
int currentIndex = 0;
|
||||
|
||||
foreach (var run in runs)
|
||||
{
|
||||
var texts = run.Elements<Text>();
|
||||
foreach (var text in texts)
|
||||
{
|
||||
var textValue = text.Text;
|
||||
if (string.IsNullOrEmpty(textValue))
|
||||
continue;
|
||||
|
||||
textNodesList.Add(new TextNodeInfo(
|
||||
text,
|
||||
currentIndex,
|
||||
textValue.Length
|
||||
));
|
||||
|
||||
sb.Append(textValue);
|
||||
currentIndex += textValue.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return new ParagraphStructure(sb.ToString(), [.. textNodesList]);
|
||||
}
|
||||
|
||||
private static List<TextNodeInfo> FindNodesToReplace(
|
||||
ParagraphStructure structure,
|
||||
int matchStart,
|
||||
int matchEnd)
|
||||
{
|
||||
var result = new List<TextNodeInfo>(4);
|
||||
int firstNodeIndex = structure.FindFirstNodeIndexAtPosition(matchStart);
|
||||
|
||||
if (firstNodeIndex == -1)
|
||||
return result;
|
||||
|
||||
var textNodes = structure.TextNodes;
|
||||
for (int i = firstNodeIndex; i < textNodes.Length; i++)
|
||||
{
|
||||
var node = textNodes[i];
|
||||
if (node.StartIndex >= matchEnd)
|
||||
break;
|
||||
|
||||
if (node.StartIndex + node.Length > matchStart)
|
||||
{
|
||||
result.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ExecuteReplacement(
|
||||
List<TextNodeInfo> nodesToReplace,
|
||||
int matchStart,
|
||||
int matchEnd,
|
||||
string newValue,
|
||||
bool breakPage = false)
|
||||
{
|
||||
if (nodesToReplace.Count == 0) return;
|
||||
|
||||
var firstNode = nodesToReplace[0];
|
||||
string oldText = firstNode.Text.Text ?? string.Empty;
|
||||
int startInFirstNode = matchStart - firstNode.StartIndex;
|
||||
int charsToReplaceInFirstNode = Math.Min(
|
||||
oldText.Length - startInFirstNode,
|
||||
matchEnd - matchStart
|
||||
);
|
||||
|
||||
string processedNewValue = ReplaceSpacesWithNonBreaking(newValue);
|
||||
firstNode.Text.Text = ReplaceSubstringOptimized(
|
||||
oldText,
|
||||
startInFirstNode,
|
||||
charsToReplaceInFirstNode,
|
||||
processedNewValue
|
||||
);
|
||||
|
||||
// Очищаем остальные текстовые ноды
|
||||
for (int i = 1; i < nodesToReplace.Count; i++)
|
||||
{
|
||||
nodesToReplace[i].Text.Text = string.Empty;
|
||||
}
|
||||
|
||||
if (breakPage)
|
||||
{
|
||||
if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para)
|
||||
{
|
||||
var breakRun = new Run(new Break() { Type = BreakValues.Page });
|
||||
if (run.RunProperties is not null)
|
||||
breakRun.RunProperties = (RunProperties)run.RunProperties.CloneNode(true);
|
||||
para.AppendChild(breakRun);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe string ReplaceSpacesWithNonBreaking(string input)
|
||||
{
|
||||
if (!input.Contains(' '))
|
||||
return input;
|
||||
|
||||
fixed (char* pInput = input)
|
||||
{
|
||||
char* resultPtr = stackalloc char[input.Length];
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
resultPtr[i] = pInput[i] == ' ' ? '\u00A0' : pInput[i];
|
||||
}
|
||||
return new string(resultPtr, 0, input.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReplaceSubstringOptimized(string original, int start, int length, string replacement)
|
||||
{
|
||||
if (string.IsNullOrEmpty(original))
|
||||
return replacement ?? string.Empty;
|
||||
|
||||
if (start < 0 || start >= original.Length || length <= 0)
|
||||
return original;
|
||||
|
||||
if (start == 0 && length == original.Length)
|
||||
return replacement;
|
||||
|
||||
int end = Math.Min(start + length, original.Length);
|
||||
|
||||
// Оптимизированная конкатенация
|
||||
var sb = new StringBuilder(original.Length - length + replacement.Length);
|
||||
if (start > 0)
|
||||
{
|
||||
sb.Append(original, 0, start);
|
||||
}
|
||||
sb.Append(replacement);
|
||||
if (end < original.Length)
|
||||
{
|
||||
sb.Append(original, end, original.Length - end);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
307
QWERTYkez.WordProcessor/TableTextProcessor.cs
Normal file
307
QWERTYkez.WordProcessor/TableTextProcessor.cs
Normal file
@@ -0,0 +1,307 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик текста в таблицах DOCX документов
|
||||
/// </summary>
|
||||
internal static class TableTextProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Заменяет текст во всех таблицах указанного элемента
|
||||
/// </summary>
|
||||
internal static void ReplaceInTables(OpenXmlElement element, string oldValue, string newValue, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || string.IsNullOrEmpty(oldValue) || newValue is null)
|
||||
return;
|
||||
|
||||
var tables = GetAllTables(element).ToList();
|
||||
if (tables.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int totalTableMatches = 0;
|
||||
int totalTableParagraphs = 0;
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var paragraphs = GetTableParagraphs(table).ToList();
|
||||
totalTableParagraphs += paragraphs.Count;
|
||||
foreach (var para in paragraphs)
|
||||
{
|
||||
if (para?.InnerText?.IndexOf(oldValue, comparisonType) >= 0)
|
||||
{
|
||||
totalTableMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalTableMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Looking for '{oldValue}' in {tables.Count} tables");
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Total paragraphs in tables: {totalTableParagraphs}");
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Tables with matches: {totalTableMatches}");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
ReplaceInTable(table, oldValue, newValue, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет текст во всех таблицах указанного элемента
|
||||
/// </summary>
|
||||
internal static void ReplaceInTables(OpenXmlElement element, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || string.IsNullOrEmpty(oldValue) || newValues is null)
|
||||
return;
|
||||
|
||||
var tables = GetAllTables(element).ToList();
|
||||
if (tables.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int totalTableMatches = 0;
|
||||
int totalTableParagraphs = 0;
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var paragraphs = GetTableParagraphs(table).ToList();
|
||||
totalTableParagraphs += paragraphs.Count;
|
||||
foreach (var para in paragraphs)
|
||||
{
|
||||
if (para?.InnerText?.IndexOf(oldValue, comparisonType) >= 0)
|
||||
{
|
||||
totalTableMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalTableMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Looking for '{oldValue}' in {tables.Count} tables");
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Total paragraphs in tables: {totalTableParagraphs}");
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables] Tables with matches: {totalTableMatches}");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
ReplaceInTable(table, oldValue, newValues, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену во всех таблицах
|
||||
/// </summary>
|
||||
internal static void ReplaceInTables(OpenXmlElement element, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (element is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var tables = GetAllTables(element).ToList();
|
||||
if (tables.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
int tablesWithMatches = 0;
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var paragraphs = GetTableParagraphs(table).ToList();
|
||||
foreach (var para in paragraphs)
|
||||
{
|
||||
var paraText = para.InnerText;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && paraText.IndexOf(kvp.Key, comparisonType) >= 0)
|
||||
{
|
||||
tablesWithMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tablesWithMatches > 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tablesWithMatches > 0)
|
||||
{
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables(dict)] START with {replacements.Count()} items");
|
||||
Debug.WriteLine($"[DEBUG] [TableTextProcessor.ReplaceInTables(dict)] Processing {tables.Count} tables, {tablesWithMatches} have matches");
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
ReplaceInTable(table, replacements, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет текст в конкретной таблице
|
||||
/// </summary>
|
||||
private static void ReplaceInTable(Table table, string oldValue, string newValue, StringComparison comparisonType)
|
||||
{
|
||||
if (table is null)
|
||||
return;
|
||||
|
||||
var paragraphs = GetTableParagraphs(table).ToList();
|
||||
if (paragraphs.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
paragraph?.SimpleReplace(oldValue, newValue, comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет текст в конкретной таблице
|
||||
/// </summary>
|
||||
private static void ReplaceInTable(Table table, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
|
||||
{
|
||||
if (table is null)
|
||||
return;
|
||||
|
||||
var paragraphs = GetTableParagraphs(table).ToList();
|
||||
if (paragraphs.Count == 0)
|
||||
return;
|
||||
|
||||
if (newValues.Count() == 1)
|
||||
{
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
paragraph?.SimpleReplace(oldValue, newValues.First(), comparisonType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var paragraph = paragraphs[i];
|
||||
if (paragraph is not null && paragraph.InnerText.IndexOf(oldValue, comparisonType) >= 0)
|
||||
{
|
||||
paragraph.ReplaceWithMultiple(oldValue, newValues, comparisonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет словарную замену в конкретной таблице
|
||||
/// </summary>
|
||||
private static void ReplaceInTable(Table table, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
|
||||
{
|
||||
if (table is null || replacements is null || !replacements.Any())
|
||||
return;
|
||||
|
||||
var paragraphs = GetTableParagraphs(table).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;
|
||||
bool hasMatch = false;
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key) && paraText.IndexOf(kvp.Key, comparisonType) >= 0)
|
||||
{
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMatch)
|
||||
continue;
|
||||
|
||||
var newParagraphs = MultiReplaceExt.ProcessParagraphWithAllReplacements(paragraph, replacements, comparisonType);
|
||||
if (newParagraphs is not null && newParagraphs.Count > 0)
|
||||
{
|
||||
ReplaceParagraphsInTableCell(paragraph, newParagraphs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит и возвращает все таблицы в указанном элементе
|
||||
/// </summary>
|
||||
internal static IEnumerable<Table> GetAllTables(OpenXmlElement element)
|
||||
{
|
||||
if (element is null)
|
||||
yield break;
|
||||
|
||||
// Используем стек вместо очереди для рекурсивного поиска
|
||||
var stack = new Stack<OpenXmlElement>();
|
||||
stack.Push(element);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var current = stack.Pop();
|
||||
|
||||
if (current is Table table)
|
||||
{
|
||||
yield return table;
|
||||
// Не ищем таблицы внутри таблиц (вложенные таблицы уже будут обработаны как дочерние элементы)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Добавляем дочерние элементы в обратном порядке для сохранения порядка
|
||||
var children = current.ChildElements;
|
||||
for (int i = children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
stack.Push(children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит все параграфы внутри таблицы
|
||||
/// </summary>
|
||||
internal static IEnumerable<Paragraph> GetTableParagraphs(Table table)
|
||||
{
|
||||
if (table is null)
|
||||
yield break;
|
||||
|
||||
// Используем обход в ширину для таблиц
|
||||
var cells = new Queue<TableCell>();
|
||||
|
||||
foreach (var row in table.Elements<TableRow>())
|
||||
{
|
||||
foreach (var cell in row.Elements<TableCell>())
|
||||
{
|
||||
cells.Enqueue(cell);
|
||||
}
|
||||
}
|
||||
|
||||
while (cells.Count > 0)
|
||||
{
|
||||
var cell = cells.Dequeue();
|
||||
|
||||
foreach (var paragraph in cell.Elements<Paragraph>())
|
||||
{
|
||||
yield return paragraph;
|
||||
}
|
||||
|
||||
// Ищем вложенные таблицы в ячейке
|
||||
foreach (var nestedTable in cell.Elements<Table>())
|
||||
{
|
||||
foreach (var nestedPara in GetTableParagraphs(nestedTable))
|
||||
{
|
||||
yield return nestedPara;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет параграф в ячейке таблицы на новые параграфы
|
||||
/// </summary>
|
||||
private static void ReplaceParagraphsInTableCell(Paragraph oldParagraph, List<Paragraph> newParagraphs)
|
||||
{
|
||||
ParagraphReplacer.ReplaceParagraph(oldParagraph, newParagraphs);
|
||||
}
|
||||
}
|
||||
321
QWERTYkez.WordProcessor/WordProcessor.cs
Normal file
321
QWERTYkez.WordProcessor/WordProcessor.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Статический класс для работы с документами Word через процессоры чтения/записи.
|
||||
/// </summary>
|
||||
public static class WordProcessor
|
||||
{
|
||||
#region Read Operations
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .docx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(string sourcePath, Action<IWordReader> read)
|
||||
{
|
||||
return TryRead(new FileInfo(sourcePath), read);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .docx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(FileInfo sourceFile, Action<IWordReader> read)
|
||||
{
|
||||
if (sourceFile is null || read is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] SourceFile or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = WordReader.CreateInternal(sourceFile);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
read(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in read action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read Operations from Memory
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из массива байт только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="data">Массив байт, содержащий документ .docx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(byte[] data, Action<IWordReader> read)
|
||||
=> TryRead(new ReadOnlyMemory<byte>(data), read);
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> только для чтения и выполнить действия.
|
||||
/// </summary>
|
||||
/// <param name="data">Буфер с документом .docx</param>
|
||||
/// <param name="read">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryRead(ReadOnlyMemory<byte> data, Action<IWordReader> read)
|
||||
{
|
||||
if (data.IsEmpty || read is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Data is empty or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = WordReader.CreateFromData(data);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
read(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in read action from memory: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .docx</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(string sourcePath, Action<IWordWriter> write)
|
||||
{
|
||||
return TryWrite(sourcePath, sourcePath, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .docx</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(FileInfo sourceFile, Action<IWordWriter> write)
|
||||
{
|
||||
return TryWrite(sourceFile, sourceFile.FullName, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Путь к исходному файлу .docx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(string sourcePath, string? destinationPath, Action<IWordWriter> write)
|
||||
{
|
||||
return TryWrite(new FileInfo(sourcePath), destinationPath, write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .docx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(FileInfo sourceFile, string? destinationPath, Action<IWordWriter> write)
|
||||
{
|
||||
if (sourceFile is null || write is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] SourceFile or action is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = WordWriter.CreateInternal(sourceFile, destinationPath);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
write(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in write action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Operations from Memory
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из массива байт для записи, выполнить действия и сохранить результат по указанному пути.
|
||||
/// </summary>
|
||||
/// <param name="data">Массив байт, содержащий документ .docx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(byte[] data, string destinationPath, Action<IWordWriter> write)
|
||||
=> TryWrite(new ReadOnlyMemory<byte>(data), destinationPath, write);
|
||||
|
||||
/// <summary>
|
||||
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> для записи, выполнить действия и сохранить результат по указанному пути.
|
||||
/// </summary>
|
||||
/// <param name="data">Буфер с документом .docx</param>
|
||||
/// <param name="destinationPath">Путь для сохранения результата</param>
|
||||
/// <param name="write">Действия для выполнения над документом</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
|
||||
public static bool TryWrite(ReadOnlyMemory<byte> data, string destinationPath, Action<IWordWriter> write)
|
||||
{
|
||||
if (data.IsEmpty || write is null)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Data is empty or write is null");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
using var processor = WordWriter.CreateFromData(data, destinationPath);
|
||||
if (processor is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
write(processor);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in write action from memory: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Word, отредактировать его и вернуть результат в виде массива байт.
|
||||
/// </summary>
|
||||
/// <param name="result">Результирующий массив байт (если операция успешна).</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(out byte[] result, Action<IWordWriter> write)
|
||||
{
|
||||
result = null!;
|
||||
if (write is null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = WordWriter.CreateNew();
|
||||
write(writer);
|
||||
result = writer.GetDocumentBytes();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Word, отредактировать его и записать в указанный поток.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">Поток, в который будет записан документ.</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(Stream outputStream, Action<IWordWriter> write)
|
||||
{
|
||||
if (write is null || outputStream is null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = WordWriter.CreateNew();
|
||||
write(writer);
|
||||
writer.SaveTo(outputStream);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Word, отредактировать его и сохранить на диск.
|
||||
/// </summary>
|
||||
/// <param name="destinationPath">Путь для сохранения результата.</param>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(string destinationPath, Action<IWordWriter> write)
|
||||
{
|
||||
if (string.IsNullOrEmpty(destinationPath) || write is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = WordWriter.CreateNew(destinationPath);
|
||||
write(writer);
|
||||
writer.Save();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается создать новый документ Word, отредактировать его и прочитать результат в памяти.
|
||||
/// </summary>
|
||||
/// <param name="write">Действия для заполнения документа.</param>
|
||||
/// <param name="read">Действия для чтения полученного документа.</param>
|
||||
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
|
||||
public static bool TryCreate(Action<IWordWriter> write, Action<IWordReader> read)
|
||||
{
|
||||
if (write is null || read is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var writer = WordWriter.CreateNew(); // без привязки к файлу
|
||||
write(writer);
|
||||
using var reader = writer.ToReader(); // преобразуем в read-only
|
||||
read(reader);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
285
QWERTYkez.WordProcessor/WordReader.cs
Normal file
285
QWERTYkez.WordProcessor/WordReader.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет потокобезопасный процессор только для чтения документов Word (DOCX) формата.
|
||||
/// <para>Не поддерживает операции изменения документа.</para>
|
||||
/// </summary>
|
||||
internal class WordReader : IDisposable, IWordReader
|
||||
{
|
||||
protected MemoryStream _ms = null!;
|
||||
protected WordprocessingDocument _doc = null!;
|
||||
protected Body _body = null!;
|
||||
protected bool _disposed;
|
||||
protected readonly object _syncLock = new();
|
||||
protected string? _originalSourcePath;
|
||||
|
||||
public Body Body => _body;
|
||||
|
||||
internal WordReader() { }
|
||||
|
||||
#region Factory Methods
|
||||
|
||||
internal static WordReader? CreateInternal(FileInfo sourceFile)
|
||||
{
|
||||
if (sourceFile is null || !sourceFile.Exists)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
MemoryStream? ms = null;
|
||||
WordprocessingDocument? doc = null;
|
||||
|
||||
try
|
||||
{
|
||||
ms = new MemoryStream();
|
||||
using (var file = new FileStream(sourceFile.FullName,
|
||||
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
file.CopyTo(ms);
|
||||
}
|
||||
ms.Position = 0;
|
||||
|
||||
doc = WordprocessingDocument.Open(ms, isEditable: false,
|
||||
new OpenSettings { AutoSave = false });
|
||||
|
||||
if (doc.MainDocumentPart?.Document?.Body is not { } body)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[DEBUG] Document body is null or empty");
|
||||
#endif
|
||||
doc.Dispose();
|
||||
ms.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
var processor = new WordReader
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
_originalSourcePath = sourceFile.FullName,
|
||||
FilePath = sourceFile.FullName
|
||||
};
|
||||
|
||||
return processor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
|
||||
#endif
|
||||
doc?.Dispose();
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static WordReader? CreateFromData(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
if (data.IsEmpty)
|
||||
return null;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
ms.Write(data.ToArray(), 0, data.Length);
|
||||
ms.Position = 0;
|
||||
|
||||
var doc = WordprocessingDocument.Open(ms, false, new OpenSettings { AutoSave = false });
|
||||
if (doc.MainDocumentPart?.Document?.Body is not { } body)
|
||||
{
|
||||
doc.Dispose();
|
||||
ms.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WordReader
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
FilePath = null // из памяти – нет файла
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Writers
|
||||
|
||||
public bool TryWrite(string destinationPath, Action<IWordWriter> action)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(destinationPath))
|
||||
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
|
||||
if (action is null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
// Копируем данные из текущего потока
|
||||
byte[] data;
|
||||
lock (_syncLock)
|
||||
{
|
||||
_ms.Position = 0;
|
||||
data = _ms.ToArray();
|
||||
}
|
||||
|
||||
using var writable = WordWriter.CreateFromData(new ReadOnlyMemory<byte>(data), destinationPath);
|
||||
if (writable is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
action(writable);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(Action<IWordWriter> write, Action<IWordReader> read)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (write is null) throw new ArgumentNullException(nameof(write));
|
||||
if (read is null) throw new ArgumentNullException(nameof(read));
|
||||
|
||||
// Копируем текущие данные
|
||||
byte[] data;
|
||||
lock (_syncLock)
|
||||
{
|
||||
_ms.Position = 0;
|
||||
data = _ms.ToArray();
|
||||
}
|
||||
|
||||
WordWriter? writable = null;
|
||||
WordReader? resultReader = null;
|
||||
try
|
||||
{
|
||||
// Создаём редактируемую копию (без привязки к файлу)
|
||||
writable = WordWriter.CreateFromData(data);
|
||||
if (writable is null)
|
||||
return false;
|
||||
|
||||
// Применяем изменения
|
||||
write(writable);
|
||||
|
||||
// Сохраняем изменения в поток и создаём read-only процессор
|
||||
resultReader = writable.ToReader();
|
||||
|
||||
if (resultReader is null)
|
||||
return false;
|
||||
|
||||
// Работаем с изменённой копией
|
||||
read(resultReader);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Гарантированно освобождаем созданные процессоры
|
||||
resultReader?.Dispose();
|
||||
writable?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Получает путь к исходному файлу.
|
||||
/// </summary>
|
||||
public string? FilePath { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
|
||||
/// </summary>
|
||||
public bool IsValid => !_disposed && _doc is not null && _body is not null && _ms is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Получает текущий размер документа в байтах.
|
||||
/// </summary>
|
||||
public long DocumentSize => _ms.Length;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read Operations
|
||||
|
||||
/// <summary>
|
||||
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
|
||||
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
|
||||
/// </summary>
|
||||
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
|
||||
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
|
||||
public ISet<string> FindPlaceholders()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
return PlaceholderFinder.FindInDocument(_doc, _body);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Pattern
|
||||
|
||||
protected void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_doc.Dispose();
|
||||
_ms.Dispose();
|
||||
}
|
||||
|
||||
_doc = null!;
|
||||
_body = null!;
|
||||
_ms = null!;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~WordReader()
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
626
QWERTYkez.WordProcessor/WordWriter.cs
Normal file
626
QWERTYkez.WordProcessor/WordWriter.cs
Normal file
@@ -0,0 +1,626 @@
|
||||
using QWERTYkez.WordProcessor.Builders;
|
||||
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет потокобезопасный процессор для чтения и записи документов Word (DOCX) формата.
|
||||
/// <para>Наследует от <see cref="WordReader"/> и добавляет операции изменения документа.</para>
|
||||
/// </summary>
|
||||
internal sealed class WordWriter : WordReader, IWordWriter
|
||||
{
|
||||
private bool _isModified = false;
|
||||
|
||||
internal WordWriter() { }
|
||||
|
||||
#region Factory Methods
|
||||
|
||||
internal static WordWriter? CreateFromData(ReadOnlyMemory<byte> data, string destinationPath)
|
||||
{
|
||||
if (data.IsEmpty || string.IsNullOrEmpty(destinationPath))
|
||||
return null;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
// Копируем данные в MemoryStream
|
||||
ms.Write(data.ToArray(), 0, data.Length);
|
||||
ms.Position = 0;
|
||||
|
||||
var doc = WordprocessingDocument.Open(ms, true, new OpenSettings { AutoSave = false });
|
||||
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
||||
{
|
||||
return new WordWriter
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
FilePath = destinationPath,
|
||||
_originalSourcePath = null // нет исходного файла
|
||||
};
|
||||
}
|
||||
|
||||
doc.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static new WordWriter? CreateFromData(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
if (data.IsEmpty)
|
||||
return null;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
ms.Write(data.ToArray(), 0, data.Length);
|
||||
ms.Position = 0;
|
||||
|
||||
var doc = WordprocessingDocument.Open(ms, true, new OpenSettings { AutoSave = false });
|
||||
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
||||
{
|
||||
return new WordWriter
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
FilePath = null, // нет привязки к файлу
|
||||
_originalSourcePath = null
|
||||
};
|
||||
}
|
||||
|
||||
doc.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
ms.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static WordWriter? CreateInternal(FileInfo sourceFile, string? destinationPath = null!)
|
||||
{
|
||||
if (sourceFile is null || !sourceFile.Exists) return null;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
using (var file = new FileStream(sourceFile.FullName,
|
||||
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
file.CopyTo(ms);
|
||||
}
|
||||
ms.Position = 0;
|
||||
|
||||
var doc = WordprocessingDocument.Open(ms, isEditable: true,
|
||||
new OpenSettings { AutoSave = false });
|
||||
|
||||
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
||||
{
|
||||
var processor = new WordWriter
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
_originalSourcePath = sourceFile.FullName,
|
||||
FilePath = destinationPath ?? sourceFile.FullName
|
||||
};
|
||||
|
||||
return processor;
|
||||
}
|
||||
|
||||
doc?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
ms?.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт новый пустой документ Word.
|
||||
/// </summary>
|
||||
/// <param name="destinationPath">Путь, по которому будет сохранён документ (необязательный).</param>
|
||||
/// <returns>Экземпляр <see cref="WordWriter"/> для редактирования нового документа.</returns>
|
||||
internal static WordWriter CreateNew(string? destinationPath = null)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
// Создаём документ, НЕ используем using
|
||||
var doc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document();
|
||||
var body = new Body();
|
||||
mainPart.Document.AppendChild(body);
|
||||
|
||||
var writer = new WordWriter
|
||||
{
|
||||
_ms = ms,
|
||||
_doc = doc,
|
||||
_body = body,
|
||||
FilePath = destinationPath,
|
||||
_originalSourcePath = null
|
||||
};
|
||||
return writer;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ms.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла.
|
||||
/// </summary>
|
||||
public bool WillOverwriteSource => FilePath == _originalSourcePath;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Replace text
|
||||
|
||||
public void ReplaceString(string oldValue, params string[] newValues) => ReplaceString(oldValue, (IEnumerable<string>)newValues);
|
||||
public void ReplaceString(string oldValue, IEnumerable<string> newValues)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(oldValue))
|
||||
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||||
|
||||
if (newValues is null) return;
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] [WordProcessor.Replace] START: '{oldValue}' -> [{string.Join(", ", newValues)}] (comparison: {StringComparison.OrdinalIgnoreCase})");
|
||||
#endif
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
TableTextProcessor.ReplaceInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 3. Колонтитулы
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplaceString(IDictionary<string, string> replacements) =>
|
||||
/* */ReplaceString((IEnumerable<KeyValuePair<string, string>>)replacements);
|
||||
public void ReplaceString(IEnumerable<KeyValuePair<string, string>> replacements)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (replacements is null) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
foreach (var kvp in replacements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key))
|
||||
{
|
||||
TableTextProcessor.ReplaceInTables(_body, kvp.Key, [kvp.Value ?? string.Empty], StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplaceString(IDictionary<string, IEnumerable<string>> replacements) =>
|
||||
/* */ReplaceString((IEnumerable<KeyValuePair<string, IEnumerable<string>>>)replacements);
|
||||
public void ReplaceString(IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (replacements is null) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
TableTextProcessor.ReplaceInTables(_body, replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 3. Колонтитулы
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Replace ReplaceItem
|
||||
public void ReplaceItem(string oldValue, params ReplaceItem[] newValues) => ReplaceItem(oldValue, (IEnumerable<ReplaceItem>)newValues);
|
||||
public void ReplaceItem(string oldValue, IEnumerable<ReplaceItem> newValues)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(oldValue))
|
||||
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||||
|
||||
if (newValues is null) return;
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] [WordProcessor.Replace] START: '{oldValue}' -> [{string.Join(", ", newValues)}] (comparison: {StringComparison.OrdinalIgnoreCase})");
|
||||
#endif
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var texts = newValues.Select(val => val.Text).ToArray();
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
TableTextProcessor.ReplaceInTables(_body, oldValue, texts, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 3. Колонтитулы
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, oldValue, texts, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplaceItem(IDictionary<string, ReplaceItem> replacements) =>
|
||||
/* */ReplaceItem((IEnumerable<KeyValuePair<string, ReplaceItem>>)replacements);
|
||||
public void ReplaceItem(IEnumerable<KeyValuePair<string, ReplaceItem>> replacements)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (replacements is null) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
var texts = replacements.ToDictionary(val => val.Key, val => val.Value.Text);
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
foreach (var kvp in texts)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Key))
|
||||
{
|
||||
TableTextProcessor.ReplaceInTables(_body, kvp.Key, kvp.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Колонтитулы
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, texts, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplaceItem(IDictionary<string, IEnumerable<ReplaceItem>> replacements) =>
|
||||
/* */ReplaceItem((IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>)replacements);
|
||||
public void ReplaceItem(IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (replacements is null) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 1. Основной текст
|
||||
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
var texts = replacements.ToDictionary(val => val.Key, val => val.Value.Select(val => val.Text));
|
||||
|
||||
// 2. Таблицы в основном тексте
|
||||
TableTextProcessor.ReplaceInTables(_body, texts, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 3. Колонтитулы
|
||||
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, texts, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет новый параграф с указанным текстом в конец документа.
|
||||
/// </summary>
|
||||
public void AddParagraph(string text, bool preserveFormatting = true)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (_body is null)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
var paragraph = new Paragraph();
|
||||
var run = new Run();
|
||||
|
||||
if (preserveFormatting && _body.Elements<Paragraph>().FirstOrDefault() is { } firstPara)
|
||||
{
|
||||
if (firstPara.ParagraphProperties is not null)
|
||||
{
|
||||
paragraph.ParagraphProperties = firstPara.ParagraphProperties.CloneNode(true) as ParagraphProperties;
|
||||
}
|
||||
|
||||
if (firstPara.Elements<Run>().FirstOrDefault()?.RunProperties is not null)
|
||||
{
|
||||
run.RunProperties = firstPara.Elements<Run>().First().RunProperties?.CloneNode(true) as RunProperties;
|
||||
}
|
||||
}
|
||||
|
||||
string processedText = text.Replace(' ', '\u00A0');
|
||||
run.AppendChild(new Text(processedText));
|
||||
paragraph.AppendChild(run);
|
||||
_body.AppendChild(paragraph);
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#region Replace to table
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TableBuilder
|
||||
/// </summary>
|
||||
/// <param name="oldValue">Текст для поиска в параграфах</param>
|
||||
/// <param name="buildTable">Действие для настройки таблицы через TableBuilder</param>
|
||||
public void ReplaceToTable(string oldValue, Action<ITable> buildTable)
|
||||
{
|
||||
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] Looking for '{oldValue}' to replace with custom table");
|
||||
#endif
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Используем метод расширения для Body с TableBuilder
|
||||
ReplaceToTableExt.ReplaceParagraphsContainingTextToTable(_body, oldValue, buildTable);
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Replace to text
|
||||
|
||||
/// <summary>
|
||||
/// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TextBuilder
|
||||
/// </summary>
|
||||
/// <param name="oldValue">Текст для поиска в параграфах</param>
|
||||
/// <param name="buildText">Действие для настройки таблицы через TextBuilder</param>
|
||||
public void ReplaceToText(string oldValue, Action<IText> buildText)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(oldValue))
|
||||
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||||
|
||||
if (buildText is null) return;
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceToText] Looking for '{oldValue}' to replace with custom text");
|
||||
#endif
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Используем метод расширения для Body с TextBuilder
|
||||
ReplaceToTextExt.ReplaceParagraphsContainingTextToText(_body, oldValue, buildText);
|
||||
_isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Save Operations
|
||||
|
||||
/// <summary>
|
||||
/// Сохраняет документ в файл, указанный при создании процессора.
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
throw new InvalidOperationException("Cannot save - no file path specified");
|
||||
|
||||
SaveTo(FilePath!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сохраняет документ в указанный файл.
|
||||
/// </summary>
|
||||
public void SaveTo(string path)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_ms is not null)
|
||||
{
|
||||
_doc.Save();
|
||||
_ms.Position = 0;
|
||||
|
||||
using var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
bufferSize: 65536);
|
||||
|
||||
_ms.CopyTo(fileStream);
|
||||
_isModified = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new IOException($"Failed to save document to '{path}'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
internal byte[] GetDocumentBytes()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_doc.Save();
|
||||
_ms.Position = 0;
|
||||
return _ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveTo(Stream outputStream)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_doc.Save();
|
||||
_ms.Position = 0;
|
||||
_ms.CopyTo(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается сохранить документ в указанный файл.
|
||||
/// </summary>
|
||||
public bool TrySaveTo(string path, out Exception? error)
|
||||
{
|
||||
error = null;
|
||||
|
||||
if (!IsValid || string.IsNullOrEmpty(path))
|
||||
{
|
||||
error = new InvalidOperationException("Processor is not valid or path is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SaveTo(path);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Failed to save to '{path}': {ex.Message}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Асинхронно сохраняет документ в указанный файл.
|
||||
/// </summary>
|
||||
public async Task SaveToAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
|
||||
if (_ms is null)
|
||||
throw new InvalidOperationException("Memory stream is not available");
|
||||
|
||||
byte[] buffer;
|
||||
lock (_syncLock)
|
||||
{
|
||||
_doc.Save();
|
||||
_ms.Position = 0;
|
||||
buffer = _ms.ToArray();
|
||||
}
|
||||
|
||||
using var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
bufferSize: 81920,
|
||||
useAsync: true);
|
||||
|
||||
await fileStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary> Создаёт read-only процессор из текущего документа. </summary>
|
||||
internal WordReader ToReader()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_doc.Save();
|
||||
_ms.Position = 0;
|
||||
var data = _ms.ToArray();
|
||||
return WordReader.CreateFromData(data) ?? throw new InvalidOperationException("Failed to create reader");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Pattern
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_isModified && !string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_doc.Save();
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
|
||||
|
||||
_ms.Position = 0;
|
||||
using var fileStream = new FileStream(
|
||||
FilePath!,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
bufferSize: 81920);
|
||||
|
||||
_ms.CopyTo(fileStream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[DEBUG] Auto-save failed during Dispose: {ex.Message}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
11
QWERTYkez.WordProcessor/globals.cs
Normal file
11
QWERTYkez.WordProcessor/globals.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
global using DocumentFormat.OpenXml;
|
||||
global using DocumentFormat.OpenXml.Packaging;
|
||||
global using DocumentFormat.OpenXml.Wordprocessing;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Diagnostics;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
Reference in New Issue
Block a user