Skip to content

Commit 135af90

Browse files
committed
Update FormattingAssembler.cs module so that it properly processes documents that contain tracked revisions.
1 parent 485f8b0 commit 135af90

27 files changed

+331
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Drawing;
7+
using System.Drawing.Imaging;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Text;
11+
using System.Threading.Tasks;
12+
using System.Xml.Linq;
13+
using DocumentFormat.OpenXml.Packaging;
14+
using DocumentFormat.OpenXml.Validation;
15+
using DocumentFormat.OpenXml.Wordprocessing;
16+
using OpenXmlPowerTools;
17+
using Xunit;
18+
19+
#if !ELIDE_XUNIT_TESTS
20+
21+
namespace OxPt
22+
{
23+
public class FaTests
24+
{
25+
[Theory]
26+
[InlineData("FA001-00010", "FA/RevTracking/001-DeletedRun.docx")]
27+
[InlineData("FA001-00020", "FA/RevTracking/002-DeletedNumberedParagraphs.docx")]
28+
[InlineData("FA001-00030", "FA/RevTracking/003-DeletedFieldCode.docx")]
29+
[InlineData("FA001-00040", "FA/RevTracking/004-InsertedNumberingProperties.docx")]
30+
[InlineData("FA001-00050", "FA/RevTracking/005-InsertedNumberedParagraph.docx")]
31+
[InlineData("FA001-00060", "FA/RevTracking/006-DeletedTableRow.docx")]
32+
[InlineData("FA001-00070", "FA/RevTracking/007-InsertedTableRow.docx")]
33+
[InlineData("FA001-00080", "FA/RevTracking/008-InsertedFieldCode.docx")]
34+
[InlineData("FA001-00090", "FA/RevTracking/009-InsertedParagraph.docx")]
35+
[InlineData("FA001-00100", "FA/RevTracking/010-InsertedRun.docx")]
36+
[InlineData("FA001-00110", "FA/RevTracking/011-InsertedMathChar.docx")]
37+
[InlineData("FA001-00120", "FA/RevTracking/012-DeletedMathChar.docx")]
38+
[InlineData("FA001-00130", "FA/RevTracking/013-DeletedParagraph.docx")]
39+
[InlineData("FA001-00140", "FA/RevTracking/014-MovedParagraph.docx")]
40+
[InlineData("FA001-00150", "FA/RevTracking/015-InsertedContentControl.docx")]
41+
[InlineData("FA001-00160", "FA/RevTracking/016-DeletedContentControl.docx")]
42+
[InlineData("FA001-00170", "FA/RevTracking/017-NumberingChange.docx")]
43+
[InlineData("FA001-00180", "FA/RevTracking/018-ParagraphPropertiesChange.docx")]
44+
[InlineData("FA001-00190", "FA/RevTracking/019-RunPropertiesChange.docx")]
45+
[InlineData("FA001-00200", "FA/RevTracking/020-SectionPropertiesChange.docx")]
46+
[InlineData("FA001-00210", "FA/RevTracking/021-TableGridChange.docx")]
47+
[InlineData("FA001-00220", "FA/RevTracking/022-TablePropertiesChange.docx")]
48+
[InlineData("FA001-00230", "FA/RevTracking/023-CellPropertiesChange.docx")]
49+
[InlineData("FA001-00240", "FA/RevTracking/024-RowPropertiesChange.docx")]
50+
51+
public void FA001_DocumentsWithRevTracking(string testId, string src)
52+
{
53+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
54+
// Load the source document
55+
DirectoryInfo sourceDir = new DirectoryInfo("../../../../TestFiles/");
56+
FileInfo sourceDocxFi = new FileInfo(Path.Combine(sourceDir.FullName, src));
57+
WmlDocument wmlSourceDocument = new WmlDocument(sourceDocxFi.FullName);
58+
59+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
60+
// Create the dir for the test
61+
var rootTempDir = TestUtil.TempDir;
62+
var thisTestTempDir = new DirectoryInfo(Path.Combine(rootTempDir.FullName, testId));
63+
if (thisTestTempDir.Exists)
64+
Assert.True(false, "Duplicate test id: " + testId);
65+
else
66+
thisTestTempDir.Create();
67+
var tempDirFullName = thisTestTempDir.FullName;
68+
69+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
70+
// Copy src DOCX to temp directory, for ease of review
71+
72+
var sourceDocxCopiedToDestFileName = new FileInfo(Path.Combine(tempDirFullName, sourceDocxFi.Name));
73+
if (!sourceDocxCopiedToDestFileName.Exists)
74+
wmlSourceDocument.SaveAs(sourceDocxCopiedToDestFileName.FullName);
75+
76+
var sourceDocxAcceptedCopiedToDestFileName = new FileInfo(Path.Combine(tempDirFullName, sourceDocxFi.Name.ToLower().Replace(".docx", "-accepted.docx")));
77+
var wmlSourceAccepted = RevisionProcessor.AcceptRevisions(wmlSourceDocument);
78+
wmlSourceAccepted.SaveAs(sourceDocxAcceptedCopiedToDestFileName.FullName);
79+
80+
var outFi = new FileInfo(Path.Combine(tempDirFullName, "Output.docx"));
81+
FormattingAssemblerSettings settings = new FormattingAssemblerSettings();
82+
var assembledWml = FormattingAssembler.AssembleFormatting(wmlSourceDocument, settings);
83+
assembledWml.SaveAs(outFi.FullName);
84+
85+
var outAcceptedFi = new FileInfo(Path.Combine(tempDirFullName, "Output-accepted.docx"));
86+
var assembledAcceptedWml = RevisionProcessor.AcceptRevisions(assembledWml);
87+
assembledAcceptedWml.SaveAs(outAcceptedFi.FullName);
88+
89+
Validate(outFi);
90+
}
91+
92+
private void Validate(FileInfo fi)
93+
{
94+
using (WordprocessingDocument wDoc = WordprocessingDocument.Open(fi.FullName, true))
95+
{
96+
OpenXmlValidator v = new OpenXmlValidator();
97+
var errors = v.Validate(wDoc).Where(ve =>
98+
{
99+
var found = s_ExpectedErrors.Any(xe => ve.Description.Contains(xe));
100+
return !found;
101+
});
102+
103+
if (errors.Count() != 0)
104+
{
105+
StringBuilder sb = new StringBuilder();
106+
foreach (var item in errors)
107+
{
108+
sb.Append(item.Description).Append(Environment.NewLine);
109+
}
110+
var s = sb.ToString();
111+
Assert.True(false, s);
112+
}
113+
}
114+
}
115+
116+
private static List<string> s_ExpectedErrors = new List<string>()
117+
{
118+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:evenHBand' attribute is not declared.",
119+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:evenVBand' attribute is not declared.",
120+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:firstColumn' attribute is not declared.",
121+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:firstRow' attribute is not declared.",
122+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:firstRowFirstColumn' attribute is not declared.",
123+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:firstRowLastColumn' attribute is not declared.",
124+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:lastColumn' attribute is not declared.",
125+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:lastRow' attribute is not declared.",
126+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:lastRowFirstColumn' attribute is not declared.",
127+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:lastRowLastColumn' attribute is not declared.",
128+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:noHBand' attribute is not declared.",
129+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:noVBand' attribute is not declared.",
130+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:oddHBand' attribute is not declared.",
131+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:oddVBand' attribute is not declared.",
132+
"The element has unexpected child element 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:updateFields'.",
133+
"The attribute 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:name' has invalid value 'useWord2013TrackBottomHyphenation'. The Enumeration constraint failed.",
134+
"The 'http://schemas.microsoft.com/office/word/2012/wordml:restartNumberingAfterBreak' attribute is not declared.",
135+
"Attribute 'id' should have unique value. Its current value '",
136+
"The 'urn:schemas-microsoft-com:mac:vml:blur' attribute is not declared.",
137+
"Attribute 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:id' should have unique value. Its current value '",
138+
"The element has unexpected child element 'http://schemas.microsoft.com/office/word/2012/wordml:",
139+
"The element has invalid child element 'http://schemas.microsoft.com/office/word/2012/wordml:",
140+
"The 'urn:schemas-microsoft-com:mac:vml:complextextbox' attribute is not declared.",
141+
"http://schemas.microsoft.com/office/word/2010/wordml:",
142+
"http://schemas.microsoft.com/office/word/2008/9/12/wordml:",
143+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:allStyles' attribute is not declared.",
144+
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:customStyles' attribute is not declared.",
145+
"The element has invalid child element 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:ins'.",
146+
"The element has invalid child element 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:del'.",
147+
};
148+
}
149+
}
150+
#endif

