using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using DocumentFormat.OpenXml.Packaging;
namespace Neo.Afx.Services.Documents
{
public partial class DocumentUtilities
{
#region global variables
static readonly Uri DocumentPartUri = new Uri("/word/document.xml", UriKind.Relative);
const string WordNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
const string XPathExpression = "descendant::w:cellDel|" +
"descendant::w:cellIns|" +
"descendant::w:cellMerge|" +
"descendant::w:customXmlDelRangeEnd|" +
"descendant::w:customXmlDelRangeStart|" +
"descendant::w:customXmlInsRangeEnd|" +
"descendant::w:customXmlInsRangeStart|" +
"descendant::w:del|" +
"descendant::w:delInstrText|" +
"descendant::w:delText|" +
"descendant::w:moveFrom|" +
"descendant::w:moveFromRangeEnd|" +
"descendant::w:moveFromRangeStart|" +
"descendant::w:moveTo|" +
"descendant::w:moveToRangeEnd|" +
"descendant::w:moveToRangeStart|" +
"descendant::w:moveTo|" +
"descendant::w:numberingChange|" +
"descendant::w:rPrChange|" +
"descendant::w:pPrChange|" +
"descendant::w:rPrChange|" +
"descendant::w:sectPrChange|" +
"descendant::w:tcPrChange|" +
"descendant::w:tblGridChange|" +
"descendant::w:tblPrChange|" +
"descendant::w:tblPrExChange|" +
"descendant::w:trPrChange|" +
"descendant::w:trackRevisions";
#endregion
#region FillTemplatedDocument
///
/// Fills the templated document.
///
/// Content of the document.
/// The template values.
///
static byte[] FillTemplatedDocument(byte[] documentContent, IEnumerable> templateValues)
{
if(templateValues == null)
{
return null;
}
byte[] fileBytes;
using(var mem = new MemoryStream())
{
mem.Write(documentContent, 0, documentContent.Length);
using(var wordDoc = WordprocessingDocument.Open(mem, true))
{
if(HasTrackedRevisions(wordDoc))
{
throw new Exception("Search and replace will not work with documents that contain revision tracking.");
}
var xmlDoc = GetPartXmlDocument(wordDoc.MainDocumentPart);
foreach(var mapping in templateValues)
{
ReplaceInDocument(wordDoc, xmlDoc, "[[" + mapping.Key.ToUpper() + "]]", (!String.IsNullOrEmpty(mapping.Value) ? mapping.Value : " "), true);
}
var package = wordDoc.Package;
var document = package.GetPart(DocumentPartUri);
try
{
using(var writer = new StreamWriter(document.GetStream()))
{
xmlDoc.Save(writer);
}
}
catch(Exception ex)
{
throw new Exception("Template Save Failed", ex);
}
finally
{
try
{
xmlDoc = GetPartXmlDocument(wordDoc.MainDocumentPart);
}
catch
{
var docUri = package.GetPart(DocumentPartUri).Uri;
var docContentType = package.GetPart(DocumentPartUri).ContentType;
package.DeletePart(DocumentPartUri);
document = package.CreatePart(docUri, docContentType);
if(document != null)
{
using(var writer = new StreamWriter(document.GetStream()))
{
xmlDoc.Save(writer);
}
}
}
}
}
fileBytes = mem.ToArray();
}
return fileBytes;
}
#endregion
#region ReplaceInDocument
///
/// Replaces the in document.
///
/// The word document.
/// The document XML.
/// The old value.
/// The new value.
/// if set to true [match case].
static void ReplaceInDocument(WordprocessingDocument wordDocument, XmlDocument documentXml, string oldValue, string newValue, bool matchCase)
{
if(documentXml == null)
{
return;
}
foreach(var part in new List
{
wordDocument.MainDocumentPart,
wordDocument.MainDocumentPart.EndnotesPart,
wordDocument.MainDocumentPart.FootnotesPart
})
{
if(part == null)
{
continue;
}
if(part != wordDocument.MainDocumentPart)
{
documentXml = GetPartXmlDocument(part);
}
ReplaceInXmlPart(documentXml, oldValue, newValue, matchCase);
SavePartToXml(part, documentXml);
}
foreach(var part in wordDocument.MainDocumentPart.HeaderParts)
{
documentXml = GetPartXmlDocument(part);
ReplaceInXmlPart(documentXml, oldValue, newValue, matchCase);
SavePartToXml(part, documentXml);
}
foreach(var part in wordDocument.MainDocumentPart.FooterParts)
{
documentXml = GetPartXmlDocument(part);
ReplaceInXmlPart(documentXml, oldValue, newValue, matchCase);
SavePartToXml(part, documentXml);
}
}
#endregion
#region ReplaceInXmlPart
///
/// Replaces the in XML part.
///
/// The XML document.
/// The search.
/// The replace.
/// if set to true [match case].
static void ReplaceInXmlPart(XmlDocument xmlDocument, string search, string replace, bool matchCase)
{
var nsmgr = new XmlNamespaceManager(xmlDocument.NameTable);
nsmgr.AddNamespace("w", WordNamespace);
var paragraphs = xmlDocument.SelectNodes("descendant::w:p", nsmgr);
if(paragraphs == null)
{
return;
}
foreach(var paragraph in paragraphs)
{
ReplaceInParagraph((XmlElement)paragraph, search, replace, matchCase);
}
}
#endregion
#region ReplaceInParagraph
///
/// Replaces the in paragraph.
///
/// The paragraph.
/// The search.
/// The replace.
/// if set to true [match case].
static void ReplaceInParagraph(XmlElement paragraph, string search, string replace, bool matchCase)
{
var sb = new StringBuilder();
var xmlDoc = paragraph.OwnerDocument;
if(xmlDoc == null)
{
return;
}
var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsmgr.AddNamespace("w", WordNamespace);
var paragraphText = paragraph.SelectNodes("descendant::w:t", nsmgr);
if(paragraphText == null)
{
return;
}
foreach(XmlNode pTxt in paragraphText)
{
sb.Append(pTxt.InnerText);
}
if((!sb.ToString().Contains(search)) || (!matchCase && !(sb.ToString().ToUpper().Contains(search.ToUpper()))))
{
return;
}
var runs = paragraph.SelectNodes("child::w:r", nsmgr);
if(runs == null)
{
return;
}
foreach(XmlElement run in runs)
{
var childElements = run.SelectNodes("child::*", nsmgr);
if(childElements == null || childElements.Count == 0)
{
continue;
}
for(var c = childElements.Count - 1; c >= 0; --c)
{
if(childElements[c].Name == "w:rPr")
{
continue;
}
if(childElements[c].Name == "w:t")
{
var textElementString = childElements[c].InnerText;
for(var i = textElementString.Length - 1; i >= 0; --i)
{
var newRun = xmlDoc.CreateElement("w:r", WordNamespace);
var runProps = (XmlElement)run.SelectSingleNode("child::w:rPr", nsmgr);
if(runProps != null)
{
var newRunProps = (XmlElement)runProps.CloneNode(true);
newRun.AppendChild(newRunProps);
}
var newTextElement = xmlDoc.CreateElement("w:t", WordNamespace);
var newText = xmlDoc.CreateTextNode(textElementString[i].ToString());
newTextElement.AppendChild(newText);
if(textElementString[i] == ' ')
{
var xmlSpace = xmlDoc.CreateAttribute("xml", "space", "http://www.w3.org/XML/1998/namespace");
xmlSpace.Value = "preserve";
newTextElement.Attributes.Append(xmlSpace);
}
newRun.AppendChild(newTextElement);
paragraph.InsertAfter(newRun, run);
}
}
else
{
var newRun = xmlDoc.CreateElement("w:r", WordNamespace);
var runProps = (XmlElement)run.SelectSingleNode("child::w:rPr", nsmgr);
if(runProps != null)
{
var newRunProps = (XmlElement)runProps.CloneNode(true);
newRun.AppendChild(newRunProps);
}
var newChildElement = (XmlElement)childElements[c].CloneNode(true);
newRun.AppendChild(newChildElement);
paragraph.InsertAfter(newRun, run);
}
}
paragraph.RemoveChild(run);
}
while(true)
{
var cont = false;
runs = paragraph.SelectNodes("child::w:r", nsmgr);
for(var i = 0; i <= runs.Count - search.Length; ++i)
{
var match = true;
for(var c = 0; c < search.Length; ++c)
{
var textElement = (XmlElement)runs[i + c].SelectSingleNode("child::w:t", nsmgr);
if(textElement == null)
{
match = false;
break;
}
if(textElement.InnerText == search[c].ToString())
{
continue;
}
if(!matchCase &&
textElement.InnerText.ToUpper() == search[c].ToString().ToUpper())
{
continue;
}
match = false;
break;
}
if(match)
{
var runProps = (XmlElement)runs[i].SelectSingleNode("descendant::w:rPr", nsmgr);
var newRun = xmlDoc.CreateElement("w:r", WordNamespace);
if(runProps != null)
{
var newRunProps = (XmlElement)runProps.CloneNode(true);
newRun.AppendChild(newRunProps);
}
var newTextElement = xmlDoc.CreateElement("w:t", WordNamespace);
var newText = xmlDoc.CreateTextNode(replace);
newTextElement.AppendChild(newText);
if(replace[0] == ' ' || replace[replace.Length - 1] == ' ')
{
var xmlSpace = xmlDoc.CreateAttribute("xml", "space", "http://www.w3.org/XML/1998/namespace");
xmlSpace.Value = "preserve";
newTextElement.Attributes.Append(xmlSpace);
}
newRun.AppendChild(newTextElement);
paragraph.InsertAfter(newRun, (XmlNode)runs[i]);
for(int c = 0; c < search.Length; ++c)
{
paragraph.RemoveChild(runs[i + c]);
}
cont = true;
break;
}
}
if(!cont)
{
break;
}
}
// Consolidate adjacent runs that have only text elements, and have the
// same run properties. This isn't necessary to create a valid document,
// however, having the split runs is a bit messy.
var children = paragraph.SelectNodes("child::*", nsmgr);
var matchId = new List();
var id = 0;
for(var c = 0; c < children.Count; ++c)
{
if(c == 0)
{
matchId.Add(id);
continue;
}
if(children[c].Name == "w:r" &&
children[c - 1].Name == "w:r" &&
children[c].SelectSingleNode("w:t", nsmgr) != null &&
children[c - 1].SelectSingleNode("w:t", nsmgr) != null)
{
var runProps = (XmlElement)children[c].SelectSingleNode("w:rPr", nsmgr);
var lastRunProps = (XmlElement)children[c - 1].SelectSingleNode("w:rPr", nsmgr);
if((runProps == null && lastRunProps != null) || (runProps != null && lastRunProps == null))
{
matchId.Add(++id);
continue;
}
if(runProps != null && runProps.InnerXml != lastRunProps.InnerXml)
{
matchId.Add(++id);
continue;
}
matchId.Add(id);
continue;
}
matchId.Add(++id);
}
for(var i = 0; i <= id; ++i)
{
var x1 = matchId.IndexOf(i);
var x2 = matchId.LastIndexOf(i);
if(x1 == x2)
{
continue;
}
var sb2 = new StringBuilder();
for(var z = x1; z <= x2; ++z)
{
sb2.Append((children[z].SelectSingleNode("w:t", nsmgr)).InnerText);
}
var newRun = xmlDoc.CreateElement("w:r", WordNamespace);
var runProps = (XmlElement)children[x1].SelectSingleNode("child::w:rPr", nsmgr);
if(runProps != null)
{
var newRunProps = (XmlElement)runProps.CloneNode(true);
newRun.AppendChild(newRunProps);
}
var newTextElement = xmlDoc.CreateElement("w:t", WordNamespace);
var newText = xmlDoc.CreateTextNode(sb2.ToString());
newTextElement.AppendChild(newText);
if(sb2[0] == ' ' || sb2[sb2.Length - 1] == ' ')
{
var xmlSpace = xmlDoc.CreateAttribute("xml", "space", "http://www.w3.org/XML/1998/namespace");
xmlSpace.Value = "preserve";
newTextElement.Attributes.Append(xmlSpace);
}
newRun.AppendChild(newTextElement);
paragraph.InsertAfter(newRun, children[x2]);
for(var z = x1; z <= x2; ++z)
{
paragraph.RemoveChild(children[z]);
}
}
var txbxParagraphs = paragraph.SelectNodes("descendant::w:p", nsmgr);
if(txbxParagraphs == null)
{
return;
}
foreach(XmlElement p in txbxParagraphs)
{
ReplaceInParagraph(p, search, replace, matchCase);
}
}
#endregion
#region SavePartToXml
///
/// Saves the part to XML.
///
/// The part.
/// The XML doc.
static void SavePartToXml(OpenXmlPart part, XmlDocument xmlDoc)
{
using(var partStream = part.GetStream())
{
using(var partXmlWriter = XmlWriter.Create(partStream))
{
xmlDoc.Save(partXmlWriter);
}
}
}
#endregion
#region HasTrackedRevisions
///
/// Determines whether [has tracked revisions] [the specified doc].
///
/// The doc.
///
/// true if [has tracked revisions] [the specified doc]; otherwise, false.
///
static bool HasTrackedRevisions(WordprocessingDocument doc)
{
if(PartHasTrackedRevisions(doc.MainDocumentPart.DocumentSettingsPart))
{
return true;
}
if(PartHasTrackedRevisions(doc.MainDocumentPart))
{
return true;
}
if(doc.MainDocumentPart.HeaderParts.Any(PartHasTrackedRevisions))
{
return true;
}
if(doc.MainDocumentPart.FooterParts.Any(PartHasTrackedRevisions))
{
return true;
}
if(doc.MainDocumentPart.EndnotesPart != null && PartHasTrackedRevisions(doc.MainDocumentPart.EndnotesPart))
{
return true;
}
return doc.MainDocumentPart.FootnotesPart != null && PartHasTrackedRevisions(doc.MainDocumentPart.FootnotesPart);
}
#endregion
#region PartHasTrackedRevisions
///
/// Parts the has tracked revisions.
///
/// The part.
///
static bool PartHasTrackedRevisions(OpenXmlPart part)
{
var doc = GetPartXmlDocument(part);
var nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("w", WordNamespace);
var descendants = doc.SelectNodes(XPathExpression, nsmgr);
return (descendants != null) ? descendants.Count > 0 : false;
}
#endregion
#region GetPartXmlDocument
///
/// Gets the part XML document.
///
/// The part.
///
static XmlDocument GetPartXmlDocument(OpenXmlPart part)
{
var xmlDoc = new XmlDocument();
using(var partStream = part.GetStream())
{
using(var partXmlReader = XmlReader.Create(partStream))
{
xmlDoc.Load(partXmlReader);
}
}
return xmlDoc;
}
#endregion
}
}