**** Merged from MCS ****
[mono-project.git] / mcs / class / System.XML / Mono.Xml.Xsl / HtmlEmitter.cs
blobad5872c3c9a9cbfabc95819b8636173a58cc73fa
1 //
2 // HtmlEmitter.cs
3 //
4 // Author:
5 // Atsushi Enomoto <ginga@kit.hi-ho.ne.jp>
6 //
7 // (C)2003 Atsushi Enomoto
8 //
9 // TODO:
10 // indent, uri escape, allowed entity char such as &nbsp;,
11 // encoding to meta tag, doctype-public/doctype-system.
15 // Permission is hereby granted, free of charge, to any person obtaining
16 // a copy of this software and associated documentation files (the
17 // "Software"), to deal in the Software without restriction, including
18 // without limitation the rights to use, copy, modify, merge, publish,
19 // distribute, sublicense, and/or sell copies of the Software, and to
20 // permit persons to whom the Software is furnished to do so, subject to
21 // the following conditions:
22 //
23 // The above copyright notice and this permission notice shall be
24 // included in all copies or substantial portions of the Software.
25 //
26 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
30 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
31 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
32 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 using System;
36 using System.Collections;
37 using System.Globalization;
38 using System.IO;
39 using System.Text;
40 using System.Xml;
42 namespace Mono.Xml.Xsl
44 internal class HtmlEmitter : Emitter
46 TextWriter writer;
47 Stack elementNameStack;
48 bool openElement;
49 bool openAttribute;
50 int nonHtmlDepth;
51 bool indent;
52 Encoding outputEncoding;
54 public HtmlEmitter (TextWriter writer, XslOutput output)
56 this.writer = writer;
57 indent = !(output.Indent == "no");
58 elementNameStack = new Stack ();
59 nonHtmlDepth = -1;
60 outputEncoding = writer.Encoding == null ? output.Encoding : writer.Encoding;
63 public override void WriteStartDocument (Encoding encoding, StandaloneType standalone)
65 // do nothing
68 public override void WriteEndDocument ()
70 // do nothing
73 public override void WriteDocType (string name, string publicId, string systemId)
75 writer.Write ("<!DOCTYPE html ");
76 if (publicId != null && publicId != String.Empty) {
77 writer.Write ("PUBLIC \"");
78 writer.Write (publicId);
79 writer.Write ("\" ");
80 if (systemId != null && systemId != String.Empty) {
81 writer.Write ("\"");
82 writer.Write (systemId);
83 writer.Write ("\"");
85 } else if (systemId != null && systemId != String.Empty) {
86 writer.Write ("SYSTEM \"");
87 writer.Write (systemId);
88 writer.Write ('\"');
90 writer.Write ('>');
91 if (indent)
92 writer.WriteLine ();
95 private void CloseAttribute ()
97 writer.Write ('\"');
98 openAttribute = false;
101 private void CloseStartElement ()
103 if (openAttribute)
104 CloseAttribute ();
105 writer.Write ('>');
106 openElement = false;
108 if (outputEncoding != null && elementNameStack.Count > 0) {
109 string name = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture);
110 switch (name) {
111 case "HEAD":
112 WriteStartElement (String.Empty, "META", String.Empty);
113 WriteAttributeString (String.Empty, "http-equiv", String.Empty, "Content-Type");
114 WriteAttributeString (String.Empty, "content", String.Empty, "text/html; charset=" + outputEncoding.WebName);
115 WriteEndElement ();
116 break;
117 case "STYLE":
118 case "SCRIPT":
119 writer.WriteLine ();
120 for (int i = 0; i <= elementNameStack.Count; i++)
121 writer.Write (" ");
122 break;
127 // FIXME: check all HTML elements' indentation.
128 private void Indent (string elementName, bool endIndent)
130 if (!indent)
131 return;
132 switch (elementName.ToUpper (CultureInfo.InvariantCulture)) {
133 case "ADDRESS":
134 case "APPLET":
135 case "BDO":
136 case "BLOCKQUOTE":
137 case "BODY":
138 case "BUTTON":
139 case "CAPTION":
140 case "CENTER":
141 case "DD":
142 case "DEL":
143 case "DIR":
144 case "DIV":
145 case "DL":
146 case "DT":
147 case "FIELDSET":
148 case "HEAD":
149 case "HTML":
150 case "IFRAME":
151 case "INS":
152 case "LI":
153 case "MAP":
154 case "MENU":
155 case "NOFRAMES":
156 case "NOSCRIPT":
157 case "OBJECT":
158 case "OPTION":
159 case "PRE":
160 case "TABLE":
161 case "TD":
162 case "TH":
163 case "TR":
164 writer.Write (writer.NewLine);
165 int count = elementNameStack.Count;
166 for (int i = 0; i < count; i++)
167 writer.Write (" ");
168 break;
169 default:
170 if (elementName.Length > 0 && nonHtmlDepth > 0)
171 goto case "HTML";
172 break;
176 public override void WriteStartElement (string prefix, string localName, string nsURI)
178 if (openElement)
179 CloseStartElement ();
180 Indent (elementNameStack.Count > 0 ? elementNameStack.Peek () as String : String.Empty, false);
181 string formatName = localName;
183 writer.Write ('<');
184 if (nsURI != String.Empty || !IsHtmlElement (localName)) {
185 // XML output
186 if (prefix != String.Empty) {
187 writer.Write (prefix);
188 writer.Write (':');
189 formatName = String.Concat (prefix, ":", localName);
192 if (nonHtmlDepth < 0)
193 nonHtmlDepth = elementNameStack.Count + 1;
195 writer.Write (formatName);
196 elementNameStack.Push (formatName);
197 openElement = true;
200 private bool IsHtmlElement (string localName)
202 // see http://www.w3.org/TR/html401/index/elements.html
203 switch (localName.ToUpper (CultureInfo.InvariantCulture)) {
204 case "A": case "ABBR": case "ACRONYM":
205 case "ADDRESS": case "APPLET": case "AREA":
206 case "B": case "BASE": case "BASEFONT": case "BDO": case "BIG":
207 case "BLOCKQUOTE": case "BODY": case "BR": case "BUTTON":
208 case "CAPTION": case "CENTER": case "CITE":
209 case "CODE": case "COL": case "COLGROUP":
210 case "DD": case "DEL": case "DFN": case "DIR":
211 case "DIV": case "DL": case "DT":
212 case "EM":
213 case "FIELDSET": case "FONT": case "FORM": case "FRAME": case "FRAMESET":
214 case "H1": case "H2": case "H3": case "H4": case "H5": case "H6":
215 case "HEAD": case "HR": case "HTML":
216 case "I": case "IFRAME": case "IMG":
217 case "INPUT": case "INS": case "ISINDEX":
218 case "KBD":
219 case "LABEL": case "LEGEND": case "LI": case "LINK":
220 case "MAP": case "MENU": case "META":
221 case "NOFRAMES": case "NOSCRIPT":
222 case "OBJECT": case "OL": case "OPTGROUP": case "OPTION":
223 case "P": case "PARAM": case "PRE":
224 case "Q":
225 case "S": case "SAMP": case "SCRIPT": case "SELECT":
226 case "SMALL": case "SPAN": case "STRIKE": case "STRONG":
227 case "STYLE": case "SUB": case "SUP":
228 case "TABLE": case "TBODY": case "TD": case "TEXTAREA":
229 case "TFOOT": case "TH": case "THEAD": case "TITLE":
230 case "TR": case "TT": case "U": case "UL": case "VAR":
231 return true;
233 return false;
236 public override void WriteEndElement ()
238 WriteFullEndElement ();
241 public override void WriteFullEndElement ()
243 string element = elementNameStack.Peek () as string;
244 switch (element.ToUpper (CultureInfo.InvariantCulture)) {
245 case "AREA":
246 case "BASE":
247 case "BASEFONT":
248 case "BR":
249 case "COL":
250 case "FRAME":
251 case "HR":
252 case "IMG":
253 case "INPUT":
254 case "ISINDEX":
255 case "LINK":
256 case "META":
257 case "PARAM":
258 if (openAttribute)
259 CloseAttribute ();
260 writer.Write ('>');
261 elementNameStack.Pop ();
262 break;
263 default:
264 if (openElement)
265 CloseStartElement ();
266 elementNameStack.Pop ();
267 if (IsHtmlElement (element))
268 Indent (element as string, true);
269 writer.Write ("</");
270 writer.Write (element);
271 writer.Write (">");
272 break;
274 if (nonHtmlDepth > elementNameStack.Count)
275 nonHtmlDepth = -1;
276 openElement = false;
279 public override void WriteAttributeString (string prefix, string localName, string nsURI, string value)
281 if (nonHtmlDepth >= 0) {
282 writer.Write (' ');
283 writer.Write (localName);
284 writer.Write ("=\"");
285 openAttribute = true;
286 WriteFormattedString (value);
287 openAttribute = false;
288 writer.Write ('\"');
290 return;
293 string attribute = localName.ToUpper (CultureInfo.InvariantCulture);
294 writer.Write (' ');
295 writer.Write (localName);
297 switch (attribute) {
298 case "OPTION":
299 case "CHECKED":
300 case "SELECTED":
301 return;
304 writer.Write ("=\"");
305 openAttribute = true;
307 // URI attribute should be escaped.
308 string element = ((string) elementNameStack.Peek ()).ToLower (CultureInfo.InvariantCulture);
309 string attrName = null;
310 string [] attrNames = null;
311 switch (element) {
312 case "q":
313 case "blockquote":
314 case "ins":
315 case "del":
316 attrName = "cite";
317 break;
318 case "form":
319 attrName = "action";
320 break;
321 case "a":
322 case "area":
323 case "link":
324 case "base":
325 attrName = "href";
326 break;
327 case "head":
328 attrName = "profile";
329 break;
330 case "input":
331 attrNames = new string [] {"src", "usemap"};
332 break;
333 case "img":
334 attrNames = new string [] {"src", "usemap", "longdesc"};
335 break;
336 case "object":
337 attrNames = new string [] {"classid", "codebase", "data", "archive", "usemap"};
338 break;
339 case "script":
340 attrNames = new string [] {"src", "for"};
341 break;
343 if (attrNames != null) {
344 string attr = localName.ToLower (CultureInfo.InvariantCulture);
345 foreach (string a in attrNames) {
346 if (a == attr) {
347 value = HtmlUriEscape.EscapeUri (value);
348 break;
352 else if (attrName != null && attrName == localName.ToLower (CultureInfo.InvariantCulture))
353 value = HtmlUriEscape.EscapeUri (value);
354 WriteFormattedString (value);
355 openAttribute = false;
356 writer.Write ('\"');
359 class HtmlUriEscape : Uri
361 private HtmlUriEscape () : base ("urn:foo") {}
363 public static string EscapeUri (string input)
365 StringBuilder sb = new StringBuilder ();
366 int start = 0;
367 for (int i = 0; i < input.Length; i++) {
368 char c = input [i];
369 if (c < 32 || c > 127)
370 continue;
371 bool preserve = false;
372 switch (c) {
373 case '&':
374 case '<':
375 case '>':
376 case '"':
377 case '\'':
378 preserve = true;
379 break;
380 default:
381 preserve = HtmlUriEscape.IsExcludedCharacter (c);
382 break;
384 if (preserve) {
385 sb.Append (EscapeString (input.Substring (start, i - start)));
386 sb.Append (c);
387 start = i + 1;
390 if (start < input.Length)
391 sb.Append (EscapeString (input.Substring (start)));
392 return sb.ToString ();
396 public override void WriteComment (string text) {
397 if (openElement)
398 CloseStartElement ();
399 writer.Write ("<!--");
400 writer.Write (text);
401 writer.Write ("-->");
404 public override void WriteProcessingInstruction (string name, string text)
406 if ((text.IndexOf("?>") > 0))
407 throw new ArgumentException ("Processing instruction cannot contain \"?>\" as its value.");
408 if (openElement)
409 CloseStartElement ();
411 if (elementNameStack.Count > 0)
412 Indent (elementNameStack.Peek () as string, false);
414 writer.Write ("<?");
415 writer.Write (name);
416 if (text != null && text != String.Empty) {
417 writer.Write (' ');
418 writer.Write (text);
421 if (nonHtmlDepth >= 0)
422 writer.Write ("?>");
423 else
424 writer.Write (">"); // HTML PI ends with '>'
427 public override void WriteString (string text)
429 if (openElement)
430 CloseStartElement ();
431 WriteFormattedString (text);
434 private void WriteFormattedString (string text)
436 // style and script should not be escaped.
437 if (!openAttribute) {
438 string element = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture);
439 switch (element) {
440 case "SCRIPT":
441 case "STYLE":
442 writer.Write (text);
443 return;
447 int start = 0;
448 for (int i = 0; i < text.Length; i++) {
449 switch (text [i]) {
450 case '&':
451 // '&' '{' should be "&{", not "&amp;{"
452 if (nonHtmlDepth < 0 && i + 1 < text.Length && text [i + 1] == '{')
453 continue;
454 writer.Write (text.ToCharArray (), start, i - start);
455 writer.Write ("&amp;");
456 start = i + 1;
457 break;
458 case '<':
459 if (openAttribute)
460 continue;
461 writer.Write (text.ToCharArray (), start, i - start);
462 writer.Write ("&lt;");
463 start = i + 1;
464 break;
465 case '>':
466 if (openAttribute)
467 continue;
468 writer.Write (text.ToCharArray (), start, i - start);
469 writer.Write ("&gt;");
470 start = i + 1;
471 break;
472 case '\"':
473 if (!openAttribute)
474 continue;
475 writer.Write (text.ToCharArray (), start, i - start);
476 writer.Write ("&quot;");
477 start = i + 1;
478 break;
481 if (text.Length > start)
482 writer.Write (text.ToCharArray (), start, text.Length - start);
485 public override void WriteRaw (string data)
487 if (openElement)
488 CloseStartElement ();
489 writer.Write (data);
492 public override void WriteCDataSection (string text) {
493 if (openElement)
494 CloseStartElement ();
495 writer.Write ("<![CDATA[");
496 writer.Write (text);
497 writer.Write ("]]>");
500 public override void WriteWhitespace (string value)
502 if (openElement)
503 CloseStartElement ();
504 writer.Write (value);
507 public override void Done ()
509 writer.Flush ();