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 } }