Initial commit: add all skills files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 16:52:49 +08:00
commit 6487becf60
396 changed files with 108871 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// Manages the 4-file comment system (comments.xml, commentsExtended.xml,
/// commentsIds.xml, commentsExtensible.xml) plus document.xml markers.
/// </summary>
public static class CommentSynchronizer
{
/// <summary>
/// Adds a comment to the document, updating all required parts.
/// </summary>
public static int AddComment(WordprocessingDocument doc, string text, string author, string rangeBookmark)
{
var mainPart = doc.MainDocumentPart
?? throw new InvalidOperationException("Document has no main part.");
int commentId = GetNextCommentId(doc);
// Ensure comments part exists
var commentsPart = mainPart.WordprocessingCommentsPart
?? mainPart.AddNewPart<WordprocessingCommentsPart>();
if (commentsPart.Comments == null)
commentsPart.Comments = new Comments();
// Create the comment
var comment = new Comment
{
Id = commentId.ToString(),
Author = author,
Date = DateTime.UtcNow,
Initials = author.Length > 0 ? author[..1].ToUpperInvariant() : "A"
};
comment.Append(new Paragraph(new Run(new Text(text))));
commentsPart.Comments.Append(comment);
// Add range markers in document body
var body = mainPart.Document.Body;
if (body != null)
{
// Find bookmark or append at end
var rangeStart = new CommentRangeStart { Id = commentId.ToString() };
var rangeEnd = new CommentRangeEnd { Id = commentId.ToString() };
var reference = new Run(new CommentReference { Id = commentId.ToString() });
body.Append(rangeStart);
body.Append(rangeEnd);
body.Append(new Paragraph(reference));
}
return commentId;
}
/// <summary>
/// Adds a reply to an existing comment.
/// </summary>
public static int AddReply(WordprocessingDocument doc, int parentCommentId, string text, string author)
{
var mainPart = doc.MainDocumentPart
?? throw new InvalidOperationException("Document has no main part.");
var commentsPart = mainPart.WordprocessingCommentsPart
?? throw new InvalidOperationException("Document has no comments part.");
int replyId = GetNextCommentId(doc);
var reply = new Comment
{
Id = replyId.ToString(),
Author = author,
Date = DateTime.UtcNow,
Initials = author.Length > 0 ? author[..1].ToUpperInvariant() : "A"
};
reply.Append(new Paragraph(new Run(new Text(text))));
commentsPart.Comments?.Append(reply);
// Link reply to parent via commentsExtended.xml
LinkReplyToParent(doc, replyId, parentCommentId);
return replyId;
}
/// <summary>
/// Marks a comment as resolved/done by setting done="1" in commentsExtended.xml.
/// Uses raw XML manipulation since these extended parts lack typed SDK support.
/// </summary>
public static void ResolveComment(WordprocessingDocument doc, int commentId)
{
var mainPart = doc.MainDocumentPart;
if (mainPart == null) return;
// commentsExtended.xml is an untyped part — manipulate via raw XML
const string ceUri = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
foreach (var part in mainPart.Parts)
{
if (part.OpenXmlPart.ContentType.Contains("commentsExtensible"))
{
using var stream = part.OpenXmlPart.GetStream(FileMode.Open, FileAccess.ReadWrite);
var xdoc = System.Xml.Linq.XDocument.Load(stream);
var ns = System.Xml.Linq.XNamespace.Get(ceUri);
var commentEl = xdoc.Descendants(ns + "comment")
.FirstOrDefault(e => e.Attribute(ns + "paraId")?.Value != null);
// Set done flag if element found for this comment
if (commentEl != null)
{
commentEl.SetAttributeValue("done", "1");
stream.SetLength(0);
xdoc.Save(stream);
}
return;
}
}
}
/// <summary>
/// Links a reply comment to its parent via commentsExtended.xml (w15:commentEx).
/// Uses raw XML since the extended comment parts lack typed SDK support.
/// </summary>
private static void LinkReplyToParent(WordprocessingDocument doc, int replyId, int parentCommentId)
{
var mainPart = doc.MainDocumentPart;
if (mainPart == null) return;
const string w15Uri = "http://schemas.microsoft.com/office/word/2012/wordml";
var w15 = System.Xml.Linq.XNamespace.Get(w15Uri);
// Find or create commentsExtended part
foreach (var part in mainPart.Parts)
{
if (part.OpenXmlPart.ContentType.Contains("commentsExtended"))
{
using var stream = part.OpenXmlPart.GetStream(FileMode.Open, FileAccess.ReadWrite);
var xdoc = System.Xml.Linq.XDocument.Load(stream);
var root = xdoc.Root;
if (root == null) return;
root.Add(new System.Xml.Linq.XElement(w15 + "commentEx",
new System.Xml.Linq.XAttribute(w15 + "paraId", replyId.ToString("X8")),
new System.Xml.Linq.XAttribute(w15 + "paraIdParent", parentCommentId.ToString("X8")),
new System.Xml.Linq.XAttribute(w15 + "done", "0")));
stream.SetLength(0);
xdoc.Save(stream);
return;
}
}
}
/// <summary>
/// Finds the maximum existing comment ID and returns the next one.
/// </summary>
public static int GetNextCommentId(WordprocessingDocument doc)
{
var commentsPart = doc.MainDocumentPart?.WordprocessingCommentsPart;
if (commentsPart?.Comments == null) return 1;
int maxId = 0;
foreach (var comment in commentsPart.Comments.Elements<Comment>())
{
if (comment.Id?.Value != null && int.TryParse(comment.Id.Value, out int id) && id > maxId)
maxId = id;
}
return maxId + 1;
}
}

