Index: xword/ContentFiltering/ContentFiltering.csproj =================================================================== --- xword/ContentFiltering/ContentFiltering.csproj (revision 22315) +++ xword/ContentFiltering/ContentFiltering.csproj (working copy) @@ -61,6 +61,7 @@ + @@ -98,6 +99,7 @@ + Index: xword/ContentFiltering/Html/CSSUtil.cs =================================================================== --- xword/ContentFiltering/Html/CSSUtil.cs (revision 0) +++ xword/ContentFiltering/Html/CSSUtil.cs (revision 0) @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; +using System.Collections; + +namespace ContentFiltering.Html +{ + /// + /// Provides static methods for working with CSS. + /// + public class CSSUtil + { + /// + /// Inlines CSS for nodes with an id and/or class attribute. + /// + /// 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); + + foreach (XmlNode element in allElements) + { + XmlAttribute classAttribute = element.Attributes["class"]; + XmlAttribute idAttribute = element.Attributes["id"]; + + if (classAttribute != null) + { + string cssClassName = classAttribute.Value; + + if (identifiedCSSClassesAndIDs.ContainsKey(cssClassName)) + { + XmlAttribute styleAttribute = null; + styleAttribute = element.Attributes["style"]; + if (styleAttribute == null) + { + styleAttribute = element.Attributes.Append(xmlDoc.CreateAttribute("style")); + styleAttribute.Value = ""; + } + styleAttribute.Value += identifiedCSSClassesAndIDs[cssClassName]; + } + } + + if (idAttribute != null) + { + string cssIdName = idAttribute.Value; + if (identifiedCSSClassesAndIDs.ContainsKey(cssIdName)) + { + XmlAttribute styleAttribute = null; + styleAttribute = element.Attributes["style"]; + if (styleAttribute == null) + { + styleAttribute = element.Attributes.Append(xmlDoc.CreateAttribute("style")); + styleAttribute.Value = ""; + } + styleAttribute.Value += identifiedCSSClassesAndIDs[cssIdName]; + } + } + + } + } + + + /// + /// Extracts the CSS classes and ids from the 'style' nodes. + /// + /// A list of style nodes from the document. + /// A hashtable with CSS classes names (and CSS ids names) and their properties. + private static Hashtable ExtractCSSClassesAndIDs(XmlNodeList styleNodes) + { + Hashtable identifiedCSSClassesAndIDs = new Hashtable(); + foreach (XmlNode styleNode in styleNodes) + { + char[] separator = { '}' }; + string[] css = styleNode.InnerText.Split(separator, StringSplitOptions.RemoveEmptyEntries); + foreach (string cssClass in css) + { + //several CSS classes can have same properties + string properties = ""; + List classesNames = new List(); + + int firstBrace = cssClass.IndexOf('{'); + if (firstBrace < 0) + { + continue; + } + properties = cssClass.Substring(firstBrace + 1).Replace('"', '\''); + char[] comma = { ',' }; + string[] cssNames = cssClass.Substring(0, firstBrace).Split(comma); + foreach (string className in cssNames) + { + //do not include the dot in the CSS class name or the pound in CSS id + classesNames.Add(className.Trim().Substring(1)); + } + + foreach (string identifiedClassName in classesNames) + { + identifiedCSSClassesAndIDs.Add(identifiedClassName, properties); + } + } + } + return identifiedCSSClassesAndIDs; + } + + + + } +} Index: xword/ContentFiltering/Test/Html/CSSUtilTest.cs =================================================================== --- xword/ContentFiltering/Test/Html/CSSUtilTest.cs (revision 0) +++ xword/ContentFiltering/Test/Html/CSSUtilTest.cs (revision 0) @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using System.Xml; +using ContentFiltering.Html; +using ContentFiltering.Test.Util; + +namespace ContentFiltering.Test.Html +{ + /// + /// Test class for CSSUtil. + /// + [TestFixture] + public class CSSUtilTest + { + private string initialHTML; + private string expectedHTML; + private XmlDocument initialXmlDoc; + private XmlDocument expectedXmlDoc; + + /// + /// Default constructor. + /// + public CSSUtilTest() + { + initialHTML = ""; + expectedHTML = ""; + initialXmlDoc = new XmlDocument(); + expectedXmlDoc = new XmlDocument(); + } + + /// + /// Global test setup. + /// + [TestFixtureSetUp] + public void GlobalSetup() + { + initialHTML = ""; + expectedHTML = ""; + initialXmlDoc = new XmlDocument(); + expectedXmlDoc = new XmlDocument(); + } + + /// + /// Tests the InlineCSS method. + /// + [Test] + public void TestInlineCSS() + { + initialHTML = "TITLE" + + "

HEADER 1

" + + "

Text 1

" + + "

Text 2

" + + "

Text 3 Text 4 Text 5

" + + ""; + + expectedHTML = "TITLE" + + "

HEADER 1

" + + "

Text 1

" + + "

Text 2

" + + "

Text 3 Text 4 Text 5

" + + ""; + + initialXmlDoc.LoadXml(initialHTML); + expectedXmlDoc.LoadXml(expectedHTML); + + CSSUtil.InlineCSS(ref initialXmlDoc); + Assert.IsTrue(XmlDocComparator.AreIdentical(initialXmlDoc, expectedXmlDoc)); + } + + /// + /// Performs a stress test for InlineCSS method: 15000 CSS classes and 15000 CSS ids for 30000 nodes. + /// + [Test] + public void StressTestInlineCSS() + { + initialHTML = "TITLE" + + Environment.NewLine + + "" + + Environment.NewLine + + "
{1}
" + + ""; + initialHTML = String.Format(initialHTML, GenerateStressTestStyle(), GenerateInitialElementsForStressTest()); + + initialXmlDoc = new XmlDocument(); + initialXmlDoc.LoadXml(initialHTML); + + long startTicks = DateTime.Now.Ticks; + CSSUtil.InlineCSS(ref initialXmlDoc); + long stopTicks = DateTime.Now.Ticks; + + //inline operation duration, in miliseconds + double inlineTimeMS = (1.0*stopTicks - startTicks) / 10000; + + + //inline operation should take less than 500 miliseconds + Assert.IsTrue(inlineTimeMS < 500.00); + } + + /// + /// Generates 15000 CSS classes and 15000 CSS ids for the stress test. + /// + /// The CSS to be inserted in the 'style' section. + private string GenerateStressTestStyle() + { + StringBuilder sb = new StringBuilder(); + for (int i = 1; i <= 15000; i++) + { + int r = i % 50; + if (r == 0) + { + sb.Append("{font-size:12px;color:#303030;}"); + sb.Append(Environment.NewLine); + } + else + { + sb.Append(".cssClass").Append(i); + if (r != 49) + { + sb.Append(", "); + } + } + } + sb.Append(Environment.NewLine); + + for (int i = 1; i <= 15000; i++) + { + int r = i % 50; + + if (r == 0) + { + sb.Append("{padding:4px;background-color:#FEFEFE;}"); + sb.Append(Environment.NewLine); + } + else + { + sb.Append("#cssID").Append(i); + if (r != 49) + { + sb.Append(", "); + } + } + } + return sb.ToString(); + } + + /// + /// Generates a string contaning 30000 HTML paragraphs for the stress test. + /// + /// The HTML to be inserted in the 'body' section. + private string GenerateInitialElementsForStressTest() + { + StringBuilder sb = new StringBuilder(); + for (int i = 1; i <= 15000; i++) + { + sb.Append("

Text with CSS CLASS ").Append(i).Append("

"); + sb.Append("

Text with CSS ID ").Append(i).Append("

"); + } + return sb.ToString(); + } + + + } +}