@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,487 @@
|
|
|
1
|
+
using System.CommandLine;
|
|
2
|
+
using System.Text.RegularExpressions;
|
|
3
|
+
using DocumentFormat.OpenXml;
|
|
4
|
+
using DocumentFormat.OpenXml.Packaging;
|
|
5
|
+
using DocumentFormat.OpenXml.Wordprocessing;
|
|
6
|
+
using MiniMaxAIDocx.Core.OpenXml;
|
|
7
|
+
|
|
8
|
+
namespace MiniMaxAIDocx.Core.Commands;
|
|
9
|
+
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// Scenario B: Surgical content editing operations on existing DOCX files.
|
|
12
|
+
/// Preserves all existing formatting and minimizes XML changes.
|
|
13
|
+
/// </summary>
|
|
14
|
+
public static class EditContentCommand
|
|
15
|
+
{
|
|
16
|
+
public static Command Create()
|
|
17
|
+
{
|
|
18
|
+
var cmd = new Command("edit", "Edit existing DOCX content");
|
|
19
|
+
|
|
20
|
+
cmd.Add(CreateReplaceTextCommand());
|
|
21
|
+
cmd.Add(CreateFillTableCommand());
|
|
22
|
+
cmd.Add(CreateInsertParagraphCommand());
|
|
23
|
+
cmd.Add(CreateUpdateFieldCommand());
|
|
24
|
+
cmd.Add(CreateListPlaceholdersCommand());
|
|
25
|
+
cmd.Add(CreateFillPlaceholdersCommand());
|
|
26
|
+
|
|
27
|
+
return cmd;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private static Command CreateReplaceTextCommand()
|
|
31
|
+
{
|
|
32
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
33
|
+
var outputOpt = new Option<string>("--output") { Description = "Output file path (defaults to overwriting input)" };
|
|
34
|
+
var searchOpt = new Option<string>("--search") { Description = "Text to search for", Required = true };
|
|
35
|
+
var replaceOpt = new Option<string>("--replace") { Description = "Replacement text", Required = true };
|
|
36
|
+
var regexOpt = new Option<bool>("--regex") { Description = "Treat search as a regex pattern" };
|
|
37
|
+
|
|
38
|
+
var cmd = new Command("replace-text", "Replace text while preserving formatting")
|
|
39
|
+
{
|
|
40
|
+
inputOpt, outputOpt, searchOpt, replaceOpt, regexOpt
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
cmd.SetAction((parseResult) =>
|
|
44
|
+
{
|
|
45
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
46
|
+
var output = parseResult.GetValue(outputOpt) ?? input;
|
|
47
|
+
var search = parseResult.GetValue(searchOpt)!;
|
|
48
|
+
var replace = parseResult.GetValue(replaceOpt)!;
|
|
49
|
+
var useRegex = parseResult.GetValue(regexOpt);
|
|
50
|
+
|
|
51
|
+
if (output != input) File.Copy(input, output, overwrite: true);
|
|
52
|
+
|
|
53
|
+
using var doc = WordprocessingDocument.Open(output, true);
|
|
54
|
+
var body = doc.MainDocumentPart?.Document.Body;
|
|
55
|
+
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
|
56
|
+
|
|
57
|
+
int count = 0;
|
|
58
|
+
foreach (var paragraph in body.Descendants<Paragraph>())
|
|
59
|
+
{
|
|
60
|
+
count += ReplaceInParagraph(paragraph, search, replace, useRegex);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
doc.MainDocumentPart!.Document.Save();
|
|
64
|
+
Console.WriteLine($"Replaced {count} occurrence(s) in {output}");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return cmd;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static Command CreateFillTableCommand()
|
|
71
|
+
{
|
|
72
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
73
|
+
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
|
74
|
+
var tableIndexOpt = new Option<int>("--table-index") { Description = "Zero-based index of the table to fill" };
|
|
75
|
+
tableIndexOpt.DefaultValueFactory = _ => 0;
|
|
76
|
+
var csvOpt = new Option<string>("--csv") { Description = "CSV file with data to fill", Required = true };
|
|
77
|
+
var appendOpt = new Option<bool>("--append") { Description = "Append rows instead of replacing existing data rows" };
|
|
78
|
+
|
|
79
|
+
var cmd = new Command("fill-table", "Fill a table with data from CSV")
|
|
80
|
+
{
|
|
81
|
+
inputOpt, outputOpt, tableIndexOpt, csvOpt, appendOpt
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
cmd.SetAction((parseResult) =>
|
|
85
|
+
{
|
|
86
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
87
|
+
var output = parseResult.GetValue(outputOpt) ?? input;
|
|
88
|
+
var tableIndex = parseResult.GetValue(tableIndexOpt);
|
|
89
|
+
var csvPath = parseResult.GetValue(csvOpt)!;
|
|
90
|
+
var append = parseResult.GetValue(appendOpt);
|
|
91
|
+
|
|
92
|
+
if (output != input) File.Copy(input, output, overwrite: true);
|
|
93
|
+
|
|
94
|
+
if (!File.Exists(csvPath)) { Console.Error.WriteLine($"CSV file not found: {csvPath}"); return; }
|
|
95
|
+
|
|
96
|
+
using var doc = WordprocessingDocument.Open(output, true);
|
|
97
|
+
var body = doc.MainDocumentPart?.Document.Body;
|
|
98
|
+
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
|
99
|
+
|
|
100
|
+
var tables = body.Elements<Table>().ToList();
|
|
101
|
+
if (tableIndex >= tables.Count)
|
|
102
|
+
{
|
|
103
|
+
Console.Error.WriteLine($"Table index {tableIndex} out of range (found {tables.Count} tables).");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var table = tables[tableIndex];
|
|
108
|
+
var csvLines = File.ReadAllLines(csvPath);
|
|
109
|
+
if (csvLines.Length == 0) { Console.WriteLine("CSV is empty, nothing to fill."); return; }
|
|
110
|
+
|
|
111
|
+
// Get template row properties from the first data row (second row, after header)
|
|
112
|
+
var existingRows = table.Elements<TableRow>().ToList();
|
|
113
|
+
TableRow? templateRow = existingRows.Count > 1 ? existingRows[1] : existingRows.FirstOrDefault();
|
|
114
|
+
var templateTrPr = templateRow?.TableRowProperties?.CloneNode(true) as TableRowProperties;
|
|
115
|
+
|
|
116
|
+
if (!append)
|
|
117
|
+
{
|
|
118
|
+
// Remove all rows except the header row
|
|
119
|
+
for (int i = existingRows.Count - 1; i >= 1; i--)
|
|
120
|
+
existingRows[i].Remove();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
int rowsAdded = 0;
|
|
124
|
+
// Skip header line in CSV (index 0)
|
|
125
|
+
for (int i = 1; i < csvLines.Length; i++)
|
|
126
|
+
{
|
|
127
|
+
var values = ParseCsvLine(csvLines[i]);
|
|
128
|
+
var newRow = new TableRow();
|
|
129
|
+
if (templateTrPr != null)
|
|
130
|
+
newRow.Append(templateTrPr.CloneNode(true));
|
|
131
|
+
|
|
132
|
+
foreach (var val in values)
|
|
133
|
+
{
|
|
134
|
+
var cell = new TableCell(
|
|
135
|
+
new Paragraph(new Run(new Text(val))));
|
|
136
|
+
newRow.Append(cell);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
table.Append(newRow);
|
|
140
|
+
rowsAdded++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
doc.MainDocumentPart!.Document.Save();
|
|
144
|
+
Console.WriteLine($"Added {rowsAdded} rows to table {tableIndex} in {output}");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return cmd;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static Command CreateInsertParagraphCommand()
|
|
151
|
+
{
|
|
152
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
153
|
+
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
|
154
|
+
var textOpt = new Option<string>("--text") { Description = "Paragraph text", Required = true };
|
|
155
|
+
var styleOpt = new Option<string>("--style") { Description = "Paragraph style (e.g. Heading1, Normal)" };
|
|
156
|
+
var afterOpt = new Option<int>("--after-paragraph") { Description = "Insert after this paragraph index (0-based)" };
|
|
157
|
+
afterOpt.DefaultValueFactory = _ => -1; // -1 = append at end
|
|
158
|
+
|
|
159
|
+
var cmd = new Command("insert-paragraph", "Insert a new paragraph")
|
|
160
|
+
{
|
|
161
|
+
inputOpt, outputOpt, textOpt, styleOpt, afterOpt
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
cmd.SetAction((parseResult) =>
|
|
165
|
+
{
|
|
166
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
167
|
+
var output = parseResult.GetValue(outputOpt) ?? input;
|
|
168
|
+
var text = parseResult.GetValue(textOpt)!;
|
|
169
|
+
var style = parseResult.GetValue(styleOpt);
|
|
170
|
+
var afterIndex = parseResult.GetValue(afterOpt);
|
|
171
|
+
|
|
172
|
+
if (output != input) File.Copy(input, output, overwrite: true);
|
|
173
|
+
|
|
174
|
+
using var doc = WordprocessingDocument.Open(output, true);
|
|
175
|
+
var body = doc.MainDocumentPart?.Document.Body;
|
|
176
|
+
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
|
177
|
+
|
|
178
|
+
var newPara = new Paragraph();
|
|
179
|
+
if (!string.IsNullOrEmpty(style))
|
|
180
|
+
newPara.Append(new ParagraphProperties(new ParagraphStyleId { Val = style }));
|
|
181
|
+
newPara.Append(new Run(new Text(text)));
|
|
182
|
+
|
|
183
|
+
var paragraphs = body.Elements<Paragraph>().ToList();
|
|
184
|
+
if (afterIndex >= 0 && afterIndex < paragraphs.Count)
|
|
185
|
+
{
|
|
186
|
+
paragraphs[afterIndex].InsertAfterSelf(newPara);
|
|
187
|
+
}
|
|
188
|
+
else
|
|
189
|
+
{
|
|
190
|
+
// Insert before sectPr if present, otherwise append
|
|
191
|
+
var sectPr = body.Elements<SectionProperties>().FirstOrDefault();
|
|
192
|
+
if (sectPr != null)
|
|
193
|
+
sectPr.InsertBeforeSelf(newPara);
|
|
194
|
+
else
|
|
195
|
+
body.Append(newPara);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
doc.MainDocumentPart!.Document.Save();
|
|
199
|
+
Console.WriteLine($"Inserted paragraph in {output}");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return cmd;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static Command CreateUpdateFieldCommand()
|
|
206
|
+
{
|
|
207
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
208
|
+
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
|
209
|
+
var fieldNameOpt = new Option<string>("--field") { Description = "Document property field name (e.g. TITLE, AUTHOR)", Required = true };
|
|
210
|
+
var valueOpt = new Option<string>("--value") { Description = "New field value", Required = true };
|
|
211
|
+
|
|
212
|
+
var cmd = new Command("update-field", "Update a document property field value")
|
|
213
|
+
{
|
|
214
|
+
inputOpt, outputOpt, fieldNameOpt, valueOpt
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
cmd.SetAction((parseResult) =>
|
|
218
|
+
{
|
|
219
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
220
|
+
var output = parseResult.GetValue(outputOpt) ?? input;
|
|
221
|
+
var fieldName = parseResult.GetValue(fieldNameOpt)!;
|
|
222
|
+
var value = parseResult.GetValue(valueOpt)!;
|
|
223
|
+
|
|
224
|
+
if (output != input) File.Copy(input, output, overwrite: true);
|
|
225
|
+
|
|
226
|
+
using var doc = WordprocessingDocument.Open(output, true);
|
|
227
|
+
|
|
228
|
+
// Update core properties
|
|
229
|
+
var props = doc.PackageProperties;
|
|
230
|
+
switch (fieldName.ToUpperInvariant())
|
|
231
|
+
{
|
|
232
|
+
case "TITLE": props.Title = value; break;
|
|
233
|
+
case "AUTHOR": props.Creator = value; break;
|
|
234
|
+
case "SUBJECT": props.Subject = value; break;
|
|
235
|
+
case "KEYWORDS": props.Keywords = value; break;
|
|
236
|
+
case "DESCRIPTION": props.Description = value; break;
|
|
237
|
+
case "CATEGORY": props.Category = value; break;
|
|
238
|
+
default:
|
|
239
|
+
Console.Error.WriteLine($"Unknown field: {fieldName}. Supported: TITLE, AUTHOR, SUBJECT, KEYWORDS, DESCRIPTION, CATEGORY");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Console.WriteLine($"Updated {fieldName} to \"{value}\" in {output}");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return cmd;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static Command CreateListPlaceholdersCommand()
|
|
250
|
+
{
|
|
251
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
252
|
+
var patternOpt = new Option<string>("--pattern") { Description = "Placeholder pattern (regex)" };
|
|
253
|
+
patternOpt.DefaultValueFactory = _ => @"\{\{(\w+)\}\}"; // {{PLACEHOLDER}}
|
|
254
|
+
|
|
255
|
+
var cmd = new Command("list-placeholders", "List all placeholders found in the document")
|
|
256
|
+
{
|
|
257
|
+
inputOpt, patternOpt
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
cmd.SetAction((parseResult) =>
|
|
261
|
+
{
|
|
262
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
263
|
+
var pattern = parseResult.GetValue(patternOpt)!;
|
|
264
|
+
|
|
265
|
+
using var doc = WordprocessingDocument.Open(input, false);
|
|
266
|
+
var body = doc.MainDocumentPart?.Document.Body;
|
|
267
|
+
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
|
268
|
+
|
|
269
|
+
var placeholders = new HashSet<string>();
|
|
270
|
+
var regex = new Regex(pattern);
|
|
271
|
+
|
|
272
|
+
foreach (var paragraph in body.Descendants<Paragraph>())
|
|
273
|
+
{
|
|
274
|
+
var fullText = string.Concat(paragraph.Descendants<Text>().Select(t => t.Text));
|
|
275
|
+
foreach (Match match in regex.Matches(fullText))
|
|
276
|
+
{
|
|
277
|
+
placeholders.Add(match.Value);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (placeholders.Count == 0)
|
|
282
|
+
{
|
|
283
|
+
Console.WriteLine("No placeholders found.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
Console.WriteLine($"Found {placeholders.Count} unique placeholder(s):");
|
|
288
|
+
foreach (var p in placeholders.OrderBy(x => x))
|
|
289
|
+
Console.WriteLine($" {p}");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return cmd;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private static Command CreateFillPlaceholdersCommand()
|
|
296
|
+
{
|
|
297
|
+
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
|
298
|
+
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
|
299
|
+
var mappingOpt = new Option<string>("--mapping") { Description = "JSON file mapping placeholder names to values", Required = true };
|
|
300
|
+
var patternOpt = new Option<string>("--pattern") { Description = "Placeholder pattern with capture group for the name" };
|
|
301
|
+
patternOpt.DefaultValueFactory = _ => @"\{\{(\w+)\}\}";
|
|
302
|
+
|
|
303
|
+
var cmd = new Command("fill-placeholders", "Replace placeholders with values from a mapping file")
|
|
304
|
+
{
|
|
305
|
+
inputOpt, outputOpt, mappingOpt, patternOpt
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
cmd.SetAction((parseResult) =>
|
|
309
|
+
{
|
|
310
|
+
var input = parseResult.GetValue(inputOpt)!;
|
|
311
|
+
var output = parseResult.GetValue(outputOpt) ?? input;
|
|
312
|
+
var mappingPath = parseResult.GetValue(mappingOpt)!;
|
|
313
|
+
var pattern = parseResult.GetValue(patternOpt)!;
|
|
314
|
+
|
|
315
|
+
if (!File.Exists(mappingPath)) { Console.Error.WriteLine($"Mapping file not found: {mappingPath}"); return; }
|
|
316
|
+
|
|
317
|
+
var mappingJson = File.ReadAllText(mappingPath);
|
|
318
|
+
Dictionary<string, string> mapping;
|
|
319
|
+
try
|
|
320
|
+
{
|
|
321
|
+
mapping = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(mappingJson) ?? [];
|
|
322
|
+
}
|
|
323
|
+
catch (System.Text.Json.JsonException ex)
|
|
324
|
+
{
|
|
325
|
+
Console.Error.WriteLine($"Invalid mapping JSON: {ex.Message}");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (output != input) File.Copy(input, output, overwrite: true);
|
|
330
|
+
|
|
331
|
+
using var doc = WordprocessingDocument.Open(output, true);
|
|
332
|
+
var body = doc.MainDocumentPart?.Document.Body;
|
|
333
|
+
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
|
334
|
+
|
|
335
|
+
int totalReplacements = 0;
|
|
336
|
+
var regex = new Regex(pattern);
|
|
337
|
+
|
|
338
|
+
foreach (var paragraph in body.Descendants<Paragraph>())
|
|
339
|
+
{
|
|
340
|
+
var fullText = string.Concat(paragraph.Descendants<Text>().Select(t => t.Text));
|
|
341
|
+
var matches = regex.Matches(fullText);
|
|
342
|
+
if (matches.Count == 0) continue;
|
|
343
|
+
|
|
344
|
+
foreach (Match match in matches)
|
|
345
|
+
{
|
|
346
|
+
var placeholderName = match.Groups.Count > 1 ? match.Groups[1].Value : match.Value;
|
|
347
|
+
if (mapping.TryGetValue(placeholderName, out var replacement))
|
|
348
|
+
{
|
|
349
|
+
totalReplacements += ReplaceInParagraph(paragraph, match.Value, replacement, false);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
doc.MainDocumentPart!.Document.Save();
|
|
355
|
+
Console.WriteLine($"Filled {totalReplacements} placeholder(s) in {output}");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return cmd;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// <summary>
|
|
362
|
+
/// Replaces text within a paragraph while preserving run formatting.
|
|
363
|
+
/// Handles the case where search text may span multiple runs.
|
|
364
|
+
/// </summary>
|
|
365
|
+
private static int ReplaceInParagraph(Paragraph paragraph, string search, string replace, bool useRegex)
|
|
366
|
+
{
|
|
367
|
+
var runs = paragraph.Elements<Run>().ToList();
|
|
368
|
+
if (runs.Count == 0) return 0;
|
|
369
|
+
|
|
370
|
+
// Build the full paragraph text and a map from character index to (run, position within run)
|
|
371
|
+
var fullText = string.Concat(runs.SelectMany(r => r.Elements<Text>().Select(t => t.Text)));
|
|
372
|
+
if (string.IsNullOrEmpty(fullText)) return 0;
|
|
373
|
+
|
|
374
|
+
int count = 0;
|
|
375
|
+
|
|
376
|
+
if (!useRegex)
|
|
377
|
+
{
|
|
378
|
+
// Simple case: search within each run first
|
|
379
|
+
foreach (var run in runs)
|
|
380
|
+
{
|
|
381
|
+
foreach (var textElement in run.Elements<Text>().ToList())
|
|
382
|
+
{
|
|
383
|
+
if (textElement.Text.Contains(search))
|
|
384
|
+
{
|
|
385
|
+
var newText = textElement.Text.Replace(search, replace);
|
|
386
|
+
count += (textElement.Text.Length - newText.Length + replace.Length - search.Length) == 0 ? 0 :
|
|
387
|
+
CountOccurrences(textElement.Text, search);
|
|
388
|
+
textElement.Text = newText;
|
|
389
|
+
if (newText.StartsWith(' ') || newText.EndsWith(' '))
|
|
390
|
+
textElement.Space = SpaceProcessingModeValues.Preserve;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Handle cross-run matches by concatenating all runs, replacing, and rebuilding
|
|
396
|
+
if (count == 0 && fullText.Contains(search))
|
|
397
|
+
{
|
|
398
|
+
var newFullText = fullText.Replace(search, replace);
|
|
399
|
+
count = CountOccurrences(fullText, search);
|
|
400
|
+
RebuildRunsWithText(paragraph, runs, newFullText);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else
|
|
404
|
+
{
|
|
405
|
+
var regex = new Regex(search);
|
|
406
|
+
if (regex.IsMatch(fullText))
|
|
407
|
+
{
|
|
408
|
+
count = regex.Matches(fullText).Count;
|
|
409
|
+
var newFullText = regex.Replace(fullText, replace);
|
|
410
|
+
RebuildRunsWithText(paragraph, runs, newFullText);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return count;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// <summary>
|
|
418
|
+
/// Replaces the text content of existing runs with new text,
|
|
419
|
+
/// preserving the formatting of the first run.
|
|
420
|
+
/// </summary>
|
|
421
|
+
private static void RebuildRunsWithText(Paragraph paragraph, List<Run> runs, string newText)
|
|
422
|
+
{
|
|
423
|
+
if (runs.Count == 0) return;
|
|
424
|
+
|
|
425
|
+
// Keep the first run's formatting, set its text to the full new text
|
|
426
|
+
var firstRun = runs[0];
|
|
427
|
+
var firstText = firstRun.Elements<Text>().FirstOrDefault();
|
|
428
|
+
if (firstText != null)
|
|
429
|
+
{
|
|
430
|
+
firstText.Text = newText;
|
|
431
|
+
if (newText.StartsWith(' ') || newText.EndsWith(' '))
|
|
432
|
+
firstText.Space = SpaceProcessingModeValues.Preserve;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Remove all other runs
|
|
436
|
+
for (int i = 1; i < runs.Count; i++)
|
|
437
|
+
runs[i].Remove();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private static int CountOccurrences(string text, string search)
|
|
441
|
+
{
|
|
442
|
+
int count = 0;
|
|
443
|
+
int index = 0;
|
|
444
|
+
while ((index = text.IndexOf(search, index, StringComparison.Ordinal)) != -1)
|
|
445
|
+
{
|
|
446
|
+
count++;
|
|
447
|
+
index += search.Length;
|
|
448
|
+
}
|
|
449
|
+
return count;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private static string[] ParseCsvLine(string line)
|
|
453
|
+
{
|
|
454
|
+
// Simple CSV parser (handles quoted fields)
|
|
455
|
+
var result = new List<string>();
|
|
456
|
+
bool inQuotes = false;
|
|
457
|
+
var current = new System.Text.StringBuilder();
|
|
458
|
+
|
|
459
|
+
for (int i = 0; i < line.Length; i++)
|
|
460
|
+
{
|
|
461
|
+
char c = line[i];
|
|
462
|
+
if (c == '"')
|
|
463
|
+
{
|
|
464
|
+
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
|
465
|
+
{
|
|
466
|
+
current.Append('"');
|
|
467
|
+
i++;
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
{
|
|
471
|
+
inQuotes = !inQuotes;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (c == ',' && !inQuotes)
|
|
475
|
+
{
|
|
476
|
+
result.Add(current.ToString());
|
|
477
|
+
current.Clear();
|
|
478
|
+
}
|
|
479
|
+
else
|
|
480
|
+
{
|
|
481
|
+
current.Append(c);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
result.Add(current.ToString());
|
|
485
|
+
return result.ToArray();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
using System.CommandLine;
|
|
2
|
+
using System.IO.Compression;
|
|
3
|
+
using System.Xml.Linq;
|
|
4
|
+
|
|
5
|
+
namespace MiniMaxAIDocx.Core.Commands;
|
|
6
|
+
|
|
7
|
+
public static class FixOrderCommand
|
|
8
|
+
{
|
|
9
|
+
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
10
|
+
|
|
11
|
+
// Canonical element ordering within common parent elements per ISO 29500
|
|
12
|
+
private static readonly Dictionary<string, List<string>> ElementOrder = new()
|
|
13
|
+
{
|
|
14
|
+
["pPr"] = new() { "pStyle", "keepNext", "keepLines", "pageBreakBefore", "widowControl", "numPr", "suppressLineNumbers", "pBdr", "shd", "tabs", "suppressAutoHyphens", "spacing", "ind", "jc", "outlineLvl", "rPr" },
|
|
15
|
+
["rPr"] = new() { "rStyle", "rFonts", "b", "bCs", "i", "iCs", "caps", "smallCaps", "strike", "dstrike", "vanish", "color", "spacing", "w", "kern", "position", "sz", "szCs", "highlight", "u", "effect", "vertAlign", "lang" },
|
|
16
|
+
["tblPr"] = new() { "tblStyle", "tblpPr", "tblOverlap", "tblW", "jc", "tblInd", "tblBorders", "shd", "tblLayout", "tblCellMar", "tblLook" },
|
|
17
|
+
["tcPr"] = new() { "cnfStyle", "tcW", "gridSpan", "hMerge", "vMerge", "tcBorders", "shd", "noWrap", "tcMar", "textDirection", "tcFitText", "vAlign" },
|
|
18
|
+
["sectPr"] = new() { "headerReference", "footerReference", "footnotePr", "endnotePr", "type", "pgSz", "pgMar", "paperSrc", "pgBorders", "lnNumType", "pgNumType", "cols", "docGrid" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public static Command Create()
|
|
22
|
+
{
|
|
23
|
+
var inputOption = new Option<string>("--input") { Description = "DOCX file to fix", Required = true };
|
|
24
|
+
var outputOption = new Option<string>("--output") { Description = "Output path (default: overwrite input)" };
|
|
25
|
+
var backupOption = new Option<bool>("--backup") { Description = "Create .bak before modifying", DefaultValueFactory = (_) => true };
|
|
26
|
+
|
|
27
|
+
var cmd = new Command("fix-order", "Fix OpenXML element ordering per ISO 29500")
|
|
28
|
+
{
|
|
29
|
+
inputOption, outputOption, backupOption
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
cmd.SetAction((parseResult) =>
|
|
33
|
+
{
|
|
34
|
+
var input = parseResult.GetValue(inputOption)!;
|
|
35
|
+
var output = parseResult.GetValue(outputOption) ?? input;
|
|
36
|
+
var backup = parseResult.GetValue(backupOption);
|
|
37
|
+
|
|
38
|
+
if (!File.Exists(input))
|
|
39
|
+
{
|
|
40
|
+
Console.Error.WriteLine($"File not found: {input}");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (backup && output == input)
|
|
45
|
+
File.Copy(input, input + ".bak", true);
|
|
46
|
+
|
|
47
|
+
var tempPath = Path.GetTempFileName();
|
|
48
|
+
File.Copy(input, tempPath, true);
|
|
49
|
+
|
|
50
|
+
using var zip = ZipFile.Open(tempPath, ZipArchiveMode.Update);
|
|
51
|
+
var entry = zip.GetEntry("word/document.xml");
|
|
52
|
+
if (entry == null)
|
|
53
|
+
{
|
|
54
|
+
Console.Error.WriteLine("Not a valid DOCX");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
XDocument doc;
|
|
59
|
+
using (var stream = entry.Open())
|
|
60
|
+
doc = XDocument.Load(stream);
|
|
61
|
+
|
|
62
|
+
int reorderedCount = 0;
|
|
63
|
+
|
|
64
|
+
foreach (var (parentName, order) in ElementOrder)
|
|
65
|
+
{
|
|
66
|
+
foreach (var parent in doc.Descendants(W + parentName))
|
|
67
|
+
{
|
|
68
|
+
var children = parent.Elements().ToList();
|
|
69
|
+
var sorted = children.OrderBy(e =>
|
|
70
|
+
{
|
|
71
|
+
var idx = order.IndexOf(e.Name.LocalName);
|
|
72
|
+
return idx >= 0 ? idx : order.Count;
|
|
73
|
+
}).ToList();
|
|
74
|
+
|
|
75
|
+
bool changed = false;
|
|
76
|
+
for (int i = 0; i < children.Count; i++)
|
|
77
|
+
{
|
|
78
|
+
if (children[i] != sorted[i])
|
|
79
|
+
{
|
|
80
|
+
changed = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (changed)
|
|
86
|
+
{
|
|
87
|
+
parent.ReplaceNodes(sorted);
|
|
88
|
+
reorderedCount++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
entry.Delete();
|
|
94
|
+
var newEntry = zip.CreateEntry("word/document.xml", CompressionLevel.Optimal);
|
|
95
|
+
using (var stream = newEntry.Open())
|
|
96
|
+
doc.Save(stream);
|
|
97
|
+
|
|
98
|
+
zip.Dispose();
|
|
99
|
+
File.Copy(tempPath, output, true);
|
|
100
|
+
File.Delete(tempPath);
|
|
101
|
+
|
|
102
|
+
Console.WriteLine($"Reordered {reorderedCount} element group(s)");
|
|
103
|
+
Console.WriteLine($"Written to: {output}");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return cmd;
|
|
107
|
+
}
|
|
108
|
+
}
|