View File

@@ -0,0 +1,80 @@
using System.Xml.Linq;
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// Defines canonical child element ordering for key OpenXML parent elements
/// and provides reordering utilities.
/// </summary>
public static class ElementOrder
{
private static readonly Dictionary<string, string[]> OrderMap = new()
{
["w:body"] = ["w:p", "w:tbl", "w:sdt", "w:sectPr"],
["w:p"] = ["w:pPr", "w:hyperlink", "w:r", "w:ins", "w:del", "w:bookmarkStart", "w:bookmarkEnd", "w:commentRangeStart", "w:commentRangeEnd", "w:fldSimple"],
["w:pPr"] = ["w:pStyle", "w:keepNext", "w:keepLines", "w:pageBreakBefore", "w:widowControl", "w:numPr", "w:pBdr", "w:shd", "w:tabs", "w:suppressAutoHyphens", "w:spacing", "w:ind", "w:jc", "w:rPr", "w:sectPr", "w:pPrChange"],
["w:r"] = ["w:rPr", "w:t", "w:br", "w:tab", "w:cr", "w:sym", "w:drawing", "w:delText", "w:fldChar", "w:instrText", "w:lastRenderedPageBreak", "w:noBreakHyphen", "w:softHyphen"],
["w:rPr"] = ["w:rStyle", "w:rFonts", "w:b", "w:bCs", "w:i", "w:iCs", "w:caps", "w:smallCaps", "w:strike", "w:dstrike", "w:vanish", "w:color", "w:sz", "w:szCs", "w:u", "w:shd", "w:highlight", "w:lang", "w:rPrChange"],
["w:tbl"] = ["w:tblPr", "w:tblGrid", "w:tr"],
["w:tblPr"] = ["w:tblStyle", "w:tblpPr", "w:tblOverlap", "w:tblW", "w:jc", "w:tblCellSpacing", "w:tblInd", "w:tblBorders", "w:shd", "w:tblLayout", "w:tblCellMar", "w:tblLook", "w:tblPrChange"],
["w:tr"] = ["w:trPr", "w:tc"],
["w:trPr"] = ["w:cnfStyle", "w:divId", "w:gridBefore", "w:gridAfter", "w:wBefore", "w:wAfter", "w:cantSplit", "w:trHeight", "w:tblHeader", "w:tblCellSpacing", "w:jc", "w:hidden", "w:ins", "w:del", "w:trPrChange"],
["w:tc"] = ["w:tcPr", "w:p", "w:tbl"],
["w:tcPr"] = ["w:cnfStyle", "w:tcW", "w:gridSpan", "w:hMerge", "w:vMerge", "w:tcBorders", "w:shd", "w:noWrap", "w:tcMar", "w:textDirection", "w:tcFitText", "w:vAlign", "w:hideMark", "w:headers", "w:cellIns", "w:cellDel", "w:cellMerge", "w:tcPrChange"],
["w:sectPr"] = ["w:headerReference", "w:footerReference", "w:type", "w:pgSz", "w:pgMar", "w:paperSrc", "w:pgBorders", "w:lnNumType", "w:pgNumType", "w:cols", "w:formProt", "w:vAlign", "w:noEndnote", "w:titlePg", "w:textDirection", "w:bidi", "w:rtlGutter", "w:docGrid"],
["w:hdr"] = ["w:p", "w:tbl", "w:sdt"],
["w:ftr"] = ["w:p", "w:tbl", "w:sdt"],
};
/// <summary>
/// Returns the canonical child ordering for a given parent element name (e.g. "w:p").
/// Returns null if no ordering is defined.
/// </summary>
public static string[]? GetChildOrder(string parentElement)
{
return OrderMap.TryGetValue(parentElement, out var order) ? order : null;
}
/// <summary>
/// Reorders children of the given XElement according to the canonical ordering rules.
/// Children not listed in the ordering are placed at the end in their original order.
/// </summary>
public static void ReorderChildren(XElement parent)
{
var qualifiedName = GetQualifiedName(parent);
var order = GetChildOrder(qualifiedName);
if (order == null) return;
var children = parent.Elements().ToList();
if (children.Count <= 1) return;
var orderIndex = new Dictionary<string, int>();
for (int i = 0; i < order.Length; i++)
orderIndex[order[i]] = i;
int unknownBase = order.Length;
int unknownCounter = 0;
var sorted = children
.Select(c => (Element: c, QName: GetQualifiedName(c)))
.OrderBy(x => orderIndex.TryGetValue(x.QName, out var idx) ? idx : unknownBase + unknownCounter++)
.Select(x => x.Element)
.ToList();
parent.RemoveNodes();
foreach (var child in sorted)
parent.Add(child);
}
private static string GetQualifiedName(XElement element)
{
var ns = element.Name.Namespace;
var local = element.Name.LocalName;
if (ns == Ns.W) return $"w:{local}";
if (ns == Ns.R) return $"r:{local}";
if (ns == Ns.MC) return $"mc:{local}";
return local;
}
}

