Index: xword/ContentFiltering/Html/CSSUtil.cs =================================================================== --- xword/ContentFiltering/Html/CSSUtil.cs (revision 22628) +++ xword/ContentFiltering/Html/CSSUtil.cs (working copy) @@ -19,9 +19,9 @@ /// A reference to an XmlDocument. public static void InlineCSS(ref XmlDocument xmlDoc) { - XmlNodeList styleNodes = xmlDoc.GetElementsByTagName("style"); + XmlNodeList allElements = xmlDoc.GetElementsByTagName("*"); - Hashtable identifiedCSSClassesAndIDs = ExtractCSSClassesAndIDs(styleNodes); + Hashtable identifiedCSSClassesAndIDs = ExtractCSSClassesAndIDs(ref xmlDoc); foreach (XmlNode element in allElements) { @@ -30,19 +30,32 @@ if (classAttribute != null) { - string cssClassName = classAttribute.Value; + char[] whiteSpace = { ' ' }; + string[] cssClassNames = ("" + classAttribute.Value).Split(whiteSpace, StringSplitOptions.RemoveEmptyEntries); - if (identifiedCSSClassesAndIDs.ContainsKey(cssClassName)) + foreach (string cssClassName in cssClassNames) { - XmlAttribute styleAttribute = null; - styleAttribute = element.Attributes["style"]; - if (styleAttribute == null) + if (identifiedCSSClassesAndIDs.ContainsKey(cssClassName)) { - styleAttribute = element.Attributes.Append(xmlDoc.CreateAttribute("style")); - styleAttribute.Value = ""; + XmlAttribute styleAttribute = null; + styleAttribute = element.Attributes["style"]; + if (styleAttribute == null) + { + styleAttribute = element.Attributes.Append(xmlDoc.CreateAttribute("style")); + styleAttribute.Value = ""; + } + styleAttribute.Value += identifiedCSSClassesAndIDs[cssClassName]; + + //remove inlined CSS class + classAttribute.Value = classAttribute.Value.Replace(cssClassName, "").Trim(); + + if (classAttribute.Value.Length < 1) + { + element.Attributes.Remove(classAttribute); + } } - styleAttribute.Value += identifiedCSSClassesAndIDs[cssClassName]; } + } if (idAttribute != null) @@ -64,12 +77,17 @@ } /// - /// Extracts the CSS classes and ids from the 'style' nodes. + /// Extracts and removes the CSS classes and ids from the 'style' nodes. + /// Deletes all the style nodes and creates a new one with unparsed CSS. /// - /// A list of style nodes from the document. + /// A reference to an XmlDocument. /// A hashtable with CSS classes names (and CSS ids names) and their properties. - private static Hashtable ExtractCSSClassesAndIDs(XmlNodeList styleNodes) + private static Hashtable ExtractCSSClassesAndIDs(ref XmlDocument xmlDoc) { + XmlNodeList styleNodes = xmlDoc.GetElementsByTagName("style"); + StringBuilder preservedCSS = new StringBuilder(); + + //extract CSS classes and ids Hashtable identifiedCSSClassesAndIDs = new Hashtable(); foreach (XmlNode styleNode in styleNodes) { @@ -90,19 +108,25 @@ //clean whitespaces (spaces, tabs, new lines) from CSS properites Regex whiteSpaceRegex = new Regex("\\s+", RegexOptions.Singleline | RegexOptions.Multiline); - properties = whiteSpaceRegex.Replace(properties, ""); + properties = whiteSpaceRegex.Replace(properties, " "); char[] comma = { ',' }; string[] cssNames = cssClass.Substring(0, firstBrace).Split(comma); foreach (string className in cssNames) { - string cname=className.Trim(); - //only if it's a CSS class name or CSS id, and does not have pseudoselectors - if ((cname.IndexOf('.') == 0 || cname.IndexOf('#') == 0) && cname.IndexOf(':')<0) + string cname = className.Trim(); + //only if it's a CSS class name or CSS id + //and it's not cascaded CSS + //and does not have pseudoselectors + if ((cname.IndexOf('.') == 0 || cname.IndexOf('#') == 0) && cname.IndexOf(':') < 0 && cname.IndexOf(' ') < 0) { //do not include the dot in the CSS class name or the pound in CSS id classesNames.Add(cname.Substring(1)); } + else //since we can not handle that CSS, preserve it + { + preservedCSS.Append(cssClass).Append("}").Append(Environment.NewLine); + } } foreach (string identifiedClassName in classesNames) @@ -112,13 +136,32 @@ { currentProperties = identifiedCSSClassesAndIDs[identifiedClassName].ToString(); } - + currentProperties += properties; identifiedCSSClassesAndIDs.Remove(identifiedClassName); identifiedCSSClassesAndIDs.Add(identifiedClassName, currentProperties); } } } + //remove style nodes + List styleNodesToRemove = new List(); + foreach (XmlNode styleNode in styleNodes) + { + styleNodesToRemove.Add(styleNode); + } + foreach (XmlNode styleNodeToRemove in styleNodesToRemove) + { + styleNodeToRemove.ParentNode.RemoveChild(styleNodeToRemove); + } + + //only one style node, with the preserved CSS + if (preservedCSS.ToString().Length > 0) + { + XmlNode remainingStyleNode = xmlDoc.CreateElement("style"); + remainingStyleNode.InnerText = preservedCSS.ToString().Trim(); + xmlDoc.GetElementsByTagName("head")[0].AppendChild(remainingStyleNode); + } + return identifiedCSSClassesAndIDs; } @@ -338,6 +381,6 @@ "text-shadow", "top", "vertical-align", "visibility", "white-space", "width", "word-break", "word-spacing", "z-index" }; - + } } Index: xword/ContentFiltering/Office/Word/Filters/LocalToWebStyleFilter.cs =================================================================== --- xword/ContentFiltering/Office/Word/Filters/LocalToWebStyleFilter.cs (revision 22628) +++ xword/ContentFiltering/Office/Word/Filters/LocalToWebStyleFilter.cs (working copy) @@ -45,7 +45,7 @@ //step1: inline CSS for existing CSS classes and ids, for better manipulation at step2 and step3 CSSUtil.InlineCSS(ref xmlDoc); - //step2: convert all inlined CSS to CSS classes + //step2: convert all inlined style to CSS classes //(including, but not limited to, those generated at step1) body = CSSUtil.ConvertInlineStylesToCssClasses(body, ref xmlDoc, ref counter, ref cssClasses); @@ -66,8 +66,18 @@ /// A reference to the XmlDocument. private void InsertCssClassesInHeader(ref XmlNode headNode, ref XmlDocument xmlDoc) { - XmlNode styleNode = xmlDoc.CreateNode(XmlNodeType.Element, "style", xmlDoc.NamespaceURI); + XmlNode styleNode = null; + if (xmlDoc.GetElementsByTagName("style") != null) + { + styleNode = xmlDoc.GetElementsByTagName("style")[0]; + } + if (styleNode == null) + { + styleNode = xmlDoc.CreateNode(XmlNodeType.Element, "style", xmlDoc.NamespaceURI); + headNode.AppendChild(styleNode); + } + string value = ""; foreach (Object key in cssClasses.Keys) Index: xword/ContentFiltering/StyleSheetExtensions/SSXManager.cs =================================================================== --- xword/ContentFiltering/StyleSheetExtensions/SSXManager.cs (revision 22628) +++ xword/ContentFiltering/StyleSheetExtensions/SSXManager.cs (working copy) @@ -129,7 +129,7 @@ /// /// Adds to server SSX objects for the current page. /// - public void AddStyleSheetExtensions() + public void UploadStyleSheetExtensions() { IXWikiClient client = pageConverter.XWikiClient; int i = 0; Index: xword/ContentFiltering/Test/Office/Word/Filters/LocalToWebStyleFilterTest.cs =================================================================== --- xword/ContentFiltering/Test/Office/Word/Filters/LocalToWebStyleFilterTest.cs (revision 22628) +++ xword/ContentFiltering/Test/Office/Word/Filters/LocalToWebStyleFilterTest.cs (working copy) @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using NUnit.Framework; using System.Xml; using ContentFiltering.Test.Util; @@ -20,6 +19,7 @@ private string initialHTML; private string expectedHTML; private XmlDocument initialXmlDoc; + private XmlDocument expectedXmlDoc; /// /// Default constructor. @@ -30,6 +30,7 @@ initialHTML = ""; expectedHTML = ""; initialXmlDoc = new XmlDocument(); + expectedXmlDoc = new XmlDocument(); } /// @@ -38,32 +39,50 @@ [TestFixtureSetUp] public void TestSetup() { - initialHTML = "" - + "

