Upload to server

uploading
This commit is contained in:
2025-08-02 05:20:17 +07:00
commit a5eccbd452
984 changed files with 3031800 additions and 0 deletions

View File

@@ -0,0 +1,672 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using DocumentFormat.OpenXml.Packaging;
using System.IO;
namespace OpenXmlPowerTools
{
public partial class WmlDocument : OpenXmlPowerToolsDocument
{
public WmlDocument MergeComments(WmlDocument other)
{
return CommentMerger.MergeComments(this, other);
}
}
public class CommentMergerInternalException : Exception
{
public CommentMergerInternalException(string message) : base(message) { }
}
public class CommentMergerDifferingContentsException : Exception
{
public CommentMergerDifferingContentsException(string message) : base(message) { }
}
public class CommentMergerUnlockedDocumentException : Exception
{
public CommentMergerUnlockedDocumentException(string message) : base(message) { }
}
public class CommentMerger
{
public static WmlDocument MergeComments(WmlDocument document1, WmlDocument document2)
{
return MergeComments(document1, document2, true);
}
public static WmlDocument MergeComments(WmlDocument document1, WmlDocument document2,
bool ensureLocked)
{
WmlDocument cDoc1 = new WmlDocument(document1);
WmlDocument cDoc2 = new WmlDocument(document2);
using (OpenXmlMemoryStreamDocument streamDoc1 = new OpenXmlMemoryStreamDocument(cDoc1))
using (WordprocessingDocument doc1 = streamDoc1.GetWordprocessingDocument())
using (OpenXmlMemoryStreamDocument streamDoc2 = new OpenXmlMemoryStreamDocument(cDoc2))
using (WordprocessingDocument doc2 = streamDoc2.GetWordprocessingDocument())
{
SimplifyMarkupSettings mss = new SimplifyMarkupSettings()
{
RemoveProof = true,
RemoveRsidInfo = true,
RemoveGoBackBookmark = true,
};
MarkupSimplifier.SimplifyMarkup(doc1, mss);
MarkupSimplifier.SimplifyMarkup(doc2, mss);
// If documents don't contain the same content, then don't attempt to merge comments.
bool same = DocumentComparer.CompareDocuments(doc1, doc2);
if (!same)
throw new CommentMergerDifferingContentsException(
"Documents do not contain the same content");
if (doc1.MainDocumentPart.WordprocessingCommentsPart == null &&
doc2.MainDocumentPart.WordprocessingCommentsPart == null)
return new WmlDocument(document1);
if (doc1.MainDocumentPart.WordprocessingCommentsPart != null &&
doc2.MainDocumentPart.WordprocessingCommentsPart == null)
return new WmlDocument(document1);
if (doc1.MainDocumentPart.WordprocessingCommentsPart == null &&
doc2.MainDocumentPart.WordprocessingCommentsPart != null)
return new WmlDocument(document2);
// If either of the documents have no comments, then return the other one.
if (! doc1.MainDocumentPart.WordprocessingCommentsPart.GetXDocument().Root
.Elements(W.comment).Any())
return new WmlDocument(document2);
if (! doc2.MainDocumentPart.WordprocessingCommentsPart.GetXDocument().Root
.Elements(W.comment).Any())
return new WmlDocument(document1);
if (ensureLocked)
{
// If either document is not locked (allowing only commenting), don't attempt to
// merge comments.
if (doc1.ExtendedFilePropertiesPart.GetXDocument().Root
.Element(EP.DocSecurity).Value != "8")
throw new CommentMergerUnlockedDocumentException(
"Document1 is not locked");
if (doc2.ExtendedFilePropertiesPart.GetXDocument().Root
.Element(EP.DocSecurity).Value != "8")
throw new CommentMergerUnlockedDocumentException(
"Document2 is not locked");
}
RenumberCommentsInDoc2(doc1, doc2);
WmlDocument destDoc = new WmlDocument(document1);
using (OpenXmlMemoryStreamDocument streamDoc = new OpenXmlMemoryStreamDocument(destDoc))
{
using (WordprocessingDocument destWDoc = streamDoc.GetWordprocessingDocument())
{
// Merge the comments part.
XDocument commentsPartXDoc = new XDocument(
new XElement(W.comments,
new XAttribute(XNamespace.Xmlns + "w", W.w),
doc1.MainDocumentPart.WordprocessingCommentsPart.GetXDocument().Root.Elements(),
doc2.MainDocumentPart.WordprocessingCommentsPart.GetXDocument().Root.Elements()));
destWDoc.MainDocumentPart.WordprocessingCommentsPart.PutXDocument(commentsPartXDoc);
MergeCommentsInPart(doc1.MainDocumentPart, doc2.MainDocumentPart,
destWDoc.MainDocumentPart, commentsPartXDoc);
}
return streamDoc.GetModifiedWmlDocument();
}
}
}
private static bool IsCommentElement(XElement e)
{
return e.Name == W.commentRangeStart || e.Name == W.commentRangeEnd ||
e.Name == W.commentReference;
}
private static IEnumerable<object> SplitElements(IEnumerable<XElement> elements)
{
IEnumerable<object> splitElements = elements
.Select(e =>
{
if ((e.Name == W.r && e.Elements(W.t).Any()) ||
(e.Name == M.r && e.Elements(M.t).Any()))
{
var grouped = e.Elements().Where(ce => ce.Name != W.rPr && ce.Name != M.rPr)
.GroupAdjacent(ce =>
(ce.Name == W.t || ce.Name == M.t));
return grouped.Select(g =>
{
if (g.Key == true)
{
string s = g.Select(t => (string)t).StringConcatenate();
return s.Select(c => new XElement(e.Name,
e.Attributes(),
e.Elements(W.rPr),
e.Elements(M.rPr),
new XElement(g.First().Name, c)));
}
return g.Select(ce => new XElement(e.Name,
e.Attributes(),
e.Elements(W.rPr),
e.Elements(M.rPr),
ce));
});
}
return (object)e;
});
return splitElements;
}
private class RunOrderingInfo
{
public int Integer;
public bool IsRun;
public RunOrderingInfo(int integer, bool isRun)
{
Integer = integer;
IsRun = isRun;
}
public RunOrderingInfo(int integer)
{
Integer = integer;
IsRun = false;
}
}
private static XName[] RunContentElements = new[]
{
M.t,
W.br,
W.cr,
W.dayLong,
W.dayShort,
W.drawing,
W.endnoteReference,
W.fldChar,
W.footnoteReference,
W.instrText,
W.lastRenderedPageBreak,
W.monthLong,
W.monthShort,
W.noBreakHyphen,
W._object,
W.ptab,
W.ruby,
W.softHyphen,
W.sym,
W.t,
W.tab,
W.yearLong,
W.yearShort,
};
private static void AddAnnotationsToChildren(XElement parent)
{
int index = 1000;
foreach (var ce in parent
.Elements().Where(e => e.Name == W.r || e.Name == M.r)
.Where(r =>
{
foreach (var item in RunContentElements)
if (r.Element(item) != null)
return true;
return false;
}))
{
ce.AddAnnotation(new RunOrderingInfo(index, true));
index += 1000;
}
foreach (var ce in parent.Elements().Where(e => e.Annotation<RunOrderingInfo>() == null))
{
var elementWithAnnoAfter = ce
.ElementsAfterSelf()
.FirstOrDefault(e => e.Annotation<RunOrderingInfo>() != null);
if (elementWithAnnoAfter != null)
{
int nextIndex = elementWithAnnoAfter.Annotation<RunOrderingInfo>().Integer;
var elementsAfter = ce.ElementsAfterSelf()
.TakeWhile(ea => ea.Annotation<RunOrderingInfo>() == null);
ce.AddAnnotation(new RunOrderingInfo(nextIndex - elementsAfter.Count() - 1));
continue;
}
// The element before must have an annotation.
var elementBefore = ce.ElementsBeforeSelfReverseDocumentOrder().FirstOrDefault();
if (elementBefore != null)
{
RunOrderingInfo elementBeforeAnnotation = elementBefore.Annotation<RunOrderingInfo>();
ce.AddAnnotation(new RunOrderingInfo(elementBeforeAnnotation.Integer + 1));
}
}
}
private static XAttribute GetXmlSpaceAttribute(
string textElementValue)
{
if (textElementValue.Length > 0 &&
(textElementValue[0] == ' ' ||
textElementValue[textElementValue.Length - 1] == ' '))
return new XAttribute(XNamespace.Xml + "space",
"preserve");
return null;
}
private static XElement MergeElementWithChildrenCommentElements(XElement e1, XElement e2,
XDocument commentsPartXDoc)
{
if (e1.Name.Namespace != W.w)
Console.WriteLine("processing mathml");
if (e1.Name != e2.Name)
throw new CommentMergerInternalException(
"attempting to merge elements that do not match up.");
var split1 = SplitElements(e1.Elements());
var split2 = SplitElements(e2.Elements());
XElement temp1 = new XElement(e1.Name,
e1.Attributes(),
split1);
XElement temp2 = new XElement(e2.Name,
e2.Attributes(),
split2);
// todo may want to remove following test.
if (temp1.Elements().Where(e => e.Name == W.r || e.Name == M.r).Where(r =>
{
foreach (var item in RunContentElements)
if (r.Element(item) != null)
return true;
return false;
}).Count() !=
temp2.Elements().Where(e => e.Name == W.r || e.Name == M.r).Where(r =>
{
foreach (var item in RunContentElements)
if (r.Element(item) != null)
return true;
return false;
}).Count())
throw new CommentMergerInternalException("runs do not line up");
AddAnnotationsToChildren(temp1);
AddAnnotationsToChildren(temp2);
if (temp1.Elements().Any(e => e.Annotation<RunOrderingInfo>() == null) ||
temp2.Elements().Any(e => e.Annotation<RunOrderingInfo>() == null))
throw new CommentMergerInternalException("some child does not have annotation");
var m = temp1
.Elements()
.Concat(temp2.Elements())
.OrderBy(e => e.Annotation<RunOrderingInfo>().Integer)
.GroupAdjacent(e => e.Annotation<RunOrderingInfo>().Integer);
XElement mergedElementWithSplitRuns = new XElement(e1.Name,
e1.Attributes(),
m.Select(g =>
{
if (g.First().Annotation<RunOrderingInfo>().IsRun)
return (object)g.First();
else
return g;
}));
// The following query serves to remove duplicate comments, i.e. the same exact
// comment from the same person is in each of the documents being merged.
var groupedCommentReferences = mergedElementWithSplitRuns.Elements()
.GroupAdjacent(e =>
{
if (e.Name == W.r || e.Name == M.r)
{
bool onlyOneChild = e.Elements().Where(z => z.Name != W.rPr).Count() == 1;
if (onlyOneChild)
{
XElement child = e.Elements().Where(z => z.Name != W.rPr).First();
if (child.Name == W.commentRangeStart ||
child.Name == W.commentRangeEnd ||
child.Name == W.commentReference)
{
XElement comment = commentsPartXDoc.Root
.Elements(W.comment)
.Where(c => (string)c.Attribute(W.id) == (string)child.Attribute(W.id))
.FirstOrDefault();
string s = child.Name.LocalName + "|" +
comment.Attribute(W.author).Value + "|" +
comment.Attribute(W.date).Value + "|" +
comment.Attribute(W.initials).Value + "|" +
comment.Descendants()
.Where(d => d.Name == W.t || d.Name == M.t)
.Select(t => (string)t).StringConcatenate();
return s;
}
}
}
if (e.Name != W.commentRangeStart &&
e.Name != W.commentRangeEnd &&
e.Name != W.commentReference)
return "NotAComment";
XElement comment2 = commentsPartXDoc.Root
.Elements(W.comment)
.Where(c => (string)c.Attribute(W.id) == (string)e.Attribute(W.id))
.FirstOrDefault();
string s2 = e.Name.LocalName + "|" +
comment2.Attribute(W.author).Value + "|" +
comment2.Attribute(W.date).Value + "|" +
comment2.Attribute(W.initials).Value + "|" +
comment2.Descendants()
.Where(d => d.Name == W.t || d.Name == M.t)
.Select(t => (string)t).StringConcatenate();
return s2;
});
XElement duplicateCommentsEliminated = new XElement(e1.Name,
e1.Attributes(),
groupedCommentReferences
.Select(g =>
{
if (g.Key == "NotAComment")
return (object)g;
return g.First();
}));
var runGroups = duplicateCommentsEliminated.Elements()
.GroupAdjacent(r =>
{
if (r.Name != W.r && r.Name != M.r)
return "NotRuns";
if (! r.Elements().Where(e => e.Name == W.rPr || e.Name == M.rPr).Any())
return "NoRunProperties";
XElement frag = new XElement("frag",
new XAttribute(XNamespace.Xmlns + "w", W.w),
r.Elements().Where(e => e.Name == W.rPr || e.Name == M.rPr));
return frag.ToString(
SaveOptions.DisableFormatting);
});
XElement newParagraph = new XElement(e1.Name,
e1.Attributes(),
runGroups.Select(g =>
{
if (g.Key == "NotRuns")
return (object)g;
if (g.Key == "NoRunProperties")
{
XElement newRun = new XElement(g.First().Name,
g.First().Attributes(),
g.Elements()
.GroupAdjacent(c => c.Name)
.Select(gc =>
{
if (gc.Key != W.t && gc.Key != M.t)
return (object)gc;
string textElementValue =
gc.Select(t => (string)t)
.StringConcatenate();
return new XElement(gc.First().Name.Namespace + "t",
GetXmlSpaceAttribute(
textElementValue),
textElementValue);
}));
return newRun;
}
XElement runPropertyFragment = XElement.Parse(
g.Key);
runPropertyFragment.Elements().Attributes()
.Where(a => a.IsNamespaceDeclaration)
.Remove();
XElement newRunWithProperties = new XElement(g.First().Name.Namespace + "r",
runPropertyFragment.Elements(),
g.Elements()
.Where(e => e.Name != W.rPr && e.Name != M.rPr)
.GroupAdjacent(c => c.Name)
.Select(gc =>
{
if (gc.Key != W.t && gc.Key != M.t)
return (object)gc;
string textElementValue = gc
.Select(t => (string)t)
.StringConcatenate();
return new XElement(gc.Key,
GetXmlSpaceAttribute(textElementValue),
textElementValue);
}));
return newRunWithProperties;
}
));
return newParagraph;
}
private static object MergeElementTransform(XElement e1, XElement e2, XDocument commentsPartXDoc)
{
// todo need better message
if (e1.Name != e2.Name)
throw new CommentMergerDifferingContentsException("Internal error");
if (e1.Elements()
.Where(e => IsCommentElement(e)).Any() || e2.Elements().Where(e => IsCommentElement(e))
.Any())
return MergeElementWithChildrenCommentElements(e1, e2, commentsPartXDoc);
var zippedChildren = e1.Elements().Zip(e2.Elements(), (p1, p2) => new
{
Element1 = p1,
Element2 = p2,
});
return new XElement(e1.Name,
e1.Attributes(),
zippedChildren.Select(z =>
{
if (z.Element1.Name == W.t && z.Element2.Name == W.t &&
z.Element1.Value == z.Element2.Value)
return new XElement(W.t,
GetXmlSpaceAttribute(z.Element1.Value),
z.Element1.Value);
if (z.Element1.Name == M.t && z.Element2.Name == M.t &&
z.Element1.Value == z.Element2.Value)
return new XElement(M.t,
GetXmlSpaceAttribute(z.Element1.Value),
z.Element1.Value);
return MergeElementTransform(z.Element1, z.Element2, commentsPartXDoc);
}));
}
private static void MergeCommentsInPart(OpenXmlPart part1, OpenXmlPart part2,
OpenXmlPart destinationPart, XDocument commentsPartXDoc)
{
XDocument xdoc1 = part1.GetXDocument();
XDocument xdoc2 = part2.GetXDocument();
XElement newRootElement = (XElement)MergeElementTransform(xdoc1.Root, xdoc2.Root,
commentsPartXDoc);
destinationPart.PutXDocument(new XDocument(newRootElement));
}
// todo are there any other elements that need ids fixed? see children of paragraph, run
private static void FixIdsInPart(OpenXmlPart part, int nextCommentId, int nextBookmarkId)
{
if (part == null)
return;
foreach (var element in part.GetXDocument().Root.Descendants()
.Where(e => e.Name == W.commentRangeStart ||
e.Name == W.commentRangeEnd ||
e.Name == W.commentReference))
element.Attribute(W.id).Value = ((int)element.Attribute(W.id) + nextCommentId).ToString();
foreach (var element in part.GetXDocument().Root.Descendants()
.Where(e => e.Name == W.bookmarkStart || e.Name == W.bookmarkEnd))
element.Attribute(W.id).Value = ((int)element.Attribute(W.id) + nextBookmarkId).ToString();
}
private static void RenumberCommentsInDoc2(WordprocessingDocument doc1, WordprocessingDocument doc2)
{
// Get the XDocuments for the two comments parts.
XDocument doc1WordprocessingCommentsXDocument = doc1
.MainDocumentPart
.WordprocessingCommentsPart
.GetXDocument();
int commentIdIncrease = doc1WordprocessingCommentsXDocument
.Root
.Elements(W.comment)
.Attributes(W.id)
.Select(a => (int)a)
.Max() + 1;
int nextBookmarkId = new[] { 0 }
.Concat(doc1.MainDocumentPart
.GetXDocument()
.Root
.Descendants(W.bookmarkStart)
.Attributes(W.id)
.Select(a => (int)a))
.Max();
foreach (var part in doc1.MainDocumentPart.HeaderParts)
nextBookmarkId = new[] { nextBookmarkId }
.Concat(part
.GetXDocument()
.Root
.Descendants(W.bookmarkStart)
.Attributes(W.id)
.Select(a => (int)a))
.Max();
foreach (var part in doc1.MainDocumentPart.FooterParts)
nextBookmarkId = new[] { nextBookmarkId }
.Concat(part
.GetXDocument()
.Root
.Descendants(W.bookmarkStart)
.Attributes(W.id)
.Select(a => (int)a))
.Max();
if (doc1.MainDocumentPart.FootnotesPart != null)
nextBookmarkId = new[] { nextBookmarkId }
.Concat(doc1
.MainDocumentPart
.FootnotesPart
.GetXDocument()
.Root
.Descendants(W.bookmarkStart)
.Attributes(W.id)
.Select(a => (int)a))
.Max();
if (doc1.MainDocumentPart.EndnotesPart != null)
nextBookmarkId = new[] { nextBookmarkId }
.Concat(doc1
.MainDocumentPart
.EndnotesPart
.GetXDocument()
.Root
.Descendants(W.bookmarkStart)
.Attributes(W.id)
.Select(a => (int)a))
.Max();
nextBookmarkId++;
foreach (var element in doc2.MainDocumentPart.WordprocessingCommentsPart.GetXDocument()
.Root.Elements(W.comment))
element.Attribute(W.id).Value =
(((int)element.Attribute(W.id)) + commentIdIncrease).ToString();
FixIdsInPart(doc2.MainDocumentPart, commentIdIncrease, nextBookmarkId);
foreach (var header in doc2.MainDocumentPart.HeaderParts)
FixIdsInPart(header, commentIdIncrease, nextBookmarkId);
foreach (var footer in doc2.MainDocumentPart.FooterParts)
FixIdsInPart(footer, commentIdIncrease, nextBookmarkId);
FixIdsInPart(doc2.MainDocumentPart.FootnotesPart, commentIdIncrease, nextBookmarkId);
FixIdsInPart(doc2.MainDocumentPart.EndnotesPart, commentIdIncrease, nextBookmarkId);
}
}
}
#if false
todo here are two m:oMath elements that need merged.
<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>W</m:t>
</m:r>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>=</m:t>
</m:r>
<m:f>
<m:fPr>
<m:ctrlPr>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
<w:i />
</w:rPr>
</m:ctrlPr>
</m:fPr>
<m:num>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>1</m:t>
</m:r>
</m:num>
<m:den>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>μ(1-U)</m:t>
</m:r>
</m:den>
</m:f>
</m:oMath>
<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">
<w:commentRangeStart w:id="6" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" />
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>W</m:t>
</m:r>
<w:commentRangeEnd w:id="6" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" />
<m:r>
<m:rPr>
<m:sty m:val="p" />
</m:rPr>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rStyle w:val="CommentReference" />
</w:rPr>
<w:commentReference w:id="6" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" />
</m:r>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>=</m:t>
</m:r>
<m:f>
<m:fPr>
<m:ctrlPr>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
<w:i />
</w:rPr>
</m:ctrlPr>
</m:fPr>
<m:num>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>1</m:t>
</m:r>
</m:num>
<m:den>
<m:r>
<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math" />
</w:rPr>
<m:t>μ(1-U)</m:t>
</m:r>
</m:den>
</m:f>
</m:oMath>
#endif