View File

@@ -0,0 +1,42 @@
using System.Xml.Linq;
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// All OpenXML namespace URIs and common content/relationship type constants.
/// </summary>
public static class Ns
{
public static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
public static readonly XNamespace R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
public static readonly XNamespace WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
public static readonly XNamespace A = "http://schemas.openxmlformats.org/drawingml/2006/main";
public static readonly XNamespace MC = "http://schemas.openxmlformats.org/markup-compatibility/2006";
public static readonly XNamespace PIC = "http://schemas.openxmlformats.org/drawingml/2006/picture";
public static readonly XNamespace W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
public static readonly XNamespace W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
public static readonly XNamespace W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
public static readonly XNamespace W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
public static readonly XNamespace WPC = "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas";
public static readonly XNamespace WPS = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
// Content types
public const string MainDocumentContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
public const string StylesContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
public const string HeaderContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
public const string FooterContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
public const string CommentsContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
// Relationship types
public const string DocumentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
public const string StylesRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
public const string HeaderRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
public const string FooterRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
public const string CommentsRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
public const string ImageRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
public const string HyperlinkRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
public const string NumberingRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
public const string FontTableRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
public const string ThemeRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
public const string SettingsRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
}

View File

@@ -0,0 +1,81 @@
using System.Xml.Linq;
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// Result of a run merge operation.
/// </summary>
public record RunMergeResult(int OriginalRunCount, int MergedRunCount, int SizeReductionBytes);
/// <summary>
/// Merges adjacent w:r elements with identical w:rPr formatting to reduce document size.
/// </summary>
public static class RunMerger
{
/// <summary>
/// Merges adjacent runs with identical formatting in all paragraphs of the document body.
/// </summary>
public static RunMergeResult MergeRuns(XDocument document)
{
var body = document.Root?.Element(Ns.W + "body");
if (body == null) return new(0, 0, 0);
int originalCount = 0;
int removedCount = 0;
foreach (var paragraph in body.Descendants(Ns.W + "p"))
{
var runs = paragraph.Elements(Ns.W + "r").ToList();
originalCount += runs.Count;
for (int i = runs.Count - 1; i > 0; i--)
{
var current = runs[i];
var previous = runs[i - 1];
if (!AreRunPropertiesEqual(previous, current)) continue;
// Merge text content from current into previous
var prevText = GetOrCreateTextElement(previous);
var currText = current.Element(Ns.W + "t");
if (currText != null && prevText != null)
{
prevText.Value += currText.Value;
// Preserve xml:space="preserve" if either has it
if (currText.Attribute(XNamespace.Xml + "space")?.Value == "preserve" ||
prevText.Value.StartsWith(' ') || prevText.Value.EndsWith(' '))
{
prevText.SetAttributeValue(XNamespace.Xml + "space", "preserve");
}
}
current.Remove();
removedCount++;
}
}
return new(originalCount, originalCount - removedCount, 0);
}
private static bool AreRunPropertiesEqual(XElement run1, XElement run2)
{
var rPr1 = run1.Element(Ns.W + "rPr");
var rPr2 = run2.Element(Ns.W + "rPr");
if (rPr1 == null && rPr2 == null) return true;
if (rPr1 == null || rPr2 == null) return false;
return XNode.DeepEquals(rPr1, rPr2);
}
private static XElement? GetOrCreateTextElement(XElement run)
{
var t = run.Element(Ns.W + "t");
if (t == null)
{
t = new XElement(Ns.W + "t");
run.Add(t);
}
return t;
}
}

