using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; namespace MiniMaxAIDocx.Core.Samples; /// /// Reference implementations for field codes and Table of Contents (TOC). /// /// KEY CONCEPTS: /// - SimpleField: single-element shorthand, e.g. <w:fldSimple w:instr="PAGE"/> /// - Complex field: three FieldChar elements (Begin / Separate / End) with FieldCode between them. /// Word always writes complex fields; SimpleField is only used for trivial cases. /// - TOC is a structured document tag (SdtBlock) wrapping a complex field. /// - UpdateFieldsOnOpen tells Word to recalculate all fields when opening. /// public static class FieldAndTocSamples { // ────────────────────────────────────────────── // 1. InsertToc — TOC levels 1-3 inside SdtBlock // ────────────────────────────────────────────── /// /// Inserts a Table of Contents covering heading levels 1-3. /// Uses an SdtBlock wrapper with a complex field code: /// TOC \o "1-3" \h \z \u /// /// Switches: /// \o "1-3" — outline levels 1-3 /// \h — hyperlinks /// \z — hide tab leaders / page numbers in Web Layout /// \u — use applied paragraph outline level /// public static SdtBlock InsertToc(Body body) { var sdtBlock = new SdtBlock(); // SdtProperties — mark as TOC var sdtPr = new SdtProperties(); sdtPr.Append(new SdtContentDocPartObject( new DocPartGallery { Val = "Table of Contents" }, new DocPartUnique())); sdtBlock.Append(sdtPr); // SdtContent — contains the field code paragraph(s) var sdtContent = new SdtContentBlock(); // TOC title paragraph var titlePara = new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }), new Run(new Text("Table of Contents"))); sdtContent.Append(titlePara); // Complex field paragraph for TOC var fieldPara = new Paragraph(); InsertComplexFieldInline(fieldPara, " TOC \\o \"1-3\" \\h \\z \\u "); sdtContent.Append(fieldPara); sdtBlock.Append(sdtContent); body.Append(sdtBlock); return sdtBlock; } // ────────────────────────────────────────────── // 2. InsertTocWithCustomLevels — TOC 1-4 levels // ────────────────────────────────────────────── /// /// Inserts a TOC covering heading levels 1-4. /// Identical structure to but with "\o 1-4". /// public static SdtBlock InsertTocWithCustomLevels(Body body) { var sdtBlock = new SdtBlock(); var sdtPr = new SdtProperties(); sdtPr.Append(new SdtContentDocPartObject( new DocPartGallery { Val = "Table of Contents" }, new DocPartUnique())); sdtBlock.Append(sdtPr); var sdtContent = new SdtContentBlock(); var titlePara = new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }), new Run(new Text("Table of Contents"))); sdtContent.Append(titlePara); // 1-4 levels instead of 1-3 var fieldPara = new Paragraph(); InsertComplexFieldInline(fieldPara, " TOC \\o \"1-4\" \\h \\z \\u "); sdtContent.Append(fieldPara); sdtBlock.Append(sdtContent); body.Append(sdtBlock); return sdtBlock; } // ────────────────────────────────────────────── // 3. InsertSimpleField — PAGE, NUMPAGES, DATE, etc. // ────────────────────────────────────────────── /// /// Inserts a SimpleField element into a paragraph. /// /// SimpleField is the compact form: <w:fldSimple w:instr=" PAGE "><w:r>...</w:r></w:fldSimple> /// /// Common instructions: "PAGE", "NUMPAGES", "DATE", "TIME", "FILENAME". /// The run inside is the cached display value; Word recalculates on open. /// public static SimpleField InsertSimpleField(Paragraph para, string instruction) { var simpleField = new SimpleField { Instruction = $" {instruction} " }; // Cached display value — Word replaces this on recalculation simpleField.Append(new Run( new RunProperties(new NoProof()), new Text("«" + instruction + "»"))); para.Append(simpleField); return simpleField; } // ────────────────────────────────────────────── // 4. InsertComplexField — Begin/Separate/End // ────────────────────────────────────────────── /// /// Inserts a complex field into a paragraph using the FieldChar Begin/Separate/End pattern. /// /// Structure: /// Run1: FieldChar(Begin) + FieldCode(" PAGE ") /// Run2: FieldChar(Separate) /// Run3: Text("1") ← cached display value /// Run4: FieldChar(End) /// /// Use complex fields when you need dirty flags, lock, or nested fields. /// public static void InsertComplexField(Paragraph para, string instruction) { InsertComplexFieldInline(para, $" {instruction} "); } // ────────────────────────────────────────────── // 5. InsertDateField — DATE with format switch // ────────────────────────────────────────────── /// /// Inserts a DATE field with a format switch: DATE \@ "yyyy-MM-dd" /// /// The \@ switch specifies the date/time picture. /// Common formats: /// \@ "yyyy-MM-dd" → 2026-03-22 /// \@ "MMMM d, yyyy" → March 22, 2026 /// \@ "M/d/yyyy h:mm am/pm" → 3/22/2026 2:30 PM /// public static void InsertDateField(Paragraph para, string format) { // Field instruction with date-time picture switch string instruction = $" DATE \\@ \"{format}\" "; InsertComplexFieldInline(para, instruction); } // ────────────────────────────────────────────── // 6. InsertCrossReference — REF field // ────────────────────────────────────────────── /// /// Inserts a REF cross-reference field that refers to a bookmark. /// /// Instruction: REF bookmarkName \h /// \h — creates a hyperlink to the bookmark /// \p — inserts "above" or "below" relative position /// \n — inserts paragraph number of the bookmark /// public static void InsertCrossReference(Paragraph para, string bookmarkName) { string instruction = $" REF {bookmarkName} \\h "; InsertComplexFieldInline(para, instruction); } // ────────────────────────────────────────────── // 7. InsertSequenceField — SEQ for numbering // ────────────────────────────────────────────── /// /// Inserts a SEQ (sequence) field for auto-numbering figures, tables, etc. /// /// Usage pattern for "Figure 1": /// 1. Append a run with text "Figure " to the paragraph /// 2. Call InsertSequenceField(para, "Figure") /// /// Usage pattern for "Table 1": /// 1. Append a run with text "Table " to the paragraph /// 2. Call InsertSequenceField(para, "Table") /// /// Each unique seqName maintains its own counter across the document. /// public static void InsertSequenceField(Paragraph para, string seqName) { string instruction = $" SEQ {seqName} \\* ARABIC "; InsertComplexFieldInline(para, instruction); } // ────────────────────────────────────────────── // 8. InsertMergeField — MERGEFIELD for mail merge // ────────────────────────────────────────────── /// /// Inserts a MERGEFIELD for mail merge scenarios. /// /// Instruction: MERGEFIELD fieldName \* MERGEFORMAT /// \* MERGEFORMAT — preserves formatting applied to the field result /// \b "text" — text before if field is non-empty /// \f "text" — text after if field is non-empty /// /// The cached display shows «fieldName» as a placeholder. /// public static void InsertMergeField(Paragraph para, string fieldName) { string instruction = $" MERGEFIELD {fieldName} \\* MERGEFORMAT "; // Begin para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Begin })); // Field code para.Append(new Run( new FieldCode(instruction) { Space = SpaceProcessingModeValues.Preserve })); // Separate para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Separate })); // Cached value — shows merge field placeholder para.Append(new Run( new RunProperties(new NoProof()), new Text($"\u00AB{fieldName}\u00BB") { Space = SpaceProcessingModeValues.Preserve })); // End para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.End })); } // ────────────────────────────────────────────── // 9. InsertConditionalField — IF field // ────────────────────────────────────────────── /// /// Inserts an IF conditional field. /// /// Syntax: IF expression1 operator expression2 "true-text" "false-text" /// Example: IF { MERGEFIELD Gender } = "Male" "Mr." "Ms." /// /// This example checks if MERGEFIELD Amount > 1000 and displays different text. /// Nested fields (MERGEFIELD inside IF) require nested Begin/End pairs. /// public static void InsertConditionalField(Paragraph para) { // Outer IF field Begin para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Begin })); para.Append(new Run( new FieldCode(" IF ") { Space = SpaceProcessingModeValues.Preserve })); // Nested MERGEFIELD inside the IF condition para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Begin })); para.Append(new Run( new FieldCode(" MERGEFIELD Amount ") { Space = SpaceProcessingModeValues.Preserve })); para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Separate })); para.Append(new Run( new Text("0") { Space = SpaceProcessingModeValues.Preserve })); para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.End })); // Continuation of IF instruction para.Append(new Run( new FieldCode(" > \"1000\" \"High Value\" \"Standard\" ") { Space = SpaceProcessingModeValues.Preserve })); // Separate — cached result para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Separate })); para.Append(new Run( new RunProperties(new NoProof()), new Text("Standard") { Space = SpaceProcessingModeValues.Preserve })); // End para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.End })); } // ────────────────────────────────────────────── // 10. InsertStyleRef — STYLEREF for running headers // ────────────────────────────────────────────── /// /// Inserts a STYLEREF field, commonly used in headers/footers /// to display the current chapter or section title. /// /// Instruction: STYLEREF "Heading 1" /// Displays the text of the nearest paragraph with style "Heading 1". /// \l — search from bottom of page up (for last instance on page) /// \n — insert the paragraph number, not text /// public static void InsertStyleRef(Paragraph para) { string instruction = " STYLEREF \"Heading 1\" "; InsertComplexFieldInline(para, instruction); } // ────────────────────────────────────────────── // 11. EnableUpdateFieldsOnOpen // ────────────────────────────────────────────── /// /// Sets the UpdateFieldsOnOpen property so Word recalculates /// all fields (PAGE, TOC, SEQ, etc.) when the document is opened. /// /// Without this, TOC and cross-references show stale cached values /// until the user manually presses Ctrl+A, F9 to update. /// public static void EnableUpdateFieldsOnOpen(DocumentSettingsPart settingsPart) { settingsPart.Settings ??= new Settings(); var existing = settingsPart.Settings.GetFirstChild(); if (existing != null) { existing.Val = true; } else { settingsPart.Settings.Append(new UpdateFieldsOnOpen { Val = true }); } settingsPart.Settings.Save(); } // ────────────────────────────────────────────── // 12. CreateTocStyles — TOC1/2/3 with tab leaders // ────────────────────────────────────────────── /// /// Creates TOC1, TOC2, TOC3 paragraph styles with right-aligned tab stops /// and dot leaders (the "....." between entry text and page number). /// /// Each TOC level is indented further: /// TOC1 — 0 indent /// TOC2 — 240 twips (1/6 inch) /// TOC3 — 480 twips (1/3 inch) /// /// Tab leader: dot-filled right tab at 9360 twips (6.5 inches for letter paper). /// public static void CreateTocStyles(StyleDefinitionsPart stylesPart) { stylesPart.Styles ??= new Styles(); string[] tocStyleIds = ["TOC1", "TOC2", "TOC3"]; string[] tocStyleNames = ["toc 1", "toc 2", "toc 3"]; int[] indents = [0, 240, 480]; // twips // Right tab position: 6.5 inches = 9360 twips (standard for US Letter) const int tabPosition = 9360; for (int i = 0; i < tocStyleIds.Length; i++) { var style = new Style { Type = StyleValues.Paragraph, StyleId = tocStyleIds[i], CustomStyle = false }; style.Append(new StyleName { Val = tocStyleNames[i] }); style.Append(new BasedOn { Val = "Normal" }); style.Append(new NextParagraphStyle { Val = "Normal" }); style.Append(new UIPriority { Val = 39 }); var pPr = new StyleParagraphProperties(); // Indentation for nested levels if (indents[i] > 0) { pPr.Append(new Indentation { Left = indents[i].ToString() }); } // Spacing: no space after for compact TOC pPr.Append(new SpacingBetweenLines { After = "0", Line = "276", LineRule = LineSpacingRuleValues.Auto }); // Right-aligned tab with dot leader var tabs = new Tabs(); tabs.Append(new TabStop { Val = TabStopValues.Right, Leader = TabStopLeaderCharValues.Dot, Position = tabPosition }); pPr.Append(tabs); style.Append(pPr); stylesPart.Styles.Append(style); } stylesPart.Styles.Save(); } // ────────────────────────────────────────────── // 13. CreateMixedTocStructure — Real-world TOC // ────────────────────────────────────────────── /// /// Real-world TOC structure: Mixed SDT block + static entries + field code. /// /// IMPORTANT: Most templates do NOT have a clean TOC field code alone. /// Instead, they contain: /// 1. An SDT (Structured Document Tag) wrapper with alias "TOC" /// 2. Inside the SDT: a field code BEGIN + SEPARATE + static example entries + END /// 3. The static entries are placeholder text (e.g., "第1章 绪论...........1") /// that Word replaces when user presses "Update Fields" /// /// When applying a template (Scenario C), you should: /// - KEEP the entire SDT block from the template (don't rebuild it) /// - DO NOT replace static entries with programmatic content /// - The entries will auto-update when the user opens in Word and updates fields /// - If you must update entries programmatically, replace the content INSIDE /// the SDT between fldChar separate and fldChar end /// /// Common mistake: Treating TOC as pure field code and rebuilding it from scratch, /// which destroys the SDT wrapper and breaks Word's "Update Table" functionality. /// public static void CreateMixedTocStructure(string outputPath) { using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document); var mainPart = doc.AddMainDocumentPart(); mainPart.Document = new Document(); var body = new Body(); mainPart.Document.Append(body); // Add styles part with TOC styles var stylesPart = mainPart.AddNewPart(); CreateTocStyles(stylesPart); // ─── SDT Block wrapping the entire TOC ─── var sdtBlock = new SdtBlock(); // SDT Properties: alias "TOC", tag, and DocPartGallery var sdtPr = new SdtProperties(); sdtPr.Append(new SdtAlias { Val = "TOC" }); sdtPr.Append(new Tag { Val = "TOC" }); sdtPr.Append(new SdtContentDocPartObject( new DocPartGallery { Val = "Table of Contents" }, new DocPartUnique())); sdtBlock.Append(sdtPr); // SDT Content: field code + static entries var sdtContent = new SdtContentBlock(); // ─── TOC title paragraph ─── var titlePara = new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }), new Run(new Text("目 录"))); sdtContent.Append(titlePara); // ─── Field code BEGIN paragraph ─── var fieldBeginPara = new Paragraph(); // fldChar Begin fieldBeginPara.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Begin })); // instrText: TOC \o "1-3" \h \z \u fieldBeginPara.Append(new Run( new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve })); // fldChar Separate fieldBeginPara.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Separate })); sdtContent.Append(fieldBeginPara); // ─── Static placeholder entries (TOC1/TOC2/TOC3) ─── // These are the example entries that Word will replace when user clicks "Update Table". // In real templates, these show example chapter titles with dot leaders and page numbers. // TOC level 1 entry: "第1章 绪论...........1" sdtContent.Append(CreateStaticTocEntry("TOC1", "第1章 绪论", "1")); // TOC level 2 entry: "1.1 研究背景...........1" sdtContent.Append(CreateStaticTocEntry("TOC2", "1.1 研究背景", "1")); // TOC level 2 entry: "1.2 研究目的...........2" sdtContent.Append(CreateStaticTocEntry("TOC2", "1.2 研究目的", "2")); // TOC level 1 entry: "第2章 文献综述...........3" sdtContent.Append(CreateStaticTocEntry("TOC1", "第2章 文献综述", "3")); // TOC level 2 entry: "2.1 国内研究现状...........3" sdtContent.Append(CreateStaticTocEntry("TOC2", "2.1 国内研究现状", "3")); // TOC level 3 entry: "2.1.1 早期研究...........4" sdtContent.Append(CreateStaticTocEntry("TOC3", "2.1.1 早期研究", "4")); // TOC level 1 entry: "第3章 研究方法...........5" sdtContent.Append(CreateStaticTocEntry("TOC1", "第3章 研究方法", "5")); // ─── Field code END paragraph ─── var fieldEndPara = new Paragraph(); fieldEndPara.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.End })); sdtContent.Append(fieldEndPara); sdtBlock.Append(sdtContent); body.Append(sdtBlock); // ─── Actual heading paragraphs (what the TOC references) ─── body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }), new Run(new Text("第1章 绪论")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }), new Run(new Text("1.1 研究背景")))); body.Append(new Paragraph( new Run(new Text("本研究旨在探讨……")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }), new Run(new Text("1.2 研究目的")))); body.Append(new Paragraph( new Run(new Text("研究目的包括……")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }), new Run(new Text("第2章 文献综述")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }), new Run(new Text("2.1 国内研究现状")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading3" }), new Run(new Text("2.1.1 早期研究")))); body.Append(new Paragraph( new Run(new Text("早期研究表明……")))); body.Append(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }), new Run(new Text("第3章 研究方法")))); body.Append(new Paragraph( new Run(new Text("本章介绍研究方法……")))); // ─── Enable UpdateFieldsOnOpen so TOC auto-refreshes ─── var settingsPart = mainPart.AddNewPart(); EnableUpdateFieldsOnOpen(settingsPart); mainPart.Document.Save(); } /// /// Helper: creates a single static TOC entry paragraph with style, text, tab leader, and page number. /// This mirrors what Word generates inside a TOC SDT block. /// private static Paragraph CreateStaticTocEntry(string tocStyleId, string entryText, string pageNumber) { var para = new Paragraph(); // Paragraph properties: TOC style + right-aligned tab with dot leader var pPr = new ParagraphProperties(); pPr.Append(new ParagraphStyleId { Val = tocStyleId }); para.Append(pPr); // Run with entry text para.Append(new Run( new RunProperties(new NoProof()), new Text(entryText) { Space = SpaceProcessingModeValues.Preserve })); // Tab character (creates the dot leader between text and page number) para.Append(new Run(new TabChar())); // Page number para.Append(new Run( new RunProperties(new NoProof()), new Text(pageNumber))); return para; } // ────────────────────────────────────────────── // Private helper: insert complex field inline // ────────────────────────────────────────────── /// /// Shared helper that appends Begin / FieldCode / Separate / CachedValue / End /// runs to a paragraph. /// private static void InsertComplexFieldInline(Paragraph para, string instruction) { // Run 1: FieldChar Begin para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Begin })); // Run 2: FieldCode (the instruction text) para.Append(new Run( new FieldCode(instruction) { Space = SpaceProcessingModeValues.Preserve })); // Run 3: FieldChar Separate para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.Separate })); // Run 4: Cached display value (placeholder until Word recalculates) para.Append(new Run( new RunProperties(new NoProof()), new Text("1") { Space = SpaceProcessingModeValues.Preserve })); // Run 5: FieldChar End para.Append(new Run( new FieldChar { FieldCharType = FieldCharValues.End })); } }