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
Lines changed: 150 additions & 0 deletions
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

Lines changed: 179 additions & 5 deletions
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

Lines changed: 2 additions & 0 deletions
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.

0 commit comments

Comments
 (0)