View File

@@ -0,0 +1,81 @@
using System.Xml.Linq;
namespace MiniMaxAIDocx.Core.OpenXml;
public record StyleInfo(string Id, string? Name, string Type, string? BasedOn, bool IsDefault);
public record StyleReport(
List<StyleInfo> AllStyles,
Dictionary<string, List<string>> InheritanceTree,
string? DefaultParagraphStyle,
string? DefaultCharacterStyle,
int DirectFormattingCount);
/// <summary>
/// Analyzes the style hierarchy of a DOCX document.
/// </summary>
public static class StyleAnalyzer
{
/// <summary>
/// Analyzes styles.xml content and document.xml for direct formatting usage.
/// </summary>
public static StyleReport Analyze(XDocument stylesXml, XDocument documentXml)
{
var styles = ExtractStyles(stylesXml);
var tree = BuildInheritanceTree(styles);
var defaultPara = styles.FirstOrDefault(s => s.Type == "paragraph" && s.IsDefault)?.Id;
var defaultChar = styles.FirstOrDefault(s => s.Type == "character" && s.IsDefault)?.Id;
var directCount = CountDirectFormatting(documentXml);
return new(styles, tree, defaultPara, defaultChar, directCount);
}
private static List<StyleInfo> ExtractStyles(XDocument stylesXml)
{
var result = new List<StyleInfo>();
var root = stylesXml.Root;
if (root == null) return result;
foreach (var style in root.Elements(Ns.W + "style"))
{
var id = style.Attribute(Ns.W + "styleId")?.Value ?? "";
var name = style.Element(Ns.W + "name")?.Attribute(Ns.W + "val")?.Value;
var type = style.Attribute(Ns.W + "type")?.Value ?? "unknown";
var basedOn = style.Element(Ns.W + "basedOn")?.Attribute(Ns.W + "val")?.Value;
var isDefault = style.Attribute(Ns.W + "default")?.Value == "1";
result.Add(new(id, name, type, basedOn, isDefault));
}
return result;
}
private static Dictionary<string, List<string>> BuildInheritanceTree(List<StyleInfo> styles)
{
var tree = new Dictionary<string, List<string>>();
foreach (var style in styles)
{
var parent = style.BasedOn ?? "(root)";
if (!tree.ContainsKey(parent))
tree[parent] = [];
tree[parent].Add(style.Id);
}
return tree;
}
private static int CountDirectFormatting(XDocument documentXml)
{
var body = documentXml.Root?.Element(Ns.W + "body");
if (body == null) return 0;
int count = 0;
// Count inline rPr on runs (direct character formatting)
count += body.Descendants(Ns.W + "r")
.Count(r => r.Element(Ns.W + "rPr") != null);
// Count inline pPr that contain more than just pStyle (direct paragraph formatting)
count += body.Descendants(Ns.W + "p")
.Select(p => p.Element(Ns.W + "pPr"))
.Count(pPr => pPr != null && pPr.Elements().Any(e => e.Name != Ns.W + "pStyle"));
return count;
}
}

