using QWERTYkez.WordProcessor.Builders; namespace QWERTYkez.WordProcessor; /// /// Предоставляет потокобезопасный процессор для чтения и записи документов Word (DOCX) формата. /// Наследует от и добавляет операции изменения документа. /// internal sealed class WordWriter : WordReader, IWordWriter { private bool _isModified = false; internal WordWriter() { } #region Factory Methods internal static WordWriter? CreateFromData(ReadOnlyMemory 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 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; } /// /// Создаёт новый пустой документ Word. /// /// Путь, по которому будет сохранён документ (необязательный). /// Экземпляр для редактирования нового документа. 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 /// /// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла. /// public bool WillOverwriteSource => FilePath == _originalSourcePath; #endregion #region Replace text public void ReplaceString(string oldValue, params string[] newValues) => ReplaceString(oldValue, (IEnumerable)newValues); public void ReplaceString(string oldValue, IEnumerable 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 replacements) => /* */ReplaceString((IEnumerable>)replacements); public void ReplaceString(IEnumerable> 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> replacements) => /* */ReplaceString((IEnumerable>>)replacements); public void ReplaceString(IEnumerable>> 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)newValues); public void ReplaceItem(string oldValue, IEnumerable 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 replacements) => /* */ReplaceItem((IEnumerable>)replacements); public void ReplaceItem(IEnumerable> 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> replacements) => /* */ReplaceItem((IEnumerable>>)replacements); public void ReplaceItem(IEnumerable>> 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 /// /// Добавляет новый параграф с указанным текстом в конец документа. /// 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().FirstOrDefault() is { } firstPara) { if (firstPara.ParagraphProperties is not null) { paragraph.ParagraphProperties = firstPara.ParagraphProperties.CloneNode(true) as ParagraphProperties; } if (firstPara.Elements().FirstOrDefault()?.RunProperties is not null) { run.RunProperties = firstPara.Elements().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 /// /// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TableBuilder /// /// Текст для поиска в параграфах /// Действие для настройки таблицы через TableBuilder public void ReplaceToTable(string oldValue, Action 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 /// /// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TextBuilder /// /// Текст для поиска в параграфах /// Действие для настройки таблицы через TextBuilder public void ReplaceToText(string oldValue, Action 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 /// /// Сохраняет документ в файл, указанный при создании процессора. /// public void Save() { ThrowIfDisposed(); if (string.IsNullOrEmpty(FilePath)) throw new InvalidOperationException("Cannot save - no file path specified"); SaveTo(FilePath!); } /// /// Сохраняет документ в указанный файл. /// 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); } } /// /// Пытается сохранить документ в указанный файл. /// 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; } } /// /// Асинхронно сохраняет документ в указанный файл. /// 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); } /// Создаёт read-only процессор из текущего документа. 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 }