using System.CommandLine;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using MiniMaxAIDocx.Core.OpenXml;
using MiniMaxAIDocx.Core.Typography;
namespace MiniMaxAIDocx.Core.Commands;
///
/// Scenario A: Create a new DOCX document from scratch with proper styles, sections,
/// headers/footers, and typography defaults.
///
public static class CreateCommand
{
public static Command Create()
{
var outputOption = new Option("--output") { Description = "Output DOCX file path", Required = true };
var typeOption = new Option("--type") { Description = "Document type: report, letter, memo, academic" };
typeOption.DefaultValueFactory = _ => "report";
var titleOption = new Option("--title") { Description = "Document title" };
var authorOption = new Option("--author") { Description = "Document author" };
var pageSizeOption = new Option("--page-size") { Description = "Page size: letter, a4, legal, a3" };
pageSizeOption.DefaultValueFactory = _ => "letter";
var marginsOption = new Option("--margins") { Description = "Margin preset: standard, narrow, wide" };
marginsOption.DefaultValueFactory = _ => "standard";
var headerTextOption = new Option("--header") { Description = "Header text" };
var footerTextOption = new Option("--footer") { Description = "Footer text" };
var pageNumbersOption = new Option("--page-numbers") { Description = "Add page numbers in footer" };
var tocOption = new Option("--toc") { Description = "Insert table of contents placeholder" };
var contentJsonOption = new Option("--content-json") { Description = "Path to JSON file describing document content" };
var cmd = new Command("create", "Create a new DOCX document from scratch")
{
outputOption, typeOption, titleOption, authorOption, pageSizeOption,
marginsOption, headerTextOption, footerTextOption, pageNumbersOption,
tocOption, contentJsonOption
};
cmd.SetAction((parseResult) =>
{
var output = parseResult.GetValue(outputOption)!;
var docType = parseResult.GetValue(typeOption) ?? "report";
var title = parseResult.GetValue(titleOption);
var author = parseResult.GetValue(authorOption);
var pageSizeName = parseResult.GetValue(pageSizeOption) ?? "letter";
var marginsName = parseResult.GetValue(marginsOption) ?? "standard";
var headerText = parseResult.GetValue(headerTextOption);
var footerText = parseResult.GetValue(footerTextOption);
var pageNumbers = parseResult.GetValue(pageNumbersOption);
var tocPlaceholder = parseResult.GetValue(tocOption);
var contentJson = parseResult.GetValue(contentJsonOption);
var fontConfig = GetFontConfig(docType);
var pageSize = GetPageSizeConfig(pageSizeName);
var margins = GetMargins(marginsName);
using var doc = WordprocessingDocument.Create(output, WordprocessingDocumentType.Document);
var mainPart = doc.AddMainDocumentPart();
mainPart.Document = new Document(new Body());
var body = mainPart.Document.Body!;
// Add styles part with defaults
AddDefaultStyles(mainPart, fontConfig);
// Add section properties (page size, margins)
var sectPr = new SectionProperties();
sectPr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize
{
Width = (UInt32Value)(uint)pageSize.WidthDxa,
Height = (UInt32Value)(uint)pageSize.HeightDxa
});
sectPr.Append(new PageMargin
{
Top = margins.TopDxa,
Bottom = margins.BottomDxa,
Left = (UInt32Value)(uint)margins.LeftDxa,
Right = (UInt32Value)(uint)margins.RightDxa
});
// Add header if requested
if (!string.IsNullOrEmpty(headerText))
{
var headerPart = mainPart.AddNewPart();
headerPart.Header = new Header(
new Paragraph(new Run(new Text(headerText))));
var headerRefId = mainPart.GetIdOfPart(headerPart);
sectPr.Append(new HeaderReference
{
Type = HeaderFooterValues.Default,
Id = headerRefId
});
}
// Add footer if requested
if (!string.IsNullOrEmpty(footerText) || pageNumbers)
{
var footerPart = mainPart.AddNewPart();
var footerParagraph = new Paragraph();
if (!string.IsNullOrEmpty(footerText))
{
footerParagraph.Append(new Run(new Text(footerText)));
}
if (pageNumbers)
{
if (!string.IsNullOrEmpty(footerText))
footerParagraph.Append(new Run(new Text(" — ") { Space = SpaceProcessingModeValues.Preserve }));
footerParagraph.Append(new Run(
new FieldChar { FieldCharType = FieldCharValues.Begin }));
footerParagraph.Append(new Run(
new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
footerParagraph.Append(new Run(
new FieldChar { FieldCharType = FieldCharValues.End }));
}
footerPart.Footer = new Footer(footerParagraph);
var footerRefId = mainPart.GetIdOfPart(footerPart);
sectPr.Append(new FooterReference
{
Type = HeaderFooterValues.Default,
Id = footerRefId
});
}
// Title
if (!string.IsNullOrEmpty(title))
{
var titlePara = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Title" }),
new Run(new Text(title)));
body.Append(titlePara);
}
// Author subtitle
if (!string.IsNullOrEmpty(author))
{
var authorPara = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Subtitle" }),
new Run(new Text(author)));
body.Append(authorPara);
}
// TOC placeholder
if (tocPlaceholder)
{
body.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }),
new Run(new Text("Table of Contents"))));
// Insert TOC field
var tocPara = new Paragraph();
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
tocPara.Append(new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }));
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }));
tocPara.Append(new Run(new Text("Update this field to generate table of contents.")));
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
body.Append(tocPara);
// Page break after TOC
body.Append(new Paragraph(new Run(new Break { Type = BreakValues.Page })));
}
// Content from JSON (if provided)
if (!string.IsNullOrEmpty(contentJson) && File.Exists(contentJson))
{
var jsonContent = File.ReadAllText(contentJson);
AddContentFromJson(body, jsonContent, fontConfig);
}
// Ensure body has at least one paragraph
if (!body.Elements().Any())
{
body.Append(new Paragraph());
}
// sectPr must be the last child of body
body.Append(sectPr);
mainPart.Document.Save();
Console.WriteLine($"Created {docType} document: {output}");
});
return cmd;
}
private static FontConfig GetFontConfig(string docType) => docType.ToLowerInvariant() switch
{
"letter" => FontDefaults.Letter,
"memo" => FontDefaults.Memo,
"academic" => FontDefaults.Academic,
_ => FontDefaults.Report,
};
private static Typography.PageSize GetPageSizeConfig(string name) => name.ToLowerInvariant() switch
{
"a4" => PageSizes.A4,
"legal" => PageSizes.Legal,
"a3" => PageSizes.A3,
_ => PageSizes.Letter,
};
private static MarginConfig GetMargins(string name) => name.ToLowerInvariant() switch
{
"narrow" => PageSizes.NarrowMargins,
"wide" => PageSizes.WideMargins,
_ => PageSizes.StandardMargins,
};
private static void AddDefaultStyles(MainDocumentPart mainPart, FontConfig fontConfig)
{
var stylesPart = mainPart.AddNewPart();
var styles = new Styles();
// Default run properties
var defaultRPr = new StyleRunProperties(
new RunFonts { Ascii = fontConfig.BodyFont, HighAnsi = fontConfig.BodyFont },
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize) },
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize) });
// Normal style
styles.Append(new Style(
new StyleName { Val = "Normal" },
new PrimaryStyle(),
defaultRPr)
{ Type = StyleValues.Paragraph, StyleId = "Normal", Default = true });
// Heading styles 1-6
double[] headingSizes = [fontConfig.Heading1Size, fontConfig.Heading2Size, fontConfig.Heading3Size,
fontConfig.Heading4Size, fontConfig.Heading5Size, fontConfig.Heading6Size];
for (int i = 0; i < 6; i++)
{
var level = i + 1;
var headingStyle = new Style(
new StyleName { Val = $"heading {level}" },
new BasedOn { Val = "Normal" },
new NextParagraphStyle { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new KeepNext(),
new KeepLines(),
new SpacingBetweenLines { Before = "240", After = "120" },
new OutlineLevel { Val = i }),
new StyleRunProperties(
new RunFonts { Ascii = fontConfig.HeadingFont, HighAnsi = fontConfig.HeadingFont },
new FontSize { Val = UnitConverter.FontSizeToSz(headingSizes[i]) },
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(headingSizes[i]) },
new Bold()))
{ Type = StyleValues.Paragraph, StyleId = $"Heading{level}" };
styles.Append(headingStyle);
}
// Title style
styles.Append(new Style(
new StyleName { Val = "Title" },
new BasedOn { Val = "Normal" },
new NextParagraphStyle { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new Justification { Val = JustificationValues.Center },
new SpacingBetweenLines { After = "300" }),
new StyleRunProperties(
new RunFonts { Ascii = fontConfig.HeadingFont, HighAnsi = fontConfig.HeadingFont },
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.Heading1Size + 6) },
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(fontConfig.Heading1Size + 6) }))
{ Type = StyleValues.Paragraph, StyleId = "Title" });
// Subtitle style
styles.Append(new Style(
new StyleName { Val = "Subtitle" },
new BasedOn { Val = "Normal" },
new NextParagraphStyle { Val = "Normal" },
new StyleParagraphProperties(
new Justification { Val = JustificationValues.Center },
new SpacingBetweenLines { After = "200" }),
new StyleRunProperties(
new Color { Val = "5A5A5A" },
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize + 2) }))
{ Type = StyleValues.Paragraph, StyleId = "Subtitle" });
stylesPart.Styles = styles;
stylesPart.Styles.Save();
}
private static void AddContentFromJson(Body body, string jsonContent, FontConfig fontConfig)
{
// Simple JSON content format: array of {type, text, level?}
// e.g. [{"type":"heading","text":"Introduction","level":1},{"type":"paragraph","text":"..."}]
try
{
using var jsonDoc = System.Text.Json.JsonDocument.Parse(jsonContent);
foreach (var element in jsonDoc.RootElement.EnumerateArray())
{
var type = element.GetProperty("type").GetString() ?? "paragraph";
var text = element.GetProperty("text").GetString() ?? "";
switch (type)
{
case "heading":
var level = element.TryGetProperty("level", out var lvl) ? lvl.GetInt32() : 1;
level = Math.Clamp(level, 1, 6);
body.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = $"Heading{level}" }),
new Run(new Text(text))));
break;
case "paragraph":
body.Append(new Paragraph(new Run(new Text(text))));
break;
case "pagebreak":
body.Append(new Paragraph(new Run(new Break { Type = BreakValues.Page })));
break;
}
}
}
catch (System.Text.Json.JsonException ex)
{
Console.Error.WriteLine($"Warning: could not parse content JSON: {ex.Message}");
}
}
}