OpenXmlPowerTools/FormattingAssembler.cs

+179-5
Original file line numberDiff line numberDiff line change
@@ -384,18 +384,177 @@ private static object NormalizeListItemsTransform(FormattingAssemblerInfo fai, W
384384
}
385385
AddTabAtLeftIndent(element.Element(PtOpenXml.pPr));
386386

387+
XElement tabRun = suffix != null ?
388+
new XElement(W.r,
389+
new XAttribute(PtOpenXml.ListItemRun, levelNumsString),
390+
listItemRunProps,
391+
suffix) : null;
392+
393+
bool isDeleted = false;
394+
bool isInserted = false;
395+
XAttribute authorAtt = null;
396+
XAttribute dateAtt = null;
397+
398+
var paraDelElement = newParaProps
399+
.Elements(W.rPr)
400+
.Elements(W.del)
401+
.FirstOrDefault();
402+
if (paraDelElement != null)
403+
{
404+
isDeleted = true;
405+
authorAtt = paraDelElement.Attribute(W.author);
406+
dateAtt = paraDelElement.Attribute(W.date);
407+
}
408+
409+
var paraInsElement = newParaProps
410+
.Elements(W.rPr)
411+
.Elements(W.ins)
412+
.FirstOrDefault();
413+
if (paraInsElement != null)
414+
{
415+
isInserted = true;
416+
authorAtt = paraInsElement.Attribute(W.author);
417+
dateAtt = paraInsElement.Attribute(W.date);
418+
}
419+
420+
var paragraphBefore = element
421+
.SiblingsBeforeSelfReverseDocumentOrder()
422+
.FirstOrDefault();
423+
if (paragraphBefore != null)
424+
{
425+
var paraInsElement2 = paragraphBefore
426+
.Elements(W.pPr)
427+
.Elements(W.rPr)
428+
.Elements(W.ins)
429+
.FirstOrDefault();
430+
if (paraInsElement2 != null)
431+
{
432+
isInserted = true;
433+
authorAtt = paraInsElement2.Attribute(W.author);
434+
dateAtt = paraInsElement2.Attribute(W.date);
435+
}
436+
}
437+
438+
#if false
439+
<w:p w14:paraId="448CD560"
440+
w14:textId="77777777"
441+
w:rsidR="003C33D5"
442+
w:rsidRDefault="003C33D5"
443+
w:rsidP="003C33D5">
444+
<w:pPr>
445+
<w:pStyle w:val="ListParagraph"/>
446+
<w:numPr>
447+
<w:ilvl w:val="0"/>
448+
<w:numId w:val="1"/>
449+
</w:numPr>
450+
<w:pPrChange w:id="4"
451+
w:author="e"
452+
w:date="2020-02-07T18:26:00Z">
453+
<w:pPr/>
454+
</w:pPrChange>
455+
</w:pPr>
456+
<w:r>
457+
<w:t>When you click Online Video, you can paste in the embed code for the video you want to add.</w:t>
458+
</w:r>
459+
</w:p>
460+
#endif
461+
462+
var pPrChange = element
463+
.Elements(W.pPr)
464+
.Elements(W.pPrChange)
465+
.FirstOrDefault();
466+
467+
if (pPrChange != null)
468+
{
469+
authorAtt = pPrChange.Attribute(W.author);
470+
dateAtt = pPrChange.Attribute(W.date);
471+
472+
var thisNumPr = element
473+
.Elements(W.pPr)
474+
.Elements(W.numPr)
475+
.FirstOrDefault();
476+
477+
var thisNumPrChange = pPrChange
478+
.Elements(W.numPr)
479+
.FirstOrDefault();
480+
481+
if (thisNumPr != null && thisNumPrChange == null)
482+
isInserted = true;
483+
484+
if (thisNumPr == null && thisNumPrChange != null)
485+
isDeleted = true;
486+
}
487+
488+
if (isDeleted)
489+
{
490+
// convert listItemRun and tabRun to their deleted equivalents
491+
var highestId = wDoc
492+
.MainDocumentPart
493+
.GetXDocument()
494+
.Descendants()
495+
.Attributes(W.id)
496+
.Select(id =>
497+
{
498+
int numId;
499+
if (int.TryParse((string)id, out numId))
500+
return numId;
501+
else
502+
return 0;
503+
})
504+
.Max();
505+
506+
listItemRun = new XElement(W.del,
507+
new XAttribute(W.id, highestId + 1),
508+
authorAtt,
509+
dateAtt,
510+
(XElement)TransformToDeleted(listItemRun));
511+
tabRun = new XElement(W.del,
512+
new XAttribute(W.id, highestId + 2),
513+
authorAtt,
514+
dateAtt,
515+
(XElement)TransformToDeleted(tabRun));
516+
}
517+
else
518+
{
519+
if (isInserted)
520+
{
521+
// convert listItemRun and tabRun to their inserted equivalents
522+
var highestId = wDoc
523+
.MainDocumentPart
524+
.GetXDocument()
525+
.Descendants()
526+
.Attributes(W.id)
527+
.Select(id =>
528+
{
529+
int numId;
530+
if (int.TryParse((string)id, out numId))
531+
return numId;
532+
else
533+
return 0;
534+
})
535+
.Max();
536+
537+
listItemRun = new XElement(W.ins,
538+
new XAttribute(W.id, highestId + 1),
539+
authorAtt,
540+
dateAtt,
541+
listItemRun);
542+
tabRun = new XElement(W.ins,
543+
new XAttribute(W.id, highestId + 2),
544+
authorAtt,
545+
dateAtt,
546+
tabRun);
547+
}
548+
}
549+
387550
XElement newPara = new XElement(W.p,
388551
element.Attribute(PtOpenXml.FontName),
389552
element.Attribute(PtOpenXml.LanguageType),
390553
element.Attribute(PtOpenXml.Unid),
391554
new XAttribute(PtOpenXml.AbstractNumId, abstractNumId),
392555
newParaProps,
393556
listItemRun,
394-
suffix != null ?
395-
new XElement(W.r,
396-
new XAttribute(PtOpenXml.ListItemRun, levelNumsString),
397-
listItemRunProps,
398-
suffix) : null,
557+
tabRun,
399558
element.Elements().Where(e => e.Name != W.pPr).Select(n => NormalizeListItemsTransform(fai, wDoc, n, settings)));
400559
return newPara;
401560

@@ -409,6 +568,21 @@ private static object NormalizeListItemsTransform(FormattingAssemblerInfo fai, W
409568
return node;
410569
}
411570

571+
private static object TransformToDeleted(XNode node)
572+
{
573+
XElement element = node as XElement;
574+
if (element != null)
575+
{
576+
if (element.Name == W.t)
577+
return new XElement(W.delText, element.Value);
578+
579+
return new XElement(element.Name,
580+
element.Attributes(),
581+
element.Nodes().Select(n => TransformToDeleted(n)));
582+
}
583+
return node;
584+
}
585+
412586
private static void AddTabAtLeftIndent(XElement pPr)
413587
{
414588
int left = 0;

OpenXmlPowerTools/PtOpenXmlUtil.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,8 @@ public static XElement CoalesceAdjacentRunsWithIdenticalFormatting(XElement runC
11911191

11921192
private static Dictionary<XName, int> Order_rPr = new Dictionary<XName, int>
11931193
{
1194+
{ W.moveFrom, 5 },
1195+
{ W.moveTo, 7 },
11941196
{ W.ins, 10 },
11951197
{ W.del, 20 },
11961198
{ W.rStyle, 30 },
12.5 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12.4 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12.6 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)