2026-06-08 14:31:31 +07:00
|
|
|
|
namespace QWERTYkez.ExcelProcessor;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
|
|
|
|
|
/// <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 />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public ICellText Break()
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
// Добавляем символ переноса строки в последний существующий Run
|
|
|
|
|
|
if (_runs != null && _runs.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var lastRun = _runs[_runs.Count - 1];
|
|
|
|
|
|
lastRun.Text += "\n";
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если нет ни одного Run, создаём новый с символом переноса
|
2026-06-16 12:03:56 +07:00
|
|
|
|
Run("\n", null);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public ICellText Run(string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
|
|
|
|
return this;
|
|
|
|
|
|
_runs ??= [];
|
|
|
|
|
|
var run = new ExcelRun { Text = text, Format = format };
|
|
|
|
|
|
_runs.Add(run);
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public ICellText RunBreak(string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-16 12:03:56 +07:00
|
|
|
|
Run(text, format);
|
|
|
|
|
|
Break();
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public ICellText Sub(string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
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 };
|
2026-06-16 12:03:56 +07:00
|
|
|
|
return Run(text, subFormat);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public ICellText Sup(string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
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 };
|
2026-06-16 12:03:56 +07:00
|
|
|
|
return Run(text, supFormat);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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 на следующей позиции
|
2026-06-16 12:03:56 +07:00
|
|
|
|
Break();
|
2026-06-05 15:58:03 +07:00
|
|
|
|
// Сдвигаем? Просто добавляем break в конец – неверно. Break должен быть сразу после вставленного.
|
|
|
|
|
|
// Но AddBreak добавляет в конец. Нужно вставить break на index+1.
|
|
|
|
|
|
return TryInsertRun(index + 1, "\n", null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public bool TryInsertSub(int index, string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
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 />
|
2026-06-16 12:03:56 +07:00
|
|
|
|
public bool TryInsertSup(int index, string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|