Some content

" + initialHTML = "" + + "" + + "" + + "" + + "

Verdana content

" + "

some code

" - + "

More content

" + + "

More verdana content

" + "

more code

" + + "

errors text

" + ""; + initialXmlDoc.LoadXml(initialHTML); - expectedHTML = "" + expectedHTML = "" + + "" + "" - + "

Some content

" + + "

Verdana content

" + "

some code

" - + "

More content

" + + "

More verdana content

" + "

more code

" + + "

errors text

" + ""; - initialXmlDoc.LoadXml(initialHTML); + expectedXmlDoc.LoadXml(expectedHTML); } /// /// Tests the LocalToWebStyle filter: - /// - No inline styles. - /// - Only 'xoffice[0-9]+' CSS classes. - /// - Exactly 4 'xoffice[0-9]+' CSS classes, grouped in 2 parts (optimized). + /// Only one 'style' node; + /// No inline styles; + /// Only 'xoffice[0-9]+' CSS classes. /// [Test] public void TestLocalToWebStyleFilter() @@ -73,6 +92,9 @@ new LocalToWebStyleFilter(manager).Filter(ref initialXmlDoc); + initialXmlDoc.Normalize(); + expectedXmlDoc.Normalize(); + XmlNodeList allNodes = initialXmlDoc.GetElementsByTagName("*"); foreach (XmlNode node in allNodes) @@ -101,16 +123,30 @@ } } + Assert.IsFalse(foundInlineStyles); + Assert.IsFalse(foundNonXOfficeClasses); + + XmlNodeList totalStyleNodes = initialXmlDoc.GetElementsByTagName("style"); + Assert.IsNotNull(totalStyleNodes); + Assert.IsTrue(totalStyleNodes.Count == 1); + XmlNode styleNode = initialXmlDoc.GetElementsByTagName("style")[0]; + Assert.IsNotNull(styleNode); + string cssContent = ExtractStyleContent(styleNode); - Assert.IsFalse(foundInlineStyles); - Assert.IsFalse(foundNonXOfficeClasses); - Assert.IsNotNull(styleNode); - Assert.IsTrue(CountCSSClasses(cssContent) == 4); - Assert.IsTrue(OptimizedCSSClasses(cssContent)); + int cssClassesCount = CountCSSClasses(cssContent); + + Assert.IsTrue(cssClassesCount == 5); + //Assert.IsTrue(OptimizedCSSClasses(cssContent)); + Assert.IsTrue(XmlDocComparator.AreIdentical(initialXmlDoc, expectedXmlDoc)); } + /// + /// Extracts the CSS content from a style node. + /// + /// A style XmlNode. + /// The string representing the CSS content. private string ExtractStyleContent(XmlNode styleNode) { string cssContent = ""; @@ -125,27 +161,30 @@ } /// - /// Counts the CSS classes from a CSS content. + /// Counts the 'xoffice' CSS classes from a CSS content. /// /// The CSS content. - /// Number of CSS classes found. + /// Number of 'xoffice' CSS classes found. private int CountCSSClasses(string cssContent) { int count = 0; int startIndex = 0; - while (startIndex != -1) + do { - while (startIndex >= 0) + startIndex = cssContent.IndexOf(".xoffice", startIndex); + //if found, search from the next position + if (startIndex >= 0) { - startIndex = cssContent.IndexOf(".xoffice", startIndex); + startIndex++; count++; } - } + + } while (startIndex >= 0); return count; } /// - /// Verifies the CSS content four classes grouped in 2 parts. + /// Verifies the CSS content has five classes grouped in 3 parts. /// /// The CSS content. /// TRUE if CSS seems to be optimized. @@ -154,9 +193,9 @@ bool foundOptimizedCSS = false; char[] separator = new char[] { '}' }; string[] groups = cssContent.Split(separator, StringSplitOptions.RemoveEmptyEntries); - if (groups.Length == 2) + if (groups.Length == 3) { - foundOptimizedCSS = (CountCSSClasses(groups[0]) == 2) && (CountCSSClasses(groups[1]) == 2); + foundOptimizedCSS = (CountCSSClasses(groups[0]) + CountCSSClasses(groups[1]) + CountCSSClasses(groups[2])) == 5; } return foundOptimizedCSS; } Index: xword/ContentFiltering/Test/Office/Word/Filters/WebToLocalStyleFilterTest.cs =================================================================== --- xword/ContentFiltering/Test/Office/Word/Filters/WebToLocalStyleFilterTest.cs (revision 22628) +++ xword/ContentFiltering/Test/Office/Word/Filters/WebToLocalStyleFilterTest.cs (working copy) @@ -51,19 +51,11 @@ + ""; - expectedHTML = "TITLE" - - //the 'style' node should be inserted in the head section - + "" + expectedHTML = "TITLE" + "" - - //the CSS should be inlined - + "

