2002-12-24 Atsushi Enomoto <ginga@kit.hi-ho.ne.jp>
[mono.git] / mcs / class / System.XML / System.Xml / XmlTextWriter.cs
1 //
2 // System.Xml.XmlTextWriter
3 //
4 // Author:
5 //   Kral Ferch <kral_ferch@hotmail.com>
6 //
7 // (C) 2002 Kral Ferch
8 //
9
10 using System;
11 using System.Collections;
12 using System.IO;
13 using System.Text;
14
15 namespace System.Xml
16 {
17         public class XmlTextWriter : XmlWriter
18         {
19                 #region Fields
20
21                 TextWriter w;
22                 bool nullEncoding = false;
23                 bool openWriter = true;
24                 bool openStartElement = false;
25                 bool openStartAttribute = false;
26                 bool documentStarted = false;
27                 bool namespaces = true;
28                 bool openAttribute = false;
29                 bool attributeWrittenForElement = false;
30                 Stack openElements = new Stack ();
31                 Formatting formatting = Formatting.None;
32                 int indentation = 2;
33                 char indentChar = ' ';
34                 string indentChars = "  ";
35                 char quoteChar = '\"';
36                 int indentLevel = 0;
37                 string indentFormatting;
38                 Stream baseStream = null;
39                 string xmlLang = null;
40                 XmlSpace xmlSpace = XmlSpace.None;
41                 bool openXmlLang = false;
42                 bool openXmlSpace = false;
43
44                 #endregion
45
46                 #region Constructors
47
48                 public XmlTextWriter (TextWriter w) : base ()
49                 {
50                         this.w = w;
51                         nullEncoding = (w.Encoding == null);
52                         
53                         try {
54                                 baseStream = ((StreamWriter)w).BaseStream;
55                         }
56                         catch (Exception) { }
57                 }
58
59                 public XmlTextWriter (Stream w, Encoding encoding) : base ()
60                 {
61                         if (encoding == null) {
62                                 nullEncoding = true;
63                                 encoding = new UTF8Encoding ();
64                         }
65
66                         this.w = new StreamWriter(w, encoding);
67                         baseStream = w;
68                 }
69
70                 public XmlTextWriter (string filename, Encoding encoding) :
71                         this (new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None), encoding)
72                 {
73                 }
74
75                 #endregion
76
77                 #region Properties
78
79                 public Stream BaseStream {
80                         get { return baseStream; }
81                 }
82
83
84                 public Formatting Formatting {
85                         get { return formatting; }
86                         set { formatting = value; }
87                 }
88
89                 private bool IndentingOverriden 
90                 {
91                         get {
92                                 if (openElements.Count == 0)
93                                         return false;
94                                 else
95                                         return (((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden);
96                         }
97                         set {
98                                 if (openElements.Count > 0)
99                                         ((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden = value;
100                         }
101                 }
102
103                 public int Indentation {
104                         get { return indentation; }
105                         set {
106                                 indentation = value;
107                                 UpdateIndentChars ();
108                         }
109                 }
110
111                 public char IndentChar {
112                         get { return indentChar; }
113                         set {
114                                 indentChar = value;
115                                 UpdateIndentChars ();
116                         }
117                 }
118
119                 public bool Namespaces {
120                         get { return namespaces; }
121                         set {
122                                 if (ws != WriteState.Start)
123                                         throw new InvalidOperationException ("NotInWriteState.");
124                                 
125                                 namespaces = value;
126                         }
127                 }
128
129                 public char QuoteChar {
130                         get { return quoteChar; }
131                         set {
132                                 if ((value != '\'') && (value != '\"'))
133                                         throw new ArgumentException ("This is an invalid XML attribute quote character. Valid attribute quote characters are ' and \".");
134                                 
135                                 quoteChar = value;
136                         }
137                 }
138
139                 public override WriteState WriteState {
140                         get { return ws; }
141                 }
142                 
143                 public override string XmlLang {
144                         get {
145                                 string xmlLang = null;
146                                 int i;
147
148                                 for (i = 0; i < openElements.Count; i++) 
149                                 {
150                                         xmlLang = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlLang;
151                                         if (xmlLang != null)
152                                                 break;
153                                 }
154
155                                 return xmlLang;
156                         }
157                 }
158
159                 public override XmlSpace XmlSpace {
160                         get {
161                                 XmlSpace xmlSpace = XmlSpace.None;
162                                 int i;
163
164                                 for (i = 0; i < openElements.Count; i++) 
165                                 {
166                                         xmlSpace = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlSpace;
167                                         if (xmlSpace != XmlSpace.None)
168                                                 break;
169                                 }
170
171                                 return xmlSpace;
172                         }
173                 }
174
175                 #endregion
176
177                 #region Methods
178
179                 private void CheckState ()
180                 {
181                         if (!openWriter) {
182                                 throw new InvalidOperationException ("The Writer is closed.");
183                         }
184                         if ((documentStarted == true) && (formatting == Formatting.Indented) && (!IndentingOverriden)) {
185                                 indentFormatting = "\r\n";
186                                 if (indentLevel > 0) {
187                                         for (int i = 0; i < indentLevel; i++)
188                                                 indentFormatting += indentChars;
189                                 }
190                         }
191                         else
192                                 indentFormatting = "";
193
194                         documentStarted = true;
195                 }
196
197                 public override void Close ()
198                 {
199                         CloseOpenAttributeAndElements ();
200
201                         w.Close();
202                         ws = WriteState.Closed;
203                         openWriter = false;
204                 }
205
206                 private void CloseOpenAttributeAndElements ()
207                 {
208                         if (openAttribute)
209                                 WriteEndAttribute ();
210
211                         while (openElements.Count > 0) {
212                                 WriteEndElement();
213                         }
214                 }
215
216                 private void CloseStartElement ()
217                 {
218                         if (openStartElement) {
219                                 w.Write(">");
220                                 ws = WriteState.Content;
221                                 openStartElement = false;
222                                 attributeWrittenForElement = false;
223                         }
224                 }
225
226                 public override void Flush ()
227                 {
228                         w.Flush ();
229                 }
230
231                 public override string LookupPrefix (string ns)
232                 {
233                         string prefix = namespaceManager.LookupPrefix (ns);
234
235                         if (prefix == String.Empty)
236                                 prefix = null;
237
238                         return prefix;
239                 }
240
241                 private void UpdateIndentChars ()
242                 {
243                         indentChars = "";
244                         for (int i = 0; i < indentation; i++)
245                                 indentChars += indentChar;
246                 }
247
248                 public override void WriteBase64 (byte[] buffer, int index, int count)
249                 {
250                         w.Write (Convert.ToBase64String (buffer, index, count));
251                 }
252
253                 [MonoTODO]
254                 public override void WriteBinHex (byte[] buffer, int index, int count)
255                 {
256                         throw new NotImplementedException ();
257                 }
258
259                 public override void WriteCData (string text)
260                 {
261                         if (text.IndexOf("]]>") > 0)
262                                 throw new ArgumentException ();
263
264                         CheckState ();
265                         CloseStartElement ();
266
267                         w.Write("<![CDATA[{0}]]>", text);
268                 }
269
270                 public override void WriteCharEntity (char ch)
271                 {
272                         Int16   intCh = (Int16)ch;
273
274                         // Make sure the character is not in the surrogate pair
275                         // character range, 0xd800- 0xdfff
276                         if ((intCh >= -10240) && (intCh <= -8193))
277                                 throw new ArgumentException ("Surrogate Pair is invalid.");
278
279                         w.Write("&#x{0:X};", intCh);
280                 }
281
282                 [MonoTODO]
283                 public override void WriteChars (char[] buffer, int index, int count)
284                 {
285                         throw new NotImplementedException ();
286                 }
287
288                 public override void WriteComment (string text)
289                 {
290                         if ((text.EndsWith("-")) || (text.IndexOf("-->") > 0)) {
291                                 throw new ArgumentException ();
292                         }
293
294                         CheckState ();
295                         CloseStartElement ();
296
297                         w.Write ("<!--{0}-->", text);
298                 }
299
300                 public override void WriteDocType (string name, string pubid, string sysid, string subset)
301                 {
302                         if (name == null || name.Trim ().Length == 0)
303                                 throw new ArgumentException ("Invalid DOCTYPE name", "name");
304
305                         w.Write ("<!DOCTYPE ");
306                         w.Write (name);
307                         if (pubid != null) {
308                                 w.Write (String.Format (" PUBLIC {0}{1}{0} {0}{2}{0}", quoteChar, pubid, sysid));
309                         } else if (sysid != null) {
310                                 w.Write (String.Format (" SYSTEM {0}{1}{0}", quoteChar, sysid));
311                         }
312
313                         if (subset != null)
314                                 w.Write ("[" + subset + "]");
315
316                         w.Write('>');
317                 }
318
319                 public override void WriteEndAttribute ()
320                 {
321                         if (!openAttribute)
322                                 throw new InvalidOperationException("Token EndAttribute in state Start would result in an invalid XML document.");
323
324                         CheckState ();
325
326                         if (openXmlLang) {
327                                 w.Write (xmlLang);
328                                 openXmlLang = false;
329                                 ((XmlTextWriterOpenElement)openElements.Peek()).XmlLang = xmlLang;
330                         }
331
332                         if (openXmlSpace) 
333                         {
334                                 w.Write (xmlSpace.ToString ().ToLower ());
335                                 openXmlSpace = false;
336                                 ((XmlTextWriterOpenElement)openElements.Peek()).XmlSpace = xmlSpace;
337                         }
338
339                         w.Write ("{0}", quoteChar);
340
341                         openAttribute = false;
342                 }
343
344                 public override void WriteEndDocument ()
345                 {
346                         CloseOpenAttributeAndElements ();
347
348                         if ((ws == WriteState.Start) || (ws == WriteState.Prolog))
349                                 throw new ArgumentException ("This document does not have a root element.");
350
351                         ws = WriteState.Start;
352                 }
353
354                 public override void WriteEndElement ()
355                 {
356                         WriteEndElementInternal (false);
357                 }
358
359                 private void WriteEndElementInternal (bool fullEndElement)
360                 {
361                         if (openElements.Count == 0)
362                                 throw new InvalidOperationException("There was no XML start tag open.");
363
364                         indentLevel--;
365                         CheckState ();
366
367                         if (openStartElement) {
368                                 if (openAttribute)
369                                         WriteEndAttribute ();
370                                 if (fullEndElement)
371                                         w.Write ("></{0}>", ((XmlTextWriterOpenElement)openElements.Peek ()).Name);
372                                 else
373                                         w.Write (" />");
374
375                                 openElements.Pop ();
376                                 openStartElement = false;
377                         } else {
378                                 w.Write ("{0}</{1}>", indentFormatting, openElements.Pop ());
379                         }
380
381                         namespaceManager.PopScope();
382                 }
383
384                 [MonoTODO]
385                 public override void WriteEntityRef (string name)
386                 {
387                         throw new NotImplementedException ();
388                 }
389
390                 public override void WriteFullEndElement ()
391                 {
392                         WriteEndElementInternal (true);
393                 }
394
395                 private void CheckValidChars (string name, bool firstOnlyLetter)
396                 {
397                         foreach (char c in name) {
398                                 if (XmlConvert.IsInvalid (c, firstOnlyLetter))
399                                         throw new ArgumentException ("There is an invalid character: '" + c +
400                                                                      "'", "name");
401                         }
402                 }
403
404                 public override void WriteName (string name)
405                 {
406                         CheckValidChars (name, true);
407                         w.Write (name);
408                 }
409
410                 public override void WriteNmToken (string name)
411                 {
412                         CheckValidChars (name, false);
413                         w.Write (name);
414                 }
415
416                 public override void WriteProcessingInstruction (string name, string text)
417                 {
418                         if ((name == null) || (name == string.Empty) || (name.IndexOf("?>") > 0) || (text.IndexOf("?>") > 0)) {
419                                 throw new ArgumentException ();
420                         }
421
422                         CheckState ();
423                         CloseStartElement ();
424
425                         w.Write ("{0}<?{1} {2}?>", indentFormatting, name, text);
426                 }
427
428                 [MonoTODO]
429                 public override void WriteQualifiedName (string localName, string ns)
430                 {
431                         if (localName == null || localName == String.Empty)
432                                 throw new ArgumentException ();
433
434                         CheckState ();
435                         w.Write ("{0}:{1}", ns, localName);
436                 }
437
438                 public override void WriteRaw (string data)
439                 {
440                         WriteStringInternal (data, false);
441                 }
442
443                 [MonoTODO]
444                 public override void WriteRaw (char[] buffer, int index, int count)
445                 {
446                         throw new NotImplementedException ();
447                 }
448
449                 public override void WriteStartAttribute (string prefix, string localName, string ns)
450                 {
451                         if ((prefix == "xml") && (localName == "lang"))
452                                 openXmlLang = true;
453
454                         if ((prefix == "xml") && (localName == "space"))
455                                 openXmlSpace = true;
456
457                         if ((prefix == "xmlns") && (localName == "xmlns"))
458                                 throw new ArgumentException ("Prefixes beginning with \"xml\" (regardless of whether the characters are uppercase, lowercase, or some combination thereof) are reserved for use by XML.");
459
460                         CheckState ();
461
462                         if (ws == WriteState.Content)
463                                 throw new InvalidOperationException ("Token StartAttribute in state " + WriteState + " would result in an invalid XML document.");
464
465                         if (prefix == null)
466                                 prefix = String.Empty;
467
468                         if (ns == null)
469                                 ns = String.Empty;
470
471                         string formatPrefix = "";
472                         string formatSpace = "";
473
474                         if (ns != String.Empty) 
475                         {
476                                 string existingPrefix = namespaceManager.LookupPrefix (ns);
477
478                                 if (prefix == String.Empty)
479                                         prefix = existingPrefix;
480                         }
481
482                         if (prefix != String.Empty) 
483                         {
484                                 formatPrefix = prefix + ":";
485                         }
486
487                         if (openStartElement || attributeWrittenForElement)
488                                 formatSpace = " ";
489
490                         w.Write ("{0}{1}{2}={3}", formatSpace, formatPrefix, localName, quoteChar);
491
492                         openAttribute = true;
493                         attributeWrittenForElement = true;
494                         ws = WriteState.Attribute;
495                 }
496
497                 public override void WriteStartDocument ()
498                 {
499                         WriteStartDocument ("");
500                 }
501
502                 public override void WriteStartDocument (bool standalone)
503                 {
504                         string standaloneFormatting;
505
506                         if (standalone == true)
507                                 standaloneFormatting = String.Format (" standalone={0}yes{0}", quoteChar);
508                         else
509                                 standaloneFormatting = String.Format (" standalone={0}no{0}", quoteChar);
510
511                         WriteStartDocument (standaloneFormatting);
512                 }
513
514                 private void WriteStartDocument (string standaloneFormatting)
515                 {
516                         if (documentStarted == true)
517                                 throw new InvalidOperationException("WriteStartDocument should be the first call.");
518
519                         CheckState ();
520
521                         string encodingFormatting = "";
522
523                         if (!nullEncoding) 
524                                 encodingFormatting = String.Format (" encoding={0}{1}{0}", quoteChar, w.Encoding.HeaderName);
525
526                         w.Write("<?xml version={0}1.0{0}{1}{2}?>", quoteChar, encodingFormatting, standaloneFormatting);
527                         ws = WriteState.Prolog;
528                 }
529
530                 public override void WriteStartElement (string prefix, string localName, string ns)
531                 {
532                         if (!Namespaces && (((prefix != null) && (prefix != String.Empty))
533                                 || ((ns != null) && (ns != String.Empty))))
534                                 throw new ArgumentException ("Cannot set the namespace if Namespaces is 'false'.");
535
536                         WriteStartElementInternal (prefix, localName, ns);
537                 }
538
539                 private void WriteStartElementInternal (string prefix, string localName, string ns)
540                 {
541                         if (prefix == null)
542                                 prefix = String.Empty;
543
544                         if ((prefix != String.Empty) && ((ns == null) || (ns == String.Empty)))
545                                 throw new ArgumentException ("Cannot use a prefix with an empty namespace.");
546
547                         CheckState ();
548                         CloseStartElement ();
549                         
550                         string formatXmlns = "";
551                         string formatPrefix = "";
552
553                         if(ns != null)
554                         {
555                                 if (ns != String.Empty) 
556                                 {
557                                         string existingPrefix = namespaceManager.LookupPrefix (ns);
558                                         bool addDefaultNamespace = false;
559
560                                         if (existingPrefix == String.Empty && !namespaceManager.HasNamespace (prefix)) {
561                                                 namespaceManager.AddNamespace (prefix, ns);
562                                                 addDefaultNamespace = true;
563                                         }
564
565                                         if (prefix == String.Empty)
566                                                 prefix = existingPrefix;
567
568                                         if (prefix != existingPrefix)
569                                                 formatXmlns = String.Format (" xmlns:{0}={1}{2}{1}", prefix, quoteChar, ns);
570                                         else if (addDefaultNamespace)
571                                                 formatXmlns = String.Format (" xmlns={0}{1}{0}", quoteChar, ns);
572                                 }
573                                 else if ((prefix == String.Empty) && (namespaceManager.LookupNamespace(prefix) != String.Empty)) {
574                                         formatXmlns = String.Format (" xmlns={0}{0}", quoteChar);
575                                 }
576
577                                 if (prefix != String.Empty) {
578                                         formatPrefix = prefix + ":";
579                                 }
580                         }
581
582                         w.Write ("{0}<{1}{2}{3}", indentFormatting, formatPrefix, localName, formatXmlns);
583         
584
585                         openElements.Push (new XmlTextWriterOpenElement (formatPrefix + localName));
586                         ws = WriteState.Element;
587                         openStartElement = true;
588
589                         namespaceManager.PushScope ();
590                         if(ns != null)
591                         {
592                                 namespaceManager.AddNamespace (prefix, ns);
593                         }
594                         indentLevel++;
595                 }
596
597                 public override void WriteString (string text)
598                 {
599                         if (ws == WriteState.Prolog)
600                                 throw new InvalidOperationException ("Token content in state Prolog would result in an invalid XML document.");
601
602                         WriteStringInternal (text, true);
603                 }
604
605                 private void WriteStringInternal (string text, bool entitize)
606                 {
607                         if (text == null)
608                                 text = String.Empty;
609
610                         if (text != String.Empty) 
611                         {
612                                 CheckState ();
613
614                                 if (entitize)
615                                 {
616                                         text = text.Replace ("&", "&amp;");
617                                         text = text.Replace ("<", "&lt;");
618                                         text = text.Replace (">", "&gt;");
619                                         
620                                         if (openAttribute) 
621                                         {
622                                                 if (quoteChar == '"')
623                                                         text = text.Replace ("\"", "&quot;");
624                                                 else
625                                                         text = text.Replace ("'", "&apos;");
626                                         }
627                                 }
628
629                                 if (!openAttribute)
630                                 {
631                                         IndentingOverriden = true;
632                                         CloseStartElement ();
633                                 }
634                                 if (!openXmlLang && !openXmlSpace)
635                                         w.Write (text);
636                                 else 
637                                 {
638                                         if (openXmlLang)
639                                                 xmlLang = text;
640                                         else 
641                                         {
642                                                 switch (text) 
643                                                 {
644                                                         case "default":
645                                                                 xmlSpace = XmlSpace.Default;
646                                                                 break;
647                                                         case "preserve":
648                                                                 xmlSpace = XmlSpace.Preserve;
649                                                                 break;
650                                                         default:
651                                                                 throw new ArgumentException ("'{0}' is an invalid xml:space value.");
652                                                 }
653                                         }
654                                 }
655                         }
656                 }
657
658                 [MonoTODO]
659                 public override void WriteSurrogateCharEntity (char lowChar, char highChar)
660                 {
661                         throw new NotImplementedException ();
662                 }
663
664                 public override void WriteWhitespace (string ws)
665                 {
666                         foreach (char c in ws) {
667                                 if ((c != ' ') && (c != '\t') && (c != '\r') && (c != '\n'))
668                                         throw new ArgumentException ();
669                         }
670
671                         w.Write (ws);
672                 }
673
674                 #endregion
675         }
676 }