// // 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; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Windows; using System.Windows.Documents; using System.Xml; namespace HtmlToXamlDemo { // DependencyProperty // TextElement /// /// HtmlToXamlConverter is a static class that takes an HTML string /// and converts it into XAML /// public static class HtmlToXamlConverter { // ---------------------------------------------------------------- // // Internal Constants // // ---------------------------------------------------------------- // The constants reprtesent all Xaml names used in a conversion public const string XamlFlowDocument = "FlowDocument"; public const string XamlRun = "Run"; public const string XamlSpan = "Span"; public const string XamlHyperlink = "Hyperlink"; public const string XamlHyperlinkNavigateUri = "NavigateUri"; public const string XamlHyperlinkTargetName = "TargetName"; public const string XamlSection = "Section"; public const string XamlList = "List"; public const string XamlListMarkerStyle = "MarkerStyle"; public const string XamlListMarkerStyleNone = "None"; public const string XamlListMarkerStyleDecimal = "Decimal"; public const string XamlListMarkerStyleDisc = "Disc"; public const string XamlListMarkerStyleCircle = "Circle"; public const string XamlListMarkerStyleSquare = "Square"; public const string XamlListMarkerStyleBox = "Box"; public const string XamlListMarkerStyleLowerLatin = "LowerLatin"; public const string XamlListMarkerStyleUpperLatin = "UpperLatin"; public const string XamlListMarkerStyleLowerRoman = "LowerRoman"; public const string XamlListMarkerStyleUpperRoman = "UpperRoman"; public const string XamlListItem = "ListItem"; public const string XamlLineBreak = "LineBreak"; public const string XamlParagraph = "Paragraph"; public const string XamlMargin = "Margin"; public const string XamlPadding = "Padding"; public const string XamlBorderBrush = "BorderBrush"; public const string XamlBorderThickness = "BorderThickness"; public const string XamlTable = "Table"; // flowdocument table requires this element, take Table prefix because XMLReader cannot resolve the namespace of this element public const string XamlTableColumnGroup = "Table.Columns"; public const string XamlTableColumn = "TableColumn"; public const string XamlTableRowGroup = "TableRowGroup"; public const string XamlTableRow = "TableRow"; public const string XamlTableCell = "TableCell"; public const string XamlTableCellBorderThickness = "BorderThickness"; public const string XamlTableCellBorderBrush = "BorderBrush"; public const string XamlTableCellColumnSpan = "ColumnSpan"; public const string XamlTableCellRowSpan = "RowSpan"; public const string XamlWidth = "Width"; public const string XamlBrushesBlack = "Black"; public const string XamlFontFamily = "FontFamily"; public const string XamlFontSize = "FontSize"; public const string XamlFontSizeXxLarge = "22pt"; // "XXLarge"; public const string XamlFontSizeXLarge = "20pt"; // "XLarge"; public const string XamlFontSizeLarge = "18pt"; // "Large"; public const string XamlFontSizeMedium = "16pt"; // "Medium"; public const string XamlFontSizeSmall = "12pt"; // "Small"; public const string XamlFontSizeXSmall = "10pt"; // "XSmall"; public const string XamlFontSizeXxSmall = "8pt"; // "XXSmall"; public const string XamlFontWeight = "FontWeight"; public const string XamlFontWeightBold = "Bold"; public const string XamlFontStyle = "FontStyle"; public const string XamlForeground = "Foreground"; public const string XamlBackground = "Background"; public const string XamlTextDecorations = "TextDecorations"; public const string XamlTextDecorationsUnderline = "Underline"; public const string XamlTextIndent = "TextIndent"; public const string XamlTextAlignment = "TextAlignment"; // --------------------------------------------------------------------- // // Private Fields // // --------------------------------------------------------------------- #region Private Fields private static readonly string XamlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; #endregion Private Fields // --------------------------------------------------------------------- // // Internal Methods // // --------------------------------------------------------------------- #region Internal Methods /// /// Converts an html string into xaml string. /// /// /// Input html which may be badly formated xml. /// /// /// true indicates that we need a FlowDocument as a root element; /// false means that Section or Span elements will be used /// dependeing on StartFragment/EndFragment comments locations. /// /// /// Well-formed xml representing XAML equivalent for the input html string. /// public static string ConvertHtmlToXaml(string htmlString, bool asFlowDocument) { // Create well-formed Xml from Html string var htmlElement = HtmlParser.ParseHtml(htmlString); // Decide what name to use as a root var rootElementName = asFlowDocument ? XamlFlowDocument : XamlSection; // Create an XmlDocument for generated xaml var xamlTree = new XmlDocument(); var xamlFlowDocumentElement = xamlTree.CreateElement(null, rootElementName, XamlNamespace); // Extract style definitions from all STYLE elements in the document var stylesheet = new CssStylesheet(htmlElement); // Source context is a stack of all elements - ancestors of a parentElement var sourceContext = new List(10); // Clear fragment parent _inlineFragmentParentElement = null; // convert root html element AddBlock(xamlFlowDocumentElement, htmlElement, new Hashtable(), stylesheet, sourceContext); // In case if the selected fragment is inline, extract it into a separate Span wrapper if (!asFlowDocument) { xamlFlowDocumentElement = ExtractInlineFragment(xamlFlowDocumentElement); } // Return a string representing resulting Xaml xamlFlowDocumentElement.SetAttribute("xml:space", "preserve"); var xaml = xamlFlowDocumentElement.OuterXml; return xaml; } /// /// Returns a value for an attribute by its name (ignoring casing) /// /// /// XmlElement in which we are trying to find the specified attribute /// /// /// String representing the attribute name to be searched for /// /// public static string GetAttribute(XmlElement element, string attributeName) { attributeName = attributeName.ToLower(); for (var i = 0; i < element.Attributes.Count; i++) { if (element.Attributes[i].Name.ToLower() == attributeName) { return element.Attributes[i].Value; } } return null; } /// /// Returns string extracted from quotation marks /// /// /// String representing value enclosed in quotation marks /// internal static string UnQuote(string value) { if (value.StartsWith("\"") && value.EndsWith("\"") || value.StartsWith("'") && value.EndsWith("'")) { value = value.Substring(1, value.Length - 2).Trim(); } return value; } #endregion Internal Methods // --------------------------------------------------------------------- // // Private Methods // // --------------------------------------------------------------------- #region Private Methods /// /// Analyzes the given htmlElement expecting it to be converted /// into some of xaml Block elements and adds the converted block /// to the children collection of xamlParentElement. /// Analyzes the given XmlElement htmlElement, recognizes it as some HTML element /// and adds it as a child to a xamlParentElement. /// In some cases several following siblings of the given htmlElement /// will be consumed too (e.g. LIs encountered without wrapping UL/OL, /// which must be collected together and wrapped into one implicit List element). /// /// /// Parent xaml element, to which new converted element will be added /// /// /// Source html element subject to convert to xaml. /// /// /// Properties inherited from an outer context. /// /// /// /// /// Last processed html node. Normally it should be the same htmlElement /// as was passed as a paramater, but in some irregular cases /// it could one of its following siblings. /// The caller must use this node to get to next sibling from it. /// private static XmlNode AddBlock(XmlElement xamlParentElement, XmlNode htmlNode, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { if (htmlNode is XmlComment) { DefineInlineFragmentParent((XmlComment) htmlNode, /*xamlParentElement:*/null); } else if (htmlNode is XmlText) { htmlNode = AddImplicitParagraph(xamlParentElement, htmlNode, inheritedProperties, stylesheet, sourceContext); } else if (htmlNode is XmlElement) { // Identify element name var htmlElement = (XmlElement) htmlNode; var htmlElementName = htmlElement.LocalName; // Keep the name case-sensitive to check xml names var htmlElementNamespace = htmlElement.NamespaceURI; if (htmlElementNamespace != HtmlParser.XhtmlNamespace) { // Non-html element. skip it // Isn't it too agressive? What if this is just an error in html tag name? // TODO: Consider skipping just a wparrer in recursing into the element tree, // which may produce some garbage though coming from xml fragments. return htmlElement; } // Put source element to the stack sourceContext.Add(htmlElement); // Convert the name to lowercase, because html elements are case-insensitive htmlElementName = htmlElementName.ToLower(); // Switch to an appropriate kind of processing depending on html element name switch (htmlElementName) { // Sections: case "html": case "body": case "div": case "form": // not a block according to xhtml spec case "pre": // Renders text in a fixed-width font case "blockquote": case "caption": case "center": case "cite": AddSection(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; // Paragraphs: case "p": case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": case "nsrtitle": case "textarea": case "dd": // ??? case "dl": // ??? case "dt": // ??? case "tt": // ??? AddParagraph(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "ol": case "ul": case "dir": // treat as UL element case "menu": // treat as UL element // List element conversion AddList(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "li": // LI outside of OL/UL // Collect all sibling LIs, wrap them into a List and then proceed with the element following the last of LIs htmlNode = AddOrphanListItems(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "img": // TODO: Add image processing AddImage(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "table": // hand off to table parsing function which will perform special table syntax checks AddTable(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "tbody": case "tfoot": case "thead": case "tr": case "td": case "th": // Table stuff without table wrapper // TODO: add special-case processing here for elements that should be within tables when the // parent element is NOT a table. If the parent element is a table they can be processed normally. // we need to compare against the parent element here, we can't just break on a switch goto default; // Thus we will skip this element as unknown, but still recurse into it. case "style": // We already pre-processed all style elements. Ignore it now case "meta": case "head": case "title": case "script": // Ignore these elements break; default: // Wrap a sequence of inlines into an implicit paragraph htmlNode = AddImplicitParagraph(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; } // Remove the element from the stack Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlElement); sourceContext.RemoveAt(sourceContext.Count - 1); } // Return last processed node return htmlNode; } // ............................................................. // // Line Breaks // // ............................................................. private static void AddBreak(XmlElement xamlParentElement, string htmlElementName) { // Create new xaml element corresponding to this html element var xamlLineBreak = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlLineBreak, XamlNamespace); xamlParentElement.AppendChild(xamlLineBreak); if (htmlElementName == "hr") { var xamlHorizontalLine = xamlParentElement.OwnerDocument.CreateTextNode("----------------------"); xamlParentElement.AppendChild(xamlHorizontalLine); xamlLineBreak = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlLineBreak, XamlNamespace); xamlParentElement.AppendChild(xamlLineBreak); } } // ............................................................. // // Text Flow Elements // // ............................................................. /// /// Generates Section or Paragraph element from DIV depending whether it contains any block elements or not /// /// /// XmlElement representing Xaml parent to which the converted element should be added /// /// /// XmlElement representing Html element to be converted /// /// /// properties inherited from parent context /// /// /// /// true indicates that a content added by this call contains at least one block element /// private static void AddSection(XmlElement xamlParentElement, XmlElement htmlElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Analyze the content of htmlElement to decide what xaml element to choose - Section or Paragraph. // If this Div has at least one block child then we need to use Section, otherwise use Paragraph var htmlElementContainsBlocks = false; for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { if (htmlChildNode is XmlElement) { var htmlChildName = ((XmlElement) htmlChildNode).LocalName.ToLower(); if (HtmlSchema.IsBlockElement(htmlChildName)) { htmlElementContainsBlocks = true; break; } } } if (!htmlElementContainsBlocks) { // The Div does not contain any block elements, so we can treat it as a Paragraph AddParagraph(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); } else { // The Div has some nested blocks, so we treat it as a Section // Create currentProperties as a compilation of local and inheritedProperties, set localProperties Hashtable localProperties; var currentProperties = GetElementProperties(htmlElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // Create a XAML element corresponding to this html element var xamlElement = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlSection, XamlNamespace); ApplyLocalProperties(xamlElement, localProperties, /*isBlock:*/true); // Decide whether we can unwrap this element as not having any formatting significance. if (!xamlElement.HasAttributes) { // This elements is a group of block elements whitout any additional formatting. // We can add blocks directly to xamlParentElement and avoid // creating unnecessary Sections nesting. xamlElement = xamlParentElement; } // Recurse into element subtree for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode?.NextSibling) { htmlChildNode = AddBlock(xamlElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } // Add the new element to the parent. if (xamlElement != xamlParentElement) { xamlParentElement.AppendChild(xamlElement); } } } /// /// Generates Paragraph element from P, H1-H7, Center etc. /// /// /// XmlElement representing Xaml parent to which the converted element should be added /// /// /// XmlElement representing Html element to be converted /// /// /// properties inherited from parent context /// /// /// /// true indicates that a content added by this call contains at least one block element /// private static void AddParagraph(XmlElement xamlParentElement, XmlElement htmlElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Create currentProperties as a compilation of local and inheritedProperties, set localProperties Hashtable localProperties; var currentProperties = GetElementProperties(htmlElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // Create a XAML element corresponding to this html element var xamlElement = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlParagraph, XamlNamespace); ApplyLocalProperties(xamlElement, localProperties, /*isBlock:*/true); // Recurse into element subtree for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { AddInline(xamlElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } // Add the new element to the parent. xamlParentElement.AppendChild(xamlElement); } /// /// Creates a Paragraph element and adds all nodes starting from htmlNode /// converted to appropriate Inlines. /// /// /// XmlElement representing Xaml parent to which the converted element should be added /// /// /// XmlNode starting a collection of implicitly wrapped inlines. /// /// /// properties inherited from parent context /// /// /// /// true indicates that a content added by this call contains at least one block element /// /// /// The last htmlNode added to the implicit paragraph /// private static XmlNode AddImplicitParagraph(XmlElement xamlParentElement, XmlNode htmlNode, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Collect all non-block elements and wrap them into implicit Paragraph var xamlParagraph = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlParagraph, XamlNamespace); XmlNode lastNodeProcessed = null; while (htmlNode != null) { if (htmlNode is XmlComment) { DefineInlineFragmentParent((XmlComment) htmlNode, /*xamlParentElement:*/null); } else if (htmlNode is XmlText) { if (htmlNode.Value.Trim().Length > 0) { AddTextRun(xamlParagraph, htmlNode.Value); } } else if (htmlNode is XmlElement) { var htmlChildName = ((XmlElement) htmlNode).LocalName.ToLower(); if (HtmlSchema.IsBlockElement(htmlChildName)) { // The sequence of non-blocked inlines ended. Stop implicit loop here. break; } AddInline(xamlParagraph, (XmlElement) htmlNode, inheritedProperties, stylesheet, sourceContext); } // Store last processed node to return it at the end lastNodeProcessed = htmlNode; htmlNode = htmlNode.NextSibling; } // Add the Paragraph to the parent // If only whitespaces and commens have been encountered, // then we have nothing to add in implicit paragraph; forget it. if (xamlParagraph.FirstChild != null) { xamlParentElement.AppendChild(xamlParagraph); } // Need to return last processed node return lastNodeProcessed; } // ............................................................. // // Inline Elements // // ............................................................. private static void AddInline(XmlElement xamlParentElement, XmlNode htmlNode, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { if (htmlNode is XmlComment) { DefineInlineFragmentParent((XmlComment) htmlNode, xamlParentElement); } else if (htmlNode is XmlText) { AddTextRun(xamlParentElement, htmlNode.Value); } else if (htmlNode is XmlElement) { var htmlElement = (XmlElement) htmlNode; // Check whether this is an html element if (htmlElement.NamespaceURI != HtmlParser.XhtmlNamespace) { return; // Skip non-html elements } // Identify element name var htmlElementName = htmlElement.LocalName.ToLower(); // Put source element to the stack sourceContext.Add(htmlElement); switch (htmlElementName) { case "a": AddHyperlink(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "img": AddImage(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); break; case "br": case "hr": AddBreak(xamlParentElement, htmlElementName); break; default: if (HtmlSchema.IsInlineElement(htmlElementName) || HtmlSchema.IsBlockElement(htmlElementName)) { // Note: actually we do not expect block elements here, // but if it happens to be here, we will treat it as a Span. AddSpanOrRun(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); } break; } // Ignore all other elements non-(block/inline/image) // Remove the element from the stack Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlElement); sourceContext.RemoveAt(sourceContext.Count - 1); } } private static void AddSpanOrRun(XmlElement xamlParentElement, XmlElement htmlElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Decide what XAML element to use for this inline element. // Check whether it contains any nested inlines var elementHasChildren = false; for (var htmlNode = htmlElement.FirstChild; htmlNode != null; htmlNode = htmlNode.NextSibling) { if (htmlNode is XmlElement) { var htmlChildName = ((XmlElement) htmlNode).LocalName.ToLower(); if (HtmlSchema.IsInlineElement(htmlChildName) || HtmlSchema.IsBlockElement(htmlChildName) || htmlChildName == "img" || htmlChildName == "br" || htmlChildName == "hr") { elementHasChildren = true; break; } } } var xamlElementName = elementHasChildren ? XamlSpan : XamlRun; // Create currentProperties as a compilation of local and inheritedProperties, set localProperties Hashtable localProperties; var currentProperties = GetElementProperties(htmlElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // Create a XAML element corresponding to this html element var xamlElement = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/xamlElementName, XamlNamespace); ApplyLocalProperties(xamlElement, localProperties, /*isBlock:*/false); // Recurse into element subtree for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { AddInline(xamlElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } // Add the new element to the parent. xamlParentElement.AppendChild(xamlElement); } // Adds a text run to a xaml tree private static void AddTextRun(XmlElement xamlElement, string textData) { // Remove control characters for (var i = 0; i < textData.Length; i++) { if (char.IsControl(textData[i])) { textData = textData.Remove(i--, 1); // decrement i to compensate for character removal } } // Replace No-Breaks by spaces (160 is a code of   entity in html) // This is a work around since WPF/XAML does not support  . textData = textData.Replace((char) 160, ' '); if (textData.Length > 0) { xamlElement.AppendChild(xamlElement.OwnerDocument.CreateTextNode(textData)); } } private static void AddHyperlink(XmlElement xamlParentElement, XmlElement htmlElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Convert href attribute into NavigateUri and TargetName var href = GetAttribute(htmlElement, "href"); if (href == null) { // When href attribute is missing - ignore the hyperlink AddSpanOrRun(xamlParentElement, htmlElement, inheritedProperties, stylesheet, sourceContext); } else { // Create currentProperties as a compilation of local and inheritedProperties, set localProperties Hashtable localProperties; var currentProperties = GetElementProperties(htmlElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // Create a XAML element corresponding to this html element var xamlElement = xamlParentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlHyperlink, XamlNamespace); ApplyLocalProperties(xamlElement, localProperties, /*isBlock:*/false); var hrefParts = href.Split('#'); if (hrefParts.Length > 0 && hrefParts[0].Trim().Length > 0) { xamlElement.SetAttribute(XamlHyperlinkNavigateUri, hrefParts[0].Trim()); } if (hrefParts.Length == 2 && hrefParts[1].Trim().Length > 0) { xamlElement.SetAttribute(XamlHyperlinkTargetName, hrefParts[1].Trim()); } // Recurse into element subtree for (var htmlChildNode = htmlElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { AddInline(xamlElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } // Add the new element to the parent. xamlParentElement.AppendChild(xamlElement); } } // Stores a parent xaml element for the case when selected fragment is inline. private static XmlElement _inlineFragmentParentElement; // Called when html comment is encountered to store a parent element // for the case when the fragment is inline - to extract it to a separate // Span wrapper after the conversion. private static void DefineInlineFragmentParent(XmlComment htmlComment, XmlElement xamlParentElement) { if (htmlComment.Value == "StartFragment") { _inlineFragmentParentElement = xamlParentElement; } else if (htmlComment.Value == "EndFragment") { if (_inlineFragmentParentElement == null && xamlParentElement != null) { // Normally this cannot happen if comments produced by correct copying code // in Word or IE, but when it is produced manually then fragment boundary // markers can be inconsistent. In this case StartFragment takes precedence, // but if it is not set, then we get the value from EndFragment marker. _inlineFragmentParentElement = xamlParentElement; } } } // Extracts a content of an element stored as InlineFragmentParentElement // into a separate Span wrapper. // Note: when selected content does not cross paragraph boundaries, // the fragment is marked within private static XmlElement ExtractInlineFragment(XmlElement xamlFlowDocumentElement) { if (_inlineFragmentParentElement != null) { if (_inlineFragmentParentElement.LocalName == XamlSpan) { xamlFlowDocumentElement = _inlineFragmentParentElement; } else { xamlFlowDocumentElement = xamlFlowDocumentElement.OwnerDocument.CreateElement( /*prefix:*/ null, /*localName:*/XamlSpan, XamlNamespace); while (_inlineFragmentParentElement.FirstChild != null) { var copyNode = _inlineFragmentParentElement.FirstChild; _inlineFragmentParentElement.RemoveChild(copyNode); xamlFlowDocumentElement.AppendChild(copyNode); } } } return xamlFlowDocumentElement; } // ............................................................. // // Images // // ............................................................. private static void AddImage(XmlElement xamlParentElement, XmlElement htmlElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Implement images } // ............................................................. // // Lists // // ............................................................. /// /// Converts Html ul or ol element into Xaml list element. During conversion if the ul/ol element has any children /// that are not li elements, they are ignored and not added to the list element /// /// /// XmlElement representing Xaml parent to which the converted element should be added /// /// /// XmlElement representing Html ul/ol element to be converted /// /// /// properties inherited from parent context /// /// /// private static void AddList(XmlElement xamlParentElement, XmlElement htmlListElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { var htmlListElementName = htmlListElement.LocalName.ToLower(); Hashtable localProperties; var currentProperties = GetElementProperties(htmlListElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // Create Xaml List element var xamlListElement = xamlParentElement.OwnerDocument.CreateElement(null, XamlList, XamlNamespace); // Set default list markers xamlListElement.SetAttribute(XamlListMarkerStyle, htmlListElementName == "ol" ? XamlListMarkerStyleDecimal : XamlListMarkerStyleDisc); // Apply local properties to list to set marker attribute if specified // TODO: Should we have separate list attribute processing function? ApplyLocalProperties(xamlListElement, localProperties, /*isBlock:*/true); // Recurse into list subtree for (var htmlChildNode = htmlListElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { if (htmlChildNode is XmlElement && htmlChildNode.LocalName.ToLower() == "li") { sourceContext.Add((XmlElement) htmlChildNode); AddListItem(xamlListElement, (XmlElement) htmlChildNode, currentProperties, stylesheet, sourceContext); Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlChildNode); sourceContext.RemoveAt(sourceContext.Count - 1); } } // Add the List element to xaml tree - if it is not empty if (xamlListElement.HasChildNodes) { xamlParentElement.AppendChild(xamlListElement); } } /// /// If li items are found without a parent ul/ol element in Html string, creates xamlListElement as their parent and /// adds /// them to it. If the previously added node to the same xamlParentElement was a List, adds the elements to that list. /// Otherwise, we create a new xamlListElement and add them to it. Elements are added as long as li elements appear /// sequentially. /// The first non-li or text node stops the addition. /// /// /// Parent element for the list /// /// /// Start Html li element without parent list /// /// /// Properties inherited from parent context /// /// /// XmlNode representing the first non-li node in the input after one or more li's have been processed. /// private static XmlElement AddOrphanListItems(XmlElement xamlParentElement, XmlElement htmlLiElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { Debug.Assert(htmlLiElement.LocalName.ToLower() == "li"); XmlElement lastProcessedListItemElement = null; // Find out the last element attached to the xamlParentElement, which is the previous sibling of this node var xamlListItemElementPreviousSibling = xamlParentElement.LastChild; XmlElement xamlListElement; if (xamlListItemElementPreviousSibling != null && xamlListItemElementPreviousSibling.LocalName == XamlList) { // Previously added Xaml element was a list. We will add the new li to it xamlListElement = (XmlElement) xamlListItemElementPreviousSibling; } else { // No list element near. Create our own. xamlListElement = xamlParentElement.OwnerDocument.CreateElement(null, XamlList, XamlNamespace); xamlParentElement.AppendChild(xamlListElement); } XmlNode htmlChildNode = htmlLiElement; var htmlChildNodeName = htmlChildNode == null ? null : htmlChildNode.LocalName.ToLower(); // Current element properties missed here. //currentProperties = GetElementProperties(htmlLIElement, inheritedProperties, out localProperties, stylesheet); // Add li elements to the parent xamlListElement we created as long as they appear sequentially // Use properties inherited from xamlParentElement for context while (htmlChildNode != null && htmlChildNodeName == "li") { AddListItem(xamlListElement, (XmlElement) htmlChildNode, inheritedProperties, stylesheet, sourceContext); lastProcessedListItemElement = (XmlElement) htmlChildNode; htmlChildNode = htmlChildNode.NextSibling; htmlChildNodeName = htmlChildNode?.LocalName.ToLower(); } return lastProcessedListItemElement; } /// /// Converts htmlLIElement into Xaml ListItem element, and appends it to the parent xamlListElement /// /// /// XmlElement representing Xaml List element to which the converted td/th should be added /// /// /// XmlElement representing Html li element to be converted /// /// /// Properties inherited from parent context /// private static void AddListItem(XmlElement xamlListElement, XmlElement htmlLiElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Parameter validation Debug.Assert(xamlListElement != null); Debug.Assert(xamlListElement.LocalName == XamlList); Debug.Assert(htmlLiElement != null); Debug.Assert(htmlLiElement.LocalName.ToLower() == "li"); Debug.Assert(inheritedProperties != null); Hashtable localProperties; var currentProperties = GetElementProperties(htmlLiElement, inheritedProperties, out localProperties, stylesheet, sourceContext); var xamlListItemElement = xamlListElement.OwnerDocument.CreateElement(null, XamlListItem, XamlNamespace); // TODO: process local properties for li element // Process children of the ListItem for (var htmlChildNode = htmlLiElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode?.NextSibling) { htmlChildNode = AddBlock(xamlListItemElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } // Add resulting ListBoxItem to a xaml parent xamlListElement.AppendChild(xamlListItemElement); } // ............................................................. // // Tables // // ............................................................. /// /// Converts htmlTableElement to a Xaml Table element. Adds tbody elements if they are missing so /// that a resulting Xaml Table element is properly formed. /// /// /// Parent xaml element to which a converted table must be added. /// /// /// XmlElement reprsenting the Html table element to be converted /// /// /// Hashtable representing properties inherited from parent context. /// private static void AddTable(XmlElement xamlParentElement, XmlElement htmlTableElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { // Parameter validation Debug.Assert(htmlTableElement.LocalName.ToLower() == "table"); Debug.Assert(xamlParentElement != null); Debug.Assert(inheritedProperties != null); // Create current properties to be used by children as inherited properties, set local properties Hashtable localProperties; var currentProperties = GetElementProperties(htmlTableElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // TODO: process localProperties for tables to override defaults, decide cell spacing defaults // Check if the table contains only one cell - we want to take only its content var singleCell = GetCellFromSingleCellTable(htmlTableElement); if (singleCell != null) { // Need to push skipped table elements onto sourceContext sourceContext.Add(singleCell); // Add the cell's content directly to parent for (var htmlChildNode = singleCell.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode?.NextSibling) { htmlChildNode = AddBlock(xamlParentElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == singleCell); sourceContext.RemoveAt(sourceContext.Count - 1); } else { // Create xamlTableElement var xamlTableElement = xamlParentElement.OwnerDocument.CreateElement(null, XamlTable, XamlNamespace); // Analyze table structure for column widths and rowspan attributes var columnStarts = AnalyzeTableStructure(htmlTableElement, stylesheet); // Process COLGROUP & COL elements AddColumnInformation(htmlTableElement, xamlTableElement, columnStarts, currentProperties, stylesheet, sourceContext); // Process table body - TBODY and TR elements var htmlChildNode = htmlTableElement.FirstChild; while (htmlChildNode != null) { var htmlChildName = htmlChildNode.LocalName.ToLower(); // Process the element if (htmlChildName == "tbody" || htmlChildName == "thead" || htmlChildName == "tfoot") { // Add more special processing for TableHeader and TableFooter var xamlTableBodyElement = xamlTableElement.OwnerDocument.CreateElement(null, XamlTableRowGroup, XamlNamespace); xamlTableElement.AppendChild(xamlTableBodyElement); sourceContext.Add((XmlElement) htmlChildNode); // Get properties of Html tbody element Hashtable tbodyElementLocalProperties; var tbodyElementCurrentProperties = GetElementProperties((XmlElement) htmlChildNode, currentProperties, out tbodyElementLocalProperties, stylesheet, sourceContext); // TODO: apply local properties for tbody // Process children of htmlChildNode, which is tbody, for tr elements AddTableRowsToTableBody(xamlTableBodyElement, htmlChildNode.FirstChild, tbodyElementCurrentProperties, columnStarts, stylesheet, sourceContext); if (xamlTableBodyElement.HasChildNodes) { xamlTableElement.AppendChild(xamlTableBodyElement); // else: if there is no TRs in this TBody, we simply ignore it } Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlChildNode); sourceContext.RemoveAt(sourceContext.Count - 1); htmlChildNode = htmlChildNode.NextSibling; } else if (htmlChildName == "tr") { // Tbody is not present, but tr element is present. Tr is wrapped in tbody var xamlTableBodyElement = xamlTableElement.OwnerDocument.CreateElement(null, XamlTableRowGroup, XamlNamespace); // We use currentProperties of xamlTableElement when adding rows since the tbody element is artificially created and has // no properties of its own htmlChildNode = AddTableRowsToTableBody(xamlTableBodyElement, htmlChildNode, currentProperties, columnStarts, stylesheet, sourceContext); if (xamlTableBodyElement.HasChildNodes) { xamlTableElement.AppendChild(xamlTableBodyElement); } } else { // Element is not tbody or tr. Ignore it. // TODO: add processing for thead, tfoot elements and recovery for td elements htmlChildNode = htmlChildNode.NextSibling; } } if (xamlTableElement.HasChildNodes) { xamlParentElement.AppendChild(xamlTableElement); } } } private static XmlElement GetCellFromSingleCellTable(XmlElement htmlTableElement) { XmlElement singleCell = null; for (var tableChild = htmlTableElement.FirstChild; tableChild != null; tableChild = tableChild.NextSibling) { var elementName = tableChild.LocalName.ToLower(); if (elementName == "tbody" || elementName == "thead" || elementName == "tfoot") { if (singleCell != null) { return null; } for (var tbodyChild = tableChild.FirstChild; tbodyChild != null; tbodyChild = tbodyChild.NextSibling) { if (tbodyChild.LocalName.ToLower() == "tr") { if (singleCell != null) { return null; } for (var trChild = tbodyChild.FirstChild; trChild != null; trChild = trChild.NextSibling) { var cellName = trChild.LocalName.ToLower(); if (cellName == "td" || cellName == "th") { if (singleCell != null) { return null; } singleCell = (XmlElement) trChild; } } } } } else if (tableChild.LocalName.ToLower() == "tr") { if (singleCell != null) { return null; } for (var trChild = tableChild.FirstChild; trChild != null; trChild = trChild.NextSibling) { var cellName = trChild.LocalName.ToLower(); if (cellName == "td" || cellName == "th") { if (singleCell != null) { return null; } singleCell = (XmlElement) trChild; } } } } return singleCell; } /// /// Processes the information about table columns - COLGROUP and COL html elements. /// /// /// XmlElement representing a source html table. /// /// /// XmlElement repesenting a resulting xaml table. /// /// /// Array of doubles - column start coordinates. /// Can be null, which means that column size information is not available /// and we must use source colgroup/col information. /// In case wneh it's not null, we will ignore source colgroup/col information. /// /// /// /// private static void AddColumnInformation(XmlElement htmlTableElement, XmlElement xamlTableElement, ArrayList columnStartsAllRows, Hashtable currentProperties, CssStylesheet stylesheet, List sourceContext) { // Flow document table requires element to include element as // defined in https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/how-to-define-a-table-with-xaml // Notic: CreateElement("Table", "Columns", XamlNamespace) would add xmlns attribute to and lead to XMLReader crash. XmlElement xamlTableColumnGroupElement = xamlTableElement.OwnerDocument.CreateElement(null, XamlTableColumnGroup, XamlNamespace); // Add column information if (columnStartsAllRows != null) { // We have consistent information derived from table cells; use it // The last element in columnStarts represents the end of the table for (var columnIndex = 0; columnIndex < columnStartsAllRows.Count - 1; columnIndex++) { XmlElement xamlColumnElement; xamlColumnElement = xamlTableColumnGroupElement.OwnerDocument.CreateElement(null, XamlTableColumn, XamlNamespace); xamlColumnElement.SetAttribute(XamlWidth, ((double) columnStartsAllRows[columnIndex + 1] - (double) columnStartsAllRows[columnIndex]) .ToString(CultureInfo.InvariantCulture)); xamlTableColumnGroupElement.AppendChild(xamlColumnElement); } } else { // We do not have consistent information from table cells; // Translate blindly colgroups from html. for (var htmlChildNode = htmlTableElement.FirstChild; htmlChildNode != null; htmlChildNode = htmlChildNode.NextSibling) { if (htmlChildNode.LocalName.ToLower() == "colgroup") { // TODO: add column width information to this function as a parameter and process it AddTableColumnGroup(xamlTableColumnGroupElement, (XmlElement) htmlChildNode, currentProperties, stylesheet, sourceContext); } else if (htmlChildNode.LocalName.ToLower() == "col") { AddTableColumn(xamlTableColumnGroupElement, (XmlElement) htmlChildNode, currentProperties, stylesheet, sourceContext); } else if (htmlChildNode is XmlElement) { // Some element which belongs to table body. Stop column loop. break; } } } if (xamlTableColumnGroupElement.HasChildNodes) { xamlTableElement.AppendChild(xamlTableColumnGroupElement); } } /// /// Converts htmlColgroupElement into Xaml TableColumnGroup element, and appends it to the parent /// xamlTableElement /// /// /// XmlElement representing Xaml Table element to which the converted column group should be added /// /// /// XmlElement representing Html colgroup element to be converted /// /// Properties inherited from parent context /// private static void AddTableColumnGroup(XmlElement xamlTableColumnGroupElement, XmlElement htmlColgroupElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { Hashtable localProperties; var currentProperties = GetElementProperties(htmlColgroupElement, inheritedProperties, out localProperties, stylesheet, sourceContext); // TODO: process local properties for colgroup // Process children of colgroup. Colgroup may contain only col elements. for (var htmlNode = htmlColgroupElement.FirstChild; htmlNode != null; htmlNode = htmlNode.NextSibling) { if (htmlNode is XmlElement && htmlNode.LocalName.ToLower() == "col") { AddTableColumn(xamlTableColumnGroupElement, (XmlElement) htmlNode, currentProperties, stylesheet, sourceContext); } } } /// /// Converts htmlColElement into Xaml TableColumn element, and appends it to the parent /// xamlTableColumnGroupElement /// /// /// /// XmlElement representing Html col element to be converted /// /// /// properties inherited from parent context /// /// /// private static void AddTableColumn(XmlElement xamlTableColumnGroupElement, XmlElement htmlColElement, Hashtable inheritedProperties, CssStylesheet stylesheet, List sourceContext) { Hashtable localProperties; var currentProperties = GetElementProperties(htmlColElement, inheritedProperties, out localProperties, stylesheet, sourceContext); var xamlTableColumnElement = xamlTableColumnGroupElement.OwnerDocument.CreateElement(null, XamlTableColumn, XamlNamespace); // TODO: process local properties for TableColumn element // Col is an empty element, with no subtree xamlTableColumnGroupElement.AppendChild(xamlTableColumnElement); } /// /// Adds TableRow elements to xamlTableBodyElement. The rows are converted from Html tr elements that /// may be the children of an Html tbody element or an Html table element with tbody missing /// /// /// XmlElement representing Xaml TableRowGroup element to which the converted rows should be added /// /// /// XmlElement representing the first tr child of the tbody element to be read /// /// /// Hashtable representing current properties of the tbody element that are generated and applied in the /// AddTable function; to be used as inheritedProperties when adding tr elements /// /// /// /// /// /// XmlNode representing the current position of the iterator among tr elements /// private static XmlNode AddTableRowsToTableBody(XmlElement xamlTableBodyElement, XmlNode htmlTrStartNode, Hashtable currentProperties, ArrayList columnStarts, CssStylesheet stylesheet, List sourceContext) { // Parameter validation Debug.Assert(xamlTableBodyElement.LocalName == XamlTableRowGroup); Debug.Assert(currentProperties != null); // Initialize child node for iteratimg through children to the first tr element var htmlChildNode = htmlTrStartNode; ArrayList activeRowSpans = null; if (columnStarts != null) { activeRowSpans = new ArrayList(); InitializeActiveRowSpans(activeRowSpans, columnStarts.Count); } while (htmlChildNode != null && htmlChildNode.LocalName.ToLower() != "tbody") { if (htmlChildNode.LocalName.ToLower() == "tr") { var xamlTableRowElement = xamlTableBodyElement.OwnerDocument.CreateElement(null, XamlTableRow, XamlNamespace); sourceContext.Add((XmlElement) htmlChildNode); // Get tr element properties Hashtable trElementLocalProperties; var trElementCurrentProperties = GetElementProperties((XmlElement) htmlChildNode, currentProperties, out trElementLocalProperties, stylesheet, sourceContext); // TODO: apply local properties to tr element AddTableCellsToTableRow(xamlTableRowElement, htmlChildNode.FirstChild, trElementCurrentProperties, columnStarts, activeRowSpans, stylesheet, sourceContext); if (xamlTableRowElement.HasChildNodes) { xamlTableBodyElement.AppendChild(xamlTableRowElement); } Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlChildNode); sourceContext.RemoveAt(sourceContext.Count - 1); // Advance htmlChildNode = htmlChildNode.NextSibling; } else if (htmlChildNode.LocalName.ToLower() == "td") { // Tr element is not present. We create one and add td elements to it var xamlTableRowElement = xamlTableBodyElement.OwnerDocument.CreateElement(null, XamlTableRow, XamlNamespace); // This is incorrect formatting and the column starts should not be set in this case Debug.Assert(columnStarts == null); htmlChildNode = AddTableCellsToTableRow(xamlTableRowElement, htmlChildNode, currentProperties, columnStarts, activeRowSpans, stylesheet, sourceContext); if (xamlTableRowElement.HasChildNodes) { xamlTableBodyElement.AppendChild(xamlTableRowElement); } } else { // Not a tr or td element. Ignore it. // TODO: consider better recovery here htmlChildNode = htmlChildNode.NextSibling; } } return htmlChildNode; } /// /// Adds TableCell elements to xamlTableRowElement. /// /// /// XmlElement representing Xaml TableRow element to which the converted cells should be added /// /// /// XmlElement representing the child of tr or tbody element from which we should start adding td elements /// /// /// properties of the current html tr element to which cells are to be added /// /// /// XmlElement representing the current position of the iterator among the children of the parent Html tbody/tr element /// private static XmlNode AddTableCellsToTableRow(XmlElement xamlTableRowElement, XmlNode htmlTdStartNode, Hashtable currentProperties, ArrayList columnStarts, ArrayList activeRowSpans, CssStylesheet stylesheet, List sourceContext) { // parameter validation Debug.Assert(xamlTableRowElement.LocalName == XamlTableRow); Debug.Assert(currentProperties != null); if (columnStarts != null) { Debug.Assert(activeRowSpans.Count == columnStarts.Count); } var htmlChildNode = htmlTdStartNode; double columnStart = 0; double columnWidth = 0; var columnIndex = 0; var columnSpan = 0; while (htmlChildNode != null && htmlChildNode.LocalName.ToLower() != "tr" && htmlChildNode.LocalName.ToLower() != "tbody" && htmlChildNode.LocalName.ToLower() != "thead" && htmlChildNode.LocalName.ToLower() != "tfoot") { if (htmlChildNode.LocalName.ToLower() == "td" || htmlChildNode.LocalName.ToLower() == "th") { var xamlTableCellElement = xamlTableRowElement.OwnerDocument.CreateElement(null, XamlTableCell, XamlNamespace); sourceContext.Add((XmlElement) htmlChildNode); Hashtable tdElementLocalProperties; var tdElementCurrentProperties = GetElementProperties((XmlElement) htmlChildNode, currentProperties, out tdElementLocalProperties, stylesheet, sourceContext); // TODO: determine if localProperties can be used instead of htmlChildNode in this call, and if they can, // make necessary changes and use them instead. ApplyPropertiesToTableCellElement((XmlElement) htmlChildNode, xamlTableCellElement); if (columnStarts != null) { Debug.Assert(columnIndex < columnStarts.Count - 1); while (columnIndex < activeRowSpans.Count && (int) activeRowSpans[columnIndex] > 0) { activeRowSpans[columnIndex] = (int) activeRowSpans[columnIndex] - 1; Debug.Assert((int) activeRowSpans[columnIndex] >= 0); columnIndex++; } Debug.Assert(columnIndex < columnStarts.Count - 1); columnStart = (double) columnStarts[columnIndex]; columnWidth = GetColumnWidth((XmlElement) htmlChildNode); columnSpan = CalculateColumnSpan(columnIndex, columnWidth, columnStarts); var rowSpan = GetRowSpan((XmlElement) htmlChildNode); // Column cannot have no span Debug.Assert(columnSpan > 0); Debug.Assert(columnIndex + columnSpan < columnStarts.Count); xamlTableCellElement.SetAttribute(XamlTableCellColumnSpan, columnSpan.ToString()); // Apply row span for (var spannedColumnIndex = columnIndex; spannedColumnIndex < columnIndex + columnSpan; spannedColumnIndex++) { Debug.Assert(spannedColumnIndex < activeRowSpans.Count); activeRowSpans[spannedColumnIndex] = (rowSpan - 1); Debug.Assert((int) activeRowSpans[spannedColumnIndex] >= 0); } columnIndex = columnIndex + columnSpan; } AddDataToTableCell(xamlTableCellElement, htmlChildNode.FirstChild, tdElementCurrentProperties, stylesheet, sourceContext); if (xamlTableCellElement.HasChildNodes) { xamlTableRowElement.AppendChild(xamlTableCellElement); } Debug.Assert(sourceContext.Count > 0 && sourceContext[sourceContext.Count - 1] == htmlChildNode); sourceContext.RemoveAt(sourceContext.Count - 1); htmlChildNode = htmlChildNode.NextSibling; } else { // Not td element. Ignore it. // TODO: Consider better recovery htmlChildNode = htmlChildNode.NextSibling; } } return htmlChildNode; } /// /// adds table cell data to xamlTableCellElement /// /// /// XmlElement representing Xaml TableCell element to which the converted data should be added /// /// /// XmlElement representing the start element of data to be added to xamlTableCellElement /// /// /// Current properties for the html td/th element corresponding to xamlTableCellElement /// private static void AddDataToTableCell(XmlElement xamlTableCellElement, XmlNode htmlDataStartNode, Hashtable currentProperties, CssStylesheet stylesheet, List sourceContext) { // Parameter validation Debug.Assert(xamlTableCellElement.LocalName == XamlTableCell); Debug.Assert(currentProperties != null); for (var htmlChildNode = htmlDataStartNode; htmlChildNode != null; htmlChildNode = htmlChildNode?.NextSibling) { // Process a new html element and add it to the td element htmlChildNode = AddBlock(xamlTableCellElement, htmlChildNode, currentProperties, stylesheet, sourceContext); } } /// /// Performs a parsing pass over a table to read information about column width and rowspan attributes. This /// information /// is used to determine the starting point of each column. /// /// /// XmlElement representing Html table whose structure is to be analyzed /// /// /// ArrayList of type double which contains the function output. If analysis is successful, this ArrayList contains /// all the points which are the starting position of any column in the table, ordered from left to right. /// In case if analisys was impossible we return null. /// private static ArrayList AnalyzeTableStructure(XmlElement htmlTableElement, CssStylesheet stylesheet) { // Parameter validation Debug.Assert(htmlTableElement.LocalName.ToLower() == "table"); if (!htmlTableElement.HasChildNodes) { return null; } var columnWidthsAvailable = true; var columnStarts = new ArrayList(); var activeRowSpans = new ArrayList(); Debug.Assert(columnStarts.Count == activeRowSpans.Count); var htmlChildNode = htmlTableElement.FirstChild; double tableWidth = 0; // Keep track of table width which is the width of its widest row // Analyze tbody and tr elements while (htmlChildNode != null && columnWidthsAvailable) { Debug.Assert(columnStarts.Count == activeRowSpans.Count); switch (htmlChildNode.LocalName.ToLower()) { case "tbody": // Tbody element, we should analyze its children for trows var tbodyWidth = AnalyzeTbodyStructure((XmlElement) htmlChildNode, columnStarts, activeRowSpans, tableWidth, stylesheet); if (tbodyWidth > tableWidth) { // Table width must be increased to supported newly added wide row tableWidth = tbodyWidth; } else if (tbodyWidth == 0) { // Tbody analysis may return 0, probably due to unprocessable format. // We should also fail. columnWidthsAvailable = false; // interrupt the analisys } break; case "tr": // Table row. Analyze column structure within row directly var trWidth = AnalyzeTrStructure((XmlElement) htmlChildNode, columnStarts, activeRowSpans, tableWidth, stylesheet); if (trWidth > tableWidth) { tableWidth = trWidth; } else if (trWidth == 0) { columnWidthsAvailable = false; // interrupt the analisys } break; case "td": // Incorrect formatting, too deep to analyze at this level. Return null. // TODO: implement analysis at this level, possibly by creating a new tr columnWidthsAvailable = false; // interrupt the analisys break; default: // Element should not occur directly in table. Ignore it. break; } htmlChildNode = htmlChildNode.NextSibling; } if (columnWidthsAvailable) { // Add an item for whole table width columnStarts.Add(tableWidth); VerifyColumnStartsAscendingOrder(columnStarts); } else { columnStarts = null; } return columnStarts; } /// /// Performs a parsing pass over a tbody to read information about column width and rowspan attributes. Information /// read about width /// attributes is stored in the reference ArrayList parameter columnStarts, which contains a list of all starting /// positions of all columns in the table, ordered from left to right. Row spans are taken into consideration when /// computing column starts /// /// /// XmlElement representing Html tbody whose structure is to be analyzed /// /// /// ArrayList of type double which contains the function output. If analysis fails, this parameter is set to null /// /// /// Current width of the table. This is used to determine if a new column when added to the end of table should /// come after the last column in the table or is actually splitting the last column in two. If it is only splitting /// the last column it should inherit row span for that column /// /// /// Calculated width of a tbody. /// In case of non-analizable column width structure return 0; /// private static double AnalyzeTbodyStructure(XmlElement htmlTbodyElement, ArrayList columnStarts, ArrayList activeRowSpans, double tableWidth, CssStylesheet stylesheet) { // Parameter validation Debug.Assert(htmlTbodyElement.LocalName.ToLower() == "tbody"); Debug.Assert(columnStarts != null); double tbodyWidth = 0; var columnWidthsAvailable = true; if (!htmlTbodyElement.HasChildNodes) { return tbodyWidth; } // Set active row spans to 0 - thus ignoring row spans crossing tbody boundaries ClearActiveRowSpans(activeRowSpans); var htmlChildNode = htmlTbodyElement.FirstChild; // Analyze tr elements while (htmlChildNode != null && columnWidthsAvailable) { switch (htmlChildNode.LocalName.ToLower()) { case "tr": var trWidth = AnalyzeTrStructure((XmlElement) htmlChildNode, columnStarts, activeRowSpans, tbodyWidth, stylesheet); if (trWidth > tbodyWidth) { tbodyWidth = trWidth; } break; case "td": columnWidthsAvailable = false; // interrupt the analisys break; default: break; } htmlChildNode = htmlChildNode.NextSibling; } // Set active row spans to 0 - thus ignoring row spans crossing tbody boundaries ClearActiveRowSpans(activeRowSpans); return columnWidthsAvailable ? tbodyWidth : 0; } /// /// Performs a parsing pass over a tr element to read information about column width and rowspan attributes. /// /// /// XmlElement representing Html tr element whose structure is to be analyzed /// /// /// ArrayList of type double which contains the function output. If analysis is successful, this ArrayList contains /// all the points which are the starting position of any column in the tr, ordered from left to right. If analysis /// fails, /// the ArrayList is set to null /// /// /// ArrayList representing all columns currently spanned by an earlier row span attribute. These columns should /// not be used for data in this row. The ArrayList actually contains notation for all columns in the table, if the /// active row span is set to 0 that column is not presently spanned but if it is > 0 the column is presently spanned /// /// /// Double value representing the current width of the table. /// Return 0 if analisys was insuccessful. /// private static double AnalyzeTrStructure(XmlElement htmlTrElement, ArrayList columnStarts, ArrayList activeRowSpans, double tableWidth, CssStylesheet stylesheet) { double columnWidth; // Parameter validation Debug.Assert(htmlTrElement.LocalName.ToLower() == "tr"); Debug.Assert(columnStarts != null); Debug.Assert(activeRowSpans != null); Debug.Assert(columnStarts.Count == activeRowSpans.Count); if (!htmlTrElement.HasChildNodes) { return 0; } var columnWidthsAvailable = true; double columnStart = 0; // starting position of current column var htmlChildNode = htmlTrElement.FirstChild; var columnIndex = 0; double trWidth = 0; // Skip spanned columns to get to real column start if (columnIndex < activeRowSpans.Count) { Debug.Assert((double) columnStarts[columnIndex] >= columnStart); if ((double) columnStarts[columnIndex] == columnStart) { // The new column may be in a spanned area while (columnIndex < activeRowSpans.Count && (int) activeRowSpans[columnIndex] > 0) { activeRowSpans[columnIndex] = (int) activeRowSpans[columnIndex] - 1; Debug.Assert((int) activeRowSpans[columnIndex] >= 0); columnIndex++; columnStart = (double) columnStarts[columnIndex]; } } } while (htmlChildNode != null && columnWidthsAvailable) { Debug.Assert(columnStarts.Count == activeRowSpans.Count); VerifyColumnStartsAscendingOrder(columnStarts); switch (htmlChildNode.LocalName.ToLower()) { case "td": Debug.Assert(columnIndex <= columnStarts.Count); if (columnIndex < columnStarts.Count) { Debug.Assert(columnStart <= (double) columnStarts[columnIndex]); if (columnStart < (double) columnStarts[columnIndex]) { columnStarts.Insert(columnIndex, columnStart); // There can be no row spans now - the column data will appear here // Row spans may appear only during the column analysis activeRowSpans.Insert(columnIndex, 0); } } else { // Column start is greater than all previous starts. Row span must still be 0 because // we are either adding after another column of the same row, in which case it should not inherit // the previous column's span. Otherwise we are adding after the last column of some previous // row, and assuming the table widths line up, we should not be spanned by it. If there is // an incorrect tbale structure where a columns starts in the middle of a row span, we do not // guarantee correct output columnStarts.Add(columnStart); activeRowSpans.Add(0); } columnWidth = GetColumnWidth((XmlElement) htmlChildNode); if (columnWidth != -1) { int nextColumnIndex; var rowSpan = GetRowSpan((XmlElement) htmlChildNode); nextColumnIndex = GetNextColumnIndex(columnIndex, columnWidth, columnStarts, activeRowSpans); if (nextColumnIndex != -1) { // Entire column width can be processed without hitting conflicting row span. This means that // column widths line up and we can process them Debug.Assert(nextColumnIndex <= columnStarts.Count); // Apply row span to affected columns for (var spannedColumnIndex = columnIndex; spannedColumnIndex < nextColumnIndex; spannedColumnIndex++) { activeRowSpans[spannedColumnIndex] = rowSpan - 1; Debug.Assert((int) activeRowSpans[spannedColumnIndex] >= 0); } columnIndex = nextColumnIndex; // Calculate columnsStart for the next cell columnStart = columnStart + columnWidth; if (columnIndex < activeRowSpans.Count) { Debug.Assert((double) columnStarts[columnIndex] >= columnStart); if ((double) columnStarts[columnIndex] == columnStart) { // The new column may be in a spanned area while (columnIndex < activeRowSpans.Count && (int) activeRowSpans[columnIndex] > 0) { activeRowSpans[columnIndex] = (int) activeRowSpans[columnIndex] - 1; Debug.Assert((int) activeRowSpans[columnIndex] >= 0); columnIndex++; columnStart = (double) columnStarts[columnIndex]; } } // else: the new column does not start at the same time as a pre existing column // so we don't have to check it for active row spans, it starts in the middle // of another column which has been checked already by the GetNextColumnIndex function } } else { // Full column width cannot be processed without a pre existing row span. // We cannot analyze widths columnWidthsAvailable = false; } } else { // Incorrect column width, stop processing columnWidthsAvailable = false; } break; default: break; } htmlChildNode = htmlChildNode.NextSibling; } // The width of the tr element is the position at which it's last td element ends, which is calculated in // the columnStart value after each td element is processed trWidth = columnWidthsAvailable ? columnStart : 0; return trWidth; } /// /// Gets row span attribute from htmlTDElement. Returns an integer representing the value of the rowspan attribute. /// Default value if attribute is not specified or if it is invalid is 1 /// /// /// Html td element to be searched for rowspan attribute /// private static int GetRowSpan(XmlElement htmlTdElement) { string rowSpanAsString; int rowSpan; rowSpanAsString = GetAttribute(htmlTdElement, "rowspan"); if (rowSpanAsString != null) { if (!int.TryParse(rowSpanAsString, out rowSpan)) { // Ignore invalid value of rowspan; treat it as 1 rowSpan = 1; } } else { // No row span, default is 1 rowSpan = 1; } return rowSpan; } /// /// Gets index at which a column should be inseerted into the columnStarts ArrayList. This is /// decided by the value columnStart. The columnStarts ArrayList is ordered in ascending order. /// Returns an integer representing the index at which the column should be inserted /// /// /// Array list representing starting coordinates of all columns in the table /// /// /// Starting coordinate of column we wish to insert into columnStart /// /// /// Int representing the current column index. This acts as a clue while finding the insertion index. /// If the value of columnStarts at columnIndex is the same as columnStart, then this position alrady exists /// in the array and we can jsut return columnIndex. /// /// private static int GetNextColumnIndex(int columnIndex, double columnWidth, ArrayList columnStarts, ArrayList activeRowSpans) { double columnStart; int spannedColumnIndex; // Parameter validation Debug.Assert(columnStarts != null); Debug.Assert(0 <= columnIndex && columnIndex <= columnStarts.Count); Debug.Assert(columnWidth > 0); columnStart = (double) columnStarts[columnIndex]; spannedColumnIndex = columnIndex + 1; while (spannedColumnIndex < columnStarts.Count && (double) columnStarts[spannedColumnIndex] < columnStart + columnWidth && spannedColumnIndex != -1) { if ((int) activeRowSpans[spannedColumnIndex] > 0) { // The current column should span this area, but something else is already spanning it // Not analyzable spannedColumnIndex = -1; } else { spannedColumnIndex++; } } return spannedColumnIndex; } /// /// Used for clearing activeRowSpans array in the beginning/end of each tbody /// /// /// ArrayList representing currently active row spans /// private static void ClearActiveRowSpans(ArrayList activeRowSpans) { for (var columnIndex = 0; columnIndex < activeRowSpans.Count; columnIndex++) { activeRowSpans[columnIndex] = 0; } } /// /// Used for initializing activeRowSpans array in the before adding rows to tbody element /// /// /// ArrayList representing currently active row spans /// /// /// Size to be give to array list /// private static void InitializeActiveRowSpans(ArrayList activeRowSpans, int count) { for (var columnIndex = 0; columnIndex < count; columnIndex++) { activeRowSpans.Add(0); } } /// /// Calculates width of next TD element based on starting position of current element and it's width, which /// is calculated byt he function /// /// /// XmlElement representing Html td element whose width is to be read /// /// /// Starting position of current column /// private static double GetNextColumnStart(XmlElement htmlTdElement, double columnStart) { double columnWidth; double nextColumnStart; // Parameter validation Debug.Assert(htmlTdElement.LocalName.ToLower() == "td" || htmlTdElement.LocalName.ToLower() == "th"); Debug.Assert(columnStart >= 0); nextColumnStart = -1; // -1 indicates inability to calculate columnStart width columnWidth = GetColumnWidth(htmlTdElement); if (columnWidth == -1) { nextColumnStart = -1; } else { nextColumnStart = columnStart + columnWidth; } return nextColumnStart; } private static double GetColumnWidth(XmlElement htmlTdElement) { string columnWidthAsString; double columnWidth; columnWidthAsString = null; columnWidth = -1; // Get string valkue for the width columnWidthAsString = GetAttribute(htmlTdElement, "width") ?? GetCssAttribute(GetAttribute(htmlTdElement, "style"), "width"); // We do not allow column width to be 0, if specified as 0 we will fail to record it if (!TryGetLengthValue(columnWidthAsString, out columnWidth) || columnWidth == 0) { columnWidth = -1; } return columnWidth; } /// /// Calculates column span based the column width and the widths of all other columns. Returns an integer representing /// the column span /// /// /// Index of the current column /// /// /// Width of the current column /// /// /// ArrayList repsenting starting coordinates of all columns /// private static int CalculateColumnSpan(int columnIndex, double columnWidth, ArrayList columnStarts) { // Current status of column width. Indicates the amount of width that has been scanned already double columnSpanningValue; int columnSpanningIndex; int columnSpan; double subColumnWidth; // Width of the smallest-grain columns in the table Debug.Assert(columnStarts != null); Debug.Assert(columnIndex < columnStarts.Count - 1); Debug.Assert((double) columnStarts[columnIndex] >= 0); Debug.Assert(columnWidth > 0); columnSpanningIndex = columnIndex; columnSpanningValue = 0; columnSpan = 0; subColumnWidth = 0; while (columnSpanningValue < columnWidth && columnSpanningIndex < columnStarts.Count - 1) { subColumnWidth = (double) columnStarts[columnSpanningIndex + 1] - (double) columnStarts[columnSpanningIndex]; Debug.Assert(subColumnWidth > 0); columnSpanningValue += subColumnWidth; columnSpanningIndex++; } // Now, we have either covered the width we needed to cover or reached the end of the table, in which // case the column spans all the columns until the end columnSpan = columnSpanningIndex - columnIndex; Debug.Assert(columnSpan > 0); return columnSpan; } /// /// Verifies that values in columnStart, which represent starting coordinates of all columns, are arranged /// in ascending order /// /// /// ArrayList representing starting coordinates of all columns /// private static void VerifyColumnStartsAscendingOrder(ArrayList columnStarts) { Debug.Assert(columnStarts != null); double columnStart; columnStart = -0.01; foreach (object t in columnStarts) { Debug.Assert(columnStart < (double) t); columnStart = (double) t; } } // ............................................................. // // Attributes and Properties // // ............................................................. /// /// Analyzes local properties of Html element, converts them into Xaml equivalents, and applies them to xamlElement /// /// /// XmlElement representing Xaml element to which properties are to be applied /// /// /// Hashtable representing local properties of Html element that is converted into xamlElement /// private static void ApplyLocalProperties(XmlElement xamlElement, Hashtable localProperties, bool isBlock) { var marginSet = false; var marginTop = "0"; var marginBottom = "0"; var marginLeft = "0"; var marginRight = "0"; var paddingSet = false; var paddingTop = "0"; var paddingBottom = "0"; var paddingLeft = "0"; var paddingRight = "0"; string borderColor = null; var borderThicknessSet = false; var borderThicknessTop = "0"; var borderThicknessBottom = "0"; var borderThicknessLeft = "0"; var borderThicknessRight = "0"; var propertyEnumerator = localProperties.GetEnumerator(); while (propertyEnumerator.MoveNext()) { switch ((string) propertyEnumerator.Key) { case "font-family": // Convert from font-family value list into xaml FontFamily value xamlElement.SetAttribute(XamlFontFamily, (string) propertyEnumerator.Value); break; case "font-style": xamlElement.SetAttribute(XamlFontStyle, (string) propertyEnumerator.Value); break; case "font-variant": // Convert from font-variant into xaml property break; case "font-weight": xamlElement.SetAttribute(XamlFontWeight, (string) propertyEnumerator.Value); break; case "font-size": // Convert from css size into FontSize xamlElement.SetAttribute(XamlFontSize, (string) propertyEnumerator.Value); break; case "color": SetPropertyValue(xamlElement, TextElement.ForegroundProperty, (string) propertyEnumerator.Value); break; case "background-color": SetPropertyValue(xamlElement, TextElement.BackgroundProperty, (string) propertyEnumerator.Value); break; case "text-decoration-underline": if (!isBlock) { if ((string) propertyEnumerator.Value == "true") { xamlElement.SetAttribute(XamlTextDecorations, XamlTextDecorationsUnderline); } } break; case "text-decoration-none": case "text-decoration-overline": case "text-decoration-line-through": case "text-decoration-blink": // Convert from all other text-decorations values if (!isBlock) { } break; case "text-transform": // Convert from text-transform into xaml property break; case "text-indent": if (isBlock) { xamlElement.SetAttribute(XamlTextIndent, (string) propertyEnumerator.Value); } break; case "text-align": if (isBlock) { xamlElement.SetAttribute(XamlTextAlignment, (string) propertyEnumerator.Value); } break; case "width": case "height": // Decide what to do with width and height propeties break; case "margin-top": marginSet = true; marginTop = (string) propertyEnumerator.Value; break; case "margin-right": marginSet = true; marginRight = (string) propertyEnumerator.Value; break; case "margin-bottom": marginSet = true; marginBottom = (string) propertyEnumerator.Value; break; case "margin-left": marginSet = true; marginLeft = (string) propertyEnumerator.Value; break; case "padding-top": paddingSet = true; paddingTop = (string) propertyEnumerator.Value; break; case "padding-right": paddingSet = true; paddingRight = (string) propertyEnumerator.Value; break; case "padding-bottom": paddingSet = true; paddingBottom = (string) propertyEnumerator.Value; break; case "padding-left": paddingSet = true; paddingLeft = (string) propertyEnumerator.Value; break; // NOTE: css names for elementary border styles have side indications in the middle (top/bottom/left/right) // In our internal notation we intentionally put them at the end - to unify processing in ParseCssRectangleProperty method case "border-color-top": borderColor = (string) propertyEnumerator.Value; break; case "border-color-right": borderColor = (string) propertyEnumerator.Value; break; case "border-color-bottom": borderColor = (string) propertyEnumerator.Value; break; case "border-color-left": borderColor = (string) propertyEnumerator.Value; break; case "border-style-top": case "border-style-right": case "border-style-bottom": case "border-style-left": // Implement conversion from border style break; case "border-width-top": borderThicknessSet = true; borderThicknessTop = (string) propertyEnumerator.Value; break; case "border-width-right": borderThicknessSet = true; borderThicknessRight = (string) propertyEnumerator.Value; break; case "border-width-bottom": borderThicknessSet = true; borderThicknessBottom = (string) propertyEnumerator.Value; break; case "border-width-left": borderThicknessSet = true; borderThicknessLeft = (string) propertyEnumerator.Value; break; case "list-style-type": if (xamlElement.LocalName == XamlList) { string markerStyle; switch (((string) propertyEnumerator.Value).ToLower()) { case "disc": markerStyle = XamlListMarkerStyleDisc; break; case "circle": markerStyle = XamlListMarkerStyleCircle; break; case "none": markerStyle = XamlListMarkerStyleNone; break; case "square": markerStyle = XamlListMarkerStyleSquare; break; case "box": markerStyle = XamlListMarkerStyleBox; break; case "lower-latin": markerStyle = XamlListMarkerStyleLowerLatin; break; case "upper-latin": markerStyle = XamlListMarkerStyleUpperLatin; break; case "lower-roman": markerStyle = XamlListMarkerStyleLowerRoman; break; case "upper-roman": markerStyle = XamlListMarkerStyleUpperRoman; break; case "decimal": markerStyle = XamlListMarkerStyleDecimal; break; default: markerStyle = XamlListMarkerStyleDisc; break; } xamlElement.SetAttribute(XamlListMarkerStyle, markerStyle); } break; case "float": case "clear": if (isBlock) { // Convert float and clear properties } break; case "display": break; } } if (isBlock) { if (marginSet) { ComposeThicknessProperty(xamlElement, XamlMargin, marginLeft, marginRight, marginTop, marginBottom); } if (paddingSet) { ComposeThicknessProperty(xamlElement, XamlPadding, paddingLeft, paddingRight, paddingTop, paddingBottom); } if (borderColor != null) { // We currently ignore possible difference in brush colors on different border sides. Use the last colored side mentioned xamlElement.SetAttribute(XamlBorderBrush, borderColor); } if (borderThicknessSet) { ComposeThicknessProperty(xamlElement, XamlBorderThickness, borderThicknessLeft, borderThicknessRight, borderThicknessTop, borderThicknessBottom); } } } // Create syntactically optimized four-value Thickness private static void ComposeThicknessProperty(XmlElement xamlElement, string propertyName, string left, string right, string top, string bottom) { // Xaml syntax: // We have a reasonable interpreation for one value (all four edges), two values (horizontal, vertical), // and four values (left, top, right, bottom). // switch (i) { // case 1: return new Thickness(lengths[0]); // case 2: return new Thickness(lengths[0], lengths[1], lengths[0], lengths[1]); // case 4: return new Thickness(lengths[0], lengths[1], lengths[2], lengths[3]); // } string thickness; // We do not accept negative margins if (left[0] == '0' || left[0] == '-') left = "0"; if (right[0] == '0' || right[0] == '-') right = "0"; if (top[0] == '0' || top[0] == '-') top = "0"; if (bottom[0] == '0' || bottom[0] == '-') bottom = "0"; if (left == right && top == bottom) { if (left == top) { thickness = left; } else { thickness = left + "," + top; } } else { thickness = left + "," + top + "," + right + "," + bottom; } // Need safer processing for a thickness value xamlElement.SetAttribute(propertyName, thickness); } private static void SetPropertyValue(XmlElement xamlElement, DependencyProperty property, string stringValue) { var typeConverter = TypeDescriptor.GetConverter(property.PropertyType); try { var convertedValue = typeConverter.ConvertFromInvariantString(stringValue); if (convertedValue != null) { xamlElement.SetAttribute(property.Name, stringValue); } } catch (Exception) { } } /// /// Analyzes the tag of the htmlElement and infers its associated formatted properties. /// After that parses style attribute and adds all inline css styles. /// The resulting style attributes are collected in output parameter localProperties. /// /// /// /// /// set of properties inherited from ancestor elements. Currently not used in the code. Reserved for the future /// development. /// /// /// returns all formatting properties defined by this element - implied by its tag, its attributes, or its css inline /// style /// /// /// /// /// returns a combination of previous context with local set of properties. /// This value is not used in the current code - inntended for the future development. /// private static Hashtable GetElementProperties(XmlElement htmlElement, Hashtable inheritedProperties, out Hashtable localProperties, CssStylesheet stylesheet, List sourceContext) { // Start with context formatting properties var currentProperties = new Hashtable(); var propertyEnumerator = inheritedProperties.GetEnumerator(); while (propertyEnumerator.MoveNext()) { currentProperties[propertyEnumerator.Key] = propertyEnumerator.Value; } // Identify element name var elementName = htmlElement.LocalName.ToLower(); var elementNamespace = htmlElement.NamespaceURI; // update current formatting properties depending on element tag localProperties = new Hashtable(); switch (elementName) { // Character formatting case "i": case "italic": case "em": localProperties["font-style"] = "italic"; break; case "b": case "bold": case "strong": case "dfn": localProperties["font-weight"] = "bold"; break; case "u": case "underline": localProperties["text-decoration-underline"] = "true"; break; case "font": var attributeValue = GetAttribute(htmlElement, "face"); if (attributeValue != null) { localProperties["font-family"] = attributeValue; } attributeValue = GetAttribute(htmlElement, "size"); if (attributeValue != null) { var fontSize = double.Parse(attributeValue)*(12.0/3.0); if (fontSize < 1.0) { fontSize = 1.0; } else if (fontSize > 1000.0) { fontSize = 1000.0; } localProperties["font-size"] = fontSize.ToString(CultureInfo.InvariantCulture); } attributeValue = GetAttribute(htmlElement, "color"); if (attributeValue != null) { localProperties["color"] = attributeValue; } break; case "samp": localProperties["font-family"] = "Courier New"; // code sample localProperties["font-size"] = XamlFontSizeXxSmall; localProperties["text-align"] = "Left"; break; case "sub": break; case "sup": break; // Hyperlinks case "a": // href, hreflang, urn, methods, rel, rev, title // Set default hyperlink properties break; case "acronym": break; // Paragraph formatting: case "p": // Set default paragraph properties break; case "div": // Set default div properties break; case "pre": localProperties["font-family"] = "Courier New"; // renders text in a fixed-width font localProperties["font-size"] = XamlFontSizeXxSmall; localProperties["text-align"] = "Left"; break; case "blockquote": localProperties["margin-left"] = "16"; break; case "h1": localProperties["font-size"] = XamlFontSizeXxLarge; break; case "h2": localProperties["font-size"] = XamlFontSizeXLarge; break; case "h3": localProperties["font-size"] = XamlFontSizeLarge; break; case "h4": localProperties["font-size"] = XamlFontSizeMedium; break; case "h5": localProperties["font-size"] = XamlFontSizeSmall; break; case "h6": localProperties["font-size"] = XamlFontSizeXSmall; break; // List properties case "ul": localProperties["list-style-type"] = "disc"; break; case "ol": localProperties["list-style-type"] = "decimal"; break; case "table": case "body": case "html": break; } // Override html defaults by css attributes - from stylesheets and inline settings HtmlCssParser.GetElementPropertiesFromCssAttributes(htmlElement, elementName, stylesheet, localProperties, sourceContext); // Combine local properties with context to create new current properties propertyEnumerator = localProperties.GetEnumerator(); while (propertyEnumerator.MoveNext()) { currentProperties[propertyEnumerator.Key] = propertyEnumerator.Value; } return currentProperties; } /// /// Extracts a value of css attribute from css style definition. /// /// /// Source csll style definition /// /// /// A name of css attribute to extract /// /// /// A string rrepresentation of an attribute value if found; /// null if there is no such attribute in a given string. /// private static string GetCssAttribute(string cssStyle, string attributeName) { // This is poor man's attribute parsing. Replace it by real css parsing if (cssStyle != null) { string[] styleValues; attributeName = attributeName.ToLower(); // Check for width specification in style string styleValues = cssStyle.Split(';'); foreach (string t in styleValues) { string[] styleNameValue; styleNameValue = t.Split(':'); if (styleNameValue.Length == 2) { if (styleNameValue[0].Trim().ToLower() == attributeName) { return styleNameValue[1].Trim(); } } } } return null; } /// /// Converts a length value from string representation to a double. /// /// /// Source string value of a length. /// /// /// private static bool TryGetLengthValue(string lengthAsString, out double length) { length = double.NaN; if (lengthAsString != null) { lengthAsString = lengthAsString.Trim().ToLower(); // We try to convert currentColumnWidthAsString into a double. This will eliminate widths of type "50%", etc. if (lengthAsString.EndsWith("pt")) { lengthAsString = lengthAsString.Substring(0, lengthAsString.Length - 2); if (double.TryParse(lengthAsString, out length)) { length = (length*96.0)/72.0; // convert from points to pixels } else { length = double.NaN; } } else if (lengthAsString.EndsWith("px")) { lengthAsString = lengthAsString.Substring(0, lengthAsString.Length - 2); if (!double.TryParse(lengthAsString, out length)) { length = double.NaN; } } else { if (!double.TryParse(lengthAsString, out length)) // Assuming pixels { length = double.NaN; } } } return !double.IsNaN(length); } // ................................................................. // // Pasring Color Attribute // // ................................................................. private static string GetColorValue(string colorValue) => colorValue; /// /// Applies properties to xamlTableCellElement based on the html td element it is converted from. /// /// /// Html td/th element to be converted to xaml /// /// /// XmlElement representing Xaml element for which properties are to be processed /// /// /// TODO: Use the processed properties for htmlChildNode instead of using the node itself /// private static void ApplyPropertiesToTableCellElement(XmlElement htmlChildNode, XmlElement xamlTableCellElement) { // Parameter validation Debug.Assert(htmlChildNode.LocalName.ToLower() == "td" || htmlChildNode.LocalName.ToLower() == "th"); Debug.Assert(xamlTableCellElement.LocalName == XamlTableCell); // set default border thickness for xamlTableCellElement to enable gridlines xamlTableCellElement.SetAttribute(XamlTableCellBorderThickness, "1,1,1,1"); xamlTableCellElement.SetAttribute(XamlTableCellBorderBrush, XamlBrushesBlack); var rowSpanString = GetAttribute(htmlChildNode, "rowspan"); if (rowSpanString != null) { xamlTableCellElement.SetAttribute(XamlTableCellRowSpan, rowSpanString); } } #endregion Private Methods } }