// // 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.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Xml; namespace HtmlToXamlDemo { /// /// HtmlToXamlConverter is a static class that takes an HTML string /// and converts it into XAML /// internal static class HtmlFromXamlConverter { // --------------------------------------------------------------------- // // Internal Methods // // --------------------------------------------------------------------- #region Internal Methods /// /// Main entry point for Xaml-to-Html converter. /// Converts a xaml string into html string. /// /// /// Xaml strinng to convert. /// /// /// Html string produced from a source xaml. /// internal static string ConvertXamlToHtml(string xamlString) { XmlTextReader xamlReader; StringBuilder htmlStringBuilder; XmlTextWriter htmlWriter; xamlReader = new XmlTextReader(new StringReader(xamlString)); htmlStringBuilder = new StringBuilder(100); htmlWriter = new HtmlEncodedTextWriter(new StringWriter(htmlStringBuilder)); if (!WriteFlowDocument(xamlReader, htmlWriter)) { return ""; } var htmlString = htmlStringBuilder.ToString(); return htmlString; } #endregion Internal Methods // --------------------------------------------------------------------- // // Private Methods // // --------------------------------------------------------------------- #region Private Methods /// /// Processes a root level element of XAML (normally it's FlowDocument element). /// /// /// XmlTextReader for a source xaml. /// /// /// XmlTextWriter producing resulting html /// private static bool WriteFlowDocument(XmlTextReader xamlReader, XmlTextWriter htmlWriter) { if (!ReadNextToken(xamlReader)) { // Xaml content is empty - nothing to convert return false; } if (xamlReader.NodeType != XmlNodeType.Element || xamlReader.Name != "FlowDocument") { // Root FlowDocument elemet is missing return false; } // Create a buffer StringBuilder for collecting css properties for inline STYLE attributes // on every element level (it will be re-initialized on every level). var inlineStyle = new StringBuilder(); htmlWriter.WriteStartElement("html"); htmlWriter.WriteStartElement("body"); WriteFormattingProperties(xamlReader, htmlWriter, inlineStyle); WriteElementContent(xamlReader, htmlWriter, inlineStyle); htmlWriter.WriteEndElement(); htmlWriter.WriteEndElement(); return true; } /// /// Reads attributes of the current xaml element and converts /// them into appropriate html attributes or css styles. /// /// /// XmlTextReader which is expected to be at XmlNodeType.Element /// (opening element tag) position. /// The reader will remain at the same level after function complete. /// /// /// XmlTextWriter for output html, which is expected to be in /// after WriteStartElement state. /// /// /// String builder for collecting css properties for inline STYLE attribute. /// private static void WriteFormattingProperties(XmlTextReader xamlReader, XmlTextWriter htmlWriter, StringBuilder inlineStyle) { Debug.Assert(xamlReader.NodeType == XmlNodeType.Element); // Clear string builder for the inline style inlineStyle.Remove(0, inlineStyle.Length); if (!xamlReader.HasAttributes) { return; } var borderSet = false; while (xamlReader.MoveToNextAttribute()) { string css = null; switch (xamlReader.Name) { // Character fomatting properties // ------------------------------ case "Background": css = "background-color:" + ParseXamlColor(xamlReader.Value) + ";"; break; case "FontFamily": css = "font-family:" + xamlReader.Value + ";"; break; case "FontStyle": css = "font-style:" + xamlReader.Value.ToLower() + ";"; break; case "FontWeight": css = "font-weight:" + xamlReader.Value.ToLower() + ";"; break; case "FontStretch": break; case "FontSize": css = "font-size:" + xamlReader.Value + ";"; break; case "Foreground": css = "color:" + ParseXamlColor(xamlReader.Value) + ";"; break; case "TextDecorations": css = "text-decoration:underline;"; break; case "TextEffects": break; case "Emphasis": break; case "StandardLigatures": break; case "Variants": break; case "Capitals": break; case "Fraction": break; // Paragraph formatting properties // ------------------------------- case "Padding": css = "padding:" + ParseXamlThickness(xamlReader.Value) + ";"; break; case "Margin": css = "margin:" + ParseXamlThickness(xamlReader.Value) + ";"; break; case "BorderThickness": css = "border-width:" + ParseXamlThickness(xamlReader.Value) + ";"; borderSet = true; break; case "BorderBrush": css = "border-color:" + ParseXamlColor(xamlReader.Value) + ";"; borderSet = true; break; case "LineHeight": break; case "TextIndent": css = "text-indent:" + xamlReader.Value + ";"; break; case "TextAlignment": css = "text-align:" + xamlReader.Value + ";"; break; case "IsKeptTogether": break; case "IsKeptWithNext": break; case "ColumnBreakBefore": break; case "PageBreakBefore": break; case "FlowDirection": break; // Table attributes // ---------------- case "Width": css = "width:" + xamlReader.Value + ";"; break; case "ColumnSpan": htmlWriter.WriteAttributeString("colspan", xamlReader.Value); break; case "RowSpan": htmlWriter.WriteAttributeString("rowspan", xamlReader.Value); break; } if (css != null) { inlineStyle.Append(css); } } if (borderSet) { inlineStyle.Append("border-style:solid;mso-element:para-border-div;"); } // Return the xamlReader back to element level xamlReader.MoveToElement(); Debug.Assert(xamlReader.NodeType == XmlNodeType.Element); } private static string ParseXamlColor(string color) { if (color.StartsWith("#")) { // Remove transparancy value color = "#" + color.Substring(3); } return color; } private static string ParseXamlThickness(string thickness) { var values = thickness.Split(','); for (var i = 0; i < values.Length; i++) { if (double.TryParse(values[i], NumberStyles.Any, CultureInfo.InvariantCulture, out double value)) { values[i] = Math.Ceiling(value).ToString(CultureInfo.InvariantCulture); } else { values[i] = "1"; } } string cssThickness; switch (values.Length) { case 1: cssThickness = thickness; break; case 2: cssThickness = values[1] + " " + values[0]; break; case 4: cssThickness = values[1] + " " + values[2] + " " + values[3] + " " + values[0]; break; default: cssThickness = values[0]; break; } return cssThickness; } /// /// Reads a content of current xaml element, converts it /// /// /// XmlTextReader which is expected to be at XmlNodeType.Element /// (opening element tag) position. /// /// /// May be null, in which case we are skipping the xaml element; /// witout producing any output to html. /// /// /// StringBuilder used for collecting css properties for inline STYLE attribute. /// private static void WriteElementContent(XmlTextReader xamlReader, XmlTextWriter htmlWriter, StringBuilder inlineStyle) { Debug.Assert(xamlReader.NodeType == XmlNodeType.Element); var elementContentStarted = false; if (xamlReader.IsEmptyElement) { if (htmlWriter != null && !elementContentStarted && inlineStyle.Length > 0) { // Output STYLE attribute and clear inlineStyle buffer. htmlWriter.WriteAttributeString("STYLE", inlineStyle.ToString()); inlineStyle.Remove(0, inlineStyle.Length); } elementContentStarted = true; } else { while (ReadNextToken(xamlReader) && xamlReader.NodeType != XmlNodeType.EndElement) { switch (xamlReader.NodeType) { case XmlNodeType.Element: if (xamlReader.Name.Contains(".")) { AddComplexProperty(xamlReader, inlineStyle); } else { if (htmlWriter != null && !elementContentStarted && inlineStyle.Length > 0) { // Output STYLE attribute and clear inlineStyle buffer. htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); inlineStyle.Remove(0, inlineStyle.Length); } elementContentStarted = true; WriteElement(xamlReader, htmlWriter, inlineStyle); } Debug.Assert(xamlReader.NodeType == XmlNodeType.EndElement || xamlReader.NodeType == XmlNodeType.Element && xamlReader.IsEmptyElement); break; case XmlNodeType.Comment: if (htmlWriter != null) { if (!elementContentStarted && inlineStyle.Length > 0) { htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); } htmlWriter.WriteComment(xamlReader.Value); } elementContentStarted = true; break; case XmlNodeType.CDATA: case XmlNodeType.Text: case XmlNodeType.SignificantWhitespace: if (htmlWriter != null) { if (!elementContentStarted && inlineStyle.Length > 0) { htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); } htmlWriter.WriteString(xamlReader.Value); } elementContentStarted = true; break; } } Debug.Assert(xamlReader.NodeType == XmlNodeType.EndElement); } } /// /// Conberts an element notation of complex property into /// /// /// On entry this XmlTextReader must be on Element start tag; /// on exit - on EndElement tag. /// /// /// StringBuilder containing a value for STYLE attribute. /// private static void AddComplexProperty(XmlTextReader xamlReader, StringBuilder inlineStyle) { Debug.Assert(xamlReader.NodeType == XmlNodeType.Element); if (inlineStyle != null && xamlReader.Name.EndsWith(".TextDecorations")) { inlineStyle.Append("text-decoration:underline;"); } // Skip the element representing the complex property WriteElementContent(xamlReader, /*htmlWriter:*/null, /*inlineStyle:*/null); } /// /// Converts a xaml element into an appropriate html element. /// /// /// On entry this XmlTextReader must be on Element start tag; /// on exit - on EndElement tag. /// /// /// May be null, in which case we are skipping xaml content /// without producing any html output /// /// /// StringBuilder used for collecting css properties for inline STYLE attributes on every level. /// private static void WriteElement(XmlTextReader xamlReader, XmlTextWriter htmlWriter, StringBuilder inlineStyle) { Debug.Assert(xamlReader.NodeType == XmlNodeType.Element); if (htmlWriter == null) { // Skipping mode; recurse into the xaml element without any output WriteElementContent(xamlReader, /*htmlWriter:*/null, null); } else { string htmlElementName = null; switch (xamlReader.Name) { case "Run": case "Span": htmlElementName = "span"; break; case "InlineUIContainer": htmlElementName = "span"; break; case "Bold": htmlElementName = "b"; break; case "Italic": htmlElementName = "i"; break; case "Paragraph": htmlElementName = "p"; break; case "BlockUIContainer": htmlElementName = "div"; break; case "Section": htmlElementName = "div"; break; case "Table": htmlElementName = "table"; break; case "TableColumn": htmlElementName = "col"; break; case "TableRowGroup": htmlElementName = "tbody"; break; case "TableRow": htmlElementName = "tr"; break; case "TableCell": htmlElementName = "td"; break; case "List": var marker = xamlReader.GetAttribute("MarkerStyle"); if (marker == null || marker == "None" || marker == "Disc" || marker == "Circle" || marker == "Square" || marker == "Box") { htmlElementName = "ul"; } else { htmlElementName = "ol"; } break; case "ListItem": htmlElementName = "li"; break; default: htmlElementName = null; // Ignore the element break; } if (htmlWriter != null && htmlElementName != null) { htmlWriter.WriteStartElement(htmlElementName); WriteFormattingProperties(xamlReader, htmlWriter, inlineStyle); WriteElementContent(xamlReader, htmlWriter, inlineStyle); htmlWriter.WriteEndElement(); } else { // Skip this unrecognized xaml element WriteElementContent(xamlReader, /*htmlWriter:*/null, null); } } } // Reader advance helpers // ---------------------- /// /// Reads several items from xamlReader skipping all non-significant stuff. /// /// /// XmlTextReader from tokens are being read. /// /// /// True if new token is available; false if end of stream reached. /// private static bool ReadNextToken(XmlReader xamlReader) { while (xamlReader.Read()) { Debug.Assert(xamlReader.ReadState == ReadState.Interactive, "Reader is expected to be in Interactive state (" + xamlReader.ReadState + ")"); switch (xamlReader.NodeType) { case XmlNodeType.Element: case XmlNodeType.EndElement: case XmlNodeType.None: case XmlNodeType.CDATA: case XmlNodeType.Text: case XmlNodeType.SignificantWhitespace: return true; case XmlNodeType.Whitespace: if (xamlReader.XmlSpace == XmlSpace.Preserve) { return true; } // ignore insignificant whitespace break; case XmlNodeType.EndEntity: case XmlNodeType.EntityReference: // Implement entity reading //xamlReader.ResolveEntity(); //xamlReader.Read(); //ReadChildNodes( parent, parentBaseUri, xamlReader, positionInfo); break; // for now we ignore entities as insignificant stuff case XmlNodeType.Comment: return true; case XmlNodeType.ProcessingInstruction: case XmlNodeType.DocumentType: case XmlNodeType.XmlDeclaration: default: // Ignorable stuff break; } } return false; } #endregion Private Methods // --------------------------------------------------------------------- // // Private Fields // // --------------------------------------------------------------------- #region Private Fields #endregion Private Fields } }