Files
skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

625 lines
26 KiB
C#

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace MiniMaxAIDocx.Core.Samples;
/// <summary>
/// Reference implementations for field codes and Table of Contents (TOC).
///
/// KEY CONCEPTS:
/// - SimpleField: single-element shorthand, e.g. &lt;w:fldSimple w:instr="PAGE"/&gt;
/// - 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.
/// </summary>
public static class FieldAndTocSamples
{
// ──────────────────────────────────────────────
// 1. InsertToc — TOC levels 1-3 inside SdtBlock
// ──────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// Inserts a TOC covering heading levels 1-4.
/// Identical structure to <see cref="InsertToc"/> but with "\o 1-4".
/// </summary>
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.
// ──────────────────────────────────────────────
/// <summary>
/// Inserts a SimpleField element into a paragraph.
///
/// SimpleField is the compact form: &lt;w:fldSimple w:instr=" PAGE "&gt;&lt;w:r&gt;...&lt;/w:r&gt;&lt;/w:fldSimple&gt;
///
/// Common instructions: "PAGE", "NUMPAGES", "DATE", "TIME", "FILENAME".
/// The run inside is the cached display value; Word recalculates on open.
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
public static void InsertComplexField(Paragraph para, string instruction)
{
InsertComplexFieldInline(para, $" {instruction} ");
}
// ──────────────────────────────────────────────
// 5. InsertDateField — DATE with format switch
// ──────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
public static void InsertCrossReference(Paragraph para, string bookmarkName)
{
string instruction = $" REF {bookmarkName} \\h ";
InsertComplexFieldInline(para, instruction);
}
// ──────────────────────────────────────────────
// 7. InsertSequenceField — SEQ for numbering
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
public static void InsertSequenceField(Paragraph para, string seqName)
{
string instruction = $" SEQ {seqName} \\* ARABIC ";
InsertComplexFieldInline(para, instruction);
}
// ──────────────────────────────────────────────
// 8. InsertMergeField — MERGEFIELD for mail merge
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
public static void InsertStyleRef(Paragraph para)
{
string instruction = " STYLEREF \"Heading 1\" ";
InsertComplexFieldInline(para, instruction);
}
// ──────────────────────────────────────────────
// 11. EnableUpdateFieldsOnOpen
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
public static void EnableUpdateFieldsOnOpen(DocumentSettingsPart settingsPart)
{
settingsPart.Settings ??= new Settings();
var existing = settingsPart.Settings.GetFirstChild<UpdateFieldsOnOpen>();
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
// ──────────────────────────────────────────────
/// <summary>
/// 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).
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
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<StyleDefinitionsPart>();
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<DocumentSettingsPart>();
EnableUpdateFieldsOnOpen(settingsPart);
mainPart.Document.Save();
}
/// <summary>
/// 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.
/// </summary>
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
// ──────────────────────────────────────────────
/// <summary>
/// Shared helper that appends Begin / FieldCode / Separate / CachedValue / End
/// runs to a paragraph.
/// </summary>
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 }));
}
}