| /*************************************************************************** |
| |
| Portions Copyright (c) Eric White 2017. |
| Portions Copyright (c) Microsoft Corporation 2012-2015. |
| |
| This code is licensed using the Microsoft Public License (Ms-PL). The text of the license can be found here: |
| |
| http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx |
| |
| Published at http://EricWhite.com |
| Resource Center and Documentation: http://ericwhite.com/blog/blog/open-xml-powertools-developer-center/ |
| |
| Developer: Eric White |
| Blog: http://www.ericwhite.com |
| Twitter: @EricWhiteDev |
| Email: eric@ericwhite.com, ericwhitedev@gmail.com |
| |
| ***************************************************************************/ |
| |
| using System; |
| using System.Collections.Generic; |
| using System.Linq; |
| using System.Xml.Linq; |
| using DocumentFormat.OpenXml.Packaging; |
| |
| namespace OpenXmlPowerTools |
| { |
| class ReverseRevisionsInfo |
| { |
| public bool InInsert; |
| } |
| |
| public class RevisionProcessor |
| { |
| public static WmlDocument RejectRevisions(WmlDocument document) |
| { |
| using (OpenXmlMemoryStreamDocument streamDoc = new OpenXmlMemoryStreamDocument(document)) |
| { |
| using (WordprocessingDocument doc = streamDoc.GetWordprocessingDocument()) |
| { |
| RejectRevisions(doc); |
| } |
| return streamDoc.GetModifiedWmlDocument(); |
| } |
| } |
| |
| public static void RejectRevisions(WordprocessingDocument doc) |
| { |
| RejectRevisionsForPart(doc.MainDocumentPart); |
| foreach (var part in doc.MainDocumentPart.HeaderParts) |
| RejectRevisionsForPart(part); |
| foreach (var part in doc.MainDocumentPart.FooterParts) |
| RejectRevisionsForPart(part); |
| if (doc.MainDocumentPart.EndnotesPart != null) |
| RejectRevisionsForPart(doc.MainDocumentPart.EndnotesPart); |
| if (doc.MainDocumentPart.FootnotesPart != null) |
| RejectRevisionsForPart(doc.MainDocumentPart.FootnotesPart); |
| if (doc.MainDocumentPart.StyleDefinitionsPart != null) |
| RejectRevisionsForStylesDefinitionPart(doc.MainDocumentPart.StyleDefinitionsPart); |
| |
| ReverseRevisions(doc); |
| AcceptRevisionsForPart(doc.MainDocumentPart); |
| foreach (var part in doc.MainDocumentPart.HeaderParts) |
| AcceptRevisionsForPart(part); |
| foreach (var part in doc.MainDocumentPart.FooterParts) |
| AcceptRevisionsForPart(part); |
| if (doc.MainDocumentPart.EndnotesPart != null) |
| AcceptRevisionsForPart(doc.MainDocumentPart.EndnotesPart); |
| if (doc.MainDocumentPart.FootnotesPart != null) |
| AcceptRevisionsForPart(doc.MainDocumentPart.FootnotesPart); |
| if (doc.MainDocumentPart.StyleDefinitionsPart != null) |
| AcceptRevisionsForStylesDefinitionPart(doc.MainDocumentPart.StyleDefinitionsPart); |
| } |
| |
| // Reject revisions for those revisions that can't be rejected by inverting the sense of the revision, and then accepting. |
| private static void RejectRevisionsForPart(OpenXmlPart part) |
| { |
| var xDoc = part.GetXDocument(); |
| var newRoot = RejectRevisionsForPartTransform(xDoc.Root); |
| xDoc.Root.ReplaceWith(newRoot); |
| part.PutXDocument(); |
| } |
| |
| private static object RejectRevisionsForPartTransform(XNode node) |
| { |
| var element = node as XElement; |
| if (element != null) |
| { |
| //////////////////////////////////////////////////////////////////////////////////////// |
| // Inserted Numbering Properties |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:pStyle w:val="ListParagraph"/> |
| <w:numPr> |
| <w:ilvl w:val="0"/> |
| <w:numId w:val="1"/> |
| <w:ins w:id="0" w:author="Eric White" w:date="2017-03-26T03:50:00Z" /> |
| </w:numPr> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| </w:pPr> |
| <w:r w:rsidRPr="009D59B3"> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>This is a test.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.numPr && element.Element(W.ins) != null) |
| return null; |
| |
| //////////////////////////////////////////////////////////////////////////////////////// |
| // Paragraph properties change |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:pStyle w:val="ListParagraph"/> |
| <w:numPr> |
| <w:ilvl w:val="1"/> |
| <w:numId w:val="2"/> |
| </w:numPr> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:pPrChange w:id="0" w:author="Eric White" w:date="2017-03-26T04:55:00Z"> |
| <w:pPr> |
| <w:pStyle w:val="ListParagraph"/> |
| <w:numPr> |
| <w:ilvl w:val="1"/> |
| <w:numId w:val="1"/> |
| </w:numPr> |
| <w:ind w:left="1440" w:hanging="360"/> |
| </w:pPr> |
| </w:pPrChange> |
| </w:pPr> |
| <w:r> |
| <w:t>When you click Online Video, you can paste in the embed code for the video you want to add.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.pPr && |
| element.Element(W.pPrChange) != null) |
| { |
| var pPr = element.Element(W.pPrChange).Element(W.pPr); |
| if (pPr == null) |
| pPr = new XElement(W.pPr); |
| var new_pPr = new XElement(pPr); // clone it |
| new_pPr.Add(RejectRevisionsForPartTransform(element.Element(W.rPr))); |
| return RejectRevisionsForPartTransform(new_pPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////// |
| // Run properties change |
| #if false |
| <w:p w:rsidR="00615148" w:rsidRPr="00615148" w:rsidRDefault="00615148"> |
| <w:pPr> |
| <w:rPr> |
| <w:b/> |
| <w:lang w:val="en-US"/> |
| <w:rPrChange w:id="0" w:author="Eric White" w:date="2017-03-26T05:02:00Z"> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| </w:rPrChange> |
| </w:rPr> |
| </w:pPr> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>When you click Online Video, you can paste in the embed code for the video you want to add.</w:t> |
| </w:r> |
| <w:bookmarkStart w:id="1" w:name="_GoBack"/> |
| </w:p> |
| #endif |
| if (element.Name == W.rPr && |
| element.Element(W.rPrChange) != null) |
| { |
| var new_rPr = element.Element(W.rPrChange).Element(W.rPr); |
| return RejectRevisionsForPartTransform(new_rPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////// |
| // Field code numbering change |
| #if false |
| <w:p w:rsidR="00D46247" w:rsidRDefault="00D46247"> |
| <w:r> |
| <w:fldChar w:fldCharType="begin"/> |
| </w:r> |
| <w:r> |
| <w:instrText xml:space="preserve"> LISTNUM </w:instrText> |
| </w:r> |
| <w:r> |
| <w:fldChar w:fldCharType="end"> |
| <w:numberingChange w:id="0" w:author="Eric White" w:date="2017-03-26T12:48:00Z" w:original="1)"/> |
| </w:fldChar> |
| </w:r> |
| <w:r> |
| <w:t xml:space="preserve"> Video provides a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.numberingChange) |
| return null; |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Change w:sectPr |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:ins w:id="0" w:author="Eric White" w:date="2017-03-26T15:40:00Z"/> |
| </w:rPr> |
| <w:sectPr> |
| <w:pgSz w:w="12240" w:h="15840"/> |
| <w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720" w:header="720" w:footer="720" w:gutter="0"/> |
| <w:cols w:space="720"/> |
| <w:docGrid w:linePitch="360"/> |
| <w:sectPrChange w:id="1" w:author="Eric White" w:date="2017-03-26T15:42:00Z"> |
| <w:sectPr w:rsidR="00620990" w:rsidSect="004E0757"> |
| <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/> |
| </w:sectPr> |
| </w:sectPrChange> |
| </w:sectPr> |
| </w:pPr> |
| </w:p> |
| #endif |
| if (element.Name == W.sectPr && |
| element.Element(W.sectPrChange) != null) |
| { |
| var newSectPr = element.Element(W.sectPrChange).Element(W.sectPr); |
| return RejectRevisionsForPartTransform(newSectPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // tblGridChange |
| #if false |
| <w:tblGrid> |
| <w:gridCol w:w="1525"/> |
| <w:gridCol w:w="3005"/> |
| <w:gridCol w:w="3006"/> |
| <w:tblGridChange w:id="1"> |
| <w:tblGrid> |
| <w:gridCol w:w="3005"/> |
| <w:gridCol w:w="3005"/> |
| <w:gridCol w:w="3006"/> |
| </w:tblGrid> |
| </w:tblGridChange> |
| </w:tblGrid> |
| #endif |
| if (element.Name == W.tblGrid && |
| element.Element(W.tblGridChange) != null) |
| { |
| var newTblGrid = element.Element(W.tblGridChange).Element(W.tblGrid); |
| return RejectRevisionsForPartTransform(newTblGrid); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // tcPrChange |
| #if false |
| <w:tc> |
| <w:tcPr> |
| <w:tcW w:w="1525" w:type="dxa"/> |
| <w:tcPrChange w:id="2" w:author="Eric White" w:date="2017-03-26T18:01:00Z"> |
| <w:tcPr> |
| <w:tcW w:w="3005" w:type="dxa"/> |
| </w:tcPr> |
| </w:tcPrChange> |
| </w:tcPr> |
| <w:p> |
| <w:r> |
| <w:t>1</w:t> |
| </w:r> |
| </w:p> |
| </w:tc> |
| #endif |
| if (element.Name == W.tcPr && |
| element.Element(W.tcPrChange) != null) |
| { |
| var newTcPr = element.Element(W.tcPrChange).Element(W.tcPr); |
| return RejectRevisionsForPartTransform(newTcPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // trPrChange |
| if (element.Name == W.trPr && |
| element.Element(W.trPrChange) != null) |
| { |
| var newTrPr = element.Element(W.trPrChange).Element(W.trPr); |
| return RejectRevisionsForPartTransform(newTrPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // tblPrExChange |
| #if false |
| <w:tblPrEx> |
| <w:tblW w:w="0" w:type="auto"/> |
| <w:tblPrExChange w:id="1" w:author="Eric White" w:date="2017-03-26T18:10:00Z"> |
| <w:tblPrEx> |
| <w:tblW w:w="0" w:type="auto"/> |
| </w:tblPrEx> |
| </w:tblPrExChange> |
| </w:tblPrEx> |
| #endif |
| |
| #if false |
| <w:tr w:rsidR="00097582" w:rsidTr="00F843C4"> |
| <w:tblPrEx> |
| <w:tblW w:w="0" w:type="auto"/> |
| <w:tblBorders> |
| <w:top w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:left w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:bottom w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:right w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:insideH w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:insideV w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| </w:tblBorders> |
| <w:tblPrExChange w:id="1" w:author="Eric White" w:date="2017-03-26T20:38:00Z"> |
| <w:tblPrEx> |
| <w:tblW w:w="0" w:type="auto"/> |
| <w:tblBorders> |
| <w:top w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:left w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:bottom w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:right w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:insideH w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| <w:insideV w:val="thickThinMediumGap" w:sz="24" w:space="0" w:color="auto"/> |
| </w:tblBorders> |
| </w:tblPrEx> |
| </w:tblPrExChange> |
| </w:tblPrEx> |
| #endif |
| if (element.Name == W.tblPrEx && |
| element.Element(W.tblPrExChange) != null) |
| { |
| var newTblPrEx = element.Element(W.tblPrExChange).Element(W.tblPrEx); |
| return RejectRevisionsForPartTransform(newTblPrEx); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // tblPrChange |
| #if false |
| <w:tbl> |
| <w:tblPr> |
| <w:tblStyle w:val="GridTable4-Accent1"/> |
| <w:tblW w:w="0" w:type="auto"/> |
| <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/> |
| <w:tblPrChange w:id="0" w:author="Eric White" w:date="2017-03-26T20:05:00Z"> |
| <w:tblPr> |
| <w:tblStyle w:val="TableGrid"/> |
| <w:tblW w:w="0" w:type="auto"/> |
| <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/> |
| </w:tblPr> |
| </w:tblPrChange> |
| </w:tblPr> |
| #endif |
| if (element.Name == W.tblPr && |
| element.Element(W.tblPrChange) != null) |
| { |
| var newTrPr = element.Element(W.tblPrChange).Element(W.tblPr); |
| return RejectRevisionsForPartTransform(newTrPr); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // tblPrChange |
| #if false |
| <w:tc> |
| <w:tcPr> |
| <w:tcW w:w="3005" w:type="dxa"/> |
| <w:cellDel w:id="8" w:author="Eric White" w:date="2017-03-26T21:12:00Z"/> |
| <w:tcPrChange w:id="9" w:author="Eric White" w:date="2017-03-26T21:12:00Z"> |
| <w:tcPr> |
| <w:tcW w:w="3005" w:type="dxa"/> |
| <w:gridSpan w:val="2"/> |
| <w:cellDel w:id="10" w:author="Eric White" w:date="2017-03-26T21:12:00Z"/> |
| </w:tcPr> |
| </w:tcPrChange> |
| </w:tcPr> |
| #endif |
| |
| if (element.Name == W.cellDel || |
| element.Name == W.cellMerge) |
| return null; |
| |
| if (element.Name == W.tc && |
| element.Elements(W.tcPr).Elements(W.cellIns).Any()) |
| return null; |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => RejectRevisionsForPartTransform(n))); |
| } |
| return node; |
| } |
| |
| private static void RejectRevisionsForStylesDefinitionPart(StyleDefinitionsPart stylesDefinitionsPart) |
| { |
| var xDoc = stylesDefinitionsPart.GetXDocument(); |
| var newRoot = RejectRevisionsForStylesTransform(xDoc.Root); |
| xDoc.Root.ReplaceWith(newRoot); |
| stylesDefinitionsPart.PutXDocument(); |
| } |
| |
| private static object RejectRevisionsForStylesTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.pPr && |
| element.Element(W.pPrChange) != null) |
| { |
| var new_pPr = element.Element(W.pPrChange).Element(W.pPr); |
| return RejectRevisionsForStylesTransform(new_pPr); |
| } |
| |
| if (element.Name == W.rPr && |
| element.Element(W.rPrChange) != null) |
| { |
| var new_rPr = element.Element(W.rPrChange).Element(W.rPr); |
| return RejectRevisionsForStylesTransform(new_rPr); |
| } |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => RejectRevisionsForStylesTransform(n))); |
| } |
| return node; |
| } |
| |
| |
| |
| private static void ReverseRevisions(WordprocessingDocument doc) |
| { |
| ReverseRevisionsForPart(doc.MainDocumentPart); |
| foreach (var part in doc.MainDocumentPart.HeaderParts) |
| ReverseRevisionsForPart(part); |
| foreach (var part in doc.MainDocumentPart.FooterParts) |
| ReverseRevisionsForPart(part); |
| if (doc.MainDocumentPart.EndnotesPart != null) |
| ReverseRevisionsForPart(doc.MainDocumentPart.EndnotesPart); |
| if (doc.MainDocumentPart.FootnotesPart != null) |
| ReverseRevisionsForPart(doc.MainDocumentPart.FootnotesPart); |
| } |
| |
| private static void ReverseRevisionsForPart(OpenXmlPart part) |
| { |
| var xDoc = part.GetXDocument(); |
| ReverseRevisionsInfo rri = new ReverseRevisionsInfo(); |
| rri.InInsert = false; |
| var newRoot = (XElement)ReverseRevisionsTransform(xDoc.Root, rri); |
| newRoot = (XElement)RemoveRsidTransform(newRoot); |
| xDoc.Root.ReplaceWith(newRoot); |
| part.PutXDocument(); |
| } |
| |
| private static object RemoveRsidTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.rsid) |
| return null; |
| return new XElement(element.Name, |
| element.Attributes().Where(a => a.Name != W.rsid && |
| a.Name != W.rsidDel && |
| a.Name != W.rsidP && |
| a.Name != W.rsidR && |
| a.Name != W.rsidRDefault && |
| a.Name != W.rsidRPr && |
| a.Name != W.rsidSect && |
| a.Name != W.rsidTr), |
| element.Nodes().Select(n => RemoveRsidTransform(n))); |
| } |
| return node; |
| } |
| |
| private static object MergeAdjacentTablesTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Element(W.tbl) != null) |
| { |
| var grouped = element |
| .Elements() |
| .GroupAdjacent(e => |
| { |
| if (e.Name != W.tbl) |
| return ""; |
| var bidiVisual = e.Elements(W.tblPr).Elements(W.bidiVisual).FirstOrDefault(); |
| var bidiVisString = bidiVisual == null ? "" : "|bidiVisual"; |
| var key = "tbl" + bidiVisString; |
| return key; |
| }); |
| |
| var newContent = grouped |
| .Select(g => |
| { |
| if (g.Key == "" || g.Count() == 1) |
| return (object)g; |
| var rolled = g |
| .Select(tbl => |
| { |
| var gridCols = tbl |
| .Elements(W.tblGrid) |
| .Elements(W.gridCol) |
| .Attributes(W._w) |
| .Select(a => (int)a) |
| .Rollup(0, (s, i) => s + i); |
| return gridCols; |
| }) |
| .SelectMany(m => m) |
| .Distinct() |
| .OrderBy(w => w) |
| .ToArray(); |
| var newTable = new XElement(W.tbl, |
| g.First().Elements(W.tblPr), |
| new XElement(W.tblGrid, |
| rolled.Select((r, i) => |
| { |
| int v; |
| if (i == 0) |
| v = r; |
| else |
| v = r - rolled[i - 1]; |
| return new XElement(W.gridCol, |
| new XAttribute(W._w, v)); |
| })), |
| g.Select(tbl => |
| { |
| var fixedWidthsTbl = FixWidths(tbl); |
| var newRows = fixedWidthsTbl.Elements(W.tr) |
| .Select(tr => |
| { |
| XElement newRow = new XElement(W.tr, |
| tr.Attributes(), |
| tr.Elements().Where(e => e.Name != W.tc), |
| tr.Elements(W.tc).Select(tc => |
| { |
| int? w = (int?)tc |
| .Elements(W.tcPr) |
| .Elements(W.tcW) |
| .Attributes(W._w) |
| .FirstOrDefault(); |
| if (w == null) |
| return tc; |
| var cellsToLeft = tc |
| .Parent |
| .Elements(W.tc) |
| .TakeWhile(btc => btc != tc); |
| int widthToLeft = 0; |
| if (cellsToLeft.Any()) |
| widthToLeft = cellsToLeft |
| .Elements(W.tcPr) |
| .Elements(W.tcW) |
| .Attributes(W._w) |
| .Select(wi => (int)wi) |
| .Sum(); |
| var rolledPairs = new[] { new |
| { |
| GridValue = 0, |
| Index = 0, |
| }} |
| .Concat( |
| rolled |
| .Select((r, i) => new |
| { |
| GridValue = r, |
| Index = i + 1, |
| })); |
| var start = rolledPairs |
| .FirstOrDefault(t => t.GridValue >= widthToLeft); |
| if (start != null) |
| { |
| var gridsRequired = rolledPairs |
| .Skip(start.Index) |
| .TakeWhile(rp => rp.GridValue - start.GridValue < w) |
| .Count(); |
| var tcPr = new XElement(W.tcPr, |
| tc.Elements(W.tcPr).Elements().Where(e => e.Name != W.gridSpan), |
| gridsRequired != 1 ? |
| new XElement(W.gridSpan, |
| new XAttribute(W.val, gridsRequired)) : |
| null); |
| var orderedTcPr = new XElement(W.tcPr, |
| tcPr.Elements().OrderBy(e => |
| { |
| if (Order_tcPr.ContainsKey(e.Name)) |
| return Order_tcPr[e.Name]; |
| return 999; |
| })); |
| var newCell = new XElement(W.tc, |
| orderedTcPr, |
| tc.Elements().Where(e => e.Name != W.tcPr)); |
| return newCell; |
| } |
| return tc; |
| })); |
| return newRow; |
| }); |
| return newRows; |
| })); |
| return newTable; |
| }); |
| return new XElement(element.Name, |
| element.Attributes(), |
| newContent); |
| } |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => MergeAdjacentTablesTransform(n))); |
| } |
| return node; |
| } |
| |
| private static object ReverseRevisionsTransform(XNode node, ReverseRevisionsInfo rri) |
| { |
| var element = node as XElement; |
| if (element != null) |
| { |
| var parent = element |
| .Ancestors() |
| .Where(a => a.Name != W.sdtContent && a.Name != W.sdt && a.Name != W.smartTag) |
| .FirstOrDefault(); |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted run |
| #if false |
| <w:p> |
| <w:r> |
| <w:t xml:space="preserve">Video </w:t> |
| </w:r> |
| <w:del> |
| <w:r> |
| <w:delText xml:space="preserve">provides </w:delText> |
| </w:r> |
| </w:del> |
| <w:r> |
| <w:t>a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.del && |
| parent.Name == W.p) |
| { |
| return new XElement(W.ins, |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted paragraph mark |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:del w:id="0" w:author="Eric White" w:date="2017-03-24T21:52:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:r> |
| <w:t>Video provides a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| <w:p> |
| <w:r> |
| <w:t>You can also type a keyword to search online for the video that best fits your document.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.del && |
| parent.Name == W.rPr && |
| parent.Parent.Name == W.pPr) |
| { |
| return new XElement(W.ins); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Inserted paragraph mark |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:ins w:id="0" w:author="Eric White" w:date="2017-03-24T21:58:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:r> |
| <w:t xml:space="preserve">Video provides a powerful way to help you prove your point. </w:t> |
| </w:r> |
| </w:p> |
| <w:p> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>When you click Online Video, you can paste in the embed code for the video you want to add.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.ins && |
| parent.Name == W.rPr && |
| parent.Parent.Name == W.pPr) |
| { |
| return new XElement(W.del); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Inserted run |
| #if false |
| <w:p> |
| <w:r> |
| <w:t xml:space="preserve">Video </w:t> |
| </w:r> |
| <w:ins> |
| <w:r> |
| <w:t xml:space="preserve">provides </w:t> |
| </w:r> |
| </w:ins> |
| <w:r> |
| <w:t>a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.ins && |
| parent.Name == W.p) |
| { |
| var newRri = new ReverseRevisionsInfo() { InInsert = true }; |
| return new XElement(W.del, |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted table row |
| #if false |
| <w:tbl> |
| <w:tr> |
| <w:tc> |
| <w:p> |
| <w:r> |
| <w:t>1</w:t> |
| </w:r> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| <w:tr> |
| <w:trPr> |
| <w:del w:id="0" w:author="Eric White" w:date="2017-03-24T22:15:00Z"/> |
| </w:trPr> |
| <w:tc> |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:del w:id="1" w:author="Eric White" w:date="2017-03-24T22:15:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:del w:id="2" w:author="Eric White" w:date="2017-03-24T22:15:00Z"> |
| <w:r> |
| <w:delText>4</w:delText> |
| </w:r> |
| </w:del> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| <w:tr> |
| <w:tc> |
| <w:p> |
| <w:r> |
| <w:t>7</w:t> |
| </w:r> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| </w:tbl> |
| #endif |
| if (element.Name == W.del && |
| parent.Name == W.trPr) |
| { |
| return new XElement(W.ins); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Inserted table row |
| #if false |
| <w:tbl> |
| <w:tr> |
| <w:tc> |
| <w:p> |
| <w:r> |
| <w:t>1</w:t> |
| </w:r> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| <w:tr> |
| <w:trPr> |
| <w:ins w:id="0" w:author="Eric White" w:date="2017-03-24T22:16:00Z"/> |
| </w:trPr> |
| <w:tc> |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:ins w:id="1" w:author="Eric White" w:date="2017-03-24T22:16:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:ins w:id="2" w:author="Eric White" w:date="2017-03-24T22:16:00Z"> |
| <w:r> |
| <w:t>1a</w:t> |
| </w:r> |
| </w:ins> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| <w:tr> |
| <w:tc> |
| <w:p> |
| <w:r> |
| <w:t>4</w:t> |
| </w:r> |
| </w:p> |
| </w:tc> |
| </w:tr> |
| </w:tbl> |
| #endif |
| if (element.Name == W.ins && |
| parent.Name == W.trPr) |
| { |
| return new XElement(W.del); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted math control character |
| #if false |
| <w:p w:rsidR="007F4E48" w:rsidRDefault="00C9403B"> |
| <m:oMathPara> |
| <m:oMath> |
| <m:r> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>A=</m:t> |
| </m:r> |
| <m:r> |
| <w:del w:id="0" w:author="Eric White" w:date="2017-03-24T22:53:00Z"> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>2</m:t> |
| </w:del> |
| </m:r> |
| <m:r> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>π</m:t> |
| </m:r> |
| #endif |
| if (element.Name == W.del && |
| parent.Name == M.r) |
| { |
| return new XElement(W.ins, |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Inserted math control character |
| #if false |
| <w:p w:rsidR="007F4E48" w:rsidRDefault="00C9403B"> |
| <m:oMathPara> |
| <m:oMath> |
| <m:r> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>A=</m:t> |
| </m:r> |
| <m:r> |
| <w:ins w:id="0" w:author="Eric White" w:date="2017-03-24T22:54:00Z"> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>2</m:t> |
| </w:ins> |
| </m:r> |
| <m:r> |
| <w:rPr> |
| <w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/> |
| </w:rPr> |
| <m:t>π</m:t> |
| </m:r> |
| #endif |
| if (element.Name == W.ins && |
| parent.Name == M.r) |
| { |
| return new XElement(W.del, |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // moveFrom / moveTo |
| #if false |
| <w:p> |
| <w:r> |
| <w:t>Video provides a powerful way.</w:t> |
| </w:r> |
| </w:p> |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:moveFrom w:id="0" w:author="Eric White" w:date="2017-03-24T23:18:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:moveFromRangeStart w:id="1" w:author="Eric White" w:date="2017-03-24T23:18:00Z" w:name="move478160808"/> |
| <w:moveFrom w:id="2" w:author="Eric White" w:date="2017-03-24T23:18:00Z"> |
| <w:r> |
| <w:t>When you click Online Video.</w:t> |
| </w:r> |
| </w:moveFrom> |
| </w:p> |
| <w:moveFromRangeEnd w:id="1"/> |
| <w:p> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>You can also type a keyword.</w:t> |
| </w:r> |
| </w:p> |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:moveTo w:id="3" w:author="Eric White" w:date="2017-03-24T23:18:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:moveToRangeStart w:id="5" w:author="Eric White" w:date="2017-03-24T23:18:00Z" w:name="move478160808"/> |
| <w:moveTo w:id="6" w:author="Eric White" w:date="2017-03-24T23:18:00Z"> |
| <w:r> |
| <w:t>When you click Online Video.</w:t> |
| </w:r> |
| </w:moveTo> |
| </w:p> |
| <w:moveToRangeEnd w:id="5"/> |
| <w:p> |
| <w:r> |
| <w:t>Make your document look professionally produced.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.moveFrom) |
| { |
| return new XElement(W.moveTo, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.moveFromRangeStart) |
| { |
| return new XElement(W.moveToRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.moveFromRangeEnd) |
| { |
| return new XElement(W.moveToRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.moveTo) |
| { |
| return new XElement(W.moveFrom, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.moveToRangeStart) |
| { |
| return new XElement(W.moveFromRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.moveToRangeEnd) |
| { |
| return new XElement(W.moveFromRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted content control |
| #if false |
| <w:p> |
| <w:customXmlDelRangeStart w:id="1" w:author="Eric White" w:date="2017-03-25T22:10:00Z"/> |
| <w:sdt> |
| <w:sdtPr> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:id w:val="990292373"/> |
| <w:placeholder> |
| <w:docPart w:val="DefaultPlaceholder_-1854013440"/> |
| </w:placeholder> |
| <w:text/> |
| </w:sdtPr> |
| <w:sdtContent> |
| <w:customXmlDelRangeEnd w:id="1"/> |
| <w:r> |
| <w:t>Video</w:t> |
| </w:r> |
| <w:customXmlDelRangeStart w:id="2" w:author="Eric White" w:date="2017-03-25T22:10:00Z"/> |
| </w:sdtContent> |
| </w:sdt> |
| <w:customXmlDelRangeEnd w:id="2"/> |
| <w:r> |
| <w:t xml:space="preserve"> provides a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.customXmlDelRangeStart) |
| { |
| return new XElement(W.customXmlInsRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.customXmlDelRangeEnd) |
| { |
| return new XElement(W.customXmlInsRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Inserted content control |
| #if false |
| <w:p> |
| <w:customXmlInsRangeStart w:id="0" w:author="Eric White" w:date="2017-03-25T22:10:00Z"/> |
| <w:sdt> |
| <w:sdtPr> |
| <w:id w:val="-473839966"/> |
| <w:placeholder> |
| <w:docPart w:val="DefaultPlaceholder_-1854013440"/> |
| </w:placeholder> |
| <w:text/> |
| </w:sdtPr> |
| <w:sdtContent> |
| <w:customXmlInsRangeEnd w:id="0"/> |
| <w:r> |
| <w:t>Video</w:t> |
| </w:r> |
| <w:customXmlInsRangeStart w:id="1" w:author="Eric White" w:date="2017-03-25T22:10:00Z"/> |
| </w:sdtContent> |
| </w:sdt> |
| <w:customXmlInsRangeEnd w:id="1"/> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t xml:space="preserve"> provides a powerful way to help you prove your point.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.customXmlInsRangeStart) |
| { |
| return new XElement(W.customXmlDelRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.customXmlInsRangeEnd) |
| { |
| return new XElement(W.customXmlDelRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Moved content control |
| #if false |
| <w:p> |
| <w:r> |
| <w:t>Video provides a powerful way.</w:t> |
| </w:r> |
| </w:p> |
| <w:customXmlMoveFromRangeStart w:id="0" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| <w:moveFromRangeStart w:id="1" w:author="Eric White" w:date="2017-03-25T22:21:00Z" w:name="move478243824" w:displacedByCustomXml="next"/> |
| <w:sdt> |
| <w:sdtPr> |
| <w:id w:val="-2060007328"/> |
| <w:placeholder> |
| <w:docPart w:val="DefaultPlaceholder_-1854013440"/> |
| </w:placeholder> |
| </w:sdtPr> |
| <w:sdtContent> |
| <w:customXmlMoveFromRangeEnd w:id="0"/> |
| <w:p w:rsidR="00D306FD" w:rsidDel="001037E6" w:rsidRDefault="00D306FD"> |
| <w:pPr> |
| <w:rPr> |
| <w:moveFrom w:id="2" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| </w:pPr> |
| <w:moveFrom w:id="3" w:author="Eric White" w:date="2017-03-25T22:21:00Z"> |
| <w:r w:rsidDel="001037E6"> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>When you click Online Video.</w:t> |
| </w:r> |
| </w:moveFrom> |
| </w:p> |
| <w:customXmlMoveFromRangeStart w:id="4" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| </w:sdtContent> |
| </w:sdt> |
| <w:customXmlMoveFromRangeEnd w:id="4"/> |
| <w:moveFromRangeEnd w:id="1"/> |
| <w:p> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>You can also type a keyword.</w:t> |
| </w:r> |
| </w:p> |
| <w:p> |
| <w:r> |
| <w:rPr> |
| <w:lang w:val="en-US"/> |
| </w:rPr> |
| <w:t>To make your document look.</w:t> |
| </w:r> |
| </w:p> |
| <w:customXmlMoveToRangeStart w:id="5" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| <w:moveToRangeStart w:id="6" w:author="Eric White" w:date="2017-03-25T22:21:00Z" w:name="move478243824" w:displacedByCustomXml="next"/> |
| <w:sdt> |
| <w:sdtPr> |
| <w:id w:val="-483622649"/> |
| <w:placeholder> |
| <w:docPart w:val="DC46F197491D4EC8B79DB4CE2D22E222"/> |
| </w:placeholder> |
| </w:sdtPr> |
| <w:sdtContent> |
| <w:customXmlMoveToRangeEnd w:id="5"/> |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:moveTo w:id="8" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:moveTo w:id="9" w:author="Eric White" w:date="2017-03-25T22:21:00Z"> |
| <w:r> |
| <w:t>When you click Online Video.</w:t> |
| </w:r> |
| </w:moveTo> |
| </w:p> |
| <w:customXmlMoveToRangeStart w:id="10" w:author="Eric White" w:date="2017-03-25T22:21:00Z"/> |
| </w:sdtContent> |
| </w:sdt> |
| <w:customXmlMoveToRangeEnd w:id="10"/> |
| <w:moveToRangeEnd w:id="6"/> |
| <w:p> |
| <w:ins w:id="11" w:author="Eric White" w:date="2017-03-25T22:21:00Z"> |
| <w:r> |
| <w:t xml:space="preserve"> </w:t> |
| </w:r> |
| </w:ins> |
| <w:r> |
| <w:t>For example, you can add.</w:t> |
| </w:r> |
| </w:p> |
| #endif |
| if (element.Name == W.customXmlMoveFromRangeStart) |
| { |
| return new XElement(W.customXmlMoveToRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.customXmlMoveFromRangeEnd) |
| { |
| return new XElement(W.customXmlMoveToRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.customXmlMoveToRangeStart) |
| { |
| return new XElement(W.customXmlMoveFromRangeStart, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| if (element.Name == W.customXmlMoveToRangeEnd) |
| { |
| return new XElement(W.customXmlMoveFromRangeEnd, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Deleted field code |
| #if false |
| <w:p> |
| <w:pPr> |
| <w:rPr> |
| <w:del w:id="0" w:author="Eric White" w:date="2017-03-25T22:43:00Z"/> |
| </w:rPr> |
| </w:pPr> |
| <w:del w:id="1" w:author="Eric White" w:date="2017-03-25T22:43:00Z"> |
| <w:r> |
| <w:fldChar w:fldCharType="begin"/> |
| </w:r> |
| <w:r> |
| <w:delInstrText xml:space="preserve"> D</w:delInstrText> |
| </w:r> |
| <w:r> |
| <w:rPr> |
| <w:color w:val="FF0000"/> |
| </w:rPr> |
| <w:delInstrText>A</w:delInstrText> |
| </w:r> |
| <w:r> |
| <w:delInstrText xml:space="preserve">TE </w:delInstrText> |
| </w:r> |
| <w:r> |
| <w:fldChar w:fldCharType="separate"/> |
| </w:r> |
| <w:r> |
| <w:delText>25/03/2017</w:delText> |
| </w:r> |
| <w:r> |
| <w:fldChar w:fldCharType="end"/> |
| </w:r> |
| </w:del> |
| </w:p> |
| #endif |
| if (element.Name == W.delInstrText) |
| { |
| return new XElement(W.instrText, |
| element.Attributes(), // pulls in xml:space attribute |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Change inserted instrText element to w:delInstrText |
| if (element.Name == W.instrText && rri.InInsert) |
| { |
| return new XElement(W.delInstrText, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Change inserted text element to w:delText |
| if (element.Name == W.t && rri.InInsert) |
| { |
| return new XElement(W.delText, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Change w:delText to w:t |
| if (element.Name == W.delText) |
| { |
| return new XElement(W.t, |
| element.Attributes(), // pulls in xml:space attribute |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // Identity transform |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => ReverseRevisionsTransform(n, rri))); |
| } |
| return node; |
| } |
| |
| public static WmlDocument AcceptRevisions(WmlDocument document) |
| { |
| using (OpenXmlMemoryStreamDocument streamDoc = new OpenXmlMemoryStreamDocument(document)) |
| { |
| using (WordprocessingDocument doc = streamDoc.GetWordprocessingDocument()) |
| { |
| AcceptRevisions(doc); |
| } |
| return streamDoc.GetModifiedWmlDocument(); |
| } |
| } |
| |
| public static void AcceptRevisions(WordprocessingDocument doc) |
| { |
| AcceptRevisionsForPart(doc.MainDocumentPart); |
| foreach (var part in doc.MainDocumentPart.HeaderParts) |
| AcceptRevisionsForPart(part); |
| foreach (var part in doc.MainDocumentPart.FooterParts) |
| AcceptRevisionsForPart(part); |
| if (doc.MainDocumentPart.EndnotesPart != null) |
| AcceptRevisionsForPart(doc.MainDocumentPart.EndnotesPart); |
| if (doc.MainDocumentPart.FootnotesPart != null) |
| AcceptRevisionsForPart(doc.MainDocumentPart.FootnotesPart); |
| if (doc.MainDocumentPart.StyleDefinitionsPart != null) |
| AcceptRevisionsForStylesDefinitionPart(doc.MainDocumentPart.StyleDefinitionsPart); |
| } |
| |
| private static void AcceptRevisionsForStylesDefinitionPart(StyleDefinitionsPart stylesDefinitionsPart) |
| { |
| var xDoc = stylesDefinitionsPart.GetXDocument(); |
| var newRoot = AcceptRevisionsForStylesTransform(xDoc.Root); |
| xDoc.Root.ReplaceWith(newRoot); |
| stylesDefinitionsPart.PutXDocument(); |
| } |
| |
| private static object AcceptRevisionsForStylesTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.pPrChange || element.Name == W.rPrChange) |
| return null; |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptRevisionsForStylesTransform(n))); |
| } |
| return node; |
| } |
| |
| public static void AcceptRevisionsForPart(OpenXmlPart part) |
| { |
| XElement documentElement = part.GetXDocument().Root; |
| documentElement = (XElement)RemoveRsidTransform(documentElement); |
| documentElement = (XElement)FixUpDeletedOrInsertedFieldCodesTransform(documentElement); |
| var containsMoveFromMoveTo = documentElement.Descendants(W.moveFrom).Any(); |
| documentElement = (XElement)AcceptMoveFromMoveToTransform(documentElement); |
| documentElement = AcceptMoveFromRanges(documentElement); |
| // AcceptParagraphEndTagsInMoveFromTransform needs rewritten similar to AcceptDeletedAndMoveFromParagraphMarks |
| documentElement = (XElement)AcceptParagraphEndTagsInMoveFromTransform(documentElement); |
| documentElement = AcceptDeletedAndMovedFromContentControls(documentElement); |
| documentElement = AcceptDeletedAndMoveFromParagraphMarks(documentElement); |
| if (containsMoveFromMoveTo) |
| documentElement = (XElement)RemoveRowsLeftEmptyByMoveFrom(documentElement); |
| documentElement = (XElement)AcceptAllOtherRevisionsTransform(documentElement); |
| documentElement = (XElement)AcceptDeletedCellsTransform(documentElement); |
| documentElement = (XElement)MergeAdjacentTablesTransform(documentElement); |
| documentElement = (XElement)AddEmptyParagraphToAnyEmptyCells(documentElement); |
| documentElement.Descendants().Attributes().Where(a => a.Name == PT.UniqueId || a.Name == PT.RunIds).Remove(); |
| documentElement.Descendants(W.numPr).Where(np => !np.HasElements).Remove(); |
| XDocument newXDoc = new XDocument(documentElement); |
| part.PutXDocument(newXDoc); |
| } |
| |
| // Note that AcceptRevisionsForElement is an incomplete implementation. It is not possible to accept all varieties of revisions |
| // for a single paragraph. The paragraph may contain a marker for a deleted or inserted content control, as one example, of |
| // which there are many. This method accepts simple revisions, such as deleted or inserted text, which is the most common use |
| // case. |
| public static XElement AcceptRevisionsForElement(XElement element) |
| { |
| XElement rElement = element; |
| rElement = (XElement)RemoveRsidTransform(rElement); |
| var containsMoveFromMoveTo = rElement.Descendants(W.moveFrom).Any(); |
| rElement = (XElement)AcceptMoveFromMoveToTransform(rElement); |
| rElement = (XElement)AcceptAllOtherRevisionsTransform(rElement); |
| rElement.Descendants().Attributes().Where(a => a.Name == PT.UniqueId || a.Name == PT.RunIds).Remove(); |
| rElement.Descendants(W.numPr).Where(np => !np.HasElements).Remove(); |
| return rElement; |
| } |
| |
| private static object FixUpDeletedOrInsertedFieldCodesTransform(XNode node) |
| { |
| var element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.p) |
| { |
| // 1 other |
| // 2 w:del/w:r/w:fldChar |
| // 3 w:ins/w:r/w:fldChar |
| // 4 w:instrText |
| |
| // formulate new paragraph, looking for 4 that has 2 (or 3) before and after. Then put in a w:del (or w:ins), transforming w:instrText to w:delInstrText if w:del. |
| // transform 1, 2, 3 as usual |
| |
| var groupedParaContentsKey = element.Elements().Select(e => |
| { |
| if (e.Name == W.del && e.Elements(W.r).Elements(W.fldChar).Any()) |
| return 2; |
| if (e.Name == W.ins && e.Elements(W.r).Elements(W.fldChar).Any()) |
| return 3; |
| if (e.Name == W.r && e.Element(W.instrText) != null) |
| return 4; |
| return 1; |
| }); |
| |
| var zipped = element.Elements().Zip(groupedParaContentsKey, (e, k) => new { Ele = e, Key = k }); |
| |
| var grouped = zipped.GroupAdjacent(z => z.Key).ToArray(); |
| |
| var gLen = grouped.Length; |
| |
| //if (gLen != 1) |
| // Console.WriteLine(); |
| |
| var newParaContents = grouped |
| .Select((g, i) => |
| { |
| if (g.Key == 1 || g.Key == 2 || g.Key == 3) |
| return (object)g.Select(gc => FixUpDeletedOrInsertedFieldCodesTransform(gc.Ele)); |
| if (g.Key == 4) |
| { |
| if (i == 0 || i == gLen - 1) |
| return g.Select(gc => FixUpDeletedOrInsertedFieldCodesTransform(gc.Ele)); |
| if (grouped[i-1].Key == 2 && |
| grouped[i+1].Key == 2) |
| { |
| return new XElement(W.del, |
| g.Select(gc => TransformInstrTextToDelInstrText(gc.Ele))); |
| } |
| else if (grouped[i - 1].Key == 3 && |
| grouped[i + 1].Key == 3) |
| { |
| return new XElement(W.ins, |
| g.Select(gc => FixUpDeletedOrInsertedFieldCodesTransform(gc.Ele))); |
| } |
| else |
| { |
| return g.Select(gc => FixUpDeletedOrInsertedFieldCodesTransform(gc.Ele)); |
| } |
| } |
| throw new OpenXmlPowerToolsException("Internal error"); |
| }); |
| |
| var newParagraph = new XElement(W.p, |
| element.Attributes(), |
| newParaContents); |
| return newParagraph; |
| } |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => FixUpDeletedOrInsertedFieldCodesTransform(n))); |
| } |
| return node; |
| } |
| |
| private static object TransformInstrTextToDelInstrText(XNode node) |
| { |
| var element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.instrText) |
| return new XElement(W.delInstrText, |
| element.Attributes(), |
| element.Nodes()); |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => TransformInstrTextToDelInstrText(n))); |
| } |
| return node; |
| } |
| |
| private static object AddEmptyParagraphToAnyEmptyCells(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.tc && !element.Elements().Where(e => e.Name != W.tcPr).Any()) |
| return new XElement(W.tc, |
| element.Attributes(), |
| element.Elements(), |
| new XElement(W.p)); |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AddEmptyParagraphToAnyEmptyCells(n))); |
| } |
| return node; |
| } |
| |
| private static Dictionary<XName, int> Order_tcPr = new Dictionary<XName, int> |
| { |
| { W.cnfStyle, 10 }, |
| { W.tcW, 20 }, |
| { W.gridSpan, 30 }, |
| { W.hMerge, 40 }, |
| { W.vMerge, 50 }, |
| { W.tcBorders, 60 }, |
| { W.shd, 70 }, |
| { W.noWrap, 80 }, |
| { W.tcMar, 90 }, |
| { W.textDirection, 100 }, |
| { W.tcFitText, 110 }, |
| { W.vAlign, 120 }, |
| { W.hideMark, 130 }, |
| { W.headers, 140 }, |
| }; |
| |
| private static XElement FixWidths(XElement tbl) |
| { |
| var newTbl = new XElement(tbl); |
| var gridLines = tbl.Elements(W.tblGrid).Elements(W.gridCol).Attributes(W._w).Select(w => (int)w).ToArray(); |
| foreach (var tr in newTbl.Elements(W.tr)) |
| { |
| int used = 0; |
| int lastUsed = -1; |
| foreach (var tc in tr.Elements(W.tc)) |
| { |
| var tcW = tc.Elements(W.tcPr).Elements(W.tcW).Attributes(W._w).FirstOrDefault(); |
| if (tcW != null) |
| { |
| int? gridSpan = (int?)tc.Elements(W.tcPr).Elements(W.gridSpan).Attributes(W.val).FirstOrDefault(); |
| |
| if (gridSpan == null) |
| gridSpan = 1; |
| |
| var z = Math.Min(gridLines.Length - 1, lastUsed + (int)gridSpan); |
| int w = gridLines.Where((g, i) => i > lastUsed && i <= z).Sum(); |
| tcW.Value = w.ToString(); |
| |
| lastUsed += (int)gridSpan; |
| used += (int)gridSpan; |
| } |
| } |
| } |
| return newTbl; |
| } |
| |
| private static object AcceptMoveFromMoveToTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.moveTo) |
| return element.Nodes().Select(n => AcceptMoveFromMoveToTransform(n)); |
| if (element.Name == W.moveFrom) |
| return null; |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptMoveFromMoveToTransform(n))); |
| } |
| return node; |
| } |
| |
| private static XElement AcceptMoveFromRanges(XElement document) |
| { |
| string wordProcessingNamespacePrefix = document.GetPrefixOfNamespace(W.w); |
| |
| // The following lists contain the elements that are between start/end elements. |
| List<XElement> startElementTagsInMoveFromRange = new List<XElement>(); |
| List<XElement> endElementTagsInMoveFromRange = new List<XElement>(); |
| |
| // Following are the elements that *may* be in a range that has both start and end |
| // elements. |
| Dictionary<string, PotentialInRangeElements> potentialDeletedElements = |
| new Dictionary<string, PotentialInRangeElements>(); |
| |
| foreach (var tag in DescendantAndSelfTags(document)) |
| { |
| if (tag.Element.Name == W.moveFromRangeStart) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| potentialDeletedElements.Add(id, new PotentialInRangeElements()); |
| continue; |
| } |
| if (tag.Element.Name == W.moveFromRangeEnd) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| if (potentialDeletedElements.ContainsKey(id)) |
| { |
| startElementTagsInMoveFromRange.AddRange( |
| potentialDeletedElements[id].PotentialStartElementTagsInRange); |
| endElementTagsInMoveFromRange.AddRange( |
| potentialDeletedElements[id].PotentialEndElementTagsInRange); |
| potentialDeletedElements.Remove(id); |
| } |
| continue; |
| } |
| if (potentialDeletedElements.Count > 0) |
| { |
| if (tag.TagType == TagTypeEnum.Element && |
| (tag.Element.Name != W.moveFromRangeStart && |
| tag.Element.Name != W.moveFromRangeEnd)) |
| { |
| foreach (var id in potentialDeletedElements) |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EmptyElement && |
| (tag.Element.Name != W.moveFromRangeStart && |
| tag.Element.Name != W.moveFromRangeEnd)) |
| { |
| foreach (var id in potentialDeletedElements) |
| { |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| } |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EndElement && |
| (tag.Element.Name != W.moveFromRangeStart && |
| tag.Element.Name != W.moveFromRangeEnd)) |
| { |
| foreach (var id in potentialDeletedElements) |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| } |
| } |
| var moveFromElementsToDelete = startElementTagsInMoveFromRange |
| .Intersect(endElementTagsInMoveFromRange) |
| .ToArray(); |
| if (moveFromElementsToDelete.Count() > 0) |
| return (XElement)AcceptMoveFromRangesTransform( |
| document, moveFromElementsToDelete); |
| return document; |
| } |
| |
| private enum MoveFromCollectionType |
| { |
| ParagraphEndTagInMoveFromRange, |
| Other |
| }; |
| |
| private static object AcceptParagraphEndTagsInMoveFromTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (W.BlockLevelContentContainers.Contains(element.Name)) |
| { |
| var groupedBodyChildren = element |
| .Elements() |
| .GroupAdjacent(c => |
| { |
| BlockContentInfo pi = c.GetParagraphInfo(); |
| if (pi.ThisBlockContentElement != null) |
| { |
| bool paragraphMarkIsInMoveFromRange = |
| pi.ThisBlockContentElement.Elements(W.moveFromRangeStart).Any() && |
| !pi.ThisBlockContentElement.Elements(W.moveFromRangeEnd).Any(); |
| if (paragraphMarkIsInMoveFromRange) |
| return MoveFromCollectionType.ParagraphEndTagInMoveFromRange; |
| } |
| XElement previousContentElement = c.ContentElementsBeforeSelf() |
| .Where(e => e.GetParagraphInfo().ThisBlockContentElement != null) |
| .FirstOrDefault(); |
| if (previousContentElement != null) |
| { |
| BlockContentInfo pi2 = previousContentElement.GetParagraphInfo(); |
| if (c.Name == W.p && |
| pi2.ThisBlockContentElement.Elements(W.moveFromRangeStart).Any() && |
| !pi2.ThisBlockContentElement.Elements(W.moveFromRangeEnd).Any()) |
| return MoveFromCollectionType.ParagraphEndTagInMoveFromRange; |
| } |
| return MoveFromCollectionType.Other; |
| }) |
| .ToList(); |
| |
| // If there is only one group, and it's key is MoveFromCollectionType.Other |
| // then there is nothing to do. |
| if (groupedBodyChildren.Count() == 1 && |
| groupedBodyChildren.First().Key == MoveFromCollectionType.Other) |
| { |
| XElement newElement = new XElement(element.Name, |
| element.Attributes(), |
| groupedBodyChildren.Select(g => |
| { |
| if (g.Key == MoveFromCollectionType.Other) |
| return (object)g; |
| |
| // This is a transform that produces the first element in the |
| // collection, except that the paragraph in the descendents is |
| // replaced with a new paragraph that contains all contents of the |
| // existing paragraph, plus subsequent elements in the group |
| // collection, where the paragraph in each of those groups is |
| // collapsed. |
| return CoalesqueParagraphEndTagsInMoveFromTransform(g.First(), g); |
| })); |
| return newElement; |
| } |
| else |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => |
| AcceptParagraphEndTagsInMoveFromTransform(n))); |
| } |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptParagraphEndTagsInMoveFromTransform(n))); |
| } |
| return node; |
| } |
| |
| private static object AcceptAllOtherRevisionsTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| /// Accept inserted text, inserted paragraph marks, etc. |
| /// Collapse all w:ins elements. |
| |
| if (element.Name == W.ins) |
| return element |
| .Nodes() |
| .Select(n => AcceptAllOtherRevisionsTransform(n)); |
| |
| /// Remove all of the following elements. These elements are processed in: |
| /// AcceptDeletedAndMovedFromContentControls |
| /// AcceptMoveFromMoveToTransform |
| /// AcceptDeletedAndMoveFromParagraphMarksTransform |
| /// AcceptParagraphEndTagsInMoveFromTransform |
| /// AcceptMoveFromRanges |
| |
| if (element.Name == W.customXmlDelRangeStart || |
| element.Name == W.customXmlDelRangeEnd || |
| element.Name == W.customXmlInsRangeStart || |
| element.Name == W.customXmlInsRangeEnd || |
| element.Name == W.customXmlMoveFromRangeStart || |
| element.Name == W.customXmlMoveFromRangeEnd || |
| element.Name == W.customXmlMoveToRangeStart || |
| element.Name == W.customXmlMoveToRangeEnd || |
| element.Name == W.moveFromRangeStart || |
| element.Name == W.moveFromRangeEnd || |
| element.Name == W.moveToRangeStart || |
| element.Name == W.moveToRangeEnd) |
| return null; |
| |
| /// Accept revisions in formatting on paragraphs. |
| /// Accept revisions in formatting on runs. |
| /// Accept revisions for applied styles to a table. |
| /// Accept revisions for grid revisions to a table. |
| /// Accept revisions for column properties. |
| /// Accept revisions for row properties. |
| /// Accept revisions for table level property exceptions. |
| /// Accept revisions for section properties. |
| /// Accept numbering revision in fields. |
| /// Accept deleted field code text. |
| /// Accept deleted literal text. |
| /// Accept inserted cell. |
| |
| if (element.Name == W.pPrChange || |
| element.Name == W.rPrChange || |
| element.Name == W.tblPrChange || |
| element.Name == W.tblGridChange || |
| element.Name == W.tcPrChange || |
| element.Name == W.trPrChange || |
| element.Name == W.tblPrExChange || |
| element.Name == W.sectPrChange || |
| element.Name == W.numberingChange || |
| element.Name == W.delInstrText || |
| element.Name == W.delText || |
| element.Name == W.cellIns) |
| return null; |
| |
| // Accept revisions for deleted math control character. |
| // Match m:f/m:fPr/m:ctrlPr/w:del, remove m:f. |
| |
| if (element.Name == M.f && |
| element.Elements(M.fPr).Elements(M.ctrlPr).Elements(W.del).Any()) |
| return null; |
| |
| // Accept revisions for deleted rows in tables. |
| // Match w:tr/w:trPr/w:del, remove w:tr. |
| |
| if (element.Name == W.tr && |
| element.Elements(W.trPr).Elements(W.del).Any()) |
| return null; |
| |
| // Accept deleted text in paragraphs. |
| |
| if (element.Name == W.del) |
| return null; |
| |
| // Accept revisions for vertically merged cells. |
| // cellMerge with a parent of tcPr, with attribute w:vMerge="rest" transformed |
| // to <w:vMerge w:val="restart"/> |
| // cellMerge with a parent of tcPr, with attribute w:vMerge="cont" transformed |
| // to <w:vMerge w:val="continue"/> |
| |
| if (element.Name == W.cellMerge && |
| element.Parent.Name == W.tcPr && |
| (string)element.Attribute(W.vMerge) == "rest") |
| return new XElement(W.vMerge, |
| new XAttribute(W.val, "restart")); |
| if (element.Name == W.cellMerge && |
| element.Parent.Name == W.tcPr && |
| (string)element.Attribute(W.vMerge) == "cont") |
| return new XElement(W.vMerge, |
| new XAttribute(W.val, "continue")); |
| |
| // Otherwise do identity clone. |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptAllOtherRevisionsTransform(n))); |
| } |
| return node; |
| } |
| |
| private static object CollapseParagraphTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.p) |
| return element.Elements().Where(e => e.Name != W.pPr); |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => CollapseParagraphTransform(n))); |
| } |
| return node; |
| } |
| |
| private enum DeletedParagraphCollectionType |
| { |
| DeletedParagraphMarkContent, |
| ParagraphFollowing, |
| Other |
| }; |
| |
| /// Accept deleted paragraphs. |
| /// |
| /// Group together all paragraphs that contain w:p/w:pPr/w:rPr/w:del elements. Make a |
| /// second group for the content element immediately following a paragraph that contains |
| /// a w:del element. The code uses the approach of dealing with paragraph content at |
| /// 'levels', ignoring paragraph content at other levels. Form a new paragraph that |
| /// contains the content of the grouped paragraphs with deleted paragraph marks, and the |
| /// content of the paragraph immediately following a paragraph that contains a deleted |
| /// paragraph mark. Include in the new paragraph the paragraph properties from the |
| /// paragraph following. When assembling the new paragraph, use a transform that collapses |
| /// the paragraph nodes when adding content, thereby preserving custom XML and content |
| /// controls. |
| |
| private static void AnnotateBlockContentElements(XElement contentContainer) |
| { |
| // For convenience, there is a ParagraphInfo annotation on the contentContainer. |
| // It contains the same information as the ParagraphInfo annotation on the first |
| // paragraph. |
| if (contentContainer.Annotation<BlockContentInfo>() != null) |
| return; |
| XElement firstContentElement = contentContainer |
| .Elements() |
| .DescendantsAndSelf() |
| .FirstOrDefault(e => e.Name == W.p || e.Name == W.tbl); |
| if (firstContentElement == null) |
| return; |
| |
| // Add the annotation on the contentContainer. |
| BlockContentInfo currentContentInfo = new BlockContentInfo() |
| { |
| PreviousBlockContentElement = null, |
| ThisBlockContentElement = firstContentElement, |
| NextBlockContentElement = null |
| }; |
| // Add as annotation even though NextParagraph is not set yet. |
| contentContainer.AddAnnotation(currentContentInfo); |
| while (true) |
| { |
| currentContentInfo.ThisBlockContentElement.AddAnnotation(currentContentInfo); |
| // Find next sibling content element. |
| XElement nextContentElement = null; |
| XElement current = currentContentInfo.ThisBlockContentElement; |
| while (true) |
| { |
| nextContentElement = current |
| .ElementsAfterSelf() |
| .DescendantsAndSelf() |
| .FirstOrDefault(e => e.Name == W.p || e.Name == W.tbl); |
| if (nextContentElement != null) |
| { |
| currentContentInfo.NextBlockContentElement = nextContentElement; |
| break; |
| } |
| current = current.Parent; |
| // When we've backed up the tree to the contentContainer, we're done. |
| if (current == contentContainer) |
| return; |
| } |
| currentContentInfo = new BlockContentInfo() |
| { |
| PreviousBlockContentElement = currentContentInfo.ThisBlockContentElement, |
| ThisBlockContentElement = nextContentElement, |
| NextBlockContentElement = null |
| }; |
| } |
| } |
| |
| private static IEnumerable<BlockContentInfo> IterateBlockContentElements(XElement element) |
| { |
| XElement current = element.Elements().FirstOrDefault(); |
| if (current == null) |
| yield break; |
| AnnotateBlockContentElements(element); |
| BlockContentInfo currentBlockContentInfo = element.Annotation<BlockContentInfo>(); |
| if (currentBlockContentInfo != null) |
| { |
| while (true) |
| { |
| yield return currentBlockContentInfo; |
| if (currentBlockContentInfo.NextBlockContentElement == null) |
| yield break; |
| currentBlockContentInfo = currentBlockContentInfo.NextBlockContentElement.Annotation<BlockContentInfo>(); |
| } |
| } |
| } |
| |
| public static class PT |
| { |
| public static XNamespace pt = "http://www.codeplex.com/PowerTools/2009/RevisionAccepter"; |
| public static XName UniqueId = pt + "UniqueId"; |
| public static XName RunIds = pt + "RunIds"; |
| } |
| |
| private static void AnnotateRunElementsWithId(XElement element) |
| { |
| int runId = 0; |
| foreach (XElement e in element.Descendants().Where(e => e.Name == W.r)) |
| { |
| if (e.Name == W.r) |
| e.Add(new XAttribute(PT.UniqueId, runId++)); |
| } |
| } |
| |
| private static void AnnotateContentControlsWithRunIds(XElement element) |
| { |
| int sdtId = 0; |
| foreach (XElement e in element.Descendants(W.sdt)) |
| { |
| // old version |
| //e.Add(new XAttribute(PT.RunIds, |
| // e.Descendants(W.r).Select(r => r.Attribute(PT.UniqueId).Value).StringConcatenate(s => s + ",").Trim(',')), |
| // new XAttribute(PT.UniqueId, sdtId++)); |
| e.Add(new XAttribute(PT.RunIds, |
| e.DescendantsTrimmed(W.txbxContent) |
| .Where(d => d.Name == W.r) |
| .Select(r => r.Attribute(PT.UniqueId).Value) |
| .StringConcatenate(s => s + ",") |
| .Trim(',')), |
| new XAttribute(PT.UniqueId, sdtId++)); |
| } |
| } |
| |
| private static XElement AddBlockLevelContentControls(XElement newDocument, XElement original) |
| { |
| var originalContentControls = original.Descendants(W.sdt).ToList(); |
| var existingContentControls = newDocument.Descendants(W.sdt).ToList(); |
| var contentControlsToAdd = originalContentControls |
| .Select(occ => occ.Attribute(PT.UniqueId).Value) |
| .Except(existingContentControls |
| .Select(ecc => ecc.Attribute(PT.UniqueId).Value)); |
| foreach (var contentControl in originalContentControls |
| .Where(occ => contentControlsToAdd.Contains(occ.Attribute(PT.UniqueId).Value))) |
| { |
| // TODO - Need a slight modification here. If there is a paragraph |
| // in the content control that contains no runs, then the paragraph isn't included in the |
| // content control, because the following triggers off of runs. |
| // To see an example of this, see example document "NumberingParagraphPropertiesChange.docxs" |
| |
| // find list of runs to surround |
| var runIds = contentControl.Attribute(PT.RunIds).Value.Split(','); |
| var runs = contentControl.Descendants(W.r).Where(r => runIds.Contains(r.Attribute(PT.UniqueId).Value)); |
| // find the runs in the new document |
| |
| var runsInNewDocument = runs.Select(r => newDocument.Descendants(W.r).First(z => z.Attribute(PT.UniqueId).Value == r.Attribute(PT.UniqueId).Value)).ToList(); |
| |
| // find common ancestor |
| List<XElement> runAncestorIntersection = null; |
| foreach (var run in runsInNewDocument) |
| { |
| if (runAncestorIntersection == null) |
| runAncestorIntersection = run.Ancestors().ToList(); |
| else |
| runAncestorIntersection = run.Ancestors().Intersect(runAncestorIntersection).ToList(); |
| } |
| if (runAncestorIntersection == null) |
| continue; |
| XElement commonAncestor = runAncestorIntersection.InDocumentOrder().Last(); |
| // find child of common ancestor that contains first run |
| // find child of common ancestor that contains last run |
| // create new common ancestor: |
| // elements before first run child |
| // add content control, and runs from first run child to last run child |
| // elements after last run child |
| var firstRunChild = commonAncestor |
| .Elements() |
| .First(c => c.DescendantsAndSelf() |
| .Any(z => z.Name == W.r && |
| z.Attribute(PT.UniqueId).Value == runsInNewDocument.First().Attribute(PT.UniqueId).Value)); |
| var lastRunChild = commonAncestor |
| .Elements() |
| .First(c => c.DescendantsAndSelf() |
| .Any(z => z.Name == W.r && |
| z.Attribute(PT.UniqueId).Value == runsInNewDocument.Last().Attribute(PT.UniqueId).Value)); |
| |
| /// If the list of runs for the content control is exactly the list of runs for the paragraph, then |
| /// create the content control surrounding the paragraph, not surrounding the runs. |
| |
| if (commonAncestor.Name == W.p && |
| commonAncestor.Elements() |
| .Where(e => e.Name != W.pPr && |
| e.Name != W.commentRangeStart && |
| e.Name != W.commentRangeEnd) |
| .FirstOrDefault() == firstRunChild && |
| commonAncestor.Elements() |
| .Where(e => e.Name != W.pPr && |
| e.Name != W.commentRangeStart && |
| e.Name != W.commentRangeEnd) |
| .LastOrDefault() == lastRunChild) |
| { |
| // replace commonAncestor with content control containing commonAncestor |
| XElement newContentControl = new XElement(contentControl.Name, |
| contentControl.Attributes(), |
| contentControl.Elements().Where(e => e.Name != W.sdtContent), |
| new XElement(W.sdtContent, commonAncestor)); |
| |
| XElement newContentControlOrdered = new XElement(contentControl.Name, |
| contentControl.Attributes(), |
| contentControl.Elements().OrderBy(e => |
| { |
| if (Order_sdt.ContainsKey(e.Name)) |
| return Order_sdt[e.Name]; |
| return 999; |
| })); |
| |
| commonAncestor.ReplaceWith(newContentControlOrdered); |
| continue; |
| } |
| |
| List<XElement> elementsBeforeRange = commonAncestor |
| .Elements() |
| .TakeWhile(e => e != firstRunChild) |
| .ToList(); |
| List<XElement> elementsInRange = commonAncestor |
| .Elements() |
| .SkipWhile(e => e != firstRunChild) |
| .TakeWhile(e => e != lastRunChild.ElementsAfterSelf().FirstOrDefault()) |
| .ToList(); |
| List<XElement> elementsAfterRange = commonAncestor |
| .Elements() |
| .SkipWhile(e => e != lastRunChild.ElementsAfterSelf().FirstOrDefault()) |
| .ToList(); |
| |
| // detatch from current parent |
| commonAncestor.Elements().Remove(); |
| |
| XElement newContentControl2 = new XElement(contentControl.Name, |
| contentControl.Attributes(), |
| contentControl.Elements().Where(e => e.Name != W.sdtContent), |
| new XElement(W.sdtContent, elementsInRange)); |
| |
| XElement newContentControlOrdered2 = new XElement(newContentControl2.Name, |
| newContentControl2.Attributes(), |
| newContentControl2.Elements().OrderBy(e => |
| { |
| if (Order_sdt.ContainsKey(e.Name)) |
| return Order_sdt[e.Name]; |
| return 999; |
| })); |
| |
| commonAncestor.Add( |
| elementsBeforeRange, |
| newContentControlOrdered2, |
| elementsAfterRange); |
| } |
| return newDocument; |
| } |
| |
| private static Dictionary<XName, int> Order_sdt = new Dictionary<XName, int> |
| { |
| { W.sdtPr, 10 }, |
| { W.sdtEndPr, 20 }, |
| { W.sdtContent, 30 }, |
| { W.bookmarkStart, 40 }, |
| { W.bookmarkEnd, 50 }, |
| }; |
| |
| private static XElement AcceptDeletedAndMoveFromParagraphMarks(XElement element) |
| { |
| AnnotateRunElementsWithId(element); |
| AnnotateContentControlsWithRunIds(element); |
| XElement newElement = (XElement)AcceptDeletedAndMoveFromParagraphMarksTransform(element); |
| XElement withBlockLevelContentControls = AddBlockLevelContentControls(newElement, element); |
| return withBlockLevelContentControls; |
| } |
| |
| enum GroupingType |
| { |
| DeletedRange, |
| Other, |
| }; |
| |
| class GroupingInfo |
| { |
| public GroupingType GroupingType; |
| public int GroupingKey; |
| }; |
| |
| private static object AcceptDeletedAndMoveFromParagraphMarksTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (W.BlockLevelContentContainers.Contains(element.Name)) |
| { |
| XElement bodySectPr = null; |
| if (element.Name == W.body) |
| bodySectPr = element.Element(W.sectPr); |
| |
| int currentKey = 0; |
| var deletedParagraphGroupingInfo = new List<GroupingInfo>(); |
| |
| int state = 0; // 0 = in non deleted paragraphs |
| // 1 = in deleted paragraph |
| // 2 - paragraph following deleted paragraphs |
| |
| foreach (var c in IterateBlockContentElements(element)) |
| { |
| if (c.ThisBlockContentElement.Name == W.p) |
| { |
| bool paragraphMarkIsDeletedOrMovedFrom = c |
| .ThisBlockContentElement |
| .Elements(W.pPr) |
| .Elements(W.rPr) |
| .Elements() |
| .Where(e => e.Name == W.del || e.Name == W.moveFrom) |
| .Any(); |
| |
| if (paragraphMarkIsDeletedOrMovedFrom) |
| { |
| if (state == 0) |
| { |
| state = 1; |
| currentKey += 1; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() { |
| GroupingType = GroupingType.DeletedRange, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| else if (state == 1) |
| { |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.DeletedRange, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| else if (state == 2) |
| { |
| state = 1; |
| currentKey += 1; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.DeletedRange, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| } |
| |
| if (state == 0) |
| { |
| currentKey += 1; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.Other, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| else if (state == 1) |
| { |
| state = 2; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.DeletedRange, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| else if (state == 2) |
| { |
| state = 0; |
| currentKey += 1; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.Other, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| } |
| else if (c.ThisBlockContentElement.Name == W.tbl || c.ThisBlockContentElement.Name.Namespace == M.m) |
| { |
| currentKey += 1; |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.Other, |
| GroupingKey = currentKey, |
| }); |
| state = 0; |
| continue; |
| } |
| else |
| { |
| // otherwise keep the same state, put in the same group, and continue |
| deletedParagraphGroupingInfo.Add( |
| new GroupingInfo() |
| { |
| GroupingType = GroupingType.Other, |
| GroupingKey = currentKey, |
| }); |
| continue; |
| } |
| } |
| |
| var zipped = IterateBlockContentElements(element).Zip(deletedParagraphGroupingInfo, (blc, gi) => new |
| { |
| BlockLevelContent = blc, |
| GroupingInfo = gi, |
| }); |
| |
| var groupedParagraphs = zipped |
| .GroupAdjacent(z => z.GroupingInfo.GroupingKey); |
| |
| // Create a new block level content container. |
| XElement newBlockLevelContentContainer = new XElement(element.Name, |
| element.Attributes(), |
| element.Elements().Where(e => e.Name == W.tcPr), |
| groupedParagraphs.Select((g, i) => |
| { |
| if (g.First().GroupingInfo.GroupingType == GroupingType.DeletedRange) |
| { |
| XElement newParagraph = new XElement(W.p, |
| #if false |
| // previously, this was set to g.First() |
| // however, this caused test [InlineData("RP/RP052-Deleted-Para-Mark.docx")] to lose paragraph numbering for a paragraph that we did not want to loose it for. |
| // the question is - when coalescing multiple paragraphs due to deleted paragraph marks, should we be taking the paragraph properties from the first or the last |
| // in the sequence of coalesced paragraph. It is possible that we should take Last when accepting revisions, but First when rejecting revisions. |
| g.First().BlockLevelContent.ThisBlockContentElement.Elements(W.pPr), |
| #endif |
| g.Last().BlockLevelContent.ThisBlockContentElement.Elements(W.pPr), |
| g.Select(z => CollapseParagraphTransform(z.BlockLevelContent.ThisBlockContentElement))); |
| |
| // if this contains the last paragraph in the document, and if there is no content, |
| // and if the paragraph mark is deleted, then nuke the paragraph. |
| var allIsDeleted = AllParaContentIsDeleted(newParagraph); |
| if (allIsDeleted && |
| g.Last().BlockLevelContent.ThisBlockContentElement.Elements(W.pPr).Elements(W.rPr).Elements(W.del).Any() && |
| (g.Last().BlockLevelContent.NextBlockContentElement == null || |
| g.Last().BlockLevelContent.NextBlockContentElement.Name == W.tbl)) |
| return null; |
| |
| return (object)newParagraph; |
| } |
| else |
| { |
| return g.Select(z => |
| { |
| var newEle = new XElement(z.BlockLevelContent.ThisBlockContentElement.Name, |
| z.BlockLevelContent.ThisBlockContentElement.Attributes(), |
| z.BlockLevelContent.ThisBlockContentElement.Nodes().Select(n => AcceptDeletedAndMoveFromParagraphMarksTransform(n))); |
| return newEle; |
| }); |
| } |
| }), |
| bodySectPr); |
| |
| return newBlockLevelContentContainer; |
| } |
| |
| // Otherwise, identity clone. |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptDeletedAndMoveFromParagraphMarksTransform(n))); |
| } |
| return node; |
| } |
| |
| // Determine if the paragraph contains any content that is not deleted. |
| private static bool AllParaContentIsDeleted(XElement p) |
| { |
| // needs collapse |
| // dir, bdo, sdt, ins, moveTo, smartTag |
| var testP = (XElement)CollapseTransform(p); |
| |
| var childElements = testP.Elements(); |
| var contentElements = childElements |
| .Where(ce => |
| { |
| var b = IsRunContent(ce.Name); |
| if (b != null) |
| return (bool)b; |
| throw new Exception("Internal error 20, found element " + ce.Name.ToString()); |
| }); |
| if (contentElements.Any()) |
| return false; |
| return true; |
| } |
| |
| // dir, bdo, sdt, ins, moveTo, smartTag |
| private static object CollapseTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.dir || |
| element.Name == W.bdr || |
| element.Name == W.ins || |
| element.Name == W.moveTo || |
| element.Name == W.smartTag) |
| return element.Elements(); |
| |
| if (element.Name == W.sdt) |
| return element.Elements(W.sdtContent).Elements(); |
| |
| if (element.Name == W.pPr) |
| return null; |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => CollapseTransform(n))); |
| } |
| return node; |
| } |
| |
| private static bool? IsRunContent(XName ceName) |
| { |
| // is content |
| // r, fldSimple, hyperlink, oMath, oMathPara, subDoc |
| if (ceName == W.r || |
| ceName == W.fldSimple || |
| ceName == W.hyperlink || |
| ceName == W.subDoc || |
| ceName == W.smartTag || |
| ceName == W.smartTagPr || |
| ceName.Namespace == M.m) |
| return true; |
| |
| // not content |
| // bookmarkStart, bookmarkEnd, commentRangeStart, commentRangeEnd, del, moveFrom, proofErr |
| if (ceName == W.bookmarkStart || |
| ceName == W.bookmarkEnd || |
| ceName == W.commentRangeStart || |
| ceName == W.commentRangeEnd || |
| ceName == W.customXmlDelRangeStart || |
| ceName == W.customXmlDelRangeEnd || |
| ceName == W.customXmlInsRangeStart || |
| ceName == W.customXmlInsRangeEnd || |
| ceName == W.customXmlMoveFromRangeStart || |
| ceName == W.customXmlMoveFromRangeEnd || |
| ceName == W.customXmlMoveToRangeStart || |
| ceName == W.customXmlMoveToRangeEnd || |
| ceName == W.del || |
| ceName == W.moveFrom || |
| ceName == W.moveFromRangeStart || |
| ceName == W.moveFromRangeEnd || |
| ceName == W.moveToRangeStart || |
| ceName == W.moveToRangeEnd || |
| ceName == W.permStart || |
| ceName == W.permEnd || |
| ceName == W.proofErr) |
| return false; |
| |
| return null; |
| } |
| |
| private static IEnumerable<Tag> DescendantAndSelfTags(XElement element) |
| { |
| yield return new Tag |
| { |
| Element = element, |
| TagType = TagTypeEnum.Element |
| }; |
| Stack<IEnumerator<XElement>> iteratorStack = new Stack<IEnumerator<XElement>>(); |
| iteratorStack.Push(element.Elements().GetEnumerator()); |
| while (iteratorStack.Count > 0) |
| { |
| if (iteratorStack.Peek().MoveNext()) |
| { |
| XElement currentXElement = iteratorStack.Peek().Current; |
| if (!currentXElement.Nodes().Any()) |
| { |
| yield return new Tag() |
| { |
| Element = currentXElement, |
| TagType = TagTypeEnum.EmptyElement |
| }; |
| continue; |
| } |
| yield return new Tag() |
| { |
| Element = currentXElement, |
| TagType = TagTypeEnum.Element |
| }; |
| iteratorStack.Push(currentXElement.Elements().GetEnumerator()); |
| continue; |
| } |
| iteratorStack.Pop(); |
| if (iteratorStack.Count > 0) |
| yield return new Tag() |
| { |
| Element = iteratorStack.Peek().Current, |
| TagType = TagTypeEnum.EndElement |
| }; |
| } |
| yield return new Tag |
| { |
| Element = element, |
| TagType = TagTypeEnum.EndElement |
| }; |
| } |
| |
| private class PotentialInRangeElements |
| { |
| public List<XElement> PotentialStartElementTagsInRange; |
| public List<XElement> PotentialEndElementTagsInRange; |
| |
| public PotentialInRangeElements() |
| { |
| PotentialStartElementTagsInRange = new List<XElement>(); |
| PotentialEndElementTagsInRange = new List<XElement>(); |
| } |
| } |
| |
| private enum TagTypeEnum |
| { |
| Element, |
| EndElement, |
| EmptyElement |
| } |
| |
| private class Tag |
| { |
| public XElement Element; |
| public TagTypeEnum TagType; |
| } |
| |
| private static object AcceptDeletedAndMovedFromContentControlsTransform(XNode node, |
| XElement[] contentControlElementsToCollapse, |
| XElement[] moveFromElementsToDelete) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.sdt && contentControlElementsToCollapse.Contains(element)) |
| return element |
| .Element(W.sdtContent) |
| .Nodes() |
| .Select(n => AcceptDeletedAndMovedFromContentControlsTransform( |
| n, contentControlElementsToCollapse, moveFromElementsToDelete)); |
| if (moveFromElementsToDelete.Contains(element)) |
| return null; |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptDeletedAndMovedFromContentControlsTransform( |
| n, contentControlElementsToCollapse, moveFromElementsToDelete))); |
| } |
| return node; |
| } |
| |
| private static XElement AcceptDeletedAndMovedFromContentControls(XElement documentRootElement) |
| { |
| string wordProcessingNamespacePrefix = documentRootElement.GetPrefixOfNamespace(W.w); |
| |
| // The following lists contain the elements that are between start/end elements. |
| List<XElement> startElementTagsInDeleteRange = new List<XElement>(); |
| List<XElement> endElementTagsInDeleteRange = new List<XElement>(); |
| List<XElement> startElementTagsInMoveFromRange = new List<XElement>(); |
| List<XElement> endElementTagsInMoveFromRange = new List<XElement>(); |
| |
| // Following are the elements that *may* be in a range that has both start and end |
| // elements. |
| Dictionary<string, PotentialInRangeElements> potentialDeletedElements = |
| new Dictionary<string, PotentialInRangeElements>(); |
| Dictionary<string, PotentialInRangeElements> potentialMoveFromElements = |
| new Dictionary<string, PotentialInRangeElements>(); |
| |
| foreach (var tag in DescendantAndSelfTags(documentRootElement)) |
| { |
| if (tag.Element.Name == W.customXmlDelRangeStart) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| potentialDeletedElements.Add(id, new PotentialInRangeElements()); |
| continue; |
| } |
| if (tag.Element.Name == W.customXmlDelRangeEnd) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| if (potentialDeletedElements.ContainsKey(id)) |
| { |
| startElementTagsInDeleteRange.AddRange( |
| potentialDeletedElements[id].PotentialStartElementTagsInRange); |
| endElementTagsInDeleteRange.AddRange( |
| potentialDeletedElements[id].PotentialEndElementTagsInRange); |
| potentialDeletedElements.Remove(id); |
| } |
| continue; |
| } |
| if (tag.Element.Name == W.customXmlMoveFromRangeStart) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| potentialMoveFromElements.Add(id, new PotentialInRangeElements()); |
| continue; |
| } |
| if (tag.Element.Name == W.customXmlMoveFromRangeEnd) |
| { |
| string id = tag.Element.Attribute(W.id).Value; |
| if (potentialMoveFromElements.ContainsKey(id)) |
| { |
| startElementTagsInMoveFromRange.AddRange( |
| potentialMoveFromElements[id].PotentialStartElementTagsInRange); |
| endElementTagsInMoveFromRange.AddRange( |
| potentialMoveFromElements[id].PotentialEndElementTagsInRange); |
| potentialMoveFromElements.Remove(id); |
| } |
| continue; |
| } |
| if (tag.Element.Name == W.sdt) |
| { |
| if (tag.TagType == TagTypeEnum.Element) |
| { |
| foreach (var id in potentialDeletedElements) |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| foreach (var id in potentialMoveFromElements) |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EmptyElement) |
| { |
| foreach (var id in potentialDeletedElements) |
| { |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| } |
| foreach (var id in potentialMoveFromElements) |
| { |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| } |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EndElement) |
| { |
| foreach (var id in potentialDeletedElements) |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| foreach (var id in potentialMoveFromElements) |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| throw new PowerToolsInvalidDataException("Should not have reached this point."); |
| } |
| if (potentialMoveFromElements.Count() > 0 && |
| tag.Element.Name != W.moveFromRangeStart && |
| tag.Element.Name != W.moveFromRangeEnd && |
| tag.Element.Name != W.customXmlMoveFromRangeStart && |
| tag.Element.Name != W.customXmlMoveFromRangeEnd) |
| { |
| if (tag.TagType == TagTypeEnum.Element) |
| { |
| foreach (var id in potentialMoveFromElements) |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EmptyElement) |
| { |
| foreach (var id in potentialMoveFromElements) |
| { |
| id.Value.PotentialStartElementTagsInRange.Add(tag.Element); |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| } |
| continue; |
| } |
| if (tag.TagType == TagTypeEnum.EndElement) |
| { |
| foreach (var id in potentialMoveFromElements) |
| id.Value.PotentialEndElementTagsInRange.Add(tag.Element); |
| continue; |
| } |
| } |
| } |
| |
| var contentControlElementsToCollapse = startElementTagsInDeleteRange |
| .Intersect(endElementTagsInDeleteRange) |
| .ToArray(); |
| var elementsToDeleteBecauseMovedFrom = startElementTagsInMoveFromRange |
| .Intersect(endElementTagsInMoveFromRange) |
| .ToArray(); |
| if (contentControlElementsToCollapse.Length > 0 || |
| elementsToDeleteBecauseMovedFrom.Length > 0) |
| { |
| var newDoc = AcceptDeletedAndMovedFromContentControlsTransform(documentRootElement, |
| contentControlElementsToCollapse, elementsToDeleteBecauseMovedFrom); |
| return newDoc as XElement; |
| } |
| else |
| return documentRootElement; |
| } |
| |
| private static object AcceptMoveFromRangesTransform(XNode node, |
| XElement[] elementsToDelete) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (elementsToDelete.Contains(element)) |
| return null; |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => |
| AcceptMoveFromRangesTransform(n, elementsToDelete))); |
| } |
| return node; |
| } |
| |
| private static object CoalesqueParagraphEndTagsInMoveFromTransform(XNode node, |
| IGrouping<MoveFromCollectionType, XElement> g) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.p) |
| return new XElement(W.p, |
| element.Attributes(), |
| element.Elements(), |
| g.Skip(1).Select(p => CollapseParagraphTransform(p))); |
| else |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => |
| CoalesqueParagraphEndTagsInMoveFromTransform(n, g))); |
| } |
| return node; |
| } |
| |
| private enum DeletedCellCollectionType |
| { |
| DeletedCell, |
| Other |
| }; |
| |
| // For each table row, group deleted cells plus the cell before any deleted cell. |
| // Produce a new cell that has gridSpan set appropriately for group, and clone everything |
| // else. |
| private static object AcceptDeletedCellsTransform(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.tr) |
| { |
| var groupedCells = element |
| .Elements() |
| .GroupAdjacent(e => |
| { |
| XElement cellAfter = e.ElementsAfterSelf(W.tc).FirstOrDefault(); |
| bool cellAfterIsDeleted = cellAfter != null && |
| cellAfter.Descendants(W.cellDel).Any(); |
| if (e.Name == W.tc && |
| (cellAfterIsDeleted || e.Descendants(W.cellDel).Any())) |
| { |
| var a = new |
| { |
| CollectionType = DeletedCellCollectionType.DeletedCell, |
| Disambiguator = new[] { e } |
| .Concat(e.SiblingsBeforeSelfReverseDocumentOrder()) |
| .Where(z => z.Name == W.tc && |
| !z.Descendants(W.cellDel).Any()) |
| .FirstOrDefault() |
| }; |
| return a; |
| } |
| var a2 = new |
| { |
| CollectionType = DeletedCellCollectionType.Other, |
| Disambiguator = e |
| }; |
| return a2; |
| }); |
| var tr = new XElement(W.tr, |
| element.Attributes(), |
| groupedCells.Select(g => |
| { |
| if (g.Key.CollectionType == DeletedCellCollectionType.DeletedCell |
| && g.First().Descendants(W.cellDel).Any()) |
| return null; |
| if (g.Key.CollectionType == DeletedCellCollectionType.Other) |
| return (object)g; |
| XElement gridSpanElement = g |
| .First() |
| .Elements(W.tcPr) |
| .Elements(W.gridSpan) |
| .FirstOrDefault(); |
| int gridSpan = gridSpanElement != null ? |
| (int)gridSpanElement.Attribute(W.val) : |
| 1; |
| int newGridSpan = gridSpan + g.Count() - 1; |
| XElement currentTcPr = g.First().Elements(W.tcPr).FirstOrDefault(); |
| XElement newTcPr = new XElement(W.tcPr, |
| currentTcPr != null ? currentTcPr.Attributes() : null, |
| new XElement(W.gridSpan, |
| new XAttribute(W.val, newGridSpan)), |
| currentTcPr.Elements().Where(e => e.Name != W.gridSpan)); |
| var orderedTcPr = new XElement(W.tcPr, |
| newTcPr.Elements().OrderBy(e => |
| { |
| if (Order_tcPr.ContainsKey(e.Name)) |
| return Order_tcPr[e.Name]; |
| return 999; |
| })); |
| XElement newTc = new XElement(W.tc, |
| orderedTcPr, |
| g.First().Elements().Where(e => e.Name != W.tcPr)); |
| return (object)newTc; |
| })); |
| return tr; |
| } |
| |
| // Identity clone |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => AcceptDeletedCellsTransform(n))); |
| } |
| return node; |
| } |
| |
| #if false |
| <w:tr> |
| <w:tc> |
| <w:tcPr> |
| <w:tcW w:w="5016" |
| w:type="dxa" /> |
| </w:tcPr> |
| </w:tc> |
| </w:tr> |
| #endif |
| private static XName[] BlockLevelElements = new[] { |
| W.p, |
| W.tbl, |
| W.sdt, |
| W.del, |
| W.ins, |
| M.oMath, |
| M.oMathPara, |
| W.moveTo, |
| }; |
| |
| private static object RemoveRowsLeftEmptyByMoveFrom(XNode node) |
| { |
| XElement element = node as XElement; |
| if (element != null) |
| { |
| if (element.Name == W.tr) |
| { |
| var nonEmptyCells = element.Elements(W.tc).Any(tc => tc.Elements().Any(tcc => BlockLevelElements.Contains(tcc.Name))); |
| if (nonEmptyCells) |
| { |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => RemoveRowsLeftEmptyByMoveFrom(n))); |
| } |
| return null; |
| } |
| |
| return new XElement(element.Name, |
| element.Attributes(), |
| element.Nodes().Select(n => RemoveRowsLeftEmptyByMoveFrom(n))); |
| } |
| return node; |
| } |
| |
| public static XName[] TrackedRevisionsElements = new[] |
| { |
| W.cellDel, |
| W.cellIns, |
| W.cellMerge, |
| W.customXmlDelRangeEnd, |
| W.customXmlDelRangeStart, |
| W.customXmlInsRangeEnd, |
| W.customXmlInsRangeStart, |
| W.del, |
| W.delInstrText, |
| W.delText, |
| W.ins, |
| W.moveFrom, |
| W.moveFromRangeEnd, |
| W.moveFromRangeStart, |
| W.moveTo, |
| W.moveToRangeEnd, |
| W.moveToRangeStart, |
| W.numberingChange, |
| W.pPrChange, |
| W.rPrChange, |
| W.sectPrChange, |
| W.tblGridChange, |
| W.tblPrChange, |
| W.tblPrExChange, |
| W.tcPrChange, |
| W.trPrChange, |
| }; |
| |
| public static bool PartHasTrackedRevisions(OpenXmlPart part) |
| { |
| return part.GetXDocument() |
| .Descendants() |
| .Any(e => TrackedRevisionsElements.Contains(e.Name)); |
| } |
| |
| public static bool HasTrackedRevisions(WmlDocument document) |
| { |
| using (OpenXmlMemoryStreamDocument streamDoc = new OpenXmlMemoryStreamDocument(document)) |
| { |
| using (WordprocessingDocument wdoc = streamDoc.GetWordprocessingDocument()) |
| { |
| return RevisionAccepter.HasTrackedRevisions(wdoc); |
| } |
| } |
| } |
| |
| public static bool HasTrackedRevisions(WordprocessingDocument doc) |
| { |
| if (PartHasTrackedRevisions(doc.MainDocumentPart)) |
| return true; |
| foreach (var part in doc.MainDocumentPart.HeaderParts) |
| if (PartHasTrackedRevisions(part)) |
| return true; |
| foreach (var part in doc.MainDocumentPart.FooterParts) |
| if (PartHasTrackedRevisions(part)) |
| return true; |
| if (doc.MainDocumentPart.EndnotesPart != null) |
| if (PartHasTrackedRevisions(doc.MainDocumentPart.EndnotesPart)) |
| return true; |
| if (doc.MainDocumentPart.FootnotesPart != null) |
| if (PartHasTrackedRevisions(doc.MainDocumentPart.FootnotesPart)) |
| return true; |
| return false; |
| } |
| } |
| |
| public partial class WmlDocument : OpenXmlPowerToolsDocument |
| { |
| public WmlDocument AcceptRevisions(WmlDocument document) |
| { |
| return RevisionAccepter.AcceptRevisions(document); |
| } |
| public bool HasTrackedRevisions(WmlDocument document) |
| { |
| return RevisionAccepter.HasTrackedRevisions(document); |
| } |
| } |
| |
| public class BlockContentInfo |
| { |
| public XElement PreviousBlockContentElement; |
| public XElement ThisBlockContentElement; |
| public XElement NextBlockContentElement; |
| } |
| |
| public static class RevisionAccepterExtensions |
| { |
| private static void InitializeParagraphInfo(XElement contentContext) |
| { |
| if (!(W.BlockLevelContentContainers.Contains(contentContext.Name))) |
| throw new ArgumentException( |
| "GetParagraphInfo called for element that is not child of content container"); |
| XElement prev = null; |
| foreach (var content in contentContext.Elements()) |
| { |
| // This may return null, indicating that there is no descendant paragraph. For |
| // example, comment elements have no descendant elements. |
| XElement paragraph = content |
| .DescendantsAndSelf() |
| .Where(e => e.Name == W.p || e.Name == W.tc || e.Name == W.txbxContent) |
| .FirstOrDefault(); |
| if (paragraph != null && |
| (paragraph.Name == W.tc || paragraph.Name == W.txbxContent)) |
| paragraph = null; |
| BlockContentInfo pi = new BlockContentInfo() |
| { |
| PreviousBlockContentElement = prev, |
| ThisBlockContentElement = paragraph |
| }; |
| content.AddAnnotation(pi); |
| prev = content; |
| } |
| } |
| |
| public static BlockContentInfo GetParagraphInfo(this XElement contentElement) |
| { |
| BlockContentInfo paragraphInfo = contentElement.Annotation<BlockContentInfo>(); |
| if (paragraphInfo != null) |
| return paragraphInfo; |
| InitializeParagraphInfo(contentElement.Parent); |
| return contentElement.Annotation<BlockContentInfo>(); |
| } |
| |
| public static IEnumerable<XElement> ContentElementsBeforeSelf(this XElement element) |
| { |
| XElement current = element; |
| while (true) |
| { |
| BlockContentInfo pi = current.GetParagraphInfo(); |
| if (pi.PreviousBlockContentElement == null) |
| yield break; |
| yield return pi.PreviousBlockContentElement; |
| current = pi.PreviousBlockContentElement; |
| } |
| } |
| } |
| } |
| |
| /// Markup that this code processes: |
| /// |
| /// delText |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to w:t element |
| /// |
| /// del (deleted run content) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements and descendant elements. |
| /// Reject: |
| /// Transform to w:ins element |
| /// Then Accept |
| /// |
| /// ins (inserted run content) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: InsertedParagraphsAndRuns.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Collapse these elements. |
| /// Reject: |
| /// Transform to w:del element, and child w:t transform to w:delText element |
| /// Then Accept |
| /// |
| /// ins (inserted paragraph) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: InsertedParagraphsAndRuns.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to w:del element |
| /// Then Accept |
| /// |
| /// del (deleted paragraph mark) |
| /// Method: AcceptDeletedAndMoveFromParagraphMarksTransform |
| /// Sample document: VariousTableRevisions.docx (deleted paragraph mark in paragraph in |
| /// content control) |
| /// Reviewed: tristan and zeyad **************************************** |
| /// Semantics: |
| /// Find all adjacent paragraps that have this element. |
| /// Group adjacent paragraphs plus the paragraph following paragraph that has this element. |
| /// Replace grouped paragraphs with a new paragraph containing the content from all grouped |
| /// paragraphs. Use the paragraph properties from the first paragraph in the group. |
| /// Reject: |
| /// Transform to w:ins element |
| /// Then Accept |
| /// |
| /// del (deleted table row) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Match w:tr/w:trPr/w:del, remove w:tr. |
| /// Reject: |
| /// Transform to w:ins |
| /// Then Accept |
| /// |
| /// ins (inserted table row) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to w:del |
| /// Then Accept |
| /// |
| /// del (deleted math control character) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: DeletedMathControlCharacter.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Match m:f/m:fPr/m:ctrlPr/w:del, remove m:f. |
| /// Reject: |
| /// Transform to w:ins |
| /// Then Accept |
| /// |
| /// ins (inserted math control character) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: InsertedMathControlCharacter.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to w:del |
| /// Then Accept |
| /// |
| /// moveTo (move destination paragraph mark) |
| /// Method: AcceptMoveFromMoveToTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to moveFrom |
| /// Then Accept |
| /// |
| /// moveTo (move destination run content) |
| /// Method: AcceptMoveFromMoveToTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Collapse these elements. |
| /// Reject: |
| /// Transform to moveFrom |
| /// Then Accept |
| /// |
| /// moveFrom (move source paragraph mark) |
| /// Methods: AcceptDeletedAndMoveFromParagraphMarksTransform, AcceptParagraphEndTagsInMoveFromTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: tristan and zeyad **************************************** |
| /// Semantics: |
| /// Find all adjacent paragraps that have this element or deleted paragraph mark. |
| /// Group adjacent paragraphs plus the paragraph following paragraph that has this element. |
| /// Replace grouped paragraphs with a new paragraph containing the content from all grouped |
| /// paragraphs. |
| /// This is handled in the same code that handles del (deleted paragraph mark). |
| /// Reject: |
| /// Transform to moveTo |
| /// Then Accept |
| /// |
| /// moveFrom (move source run content) |
| /// Method: AcceptMoveFromMoveToTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to moveTo |
| /// Then Accept |
| /// |
| /// moveFromRangeStart |
| /// moveFromRangeEnd |
| /// Method: AcceptMoveFromRanges |
| /// Sample document: MovedText.docx |
| /// Semantics: |
| /// Find pairs of elements. Remove all elements that have both start and end tags in a |
| /// range. |
| /// Reject: |
| /// Transform to moveToRangeStart, moveToRangeEnd |
| /// Then Accept |
| /// |
| /// moveToRangeStart |
| /// moveToRangeEnd |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: MovedText.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to moveFromRangeStart, moveFromRangeEnd |
| /// Then Accept |
| /// |
| /// customXmlDelRangeStart |
| /// customXmlDelRangeEnd |
| /// customXmlMoveFromRangeStart |
| /// customXmlMoveFromRangeEnd |
| /// Method: AcceptDeletedAndMovedFromContentControls |
| /// Reviewed: tristan and zeyad **************************************** |
| /// Semantics: |
| /// Find pairs of start/end elements, matching id attributes. Collapse sdt |
| /// elements that have both start and end tags in a range. |
| /// Reject: |
| /// Transform to customXmlInsRangeStart, customXmlInsRangeEnd, customXmlMoveToRangeStart, customXmlMoveToRangeEnd |
| /// Then Accept |
| /// |
| /// customXmlInsRangeStart |
| /// customXmlInsRangeEnd |
| /// customXmlMoveToRangeStart |
| /// customXmlMoveToRangeEnd |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Reviewed: tristan and zeyad **************************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to customXmlDelRangeStart, customXmlDelRangeEnd, customXmlMoveFromRangeStart, customXmlMoveFromRangeEnd |
| /// Then Accept |
| /// |
| /// delInstrText (deleted field code) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: NumberingParagraphPropertiesChange.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Transform to instrText |
| /// Then Accept |
| /// Note that instrText must be transformed to delInstrText when in a w:ins, in the same fashion that w:t must be transformed to w:delText when in w:ins |
| /// |
| /// ins (inserted numbering properties) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: InsertedNumberingProperties.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject |
| /// Remove the containing w:numPr |
| /// |
| /// pPrChange (revision information for paragraph properties) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: ParagraphAndRunPropertyRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace pPr with the pPr in pPrChange |
| /// |
| /// rPrChange (revision information for run properties) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: ParagraphAndRunPropertyRevisions.docx |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace rPr with the rPr in rPrChange |
| /// |
| /// rPrChange (revision information for run properties on the paragraph mark) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: ParagraphAndRunPropertyRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace rPr with the rPr in rPrChange. |
| /// |
| /// numberingChange (previous numbering field properties) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: NumberingFieldPropertiesChange.docx |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Remove these elements. |
| /// These are there for numbering created via fields, and are not important. |
| /// |
| /// numberingChange (previous paragraph numbering properties) |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: NumberingFieldPropertiesChange.docx |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Remove these elements. |
| /// |
| /// sectPrChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: SectionPropertiesChange.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace sectPr with the sectPr in sectPrChange |
| /// |
| /// tblGridChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: TableGridChange.docx |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace tblGrid with the tblGrid in tblGridChange |
| /// |
| /// tblPrChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: TableGridChange.docx |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace tblPr with the tblPr in tblPrChange |
| /// |
| /// tblPrExChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace tblPrEx with the tblPrEx in tblPrExChange |
| /// |
| /// tcPrChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: TableGridChange.docx |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace tcPr with the tcPr in tcPrChange |
| /// |
| /// trPrChange |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: VariousTableRevisions.docx |
| /// Reviewed: zeyad *************************** |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// Replace trPr with the trPr in trPrChange |
| /// |
| /// celDel |
| /// Method: AcceptDeletedCellsTransform |
| /// Sample document: HorizontallyMergedCells.docx |
| /// Semantics: |
| /// Group consecutive deleted cells, and remove them. |
| /// Adjust the cell before deleted cells: |
| /// Increase gridSpan by the number of deleted cells that are removed. |
| /// Reject: |
| /// Remove this element |
| /// |
| /// celIns |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: HorizontallyMergedCells11.docx |
| /// Semantics: |
| /// Remove these elements. |
| /// Reject: |
| /// If a w:tc contains w:tcPr/w:cellIns, then remove the cell |
| /// |
| /// cellMerge |
| /// Method: AcceptAllOtherRevisionsTransform |
| /// Sample document: MergedCell.docx |
| /// Semantics: |
| /// Transform cellMerge with a parent of tcPr, with attribute w:vMerge="rest" |
| /// to <w:vMerge w:val="restart"/>. |
| /// Transform cellMerge with a parent of tcPr, with attribute w:vMerge="cont" |
| /// to <w:vMerge w:val="continue"/> |
| /// |
| /// The following items need to be addressed in a future release: |
| /// - inserted run inside deleted paragraph - moveTo is same as insert |
| /// - must increase w:val attribute of the w:gridSpan element of the |
| /// cell immediately preceding the group of deleted cells by the |
| /// ***sum*** of the values of the w:val attributes of w:gridSpan |
| /// elements of each of the deleted cells. |
| |