WmlComparer.Public.Methods.Consolidate.cs 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. // Copyright (c) Microsoft. All rights reserved.
  2. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.IO.Packaging;
  7. using System.Linq;
  8. using System.Text;
  9. using System.Xml.Linq;
  10. using DocumentFormat.OpenXml.Packaging;
  11. namespace OpenXmlPowerTools
  12. {
  13. public static partial class WmlComparer
  14. {
  15. /*****************************************************************************************************************/
  16. // Consolidate processes footnotes and endnotes in a particular fashion - if the unmodified document has a footnote
  17. // reference, and a delta has a footnote reference, we end up with two footnotes - one is unmodified, and is referred to
  18. // from the unmodified content. The footnote reference in the delta refers to the modified footnote. This is as it
  19. // should be.
  20. /*****************************************************************************************************************/
  21. public static WmlDocument Consolidate(
  22. WmlDocument original,
  23. List<WmlRevisedDocumentInfo> revisedDocumentInfoList,
  24. WmlComparerSettings settings)
  25. {
  26. var consolidateSettings = new WmlComparerConsolidateSettings();
  27. return Consolidate(original, revisedDocumentInfoList, settings, consolidateSettings);
  28. }
  29. public static WmlDocument Consolidate(
  30. WmlDocument original,
  31. List<WmlRevisedDocumentInfo> revisedDocumentInfoList,
  32. WmlComparerSettings settings,
  33. WmlComparerConsolidateSettings consolidateSettings)
  34. {
  35. // pre-process the original, so that it already has unids for all elements
  36. // then when comparing all documents to the original, each one will have the unid as appropriate
  37. // for all revision block-level content
  38. // set unid to look for
  39. // while true
  40. // determine where to insert
  41. // get the unid for the revision
  42. // look it up in the original. if find it, then insert after that element
  43. // if not in the original
  44. // look backwards in revised document, set unid to look for, do the loop again
  45. // if get to the beginning of the document
  46. // insert at beginning of document
  47. settings.StartingIdForFootnotesEndnotes = 3000;
  48. WmlDocument originalWithUnids = PreProcessMarkup(original, settings.StartingIdForFootnotesEndnotes);
  49. var consolidated = new WmlDocument(originalWithUnids);
  50. if (SaveIntermediateFilesForDebugging && settings.DebugTempFileDi != null)
  51. {
  52. var name1 = "Original-with-Unids.docx";
  53. var preProcFi1 = new FileInfo(Path.Combine(settings.DebugTempFileDi.FullName, name1));
  54. originalWithUnids.SaveAs(preProcFi1.FullName);
  55. }
  56. int revisedDocumentInfoListCount = revisedDocumentInfoList.Count();
  57. using (var consolidatedMs = new MemoryStream())
  58. {
  59. consolidatedMs.Write(consolidated.DocumentByteArray, 0, consolidated.DocumentByteArray.Length);
  60. using (WordprocessingDocument consolidatedWDoc = WordprocessingDocument.Open(consolidatedMs, true))
  61. {
  62. MainDocumentPart consolidatedMainDocPart = consolidatedWDoc.MainDocumentPart;
  63. XDocument consolidatedMainDocPartXDoc = consolidatedMainDocPart.GetXDocument();
  64. XElement consolidatedMainDocPartRoot = consolidatedMainDocPartXDoc.Root ?? throw new ArgumentException();
  65. // save away last sectPr
  66. XElement savedSectPr = consolidatedMainDocPartRoot
  67. .Elements(W.body)
  68. .Elements(W.sectPr)
  69. .LastOrDefault();
  70. consolidatedMainDocPartRoot
  71. .Elements(W.body)
  72. .Elements(W.sectPr)
  73. .Remove();
  74. Dictionary<string, XElement> consolidatedByUnid = consolidatedMainDocPartXDoc
  75. .Descendants()
  76. .Where(d => (d.Name == W.p || d.Name == W.tbl) && d.Attribute(PtOpenXml.Unid) != null)
  77. .ToDictionary(d => (string) d.Attribute(PtOpenXml.Unid));
  78. var deltaNbr = 1;
  79. foreach (WmlRevisedDocumentInfo revisedDocumentInfo in revisedDocumentInfoList)
  80. {
  81. settings.StartingIdForFootnotesEndnotes = deltaNbr * 2000 + 3000;
  82. WmlDocument delta = CompareInternal(originalWithUnids, revisedDocumentInfo.RevisedDocument, settings,
  83. false);
  84. if (SaveIntermediateFilesForDebugging && settings.DebugTempFileDi != null)
  85. {
  86. string name1 = string.Format("Delta-{0}.docx", deltaNbr++);
  87. var deltaFi = new FileInfo(Path.Combine(settings.DebugTempFileDi.FullName, name1));
  88. delta.SaveAs(deltaFi.FullName);
  89. }
  90. using (var msOriginalWithUnids = new MemoryStream())
  91. using (var msDelta = new MemoryStream())
  92. {
  93. msOriginalWithUnids.Write(
  94. originalWithUnids.DocumentByteArray,
  95. 0,
  96. originalWithUnids.DocumentByteArray.Length);
  97. msDelta.Write(delta.DocumentByteArray, 0, delta.DocumentByteArray.Length);
  98. using (WordprocessingDocument wDocOriginalWithUnids = WordprocessingDocument.Open(msOriginalWithUnids, true))
  99. using (WordprocessingDocument wDocDelta = WordprocessingDocument.Open(msDelta, true))
  100. {
  101. MainDocumentPart modMainDocPart = wDocDelta.MainDocumentPart;
  102. XDocument modMainDocPartXDoc = modMainDocPart.GetXDocument();
  103. List<XElement> blockLevelContentToMove = modMainDocPartXDoc
  104. .Root
  105. .DescendantsTrimmed(d => d.Name == W.txbxContent || d.Name == W.tr)
  106. .Where(d => d.Name == W.p || d.Name == W.tbl)
  107. .Where(d => d.Descendants().Any(z => z.Name == W.ins || z.Name == W.del) ||
  108. ContentContainsFootnoteEndnoteReferencesThatHaveRevisions(d, wDocDelta))
  109. .ToList();
  110. foreach (XElement revision in blockLevelContentToMove)
  111. {
  112. XElement elementLookingAt = revision;
  113. while (true)
  114. {
  115. var unid = (string) elementLookingAt.Attribute(PtOpenXml.Unid);
  116. if (unid == null)
  117. throw new OpenXmlPowerToolsException("Internal error");
  118. XElement elementToInsertAfter = null;
  119. if (consolidatedByUnid.ContainsKey(unid))
  120. elementToInsertAfter = consolidatedByUnid[unid];
  121. if (elementToInsertAfter != null)
  122. {
  123. var ci = new ConsolidationInfo();
  124. ci.Revisor = revisedDocumentInfo.Revisor;
  125. ci.Color = revisedDocumentInfo.Color;
  126. ci.RevisionElement = revision;
  127. ci.Footnotes = revision
  128. .Descendants(W.footnoteReference)
  129. .Select(fr =>
  130. {
  131. var id = (int) fr.Attribute(W.id);
  132. XDocument fnXDoc = wDocDelta.MainDocumentPart.FootnotesPart.GetXDocument();
  133. XElement footnote = fnXDoc.Root.Elements(W.footnote)
  134. .FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  135. if (footnote == null)
  136. throw new OpenXmlPowerToolsException("Internal Error");
  137. return footnote;
  138. })
  139. .ToArray();
  140. ci.Endnotes = revision
  141. .Descendants(W.endnoteReference)
  142. .Select(er =>
  143. {
  144. var id = (int) er.Attribute(W.id);
  145. XDocument enXDoc = wDocDelta.MainDocumentPart.EndnotesPart.GetXDocument();
  146. XElement endnote = enXDoc.Root.Elements(W.endnote)
  147. .FirstOrDefault(en => (int) en.Attribute(W.id) == id);
  148. if (endnote == null)
  149. throw new OpenXmlPowerToolsException("Internal Error");
  150. return endnote;
  151. })
  152. .ToArray();
  153. AddToAnnotation(
  154. wDocDelta,
  155. consolidatedWDoc,
  156. elementToInsertAfter,
  157. ci,
  158. settings);
  159. break;
  160. }
  161. // find an element to insert after
  162. XElement elementBeforeRevision = elementLookingAt
  163. .SiblingsBeforeSelfReverseDocumentOrder()
  164. .FirstOrDefault(e => e.Attribute(PtOpenXml.Unid) != null);
  165. if (elementBeforeRevision == null)
  166. {
  167. XElement firstElement = consolidatedMainDocPartXDoc
  168. .Root
  169. .Element(W.body)
  170. .Elements()
  171. .FirstOrDefault(e => e.Name == W.p || e.Name == W.tbl);
  172. var ci = new ConsolidationInfo();
  173. ci.Revisor = revisedDocumentInfo.Revisor;
  174. ci.Color = revisedDocumentInfo.Color;
  175. ci.RevisionElement = revision;
  176. ci.InsertBefore = true;
  177. ci.Footnotes = revision
  178. .Descendants(W.footnoteReference)
  179. .Select(fr =>
  180. {
  181. var id = (int) fr.Attribute(W.id);
  182. XDocument fnXDoc = wDocDelta.MainDocumentPart.FootnotesPart.GetXDocument();
  183. XElement footnote = fnXDoc.Root.Elements(W.footnote)
  184. .FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  185. if (footnote == null)
  186. throw new OpenXmlPowerToolsException("Internal Error");
  187. return footnote;
  188. })
  189. .ToArray();
  190. ci.Endnotes = revision
  191. .Descendants(W.endnoteReference)
  192. .Select(er =>
  193. {
  194. var id = (int) er.Attribute(W.id);
  195. XDocument enXDoc = wDocDelta.MainDocumentPart.EndnotesPart.GetXDocument();
  196. XElement endnote = enXDoc.Root.Elements(W.endnote)
  197. .FirstOrDefault(en => (int) en.Attribute(W.id) == id);
  198. if (endnote == null)
  199. throw new OpenXmlPowerToolsException("Internal Error");
  200. return endnote;
  201. })
  202. .ToArray();
  203. AddToAnnotation(
  204. wDocDelta,
  205. consolidatedWDoc,
  206. firstElement,
  207. ci,
  208. settings);
  209. break;
  210. }
  211. elementLookingAt = elementBeforeRevision;
  212. }
  213. }
  214. CopyMissingStylesFromOneDocToAnother(wDocDelta, consolidatedWDoc);
  215. }
  216. }
  217. }
  218. // at this point, everything is added as an annotation, from all documents to be merged.
  219. // so now the process is to go through and add the annotations to the document
  220. List<XElement> elementsToProcess = consolidatedMainDocPartXDoc
  221. .Root
  222. .Descendants()
  223. .Where(d => d.Annotation<List<ConsolidationInfo>>() != null)
  224. .ToList();
  225. var emptyParagraph = new XElement(W.p,
  226. new XElement(W.pPr,
  227. new XElement(W.spacing,
  228. new XAttribute(W.after, "0"),
  229. new XAttribute(W.line, "240"),
  230. new XAttribute(W.lineRule, "auto"))));
  231. foreach (XElement ele in elementsToProcess)
  232. {
  233. var lci = ele.Annotation<List<ConsolidationInfo>>();
  234. // process before
  235. IEnumerable<XElement[]> contentToAddBefore = lci
  236. .Where(ci => ci.InsertBefore)
  237. .GroupAdjacent(ci => ci.Revisor + ci.Color.ToString())
  238. .Select((groupedCi, idx) => AssembledConjoinedRevisionContent(emptyParagraph, groupedCi, idx,
  239. consolidatedWDoc, consolidateSettings));
  240. ele.AddBeforeSelf(contentToAddBefore);
  241. // process after
  242. // if all revisions from all revisors are exactly the same, then instead of adding multiple tables after
  243. // that contains the revisions, then simply replace the paragraph with the one with the revisions.
  244. // RC004 documents contain the test data to exercise this.
  245. int lciCount = lci.Where(ci => ci.InsertBefore == false).Count();
  246. if (lciCount > 1 && lciCount == revisedDocumentInfoListCount)
  247. {
  248. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  249. // This is the code that determines if revisions should be consolidated into one.
  250. List<IGrouping<string, ConsolidationInfo>> uniqueRevisions = lci
  251. .Where(ci => ci.InsertBefore == false)
  252. .GroupBy(ci =>
  253. {
  254. // Get a hash after first accepting revisions and compressing the text.
  255. XElement acceptedRevisionElement =
  256. RevisionProcessor.AcceptRevisionsForElement(ci.RevisionElement);
  257. string sha1Hash = WmlComparerUtil.SHA1HashStringForUTF8String(acceptedRevisionElement.Value
  258. .Replace(" ", "").Replace(" ", "").Replace(" ", "").Replace("\n", "").Replace(".", "")
  259. .Replace(",", "").ToUpper());
  260. return sha1Hash;
  261. })
  262. .OrderByDescending(g => g.Count())
  263. .ToList();
  264. int uniqueRevisionCount = uniqueRevisions.Count();
  265. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  266. if (uniqueRevisionCount == 1)
  267. {
  268. MoveFootnotesEndnotesForConsolidatedRevisions(lci.First(), consolidatedWDoc);
  269. var dummyElement = new XElement("dummy", lci.First().RevisionElement);
  270. foreach (XElement rev in dummyElement.Descendants().Where(d => d.Attribute(W.author) != null))
  271. {
  272. XAttribute aut = rev.Attribute(W.author);
  273. aut.Value = "ITU";
  274. }
  275. ele.ReplaceWith(dummyElement.Elements());
  276. continue;
  277. }
  278. // this is the location where we have determined that there are the same number of revisions for this paragraph as there are revision documents.
  279. // however, the hash for all of them were not the same.
  280. // therefore, they would be added to the consolidated document as separate revisions.
  281. // create a log that shows what is different, in detail.
  282. if (settings.LogCallback != null)
  283. {
  284. var sb = new StringBuilder();
  285. sb.Append(
  286. "====================================================================================================" +
  287. NewLine);
  288. sb.Append("Non-Consolidated Revision" + NewLine);
  289. sb.Append(
  290. "====================================================================================================" +
  291. NewLine);
  292. foreach (IGrouping<string, ConsolidationInfo> urList in uniqueRevisions)
  293. {
  294. string revisorList = urList.Select(ur => ur.Revisor + " : ").StringConcatenate()
  295. .TrimEnd(' ', ':');
  296. sb.Append("Revisors: " + revisorList + NewLine);
  297. string str = RevisionToLogFormTransform(urList.First().RevisionElement, 0, false);
  298. sb.Append(str);
  299. sb.Append("=========================" + NewLine);
  300. }
  301. sb.Append(NewLine);
  302. settings.LogCallback(sb.ToString());
  303. }
  304. }
  305. IEnumerable<XElement[]> contentToAddAfter = lci
  306. .Where(ci => ci.InsertBefore == false)
  307. .GroupAdjacent(ci => ci.Revisor + ci.Color.ToString())
  308. .Select((groupedCi, idx) => AssembledConjoinedRevisionContent(emptyParagraph, groupedCi, idx,
  309. consolidatedWDoc, consolidateSettings));
  310. ele.AddAfterSelf(contentToAddAfter);
  311. }
  312. #if false
  313. // old code
  314. foreach (var ele in elementsToProcess)
  315. {
  316. var lci = ele.Annotation<List<ConsolidationInfo>>();
  317. // if all revisions from all revisors are exactly the same, then instead of adding multiple tables after
  318. // that contains the revisions, then simply replace the paragraph with the one with the revisions.
  319. // RC004 documents contain the test data to exercise this.
  320. var lciCount = lci.Count();
  321. if (lci.Count() > 1 && lciCount == revisedDocumentInfoListCount)
  322. {
  323. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  324. // This is the code that determines if revisions should be consolidated into one.
  325. var uniqueRevisions = lci
  326. .GroupBy(ci =>
  327. {
  328. // Get a hash after first accepting revisions and compressing the text.
  329. var ciz = ci;
  330. var acceptedRevisionElement = RevisionProcessor.AcceptRevisionsForElement(ci.RevisionElement);
  331. var text = acceptedRevisionElement.Value
  332. .Replace(" ", "")
  333. .Replace(" ", "")
  334. .Replace(" ", "")
  335. .Replace("\n", "");
  336. var sha1Hash = WmlComparerUtil.SHA1HashStringForUTF8String(text);
  337. return ci.InsertBefore.ToString() + sha1Hash;
  338. })
  339. .OrderByDescending(g => g.Count())
  340. .ToList();
  341. var uniqueRevisionCount = uniqueRevisions.Count();
  342. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  343. if (uniqueRevisionCount == 1)
  344. {
  345. MoveFootnotesEndnotesForConsolidatedRevisions(lci.First(), consolidatedWDoc);
  346. var dummyElement = new XElement("dummy", lci.First().RevisionElement);
  347. foreach(var rev in dummyElement.Descendants().Where(d => d.Attribute(W.author) != null))
  348. {
  349. var aut = rev.Attribute(W.author);
  350. aut.Value = "ITU";
  351. }
  352. ele.ReplaceWith(dummyElement.Elements());
  353. continue;
  354. }
  355. // this is the location where we have determined that there are the same number of revisions for this paragraph as there are revision documents.
  356. // however, the hash for all of them were not the same.
  357. // therefore, they would be added to the consolidated document as separate revisions.
  358. // create a log that shows what is different, in detail.
  359. if (settings.LogCallback != null)
  360. {
  361. StringBuilder sb = new StringBuilder();
  362. sb.Append("====================================================================================================" + nl);
  363. sb.Append("Non-Consolidated Revision" + nl);
  364. sb.Append("====================================================================================================" + nl);
  365. foreach (var urList in uniqueRevisions)
  366. {
  367. var revisorList =
  368. urList.Select(ur => ur.Revisor + " : ").StringConcatenate().TrimEnd(' ', ':');
  369. sb.Append("Revisors: " + revisorList + nl);
  370. var str = RevisionToLogFormTransform(urList.First().RevisionElement, 0, false);
  371. sb.Append(str);
  372. sb.Append("=========================" + nl);
  373. }
  374. sb.Append(nl);
  375. settings.LogCallback(sb.ToString());
  376. }
  377. }
  378. var contentToAddBefore = lci
  379. .Where(ci => ci.InsertBefore == true)
  380. .GroupAdjacent(ci => ci.Revisor + ci.Color.ToString())
  381. .Select((groupedCi, idx) => AssembledConjoinedRevisionContent(emptyParagraph, groupedCi, idx, consolidatedWDoc, consolidateSettings));
  382. var contentToAddAfter = lci
  383. .Where(ci => ci.InsertBefore == false)
  384. .GroupAdjacent(ci => ci.Revisor + ci.Color.ToString())
  385. .Select((groupedCi, idx) => AssembledConjoinedRevisionContent(emptyParagraph, groupedCi, idx, consolidatedWDoc, consolidateSettings));
  386. ele.AddBeforeSelf(contentToAddBefore);
  387. ele.AddAfterSelf(contentToAddAfter);
  388. }
  389. #endif
  390. consolidatedMainDocPartXDoc
  391. .Root?
  392. .Element(W.body)?
  393. .Add(savedSectPr);
  394. AddTableGridStyleToStylesPart(consolidatedWDoc.MainDocumentPart.StyleDefinitionsPart);
  395. FixUpRevisionIds(consolidatedWDoc, consolidatedMainDocPartXDoc);
  396. IgnorePt14NamespaceForFootnotesEndnotes(consolidatedWDoc);
  397. FixUpDocPrIds(consolidatedWDoc);
  398. FixUpShapeIds(consolidatedWDoc);
  399. FixUpGroupIds(consolidatedWDoc);
  400. FixUpShapeTypeIds(consolidatedWDoc);
  401. IgnorePt14Namespace(consolidatedMainDocPartXDoc.Root);
  402. consolidatedWDoc.MainDocumentPart.PutXDocument();
  403. AddFootnotesEndnotesStyles(consolidatedWDoc);
  404. }
  405. var newConsolidatedDocument = new WmlDocument("consolidated.docx", consolidatedMs.ToArray());
  406. return newConsolidatedDocument;
  407. }
  408. }
  409. private static void MoveFootnotesEndnotesForConsolidatedRevisions(
  410. ConsolidationInfo ci,
  411. WordprocessingDocument wDocConsolidated)
  412. {
  413. XDocument consolidatedFootnoteXDoc = wDocConsolidated.MainDocumentPart.FootnotesPart.GetXDocument();
  414. XDocument consolidatedEndnoteXDoc = wDocConsolidated.MainDocumentPart.EndnotesPart.GetXDocument();
  415. var maxFootnoteId = 1;
  416. if (consolidatedFootnoteXDoc.Root?.Elements(W.footnote).Any() == true)
  417. maxFootnoteId = consolidatedFootnoteXDoc.Root.Elements(W.footnote).Select(e => (int) e.Attribute(W.id)).Max();
  418. var maxEndnoteId = 1;
  419. if (consolidatedEndnoteXDoc.Root?.Elements(W.endnote).Any() == true)
  420. maxEndnoteId = consolidatedEndnoteXDoc.Root.Elements(W.endnote).Select(e => (int) e.Attribute(W.id)).Max();
  421. // At this point, content might contain a footnote or endnote reference.
  422. // Need to add the footnote / endnote into the consolidated document (with the same guid id)
  423. // Because of preprocessing of the documents, all footnote and endnote references will be unique at this point
  424. if (ci.RevisionElement.Descendants(W.footnoteReference).Any())
  425. {
  426. XDocument footnoteXDoc = wDocConsolidated.MainDocumentPart.FootnotesPart.GetXDocument();
  427. foreach (XElement footnoteReference in ci.RevisionElement.Descendants(W.footnoteReference))
  428. {
  429. var id = (int) footnoteReference.Attribute(W.id);
  430. XElement footnote = ci.Footnotes.FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  431. if (footnote != null)
  432. {
  433. int newId = ++maxFootnoteId;
  434. footnoteReference.SetAttributeValue(W.id, newId);
  435. var clonedFootnote = new XElement(footnote);
  436. clonedFootnote.SetAttributeValue(W.id, newId);
  437. footnoteXDoc.Root?.Add(clonedFootnote);
  438. }
  439. }
  440. wDocConsolidated.MainDocumentPart.FootnotesPart.PutXDocument();
  441. }
  442. if (ci.RevisionElement.Descendants(W.endnoteReference).Any())
  443. {
  444. XDocument endnoteXDoc = wDocConsolidated.MainDocumentPart.EndnotesPart.GetXDocument();
  445. foreach (XElement endnoteReference in ci.RevisionElement.Descendants(W.endnoteReference))
  446. {
  447. var id = (int) endnoteReference.Attribute(W.id);
  448. XElement endnote = ci.Endnotes.FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  449. if (endnote != null)
  450. {
  451. int newId = ++maxEndnoteId;
  452. endnoteReference.SetAttributeValue(W.id, newId);
  453. var clonedEndnote = new XElement(endnote);
  454. clonedEndnote.SetAttributeValue(W.id, newId);
  455. endnoteXDoc.Root?.Add(clonedEndnote);
  456. }
  457. }
  458. wDocConsolidated.MainDocumentPart.EndnotesPart.PutXDocument();
  459. }
  460. }
  461. private static void FixUpGroupIds(WordprocessingDocument wDoc)
  462. {
  463. XName elementToFind = VML.@group;
  464. IEnumerable<XElement> groupIdsToChange = wDoc
  465. .ContentParts()
  466. .Select(cp => cp.GetXDocument())
  467. .Select(xd => xd.Descendants().Where(d => d.Name == elementToFind))
  468. .SelectMany(m => m);
  469. var nextId = 1;
  470. foreach (XElement item in groupIdsToChange)
  471. {
  472. int thisId = nextId++;
  473. XAttribute idAtt = item.Attribute("id");
  474. if (idAtt != null)
  475. idAtt.Value = thisId.ToString();
  476. }
  477. foreach (OpenXmlPart cp in wDoc.ContentParts())
  478. cp.PutXDocument();
  479. }
  480. private static bool ContentContainsFootnoteEndnoteReferencesThatHaveRevisions(
  481. XElement element,
  482. WordprocessingDocument wDocDelta)
  483. {
  484. IEnumerable<XElement> footnoteEndnoteReferences = element
  485. .Descendants()
  486. .Where(d => d.Name == W.footnoteReference || d.Name == W.endnoteReference)
  487. .ToList();
  488. if (!footnoteEndnoteReferences.Any())
  489. return false;
  490. XDocument footnoteXDoc = wDocDelta.MainDocumentPart.FootnotesPart.GetXDocument();
  491. XDocument endnoteXDoc = wDocDelta.MainDocumentPart.EndnotesPart.GetXDocument();
  492. foreach (XElement note in footnoteEndnoteReferences)
  493. {
  494. XElement fnen;
  495. if (note.Name == W.footnoteReference)
  496. {
  497. var id = (int) note.Attribute(W.id);
  498. fnen = footnoteXDoc
  499. .Root?
  500. .Elements(W.footnote)
  501. .FirstOrDefault(n => (int) n.Attribute(W.id) == id);
  502. if (fnen?.Descendants().Any(d => d.Name == W.ins || d.Name == W.del) == true)
  503. return true;
  504. }
  505. if (note.Name == W.endnoteReference)
  506. {
  507. var id = (int) note.Attribute(W.id);
  508. fnen = endnoteXDoc
  509. .Root?
  510. .Elements(W.endnote)
  511. .FirstOrDefault(n => (int) n.Attribute(W.id) == id);
  512. if (fnen?.Descendants().Any(d => d.Name == W.ins || d.Name == W.del) == true)
  513. return true;
  514. }
  515. }
  516. return false;
  517. }
  518. private static string RevisionToLogFormTransform(XElement element, int depth, bool inserting)
  519. {
  520. if (element.Name == W.p)
  521. return "Paragraph" + NewLine + element.Elements().Select(e => RevisionToLogFormTransform(e, depth + 2, false))
  522. .StringConcatenate();
  523. if (element.Name == W.pPr || element.Name == W.rPr)
  524. return "";
  525. if (element.Name == W.r)
  526. return element.Elements().Select(e => RevisionToLogFormTransform(e, depth, inserting)).StringConcatenate();
  527. if (element.Name == W.t)
  528. {
  529. if (inserting)
  530. return "".PadRight(depth) + "Inserted Text:" + QuoteIt((string) element) + NewLine;
  531. return "".PadRight(depth) + "Text:" + QuoteIt((string) element) + NewLine;
  532. }
  533. if (element.Name == W.delText)
  534. return "".PadRight(depth) + "Deleted Text:" + QuoteIt((string) element) + NewLine;
  535. if (element.Name == W.ins)
  536. return element.Elements().Select(e => RevisionToLogFormTransform(e, depth, true)).StringConcatenate();
  537. if (element.Name == W.del)
  538. return element.Elements().Select(e => RevisionToLogFormTransform(e, depth, false)).StringConcatenate();
  539. return "";
  540. }
  541. private static string QuoteIt(string str)
  542. {
  543. var quoteString = "\"";
  544. if (str.Contains('\"'))
  545. {
  546. quoteString = "\'";
  547. }
  548. return quoteString + str + quoteString;
  549. }
  550. private static void IgnorePt14NamespaceForFootnotesEndnotes(WordprocessingDocument wDoc)
  551. {
  552. FootnotesPart footnotesPart = wDoc.MainDocumentPart.FootnotesPart;
  553. EndnotesPart endnotesPart = wDoc.MainDocumentPart.EndnotesPart;
  554. if (footnotesPart != null)
  555. {
  556. XDocument footnotesPartXDoc = footnotesPart.GetXDocument();
  557. IgnorePt14Namespace(footnotesPartXDoc.Root);
  558. }
  559. if (endnotesPart != null)
  560. {
  561. XDocument endnotesPartXDoc = endnotesPart.GetXDocument();
  562. IgnorePt14Namespace(endnotesPartXDoc.Root);
  563. }
  564. footnotesPart?.PutXDocument();
  565. endnotesPart?.PutXDocument();
  566. }
  567. private static XElement[] AssembledConjoinedRevisionContent(
  568. XElement emptyParagraph,
  569. IGrouping<string, ConsolidationInfo> groupedCi,
  570. int idx,
  571. WordprocessingDocument wDocConsolidated,
  572. WmlComparerConsolidateSettings consolidateSettings)
  573. {
  574. XDocument consolidatedFootnoteXDoc = wDocConsolidated.MainDocumentPart.FootnotesPart.GetXDocument();
  575. XDocument consolidatedEndnoteXDoc = wDocConsolidated.MainDocumentPart.EndnotesPart.GetXDocument();
  576. var maxFootnoteId = 1;
  577. if (consolidatedFootnoteXDoc.Root?.Elements(W.footnote).Any() == true)
  578. {
  579. maxFootnoteId = consolidatedFootnoteXDoc.Root.Elements(W.footnote).Select(e => (int) e.Attribute(W.id)).Max();
  580. }
  581. var maxEndnoteId = 1;
  582. if (consolidatedEndnoteXDoc.Root?.Elements(W.endnote).Any() == true)
  583. {
  584. maxEndnoteId = consolidatedEndnoteXDoc.Root.Elements(W.endnote).Select(e => (int) e.Attribute(W.id)).Max();
  585. }
  586. string revisor = groupedCi.First().Revisor;
  587. var captionParagraph = new XElement(W.p,
  588. new XElement(W.pPr,
  589. new XElement(W.jc, new XAttribute(W.val, "both")),
  590. new XElement(W.rPr,
  591. new XElement(W.b),
  592. new XElement(W.bCs))),
  593. new XElement(W.r,
  594. new XElement(W.rPr,
  595. new XElement(W.b),
  596. new XElement(W.bCs)),
  597. new XElement(W.t, revisor)));
  598. int colorRgb = groupedCi.First().Color.ToArgb();
  599. string colorString = colorRgb.ToString("X");
  600. if (colorString.Length == 8)
  601. {
  602. colorString = colorString.Substring(2);
  603. }
  604. if (consolidateSettings.ConsolidateWithTable)
  605. {
  606. var table = new XElement(W.tbl,
  607. new XElement(W.tblPr,
  608. new XElement(W.tblStyle, new XAttribute(W.val, "TableGridForRevisions")),
  609. new XElement(W.tblW,
  610. new XAttribute(W._w, "0"),
  611. new XAttribute(W.type, "auto")),
  612. new XElement(W.shd,
  613. new XAttribute(W.val, "clear"),
  614. new XAttribute(W.color, "auto"),
  615. new XAttribute(W.fill, colorString)),
  616. new XElement(W.tblLook,
  617. new XAttribute(W.firstRow, "0"),
  618. new XAttribute(W.lastRow, "0"),
  619. new XAttribute(W.firstColumn, "0"),
  620. new XAttribute(W.lastColumn, "0"),
  621. new XAttribute(W.noHBand, "0"),
  622. new XAttribute(W.noVBand, "0"))),
  623. new XElement(W.tblGrid,
  624. new XElement(W.gridCol, new XAttribute(W._w, "9576"))),
  625. new XElement(W.tr,
  626. new XElement(W.tc,
  627. new XElement(W.tcPr,
  628. new XElement(W.shd,
  629. new XAttribute(W.val, "clear"),
  630. new XAttribute(W.color, "auto"),
  631. new XAttribute(W.fill, colorString))),
  632. captionParagraph,
  633. groupedCi.Select(ci =>
  634. {
  635. XElement paraAfter = null;
  636. if (ci.RevisionElement.Name == W.tbl)
  637. paraAfter = emptyParagraph;
  638. XElement[] revisionInTable =
  639. {
  640. ci.RevisionElement,
  641. paraAfter
  642. };
  643. // At this point, content might contain a footnote or endnote reference.
  644. // Need to add the footnote / endnote into the consolidated document (with the same
  645. // guid id). Because of preprocessing of the documents, all footnote and endnote
  646. // references will be unique at this point
  647. AddFootnotes(ci, wDocConsolidated, ref maxFootnoteId);
  648. AddEndnotes(ci, wDocConsolidated, ref maxEndnoteId);
  649. return revisionInTable;
  650. }))));
  651. // if the last paragraph has a deleted paragraph mark, then remove the deletion from the paragraph mark.
  652. // This is to prevent Word from misbehaving. the last paragraph in a cell must not have a deleted
  653. // paragraph mark.
  654. XElement theCell = table.Descendants(W.tc).FirstOrDefault();
  655. XElement lastPara = theCell?.Elements(W.p).LastOrDefault();
  656. if (lastPara != null)
  657. {
  658. bool isDeleted = lastPara
  659. .Elements(W.pPr)
  660. .Elements(W.rPr)
  661. .Elements(W.del)
  662. .Any();
  663. if (isDeleted)
  664. {
  665. lastPara
  666. .Elements(W.pPr)
  667. .Elements(W.rPr)
  668. .Elements(W.del)
  669. .Remove();
  670. }
  671. }
  672. XElement[] content =
  673. {
  674. idx == 0 ? emptyParagraph : null,
  675. table,
  676. emptyParagraph
  677. };
  678. return content;
  679. }
  680. else
  681. {
  682. IEnumerable<XElement[]> content = groupedCi.Select(ci =>
  683. {
  684. XElement paraAfter = null;
  685. if (ci.RevisionElement.Name == W.tbl)
  686. {
  687. paraAfter = emptyParagraph;
  688. }
  689. XElement[] revisionInTable =
  690. {
  691. ci.RevisionElement,
  692. paraAfter
  693. };
  694. // At this point, content might contain a footnote or endnote reference.
  695. // Need to add the footnote / endnote into the consolidated document (with the same
  696. // guid id). Because of preprocessing of the documents, all footnote and endnote
  697. // references will be unique at this point
  698. AddFootnotes(ci, wDocConsolidated, ref maxFootnoteId);
  699. AddEndnotes(ci, wDocConsolidated, ref maxEndnoteId);
  700. return revisionInTable;
  701. });
  702. var dummyElement = new XElement("dummy", content.SelectMany(m => m));
  703. foreach (XElement rev in dummyElement.Descendants().Where(d => d.Attribute(W.author) != null))
  704. {
  705. rev.SetAttributeValue(W.author, revisor);
  706. }
  707. return dummyElement.Elements().ToArray();
  708. }
  709. }
  710. private static void AddFootnotes(ConsolidationInfo ci, WordprocessingDocument wDocConsolidated, ref int maxFootnoteId)
  711. {
  712. if (ci.RevisionElement.Descendants(W.footnoteReference).Any())
  713. {
  714. XDocument footnoteXDoc = wDocConsolidated.MainDocumentPart.FootnotesPart.GetXDocument();
  715. foreach (XElement footnoteReference in ci.RevisionElement.Descendants(W.footnoteReference))
  716. {
  717. var id = (int) footnoteReference.Attribute(W.id);
  718. XElement footnote = ci.Footnotes.FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  719. if (footnote != null)
  720. {
  721. int newId = ++maxFootnoteId;
  722. footnoteReference.SetAttributeValue(W.id, newId);
  723. var clonedFootnote = new XElement(footnote);
  724. clonedFootnote.SetAttributeValue(W.id, newId);
  725. footnoteXDoc.Root?.Add(clonedFootnote);
  726. }
  727. }
  728. wDocConsolidated.MainDocumentPart.FootnotesPart.PutXDocument();
  729. }
  730. }
  731. private static void AddEndnotes(ConsolidationInfo ci, WordprocessingDocument wDocConsolidated, ref int maxEndnoteId)
  732. {
  733. if (ci.RevisionElement.Descendants(W.endnoteReference).Any())
  734. {
  735. XDocument endnoteXDoc = wDocConsolidated.MainDocumentPart.EndnotesPart.GetXDocument();
  736. foreach (XElement endnoteReference in ci.RevisionElement.Descendants(W.endnoteReference))
  737. {
  738. var id = (int) endnoteReference.Attribute(W.id);
  739. XElement endnote = ci.Endnotes.FirstOrDefault(fn => (int) fn.Attribute(W.id) == id);
  740. if (endnote != null)
  741. {
  742. int newId = ++maxEndnoteId;
  743. endnoteReference.SetAttributeValue(W.id, newId);
  744. var clonedEndnote = new XElement(endnote);
  745. clonedEndnote.SetAttributeValue(W.id, newId);
  746. endnoteXDoc.Root?.Add(clonedEndnote);
  747. }
  748. }
  749. wDocConsolidated.MainDocumentPart.EndnotesPart.PutXDocument();
  750. }
  751. }
  752. private static void AddToAnnotation(
  753. WordprocessingDocument wDocDelta,
  754. WordprocessingDocument consolidatedWDoc,
  755. XElement elementToInsertAfter,
  756. ConsolidationInfo consolidationInfo,
  757. WmlComparerSettings settings)
  758. {
  759. Package packageOfDeletedContent = wDocDelta.MainDocumentPart.OpenXmlPackage.Package;
  760. Package packageOfNewContent = consolidatedWDoc.MainDocumentPart.OpenXmlPackage.Package;
  761. PackagePart partInDeletedDocument = packageOfDeletedContent.GetPart(wDocDelta.MainDocumentPart.Uri);
  762. PackagePart partInNewDocument = packageOfNewContent.GetPart(consolidatedWDoc.MainDocumentPart.Uri);
  763. consolidationInfo.RevisionElement =
  764. MoveRelatedPartsToDestination(partInDeletedDocument, partInNewDocument, consolidationInfo.RevisionElement);
  765. var clonedForHashing = (XElement) CloneBlockLevelContentForHashing(consolidatedWDoc.MainDocumentPart,
  766. consolidationInfo.RevisionElement, false, settings);
  767. clonedForHashing.Descendants().Where(d => d.Name == W.ins || d.Name == W.del).Attributes(W.id).Remove();
  768. string shaString = clonedForHashing.ToString(SaveOptions.DisableFormatting)
  769. .Replace(" xmlns=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"", "");
  770. string sha1Hash = WmlComparerUtil.SHA1HashStringForUTF8String(shaString);
  771. consolidationInfo.RevisionString = shaString;
  772. consolidationInfo.RevisionHash = sha1Hash;
  773. var annotationList = elementToInsertAfter.Annotation<List<ConsolidationInfo>>();
  774. if (annotationList == null)
  775. {
  776. annotationList = new List<ConsolidationInfo>();
  777. elementToInsertAfter.AddAnnotation(annotationList);
  778. }
  779. annotationList.Add(consolidationInfo);
  780. }
  781. private static void AddTableGridStyleToStylesPart(StyleDefinitionsPart styleDefinitionsPart)
  782. {
  783. XDocument sXDoc = styleDefinitionsPart.GetXDocument();
  784. XElement tableGridStyle = sXDoc
  785. .Root?
  786. .Elements(W.style)
  787. .FirstOrDefault(s => (string) s.Attribute(W.styleId) == "TableGridForRevisions");
  788. if (tableGridStyle == null)
  789. {
  790. var tableGridForRevisionsStyleMarkup =
  791. @"<w:style w:type=""table""
  792. w:styleId=""TableGridForRevisions""
  793. xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
  794. <w:name w:val=""Table Grid For Revisions""/>
  795. <w:basedOn w:val=""TableNormal""/>
  796. <w:rsid w:val=""0092121A""/>
  797. <w:rPr>
  798. <w:rFonts w:asciiTheme=""minorHAnsi""
  799. w:eastAsiaTheme=""minorEastAsia""
  800. w:hAnsiTheme=""minorHAnsi""
  801. w:cstheme=""minorBidi""/>
  802. <w:sz w:val=""22""/>
  803. <w:szCs w:val=""22""/>
  804. </w:rPr>
  805. <w:tblPr>
  806. <w:tblBorders>
  807. <w:top w:val=""single""
  808. w:sz=""4""
  809. w:space=""0""
  810. w:color=""auto""/>
  811. <w:left w:val=""single""
  812. w:sz=""4""
  813. w:space=""0""
  814. w:color=""auto""/>
  815. <w:bottom w:val=""single""
  816. w:sz=""4""
  817. w:space=""0""
  818. w:color=""auto""/>
  819. <w:right w:val=""single""
  820. w:sz=""4""
  821. w:space=""0""
  822. w:color=""auto""/>
  823. <w:insideH w:val=""single""
  824. w:sz=""4""
  825. w:space=""0""
  826. w:color=""auto""/>
  827. <w:insideV w:val=""single""
  828. w:sz=""4""
  829. w:space=""0""
  830. w:color=""auto""/>
  831. </w:tblBorders>
  832. </w:tblPr>
  833. </w:style>";
  834. XElement tgsElement = XElement.Parse(tableGridForRevisionsStyleMarkup);
  835. sXDoc.Root.Add(tgsElement);
  836. }
  837. XElement tableNormalStyle = sXDoc
  838. .Root
  839. .Elements(W.style)
  840. .FirstOrDefault(s => (string) s.Attribute(W.styleId) == "TableNormal");
  841. if (tableNormalStyle == null)
  842. {
  843. var tableNormalStyleMarkup =
  844. @"<w:style w:type=""table""
  845. w:default=""1""
  846. w:styleId=""TableNormal""
  847. xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
  848. <w:name w:val=""Normal Table""/>
  849. <w:uiPriority w:val=""99""/>
  850. <w:semiHidden/>
  851. <w:unhideWhenUsed/>
  852. <w:tblPr>
  853. <w:tblInd w:w=""0""
  854. w:type=""dxa""/>
  855. <w:tblCellMar>
  856. <w:top w:w=""0""
  857. w:type=""dxa""/>
  858. <w:left w:w=""108""
  859. w:type=""dxa""/>
  860. <w:bottom w:w=""0""
  861. w:type=""dxa""/>
  862. <w:right w:w=""108""
  863. w:type=""dxa""/>
  864. </w:tblCellMar>
  865. </w:tblPr>
  866. </w:style>";
  867. XElement tnsElement = XElement.Parse(tableNormalStyleMarkup);
  868. sXDoc.Root.Add(tnsElement);
  869. }
  870. styleDefinitionsPart.PutXDocument();
  871. }
  872. }
  873. }