Files
csharpcode/omegapro/PowerTools/Classes/RevisionAccepter.cs
2025-08-02 05:20:17 +07:00

1434 lines
65 KiB
C#

/***************************************************************************
Copyright (c) Microsoft Corporation 2011.
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
***************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using DocumentFormat.OpenXml.Packaging;
namespace OpenXmlPowerTools
{
public partial class WmlDocument : OpenXmlPowerToolsDocument
{
public WmlDocument AcceptRevisions(WmlDocument document)
{
return RevisionAccepter.AcceptRevisions(document);
}
public bool HasTrackedRevisions(WmlDocument document)
{
return RevisionAccepter.HasTrackedRevisions(document);
}
}
/// Markup that this code processes:
///
/// 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.
///
/// celIns
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: HorizontallyMergedCells11.docx
/// Semantics:
/// Remove these elements.
///
/// 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"/>
///
/// 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.
///
/// customXmlInsRangeStart
/// customXmlInsRangeEnd
/// customXmlMoveToRangeStart
/// customXmlMoveToRangeEnd
/// Method: AcceptAllOtherRevisionsTransform
/// Reviewed: tristan and zeyad ****************************************
/// Semantics:
/// Remove these elements.
///
/// 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.
///
/// del (deleted run content)
/// Method: AcceptAllOtherRevisionsTransform
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements and descendant elements.
///
/// 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.
///
/// del (deleted table row)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Match w:tr/w:trPr/w:del, remove w:tr.
///
/// delText
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: MovedText.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// delInstrText (deleted field code)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: NumberingParagraphPropertiesChange.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// ins (inserted math control character)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: InsertedMathControlCharacter.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// ins (inserted numbering properties)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: InsertedNumberingProperties.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// ins (inserted paragraph)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: InsertedParagraphsAndRuns.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// ins (inserted run content)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: InsertedParagraphsAndRuns.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Collapse these elements.
///
/// ins (inserted table row)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// moveTo (move destination paragraph mark)
/// Method: AcceptMoveFromMoveToTransform
/// Sample document: MovedText.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// moveTo (move destination run content)
/// Method: AcceptMoveFromMoveToTransform
/// Sample document: MovedText.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Collapse these elements.
///
/// 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).
///
/// moveFrom (move source run content)
/// Method: AcceptMoveFromMoveToTransform
/// Sample document: MovedText.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// 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.
///
/// moveToRangeStart
/// moveToRangeEnd
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: MovedText.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// numberingChange (previous numbering field properties)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: NumberingFieldPropertiesChange.docx
/// Semantics:
/// Remove these elements.
///
/// numberingChange (previous paragraph numbering properties)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: NumberingFieldPropertiesChange.docx
/// Semantics:
/// Remove these elements.
///
/// pPrChange (revision information for paragraph properties)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: ParagraphAndRunPropertyRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// rPrChange (revision information for run properties)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: ParagraphAndRunPropertyRevisions.docx
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// rPrChange (revision information for run properties on the paragraph mark)
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: ParagraphAndRunPropertyRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// sectPrChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: SectionPropertiesChange.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// tblGridChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: TableGridChange.docx
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// tblPrChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: TableGridChange.docx
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// tblPrExChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// tcPrChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: TableGridChange.docx
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// trPrChange
/// Method: AcceptAllOtherRevisionsTransform
/// Sample document: VariousTableRevisions.docx
/// Reviewed: zeyad ***************************
/// Semantics:
/// Remove these elements.
///
/// 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.
public class BlockContentInfo
{
public XElement PreviousBlockContentElement;
public XElement ThisBlockContentElement;
public XElement NextBlockContentElement;
}
public static class LocalExtensions
{
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;
}
}
}
public class RevisionAccepter
{
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 &&
element.Attribute(W.vMerge).Value == "rest")
return new XElement(W.vMerge,
new XAttribute(W.val, "restart"));
if (element.Name == W.cellMerge &&
element.Parent.Name == W.tcPr &&
element.Attribute(W.vMerge).Value == "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
};
private static XElement CoalesqueParagraphDeletedAndMoveFromParagraphMarksTransform(
IGrouping<DeletedParagraphCollectionType, BlockContentInfo> g,
IGrouping<DeletedParagraphCollectionType, BlockContentInfo> nextGroup)
{
// This function constructs a paragraph.
XElement newParagraph = new XElement(W.p,
nextGroup.First().ThisBlockContentElement.Elements(W.pPr),
g.Select(z => CollapseParagraphTransform(z.ThisBlockContentElement)),
nextGroup.Select(z => CollapseParagraphTransform(z.ThisBlockContentElement)));
return newParagraph;
}
private static XElement AssembleWithBlockLevelContentControlTransform(XElement element)
{
return new XElement(element.Name,
element.Attributes(),
element.Elements().Select(e => AcceptDeletedAndMoveFromParagraphMarksTransform(e)));
}
/// 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);
// 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>();
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++));
}
}
// TODO - In the future, we need to trim so that Descendants doesn't see runs under text boxes.
private static void AnnotateContentControlsWithRunIds(XElement element)
{
int sdtId = 0;
foreach (XElement e in element.Descendants(W.sdt))
{
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++));
}
}
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 because 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();
}
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));
commonAncestor.ReplaceWith(newContentControl);
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();
commonAncestor.Add(
elementsBeforeRange,
new XElement(contentControl.Name,
contentControl.Attributes(),
contentControl.Elements().Where(e => e.Name != W.sdtContent),
new XElement(W.sdtContent, elementsInRange)),
elementsAfterRange);
}
return newDocument;
}
private static XElement AcceptDeletedAndMoveFromParagraphMarks(XElement element)
{
AnnotateRunElementsWithId(element);
AnnotateContentControlsWithRunIds(element);
XElement newElement = (XElement)AcceptDeletedAndMoveFromParagraphMarksTransform(element);
XElement withBlockLevelContentControls = AddBlockLevelContentControls(newElement, element);
return withBlockLevelContentControls;
}
private static object AcceptDeletedAndMoveFromParagraphMarksTransform(XNode node)
{
XElement element = node as XElement;
if (element != null)
{
if (W.BlockLevelContentContainers.Contains(element.Name))
{
var groupedParagraphSiblings = IterateBlockContentElements(element)
.GroupAdjacent(c =>
{
bool paragraphMarkIsDeletedOrMovedFrom = c
.ThisBlockContentElement
.Elements(W.pPr)
.Elements(W.rPr)
.Elements()
.Where(e => e.Name == W.del || e.Name == W.moveFrom)
.Any();
if (paragraphMarkIsDeletedOrMovedFrom)
return DeletedParagraphCollectionType.DeletedParagraphMarkContent;
if (c.PreviousBlockContentElement != null)
{
paragraphMarkIsDeletedOrMovedFrom = c
.PreviousBlockContentElement
.Elements(W.pPr)
.Elements(W.rPr)
.Elements()
.Where(e => e.Name == W.del || e.Name == W.moveFrom)
.Any();
if (c.ThisBlockContentElement.Name == W.p && paragraphMarkIsDeletedOrMovedFrom)
return DeletedParagraphCollectionType.ParagraphFollowing;
}
return DeletedParagraphCollectionType.Other;
})
.ToList();
// Create a new block level content container.
XElement newParagraphParentElement = new XElement(element.Name,
element.Attributes(),
element.Elements().Where(e => e.Name == W.tcPr),
groupedParagraphSiblings.Select((g, i) =>
{
// THIS IS THE OLD CODE
//if (g.Key == DeletedParagraphCollectionType.Other)
// return g.Select(n =>
// AcceptDeletedAndMoveFromParagraphMarksTransform(n.ThisBlockContentElement));
if (g.Key == DeletedParagraphCollectionType.Other)
return g.Select(n =>
AssembleWithBlockLevelContentControlTransform(n.ThisBlockContentElement));
// This is a transform that produces the first element in the
// collection; 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.
if (g.Key ==
DeletedParagraphCollectionType.DeletedParagraphMarkContent)
{
if (i < groupedParagraphSiblings.Count() - 1)
{
var nextG = groupedParagraphSiblings.ElementAt(i + 1);
if (nextG.Key ==
DeletedParagraphCollectionType.ParagraphFollowing)
{
XElement newParagraph = (XElement)
CoalesqueParagraphDeletedAndMoveFromParagraphMarksTransform(
g, nextG);
return (object)newParagraph;
}
}
return g.Select(n =>
AcceptDeletedAndMoveFromParagraphMarksTransform(n.ThisBlockContentElement));
}
// Groups with DeletedParagraphCollectionType.ParagraphFollowing
// have their content incorporated when processing
// DeletedParagraphCollectionType.DeletedParagraphMarkContent.
return null;
}),
element.Elements().Where(e => e.Name == W.sectPr)
);
return newParagraphParentElement;
}
// Otherwise, identity clone.
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => AcceptDeletedAndMoveFromParagraphMarksTransform(n)));
}
return node;
}
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 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 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 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 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 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.ElementsBeforeSelfReverseDocumentOrder())
.Where(z => z.Name == W.tc &&
!z.Descendants(W.cellDel).Any())
.FirstOrDefault()
};
return a;
}
var a2 = new
{
CollectionType = DeletedCellCollectionType.Other,
Disambiguator = e
};
return a2;
});
return new XElement(W.tr,
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 newTc = new XElement(W.tc,
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)),
g.First().Elements().Where(e => e.Name != W.tcPr));
return (object)newTc;
}));
}
// Identity clone
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => AcceptDeletedCellsTransform(n)));
}
return node;
}
private static void AcceptRevisionsForPart(OpenXmlPart part)
{
XElement documentElement = part.GetXDocument().Root;
documentElement = (XElement)AcceptMoveFromMoveToTransform(documentElement);
documentElement = AcceptMoveFromRanges(documentElement);
// AcceptParagraphEndTagsInMoveFromTransform needs rewritten similar to AcceptDeletedAndMoveFromParagraphMarks
documentElement = (XElement)AcceptParagraphEndTagsInMoveFromTransform(documentElement);
documentElement = AcceptDeletedAndMovedFromContentControls(documentElement);
documentElement = AcceptDeletedAndMoveFromParagraphMarks(documentElement);
documentElement = (XElement)AcceptAllOtherRevisionsTransform(documentElement);
documentElement = (XElement)AcceptDeletedCellsTransform(documentElement);
documentElement.Descendants().Attributes().Where(a => a.Name == PT.UniqueId || a.Name == PT.RunIds).Remove();
XDocument newXDoc = new XDocument(documentElement);
part.PutXDocument(newXDoc);
}
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);
}
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;
}
}
}