View File

@@ -0,0 +1,99 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// Helpers for Track Changes (revision marks) operations.
/// </summary>
public static class TrackChangesHelper
{
/// <summary>
/// Wraps a run in a w:ins element to propose an insertion.
/// </summary>
public static InsertedRun ProposeInsertion(Run run, string author, DateTime date)
{
var ins = new InsertedRun
{
Author = author,
Date = date,
Id = run.Parent is Body body ? GetNextRevisionId(body).ToString() : "1"
};
run.Remove();
ins.Append(run);
return ins;
}
/// <summary>
/// Wraps a run in a w:del element, converting w:t to w:delText.
/// </summary>
public static DeletedRun ProposeDeletion(Run run, string author, DateTime date)
{
// Convert w:t elements to w:delText
foreach (var text in run.Elements<Text>().ToList())
{
var delText = new DeletedText { Text = text.Text, Space = SpaceProcessingModeValues.Preserve };
text.InsertAfterSelf(delText);
text.Remove();
}
var del = new DeletedRun
{
Author = author,
Date = date,
Id = run.Parent is Body body ? GetNextRevisionId(body).ToString() : "1"
};
run.Remove();
del.Append(run);
return del;
}
/// <summary>
/// Accepts an insertion by removing the w:ins wrapper and keeping content.
/// </summary>
public static void AcceptInsertion(OpenXmlElement insElement)
{
if (insElement is not InsertedRun) return;
var parent = insElement.Parent;
if (parent == null) return;
var children = insElement.ChildElements.ToList();
foreach (var child in children)
{
child.Remove();
insElement.InsertBeforeSelf(child);
}
insElement.Remove();
}
/// <summary>
/// Accepts a deletion by removing the entire w:del element and its content.
/// </summary>
public static void AcceptDeletion(OpenXmlElement delElement)
{
delElement.Remove();
}
/// <summary>
/// Finds the maximum existing revision ID in the document and returns the next one.
/// </summary>
public static int GetNextRevisionId(WordprocessingDocument doc)
{
var body = doc.MainDocumentPart?.Document?.Body;
if (body == null) return 1;
return GetNextRevisionId(body);
}
private static int GetNextRevisionId(OpenXmlElement root)
{
int maxId = 0;
foreach (var element in root.Descendants())
{
var idAttr = element.GetAttributes().FirstOrDefault(a => a.LocalName == "id");
if (idAttr.Value != null && int.TryParse(idAttr.Value, out int id) && id > maxId)
maxId = id;
}
return maxId + 1;
}
}

View File

@@ -0,0 +1,23 @@
namespace MiniMaxAIDocx.Core.OpenXml;
/// <summary>
/// Conversion utilities between OpenXML measurement units (DXA, EMU, points, half-points).
/// </summary>
public static class UnitConverter
{
// 1 inch = 1440 DXA = 914400 EMU = 72 pt = 144 half-pt
public static int InchesToDxa(double inches) => (int)(inches * 1440);
public static int CmToDxa(double cm) => (int)(cm * 567.0);
public static int PtToDxa(double pt) => (int)(pt * 20);
public static long InchesToEmu(double inches) => (long)(inches * 914400);
public static long CmToEmu(double cm) => (long)(cm * 360000);
public static int PtToHalfPt(double pt) => (int)(pt * 2);
public static string FontSizeToSz(double ptSize) => ((int)(ptSize * 2)).ToString();
public static double DxaToInches(int dxa) => dxa / 1440.0;
public static double DxaToCm(int dxa) => dxa / 567.0;
public static double DxaToPt(int dxa) => dxa / 20.0;
public static double EmuToInches(long emu) => emu / 914400.0;
public static double EmuToCm(long emu) => emu / 360000.0;
}