// // HtmlEmitter.cs // // Author: // Atsushi Enomoto // // (C)2003 Atsushi Enomoto // // TODO: // indent, uri escape, allowed entity char such as  , // encoding to meta tag, doctype-public/doctype-system. // // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.Globalization; using System.IO; using System.Text; using System.Xml; namespace Mono.Xml.Xsl { internal class HtmlEmitter : Emitter { TextWriter writer; Stack elementNameStack; bool openElement; bool openAttribute; int nonHtmlDepth; bool indent; Encoding outputEncoding; string mediaType; public HtmlEmitter (TextWriter writer, XslOutput output) { this.writer = writer; indent = output.Indent == "yes" || output.Indent == null; elementNameStack = new Stack (); nonHtmlDepth = -1; outputEncoding = writer.Encoding == null ? output.Encoding : writer.Encoding; mediaType = output.MediaType; if (mediaType == null || mediaType.Length == 0) mediaType = "text/html"; } public override void WriteStartDocument (Encoding encoding, StandaloneType standalone) { // do nothing } public override void WriteEndDocument () { // do nothing } public override void WriteDocType (string name, string publicId, string systemId) { writer.Write ("'); if (indent) writer.WriteLine (); } private void CloseAttribute () { writer.Write ('\"'); openAttribute = false; } private void CloseStartElement () { //FIXME: consider sanity check if (openElement) return; if (openAttribute) CloseAttribute (); writer.Write ('>'); openElement = false; if (outputEncoding != null && elementNameStack.Count > 0) { string name = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture); switch (name) { case "HEAD": WriteStartElement (String.Empty, "META", String.Empty); WriteAttributeString (String.Empty, "http-equiv", String.Empty, "Content-Type"); WriteAttributeString (String.Empty, "content", String.Empty, String.Concat (mediaType, "; charset=", outputEncoding.WebName)); WriteEndElement (); break; case "STYLE": case "SCRIPT": writer.WriteLine (); for (int i = 0; i <= elementNameStack.Count; i++) writer.Write (" "); break; } } } // FIXME: check all HTML elements' indentation. private void Indent (string elementName, bool endIndent) { if (!indent) return; switch (elementName.ToUpper (CultureInfo.InvariantCulture)) { case "ADDRESS": case "APPLET": case "BDO": case "BLOCKQUOTE": case "BODY": case "BUTTON": case "CAPTION": case "CENTER": case "DD": case "DEL": case "DIR": case "DIV": case "DL": case "DT": case "FIELDSET": case "HEAD": case "HTML": case "IFRAME": case "INS": case "LI": case "MAP": case "MENU": case "NOFRAMES": case "NOSCRIPT": case "OBJECT": case "OPTION": case "PRE": case "TABLE": case "TD": case "TH": case "TR": writer.Write (writer.NewLine); int count = elementNameStack.Count; for (int i = 0; i < count; i++) writer.Write (" "); break; default: if (elementName.Length > 0 && nonHtmlDepth > 0) goto case "HTML"; break; } } public override void WriteStartElement (string prefix, string localName, string nsURI) { if (openElement) CloseStartElement (); Indent (elementNameStack.Count > 0 ? elementNameStack.Peek () as String : String.Empty, false); string formatName = localName; writer.Write ('<'); if (nsURI != String.Empty) {// && !IsHtmlElement (localName)) { // XML output if (prefix != String.Empty) { formatName = String.Concat (prefix, ":", localName); } if (nonHtmlDepth < 0) nonHtmlDepth = elementNameStack.Count + 1; } writer.Write (formatName); elementNameStack.Push (formatName); openElement = true; } private bool IsHtmlElement (string localName) { // see http://www.w3.org/TR/html401/index/elements.html switch (localName.ToUpper (CultureInfo.InvariantCulture)) { case "A": case "ABBR": case "ACRONYM": case "ADDRESS": case "APPLET": case "AREA": case "B": case "BASE": case "BASEFONT": case "BDO": case "BIG": case "BLOCKQUOTE": case "BODY": case "BR": case "BUTTON": case "CAPTION": case "CENTER": case "CITE": case "CODE": case "COL": case "COLGROUP": case "DD": case "DEL": case "DFN": case "DIR": case "DIV": case "DL": case "DT": case "EM": case "FIELDSET": case "FONT": case "FORM": case "FRAME": case "FRAMESET": case "H1": case "H2": case "H3": case "H4": case "H5": case "H6": case "HEAD": case "HR": case "HTML": case "I": case "IFRAME": case "IMG": case "INPUT": case "INS": case "ISINDEX": case "KBD": case "LABEL": case "LEGEND": case "LI": case "LINK": case "MAP": case "MENU": case "META": case "NOFRAMES": case "NOSCRIPT": case "OBJECT": case "OL": case "OPTGROUP": case "OPTION": case "P": case "PARAM": case "PRE": case "Q": case "S": case "SAMP": case "SCRIPT": case "SELECT": case "SMALL": case "SPAN": case "STRIKE": case "STRONG": case "STYLE": case "SUB": case "SUP": case "TABLE": case "TBODY": case "TD": case "TEXTAREA": case "TFOOT": case "TH": case "THEAD": case "TITLE": case "TR": case "TT": case "U": case "UL": case "VAR": return true; } return false; } public override void WriteEndElement () { WriteFullEndElement (); } public override void WriteFullEndElement () { string element = elementNameStack.Peek () as string; switch (element.ToUpper (CultureInfo.InvariantCulture)) { case "AREA": case "BASE": case "BASEFONT": case "BR": case "COL": case "FRAME": case "HR": case "IMG": case "INPUT": case "ISINDEX": case "LINK": case "META": case "PARAM": if (openAttribute) CloseAttribute (); if (openElement) writer.Write ('>'); //FIXME: consider using CloseStartElement() to write '>' elementNameStack.Pop (); break; default: if (openElement) CloseStartElement (); elementNameStack.Pop (); if (IsHtmlElement (element)) Indent (element as string, true); writer.Write (""); break; } if (nonHtmlDepth > elementNameStack.Count) nonHtmlDepth = -1; openElement = false; } public override void WriteAttributeString (string prefix, string localName, string nsURI, string value) { writer.Write (' '); if (prefix != null && prefix.Length!=0) { writer.Write (prefix); writer.Write (":"); } writer.Write (localName); if (nonHtmlDepth >= 0) { writer.Write ("=\""); openAttribute = true; WriteFormattedString (value); openAttribute = false; writer.Write ('\"'); return; } string attribute = localName.ToUpper (CultureInfo.InvariantCulture); string element = ((string) elementNameStack.Peek ()).ToLower (CultureInfo.InvariantCulture); if (attribute == "SELECTED" && element == "option" || attribute == "CHECKED" && element == "input") return; writer.Write ("=\""); openAttribute = true; // URI attribute should be escaped. string attrName = null; string [] attrNames = null; switch (element) { case "q": case "blockquote": case "ins": case "del": attrName = "cite"; break; case "form": attrName = "action"; break; case "a": case "area": case "link": case "base": attrName = "href"; break; case "head": attrName = "profile"; break; case "input": attrNames = new string [] {"src", "usemap"}; break; case "img": attrNames = new string [] {"src", "usemap", "longdesc"}; break; case "object": attrNames = new string [] {"classid", "codebase", "data", "archive", "usemap"}; break; case "script": attrNames = new string [] {"src", "for"}; break; } if (attrNames != null) { string attr = localName.ToLower (CultureInfo.InvariantCulture); foreach (string a in attrNames) { if (a == attr) { value = HtmlUriEscape.EscapeUri (value); break; } } } else if (attrName != null && attrName == localName.ToLower (CultureInfo.InvariantCulture)) value = HtmlUriEscape.EscapeUri (value); WriteFormattedString (value); openAttribute = false; writer.Write ('\"'); } class HtmlUriEscape : Uri { private HtmlUriEscape () : base ("urn:foo") {} public static string EscapeUri (string input) { StringBuilder sb = new StringBuilder (); int start = 0; for (int i = 0; i < input.Length; i++) { char c = input [i]; if (c < 32 || c > 127) continue; bool preserve = false; switch (c) { case '&': case '<': case '>': case '"': case '\'': preserve = true; break; default: preserve = HtmlUriEscape.IsExcludedCharacter (c); break; } if (preserve) { sb.Append (EscapeString (input.Substring (start, i - start))); sb.Append (c); start = i + 1; } } if (start < input.Length) sb.Append (EscapeString (input.Substring (start))); return sb.ToString (); } } public override void WriteComment (string text) { if (openElement) CloseStartElement (); writer.Write (""); } public override void WriteProcessingInstruction (string name, string text) { if ((text.IndexOf("?>") > 0)) throw new ArgumentException ("Processing instruction cannot contain \"?>\" as its value."); if (openElement) CloseStartElement (); if (elementNameStack.Count > 0) Indent (elementNameStack.Peek () as string, false); writer.Write ("= 0) writer.Write ("?>"); else writer.Write (">"); // HTML PI ends with '>' } public override void WriteString (string text) { if (openElement) CloseStartElement (); WriteFormattedString (text); } private void WriteFormattedString (string text) { // style and script should not be escaped. if (!openAttribute && elementNameStack.Count > 0) { string element = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture); switch (element) { case "SCRIPT": case "STYLE": writer.Write (text); return; } } int start = 0; for (int i = 0; i < text.Length; i++) { switch (text [i]) { case '&': // '&' '{' should be "&{", not "&{" if (nonHtmlDepth < 0 && i + 1 < text.Length && text [i + 1] == '{') continue; writer.Write (text.ToCharArray (), start, i - start); writer.Write ("&"); start = i + 1; break; case '<': if (openAttribute) continue; writer.Write (text.ToCharArray (), start, i - start); writer.Write ("<"); start = i + 1; break; case '>': if (openAttribute) continue; writer.Write (text.ToCharArray (), start, i - start); writer.Write (">"); start = i + 1; break; case '\"': if (!openAttribute) continue; writer.Write (text.ToCharArray (), start, i - start); writer.Write ("""); start = i + 1; break; } } if (text.Length > start) writer.Write (text.ToCharArray (), start, text.Length - start); } public override void WriteRaw (string data) { if (openElement) CloseStartElement (); writer.Write (data); } public override void WriteCDataSection (string text) { if (openElement) CloseStartElement (); // writer.Write (""); } public override void WriteWhitespace (string value) { if (openElement) CloseStartElement (); writer.Write (value); } public override void Done () { writer.Flush (); } } }