using System.CommandLine; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; namespace MiniMaxAIDocx.Core.Commands; /// /// Scenario C: Apply formatting from a template DOCX to a source DOCX. /// Copies styles, theme, numbering, headers/footers, and section properties /// from the template while preserving all content from the source. /// public static class ApplyTemplateCommand { public static Command Create() { var inputOpt = new Option("--input") { Description = "Source DOCX (content to keep)", Required = true }; var templateOpt = new Option("--template") { Description = "Template DOCX (formatting to apply)", Required = true }; var outputOpt = new Option("--output") { Description = "Output DOCX file path", Required = true }; var applyStylesOpt = new Option("--apply-styles") { Description = "Copy styles.xml from template" }; applyStylesOpt.DefaultValueFactory = _ => true; var applyThemeOpt = new Option("--apply-theme") { Description = "Copy theme from template" }; applyThemeOpt.DefaultValueFactory = _ => true; var applyNumberingOpt = new Option("--apply-numbering") { Description = "Copy numbering.xml from template" }; applyNumberingOpt.DefaultValueFactory = _ => true; var applyHeadersFootersOpt = new Option("--apply-headers-footers") { Description = "Copy headers/footers from template" }; var applySectionsOpt = new Option("--apply-sections") { Description = "Apply section properties from template" }; applySectionsOpt.DefaultValueFactory = _ => true; var cmd = new Command("apply-template", "Apply template formatting to a DOCX") { inputOpt, templateOpt, outputOpt, applyStylesOpt, applyThemeOpt, applyNumberingOpt, applyHeadersFootersOpt, applySectionsOpt }; cmd.SetAction((parseResult) => { var inputPath = parseResult.GetValue(inputOpt)!; var templatePath = parseResult.GetValue(templateOpt)!; var outputPath = parseResult.GetValue(outputOpt)!; var applyStyles = parseResult.GetValue(applyStylesOpt); var applyTheme = parseResult.GetValue(applyThemeOpt); var applyNumbering = parseResult.GetValue(applyNumberingOpt); var applyHeadersFooters = parseResult.GetValue(applyHeadersFootersOpt); var applySections = parseResult.GetValue(applySectionsOpt); if (!File.Exists(inputPath)) { Console.Error.WriteLine($"Input file not found: {inputPath}"); return; } if (!File.Exists(templatePath)) { Console.Error.WriteLine($"Template file not found: {templatePath}"); return; } // Create output as a copy of the source File.Copy(inputPath, outputPath, overwrite: true); using var output = WordprocessingDocument.Open(outputPath, true); using var template = WordprocessingDocument.Open(templatePath, false); var outputMain = output.MainDocumentPart; var templateMain = template.MainDocumentPart; if (outputMain == null || templateMain == null) { Console.Error.WriteLine("Invalid document: missing main document part."); return; } int appliedCount = 0; if (applyStyles) { CopyStyles(templateMain, outputMain); appliedCount++; Console.WriteLine(" Applied: styles"); } if (applyTheme) { CopyTheme(templateMain, outputMain); appliedCount++; Console.WriteLine(" Applied: theme"); } if (applyNumbering) { CopyNumbering(templateMain, outputMain); appliedCount++; Console.WriteLine(" Applied: numbering"); } if (applyHeadersFooters) { CopyHeadersAndFooters(templateMain, outputMain); appliedCount++; Console.WriteLine(" Applied: headers/footers"); } if (applySections) { CopySectionProperties(templateMain, outputMain); appliedCount++; Console.WriteLine(" Applied: section properties"); } outputMain.Document.Save(); Console.WriteLine($"Applied {appliedCount} formatting component(s) from template to {outputPath}"); }); return cmd; } /// /// Replaces the output's StyleDefinitionsPart with the template's version. /// private static void CopyStyles(MainDocumentPart template, MainDocumentPart output) { var templateStyles = template.StyleDefinitionsPart; if (templateStyles == null) return; if (output.StyleDefinitionsPart != null) output.DeletePart(output.StyleDefinitionsPart); var newStylesPart = output.AddNewPart(); using var stream = templateStyles.GetStream(FileMode.Open, FileAccess.Read); newStylesPart.FeedData(stream); } /// /// Replaces the output's ThemePart with the template's version. /// private static void CopyTheme(MainDocumentPart template, MainDocumentPart output) { var templateTheme = template.ThemePart; if (templateTheme == null) return; if (output.ThemePart != null) output.DeletePart(output.ThemePart); var newThemePart = output.AddNewPart(); using var stream = templateTheme.GetStream(FileMode.Open, FileAccess.Read); newThemePart.FeedData(stream); } /// /// Copies numbering definitions from template, remapping numbering IDs /// referenced in the output document's paragraphs. /// private static void CopyNumbering(MainDocumentPart template, MainDocumentPart output) { var templateNumbering = template.NumberingDefinitionsPart; if (templateNumbering == null) return; var referencedNumIds = new HashSet(); var body = output.Document.Body; if (body != null) { foreach (var numId in body.Descendants()) { if (numId.Val?.Value != null) referencedNumIds.Add(numId.Val.Value.ToString()); } } if (output.NumberingDefinitionsPart != null) output.DeletePart(output.NumberingDefinitionsPart); var newNumberingPart = output.AddNewPart(); using var stream = templateNumbering.GetStream(FileMode.Open, FileAccess.Read); newNumberingPart.FeedData(stream); if (referencedNumIds.Count > 0) { Console.WriteLine($" Note: {referencedNumIds.Count} numbering reference(s) in document content mapped to template definitions."); } } /// /// Copies headers and footers from the template, remapping relationship IDs. /// private static void CopyHeadersAndFooters(MainDocumentPart template, MainDocumentPart output) { var outputBody = output.Document.Body; if (outputBody == null) return; // Remove existing header/footer parts from output foreach (var hp in output.HeaderParts.ToList()) output.DeletePart(hp); foreach (var fp in output.FooterParts.ToList()) output.DeletePart(fp); // Remove existing header/footer references from all section properties foreach (var sectPr in outputBody.Descendants()) { foreach (var hr in sectPr.Elements().ToList()) hr.Remove(); foreach (var fr in sectPr.Elements().ToList()) fr.Remove(); } var templateBody = template.Document?.Body; if (templateBody == null) return; var templateFinalSectPr = templateBody.Descendants().LastOrDefault(); if (templateFinalSectPr == null) return; var outputFinalSectPr = outputBody.Descendants().LastOrDefault(); if (outputFinalSectPr == null) { outputFinalSectPr = new SectionProperties(); outputBody.Append(outputFinalSectPr); } // Copy headers foreach (var headerRef in templateFinalSectPr.Elements()) { var templateHeaderPart = template.GetPartById(headerRef.Id!) as HeaderPart; if (templateHeaderPart == null) continue; var newHeaderPart = output.AddNewPart(); using (var stream = templateHeaderPart.GetStream(FileMode.Open, FileAccess.Read)) { newHeaderPart.FeedData(stream); } CopyPartRelationships(templateHeaderPart, newHeaderPart); var newRefId = output.GetIdOfPart(newHeaderPart); outputFinalSectPr.InsertAt(new HeaderReference { Type = headerRef.Type, Id = newRefId }, 0); } // Copy footers foreach (var footerRef in templateFinalSectPr.Elements()) { var templateFooterPart = template.GetPartById(footerRef.Id!) as FooterPart; if (templateFooterPart == null) continue; var newFooterPart = output.AddNewPart(); using (var stream = templateFooterPart.GetStream(FileMode.Open, FileAccess.Read)) { newFooterPart.FeedData(stream); } CopyPartRelationships(templateFooterPart, newFooterPart); var newRefId = output.GetIdOfPart(newFooterPart); var lastHeaderRef = outputFinalSectPr.Elements().LastOrDefault(); if (lastHeaderRef != null) lastHeaderRef.InsertAfterSelf(new FooterReference { Type = footerRef.Type, Id = newRefId }); else outputFinalSectPr.InsertAt(new FooterReference { Type = footerRef.Type, Id = newRefId }, 0); } } /// /// Copies sub-relationships (images, etc.) from a source part to a target part. /// private static void CopyPartRelationships(OpenXmlPart source, OpenXmlPart target) { foreach (var rel in source.ExternalRelationships) { target.AddExternalRelationship(rel.RelationshipType, rel.Uri, rel.Id); } foreach (var childPart in source.Parts) { try { var contentType = childPart.OpenXmlPart.ContentType; if (contentType.StartsWith("image/")) { var newChild = target.AddNewPart(contentType, childPart.RelationshipId); using var stream = childPart.OpenXmlPart.GetStream(FileMode.Open, FileAccess.Read); newChild.FeedData(stream); } } catch (Exception ex) { Console.Error.WriteLine($"[WARN] Skipped non-image embedded part: {ex.Message}"); } } } /// /// Copies page size, margins, columns, and document grid from template section properties. /// private static void CopySectionProperties(MainDocumentPart template, MainDocumentPart output) { var templateBody = template.Document?.Body; var outputBody = output.Document?.Body; if (templateBody == null || outputBody == null) return; var templateSectPr = templateBody.Descendants().LastOrDefault(); if (templateSectPr == null) return; var outputSectPr = outputBody.Descendants().LastOrDefault(); if (outputSectPr == null) { outputSectPr = new SectionProperties(); outputBody.Append(outputSectPr); } CopyChildElement(templateSectPr, outputSectPr); CopyChildElement(templateSectPr, outputSectPr); CopyChildElement(templateSectPr, outputSectPr); CopyChildElement(templateSectPr, outputSectPr); CopyChildElement(templateSectPr, outputSectPr); } private static void CopyChildElement(SectionProperties source, SectionProperties target) where T : OpenXmlElement { var sourceElement = source.GetFirstChild(); if (sourceElement == null) return; var existing = target.GetFirstChild(); existing?.Remove(); target.Append((T)sourceElement.CloneNode(true)); } }