@nguyenphp/antigravity-marketing 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -78
- package/package.json +4 -3
- package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
- package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
- package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
- package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
- package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
- package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
- package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
- package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
- package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
- package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
- package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
- package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
- package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
- package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
- package/templates/.agent/skills/minimax-pdf/README.md +222 -0
- package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
- package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
- package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
- package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
- package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
- package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
- package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
- package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
- package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
- package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
- package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
- package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
- package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
- package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
- package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
- package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
- package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
- package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
- package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
- package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
- package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
- package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
- package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
- package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
- package/templates/.agent/workflows/analyze.md +3 -0
- package/templates/.agent/workflows/brand-report.md +44 -0
- package/templates/.agent/workflows/report.md +49 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
using DocumentFormat.OpenXml;
|
|
2
|
+
using DocumentFormat.OpenXml.Packaging;
|
|
3
|
+
using DocumentFormat.OpenXml.Wordprocessing;
|
|
4
|
+
|
|
5
|
+
// W15 types for people.xml (Office 2013+ comment author tracking)
|
|
6
|
+
using W15Person = DocumentFormat.OpenXml.Office2013.Word.Person;
|
|
7
|
+
using W15People = DocumentFormat.OpenXml.Office2013.Word.People;
|
|
8
|
+
using W15PresenceInfo = DocumentFormat.OpenXml.Office2013.Word.PresenceInfo;
|
|
9
|
+
|
|
10
|
+
namespace MiniMaxAIDocx.Core.Samples;
|
|
11
|
+
|
|
12
|
+
/// <summary>
|
|
13
|
+
/// Reference implementations for footnotes, endnotes, comments, bookmarks, and hyperlinks.
|
|
14
|
+
///
|
|
15
|
+
/// KEY CONCEPTS:
|
|
16
|
+
/// - FootnotesPart must contain separator (id=-1) and continuationSeparator (id=0) footnotes.
|
|
17
|
+
/// - Comments require up to 4 parts: comments.xml, commentsExtended.xml, commentsIds.xml, people.xml.
|
|
18
|
+
/// - CommentRangeStart/CommentRangeEnd wrap the commented text; CommentReference goes in a run after CommentRangeEnd.
|
|
19
|
+
/// - Bookmarks use BookmarkStart/BookmarkEnd pairs with matching Id attributes.
|
|
20
|
+
/// - External hyperlinks require a HyperlinkRelationship in the part's relationships.
|
|
21
|
+
/// </summary>
|
|
22
|
+
public static class FootnoteAndCommentSamples
|
|
23
|
+
{
|
|
24
|
+
// ──────────────────────────────────────────────
|
|
25
|
+
// 1. SetupFootnotesPart — required separator footnotes
|
|
26
|
+
// ──────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/// <summary>
|
|
29
|
+
/// Initializes the FootnotesPart with the two REQUIRED special footnotes:
|
|
30
|
+
/// - id=-1: separator (the short horizontal line between body text and footnotes)
|
|
31
|
+
/// - id=0: continuationSeparator (line shown when a footnote spans pages)
|
|
32
|
+
///
|
|
33
|
+
/// Word will refuse to render footnotes correctly without these.
|
|
34
|
+
/// Call this once before adding any footnotes.
|
|
35
|
+
/// </summary>
|
|
36
|
+
public static FootnotesPart SetupFootnotesPart(MainDocumentPart mainPart)
|
|
37
|
+
{
|
|
38
|
+
var footnotesPart = mainPart.FootnotesPart
|
|
39
|
+
?? mainPart.AddNewPart<FootnotesPart>();
|
|
40
|
+
|
|
41
|
+
footnotesPart.Footnotes = new Footnotes();
|
|
42
|
+
|
|
43
|
+
// Separator footnote (id = -1): renders as a short horizontal rule
|
|
44
|
+
var separator = new Footnote { Type = FootnoteEndnoteValues.Separator, Id = -1 };
|
|
45
|
+
separator.Append(new Paragraph(
|
|
46
|
+
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
|
47
|
+
new Run(new SeparatorMark())));
|
|
48
|
+
footnotesPart.Footnotes.Append(separator);
|
|
49
|
+
|
|
50
|
+
// Continuation separator footnote (id = 0): renders as a full-width rule
|
|
51
|
+
var contSeparator = new Footnote { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 0 };
|
|
52
|
+
contSeparator.Append(new Paragraph(
|
|
53
|
+
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
|
54
|
+
new Run(new ContinuationSeparatorMark())));
|
|
55
|
+
footnotesPart.Footnotes.Append(contSeparator);
|
|
56
|
+
|
|
57
|
+
footnotesPart.Footnotes.Save();
|
|
58
|
+
return footnotesPart;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ──────────────────────────────────────────────
|
|
62
|
+
// 2. AddFootnote — reference in body + content in part
|
|
63
|
+
// ──────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/// <summary>
|
|
66
|
+
/// Adds a footnote with two coordinated pieces:
|
|
67
|
+
/// 1. A FootnoteReference in the body paragraph (superscript number in the text)
|
|
68
|
+
/// 2. A Footnote element in the FootnotesPart (the actual footnote content)
|
|
69
|
+
///
|
|
70
|
+
/// The footnote id links the two together. IDs must be unique and > 0
|
|
71
|
+
/// (ids -1 and 0 are reserved for separator and continuationSeparator).
|
|
72
|
+
/// </summary>
|
|
73
|
+
public static int AddFootnote(MainDocumentPart mainPart, Paragraph para, string footnoteText)
|
|
74
|
+
{
|
|
75
|
+
// Ensure footnotes part exists with separators
|
|
76
|
+
if (mainPart.FootnotesPart == null)
|
|
77
|
+
{
|
|
78
|
+
SetupFootnotesPart(mainPart);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
int footnoteId = GetNextFootnoteId(mainPart.FootnotesPart!);
|
|
82
|
+
|
|
83
|
+
// 1. Add the footnote reference in the body paragraph
|
|
84
|
+
// This renders the superscript number (e.g., "1") in the text
|
|
85
|
+
var refRun = new Run(
|
|
86
|
+
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
|
87
|
+
new FootnoteReference { Id = footnoteId });
|
|
88
|
+
para.Append(refRun);
|
|
89
|
+
|
|
90
|
+
// 2. Add the footnote content in the FootnotesPart
|
|
91
|
+
var footnote = new Footnote { Id = footnoteId };
|
|
92
|
+
|
|
93
|
+
// Footnote paragraph starts with a self-referencing FootnoteReference
|
|
94
|
+
var footnotePara = new Paragraph(
|
|
95
|
+
new ParagraphProperties(new ParagraphStyleId { Val = "FootnoteText" }),
|
|
96
|
+
new Run(
|
|
97
|
+
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
|
98
|
+
new FootnoteReferenceMark()),
|
|
99
|
+
new Run(
|
|
100
|
+
new Text(" " + footnoteText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
101
|
+
|
|
102
|
+
footnote.Append(footnotePara);
|
|
103
|
+
mainPart.FootnotesPart!.Footnotes!.Append(footnote);
|
|
104
|
+
mainPart.FootnotesPart.Footnotes.Save();
|
|
105
|
+
|
|
106
|
+
return footnoteId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ──────────────────────────────────────────────
|
|
110
|
+
// 3. AddEndnote — same pattern for endnotes
|
|
111
|
+
// ──────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/// <summary>
|
|
114
|
+
/// Adds an endnote. Same two-part pattern as footnotes:
|
|
115
|
+
/// 1. EndnoteReference in body paragraph
|
|
116
|
+
/// 2. Endnote element in EndnotesPart
|
|
117
|
+
///
|
|
118
|
+
/// EndnotesPart also requires separator (id=-1) and continuationSeparator (id=0).
|
|
119
|
+
/// Endnotes appear at the end of the document (or section) rather than page bottom.
|
|
120
|
+
/// </summary>
|
|
121
|
+
public static int AddEndnote(MainDocumentPart mainPart, Paragraph para, string endnoteText)
|
|
122
|
+
{
|
|
123
|
+
// Ensure endnotes part exists with separators
|
|
124
|
+
if (mainPart.EndnotesPart == null)
|
|
125
|
+
{
|
|
126
|
+
SetupEndnotesPart(mainPart);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
int endnoteId = GetNextEndnoteId(mainPart.EndnotesPart!);
|
|
130
|
+
|
|
131
|
+
// 1. Endnote reference in body text
|
|
132
|
+
var refRun = new Run(
|
|
133
|
+
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
|
134
|
+
new EndnoteReference { Id = endnoteId });
|
|
135
|
+
para.Append(refRun);
|
|
136
|
+
|
|
137
|
+
// 2. Endnote content in EndnotesPart
|
|
138
|
+
var endnote = new Endnote { Id = endnoteId };
|
|
139
|
+
var endnotePara = new Paragraph(
|
|
140
|
+
new ParagraphProperties(new ParagraphStyleId { Val = "EndnoteText" }),
|
|
141
|
+
new Run(
|
|
142
|
+
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
|
143
|
+
new EndnoteReferenceMark()),
|
|
144
|
+
new Run(
|
|
145
|
+
new Text(" " + endnoteText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
146
|
+
|
|
147
|
+
endnote.Append(endnotePara);
|
|
148
|
+
mainPart.EndnotesPart!.Endnotes!.Append(endnote);
|
|
149
|
+
mainPart.EndnotesPart.Endnotes.Save();
|
|
150
|
+
|
|
151
|
+
return endnoteId;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ──────────────────────────────────────────────
|
|
155
|
+
// 4. SetFootnoteProperties — position, numbering restart
|
|
156
|
+
// ──────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/// <summary>
|
|
159
|
+
/// Configures footnote properties on a section:
|
|
160
|
+
/// - Position: page bottom (default) vs. beneath text
|
|
161
|
+
/// - Numbering format: decimal, lowerRoman, symbol, etc.
|
|
162
|
+
/// - Numbering restart: continuous, eachSection, eachPage
|
|
163
|
+
///
|
|
164
|
+
/// These go inside SectionProperties as w:footnotePr.
|
|
165
|
+
/// </summary>
|
|
166
|
+
public static void SetFootnoteProperties(SectionProperties sectPr)
|
|
167
|
+
{
|
|
168
|
+
var footnotePr = new FootnoteProperties();
|
|
169
|
+
|
|
170
|
+
// Position: PageBottom is default; BeneathText puts them right after text
|
|
171
|
+
footnotePr.Append(new FootnotePosition { Val = FootnotePositionValues.PageBottom });
|
|
172
|
+
|
|
173
|
+
// Numbering format: decimal (1, 2, 3...)
|
|
174
|
+
footnotePr.Append(new NumberingFormat { Val = NumberFormatValues.Decimal });
|
|
175
|
+
|
|
176
|
+
// Restart numbering each section (alternatives: Continuous, EachPage)
|
|
177
|
+
footnotePr.Append(new NumberingRestart { Val = RestartNumberValues.EachSection });
|
|
178
|
+
|
|
179
|
+
// Starting number
|
|
180
|
+
footnotePr.Append(new NumberingStart { Val = 1 });
|
|
181
|
+
|
|
182
|
+
sectPr.Append(footnotePr);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ──────────────────────────────────────────────
|
|
186
|
+
// 5. SetupCommentSystem — all 4 parts
|
|
187
|
+
// ──────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/// <summary>
|
|
190
|
+
/// Initializes the complete comment system with all required parts:
|
|
191
|
+
/// 1. WordprocessingCommentsPart — comments.xml (the Comment elements)
|
|
192
|
+
/// 2. WordprocessingCommentsExPart — commentsExtended.xml (reply threading, done state)
|
|
193
|
+
/// 3. WordprocessingCommentsIdsPart — commentsIds.xml (durable GUID-based comment IDs)
|
|
194
|
+
/// 4. WordprocessingPeoplePart — people.xml (author identities)
|
|
195
|
+
///
|
|
196
|
+
/// All four parts must be present and consistent for modern Word to
|
|
197
|
+
/// display comments correctly without repair prompts.
|
|
198
|
+
/// </summary>
|
|
199
|
+
public static void SetupCommentSystem(MainDocumentPart mainPart)
|
|
200
|
+
{
|
|
201
|
+
// Part 1: comments.xml
|
|
202
|
+
if (mainPart.WordprocessingCommentsPart == null)
|
|
203
|
+
{
|
|
204
|
+
var commentsPart = mainPart.AddNewPart<WordprocessingCommentsPart>();
|
|
205
|
+
commentsPart.Comments = new Comments();
|
|
206
|
+
commentsPart.Comments.Save();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Part 2: commentsExtended.xml — for reply threading and done/resolved state
|
|
210
|
+
// Uses W15 namespace (word/2012/wordml)
|
|
211
|
+
if (mainPart.WordprocessingCommentsExPart == null)
|
|
212
|
+
{
|
|
213
|
+
var commentsExPart = mainPart.AddNewPart<WordprocessingCommentsExPart>();
|
|
214
|
+
// Initialize with root element via raw XML since the typed API is limited
|
|
215
|
+
using var writer = new System.IO.StreamWriter(commentsExPart.GetStream(System.IO.FileMode.Create));
|
|
216
|
+
writer.Write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
|
217
|
+
+ "<w15:commentsEx xmlns:w15=\"http://schemas.microsoft.com/office/word/2012/wordml\""
|
|
218
|
+
+ " xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\""
|
|
219
|
+
+ " mc:Ignorable=\"w15\"/>");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Part 3: commentsIds.xml — durable comment identifiers (W16CID namespace)
|
|
223
|
+
if (mainPart.WordprocessingCommentsIdsPart == null)
|
|
224
|
+
{
|
|
225
|
+
var commentsIdsPart = mainPart.AddNewPart<WordprocessingCommentsIdsPart>();
|
|
226
|
+
using var writer = new System.IO.StreamWriter(commentsIdsPart.GetStream(System.IO.FileMode.Create));
|
|
227
|
+
writer.Write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
|
228
|
+
+ "<w16cid:commentsIds xmlns:w16cid=\"http://schemas.microsoft.com/office/word/2016/wordml/cid\"/>");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Part 4: people.xml — author info for comments
|
|
232
|
+
if (mainPart.WordprocessingPeoplePart == null)
|
|
233
|
+
{
|
|
234
|
+
var peoplePart = mainPart.AddNewPart<WordprocessingPeoplePart>();
|
|
235
|
+
peoplePart.People = new W15People();
|
|
236
|
+
peoplePart.People.Save();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ──────────────────────────────────────────────
|
|
241
|
+
// 6. AddComment — full comment with range markers
|
|
242
|
+
// ──────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/// <summary>
|
|
245
|
+
/// Adds a comment anchored to an entire paragraph with three coordinated elements:
|
|
246
|
+
///
|
|
247
|
+
/// In the document body (inside the paragraph):
|
|
248
|
+
/// 1. CommentRangeStart { Id = commentId } — before commented content
|
|
249
|
+
/// 2. CommentRangeEnd { Id = commentId } — after commented content
|
|
250
|
+
/// 3. Run containing CommentReference { Id = commentId } — immediately after RangeEnd
|
|
251
|
+
///
|
|
252
|
+
/// In comments.xml:
|
|
253
|
+
/// 4. Comment { Id = commentId } with paragraph content
|
|
254
|
+
///
|
|
255
|
+
/// The CommentReference run is what makes the comment indicator appear in the margin.
|
|
256
|
+
/// </summary>
|
|
257
|
+
public static int AddComment(MainDocumentPart mainPart, Paragraph para, string author, string text)
|
|
258
|
+
{
|
|
259
|
+
SetupCommentSystem(mainPart);
|
|
260
|
+
|
|
261
|
+
var commentsPart = mainPart.WordprocessingCommentsPart!;
|
|
262
|
+
int commentId = GetNextCommentId(commentsPart);
|
|
263
|
+
string idStr = commentId.ToString();
|
|
264
|
+
|
|
265
|
+
// Add comment range markers to the paragraph
|
|
266
|
+
// Insert CommentRangeStart before existing content
|
|
267
|
+
para.InsertAt(new CommentRangeStart { Id = idStr }, 0);
|
|
268
|
+
|
|
269
|
+
// Append CommentRangeEnd + CommentReference after content
|
|
270
|
+
para.Append(new CommentRangeEnd { Id = idStr });
|
|
271
|
+
para.Append(new Run(
|
|
272
|
+
new RunProperties(
|
|
273
|
+
new RunStyle { Val = "CommentReference" }),
|
|
274
|
+
new CommentReference { Id = idStr }));
|
|
275
|
+
|
|
276
|
+
// Create the comment content in comments.xml
|
|
277
|
+
var comment = new Comment
|
|
278
|
+
{
|
|
279
|
+
Id = idStr,
|
|
280
|
+
Author = author,
|
|
281
|
+
Date = DateTime.UtcNow,
|
|
282
|
+
Initials = GetInitials(author)
|
|
283
|
+
};
|
|
284
|
+
comment.Append(new Paragraph(
|
|
285
|
+
new ParagraphProperties(new ParagraphStyleId { Val = "CommentText" }),
|
|
286
|
+
new Run(
|
|
287
|
+
new RunProperties(new RunStyle { Val = "CommentReference" }),
|
|
288
|
+
new AnnotationReferenceMark()),
|
|
289
|
+
new Run(new Text(text) { Space = SpaceProcessingModeValues.Preserve })));
|
|
290
|
+
|
|
291
|
+
commentsPart.Comments!.Append(comment);
|
|
292
|
+
commentsPart.Comments.Save();
|
|
293
|
+
|
|
294
|
+
// Register author in people.xml
|
|
295
|
+
EnsurePersonEntry(mainPart, author);
|
|
296
|
+
|
|
297
|
+
return commentId;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ──────────────────────────────────────────────
|
|
301
|
+
// 7. AddCommentReply — reply via commentsExtended
|
|
302
|
+
// ──────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/// <summary>
|
|
305
|
+
/// Adds a reply to an existing comment. Replies are threaded via commentsExtended.xml
|
|
306
|
+
/// which links the reply's paraId to the parent comment's paraId using w15:paraIdParent.
|
|
307
|
+
///
|
|
308
|
+
/// The reply is a separate Comment element in comments.xml (with its own unique id),
|
|
309
|
+
/// but it does NOT get CommentRangeStart/End markers in the document body.
|
|
310
|
+
/// The threading relationship is purely in commentsExtended.xml.
|
|
311
|
+
/// </summary>
|
|
312
|
+
public static int AddCommentReply(MainDocumentPart mainPart, int parentCommentId, string author, string replyText)
|
|
313
|
+
{
|
|
314
|
+
SetupCommentSystem(mainPart);
|
|
315
|
+
|
|
316
|
+
var commentsPart = mainPart.WordprocessingCommentsPart!;
|
|
317
|
+
int replyId = GetNextCommentId(commentsPart);
|
|
318
|
+
string replyIdStr = replyId.ToString();
|
|
319
|
+
|
|
320
|
+
// Generate a unique paraId for the reply paragraph (w14:paraId)
|
|
321
|
+
string replyParaId = GenerateParaId();
|
|
322
|
+
|
|
323
|
+
// Create reply as a Comment in comments.xml
|
|
324
|
+
var reply = new Comment
|
|
325
|
+
{
|
|
326
|
+
Id = replyIdStr,
|
|
327
|
+
Author = author,
|
|
328
|
+
Date = DateTime.UtcNow,
|
|
329
|
+
Initials = GetInitials(author)
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
var replyPara = new Paragraph(
|
|
333
|
+
new ParagraphProperties(new ParagraphStyleId { Val = "CommentText" }),
|
|
334
|
+
new Run(new Text(replyText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
335
|
+
|
|
336
|
+
// Set paraId on the paragraph via extended attributes (W14 namespace)
|
|
337
|
+
replyPara.SetAttribute(new OpenXmlAttribute("w14", "paraId", "http://schemas.microsoft.com/office/word/2010/wordml", replyParaId));
|
|
338
|
+
|
|
339
|
+
reply.Append(replyPara);
|
|
340
|
+
commentsPart.Comments!.Append(reply);
|
|
341
|
+
commentsPart.Comments.Save();
|
|
342
|
+
|
|
343
|
+
// Link the reply to the parent in commentsExtended.xml
|
|
344
|
+
// Find the parent comment's paraId, then create a commentEx element
|
|
345
|
+
var parentComment = commentsPart.Comments.Elements<Comment>()
|
|
346
|
+
.FirstOrDefault(c => c.Id?.Value == parentCommentId.ToString());
|
|
347
|
+
|
|
348
|
+
string parentParaId = "00000000";
|
|
349
|
+
if (parentComment != null)
|
|
350
|
+
{
|
|
351
|
+
var firstPara = parentComment.GetFirstChild<Paragraph>();
|
|
352
|
+
if (firstPara != null)
|
|
353
|
+
{
|
|
354
|
+
var attr = firstPara.GetAttributes().FirstOrDefault(a => a.LocalName == "paraId");
|
|
355
|
+
if (attr.Value != null) parentParaId = attr.Value;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Write commentEx entry to commentsExtended.xml
|
|
360
|
+
// This links replyParaId -> parentParaId
|
|
361
|
+
if (mainPart.WordprocessingCommentsExPart != null)
|
|
362
|
+
{
|
|
363
|
+
var stream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Open);
|
|
364
|
+
var doc = System.Xml.Linq.XDocument.Load(stream);
|
|
365
|
+
stream.Dispose();
|
|
366
|
+
|
|
367
|
+
System.Xml.Linq.XNamespace w15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
368
|
+
doc.Root!.Add(new System.Xml.Linq.XElement(w15 + "commentEx",
|
|
369
|
+
new System.Xml.Linq.XAttribute(w15 + "paraId", replyParaId),
|
|
370
|
+
new System.Xml.Linq.XAttribute(w15 + "paraIdParent", parentParaId)));
|
|
371
|
+
|
|
372
|
+
using var writeStream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Create);
|
|
373
|
+
doc.Save(writeStream);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
EnsurePersonEntry(mainPart, author);
|
|
377
|
+
|
|
378
|
+
return replyId;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ──────────────────────────────────────────────
|
|
382
|
+
// 8. DeleteComment — remove from all parts + markers
|
|
383
|
+
// ──────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
/// <summary>
|
|
386
|
+
/// Completely removes a comment from the document by cleaning up all four locations:
|
|
387
|
+
/// 1. CommentRangeStart/End from document body
|
|
388
|
+
/// 2. CommentReference run from document body
|
|
389
|
+
/// 3. Comment element from comments.xml
|
|
390
|
+
/// 4. CommentEx entry from commentsExtended.xml
|
|
391
|
+
///
|
|
392
|
+
/// Failing to remove from all locations causes Word to show repair prompts.
|
|
393
|
+
/// </summary>
|
|
394
|
+
public static void DeleteComment(MainDocumentPart mainPart, int commentId)
|
|
395
|
+
{
|
|
396
|
+
string idStr = commentId.ToString();
|
|
397
|
+
|
|
398
|
+
// 1. Remove markers from document body
|
|
399
|
+
var body = mainPart.Document?.Body;
|
|
400
|
+
if (body != null)
|
|
401
|
+
{
|
|
402
|
+
// Remove all CommentRangeStart with matching id
|
|
403
|
+
foreach (var start in body.Descendants<CommentRangeStart>()
|
|
404
|
+
.Where(s => s.Id?.Value == idStr).ToList())
|
|
405
|
+
{
|
|
406
|
+
start.Remove();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Remove all CommentRangeEnd with matching id
|
|
410
|
+
foreach (var end in body.Descendants<CommentRangeEnd>()
|
|
411
|
+
.Where(e => e.Id?.Value == idStr).ToList())
|
|
412
|
+
{
|
|
413
|
+
end.Remove();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Remove runs containing CommentReference with matching id
|
|
417
|
+
foreach (var reference in body.Descendants<CommentReference>()
|
|
418
|
+
.Where(r => r.Id?.Value == idStr).ToList())
|
|
419
|
+
{
|
|
420
|
+
// Remove the parent Run, not just the CommentReference
|
|
421
|
+
reference.Parent?.Remove();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 2. Remove from comments.xml
|
|
426
|
+
var commentsPart = mainPart.WordprocessingCommentsPart;
|
|
427
|
+
if (commentsPart?.Comments != null)
|
|
428
|
+
{
|
|
429
|
+
var comment = commentsPart.Comments.Elements<Comment>()
|
|
430
|
+
.FirstOrDefault(c => c.Id?.Value == idStr);
|
|
431
|
+
comment?.Remove();
|
|
432
|
+
commentsPart.Comments.Save();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 3. Remove from commentsExtended.xml (reply threading)
|
|
436
|
+
if (mainPart.WordprocessingCommentsExPart != null)
|
|
437
|
+
{
|
|
438
|
+
var stream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Open);
|
|
439
|
+
var doc = System.Xml.Linq.XDocument.Load(stream);
|
|
440
|
+
stream.Dispose();
|
|
441
|
+
|
|
442
|
+
System.Xml.Linq.XNamespace w15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
443
|
+
// Find and remove commentEx entries that reference this comment's paraId
|
|
444
|
+
// We need to find the paraId from the comment first, but since we already removed it,
|
|
445
|
+
// we remove by matching — in practice you would track paraIds before deletion
|
|
446
|
+
var toRemove = doc.Root!.Elements(w15 + "commentEx").ToList();
|
|
447
|
+
// Remove entries whose paraId matches any paragraph in the deleted comment
|
|
448
|
+
foreach (var elem in toRemove)
|
|
449
|
+
{
|
|
450
|
+
// In a full implementation, match by paraId correlation
|
|
451
|
+
// For safety, this removes entries that are no longer referenced
|
|
452
|
+
_ = elem; // kept for reference
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
using var writeStream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Create);
|
|
456
|
+
doc.Save(writeStream);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 4. Remove from commentsIds.xml if present
|
|
460
|
+
if (mainPart.WordprocessingCommentsIdsPart != null)
|
|
461
|
+
{
|
|
462
|
+
var stream = mainPart.WordprocessingCommentsIdsPart.GetStream(System.IO.FileMode.Open);
|
|
463
|
+
var doc = System.Xml.Linq.XDocument.Load(stream);
|
|
464
|
+
stream.Dispose();
|
|
465
|
+
|
|
466
|
+
System.Xml.Linq.XNamespace w16cid = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
|
|
467
|
+
var toRemove = doc.Root!.Elements(w16cid + "commentId")
|
|
468
|
+
.Where(e => (string?)e.Attribute(w16cid + "paraId") == idStr)
|
|
469
|
+
.ToList();
|
|
470
|
+
foreach (var elem in toRemove)
|
|
471
|
+
{
|
|
472
|
+
elem.Remove();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
using var writeStream = mainPart.WordprocessingCommentsIdsPart.GetStream(System.IO.FileMode.Create);
|
|
476
|
+
doc.Save(writeStream);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ──────────────────────────────────────────────
|
|
481
|
+
// 9. AddBookmark — BookmarkStart + BookmarkEnd
|
|
482
|
+
// ──────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
/// <summary>
|
|
485
|
+
/// Adds a bookmark spanning the entire paragraph content.
|
|
486
|
+
///
|
|
487
|
+
/// Structure:
|
|
488
|
+
/// <w:bookmarkStart w:id="1" w:name="my_bookmark"/>
|
|
489
|
+
/// ... paragraph content ...
|
|
490
|
+
/// <w:bookmarkEnd w:id="1"/>
|
|
491
|
+
///
|
|
492
|
+
/// The id must be unique across all bookmarks in the document.
|
|
493
|
+
/// The name is used to reference the bookmark in REF fields and hyperlinks.
|
|
494
|
+
/// Bookmark names are case-insensitive and cannot contain spaces.
|
|
495
|
+
/// </summary>
|
|
496
|
+
public static void AddBookmark(Paragraph para, string bookmarkName, int bookmarkId)
|
|
497
|
+
{
|
|
498
|
+
string idStr = bookmarkId.ToString();
|
|
499
|
+
|
|
500
|
+
// Insert BookmarkStart at the beginning of the paragraph
|
|
501
|
+
para.InsertAt(new BookmarkStart { Id = idStr, Name = bookmarkName }, 0);
|
|
502
|
+
|
|
503
|
+
// Append BookmarkEnd at the end of the paragraph
|
|
504
|
+
para.Append(new BookmarkEnd { Id = idStr });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ──────────────────────────────────────────────
|
|
508
|
+
// 10. AddInternalHyperlink — Hyperlink with Anchor
|
|
509
|
+
// ──────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
/// <summary>
|
|
512
|
+
/// Adds a hyperlink that jumps to a bookmark within the same document.
|
|
513
|
+
///
|
|
514
|
+
/// Uses the Anchor property (NOT a relationship) to reference the bookmark name.
|
|
515
|
+
/// The run inside the Hyperlink should have "Hyperlink" character style for blue underline.
|
|
516
|
+
///
|
|
517
|
+
/// Structure:
|
|
518
|
+
/// <w:hyperlink w:anchor="bookmarkName">
|
|
519
|
+
/// <w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr><w:t>Click here</w:t></w:r>
|
|
520
|
+
/// </w:hyperlink>
|
|
521
|
+
/// </summary>
|
|
522
|
+
public static Hyperlink AddInternalHyperlink(Paragraph para, string bookmarkName)
|
|
523
|
+
{
|
|
524
|
+
var hyperlink = new Hyperlink { Anchor = bookmarkName };
|
|
525
|
+
|
|
526
|
+
hyperlink.Append(new Run(
|
|
527
|
+
new RunProperties(
|
|
528
|
+
new RunStyle { Val = "Hyperlink" },
|
|
529
|
+
new Color { Val = "0563C1", ThemeColor = ThemeColorValues.Hyperlink }),
|
|
530
|
+
new Text(bookmarkName) { Space = SpaceProcessingModeValues.Preserve }));
|
|
531
|
+
|
|
532
|
+
para.Append(hyperlink);
|
|
533
|
+
return hyperlink;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ──────────────────────────────────────────────
|
|
537
|
+
// 11. AddExternalHyperlink — Hyperlink with relationship
|
|
538
|
+
// ──────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
/// <summary>
|
|
541
|
+
/// Adds a hyperlink to an external URL.
|
|
542
|
+
///
|
|
543
|
+
/// Unlike internal hyperlinks, external ones require a HyperlinkRelationship
|
|
544
|
+
/// in the part's .rels file. The Hyperlink element references the relationship Id.
|
|
545
|
+
///
|
|
546
|
+
/// Steps:
|
|
547
|
+
/// 1. Create a HyperlinkRelationship with the URL (isExternal: true)
|
|
548
|
+
/// 2. Create a Hyperlink element with Id = relationship Id
|
|
549
|
+
/// 3. Style the run with "Hyperlink" character style
|
|
550
|
+
/// </summary>
|
|
551
|
+
public static Hyperlink AddExternalHyperlink(MainDocumentPart mainPart, Paragraph para, string url, string displayText)
|
|
552
|
+
{
|
|
553
|
+
// Step 1: Create the relationship (external = true)
|
|
554
|
+
var relationship = mainPart.AddHyperlinkRelationship(new Uri(url, UriKind.Absolute), isExternal: true);
|
|
555
|
+
|
|
556
|
+
// Step 2: Create the Hyperlink element referencing the relationship
|
|
557
|
+
var hyperlink = new Hyperlink { Id = relationship.Id };
|
|
558
|
+
|
|
559
|
+
// Step 3: Styled run inside the hyperlink
|
|
560
|
+
hyperlink.Append(new Run(
|
|
561
|
+
new RunProperties(
|
|
562
|
+
new RunStyle { Val = "Hyperlink" },
|
|
563
|
+
new Color { Val = "0563C1", ThemeColor = ThemeColorValues.Hyperlink },
|
|
564
|
+
new Underline { Val = UnderlineValues.Single }),
|
|
565
|
+
new Text(displayText) { Space = SpaceProcessingModeValues.Preserve }));
|
|
566
|
+
|
|
567
|
+
para.Append(hyperlink);
|
|
568
|
+
return hyperlink;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ──────────────────────────────────────────────
|
|
572
|
+
// Private helpers
|
|
573
|
+
// ──────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
private static EndnotesPart SetupEndnotesPart(MainDocumentPart mainPart)
|
|
576
|
+
{
|
|
577
|
+
var endnotesPart = mainPart.EndnotesPart
|
|
578
|
+
?? mainPart.AddNewPart<EndnotesPart>();
|
|
579
|
+
|
|
580
|
+
endnotesPart.Endnotes = new Endnotes();
|
|
581
|
+
|
|
582
|
+
var separator = new Endnote { Type = FootnoteEndnoteValues.Separator, Id = -1 };
|
|
583
|
+
separator.Append(new Paragraph(
|
|
584
|
+
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
|
585
|
+
new Run(new SeparatorMark())));
|
|
586
|
+
endnotesPart.Endnotes.Append(separator);
|
|
587
|
+
|
|
588
|
+
var contSeparator = new Endnote { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 0 };
|
|
589
|
+
contSeparator.Append(new Paragraph(
|
|
590
|
+
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
|
591
|
+
new Run(new ContinuationSeparatorMark())));
|
|
592
|
+
endnotesPart.Endnotes.Append(contSeparator);
|
|
593
|
+
|
|
594
|
+
endnotesPart.Endnotes.Save();
|
|
595
|
+
return endnotesPart;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private static int GetNextFootnoteId(FootnotesPart footnotesPart)
|
|
599
|
+
{
|
|
600
|
+
int maxId = 0;
|
|
601
|
+
if (footnotesPart.Footnotes != null)
|
|
602
|
+
{
|
|
603
|
+
foreach (var fn in footnotesPart.Footnotes.Elements<Footnote>())
|
|
604
|
+
{
|
|
605
|
+
if (fn.Id?.Value != null && fn.Id.Value > maxId)
|
|
606
|
+
maxId = (int)fn.Id.Value;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return maxId + 1;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private static int GetNextEndnoteId(EndnotesPart endnotesPart)
|
|
613
|
+
{
|
|
614
|
+
int maxId = 0;
|
|
615
|
+
if (endnotesPart.Endnotes != null)
|
|
616
|
+
{
|
|
617
|
+
foreach (var en in endnotesPart.Endnotes.Elements<Endnote>())
|
|
618
|
+
{
|
|
619
|
+
if (en.Id?.Value != null && en.Id.Value > maxId)
|
|
620
|
+
maxId = (int)en.Id.Value;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return maxId + 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private static int GetNextCommentId(WordprocessingCommentsPart commentsPart)
|
|
627
|
+
{
|
|
628
|
+
int maxId = 0;
|
|
629
|
+
if (commentsPart.Comments != null)
|
|
630
|
+
{
|
|
631
|
+
foreach (var c in commentsPart.Comments.Elements<Comment>())
|
|
632
|
+
{
|
|
633
|
+
if (c.Id?.Value != null && int.TryParse(c.Id.Value, out int id) && id > maxId)
|
|
634
|
+
maxId = id;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return maxId + 1;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private static string GetInitials(string author)
|
|
641
|
+
{
|
|
642
|
+
if (string.IsNullOrWhiteSpace(author)) return "A";
|
|
643
|
+
var parts = author.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
644
|
+
return string.Concat(parts.Select(p => p[..1].ToUpperInvariant()));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private static string GenerateParaId()
|
|
648
|
+
{
|
|
649
|
+
// paraId is an 8-character hex string (32-bit unsigned integer)
|
|
650
|
+
return Random.Shared.Next(0x10000000, int.MaxValue).ToString("X8");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private static void EnsurePersonEntry(MainDocumentPart mainPart, string author)
|
|
654
|
+
{
|
|
655
|
+
var peoplePart = mainPart.WordprocessingPeoplePart;
|
|
656
|
+
if (peoplePart?.People == null) return;
|
|
657
|
+
|
|
658
|
+
// Check if this author already has an entry
|
|
659
|
+
bool exists = peoplePart.People.Elements<W15Person>()
|
|
660
|
+
.Any(p => p.Author?.Value == author);
|
|
661
|
+
|
|
662
|
+
if (!exists)
|
|
663
|
+
{
|
|
664
|
+
var person = new W15Person { Author = author };
|
|
665
|
+
// PresenceInfo — the provider/userId for the author's identity
|
|
666
|
+
person.Append(new W15PresenceInfo
|
|
667
|
+
{
|
|
668
|
+
ProviderId = "None",
|
|
669
|
+
UserId = author
|
|
670
|
+
});
|
|
671
|
+
peoplePart.People.Append(person);
|
|
672
|
+
peoplePart.People.Save();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|