System.Drawing: added email to icon and test file headers
[mono.git] / mcs / class / System.XML / Mono.Xml.Xsl / HtmlEmitter.cs
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.
12 //
13
14 //
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.
33 //
34
35 using System;
36 using System.Collections;
37 using System.Globalization;
38 using System.IO;
39 using System.Text;
40 using System.Xml;
41
42 namespace Mono.Xml.Xsl
43 {
44         internal class HtmlEmitter : Emitter
45         {
46                 TextWriter writer;
47                 Stack elementNameStack;
48                 bool openElement;
49                 bool openAttribute;
50                 int nonHtmlDepth;
51                 bool indent;
52                 Encoding outputEncoding;
53                 string mediaType;
54
55                 public HtmlEmitter (TextWriter writer, XslOutput output)
56                 {
57                         this.writer = writer;
58                         indent = output.Indent == "yes" || output.Indent == null;
59                         elementNameStack = new Stack ();
60                         nonHtmlDepth = -1;
61                         outputEncoding = writer.Encoding == null ? output.Encoding : writer.Encoding;
62                         mediaType = output.MediaType;
63                         if (mediaType == null || mediaType.Length == 0)
64                                 mediaType = "text/html";
65                 }
66
67                 public override void WriteStartDocument (Encoding encoding, StandaloneType standalone)
68                 {
69                         // do nothing
70                 }
71                 
72                 public override void WriteEndDocument ()
73                 {
74                         // do nothing
75                 }
76
77                 public override void WriteDocType (string name, string publicId, string systemId)
78                 {
79                         writer.Write ("<!DOCTYPE html ");
80                         if (publicId != null) {
81                                 writer.Write ("PUBLIC \"");
82                                 writer.Write (publicId);
83                                 writer.Write ("\" ");
84                                 if (systemId != null) {
85                                         writer.Write ("\"");
86                                         writer.Write (systemId);
87                                         writer.Write ("\"");
88                                 }
89                         } else if (systemId != null) {
90                                 writer.Write ("SYSTEM \"");
91                                 writer.Write (systemId);
92                                 writer.Write ('\"');
93                         }
94                         writer.Write ('>');
95                         if (indent)
96                                 writer.WriteLine ();
97                 }
98
99                 private void CloseAttribute ()
100                 {
101                         writer.Write ('\"');
102                         openAttribute = false;
103                 }
104
105                 private void CloseStartElement ()
106                 {
107                         //FIXME: consider sanity check if (openElement) return;
108                         if (openAttribute)
109                                 CloseAttribute ();
110                         writer.Write ('>');
111                         openElement = false;
112
113                         if (outputEncoding != null && elementNameStack.Count > 0) {
114                                 string name = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture);
115                                 switch (name) {
116                                 case "HEAD":
117                                         WriteStartElement (String.Empty, "META", String.Empty);
118                                         WriteAttributeString (String.Empty, "http-equiv", String.Empty, "Content-Type");
119                                         WriteAttributeString (String.Empty, "content", String.Empty, String.Concat (mediaType, "; charset=", outputEncoding.WebName));
120                                         WriteEndElement ();
121                                         break;
122                                 case "STYLE":
123                                 case "SCRIPT":
124                                         writer.WriteLine ();
125                                         for (int i = 0; i <= elementNameStack.Count; i++)
126                                                 writer.Write ("  ");
127                                         break;
128                                 }
129                         }
130                 }
131
132                 // FIXME: check all HTML elements' indentation.
133                 private void Indent (string elementName, bool endIndent)
134                 {
135                         if (!indent)
136                                 return;
137                         switch (elementName.ToUpper (CultureInfo.InvariantCulture)) {
138                         case "ADDRESS":
139                         case "APPLET":
140                         case "BDO":
141                         case "BLOCKQUOTE":
142                         case "BODY":
143                         case "BUTTON":
144                         case "CAPTION":
145                         case "CENTER":
146                         case "DD":
147                         case "DEL":
148                         case "DIR":
149                         case "DIV":
150                         case "DL":
151                         case "DT":
152                         case "FIELDSET":
153                         case "HEAD":
154                         case "HTML":
155                         case "IFRAME":
156                         case "INS":
157                         case "LI":
158                         case "MAP":
159                         case "MENU":
160                         case "NOFRAMES":
161                         case "NOSCRIPT":
162                         case "OBJECT":
163                         case "OPTION":
164                         case "PRE":
165                         case "TABLE":
166                         case "TD":
167                         case "TH":
168                         case "TR":
169                                 writer.Write (writer.NewLine);
170                                 int count = elementNameStack.Count;
171                                 for (int i = 0; i < count; i++)
172                                         writer.Write ("  ");
173                                 break;
174                         default:
175                                 if (elementName.Length > 0 && nonHtmlDepth > 0)
176                                         goto case "HTML";
177                                 break;
178                         }
179                 }
180
181                 public override void WriteStartElement (string prefix, string localName, string nsURI)
182                 {
183                         if (openElement)
184                                 CloseStartElement ();
185                         Indent (elementNameStack.Count > 0 ? elementNameStack.Peek () as String : String.Empty, false);
186                         string formatName = localName;
187
188                         writer.Write ('<');
189                         if (nsURI != String.Empty) {// && !IsHtmlElement (localName)) {
190                                 // XML output
191                                 if (prefix != String.Empty) {
192                                         formatName = String.Concat (prefix, ":", localName);
193                                 }
194                                 
195                                 if (nonHtmlDepth < 0)
196                                         nonHtmlDepth = elementNameStack.Count + 1;
197                         }
198                         writer.Write (formatName);
199                         elementNameStack.Push (formatName);
200                         openElement = true;
201                 }
202
203                 private bool IsHtmlElement (string localName)
204                 {
205                         // see http://www.w3.org/TR/html401/index/elements.html
206                         switch (localName.ToUpper (CultureInfo.InvariantCulture)) {
207                         case "A": case "ABBR": case "ACRONYM":
208                         case "ADDRESS": case "APPLET": case "AREA":
209                         case "B": case "BASE": case "BASEFONT": case "BDO": case "BIG": 
210                         case "BLOCKQUOTE": case "BODY": case "BR": case "BUTTON":
211                         case "CAPTION": case "CENTER": case "CITE":
212                         case "CODE": case "COL": case "COLGROUP":
213                         case "DD": case "DEL": case "DFN": case "DIR":
214                         case "DIV": case "DL": case "DT":
215                         case "EM":
216                         case "FIELDSET": case "FONT": case "FORM": case "FRAME": case "FRAMESET":
217                         case "H1": case "H2": case "H3": case "H4": case "H5": case "H6":
218                         case "HEAD": case "HR": case "HTML":
219                         case "I": case "IFRAME": case "IMG":
220                         case "INPUT": case "INS": case "ISINDEX":
221                         case "KBD":
222                         case "LABEL": case "LEGEND": case "LI": case "LINK":
223                         case "MAP": case "MENU": case "META":
224                         case "NOFRAMES": case "NOSCRIPT":
225                         case "OBJECT": case "OL": case "OPTGROUP": case "OPTION":
226                         case "P": case "PARAM": case "PRE":
227                         case "Q":
228                         case "S": case "SAMP": case "SCRIPT": case "SELECT":
229                         case "SMALL": case "SPAN": case "STRIKE": case "STRONG":
230                         case "STYLE": case "SUB": case "SUP":
231                         case "TABLE": case "TBODY": case "TD": case "TEXTAREA":
232                         case "TFOOT": case "TH": case "THEAD": case "TITLE":
233                         case "TR": case "TT": case "U": case "UL": case "VAR":
234                                 return true;
235                         }
236                         return false;
237                 }
238
239                 public override void WriteEndElement ()
240                 {
241                         WriteFullEndElement ();
242                 }
243
244                 public override void WriteFullEndElement ()
245                 {
246                         string element = elementNameStack.Peek () as string;
247                         switch (element.ToUpper (CultureInfo.InvariantCulture)) {
248                         case "AREA":
249                         case "BASE":
250                         case "BASEFONT":
251                         case "BR":
252                         case "COL":
253                         case "FRAME":
254                         case "HR":
255                         case "IMG":
256                         case "INPUT":
257                         case "ISINDEX":
258                         case "LINK":
259                         case "META":
260                         case "PARAM":
261                                 if (openAttribute)
262                                         CloseAttribute ();
263                                 if (openElement)
264                                         writer.Write ('>'); //FIXME: consider using CloseStartElement() to write '>'
265                                 elementNameStack.Pop ();
266                                 break;
267                         default:
268                                 if (openElement)
269                                         CloseStartElement ();
270                                 elementNameStack.Pop ();
271                                 if (IsHtmlElement (element))
272                                         Indent (element as string, true);
273                                 writer.Write ("</");
274                                 writer.Write (element);
275                                 writer.Write (">");
276                                 break;
277                         }
278                         if (nonHtmlDepth > elementNameStack.Count)
279                                 nonHtmlDepth = -1;
280                         openElement = false;
281                 }
282
283                 public override void WriteAttributeString (string prefix, string localName, string nsURI, string value)
284                 {
285                         writer.Write (' ');
286                         if (prefix != null && prefix.Length!=0)
287                         {
288                                 writer.Write (prefix);
289                                 writer.Write (":");
290                         }
291                         writer.Write (localName);
292
293                         if (nonHtmlDepth >= 0) 
294                         {
295                                 writer.Write ("=\"");
296                                 openAttribute = true;
297                                 WriteFormattedString (value);
298                                 openAttribute = false;
299                                 writer.Write ('\"');
300
301                                 return;
302                         }
303
304                         string attribute = localName.ToUpper (CultureInfo.InvariantCulture);
305                         string element = ((string) elementNameStack.Peek ()).ToLower (CultureInfo.InvariantCulture);
306
307                         if (attribute == "SELECTED" && element == "option"
308                                 || attribute == "CHECKED" && element == "input")
309                                 return;
310
311                         writer.Write ("=\"");
312                         openAttribute = true;
313
314                         // URI attribute should be escaped.
315                         string attrName = null;
316                         string [] attrNames = null;
317                         switch (element) {
318                         case "q":
319                         case "blockquote":
320                         case "ins":
321                         case "del":
322                                 attrName = "cite";
323                                 break;
324                         case "form":
325                                 attrName = "action";
326                                 break;
327                         case "a":
328                         case "area":
329                         case "link":
330                         case "base":
331                                 attrName = "href";
332                                 break;
333                         case "head":
334                                 attrName = "profile";
335                                 break;
336                         case "input":
337                                 attrNames = new string [] {"src", "usemap"};
338                                 break;
339                         case "img":
340                                 attrNames = new string [] {"src", "usemap", "longdesc"};
341                                 break;
342                         case "object":
343                                 attrNames = new string [] {"classid", "codebase", "data", "archive", "usemap"};
344                                 break;
345                         case "script":
346                                 attrNames = new string [] {"src", "for"};
347                                 break;
348                         }
349                         if (attrNames != null) {
350                                 string attr = localName.ToLower (CultureInfo.InvariantCulture);
351                                 foreach (string a in attrNames) {
352                                         if (a == attr) {
353                                                 value = HtmlUriEscape.EscapeUri (value);
354                                                 break;
355                                         }
356                                 }
357                         }
358                         else if (attrName != null && attrName == localName.ToLower (CultureInfo.InvariantCulture))
359                                 value = HtmlUriEscape.EscapeUri (value);
360                         WriteFormattedString (value);
361                         openAttribute = false;
362                         writer.Write ('\"');
363                 }
364
365                 class HtmlUriEscape : Uri
366                 {
367                         private HtmlUriEscape () : base ("urn:foo") {}
368
369                         public static string EscapeUri (string input)
370                         {
371                                 StringBuilder sb = new StringBuilder ();
372                                 int start = 0;
373                                 for (int i = 0; i < input.Length; i++) {
374                                         char c = input [i];
375                                         if (c < 32 || c > 127)
376                                                 continue;
377                                         bool preserve = false;
378                                         switch (c) {
379                                         case '&':
380                                         case '<':
381                                         case '>':
382                                         case '"':
383                                         case '\'':
384                                                 preserve = true;
385                                                 break;
386                                         default:
387                                                 preserve = HtmlUriEscape.IsExcludedCharacter (c);
388                                                 break;
389                                         }
390                                         if (preserve) {
391                                                 sb.Append (EscapeString (input.Substring (start, i - start)));
392                                                 sb.Append (c);
393                                                 start = i + 1;
394                                         }
395                                 }
396                                 if (start < input.Length)
397                                         sb.Append (EscapeString (input.Substring (start)));
398                                 return sb.ToString ();
399                         }
400                 }
401
402                 public override void WriteComment (string text) {
403                         if (openElement)
404                                 CloseStartElement ();
405                         writer.Write ("<!--");
406                         writer.Write (text);
407                         writer.Write ("-->");
408                 }
409
410                 public override void WriteProcessingInstruction (string name, string text)
411                 {
412                         if ((text.IndexOf("?>") > 0))
413                                 throw new ArgumentException ("Processing instruction cannot contain \"?>\" as its value.");
414                         if (openElement)
415                                 CloseStartElement ();
416
417                         if (elementNameStack.Count > 0)
418                                 Indent (elementNameStack.Peek () as string, false);
419
420                         writer.Write ("<?");
421                         writer.Write (name);
422                         if (text != null && text != String.Empty) {
423                                 writer.Write (' ');
424                                 writer.Write (text);
425                         }
426
427                         if (nonHtmlDepth >= 0)
428                                 writer.Write ("?>");
429                         else
430                                 writer.Write (">"); // HTML PI ends with '>'
431                 }
432
433                 public override void WriteString (string text)
434                 {
435                         if (openElement)
436                                 CloseStartElement ();
437                         WriteFormattedString (text);
438                 }
439
440                 private void WriteFormattedString (string text)
441                 {
442                         // style and script should not be escaped.
443                         if (!openAttribute && elementNameStack.Count > 0) {
444                                 string element = ((string) elementNameStack.Peek ()).ToUpper (CultureInfo.InvariantCulture);
445                                 switch (element) {
446                                 case "SCRIPT":
447                                 case "STYLE":
448                                         writer.Write (text);
449                                         return;
450                                 }
451                         }
452
453                         int start = 0;
454                         for (int i = 0; i < text.Length; i++) {
455                                 switch (text [i]) {
456                                 case '&':
457                                         // '&' '{' should be "&{", not "&amp;{"
458                                         if (nonHtmlDepth < 0 && i + 1 < text.Length && text [i + 1] == '{')
459                                                 continue;
460                                         writer.Write (text.ToCharArray (), start, i - start);
461                                         writer.Write ("&amp;");
462                                         start = i + 1;
463                                         break;
464                                 case '<':
465                                         if (openAttribute)
466                                                 continue;
467                                         writer.Write (text.ToCharArray (), start, i - start);
468                                         writer.Write ("&lt;");
469                                         start = i + 1;
470                                         break;
471                                 case '>':
472                                         if (openAttribute)
473                                                 continue;
474                                         writer.Write (text.ToCharArray (), start, i - start);
475                                         writer.Write ("&gt;");
476                                         start = i + 1;
477                                         break;
478                                 case '\"':
479                                         if (!openAttribute)
480                                                 continue;
481                                         writer.Write (text.ToCharArray (), start, i - start);
482                                         writer.Write ("&quot;");
483                                         start = i + 1;
484                                         break;
485                                 }
486                         }
487                         if (text.Length > start)
488                                 writer.Write (text.ToCharArray (), start, text.Length - start);
489                 }
490
491                 public override void WriteRaw (string data)
492                 {
493                         if (openElement)
494                                 CloseStartElement ();
495                         writer.Write (data);
496                 }
497
498                 public override void WriteCDataSection (string text) {
499                         if (openElement)
500                                 CloseStartElement ();
501 //                      writer.Write ("<![CDATA[");
502                         writer.Write (text);
503 //                      writer.Write ("]]>");
504                 }
505
506                 public override void WriteWhitespace (string value)
507                 {
508                         if (openElement)
509                                 CloseStartElement ();
510                         writer.Write (value);
511                 }
512
513                 public override void Done ()
514                 {
515                         writer.Flush ();
516                 }
517         }
518 }