@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,224 @@
|
|
|
1
|
+
using System.IO.Compression;
|
|
2
|
+
using System.Xml.Linq;
|
|
3
|
+
|
|
4
|
+
namespace MiniMaxAIDocx.Core.Validation;
|
|
5
|
+
|
|
6
|
+
public class BusinessRuleValidator
|
|
7
|
+
{
|
|
8
|
+
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
9
|
+
private static readonly XNamespace R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
10
|
+
private static readonly XNamespace WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
|
11
|
+
private static readonly XNamespace A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
12
|
+
|
|
13
|
+
private const int MinMarginDxa = 360; // 0.25 inch
|
|
14
|
+
private const int MaxMarginDxa = 4320; // 3 inches
|
|
15
|
+
private const int MinBodyFontHps = 16; // 8pt
|
|
16
|
+
private const int MaxBodyFontHps = 144; // 72pt
|
|
17
|
+
private const int MinHeadingFontHps = 20; // 10pt
|
|
18
|
+
private const int MaxHeadingFontHps = 192; // 96pt
|
|
19
|
+
|
|
20
|
+
public ValidationResult Validate(string docxPath)
|
|
21
|
+
{
|
|
22
|
+
var result = new ValidationResult();
|
|
23
|
+
|
|
24
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
25
|
+
var docEntry = zip.GetEntry("word/document.xml")
|
|
26
|
+
?? throw new InvalidOperationException("Missing word/document.xml");
|
|
27
|
+
|
|
28
|
+
var doc = LoadXml(docEntry);
|
|
29
|
+
var body = doc.Root?.Element(W + "body");
|
|
30
|
+
if (body == null)
|
|
31
|
+
{
|
|
32
|
+
result.Errors.Add(Error("Document has no body element"));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ValidateMargins(body, result);
|
|
37
|
+
ValidateFontSizes(body, result);
|
|
38
|
+
ValidateHeadingHierarchy(body, result);
|
|
39
|
+
ValidateTableColumnWidths(body, result);
|
|
40
|
+
ValidateRelationships(zip, doc, result);
|
|
41
|
+
ValidateComments(zip, result);
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private void ValidateMargins(XElement body, ValidationResult result)
|
|
47
|
+
{
|
|
48
|
+
foreach (var sectPr in body.Descendants(W + "sectPr"))
|
|
49
|
+
{
|
|
50
|
+
var pgMar = sectPr.Element(W + "pgMar");
|
|
51
|
+
if (pgMar == null) continue;
|
|
52
|
+
|
|
53
|
+
foreach (var attr in new[] { "top", "bottom", "left", "right" })
|
|
54
|
+
{
|
|
55
|
+
var val = (string?)pgMar.Attribute(W + attr);
|
|
56
|
+
if (val != null && int.TryParse(val, out var dxa))
|
|
57
|
+
{
|
|
58
|
+
var absDxa = Math.Abs(dxa);
|
|
59
|
+
if (absDxa < MinMarginDxa)
|
|
60
|
+
result.Errors.Add(Error($"Margin '{attr}' is {absDxa} DXA ({absDxa / 1440.0:F2}\"), below minimum {MinMarginDxa} DXA"));
|
|
61
|
+
if (absDxa > MaxMarginDxa)
|
|
62
|
+
result.Warnings.Add(Warning($"Margin '{attr}' is {absDxa} DXA ({absDxa / 1440.0:F2}\"), above maximum {MaxMarginDxa} DXA"));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private void ValidateFontSizes(XElement body, ValidationResult result)
|
|
69
|
+
{
|
|
70
|
+
foreach (var p in body.Descendants(W + "p"))
|
|
71
|
+
{
|
|
72
|
+
var pStyle = p.Element(W + "pPr")?.Element(W + "pStyle")?.Attribute(W + "val")?.Value;
|
|
73
|
+
bool isHeading = pStyle?.StartsWith("Heading", StringComparison.OrdinalIgnoreCase) == true;
|
|
74
|
+
|
|
75
|
+
foreach (var rPr in p.Descendants(W + "rPr"))
|
|
76
|
+
{
|
|
77
|
+
var szEl = rPr.Element(W + "sz");
|
|
78
|
+
var val = (string?)szEl?.Attribute(W + "val");
|
|
79
|
+
if (val != null && int.TryParse(val, out var hps))
|
|
80
|
+
{
|
|
81
|
+
int min = isHeading ? MinHeadingFontHps : MinBodyFontHps;
|
|
82
|
+
int max = isHeading ? MaxHeadingFontHps : MaxBodyFontHps;
|
|
83
|
+
if (hps < min || hps > max)
|
|
84
|
+
result.Warnings.Add(Warning($"Font size {hps / 2.0}pt is outside {(isHeading ? "heading" : "body")} range ({min / 2}-{max / 2}pt)"));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private void ValidateHeadingHierarchy(XElement body, ValidationResult result)
|
|
91
|
+
{
|
|
92
|
+
int lastLevel = 0;
|
|
93
|
+
foreach (var p in body.Descendants(W + "p"))
|
|
94
|
+
{
|
|
95
|
+
var pStyle = p.Element(W + "pPr")?.Element(W + "pStyle")?.Attribute(W + "val")?.Value;
|
|
96
|
+
if (pStyle == null) continue;
|
|
97
|
+
|
|
98
|
+
int level = 0;
|
|
99
|
+
if (pStyle.StartsWith("Heading", StringComparison.OrdinalIgnoreCase))
|
|
100
|
+
{
|
|
101
|
+
var numPart = pStyle.AsSpan(7);
|
|
102
|
+
if (int.TryParse(numPart, out var parsed)) level = parsed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (level > 0)
|
|
106
|
+
{
|
|
107
|
+
if (lastLevel > 0 && level > lastLevel + 1)
|
|
108
|
+
result.Warnings.Add(Warning($"Heading level skips from {lastLevel} to {level} (missing Heading{lastLevel + 1})"));
|
|
109
|
+
lastLevel = level;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private void ValidateTableColumnWidths(XElement body, ValidationResult result)
|
|
115
|
+
{
|
|
116
|
+
var sectPr = body.Element(W + "sectPr");
|
|
117
|
+
if (sectPr == null) return;
|
|
118
|
+
|
|
119
|
+
var pgSz = sectPr.Element(W + "pgSz");
|
|
120
|
+
var pgMar = sectPr.Element(W + "pgMar");
|
|
121
|
+
if (pgSz == null || pgMar == null) return;
|
|
122
|
+
|
|
123
|
+
if (!int.TryParse((string?)pgSz.Attribute(W + "w"), out var pageWidth)) return;
|
|
124
|
+
int.TryParse((string?)pgMar.Attribute(W + "left"), out var marginLeft);
|
|
125
|
+
int.TryParse((string?)pgMar.Attribute(W + "right"), out var marginRight);
|
|
126
|
+
var contentWidth = pageWidth - marginLeft - marginRight;
|
|
127
|
+
|
|
128
|
+
int tableIndex = 0;
|
|
129
|
+
foreach (var tbl in body.Descendants(W + "tbl"))
|
|
130
|
+
{
|
|
131
|
+
tableIndex++;
|
|
132
|
+
var firstRow = tbl.Element(W + "tr");
|
|
133
|
+
if (firstRow == null) continue;
|
|
134
|
+
|
|
135
|
+
int totalWidth = 0;
|
|
136
|
+
foreach (var tc in firstRow.Elements(W + "tc"))
|
|
137
|
+
{
|
|
138
|
+
var tcW = tc.Element(W + "tcPr")?.Element(W + "tcW");
|
|
139
|
+
var w = (string?)tcW?.Attribute(W + "w");
|
|
140
|
+
if (w != null && int.TryParse(w, out var cellWidth))
|
|
141
|
+
totalWidth += cellWidth;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (totalWidth > 0)
|
|
145
|
+
{
|
|
146
|
+
var tolerance = contentWidth * 0.02;
|
|
147
|
+
if (Math.Abs(totalWidth - contentWidth) > tolerance)
|
|
148
|
+
result.Warnings.Add(Warning($"Table {tableIndex}: column widths sum to {totalWidth} DXA but content width is {contentWidth} DXA"));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private void ValidateRelationships(ZipArchive zip, XDocument doc, ValidationResult result)
|
|
154
|
+
{
|
|
155
|
+
var relsEntry = zip.GetEntry("word/_rels/document.xml.rels");
|
|
156
|
+
if (relsEntry == null) return;
|
|
157
|
+
|
|
158
|
+
var relDoc = LoadXml(relsEntry);
|
|
159
|
+
var ns = relDoc.Root?.Name.Namespace ?? XNamespace.None;
|
|
160
|
+
var definedIds = new HashSet<string>();
|
|
161
|
+
|
|
162
|
+
foreach (var rel in relDoc.Descendants(ns + "Relationship"))
|
|
163
|
+
{
|
|
164
|
+
var id = (string?)rel.Attribute("Id");
|
|
165
|
+
if (id != null) definedIds.Add(id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var referencedIds = new HashSet<string>();
|
|
169
|
+
foreach (var el in doc.Descendants())
|
|
170
|
+
{
|
|
171
|
+
var rid = (string?)el.Attribute(R + "id") ?? (string?)el.Attribute(R + "embed");
|
|
172
|
+
if (rid != null) referencedIds.Add(rid);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
foreach (var id in referencedIds.Except(definedIds))
|
|
176
|
+
result.Errors.Add(Error($"Reference r:id='{id}' has no matching relationship"));
|
|
177
|
+
|
|
178
|
+
foreach (var id in definedIds.Except(referencedIds))
|
|
179
|
+
result.Warnings.Add(Warning($"Orphaned relationship: Id='{id}' is defined but never referenced"));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private void ValidateComments(ZipArchive zip, ValidationResult result)
|
|
183
|
+
{
|
|
184
|
+
var commentFiles = new[] { "word/comments.xml", "word/commentsExtended.xml", "word/commentsIds.xml", "word/commentsExtensible.xml" };
|
|
185
|
+
var existing = commentFiles.Where(f => zip.GetEntry(f) != null).ToList();
|
|
186
|
+
|
|
187
|
+
if (existing.Count > 0 && existing.Count < 4)
|
|
188
|
+
{
|
|
189
|
+
var missing = commentFiles.Except(existing);
|
|
190
|
+
result.Warnings.Add(Warning($"Comments partially present. Missing: {string.Join(", ", missing)}"));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (zip.GetEntry("word/comments.xml") is { } commentsEntry)
|
|
194
|
+
{
|
|
195
|
+
var commentsDoc = LoadXml(commentsEntry);
|
|
196
|
+
var commentIds = commentsDoc.Descendants(W + "comment")
|
|
197
|
+
.Select(c => (string?)c.Attribute(W + "id"))
|
|
198
|
+
.Where(id => id != null)
|
|
199
|
+
.ToHashSet();
|
|
200
|
+
|
|
201
|
+
if (zip.GetEntry("word/commentsExtended.xml") is { } extEntry)
|
|
202
|
+
{
|
|
203
|
+
var W15 = XNamespace.Get("http://schemas.microsoft.com/office/word/2012/wordml");
|
|
204
|
+
var extDoc = LoadXml(extEntry);
|
|
205
|
+
var extIds = extDoc.Descendants(W15 + "commentEx")
|
|
206
|
+
.Select(c => (string?)c.Attribute(W15 + "paraId"))
|
|
207
|
+
.Where(id => id != null)
|
|
208
|
+
.ToHashSet();
|
|
209
|
+
|
|
210
|
+
if (commentIds.Count > 0 && extIds.Count == 0)
|
|
211
|
+
result.Warnings.Add(Warning("comments.xml has entries but commentsExtended.xml has none"));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private static XDocument LoadXml(ZipArchiveEntry entry)
|
|
217
|
+
{
|
|
218
|
+
using var stream = entry.Open();
|
|
219
|
+
return XDocument.Load(stream);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static ValidationError Error(string msg) => new() { Message = msg, Severity = "Error" };
|
|
223
|
+
private static ValidationError Warning(string msg) => new() { Message = msg, Severity = "Warning" };
|
|
224
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
using System.IO.Compression;
|
|
2
|
+
using System.Xml.Linq;
|
|
3
|
+
|
|
4
|
+
namespace MiniMaxAIDocx.Core.Validation;
|
|
5
|
+
|
|
6
|
+
public class GateCheckResult
|
|
7
|
+
{
|
|
8
|
+
public bool Passed => Violations.Count == 0;
|
|
9
|
+
public List<string> Violations { get; set; } = new();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public class GateCheckValidator
|
|
13
|
+
{
|
|
14
|
+
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
15
|
+
|
|
16
|
+
public GateCheckResult Validate(string outputDocxPath, string templateDocxPath)
|
|
17
|
+
{
|
|
18
|
+
var result = new GateCheckResult();
|
|
19
|
+
|
|
20
|
+
var templateStyles = ExtractStyles(templateDocxPath);
|
|
21
|
+
var outputStyles = ExtractStyles(outputDocxPath);
|
|
22
|
+
var templateSectPr = ExtractSectionProperties(templateDocxPath);
|
|
23
|
+
var outputSectPr = ExtractSectionProperties(outputDocxPath);
|
|
24
|
+
|
|
25
|
+
// All template styles must exist in output
|
|
26
|
+
foreach (var style in templateStyles)
|
|
27
|
+
{
|
|
28
|
+
if (!outputStyles.Contains(style))
|
|
29
|
+
result.Violations.Add($"Missing style: '{style}' defined in template but absent from output");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Page margins must match
|
|
33
|
+
if (templateSectPr.Margins != null && outputSectPr.Margins != null)
|
|
34
|
+
{
|
|
35
|
+
var tm = templateSectPr.Margins;
|
|
36
|
+
var om = outputSectPr.Margins;
|
|
37
|
+
if (tm.Top != om.Top || tm.Bottom != om.Bottom || tm.Left != om.Left || tm.Right != om.Right)
|
|
38
|
+
result.Violations.Add($"Page margins mismatch: template=({tm.Top},{tm.Bottom},{tm.Left},{tm.Right}) output=({om.Top},{om.Bottom},{om.Left},{om.Right})");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Page size must match
|
|
42
|
+
if (templateSectPr.PageWidth != outputSectPr.PageWidth || templateSectPr.PageHeight != outputSectPr.PageHeight)
|
|
43
|
+
result.Violations.Add($"Page size mismatch: template=({templateSectPr.PageWidth}x{templateSectPr.PageHeight}) output=({outputSectPr.PageWidth}x{outputSectPr.PageHeight})");
|
|
44
|
+
|
|
45
|
+
// Default font must match
|
|
46
|
+
var templateFont = ExtractDefaultFont(templateDocxPath);
|
|
47
|
+
var outputFont = ExtractDefaultFont(outputDocxPath);
|
|
48
|
+
if (templateFont != null && outputFont != null && templateFont != outputFont)
|
|
49
|
+
result.Violations.Add($"Default font mismatch: template='{templateFont}' output='{outputFont}'");
|
|
50
|
+
|
|
51
|
+
// Heading font hierarchy consistency
|
|
52
|
+
ValidateHeadingFontHierarchy(outputDocxPath, result);
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private HashSet<string> ExtractStyles(string docxPath)
|
|
58
|
+
{
|
|
59
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
60
|
+
var entry = zip.GetEntry("word/styles.xml");
|
|
61
|
+
if (entry == null) return new();
|
|
62
|
+
|
|
63
|
+
using var stream = entry.Open();
|
|
64
|
+
var doc = XDocument.Load(stream);
|
|
65
|
+
return doc.Descendants(W + "style")
|
|
66
|
+
.Select(s => (string?)s.Attribute(W + "styleId"))
|
|
67
|
+
.Where(id => id != null)
|
|
68
|
+
.ToHashSet()!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private record SectionProps(int PageWidth, int PageHeight, MarginInfo? Margins);
|
|
72
|
+
private record MarginInfo(int Top, int Bottom, int Left, int Right);
|
|
73
|
+
|
|
74
|
+
private SectionProps ExtractSectionProperties(string docxPath)
|
|
75
|
+
{
|
|
76
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
77
|
+
var entry = zip.GetEntry("word/document.xml")!;
|
|
78
|
+
using var stream = entry.Open();
|
|
79
|
+
var doc = XDocument.Load(stream);
|
|
80
|
+
|
|
81
|
+
var sectPr = doc.Descendants(W + "sectPr").LastOrDefault();
|
|
82
|
+
if (sectPr == null) return new(0, 0, null);
|
|
83
|
+
|
|
84
|
+
int.TryParse((string?)sectPr.Element(W + "pgSz")?.Attribute(W + "w"), out var pw);
|
|
85
|
+
int.TryParse((string?)sectPr.Element(W + "pgSz")?.Attribute(W + "h"), out var ph);
|
|
86
|
+
|
|
87
|
+
var pgMar = sectPr.Element(W + "pgMar");
|
|
88
|
+
MarginInfo? margins = null;
|
|
89
|
+
if (pgMar != null)
|
|
90
|
+
{
|
|
91
|
+
int.TryParse((string?)pgMar.Attribute(W + "top"), out var t);
|
|
92
|
+
int.TryParse((string?)pgMar.Attribute(W + "bottom"), out var b);
|
|
93
|
+
int.TryParse((string?)pgMar.Attribute(W + "left"), out var l);
|
|
94
|
+
int.TryParse((string?)pgMar.Attribute(W + "right"), out var r);
|
|
95
|
+
margins = new(t, b, l, r);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new(pw, ph, margins);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private string? ExtractDefaultFont(string docxPath)
|
|
102
|
+
{
|
|
103
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
104
|
+
var entry = zip.GetEntry("word/styles.xml");
|
|
105
|
+
if (entry == null) return null;
|
|
106
|
+
|
|
107
|
+
using var stream = entry.Open();
|
|
108
|
+
var doc = XDocument.Load(stream);
|
|
109
|
+
|
|
110
|
+
var defaultStyle = doc.Descendants(W + "style")
|
|
111
|
+
.FirstOrDefault(s => (string?)s.Attribute(W + "type") == "paragraph"
|
|
112
|
+
&& (string?)s.Attribute(W + "default") == "1");
|
|
113
|
+
|
|
114
|
+
return (string?)defaultStyle?.Descendants(W + "rFonts").FirstOrDefault()?.Attribute(W + "ascii");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private void ValidateHeadingFontHierarchy(string docxPath, GateCheckResult result)
|
|
118
|
+
{
|
|
119
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
120
|
+
var entry = zip.GetEntry("word/styles.xml");
|
|
121
|
+
if (entry == null) return;
|
|
122
|
+
|
|
123
|
+
using var stream = entry.Open();
|
|
124
|
+
var doc = XDocument.Load(stream);
|
|
125
|
+
|
|
126
|
+
var headingSizes = new SortedDictionary<int, int>();
|
|
127
|
+
foreach (var style in doc.Descendants(W + "style"))
|
|
128
|
+
{
|
|
129
|
+
var id = (string?)style.Attribute(W + "styleId");
|
|
130
|
+
if (id == null || !id.StartsWith("Heading", StringComparison.OrdinalIgnoreCase)) continue;
|
|
131
|
+
|
|
132
|
+
var numPart = id.AsSpan(7);
|
|
133
|
+
if (!int.TryParse(numPart, out var level)) continue;
|
|
134
|
+
|
|
135
|
+
var sz = (string?)style.Descendants(W + "sz").FirstOrDefault()?.Attribute(W + "val");
|
|
136
|
+
if (sz != null && int.TryParse(sz, out var hps))
|
|
137
|
+
headingSizes[level] = hps;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
int prevSize = int.MaxValue;
|
|
141
|
+
foreach (var (level, size) in headingSizes)
|
|
142
|
+
{
|
|
143
|
+
if (size > prevSize)
|
|
144
|
+
result.Violations.Add($"Heading{level} ({size / 2}pt) is larger than a higher-level heading ({prevSize / 2}pt)");
|
|
145
|
+
prevSize = size;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
namespace MiniMaxAIDocx.Core.Validation;
|
|
2
|
+
|
|
3
|
+
public class ValidationResult
|
|
4
|
+
{
|
|
5
|
+
public bool IsValid => Errors.Count == 0;
|
|
6
|
+
public List<ValidationError> Errors { get; set; } = new();
|
|
7
|
+
public List<ValidationError> Warnings { get; set; } = new();
|
|
8
|
+
|
|
9
|
+
public void Merge(ValidationResult other)
|
|
10
|
+
{
|
|
11
|
+
Errors.AddRange(other.Errors);
|
|
12
|
+
Warnings.AddRange(other.Warnings);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public class ValidationError
|
|
17
|
+
{
|
|
18
|
+
public int LineNumber { get; set; }
|
|
19
|
+
public int LinePosition { get; set; }
|
|
20
|
+
public string Element { get; set; } = "";
|
|
21
|
+
public string Message { get; set; } = "";
|
|
22
|
+
public string Severity { get; set; } = "Error";
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
using System.IO.Compression;
|
|
2
|
+
using System.Xml;
|
|
3
|
+
using System.Xml.Schema;
|
|
4
|
+
|
|
5
|
+
namespace MiniMaxAIDocx.Core.Validation;
|
|
6
|
+
|
|
7
|
+
public class XsdValidator
|
|
8
|
+
{
|
|
9
|
+
public ValidationResult Validate(string docxPath, string xsdPath)
|
|
10
|
+
{
|
|
11
|
+
using var zip = ZipFile.OpenRead(docxPath);
|
|
12
|
+
var entry = zip.GetEntry("word/document.xml")
|
|
13
|
+
?? throw new InvalidOperationException("DOCX does not contain word/document.xml");
|
|
14
|
+
|
|
15
|
+
using var stream = entry.Open();
|
|
16
|
+
using var reader = new StreamReader(stream);
|
|
17
|
+
var xmlContent = reader.ReadToEnd();
|
|
18
|
+
|
|
19
|
+
return ValidateXml(xmlContent, xsdPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public ValidationResult ValidateXml(string xmlContent, string xsdPath)
|
|
23
|
+
{
|
|
24
|
+
var result = new ValidationResult();
|
|
25
|
+
var settings = new XmlReaderSettings();
|
|
26
|
+
|
|
27
|
+
var schemaSet = new XmlSchemaSet();
|
|
28
|
+
schemaSet.Add(null, xsdPath);
|
|
29
|
+
settings.Schemas = schemaSet;
|
|
30
|
+
settings.ValidationType = ValidationType.Schema;
|
|
31
|
+
settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
|
|
32
|
+
|
|
33
|
+
settings.ValidationEventHandler += (sender, e) =>
|
|
34
|
+
{
|
|
35
|
+
var error = new ValidationError
|
|
36
|
+
{
|
|
37
|
+
LineNumber = e.Exception?.LineNumber ?? 0,
|
|
38
|
+
LinePosition = e.Exception?.LinePosition ?? 0,
|
|
39
|
+
Message = e.Message,
|
|
40
|
+
Severity = e.Severity == XmlSeverityType.Warning ? "Warning" : "Error"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (e.Severity == XmlSeverityType.Warning)
|
|
44
|
+
result.Warnings.Add(error);
|
|
45
|
+
else
|
|
46
|
+
result.Errors.Add(error);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
using var stringReader = new StringReader(xmlContent);
|
|
50
|
+
using var xmlReader = XmlReader.Create(stringReader, settings);
|
|
51
|
+
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
while (xmlReader.Read()) { }
|
|
55
|
+
}
|
|
56
|
+
catch (XmlException ex)
|
|
57
|
+
{
|
|
58
|
+
result.Errors.Add(new ValidationError
|
|
59
|
+
{
|
|
60
|
+
LineNumber = ex.LineNumber,
|
|
61
|
+
LinePosition = ex.LinePosition,
|
|
62
|
+
Message = $"XML parse error: {ex.Message}",
|
|
63
|
+
Severity = "Error"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# minimax-docx Quick Environment Check
|
|
3
|
+
# Cross-platform: macOS, Linux, WSL, Git Bash
|
|
4
|
+
# Run this BEFORE any minimax-docx operation. Use setup.sh for initial installation.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
9
|
+
DOTNET_DIR="$SCRIPT_DIR/dotnet"
|
|
10
|
+
|
|
11
|
+
# Force English output for dotnet CLI
|
|
12
|
+
export DOTNET_CLI_UI_LANGUAGE=en
|
|
13
|
+
|
|
14
|
+
echo "=== minimax-docx Environment Check ==="
|
|
15
|
+
echo ""
|
|
16
|
+
|
|
17
|
+
STATUS="READY"
|
|
18
|
+
WARNINGS=0
|
|
19
|
+
|
|
20
|
+
# --- Detect platform ---
|
|
21
|
+
OS="unknown"
|
|
22
|
+
case "$(uname -s)" in
|
|
23
|
+
Darwin) OS="macos" ;;
|
|
24
|
+
Linux)
|
|
25
|
+
OS="linux"
|
|
26
|
+
grep -qi microsoft /proc/version 2>/dev/null && OS="wsl"
|
|
27
|
+
;;
|
|
28
|
+
MINGW*|MSYS*|CYGWIN*) OS="windows-shell" ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
# --- Critical: .NET SDK ---
|
|
32
|
+
if ! command -v dotnet &>/dev/null; then
|
|
33
|
+
printf "[FAIL] %-14s not found\n" "dotnet"
|
|
34
|
+
echo ""
|
|
35
|
+
echo " .NET SDK is REQUIRED. Install it:"
|
|
36
|
+
case "$OS" in
|
|
37
|
+
macos) echo " brew install --cask dotnet-sdk" ;;
|
|
38
|
+
linux|wsl)
|
|
39
|
+
echo " # Option 1: Microsoft install script"
|
|
40
|
+
echo " wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh"
|
|
41
|
+
echo " chmod +x /tmp/dotnet-install.sh && /tmp/dotnet-install.sh --channel 8.0"
|
|
42
|
+
echo " # Option 2 (Ubuntu/Debian): sudo apt-get install -y dotnet-sdk-8.0"
|
|
43
|
+
;;
|
|
44
|
+
windows-shell) echo " winget install Microsoft.DotNet.SDK.8" ;;
|
|
45
|
+
*) echo " https://dotnet.microsoft.com/download" ;;
|
|
46
|
+
esac
|
|
47
|
+
echo ""
|
|
48
|
+
echo " Or run the full setup: bash scripts/setup.sh"
|
|
49
|
+
echo ""
|
|
50
|
+
STATUS="NOT READY"
|
|
51
|
+
else
|
|
52
|
+
local_ver=$(dotnet --version 2>/dev/null || echo "0.0.0")
|
|
53
|
+
local_major="${local_ver%%.*}"
|
|
54
|
+
if [ "$local_major" -ge 8 ] 2>/dev/null; then
|
|
55
|
+
printf "[OK] %-14s %s (>= 8.0)\n" "dotnet" "$local_ver"
|
|
56
|
+
else
|
|
57
|
+
printf "[FAIL] %-14s %s (requires >= 8.0)\n" "dotnet" "$local_ver"
|
|
58
|
+
STATUS="NOT READY"
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# --- Critical: NuGet packages ---
|
|
63
|
+
if [ -d "$DOTNET_DIR" ]; then
|
|
64
|
+
if [ -f "$DOTNET_DIR/MiniMaxAIDocx.Cli/bin/Debug/net10.0/MiniMaxAIDocx.Cli.dll" ] || \
|
|
65
|
+
[ -f "$DOTNET_DIR/MiniMaxAIDocx.Cli/bin/Debug/net8.0/MiniMaxAIDocx.Cli.dll" ]; then
|
|
66
|
+
printf "[OK] %-14s built\n" "project"
|
|
67
|
+
else
|
|
68
|
+
# Try restore + build
|
|
69
|
+
if dotnet restore "$DOTNET_DIR" --verbosity quiet &>/dev/null; then
|
|
70
|
+
printf "[OK] %-14s packages restored\n" "nuget"
|
|
71
|
+
if dotnet build "$DOTNET_DIR" --verbosity quiet --no-restore &>/dev/null; then
|
|
72
|
+
printf "[OK] %-14s build succeeded\n" "project"
|
|
73
|
+
else
|
|
74
|
+
printf "[FAIL] %-14s build failed (run: dotnet build %s)\n" "project" "$DOTNET_DIR"
|
|
75
|
+
STATUS="NOT READY"
|
|
76
|
+
fi
|
|
77
|
+
else
|
|
78
|
+
printf "[FAIL] %-14s restore failed\n" "nuget"
|
|
79
|
+
echo ""
|
|
80
|
+
echo " Common causes:"
|
|
81
|
+
echo " - No internet access (NuGet needs to download packages)"
|
|
82
|
+
echo " - Corporate proxy blocking nuget.org"
|
|
83
|
+
echo " - SSL certificate issues (try: dotnet nuget list source)"
|
|
84
|
+
echo ""
|
|
85
|
+
STATUS="NOT READY"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
printf "[FAIL] %-14s directory not found: %s\n" "project" "$DOTNET_DIR"
|
|
90
|
+
STATUS="NOT READY"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# --- Optional: pandoc ---
|
|
94
|
+
if command -v pandoc &>/dev/null; then
|
|
95
|
+
pandoc_ver=$(pandoc --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || echo "?")
|
|
96
|
+
printf "[OK] %-14s %s (content preview)\n" "pandoc" "$pandoc_ver"
|
|
97
|
+
else
|
|
98
|
+
printf "[WARN] %-14s not found — docx_preview.sh will use fallback\n" "pandoc"
|
|
99
|
+
WARNINGS=$((WARNINGS + 1))
|
|
100
|
+
case "$OS" in
|
|
101
|
+
macos) echo " Install: brew install pandoc" ;;
|
|
102
|
+
linux|wsl) echo " Install: sudo apt-get install pandoc # or dnf/pacman" ;;
|
|
103
|
+
windows-shell) echo " Install: winget install JohnMacFarlane.Pandoc" ;;
|
|
104
|
+
esac
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# --- Optional: LibreOffice ---
|
|
108
|
+
if command -v soffice &>/dev/null; then
|
|
109
|
+
soffice_ver=$(soffice --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || echo "?")
|
|
110
|
+
printf "[OK] %-14s %s (.doc conversion)\n" "soffice" "$soffice_ver"
|
|
111
|
+
else
|
|
112
|
+
# Check common paths
|
|
113
|
+
soffice_found=false
|
|
114
|
+
for p in \
|
|
115
|
+
"/Applications/LibreOffice.app/Contents/MacOS/soffice" \
|
|
116
|
+
"/usr/lib/libreoffice/program/soffice" \
|
|
117
|
+
"/snap/bin/libreoffice" \
|
|
118
|
+
"/opt/libreoffice/program/soffice"; do
|
|
119
|
+
if [ -x "$p" ]; then
|
|
120
|
+
printf "[OK] %-14s found at %s (.doc conversion)\n" "soffice" "$p"
|
|
121
|
+
soffice_found=true
|
|
122
|
+
break
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
if ! $soffice_found; then
|
|
126
|
+
printf "[WARN] %-14s not found — .doc files cannot be converted\n" "soffice"
|
|
127
|
+
WARNINGS=$((WARNINGS + 1))
|
|
128
|
+
case "$OS" in
|
|
129
|
+
macos) echo " Install: brew install --cask libreoffice" ;;
|
|
130
|
+
linux|wsl) echo " Install: sudo apt-get install libreoffice-core" ;;
|
|
131
|
+
windows-shell) echo " Install: winget install TheDocumentFoundation.LibreOffice" ;;
|
|
132
|
+
esac
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Optional: zip/unzip ---
|
|
137
|
+
zip_ok=true
|
|
138
|
+
if ! command -v zip &>/dev/null; then
|
|
139
|
+
printf "[WARN] %-14s not found (optional, .NET handles DOCX natively)\n" "zip"
|
|
140
|
+
zip_ok=false
|
|
141
|
+
WARNINGS=$((WARNINGS + 1))
|
|
142
|
+
fi
|
|
143
|
+
if ! command -v unzip &>/dev/null; then
|
|
144
|
+
printf "[WARN] %-14s not found (optional, .NET handles DOCX natively)\n" "unzip"
|
|
145
|
+
zip_ok=false
|
|
146
|
+
WARNINGS=$((WARNINGS + 1))
|
|
147
|
+
fi
|
|
148
|
+
if $zip_ok; then
|
|
149
|
+
printf "[OK] %-14s available\n" "zip/unzip"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# --- Encoding check ---
|
|
153
|
+
current_lang="${LANG:-}"
|
|
154
|
+
if [ -n "$current_lang" ] && echo "$current_lang" | grep -qi "utf-8\|utf8"; then
|
|
155
|
+
printf "[OK] %-14s %s\n" "locale" "$current_lang"
|
|
156
|
+
else
|
|
157
|
+
if [ -z "$current_lang" ]; then
|
|
158
|
+
printf "[WARN] %-14s LANG not set (CJK text may have issues)\n" "locale"
|
|
159
|
+
else
|
|
160
|
+
printf "[WARN] %-14s %s (not UTF-8, CJK text may have issues)\n" "locale" "$current_lang"
|
|
161
|
+
fi
|
|
162
|
+
WARNINGS=$((WARNINGS + 1))
|
|
163
|
+
echo " Fix: export LANG=en_US.UTF-8"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# --- Shell script permissions ---
|
|
167
|
+
perm_issues=0
|
|
168
|
+
for s in "$SCRIPT_DIR"/*.sh; do
|
|
169
|
+
if [ -f "$s" ] && [ ! -x "$s" ]; then
|
|
170
|
+
perm_issues=$((perm_issues + 1))
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
if [ "$perm_issues" -gt 0 ]; then
|
|
174
|
+
printf "[WARN] %-14s %d script(s) not executable\n" "permissions" "$perm_issues"
|
|
175
|
+
echo " Fix: chmod +x scripts/*.sh"
|
|
176
|
+
WARNINGS=$((WARNINGS + 1))
|
|
177
|
+
else
|
|
178
|
+
printf "[OK] %-14s all scripts executable\n" "permissions"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# --- Result ---
|
|
182
|
+
echo ""
|
|
183
|
+
if [ "$STATUS" = "READY" ]; then
|
|
184
|
+
if [ "$WARNINGS" -gt 0 ]; then
|
|
185
|
+
echo "Status: READY (with $WARNINGS warning(s) — optional features may be limited)"
|
|
186
|
+
else
|
|
187
|
+
echo "Status: READY"
|
|
188
|
+
fi
|
|
189
|
+
else
|
|
190
|
+
echo "Status: NOT READY"
|
|
191
|
+
echo ""
|
|
192
|
+
echo "Critical dependencies missing. Run the full setup:"
|
|
193
|
+
echo " bash scripts/setup.sh # macOS / Linux / WSL"
|
|
194
|
+
echo " powershell scripts/setup.ps1 # Windows PowerShell"
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|