// // Copyright (c) Microsoft. All rights reserved. // // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Xml; namespace HtmlToXamlDemo { internal class CssStylesheet { private List _styleDefinitions; // Constructor public CssStylesheet(XmlElement htmlElement) { if (htmlElement != null) { DiscoverStyleDefinitions(htmlElement); } } // Recursively traverses an html tree, discovers STYLE elements and creates a style definition table // for further cascading style application public void DiscoverStyleDefinitions(XmlElement htmlElement) { if (htmlElement.LocalName.ToLower() == "link") { return; // Add LINK elements processing for included stylesheets // } if (htmlElement.LocalName.ToLower() != "style") { // This is not a STYLE element. Recurse into it for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { if (htmlChildNode is XmlElement) { DiscoverStyleDefinitions((XmlElement) htmlChildNode); } } return; } // Add style definitions from this style. // Collect all text from this style definition var stylesheetBuffer = new StringBuilder(); for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { if (htmlChildNode is XmlText || htmlChildNode is XmlComment) { stylesheetBuffer.Append(RemoveComments(htmlChildNode.Value)); } } // CssStylesheet has the following syntactical structure: // @import declaration; // selector { definition } // where "selector" is one of: ".classname", "tagname" // It can contain comments in the following form: /*...*/ var nextCharacterIndex = 0; while (nextCharacterIndex < stylesheetBuffer.Length) { // Extract selector var selectorStart = nextCharacterIndex; while (nextCharacterIndex < stylesheetBuffer.Length && stylesheetBuffer[nextCharacterIndex] != '{') { // Skip declaration directive starting from @ if (stylesheetBuffer[nextCharacterIndex] == '@') { while (nextCharacterIndex < stylesheetBuffer.Length && stylesheetBuffer[nextCharacterIndex] != ';') { nextCharacterIndex++; } selectorStart = nextCharacterIndex + 1; } nextCharacterIndex++; } if (nextCharacterIndex < stylesheetBuffer.Length) { // Extract definition var definitionStart = nextCharacterIndex; while (nextCharacterIndex < stylesheetBuffer.Length && stylesheetBuffer[nextCharacterIndex] != '}') { nextCharacterIndex++; } // Define a style if (nextCharacterIndex - definitionStart > 2) { AddStyleDefinition( stylesheetBuffer.ToString(selectorStart, definitionStart - selectorStart), stylesheetBuffer.ToString(definitionStart + 1, nextCharacterIndex - definitionStart - 2)); } // Skip closing brace if (nextCharacterIndex < stylesheetBuffer.Length) { Debug.Assert(stylesheetBuffer[nextCharacterIndex] == '}'); nextCharacterIndex++; } } } } // Returns a string with all c-style comments replaced by spaces private string RemoveComments(string text) { var commentStart = text.IndexOf("/*", StringComparison.Ordinal); if (commentStart < 0) { return text; } var commentEnd = text.IndexOf("*/", commentStart + 2, StringComparison.Ordinal); if (commentEnd < 0) { return text.Substring(0, commentStart); } return text.Substring(0, commentStart) + " " + RemoveComments(text.Substring(commentEnd + 2)); } public void AddStyleDefinition(string selector, string definition) { // Notrmalize parameter values selector = selector.Trim().ToLower(); definition = definition.Trim().ToLower(); if (selector.Length == 0 || definition.Length == 0) { return; } if (_styleDefinitions == null) { _styleDefinitions = new List(); } var simpleSelectors = selector.Split(','); foreach (string t in simpleSelectors) { var simpleSelector = t.Trim(); if (simpleSelector.Length > 0) { _styleDefinitions.Add(new StyleDefinition(simpleSelector, definition)); } } } public string GetStyle(string elementName, List sourceContext) { Debug.Assert(sourceContext.Count > 0); Debug.Assert(elementName == sourceContext[sourceContext.Count - 1].LocalName); // Add id processing for style selectors if (_styleDefinitions != null) { for (var i = _styleDefinitions.Count - 1; i >= 0; i--) { var selector = _styleDefinitions[i].Selector; var selectorLevels = selector.Split(' '); var indexInSelector = selectorLevels.Length - 1; var indexInContext = sourceContext.Count - 1; var selectorLevel = selectorLevels[indexInSelector].Trim(); if (MatchSelectorLevel(selectorLevel, sourceContext[sourceContext.Count - 1])) { return _styleDefinitions[i].Definition; } } } return null; } private bool MatchSelectorLevel(string selectorLevel, XmlElement xmlElement) { if (selectorLevel.Length == 0) { return false; } var indexOfDot = selectorLevel.IndexOf('.'); var indexOfPound = selectorLevel.IndexOf('#'); string selectorClass = null; string selectorId = null; string selectorTag = null; if (indexOfDot >= 0) { if (indexOfDot > 0) { selectorTag = selectorLevel.Substring(0, indexOfDot); } selectorClass = selectorLevel.Substring(indexOfDot + 1); } else if (indexOfPound >= 0) { if (indexOfPound > 0) { selectorTag = selectorLevel.Substring(0, indexOfPound); } selectorId = selectorLevel.Substring(indexOfPound + 1); } else { selectorTag = selectorLevel; } if (selectorTag != null && selectorTag != xmlElement.LocalName) { return false; } if (selectorId != null && HtmlToXamlConverter.GetAttribute(xmlElement, "id") != selectorId) { return false; } if (selectorClass != null && HtmlToXamlConverter.GetAttribute(xmlElement, "class") != selectorClass) { return false; } return true; } private class StyleDefinition { public readonly string Definition; public readonly string Selector; public StyleDefinition(string selector, string definition) { Selector = selector; Definition = definition; } } } }