9 Commits
Author SHA1 Message Date
melekhin fa77b2ce39 666 2026-08-10 15:23:45 +07:00
melekhin b023d2755e 555+ 2026-08-10 14:59:18 +07:00
melekhin 2811a40c6d Rotates 555 2026-08-10 14:48:49 +07:00
melekhin 9c81a084f1 ROTATES 2 2026-08-10 13:58:11 +07:00
melekhin 265bfc7419 optimize 2026-08-03 16:31:54 +07:00
melekhin 25207773f7 [ Word ROTATES ] 2026-08-03 16:11:54 +07:00
melekhin da188eaeab 26.07.23 2026-07-23 16:42:26 +07:00
melekhin 7f40ba664f ++ 2026-07-22 14:00:18 +07:00
melekhin 1dcb22e1d5 26.07.21 2026-07-21 16:43:59 +07:00
11 changed files with 669 additions and 341 deletions
+22 -4
View File
@@ -21,16 +21,16 @@ internal sealed class ExcelBook : IBook
public IReadOnlyList<ISheet> GetSheets() => Writer.GetSheets();
/// <inheritdoc />
public ISheet? Sheet(string name) => Writer.Sheet(name);
public ISheet? Sheet(string name) => Writer.Sheet(name.EscapeSymbols());
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name, out sheet);
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name.EscapeSymbols(), out sheet);
/// <inheritdoc />
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name, edit);
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name.EscapeSymbols(), edit);
/// <inheritdoc />
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name);
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name.EscapeSymbols());
/// <inheritdoc />
public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet);
@@ -41,3 +41,21 @@ internal sealed class ExcelBook : IBook
/// <inheritdoc />
public NumberFormatPattern CreateNumberFormat(string format) => Writer.CreateNumberFormat(format);
}
public static class EscapeExt
{
public static string EscapeSymbols(this string source)
{
var sb = new StringBuilder(source);
sb.Replace('/', '');
sb.Replace('\\', '');
sb.Replace('*', '');
sb.Replace('?', '');
sb.Replace(':', '˸');
sb.Replace('[', '');
sb.Replace(']', '');
return sb.ToString();
}
}
@@ -33,7 +33,7 @@ internal sealed class ExcelSheet : ISheet
{
if (string.IsNullOrEmpty(name)) return false;
Book.ThrowIfDisposed();
SheetElement.Name = name;
SheetElement.Name = name.EscapeSymbols();
return true;
}
+6 -2
View File
@@ -1095,6 +1095,8 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
if (string.IsNullOrEmpty(name))
return null;
name = name.EscapeSymbols();
lock (_syncLock)
{
var workbookPart = _doc.WorkbookPart;
@@ -1113,7 +1115,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet)
{
sheet = Sheet(name)!;
sheet = Sheet(name.EscapeSymbols())!;
return sheet != null;
}
@@ -1130,6 +1132,8 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
if (workbookPart?.Workbook?.Sheets == null)
return false;
name = name.EscapeSymbols();
// Проверка уникальности имени
foreach (Sheet s in workbookPart.Workbook.Sheets.Elements<Sheet>())
{
@@ -1168,7 +1172,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
/// <inheritdoc />
public bool TryRemoveSheet(string name)
{
var sheet = Sheet(name);
var sheet = Sheet(name.EscapeSymbols());
return sheet != null && TryRemoveSheet(sheet);
}
+31
View File
@@ -0,0 +1,31 @@
namespace QWERTYkez.WordProcessor;
internal static class Extension
{
public static void SwapBottomRight(this PageMargin margin)
{
if (margin.Right is { } r)
{
margin.Right = margin.Bottom is not null ? new((uint)margin.Bottom.Value) : null;
margin.Bottom = new((int)r.Value);
}
else if(margin.Bottom is { } b)
{
margin.Bottom = null;
margin.Right = new((uint)b.Value);
}
}
public static void SwapValues(this PageSize size)
{
if (size.Width is { } w)
{
size.Width = size.Height is not null ? new(size.Height.Value) : null;
size.Height = new(w.Value);
}
else if (size.Height is { } h)
{
size.Height = null;
size.Width = new(h.Value);
}
}
}
+4
View File
@@ -6,6 +6,10 @@ public interface IWordReader
string? FilePath { get; }
bool IsValid { get; }
#if DEBUG
Body Body { get; }
#endif
ISet<string> FindPlaceholders();
-106
View File
@@ -1,106 +0,0 @@
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);
}
}
+507 -179
View File
@@ -2,13 +2,11 @@
/// <summary>
/// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений.
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение
/// остаётся в текущем параграфе, а последующие создают новые.
/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах.
/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem"/>.
/// </summary>
internal static class MultiReplaceExt
{
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (СИГНАТУРЫ НЕИЗМЕННЫ) ----------
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (для Body) ----------
#region Body.Replace с одним ключом
@@ -152,7 +150,7 @@ internal static class MultiReplaceExt
public int End { get; } = end;
}
private class ParagraphStructure(string fullText, List<MultiReplaceExt.RunSegment> segments)
private class ParagraphStructure(string fullText, List<RunSegment> segments)
{
public string FullText { get; } = fullText;
public List<RunSegment> Segments { get; } = segments;
@@ -182,148 +180,53 @@ internal static class MultiReplaceExt
return sb.ToString();
}
private static Paragraph CloneParagraphProperties(Paragraph original)
/// <summary>Клонирует параграф, удаляя все SectionProperties и PageBreakBefore.</summary>
private static Paragraph CloneParagraphWithoutSection(Paragraph original)
{
var newPara = new Paragraph();
if (original.ParagraphProperties is not null)
newPara.ParagraphProperties = (ParagraphProperties)original.ParagraphProperties.CloneNode(true);
{
var newProps = new ParagraphProperties();
foreach (var child in original.ParagraphProperties.ChildElements)
{
if (child is not SectionProperties && child is not PageBreakBefore)
newProps.AppendChild(child.CloneNode(true));
}
newPara.ParagraphProperties = newProps;
}
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)
private static Paragraph? BuildRemainderParagraph(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)
var newPara = CloneParagraphWithoutSection(original);
foreach (var seg in structure.Segments)
{
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.End <= position) continue;
if (seg.Start >= position)
{
// Полностью после позиции
newPara.AppendChild(run.CloneNode(true));
if (seg == firstTextSeg)
passedFirstText = true;
newPara.AppendChild(seg.Run.CloneNode(true));
}
else if (seg.End > position)
{
// Частично пересекает – обрезаем текст
var runClone = (Run)run.CloneNode(true);
foreach (var t in runClone.Elements<Text>().ToList())
t.Remove();
var runClone = (Run)seg.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)
private static void InsertFormattedRun(Paragraph para, string text, ParagraphStructure structure, int position)
{
var seg = structure.Segments.FirstOrDefault(s => position >= s.Start && position < s.End);
if (seg is null) return;
@@ -331,44 +234,205 @@ internal static class MultiReplaceExt
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));
textRun.AppendChild(new Text(text));
para.AppendChild(textRun);
}
if (item.BreakPage)
private static void AddSectionProperties(
Paragraph para,
BreakType splitValue,
bool addPageSize,
SectionProperties sourceSection,
SectionProperties portraitSection)
{
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);
}
if (para is null) return;
if (para.ParagraphProperties is not null)
{
var sections = para.ParagraphProperties.Elements<SectionProperties>().ToList();
foreach (var sec in sections) sec.Remove();
var pageBreaks = para.ParagraphProperties.Elements<PageBreakBefore>().ToList();
foreach (var pb in pageBreaks) pb.Remove();
}
para.ParagraphProperties ??= new ParagraphProperties();
var sectionProps = new SectionProperties();
var pageSizeClone = CreatePageSizeClone(addPageSize, splitValue, sourceSection, portraitSection);
if (pageSizeClone is not null)
sectionProps.AppendChild(pageSizeClone);
var marginClone = CreateMarginClone(addPageSize, splitValue, sourceSection, portraitSection);
if (marginClone is not null)
sectionProps.AppendChild(marginClone);
sectionProps.AppendChild(new SectionType { Val = SectionMarkValues.NextPage });
para.ParagraphProperties.AppendChild(sectionProps);
}
private static PageSize? CreatePageSizeClone(
bool addPageSize,
BreakType splitValue,
SectionProperties sourceSection,
SectionProperties portraitSection)
{
if (!addPageSize && splitValue != BreakType.NewLandscapeSection)
return null;
SectionProperties? sizeSource = addPageSize ? sourceSection : portraitSection;
var sourcePageSize = sizeSource?.GetFirstChild<PageSize>()
?? portraitSection.GetFirstChild<PageSize>()
?? sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is null)
return null;
var clone = (PageSize)sourcePageSize.CloneNode(true);
bool sourceIsLandscape = clone.Orient?.Value == PageOrientationValues.Landscape ||
(clone.Width?.Value > clone.Height?.Value);
bool targetIsLandscape = addPageSize && splitValue == BreakType.NewLandscapeSection;
if (targetIsLandscape && !sourceIsLandscape)
{
clone.SwapValues();
clone.Orient = PageOrientationValues.Landscape;
}
else if (!targetIsLandscape && sourceIsLandscape)
{
clone.SwapValues();
clone.Orient = addPageSize ? PageOrientationValues.Portrait : null;
}
else
{
clone.Orient = targetIsLandscape ? PageOrientationValues.Landscape :
addPageSize ? PageOrientationValues.Portrait : null;
}
return clone;
}
private static PageMargin? CreateMarginClone(
bool addPageSize,
BreakType splitValue,
SectionProperties sourceSection,
SectionProperties portraitSection)
{
PageMargin? marginToUse = null;
if (addPageSize)
{
var sourceMargin = sourceSection?.GetFirstChild<PageMargin>()
?? portraitSection?.GetFirstChild<PageMargin>();
if (sourceMargin is not null)
{
var clone = (PageMargin)sourceMargin.CloneNode(true);
var sourceOrient = sourceSection?.GetFirstChild<PageSize>()?.Orient;
bool sourceIsLandscape = sourceOrient?.Value == PageOrientationValues.Landscape ||
(sourceSection?.GetFirstChild<PageSize>()?.Width?.Value > sourceSection?.GetFirstChild<PageSize>()?.Height?.Value);
if ((splitValue == BreakType.NewLandscapeSection && !sourceIsLandscape) ||
(splitValue == BreakType.NewPortraitSection && sourceIsLandscape))
{
clone.SwapBottomRight();
}
marginToUse = clone;
}
}
else
{
var portraitMargin = portraitSection?.GetFirstChild<PageMargin>();
if (portraitMargin is not null)
marginToUse = (PageMargin)portraitMargin.CloneNode(true);
}
return marginToUse;
}
private static void EnsureBodyLandscapeSection(
Body body,
SectionProperties sourceSection,
bool sectionChangeInsideGroup,
BreakType? lastOrientation)
{
if (body is null || sourceSection is null) return;
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is null) return;
bool sourceIsLandscape = sourcePageSize.Orient?.Value == PageOrientationValues.Landscape ||
(sourcePageSize.Width?.Value > sourcePageSize.Height?.Value);
bool lastIsPortrait = lastOrientation.HasValue && lastOrientation.Value == BreakType.NewPortraitSection;
if (!(sourceIsLandscape && sectionChangeInsideGroup && lastIsPortrait))
return;
foreach (var bodySec in body.Elements<SectionProperties>())
{
var ps = bodySec.GetFirstChild<PageSize>();
if (ps is not null && ps.Orient?.Value == PageOrientationValues.Landscape)
return;
}
var bodySection = new SectionProperties();
var pageSizeClone = (PageSize)sourcePageSize.CloneNode(true);
if (pageSizeClone.Orient?.Value != PageOrientationValues.Landscape)
{
pageSizeClone.SwapValues();
pageSizeClone.Orient = PageOrientationValues.Landscape;
}
bodySection.AppendChild(pageSizeClone);
var sourceMargin = sourceSection.GetFirstChild<PageMargin>();
if (sourceMargin is not null)
{
var marginClone = (PageMargin)sourceMargin.CloneNode(true);
if (sourceSection.GetFirstChild<PageSize>()?.Orient?.Value != PageOrientationValues.Landscape)
marginClone.SwapBottomRight();
bodySection.AppendChild(marginClone);
}
bodySection.AppendChild(new SectionType { Val = SectionMarkValues.NextPage });
body.AppendChild(bodySection);
}
/// <summary>
/// Добавляет содержимое одного параграфа в другой (клонируя элементы).
/// </summary>
private static void MergeParagraph(Paragraph target, Paragraph source)
{
foreach (var child in source.ChildElements)
target.AppendChild(child.CloneNode(true));
}
/// <summary>
/// Основной алгоритм: обрабатывает все вхождения всех ключей из предоставленных словарей.
/// </summary>
// ---------- ОСНОВНОЙ АЛГОРИТМ ----------
/// <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 body = original.Ancestors<Body>().FirstOrDefault();
// Определяем, есть ли пустые параграфы перед original
bool hasEmptyParagraphsBefore = false;
var prevPara = original.PreviousSibling<Paragraph>();
while (prevPara is not null)
{
if (!string.IsNullOrEmpty(prevPara.InnerText))
break;
hasEmptyParagraphsBefore = true;
prevPara = prevPara.PreviousSibling<Paragraph>();
}
// 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))]));
var items = kvp.Value.Select(v => new ReplaceItem(v));
definitions.Add(new MatchDefinition(kvp.Key, items));
}
}
if (itemReplacements is not null)
@@ -379,7 +443,6 @@ internal static class MultiReplaceExt
definitions.Add(new MatchDefinition(kvp.Key, kvp.Value));
}
}
if (definitions.Count == 0) return null;
// 2. Анализ структуры параграфа
@@ -389,7 +452,7 @@ internal static class MultiReplaceExt
string fullText = structure.FullText;
if (fullText.Length == 0) return null;
// 3. Находим все вхождения всех ключей
// 3. Поиск всех вхождений
var matches = new List<Match>();
foreach (var def in definitions)
{
@@ -405,90 +468,355 @@ internal static class MultiReplaceExt
pos += def.Key.Length;
}
}
if (matches.Count == 0) return null;
// 4. Сортируем по позиции
matches.Sort((a, b) => a.Start.CompareTo(b.Start));
// 5. Построение результирующих параграфов
// 4. Получаем исходную секцию для копирования (из original или из документа)
SectionProperties? sourceSection = original.ParagraphProperties?.GetFirstChild<SectionProperties>()?.CloneNode(true) as SectionProperties;
if (sourceSection is null && body is not null)
{
sourceSection = body.Elements<SectionProperties>().LastOrDefault()?.CloneNode(true) as SectionProperties;
}
sourceSection ??= new SectionProperties();
SectionProperties? portraitSection = null;
if (body is not null)
{
portraitSection = body.Elements<SectionProperties>().FirstOrDefault()?.CloneNode(true) as SectionProperties;
}
portraitSection ??= new SectionProperties();
// 5. Построение результата
var resultParas = new List<Paragraph>();
Paragraph? currentPara = null;
int currentPos = 0;
bool sectionChangeInsideGroup = false;
BreakType? lastOrientation = null;
// Состояние обработки группы
bool pageBreakBeforeNext = false;
BreakType? pendingOrientation = null;
bool firstSplitMarkerHandled = false;
int textCount = 0;
Paragraph? lastTextPara = null;
bool emptyParagraphCreated = false;
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)
var beforePara = CloneParagraphWithoutSection(original);
foreach (var seg in structure.Segments)
{
if (currentPara is null)
if (seg.End <= currentPos) continue;
if (seg.Start >= match.Start) break;
if (seg.Start >= currentPos && seg.End <= match.Start)
{
currentPara = textPart;
resultParas.Add(currentPara);
beforePara.AppendChild(seg.Run.CloneNode(true));
}
else
else if (seg.Start < match.Start && seg.End > currentPos)
{
MergeParagraph(currentPara, textPart);
int startOffset = Math.Max(0, currentPos - seg.Start);
int endOffset = Math.Min(seg.Text.Length, match.Start - seg.Start);
var runClone = (Run)seg.Run.CloneNode(true);
foreach (var t in runClone.Elements<Text>().ToList()) t.Remove();
string textPart = seg.Text.Substring(startOffset, endOffset - startOffset);
runClone.AppendChild(new Text(textPart));
beforePara.AppendChild(runClone);
}
}
if (beforePara.ChildElements.OfType<Run>().Any())
{
resultParas.Add(beforePara);
currentPara = beforePara;
lastTextPara = beforePara;
}
}
var values = match.Definition.Values.ToList();
if (values.Count == 0) continue;
// Сбрасываем флаги для новой группы
pageBreakBeforeNext = false;
pendingOrientation = null;
firstSplitMarkerHandled = false;
textCount = 0;
lastTextPara = null;
emptyParagraphCreated = false;
foreach (var item in values)
{
if (!string.IsNullOrEmpty(item.Text))
{
ProcessTextItem(
item.Text,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref textCount,
ref pageBreakBeforeNext,
ref sectionChangeInsideGroup,
ref lastOrientation,
original,
structure,
match.Start,
sourceSection,
portraitSection,
resultParas);
}
else if (item.BreakValue.HasValue)
{
ProcessBreakItem(
item.BreakValue.Value,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref pageBreakBeforeNext,
ref firstSplitMarkerHandled,
ref sectionChangeInsideGroup,
ref lastOrientation,
ref textCount,
ref emptyParagraphCreated,
hasEmptyParagraphsBefore,
original,
sourceSection,
portraitSection,
resultParas);
}
}
// Закрываем секцию, если была смена
if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection);
}
}
// Если внутри группы не было смены, но есть sourceSection с PageSize,
// копируем её в последний параграф группы (если он не имеет секции)
if (!sectionChangeInsideGroup && currentPara is not null)
{
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
currentPara.ParagraphProperties ??= new ParagraphProperties();
currentPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true));
}
}
}
// Обрабатываем значения замены для этого совпадения
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; // теперь текущий параграф – последний созданный
});
EnsureBodyLandscapeSection(body!, sourceSection, sectionChangeInsideGroup, lastOrientation);
currentPos = match.End;
}
// Текст после последнего совпадения – используем BuildAfterParagraph, чтобы пропустить лишние разрывы
// 6. Обработка остатка текста
if (currentPos < fullText.Length)
{
var textPart = BuildAfterParagraph(original, structure, currentPos);
if (textPart is not null)
var remainderPara = BuildRemainderParagraph(original, structure, currentPos);
if (remainderPara is not null)
{
if (currentPara is null)
// Если есть отложенная ориентация — применяем её
if (pendingOrientation.HasValue)
{
currentPara = textPart;
resultParas.Add(currentPara);
var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(remainderPara, pendingOrientation.Value, addPageSize, sourceSection, portraitSection);
pendingOrientation = null;
}
else if (sectionChangeInsideGroup && lastOrientation.HasValue)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(remainderPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection);
}
else
{
MergeParagraph(currentPara, textPart);
// Если нет смены, но есть sourceSection с PageSize, копируем её в остаток
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
remainderPara.ParagraphProperties ??= new ParagraphProperties();
remainderPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true));
}
}
resultParas.Add(remainderPara);
}
}
// Удаляем пустые параграфы
// 7. Очистка пустых параграфов
for (int i = resultParas.Count - 1; i >= 0; i--)
{
if (!resultParas[i].ChildElements.OfType<Run>().Any() && resultParas[i].ParagraphProperties is null)
var p = resultParas[i];
if (!p.ChildElements.OfType<Run>().Any() && p.ParagraphProperties is null)
resultParas.RemoveAt(i);
}
return resultParas.Count > 0 ? resultParas : null;
}
// ---------- ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ОБРАБОТКИ ГРУПП ----------
private static void ProcessTextItem(
string text,
ref BreakType? pendingOrientation,
ref Paragraph? currentPara,
ref Paragraph? lastTextPara,
ref int textCount,
ref bool pageBreakBeforeNext,
ref bool sectionChangeInsideGroup,
ref BreakType? lastOrientation,
Paragraph original,
ParagraphStructure structure,
int position,
SectionProperties sourceSection,
SectionProperties portraitSection,
List<Paragraph> resultParas)
{
if (pendingOrientation.HasValue)
{
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(currentPara, pendingOrientation.Value, addPageSize, sourceSection, portraitSection);
lastOrientation = pendingOrientation;
sectionChangeInsideGroup = true;
pendingOrientation = null;
textCount++;
lastTextPara = currentPara;
if (pageBreakBeforeNext)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
currentPara.InsertAt(breakRun, 0);
pageBreakBeforeNext = false;
}
InsertFormattedRun(currentPara, text, structure, position);
}
else
{
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
textCount++;
lastTextPara = currentPara;
if (pageBreakBeforeNext)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
currentPara.InsertAt(breakRun, 0);
pageBreakBeforeNext = false;
}
InsertFormattedRun(currentPara, text, structure, position);
}
}
private static void ProcessBreakItem(
BreakType breakType,
ref BreakType? pendingOrientation,
ref Paragraph? currentPara,
ref Paragraph? lastTextPara,
ref bool pageBreakBeforeNext,
ref bool firstSplitMarkerHandled,
ref bool sectionChangeInsideGroup,
ref BreakType? lastOrientation,
ref int textCount,
ref bool emptyParagraphCreated,
bool hasEmptyParagraphsBefore,
Paragraph original,
SectionProperties sourceSection,
SectionProperties portraitSection,
List<Paragraph> resultParas)
{
if (breakType == BreakType.PageBreak)
{
if (currentPara is not null)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
var lastRun = currentPara.Descendants<Run>().LastOrDefault();
if (lastRun?.RunProperties is not null)
breakRun.RunProperties = (RunProperties)lastRun.RunProperties.CloneNode(true);
currentPara.AppendChild(breakRun);
}
else
{
pageBreakBeforeNext = true;
}
}
else if (breakType == BreakType.NewLandscapeSection || breakType == BreakType.NewPortraitSection)
{
if (currentPara is null && hasEmptyParagraphsBefore && !firstSplitMarkerHandled)
{
var newPara = CloneParagraphWithoutSection(original);
resultParas.Add(newPara);
currentPara = newPara;
AddSectionProperties(currentPara, breakType, false, sourceSection, portraitSection);
lastOrientation = breakType;
sectionChangeInsideGroup = true;
firstSplitMarkerHandled = true;
emptyParagraphCreated = true;
pendingOrientation = null;
textCount = 0;
lastTextPara = null;
}
else if (currentPara is null && !hasEmptyParagraphsBefore && !firstSplitMarkerHandled)
{
pendingOrientation = breakType;
firstSplitMarkerHandled = true;
}
else if (currentPara is not null)
{
if (!firstSplitMarkerHandled)
{
if (lastTextPara is not null)
{
if (lastTextPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
lastTextPara.ParagraphProperties ??= new ParagraphProperties();
var sourcePageSize = sourceSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
lastTextPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true));
}
else
{
AddSectionProperties(lastTextPara, breakType, false, sourceSection, portraitSection);
}
lastOrientation = breakType;
sectionChangeInsideGroup = true;
}
firstSplitMarkerHandled = true;
pendingOrientation = null;
}
else
{
AddSectionProperties(currentPara, breakType, false, sourceSection, portraitSection);
lastOrientation = breakType;
sectionChangeInsideGroup = true;
firstSplitMarkerHandled = true;
pendingOrientation = null;
}
}
else
{
pendingOrientation = breakType;
}
}
}
}
}
+35 -20
View File
@@ -1,24 +1,39 @@
namespace QWERTYkez.WordProcessor;
public readonly struct ReplaceItem
/// <summary> Определяет тип разрыва или смены ориентации страницы </summary>
public enum BreakType
{
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 };
/// <summary>Обычный разрыв страницы (новый лист).</summary>
PageBreak,
/// <summary>Начать новую секцию с альбомной ориентацией страницы.</summary>
NewLandscapeSection,
/// <summary>Начать новую секцию с книжной ориентацией страницы.</summary>
NewPortraitSection,
}
public class ReplaceItem
{
private ReplaceItem() { }
public ReplaceItem(string text) => _Text = text;
public ReplaceItem(BreakType item) => _BreakValue = item;
public static implicit operator ReplaceItem(string text) => new(text);
public static implicit operator ReplaceItem(BreakType item) => item switch
{
BreakType.PageBreak => PageBreak,
BreakType.NewLandscapeSection => NewLandscapeSection,
BreakType.NewPortraitSection => NewPortraitSection,
_ => throw new NotImplementedException()
};
public static ReplaceItem PageBreak { get; } = new() { _BreakValue = BreakType.PageBreak };
public static ReplaceItem NewLandscapeSection { get; } = new() { _BreakValue = BreakType.NewLandscapeSection };
public static ReplaceItem NewPortraitSection { get; } = new() { _BreakValue = BreakType.NewPortraitSection };
public string Text => _Text;
public string _Text = string.Empty;
public BreakType? BreakValue => _BreakValue;
public BreakType? _BreakValue;
}
+56 -22
View File
@@ -9,7 +9,7 @@ internal static class SimplyReplaceExt
internal readonly int Length = length;
}
private sealed class ParagraphStructure(string fullText, SimplyReplaceExt.TextNodeInfo[] textNodes)
private sealed class ParagraphStructure(string fullText, TextNodeInfo[] textNodes)
{
internal readonly string FullText = fullText;
internal readonly TextNodeInfo[] TextNodes = textNodes;
@@ -77,7 +77,7 @@ internal static class SimplyReplaceExt
}
}
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage = false)
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, BreakType? splitValue = null)
{
if (paragraph is null || string.IsNullOrEmpty(oldValue))
return false;
@@ -103,10 +103,13 @@ internal static class SimplyReplaceExt
if (nodesToReplace.Count == 0)
return false;
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, breakPage);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, splitValue);
return true;
}
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage)
=> SimpleReplace(paragraph, oldValue, newValue, comparisonType, breakPage ? BreakType.PageBreak : null);
internal static void Replace(this Paragraph paragraph, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
{
if (paragraph is null || replacements is null || replacements.Count() == 0)
@@ -120,10 +123,8 @@ internal static class SimplyReplaceExt
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
// Сначала находим все вхождения
var fullText = structure.FullText;
foreach (var kvp in replacements)
{
@@ -137,7 +138,8 @@ internal static class SimplyReplaceExt
{
OldValue = kvp.Key,
NewValue = kvp.Value ?? string.Empty,
Index = pos
Index = pos,
BreakValue = null
});
pos += kvp.Key.Length;
}
@@ -146,10 +148,8 @@ internal static class SimplyReplaceExt
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];
@@ -159,7 +159,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0)
{
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakValue);
}
}
}
@@ -177,10 +177,8 @@ internal static class SimplyReplaceExt
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
// Сначала находим все вхождения
var fullText = structure.FullText;
foreach (var kvp in replacements)
{
@@ -194,8 +192,8 @@ internal static class SimplyReplaceExt
{
OldValue = kvp.Key,
NewValue = kvp.Value.Text ?? string.Empty,
BreakPage = kvp.Value.BreakPage,
Index = pos
Index = pos,
BreakValue = kvp.Value.BreakValue
});
pos += kvp.Key.Length;
}
@@ -204,10 +202,8 @@ internal static class SimplyReplaceExt
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];
@@ -217,7 +213,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0)
{
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakPage);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakValue);
}
}
}
@@ -227,7 +223,7 @@ internal static class SimplyReplaceExt
internal string OldValue { get; set; } = null!;
internal string NewValue { get; set; } = null!;
internal int Index { get; set; }
internal bool BreakPage { get; set; }
internal BreakType? BreakValue { get; set; } = null;
}
private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable<Run> runs)
@@ -291,7 +287,7 @@ internal static class SimplyReplaceExt
int matchStart,
int matchEnd,
string newValue,
bool breakPage = false)
BreakType? splitValue)
{
if (nodesToReplace.Count == 0) return;
@@ -311,22 +307,61 @@ internal static class SimplyReplaceExt
processedNewValue
);
// Очищаем остальные текстовые ноды
for (int i = 1; i < nodesToReplace.Count; i++)
{
nodesToReplace[i].Text.Text = string.Empty;
}
if (breakPage)
if (splitValue == BreakType.PageBreak)
{
if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para)
{
var breakRun = new Run(new Break() { Type = BreakValues.Page });
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);
}
}
else if (splitValue == BreakType.NewLandscapeSection || splitValue == BreakType.NewPortraitSection)
{
var firstText = nodesToReplace[0].Text;
if (firstText.Parent is Run run && run.Parent is Paragraph para)
{
AddSectionProperties(para, splitValue);
}
}
}
private static void AddSectionProperties(Paragraph para, BreakType? splitValue)
{
if (para is null) return;
PageOrientationValues orientation = splitValue == BreakType.NewLandscapeSection
? PageOrientationValues.Landscape
: PageOrientationValues.Portrait;
uint width, height;
if (orientation == PageOrientationValues.Landscape)
{
width = 16838;
height = 11906;
}
else
{
width = 11906;
height = 16838;
}
var sectionProps = new SectionProperties(
new PageSize
{
Width = width,
Height = height,
Orient = orientation
}
);
para.ParagraphProperties ??= new ParagraphProperties();
para.ParagraphProperties.AppendChild(sectionProps);
}
private static unsafe string ReplaceSpacesWithNonBreaking(string input)
@@ -358,7 +393,6 @@ internal static class SimplyReplaceExt
int end = Math.Min(start + length, original.Length);
// Оптимизированная конкатенация
var sb = new StringBuilder(original.Length - length + replacement.Length);
if (start > 0)
{
+1 -1
View File
@@ -21,7 +21,7 @@ internal class WordReader : IDisposable, IWordReader
internal static WordReader? CreateInternal(FileInfo sourceFile)
{
if (sourceFile is null || !sourceFile.Exists)
if (sourceFile is null || !File.Exists(sourceFile.FullName))
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
+1 -1
View File
@@ -81,7 +81,7 @@ internal sealed class WordWriter : WordReader, IWordWriter
internal static WordWriter? CreateInternal(FileInfo sourceFile, string? destinationPath = null!)
{
if (sourceFile is null || !sourceFile.Exists) return null;
if (sourceFile is null || !File.Exists(sourceFile.FullName)) return null;
var ms = new MemoryStream();
try