blob: 741263c1416ca0d10bd5ea8c64514adf0ef30179 [file] [log] [blame]
/***************************************************************************
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.