root/trunk/mango/xml/sax/parser/teqXML.d

Revision 1055, 26.7 kB (checked in by teqdruid, 10 months ago)

Forgot about the other encoding cases for exception

  • Property svn:eol-style set to native
Line 
1 /*******************************************************************************
2     License:
3         Copyright (c) 2005 John Demme
4
5         This software is provided 'as-is', without any express or implied
6         warranty. In no event will the authors be held liable for damages
7         of any kind arising from the use of this software.
8
9         Permission is hereby granted to anyone to use this software for any
10         purpose, including commercial applications, and to alter it and/or
11         redistribute it freely, subject to the following restrictions:
12
13         1. The origin of this software must not be misrepresented; you must
14            not claim that you wrote the original software. If you use this
15            software in a product, an acknowledgment within documentation of
16            said product would be appreciated but is not required.
17
18         2. Altered source versions must be plainly marked as such, and must
19            not be misrepresented as being the original software.
20
21         3. This notice may not be removed or altered from any distribution
22            of the source.
23
24         4. Derivative works are permitted, but they must carry this notice
25            in full and credit the original source.
26
27
28         Date:       Initial version, December 2005
29         Authors:        John Demme (me@teqdruid.com)
30
31     Version:    0.1b
32     History:    Converted from the original mango.xml.dom.parser.teqXML
33
34     See_Also: ISAXReader, ISAXHandler
35
36 *******************************************************************************/
37
38 module mango.xml.sax.parser.teqXML;
39
40 private import  Text = tango.text.Util;
41
42 private import  tango.text.convert.UnicodeBom,
43         Integer = tango.text.convert.Integer,
44         Utf = tango.text.convert.Utf;
45
46 private import  mango.xml.sax.Exceptions,
47         mango.xml.sax.model.ISAXHandler,
48         mango.xml.sax.model.ISAXParser;
49
50 private import  tango.io.model.IConduit,
51         tango.io.model.IBuffer;
52
53 private import  tango.io.Buffer;
54
55 private import tango.stdc.stdlib;
56
57 private import tango.io.Stdout;
58
59 public class TeqXMLException : SAXException
60 {
61     char[] msg;
62    
63     this(char[] msg)
64     {
65         super(msg);
66         this.msg = msg;
67     }
68    
69     char[] toString()
70     {
71         return msg;
72     }
73 }
74
75 public class TeqXMLEofException : TeqXMLException
76 {
77     this()
78     {
79         super("Unexpectedly reached EOF");
80     }
81 }
82
83 deprecated alias XMLWriter TeqXMLWriter;
84
85 /**
86    A simplistic XML encoder.  Reusable, so a pool of these would work rather
87    nicely.  Not thread safe, however, so be sure that only one thread is trying
88    to use it at once.
89 */
90 class XMLWriter(T=char): ISAXWriter!(T)
91 {
92     static assert ( is (T == char)
93             || is (T == wchar)
94             || is (T == dchar));
95
96     private class MyWriter {
97         MyWriter opCall(T[] str) {
98             void[] output = encoder.encode(str);
99             buffer.consume(output);
100             return this;
101         }       
102
103         MyWriter opCall(T c) {
104             T[] str = (&c)[0..1];
105             void[] output = encoder.encode(str);
106             buffer.consume(output);
107             return this;
108         }
109     }
110
111     private static enum Status
112     {
113         Start,
114         End,
115         InTag,
116         InElement
117     }
118    
119     private Status status;
120     private uint tabs;
121     private bool pretty;
122     private UnicodeBom!(T) encoder;
123     private MyWriter writer;
124     private IBuffer buffer;
125
126     private static struct Const
127     {
128         static:
129         const T[] lt = "<";
130         const T[] ltE = "&lt;"; // The capitol E is for entity
131         const T[] gt = ">";
132         const T[] gtE = "&gt;";
133         const T[] ampE = "&amp;";
134         const T[] aposE = "&apos;";
135         const T[] quest = "?";
136         const T[] space = " ";
137         const T[] tab = "\t";
138         const T[] dqm = "\"";
139         const T[] dqmE = "&quot;";
140         const T[] equals = "=";
141         const T[] fs = "/";
142         const T[] openComment = "<!--";
143         const T[] closeComment = "-->";
144         const T[] preambleStart = "<?xml version=\"1.0\" encoding=\"";
145         const T[] preambleEnd = "\"?>";
146         const T[] utf8 = "UTF-8";
147         const T[] utf16 = "UTF-16";
148         const T[] utf32 = "UTF32";
149         const T[] CR = "\n";         
150     }
151    
152     public this()
153     {
154         status = Status.End;
155         tabs = 0;
156         writer = new MyWriter();
157         encoder = new UnicodeBom!(T)(Encoding.Unknown);
158     }
159    
160     private void writeTabs()
161     {
162         if (pretty)
163             for(uint i=0; i<tabs; i++)
164                 writer(Const.tab);
165     }
166  
167     void startDocument(OutputStream outStream, Encoding outputType = Encoding.UTF_8N, bool pretty=true)
168     {
169         buffer = cast(IBuffer) outStream;
170         if (buffer is null)
171             buffer = new Buffer(outStream.conduit());
172
173         assert(status == Status.End);
174         if (outputType == Encoding.UTF_8)
175             outputType = Encoding.UTF_8N;
176         encoder.setup(outputType);
177
178         status = Status.Start;
179         tabs = 0;
180         this.pretty = pretty;
181
182         writer(Const.preambleStart);
183         switch (outputType) {
184         case Encoding.UTF_8:
185         case Encoding.UTF_8N:
186             writer(Const.utf8);
187             break;
188         case Encoding.UTF_16:
189         case Encoding.UTF_16LE:
190         case Encoding.UTF_16BE:
191             writer(Const.utf16);
192             break;
193         case Encoding.UTF_32:
194         case Encoding.UTF_32LE:
195         case Encoding.UTF_32BE:
196             writer(Const.utf32);
197             break;
198         default:
199             throw new TeqXMLException("Specified encoding not supported");         
200         }
201         writer(Const.preambleEnd);
202         if (pretty) writer(Const.CR);
203     }
204
205     void endDocument()
206     {
207         buffer.flush();
208         status = Status.End;
209     }
210
211     void processingInstruction(T[] target, T[] data)
212     {
213         assert(status != Status.End);
214         writeTabs();
215         writer(Const.lt)(Const.quest)(target)(Const.space)(data)(Const.quest)(Const.gt);
216         if (pretty) writer(Const.CR);
217     }
218  
219     void startElement(T[] name)
220     {
221         assert(status == Status.Start || status == Status.InElement || status == Status.InTag);
222         if (status == Status.InTag)
223         {
224             status = Status.InElement;
225             writer(Const.gt);
226             if (pretty) writer(Const.CR);
227         }
228         writeTabs();
229         writer(Const.lt)(name);
230         tabs++;
231         status = Status.InTag;
232     }
233  
234     void addAttribute(T[] key, T[] value)
235     {
236         assert(status == Status.InTag);
237         writer(Const.space)(key)(Const.equals)(Const.dqm);
238         outputChecked(value);
239         writer(Const.dqm);
240     }
241    
242     void endElement(T[] name)
243     {
244         tabs--;
245         if (status == Status.InTag)
246         {
247             writer(Const.fs)(Const.gt);
248             status = Status.InElement;
249         }
250         else
251         {
252             assert(status == Status.InElement);
253             if (pretty) writeTabs();
254             writer(Const.lt)(Const.fs)(name)(Const.gt);
255         }
256        
257         if (pretty) writer(Const.CR);
258     }
259  
260     void characterData (T[] data, CDataStatus cstatus, bool check = true)
261     {
262         assert(status != Status.End);
263        
264         if (status == Status.InElement)
265         {
266             if (cstatus.containsStart())
267                 writeTabs();
268         }
269         else if (status == Status.InTag)
270         {
271             assert(cstatus.containsStart());
272             status = Status.InElement;
273             writer(Const.gt);
274             if (pretty) writer(Const.CR);
275             writeTabs();
276         }
277        
278         if (!check)
279         {
280             writer(data);
281         }
282         else
283         {
284             outputChecked(data);
285         }
286        
287         if (pretty && cstatus.containsEnd())
288             writer(Const.CR);
289     }
290    
291     private void outputChecked(T[] data)
292     {
293         foreach(T c; data)
294         {
295             if (c == '<')
296                 writer(Const.ltE);
297             else if (c == '>')
298                 writer(Const.gtE);
299             else if (c == '&')
300                 writer(Const.ampE);
301             else if (c == '\'')
302                 writer(Const.aposE);
303             else if (c == '"')
304                 writer(Const.dqmE);
305             else
306                 writer(c);
307         }
308         return;
309     }
310
311     void comment(T[] text)
312     {
313         assert(status != Status.End);
314         writeTabs();
315         writer(Const.openComment)(text)(Const.closeComment);
316         if (pretty) writer(Const.CR);
317     }
318 }
319
320 private final const uint LONG_ENTITY = 1024; //Longest entity allowed is 1024 - 2
321
322 deprecated alias XMLReader TeqXMLReader;
323
324 /**
325    A fairly simplistic SAX parser.  Reusable, so a pool of these would work nicely. This Reader
326    is not thread-safe, however, so be sure that only one thread attempts to call parse at once.
327    Non-validating.  Memory efficient: the only heap allocations after initialization are increasing
328    the size of the input buffer (if necessary) and (small constant)*(maximum depth of XML tree)
329    for a tag validation stack.
330
331    Standards: Mostly conforms to the XML 1.0 spec
332
333    Bugs:
334    Does not support parsed entities or external references.
335 */
336 class XMLReader(T=char): ISAXReader!(T)
337 {
338     private alias bool function(T) PropCheck;
339
340     private ISAXHandler!(T) myHandler;
341     private uint[] myStack; //Just store hashes instead of names
342
343     package ReaderString!(T) reader;
344     private ReaderString!(T).Marker start, end;
345
346     private T[][T[]] entities;
347     private T[] amp = "amp", lt = "lt", gt = "gt", apos = "apos", quot = "quot";
348     private T[] ampV = "&",  ltV = "<", gtV = ">", aposV = "'",   quotV = "\"";
349
350     private bool first;
351  
352     /**
353         Initializes the parser.  This parser is reusable, so you can process multiple documents with
354         only one instance (just not at once).
355
356         Params:
357         bufferLength =  When we initialize our internal buffer, we'll use this value (if supplied).  Unit
358         is code-points, so a buffer length of 1024 allows for 1024 transcoded code-points in the buffer.
359         This buffer size must be at least a large as the longest tag in a document if resizing is to be
360         avoided.  Default is 512.
361     */
362     public this(uint bufferLength=0)
363     {
364         if (bufferLength == 0)
365             bufferLength = 512;
366         reader = new ReaderString!(T)(bufferLength);
367         start = reader.markers[0];
368         end = reader.markers[1];
369     }
370
371     private void parse(ISAXHandler!(T) handler) {
372         myStack.length = 0;
373         this.myHandler = handler;
374         foreach(T[] key; entities.keys)
375             entities.remove(key);
376         entities[amp] = ampV;
377         entities[lt] = ltV;
378         entities[gt] = gtV;
379         entities[apos] = aposV;
380         entities[quot] = quotV;
381         myHandler.startDocument();
382         go();
383         myHandler.endDocument();
384     }
385  
386     /**
387         Tells the parser to begin processing a document
388
389         Params:
390         source =    The IBuffer that the parser should draw xml content from
391         handler =   The client interface who's methods the parser should call
392         encoding =  If the client knows it, tell the parser the text encoding of the incoming XML.
393         If not specified, the parser will guess, and failing that assume UTF8N.  The enum for these types
394         is in mango.convert.Unicode.
395     */
396     public void parse(InputStream source, ISAXHandler!(T) handler, Encoding inputEncoding=Encoding.Unknown)
397     in
398     {
399         assert(source !is null);
400         assert(handler !is null);
401     }
402     body
403     {
404         //Initialize the parser
405         reader.init(source, inputEncoding);
406         parse(handler);
407     }
408
409     private static bool isWhiteSpace(T wc)
410     {
411         uint n = cast(uint)wc;
412         return (n == 32 || n == 13 || n == 10 || n == 9);
413     }
414
415     private static bool isWhiteSpace(T[] str)
416     {
417         foreach(T wc; str)
418             if (!isWhiteSpace(wc))
419                 return false;
420         return true;
421     }
422
423     private void go()
424     {
425         first = true;
426         CDataStatus status = CDataStatus.End;
427         T[] whiteSpace;
428         do
429         {
430             end.advanceToNoFill('<');
431
432             if (start.isBefore(end))
433             {
434                 T[] text = start.sliceTo(end);
435                 if (!status.containsEnd() || !isWhiteSpace(text))
436                 {
437                     if (myStack.length == 0) {
438                         static if (is(T == char)) {
439                             throw new TeqXMLException("Cannot have text (" ~ text ~ ") before first tag!");
440                         } else {
441                             throw new TeqXMLException("Cannot have text (" ~ Utf.toString(text) ~ ") before first tag!");
442                         }
443                     }
444
445                     if (whiteSpace.length > 0) {
446                         status = CDataStatus.Start;
447                         sendCData(whiteSpace, status);
448                     }
449
450                     int amp = Text.locatePrior!(T)(text, '&');
451                     if (amp < text.length) {
452                         T[] lct = text[amp..$];
453                         int semi = Text.locate!(T)(lct, ';');
454                         if (semi >= lct.length) {
455                             if (amp == 0 && text.length > LONG_ENTITY) {  //Entities can only be so long
456                                 throw new TeqXMLException("Entity name longer than allowed or semicolon missing after &");
457                             }
458                             if (status.containsEnd())
459                                 status = CDataStatus.Start;
460                             else
461                                 status = CDataStatus.Append;
462
463                             sendCData(text[0..amp], status);
464                             end.backupTo('&');
465                             start.moveTo(end);
466                             reader.fillBuffer();
467                             goto doneCData;
468                         }
469                     }
470
471                     if (end.getAdvance() == '<')
472                         if (status.containsEnd())
473                             status = CDataStatus.All;
474                         else
475                             status = CDataStatus.End;
476                     else
477                         if (status.containsEnd())
478                             status = CDataStatus.Start;
479                         else
480                             status = CDataStatus.Append;
481                    
482                     text = start.sliceTo(end);
483                     sendCData(text, status);
484                 } else if (isWhiteSpace(text)) {
485                     whiteSpace ~= text;
486                 }
487             }
488
489         doneCData:
490             start.moveTo(end);
491
492             if (end.read() == '<')
493             {
494                 whiteSpace.length = 0;;
495                 parseOpen();
496                 start.moveTo(end);
497             }
498         } while (first || myStack.length > 0);
499     }
500    
501     //If the data to be sent has any entities, they must be entirely contained within one call.
502     void sendCData(T[] data, CDataStatus status)
503     {
504         int ampIndx = Text.locate!(T)(data, '&');
505         int last = 0;
506         bool sentStart = false;
507         while (ampIndx < data.length)
508         {
509             int scIndx = Text.locate!(T)(data, ';', ampIndx);
510             if (scIndx >= data.length)
511                 break;
512
513             T[] str = data[last..ampIndx];
514             if (status.containsStart && !sentStart)
515             {
516                 myHandler.characterData(str, CDataStatus.Start);
517                 sentStart = true;
518             }
519             else
520                 myHandler.characterData(str, CDataStatus.Append);
521            
522             T[] ent = data[ampIndx+1..scIndx];
523             T[1] chr;
524             if (!(ent in entities)) {
525                 if (ent[0] == '#' && ent.length >= 2) {
526                     ent = ent[1..$];
527                     if (ent[0] == 'x' && ent.length >= 2)
528                         chr[0] = Integer.toInt(ent, 16);
529                     else
530                         chr[0] = Integer.toInt(ent, 10);
531                     str = chr;
532                 } else {
533                     char[] str8;
534                     static if (is(T == char)) {
535                         str8 = str;
536                     } else {
537                         str8 = tango.text.convert.Utf.toString(str);
538                     }
539                     throw new TeqXMLException(
540                         "Cannot resolve entity '" ~ str8 ~ "'");
541                 }
542             } else {
543                 str = entities[ent];
544             }
545
546             if (status.containsEnd() && scIndx == (data.length-1))
547                 myHandler.characterData(str, CDataStatus.End);
548             else
549                 myHandler.characterData(str, CDataStatus.Append);
550
551             last = scIndx + 1;
552             ampIndx = Text.locate!(T)(data, '&', scIndx);
553         }
554        
555         if (last < data.length)
556         {
557             T[] str = data[last..$];
558             if (last > 0)
559                 if (status.containsEnd)
560                     myHandler.characterData(str, CDataStatus.End);
561                 else
562                     myHandler.characterData(str, CDataStatus.Append);
563             else
564                 myHandler.characterData(str, status);
565         }
566     }
567
568     void parseOpen()
569     {
570         start.moveTo(end);
571         switch (end.read())
572         {
573         case '/':
574             parseCloseTag();
575             break;
576         case '?':
577             parseQuest();
578             break;
579         case '!':
580             parseExcl();
581             break;
582         default:
583             parseElement();
584             break;
585         }
586     }
587
588     void parseElement() {
589         static bool stop(