Text0

" + + //the CSS should be inlined, the classes for inlined CSS should be removed + + "

Text0

" + "

Text1

" + "" Index: xword/ContentFiltering/Test/Util/XmlDocComparator.cs =================================================================== --- xword/ContentFiltering/Test/Util/XmlDocComparator.cs (revision 22628) +++ xword/ContentFiltering/Test/Util/XmlDocComparator.cs (working copy) @@ -4,6 +4,7 @@ using System.Text; using System.Xml; using System.Collections; +using System.Text.RegularExpressions; namespace ContentFiltering.Test.Util { @@ -14,12 +15,16 @@ /// Returns TRUE if the xml dcouments have the same nodes, in the same position with the exact attributes. ///
/// True if the xml dcouments have the same nodes, in the same position with the exact attributes. - public static bool AreIdentical(XmlDocument xmlDoc1,XmlDocument xmlDoc2) + public static bool AreIdentical(XmlDocument xmlDoc1, XmlDocument xmlDoc2) { + //normalize the documents to avoid adjacent XmlText nodes. + xmlDoc1.Normalize(); + xmlDoc2.Normalize(); + XmlNodeList nodeList1 = xmlDoc1.ChildNodes; XmlNodeList nodeList2 = xmlDoc2.ChildNodes; bool same = true; - + if (nodeList1.Count != nodeList2.Count) { return false; @@ -37,7 +42,7 @@ private static bool CompareNodes(XmlNode node1, XmlNode node2) { //compare properties - if (node1.Attributes == null||node2.Attributes==null) + if (node1.Attributes == null || node2.Attributes == null) { bool nullAttributes = (node1.Attributes == null && node2.Attributes == null); if (!nullAttributes) @@ -73,7 +78,17 @@ return false; } - if ((""+node1.Value).Trim() != (""+node2.Value).Trim()) + //the content may have extra spaces or new lines + + string value1 = ("" + node1.Value).Trim().Replace(Environment.NewLine, ""); + string value2 = ("" + node2.Value).Trim().Replace(Environment.NewLine, ""); + + //replace consecutive whitespaces with one space + Regex whiteSpaces = new Regex("\\s+", RegexOptions.Singleline | RegexOptions.Multiline); + value1 = whiteSpaces.Replace(value1, " "); + value2 = whiteSpaces.Replace(value2, " "); + + if (value1 != value2) { Console.WriteLine("Nodes value: " + node1.Value + "!=" + node2.Value); return false; Index: xword/XWord/AddinActions.cs =================================================================== --- xword/XWord/AddinActions.cs (revision 22628) +++ xword/XWord/AddinActions.cs (working copy) @@ -18,6 +18,7 @@ using XWord.VstoExtensions; using XWiki.Logging; using ContentFiltering.Office.Word.Cleaners; +using ContentFiltering.StyleSheetExtensions; namespace XWord { @@ -358,8 +359,10 @@ /// The full name of the wiki page. /// The contant to be saved. /// The wiki syntax of the saved page. - private void SavePage(String pageName, ref String pageContent, String syntax) + /// TRUE if the page was saved successfully. + private bool SavePage(String pageName, ref String pageContent, String syntax) { + bool saveSucceeded = false; SaveGrammarAndSpellingSettings(); DisableGrammarAndSpellingChecking(); @@ -373,9 +376,11 @@ { Log.Error("Failed to save page " + pageName + "on server " + addin.serverURL); UserNotifier.Error("There was an error on the server when trying to save the page"); + saveSucceeded = false; } else { + saveSucceeded = true; //mark the page from wiki structure as published bool markedDone = false; foreach (Space sp in addin.wiki.spaces) @@ -398,6 +403,8 @@ } RestoreGrammarAndSpellingSettings(); + + return saveSucceeded; } /// @@ -487,6 +494,9 @@ addin.currentPageFullName, Path.GetFileName(contentFilePath), addin.Client); } cleanHTML = pageConverter.ConvertFromWordToWeb(cleanHTML); + + SSXManager ssxManager = SSXManager.BuildFromLocalHTML(pageConverter, cleanHTML); + cleanHTML = new BodyContentExtractor().Clean(cleanHTML); //openHTMLDocument(addin.currentLocalFilePath); @@ -502,7 +512,11 @@ byte[] wikiContent = null; wikiContent = Encoding.Convert(Encoding.Unicode, iso, content); cleanHTML = iso.GetString(wikiContent); - SavePage(addin.currentPageFullName, ref cleanHTML, addin.AddinStatus.Syntax); + + if (SavePage(addin.currentPageFullName, ref cleanHTML, addin.AddinStatus.Syntax)) + { + ssxManager.UploadStyleSheetExtensions(); + } } catch (COMException ex) {