blob: 6f505c9ed5f44915e91ad01beb968b00933bf5ee [file] [log] [blame]
/***************************************************************************
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://OpenXmlDeveloper.org
Resource Center and Documentation: http://openxmldeveloper.org/wiki/w/wiki/powertools-for-open-xml.aspx
Developer: Eric White
Blog: http://www.ericwhite.com
Twitter: @EricWhiteDev
Email: eric@ericwhite.com
***************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Xml.Schema;
using DocumentFormat.OpenXml.Office.CustomUI;
using DocumentFormat.OpenXml.Packaging;
using OpenXmlPowerTools;
using System.Collections;
namespace OpenXmlPowerTools
{
public class DocumentAssembler
{
public static WmlDocument AssembleDocument(WmlDocument templateDoc, XmlDocument data, out bool templateError)
{
XDocument xDoc = data.GetXDocument();
return AssembleDocument(templateDoc, xDoc.Root, out templateError);
}
public static WmlDocument AssembleDocument(WmlDocument templateDoc, XElement data, out bool templateError)
{
byte[] byteArray = templateDoc.DocumentByteArray;
using (MemoryStream mem = new MemoryStream())
{
mem.Write(byteArray, 0, (int)byteArray.Length);
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(mem, true))
{
if (RevisionAccepter.HasTrackedRevisions(wordDoc))
throw new OpenXmlPowerToolsException("Invalid DocumentAssembler template - contains tracked revisions");
var te = new TemplateError();
foreach (var part in wordDoc.ContentParts())
{
ProcessTemplatePart(data, te, part);
}
templateError = te.HasError;
}
WmlDocument assembledDocument = new WmlDocument("TempFileName.docx", mem.ToArray());
return assembledDocument;
}
}
private static void ProcessTemplatePart(XElement data, TemplateError te, OpenXmlPart part)
{
XDocument xDoc = part.GetXDocument();
var xDocRoot = RemoveGoBackBookmarks(xDoc.Root);
// content controls in cells can surround the W.tc element, so transform so that such content controls are within the cell content
xDocRoot = (XElement)NormalizeContentControlsInCells(xDocRoot);
xDocRoot = (XElement)TransformToMetadata(xDocRoot, data, te);
// Table might have been placed at run-level, when it should be at block-level, so fix this.
// Repeat, EndRepeat, Conditional, EndConditional are allowed at run level, but only if there is a matching pair
// if there is only one Repeat, EndRepeat, Conditional, EndConditional, then move to block level.
// if there is a matching pair, then is OK.
xDocRoot = (XElement)ForceBlockLevelAsAppropriate(xDocRoot, te);
NormalizeTablesRepeatAndConditional(xDocRoot, te);
// any EndRepeat, EndConditional that remain are orphans, so replace with an error
ProcessOrphanEndRepeatEndConditional(xDocRoot, te);
// do the actual content replacement
xDocRoot = (XElement)ContentReplacementTransform(xDocRoot, data, te);
xDoc.Elements().First().ReplaceWith(xDocRoot);
part.PutXDocument();
return;
}
private static XName[] s_MetaToForceToBlock = new XName[] {
PA.Conditional,
PA.EndConditional,
PA.Repeat,
PA.EndRepeat,
PA.Table,
};
private static object ForceBlockLevelAsAppropriate(XNode node, TemplateError te)
{
XElement element = node as XElement;
if (element != null)
{
if (element.Name == W.p)
{
var childMeta = element.Elements().Where(n => s_MetaToForceToBlock.Contains(n.Name)).ToList();
if (childMeta.Count() == 1)
{
var child = childMeta.First();
var otherTextInParagraph = element.Elements(W.r).Elements(W.t).Select(t => (string)t).StringConcatenate().Trim();
if (otherTextInParagraph != "")
{
var newPara = new XElement(element);
var newMeta = newPara.Elements().Where(n => s_MetaToForceToBlock.Contains(n.Name)).First();
newMeta.ReplaceWith(CreateRunErrorMessage("Error: Unmatched metadata can't be in paragraph with other text", te));
return newPara;
}
var meta = new XElement(child.Name,
child.Attributes(),
new XElement(W.p,
element.Attributes(),
element.Elements(W.pPr),
child.Elements()));
return meta;
}
var count = childMeta.Count();
if (count % 2 == 0)
{
if (childMeta.Where(c => c.Name == PA.Repeat).Count() != childMeta.Where(c => c.Name == PA.EndRepeat).Count())
return CreateContextErrorMessage(element, "Error: Mismatch Repeat / EndRepeat at run level", te);
if (childMeta.Where(c => c.Name == PA.Conditional).Count() != childMeta.Where(c => c.Name == PA.EndConditional).Count())
return CreateContextErrorMessage(element, "Error: Mismatch Conditional / EndConditional at run level", te);
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => ForceBlockLevelAsAppropriate(n, te)));
}
else
{
return CreateContextErrorMessage(element, "Error: Invalid metadata at run level", te);
}
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => ForceBlockLevelAsAppropriate(n, te)));
}
return node;
}
private static void ProcessOrphanEndRepeatEndConditional(XElement xDocRoot, TemplateError te)
{
foreach (var element in xDocRoot.Descendants(PA.EndRepeat).ToList())
{
var error = CreateContextErrorMessage(element, "Error: EndRepeat without matching Repeat", te);
element.ReplaceWith(error);
}
foreach (var element in xDocRoot.Descendants(PA.EndConditional).ToList())
{
var error = CreateContextErrorMessage(element, "Error: EndConditional without matching Conditional", te);
element.ReplaceWith(error);
}
}
private static XElement RemoveGoBackBookmarks(XElement xElement)
{
var cloneXDoc = new XElement(xElement);
while (true)
{
var bm = cloneXDoc.DescendantsAndSelf(W.bookmarkStart).FirstOrDefault(b => (string)b.Attribute(W.name) == "_GoBack");
if (bm == null)
break;
var id = (string)bm.Attribute(W.id);
var endBm = cloneXDoc.DescendantsAndSelf(W.bookmarkEnd).FirstOrDefault(b => (string)b.Attribute(W.id) == id);
bm.Remove();
endBm.Remove();
}
return cloneXDoc;
}
// this transform inverts content controls that surround W.tc elements. After transforming, the W.tc will contain
// the content control, which contains the paragraph content of the cell.
private static object NormalizeContentControlsInCells(XNode node)
{
XElement element = node as XElement;
if (element != null)
{
if (element.Name == W.sdt && element.Parent.Name == W.tr)
{
var newCell = new XElement(W.tc,
element.Elements(W.tc).Elements(W.tcPr),
new XElement(W.sdt,
element.Elements(W.sdtPr),
element.Elements(W.sdtEndPr),
new XElement(W.sdtContent,
element.Elements(W.sdtContent).Elements(W.tc).Elements().Where(e => e.Name != W.tcPr))));
return newCell;
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => NormalizeContentControlsInCells(n)));
}
return node;
}
// The following method is written using tree modification, not RPFT, because it is easier to write in this fashion.
// These types of operations are not as easy to write using RPFT.
// Unless you are completely clear on the semantics of LINQ to XML DML, do not make modifications to this method.
private static void NormalizeTablesRepeatAndConditional(XElement xDoc, TemplateError te)
{
var tables = xDoc.Descendants(PA.Table).ToList();
foreach (var table in tables)
{
var followingElement = table.ElementsAfterSelf().Where(e => e.Name == W.tbl || e.Name == W.p).FirstOrDefault();
if (followingElement == null || followingElement.Name != W.tbl)
{
table.ReplaceWith(CreateParaErrorMessage("Table metadata is not immediately followed by a table", te));
continue;
}
// remove superflous paragraph from Table metadata
table.RemoveNodes();
// detach w:tbl from parent, and add to Table metadata
followingElement.Remove();
table.Add(followingElement);
}
int repeatDepth = 0;
int conditionalDepth = 0;
foreach (var metadata in xDoc.Descendants().Where(d =>
d.Name == PA.Repeat ||
d.Name == PA.Conditional ||
d.Name == PA.EndRepeat ||
d.Name == PA.EndConditional))
{
if (metadata.Name == PA.Repeat)
{
++repeatDepth;
metadata.Add(new XAttribute(PA.Depth, repeatDepth));
continue;
}
if (metadata.Name == PA.EndRepeat)
{
metadata.Add(new XAttribute(PA.Depth, repeatDepth));
--repeatDepth;
continue;
}
if (metadata.Name == PA.Conditional)
{
++conditionalDepth;
metadata.Add(new XAttribute(PA.Depth, conditionalDepth));
continue;
}
if (metadata.Name == PA.EndConditional)
{
metadata.Add(new XAttribute(PA.Depth, conditionalDepth));
--conditionalDepth;
continue;
}
}
while (true)
{
bool didReplace = false;
foreach (var metadata in xDoc.Descendants().Where(d => (d.Name == PA.Repeat || d.Name == PA.Conditional) && d.Attribute(PA.Depth) != null).ToList())
{
var depth = (int)metadata.Attribute(PA.Depth);
XName matchingEndName = null;
if (metadata.Name == PA.Repeat)
matchingEndName = PA.EndRepeat;
else if (metadata.Name == PA.Conditional)
matchingEndName = PA.EndConditional;
if (matchingEndName == null)
throw new OpenXmlPowerToolsException("Internal error");
var matchingEnd = metadata.ElementsAfterSelf(matchingEndName).FirstOrDefault(end => { return (int)end.Attribute(PA.Depth) == depth; });
if (matchingEnd == null)
{
metadata.ReplaceWith(CreateParaErrorMessage(string.Format("{0} does not have matching {1}", metadata.Name.LocalName, matchingEndName.LocalName), te));
continue;
}
metadata.RemoveNodes();
var contentBetween = metadata.ElementsAfterSelf().TakeWhile(after => after != matchingEnd).ToList();
foreach (var item in contentBetween)
item.Remove();
contentBetween = contentBetween.Where(n => n.Name != W.bookmarkStart && n.Name != W.bookmarkEnd).ToList();
metadata.Add(contentBetween);
metadata.Attributes(PA.Depth).Remove();
matchingEnd.Remove();
didReplace = true;
break;
}
if (!didReplace)
break;
}
}
private static List<string> s_AliasList = new List<string>()
{
"Content",
"Table",
"Repeat",
"EndRepeat",
"Conditional",
"EndConditional",
};
private static object TransformToMetadata(XNode node, XElement data, TemplateError te)
{
XElement element = node as XElement;
if (element != null)
{
if (element.Name == W.sdt)
{
var alias = (string)element.Elements(W.sdtPr).Elements(W.alias).Attributes(W.val).FirstOrDefault();
if (alias == null || alias == "" || s_AliasList.Contains(alias))
{
var ccContents = element
.DescendantsTrimmed(W.txbxContent)
.Where(e => e.Name == W.t)
.Select(t => (string)t)
.StringConcatenate()
.Trim()
.Replace('“', '"')
.Replace('”', '"');
if (ccContents.StartsWith("<"))
{
XElement xml = TransformXmlTextToMetadata(te, ccContents);
if (xml.Name == W.p || xml.Name == W.r) // this means there was an error processing the XML.
{
if (element.Parent.Name == W.p)
return xml.Elements(W.r);
return xml;
}
if (alias != null && xml.Name.LocalName != alias)
{
if (element.Parent.Name == W.p)
return CreateRunErrorMessage("Error: Content control alias does not match metadata element name", te);
else
return CreateParaErrorMessage("Error: Content control alias does not match metadata element name", te);
}
xml.Add(element.Elements(W.sdtContent).Elements());
return xml;
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => TransformToMetadata(n, data, te)));
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => TransformToMetadata(n, data, te)));
}
if (element.Name == W.p)
{
var paraContents = element
.DescendantsTrimmed(W.txbxContent)
.Where(e => e.Name == W.t)
.Select(t => (string)t)
.StringConcatenate()
.Trim();
int occurances = paraContents.Select((c, i) => paraContents.Substring(i)).Count(sub => sub.StartsWith("<#"));
if (paraContents.StartsWith("<#") && paraContents.EndsWith("#>") && occurances == 1)
{
var xmlText = paraContents.Substring(2, paraContents.Length - 4).Trim();
XElement xml = TransformXmlTextToMetadata(te, xmlText);
if (xml.Name == W.p || xml.Name == W.r)
return xml;
xml.Add(element);
return xml;
}
if (paraContents.Contains("<#"))
{
List<RunReplacementInfo> runReplacementInfo = new List<RunReplacementInfo>();
var thisGuid = Guid.NewGuid().ToString();
var r = new Regex("<#.*?#>");
XElement xml = null;
OpenXmlRegex.Replace(new[] { element }, r, thisGuid, (para, match) =>
{
var matchString = match.Value.Trim();
var xmlText = matchString.Substring(2, matchString.Length - 4).Trim().Replace('“', '"').Replace('”', '"');
try
{
xml = XElement.Parse(xmlText);
}
catch (XmlException e)
{
RunReplacementInfo rri = new RunReplacementInfo()
{
Xml = null,
XmlExceptionMessage = "XmlException: " + e.Message,
SchemaValidationMessage = null,
};
runReplacementInfo.Add(rri);
return true;
}
string schemaError = ValidatePerSchema(xml);
if (schemaError != null)
{
RunReplacementInfo rri = new RunReplacementInfo()
{
Xml = null,
XmlExceptionMessage = null,
SchemaValidationMessage = "Schema Validation Error: " + schemaError,
};
runReplacementInfo.Add(rri);
return true;
}
RunReplacementInfo rri2 = new RunReplacementInfo()
{
Xml = xml,
XmlExceptionMessage = null,
SchemaValidationMessage = null,
};
runReplacementInfo.Add(rri2);
return true;
}, false);
var newPara = new XElement(element);
foreach (var rri in runReplacementInfo)
{
var runToReplace = newPara.Descendants(W.r).FirstOrDefault(rn => rn.Value == thisGuid && rn.Parent.Name != PA.Content);
if (runToReplace == null)
throw new OpenXmlPowerToolsException("Internal error");
if (rri.XmlExceptionMessage != null)
runToReplace.ReplaceWith(CreateRunErrorMessage(rri.XmlExceptionMessage, te));
else if (rri.SchemaValidationMessage != null)
runToReplace.ReplaceWith(CreateRunErrorMessage(rri.SchemaValidationMessage, te));
else
{
var newXml = new XElement(rri.Xml);
newXml.Add(runToReplace);
runToReplace.ReplaceWith(newXml);
}
}
var coalescedParagraph = WordprocessingMLUtil.CoalesceAdjacentRunsWithIdenticalFormatting(newPara);
return coalescedParagraph;
}
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => TransformToMetadata(n, data, te)));
}
return node;
}
private static XElement TransformXmlTextToMetadata(TemplateError te, string xmlText)
{
XElement xml;
try
{
xml = XElement.Parse(xmlText);
}
catch (XmlException e)
{
return CreateParaErrorMessage("XmlException: " + e.Message, te);
}
string schemaError = ValidatePerSchema(xml);
if (schemaError != null)
return CreateParaErrorMessage("Schema Validation Error: " + schemaError, te);
return xml;
}
private class RunReplacementInfo
{
public XElement Xml;
public string XmlExceptionMessage;
public string SchemaValidationMessage;
}
private static string ValidatePerSchema(XElement element)
{
if (s_PASchemaSets == null)
{
s_PASchemaSets = new Dictionary<XName, PASchemaSet>()
{
{
PA.Content,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='Content'>
<xs:complexType>
<xs:attribute name='Select' type='xs:string' use='required' />
<xs:attribute name='Optional' type='xs:boolean' use='optional' />
</xs:complexType>
</xs:element>
</xs:schema>",
}
},
{
PA.Table,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='Table'>
<xs:complexType>
<xs:attribute name='Select' type='xs:string' use='required' />
</xs:complexType>
</xs:element>
</xs:schema>",
}
},
{
PA.Repeat,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='Repeat'>
<xs:complexType>
<xs:attribute name='Select' type='xs:string' use='required' />
<xs:attribute name='Optional' type='xs:boolean' use='optional' />
</xs:complexType>
</xs:element>
</xs:schema>",
}
},
{
PA.EndRepeat,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='EndRepeat' />
</xs:schema>",
}
},
{
PA.Conditional,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='Conditional'>
<xs:complexType>
<xs:attribute name='Select' type='xs:string' use='required' />
<xs:attribute name='Match' type='xs:string' use='optional' />
<xs:attribute name='NotMatch' type='xs:string' use='optional' />
</xs:complexType>
</xs:element>
</xs:schema>",
}
},
{
PA.EndConditional,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='EndConditional' />
</xs:schema>",
}
},
};
foreach (var item in s_PASchemaSets)
{
var itemPAss = item.Value;
XmlSchemaSet schemas = new XmlSchemaSet();
schemas.Add("", XmlReader.Create(new StringReader(itemPAss.XsdMarkup)));
itemPAss.SchemaSet = schemas;
}
}
if (!s_PASchemaSets.ContainsKey(element.Name))
{
return string.Format("Invalid XML: {0} is not a valid element", element.Name.LocalName);
}
var paSchemaSet = s_PASchemaSets[element.Name];
XDocument d = new XDocument(element);
string message = null;
d.Validate(paSchemaSet.SchemaSet, (sender, e) =>
{
if (message == null)
message = e.Message;
}, true);
if (message != null)
return message;
return null;
}
private class PA
{
public static XName Content = "Content";
public static XName Table = "Table";
public static XName Repeat = "Repeat";
public static XName EndRepeat = "EndRepeat";
public static XName Conditional = "Conditional";
public static XName EndConditional = "EndConditional";
public static XName Select = "Select";
public static XName Optional = "Optional";
public static XName Match = "Match";
public static XName NotMatch = "NotMatch";
public static XName Depth = "Depth";
}
private class PASchemaSet
{
public string XsdMarkup;
public XmlSchemaSet SchemaSet;
}
private static Dictionary<XName, PASchemaSet> s_PASchemaSets = null;
private class TemplateError
{
public bool HasError = false;
}
static object ContentReplacementTransform(XNode node, XElement data, TemplateError templateError)
{
XElement element = node as XElement;
if (element != null)
{
if (element.Name == PA.Content)
{
XElement para = element.Descendants(W.p).FirstOrDefault();
XElement run = element.Descendants(W.r).FirstOrDefault();
var xPath = (string) element.Attribute(PA.Select);
var optionalString = (string) element.Attribute(PA.Optional);
bool optional = (optionalString != null && optionalString.ToLower() == "true");
string newValue;
try
{
newValue = EvaluateXPathToString(data, xPath, optional);
}
catch (XPathException e)
{
return CreateContextErrorMessage(element, "XPathException: " + e.Message, templateError);
}
if (para != null)
{
XElement p = new XElement(W.p, para.Elements(W.pPr));
foreach(string line in newValue.Split('\n'))
{
p.Add(new XElement(W.r,
para.Elements(W.r).Elements(W.rPr).FirstOrDefault(),
(p.Elements().Count() > 1) ? new XElement(W.br) : null,
new XElement(W.t, line)));
}
return p;
}
else
{
List<XElement> list = new List<XElement>();
foreach(string line in newValue.Split('\n'))
{
list.Add(new XElement(W.r,
run.Elements().Where(e => e.Name != W.t),
(list.Count > 0) ? new XElement(W.br) : null,
new XElement(W.t, line)));
}
return list;
}
}
if (element.Name == PA.Repeat)
{
string selector = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
bool optional = (optionalString != null && optionalString.ToLower() == "true");
IEnumerable<XElement> repeatingData;
try
{
repeatingData = data.XPathSelectElements(selector);
}
catch (XPathException e)
{
return CreateContextErrorMessage(element, "XPathException: " + e.Message, templateError);
}
if (!repeatingData.Any())
{
if (optional)
{
return null;
//XElement para = element.Descendants(W.p).FirstOrDefault();
//if (para != null)
// return new XElement(W.p, new XElement(W.r));
//else
// return new XElement(W.r);
}
return CreateContextErrorMessage(element, "Repeat: Select returned no data", templateError);
}
var newContent = repeatingData.Select(d =>
{
var content = element
.Elements()
.Select(e => ContentReplacementTransform(e, d, templateError))
.ToList();
return content;
})
.ToList();
return newContent;
}
if (element.Name == PA.Table)
{
IEnumerable<XElement> tableData;
try
{
tableData = data.XPathSelectElements((string)element.Attribute(PA.Select));
}
catch (XPathException e)
{
return CreateContextErrorMessage(element, "XPathException: " + e.Message, templateError);
}
if (tableData.Count() == 0)
return CreateContextErrorMessage(element, "Table Select returned no data", templateError);
XElement table = element.Element(W.tbl);
XElement protoRow = table.Elements(W.tr).Skip(1).FirstOrDefault();
var footerRowsBeforeTransform = table
.Elements(W.tr)
.Skip(2)
.ToList();
var footerRows = footerRowsBeforeTransform
.Select(x => ContentReplacementTransform(x, data, templateError))
.ToList();
if (protoRow == null)
return CreateContextErrorMessage(element, string.Format("Table does not contain a prototype row"), templateError);
protoRow.Descendants(W.bookmarkStart).Remove();
protoRow.Descendants(W.bookmarkEnd).Remove();
XElement newTable = new XElement(W.tbl,
table.Elements().Where(e => e.Name != W.tr),
table.Elements(W.tr).FirstOrDefault(),
tableData.Select(d =>
new XElement(W.tr,
protoRow.Elements().Where(r => r.Name != W.tc),
protoRow.Elements(W.tc)
.Select(tc =>
{
XElement paragraph = tc.Elements(W.p).FirstOrDefault();
XElement cellRun = paragraph.Elements(W.r).FirstOrDefault();
string xPath = paragraph.Value;
string newValue = null;
try
{
newValue = EvaluateXPathToString(d, xPath, false);
}
catch (XPathException e)
{
XElement errorCell = new XElement(W.tc,
tc.Elements().Where(z => z.Name != W.p),
new XElement(W.p,
paragraph.Element(W.pPr),
CreateRunErrorMessage(e.Message, templateError)));
return errorCell;
}
XElement newCell = new XElement(W.tc,
tc.Elements().Where(z => z.Name != W.p),
new XElement(W.p,
paragraph.Element(W.pPr),
new XElement(W.r,
cellRun != null ? cellRun.Element(W.rPr) : new XElement(W.rPr), //if the cell was empty there is no cellrun
new XElement(W.t, newValue))));
return newCell;
}))),
footerRows
);
return newTable;
}
if (element.Name == PA.Conditional)
{
string xPath = (string)element.Attribute(PA.Select);
var match = (string)element.Attribute(PA.Match);
var notMatch = (string)element.Attribute(PA.NotMatch);
if (match == null && notMatch == null)
return CreateContextErrorMessage(element, "Conditional: Must specify either Match or NotMatch", templateError);
if (match != null && notMatch != null)
return CreateContextErrorMessage(element, "Conditional: Cannot specify both Match and NotMatch", templateError);
string testValue = null;
try
{
testValue = EvaluateXPathToString(data, xPath, false);
}
catch (XPathException e)
{
return CreateContextErrorMessage(element, e.Message, templateError);
}
if ((match != null && testValue == match) || (notMatch != null && testValue != notMatch))
{
var content = element.Elements().Select(e => ContentReplacementTransform(e, data, templateError));
return content;
}
return null;
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError)));
}
return node;
}
private static object CreateContextErrorMessage(XElement element, string errorMessage, TemplateError templateError)
{
XElement para = element.Descendants(W.p).FirstOrDefault();
XElement run = element.Descendants(W.r).FirstOrDefault();
var errorRun = CreateRunErrorMessage(errorMessage, templateError);
if (para != null)
return new XElement(W.p, errorRun);
else
return errorRun;
}
private static XElement CreateRunErrorMessage(string errorMessage, TemplateError templateError)
{
templateError.HasError = true;
var errorRun = new XElement(W.r,
new XElement(W.rPr,
new XElement(W.color, new XAttribute(W.val, "FF0000")),
new XElement(W.highlight, new XAttribute(W.val, "yellow"))),
new XElement(W.t, errorMessage));
return errorRun;
}
private static XElement CreateParaErrorMessage(string errorMessage, TemplateError templateError)
{
templateError.HasError = true;
var errorPara = new XElement(W.p,
new XElement(W.r,
new XElement(W.rPr,
new XElement(W.color, new XAttribute(W.val, "FF0000")),
new XElement(W.highlight, new XAttribute(W.val, "yellow"))),
new XElement(W.t, errorMessage)));
return errorPara;
}
private static string EvaluateXPathToString(XElement element, string xPath, bool optional )
{
object xPathSelectResult;
try
{
//support some cells in the table may not have an xpath expression.
if (String.IsNullOrWhiteSpace(xPath)) return String.Empty;
xPathSelectResult = element.XPathEvaluate(xPath);
}
catch (XPathException e)
{
throw new XPathException("XPathException: " + e.Message, e);
}
if ((xPathSelectResult is IEnumerable) && !(xPathSelectResult is string))
{
var selectedData = ((IEnumerable) xPathSelectResult).Cast<XObject>();
if (!selectedData.Any())
{
if (optional) return string.Empty;
throw new XPathException(string.Format("XPath expression ({0}) returned no results", xPath));
}
if (selectedData.Count() > 1)
{
throw new XPathException(string.Format("XPath expression ({0}) returned more than one node", xPath));
}
XObject selectedDatum = selectedData.First();
if (selectedDatum is XElement) return ((XElement) selectedDatum).Value;
if (selectedDatum is XAttribute) return ((XAttribute) selectedDatum).Value;
}
return xPathSelectResult.ToString();
}
}
}