]> err.no Git - sope/blob - sope-ical/versitSaxDriver/VSSaxDriver.m
bugfix
[sope] / sope-ical / versitSaxDriver / VSSaxDriver.m
1 /*
2  Copyright (C) 2003-2004 Max Berger
3  Copyright (C) 2004 OpenGroupware.org
4
5  This file is part of versitSaxDriver, written for the OpenGroupware.org 
6  project (OGo).
7  
8  OGo is free software; you can redistribute it and/or modify it under
9  the terms of the GNU Lesser General Public License as published by the
10  Free Software Foundation; either version 2, or (at your option) any
11  later version.
12  
13  OGo is distributed in the hope that it will be useful, but WITHOUT ANY
14  WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
16  License for more details.
17  
18  You should have received a copy of the GNU Lesser General Public
19  License along with OGo; see the file COPYING.  If not, write to the
20  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
21  02111-1307, USA.
22 */
23 // $Id$
24
25 #include "VSSaxDriver.h"
26 #include "VSStringFormatter.h"
27 #include "common.h"
28
29 @implementation VSSaxDriver
30
31 static BOOL debugOn = NO;
32
33 static NSCharacterSet *dotCharSet = nil;
34 static NSCharacterSet *equalSignCharSet = nil;
35 static NSCharacterSet *commaCharSet = nil;
36 static NSCharacterSet *colonAndSemicolonCharSet = nil;
37 static NSCharacterSet *colonSemicolonAndDquoteCharSet = nil;
38 static NSCharacterSet *whitespaceCharSet = nil;
39
40 static VSStringFormatter *stringFormatter = nil;
41
42 + (void)initialize {
43   static BOOL didInit = NO;
44   NSUserDefaults *ud;
45
46   if(didInit)
47     return;
48   didInit = YES;
49
50   ud      = [NSUserDefaults standardUserDefaults];
51   debugOn = [ud boolForKey:@"VSSaxDriverDebugEnabled"];
52
53   dotCharSet =
54     [[NSCharacterSet characterSetWithCharactersInString:@"."] retain];
55   equalSignCharSet =
56     [[NSCharacterSet characterSetWithCharactersInString:@"="] retain];
57   commaCharSet =
58     [[NSCharacterSet characterSetWithCharactersInString:@","] retain];
59   colonAndSemicolonCharSet =
60     [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain];
61   colonSemicolonAndDquoteCharSet =
62     [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain];
63   whitespaceCharSet =
64     [[NSCharacterSet whitespaceCharacterSet] retain];
65
66   stringFormatter = [VSStringFormatter sharedFormatter];
67 }
68
69
70 - (id)init {
71   if ((self = [super init])) {
72     self->prefixURI         = @"";
73     self->cardStack         = [[NSMutableArray alloc]      init];
74     self->elementList       = [[NSMutableArray alloc]      init];
75     self->attributeMapping  = [[NSMutableDictionary alloc] init];
76     self->subItemMapping    = [[NSMutableDictionary alloc] init];
77   }
78   return self;
79 }
80
81 - (void)dealloc {
82   [self->contentHandler    release];
83   [self->prefixURI         release];
84   [self->cardStack         release];
85   [self->elementList       release];
86   [self->attributeElements release];
87   [self->elementMapping    release];
88   [self->attributeMapping  release];
89   [self->subItemMapping    release];
90   [super dealloc];
91 }
92
93 /* accessors */
94
95 - (void)setFeature:(NSString *)_name to:(BOOL)_value {
96 }
97 - (BOOL)feature:(NSString *)_name {
98   return NO;
99 }
100
101 - (void)setProperty:(NSString *)_name to:(id)_value {
102 }
103 - (id)property:(NSString *)_name {
104   return nil;
105 }
106
107 /* handlers */
108
109 - (void)setContentHandler:(id<NSObject,SaxContentHandler>)_handler {
110   ASSIGN(self->contentHandler,_handler);
111 }
112
113 - (void)setDTDHandler:(id<NSObject,SaxDTDHandler>)_handler {
114   // FIXME
115 }
116
117 - (void)setErrorHandler:(id<NSObject,SaxErrorHandler>)_handler {
118   // FIXME
119 }
120 - (void)setEntityResolver:(id<NSObject,SaxEntityResolver>)_handler {
121   // FIXME
122 }
123
124 - (id<NSObject,SaxContentHandler>)contentHandler {
125   return self->contentHandler;
126 }
127
128 - (id<NSObject,SaxDTDHandler>)dtdHandler {
129   // FIXME
130   return NULL;
131 }
132
133 - (id<NSObject,SaxErrorHandler>)errorHandler {
134   // FIXME
135   return NULL;
136 }
137 - (id<NSObject,SaxEntityResolver>)entityResolver {
138   // FIXME
139   return NULL;
140 }
141
142 - (void)setPrefixURI:(NSString *)_uri {
143   ASSIGNCOPY(self->prefixURI, _uri);
144 }
145 - (NSString *)prefixURI {
146   return self->prefixURI;
147 }
148
149 - (void)setAttributeElements:(NSSet *)_elements {
150   ASSIGNCOPY(self->attributeElements, _elements);
151 }
152 - (NSSet *)attributeElements {
153   return self->attributeElements;
154 }
155
156 - (void)setElementMapping:(NSDictionary *)_mapping {
157   ASSIGNCOPY(self->elementMapping, _mapping);
158 }
159 - (NSDictionary *)elementMapping {
160   return self->elementMapping;
161 }
162
163 - (void)setAttributeMapping:(NSDictionary *)_mapping {
164   [self setAttributeMapping:_mapping forElement:@""];
165 }
166
167 - (void)setAttributeMapping:(NSDictionary *)_mapping 
168   forElement:(NSString *)_element 
169 {
170   if (!_element)
171     _element = @"";
172   [attributeMapping setObject:_mapping forKey:_element];
173 }
174
175 - (void)setSubItemMapping:(NSArray *)_mapping 
176   forElement:(NSString *)_element 
177 {
178   [subItemMapping setObject:_mapping forKey:_element];  
179 }
180
181
182
183 /* parsing */
184
185 - (NSString *)_mapTagName:(NSString *)_tagName {
186   NSString *ret;
187   NSRange  r;
188
189   if ((ret = [self->elementMapping objectForKey:_tagName]) == nil) {
190     //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
191     ret = _tagName;
192
193     /* This is to allow parsing of vCards produced by Apple
194        Addressbook. AFAIK the .dot notation is a non-standard
195        extension */
196     r = [_tagName rangeOfCharacterFromSet:dotCharSet];
197     if (r.length > 0) {
198       ret = [self _mapTagName:[_tagName substringFromIndex:(r.location + 1)]];
199     }
200   }
201   return ret;
202 }
203
204 - (void)_addAttribute:(NSString *)_attribute
205   value:(NSString *)_value 
206   toAttrs:(SaxAttributes *)_attrs
207 {
208   [_attrs addAttribute:_attribute
209           uri:self->prefixURI 
210           rawName:_attribute
211           type:@"CDATA"
212           value:_value];
213 }
214
215 - (void)_addAttribute:(NSString *)_attribute value:(NSString *)_value {
216   NSArray *element = [cardStack lastObject];
217   SaxAttributes *attrs = [element objectAtIndex:2];
218   [self _addAttribute:_attribute value:_value toAttrs:attrs];
219 }
220
221 - (NSString *)_mapAttrName:(NSString *)_attrName forTag:(NSString *)_tagName {
222   NSString *mappedName;
223
224   mappedName = [[self->attributeMapping objectForKey:_tagName]
225                                         objectForKey:_attrName];
226   if (!mappedName) {
227     mappedName = [[self->attributeMapping objectForKey:
228                                           [self _mapTagName:_tagName]]
229                                           objectForKey:_attrName];
230   }
231   if (!mappedName) {
232     mappedName = [[self->attributeMapping objectForKey:@""]
233                                           objectForKey:_attrName];
234   }
235   if (!mappedName)
236     mappedName = _attrName;
237   
238   return mappedName;
239 }
240
241 - (void)_parseAttr:(NSString *)_attr 
242   forTag:(NSString *)_tagName
243   intoAttr:(NSString **)attr_
244   intoValue:(NSString **)value_
245 {
246   NSRange  r;
247   NSString *attrName, *attrValue, *mappedName;
248   
249   r = [_attr rangeOfCharacterFromSet:equalSignCharSet];
250   if (r.length > 0) {
251     unsigned left, right;
252
253     attrName  = [[_attr substringToIndex:r.location] uppercaseString];
254     left = NSMaxRange(r);
255     right = [_attr length] - 1;
256     if(left < right) {
257       if(([_attr characterAtIndex:left]  == '"') &&
258          ([_attr characterAtIndex:right] == '"'))
259       {
260         left += 1;
261         r = NSMakeRange(left, right - left);
262         attrValue = [_attr substringWithRange:r];
263       }
264       else {
265         attrValue = [_attr substringFromIndex:left];
266       }
267     }
268     else if(left == right) {
269       attrValue = [_attr substringFromIndex:left];
270     }
271     else {
272       attrValue = @"";
273     }
274   }
275   else {
276     attrName  = @"TYPE";
277     attrValue = _attr;
278   }
279   
280 #if 0
281   // ZNeK: what's this for?
282   r = [attrValue rangeOfCharacterFromSet:commaCharSet];
283   while (r.length > 0) {
284     [attrValue replaceCharactersInRange:r withString:@" "];
285     r = [attrValue rangeOfCharacterFromSet:commaCharSet];
286   }
287 #endif
288
289   mappedName = [self _mapAttrName:attrName forTag:_tagName];
290   *attr_ = mappedName;
291   *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue];
292 }
293
294 - (id<NSObject,SaxAttributes>)_mapAttrs:(NSArray *)_attrs 
295   forTag:(NSString *)_tagName 
296 {
297   SaxAttributes       *retAttrs;
298   NSEnumerator        *attrEnum;
299   NSString            *curAttr, *mappedAttr, *mappedValue, *oldValue;
300   NSMutableDictionary *attributes;
301
302   if (!_attrs || [_attrs count] == 0)
303     return nil;
304
305   attributes = [[NSMutableDictionary alloc] init];
306   retAttrs = [[[SaxAttributes alloc] init] autorelease];
307   attrEnum = [_attrs objectEnumerator];
308   while ((curAttr = [attrEnum nextObject])) {
309     [self _parseAttr:curAttr
310           forTag:_tagName
311           intoAttr:&mappedAttr
312           intoValue:&mappedValue];
313     if ((oldValue = [attributes objectForKey:mappedAttr])) {
314       NSString *val;
315       /* ZNeK: duh! */
316       val = [NSString stringWithFormat:@"%@ %@",oldValue, mappedValue];
317       [attributes setObject:val forKey:mappedAttr];
318     }
319     else  
320       [attributes setObject:mappedValue forKey:mappedAttr];
321   }
322
323   attrEnum = [attributes keyEnumerator];
324   while ((curAttr = [attrEnum nextObject])) {
325     [self _addAttribute:curAttr
326           value:[attributes objectForKey:curAttr]
327           toAttrs:retAttrs];
328   }
329   
330   [attributes release];
331   
332   return retAttrs;
333 }
334
335 - (NSArray *)_beginTag:(NSString *)_tagName 
336   withAttrs:(id<NSObject,SaxAttributes>)_attrs 
337 {
338   NSArray *tag = [NSArray arrayWithObjects:@"BEGIN",_tagName,_attrs,NULL];
339   [self->elementList addObject:tag];
340   return tag;
341 }
342
343 - (void)_endTag:(NSString *)_tagName {
344   [self->elementList addObject:
345          [NSArray arrayWithObjects:@"END",_tagName,NULL]];
346 }
347
348 - (void)_addSubItems:(NSArray *)_items withData:(NSString *)_content {
349   NSEnumerator *itemEnum, *contentEnum;
350   NSString *subTag;
351   NSString *subContent;
352   
353   itemEnum    = [_items objectEnumerator];
354   contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator];
355   
356   while ((subTag=[itemEnum nextObject])) {
357     subContent = [contentEnum nextObject];
358     
359     [self _beginTag:subTag withAttrs:nil];
360     if ([subContent length]>0) 
361       [self->elementList addObject:
362         [NSArray arrayWithObjects:@"DATA", subContent, nil]];  
363     [self _endTag:subTag];
364   }
365 }
366
367 - (void)_dataTag:(NSString *)_tagName 
368   withAttrs:(id<NSObject,SaxAttributes>)_attrs 
369   andContent:(NSString *)_content 
370 {
371   NSArray *subItems;
372
373   _content = [stringFormatter stringByUnescapingRFC2445Text:_content];
374   if ([self->attributeElements containsObject:_tagName]) {
375     [self _addAttribute:_tagName value:_content];
376   } 
377   else {
378     [self _beginTag:_tagName withAttrs:_attrs];
379     if ([_content length] > 0) {
380       if ((subItems = [self->subItemMapping objectForKey:_tagName])) {
381         [self _addSubItems:subItems withData:_content];
382       }
383       else {
384         [self->elementList addObject:
385           [NSArray arrayWithObjects:@"DATA", _content, nil]];  
386       }
387     }
388     [self _endTag:_tagName];
389   }
390 }
391
392 - (void)_eventsForElements {
393   NSEnumerator *enu;
394   NSArray  *obj;
395   NSString *type;
396   NSString *name;
397   unichar  *chardata;
398   id<NSObject,SaxAttributes> attrs;
399   
400   enu = [elementList objectEnumerator];
401   while ((obj = [enu nextObject])) {
402     type = [obj objectAtIndex:0];
403     name = [obj objectAtIndex:1];
404     
405     if ([obj count] > 2) 
406       attrs = [obj objectAtIndex:2];
407     else
408       attrs = nil;
409
410     if ([type isEqualToString:@"BEGIN"]) {
411       [self->contentHandler startElement:name
412                             namespace:self->prefixURI
413                             rawName:name
414                             attributes:attrs];
415     } 
416     else if ([type isEqualToString:@"END"]) {
417       [self->contentHandler endElement:name
418                             namespace:self->prefixURI
419                             rawName:name];
420     }
421     else {
422       unsigned len = [name length];
423       chardata = malloc(len * sizeof(unichar));
424       [name getCharacters:chardata range:NSMakeRange(0, len)];
425       [self->contentHandler characters:chardata length:len];
426       if (chardata)
427         free(chardata);
428     }
429   }
430   [elementList removeAllObjects];
431 }
432
433 - (void)_parseLine:(NSString *)_line {
434   NSString       *tagName, *tagValue;
435   NSMutableArray *tagAttributes;
436   NSRange        r, todoRange;
437   unsigned       length;
438
439   length = [_line length];
440   todoRange = NSMakeRange(0, length);
441   r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet
442              options:0
443              range:todoRange];
444   /* is line well-formed? */
445   if(r.length == 0) {
446     if(debugOn) {
447       NSLog(@"%s got an improper content line! ->\n%@",
448             __PRETTY_FUNCTION__,
449             _line);
450     }
451     return;
452   }
453
454   tagName = [[_line substringToIndex:r.location] uppercaseString];
455   tagAttributes = [[NSMutableArray alloc] init];
456
457   /* possible shortcut: if we spotted a ':', we don't have to do "expensive"
458      argument scanning/processing.
459   */
460   if([_line characterAtIndex:r.location] != ':') {
461     BOOL isAtEnd = NO, isInDquote = NO;
462     unsigned start = NSMaxRange(r);
463
464     todoRange = NSMakeRange(start, length - start);
465     while(!isAtEnd) {
466       BOOL skip = YES;
467
468       /* scan for parameters */
469       r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
470                  options:0
471                  range:todoRange];
472       /* is line well-formed? */
473       if(r.length == 0 || r.location == 0) {
474         if(debugOn) {
475           NSLog(@"%s got an improper content line! ->\n%@",
476                 __PRETTY_FUNCTION__,
477                 _line);
478         }
479         [tagAttributes release];
480         return;
481       }
482       /* first check if delimiter candidate is escaped */
483       if([_line characterAtIndex:(r.location - 1)] != '\\') {
484         unichar delimiter;
485         NSRange copyRange;
486
487         delimiter = [_line characterAtIndex:r.location];
488         if(delimiter == '\"') {
489           /* not a real delimiter - toggle isInDquote for proper escaping */
490           isInDquote = !isInDquote;
491         }
492         else {
493           if(!isInDquote) {
494             /* is a delimiter, which one? */
495             skip = NO;
496             if(delimiter == ':') {
497               isAtEnd = YES;
498             }
499             copyRange = NSMakeRange(start, r.location - start);
500             [tagAttributes addObject:[_line substringWithRange:copyRange]];
501             if(!isAtEnd) {
502               /* adjust start, todoRange */
503               start     = NSMaxRange(r);
504               todoRange = NSMakeRange(start, length - start);
505             }
506           }
507         }
508       }
509       if(skip) {
510         /* adjust todoRange */
511         unsigned offset = NSMaxRange(r);
512         todoRange = NSMakeRange(offset, length - offset);
513       }
514     }
515   }
516   tagValue = [_line substringFromIndex:NSMaxRange(r)];
517
518   if ([tagName isEqualToString:@"BEGIN"]) {
519     id tag;
520     
521     tag = [self _beginTag:[self _mapTagName:tagValue] 
522                 withAttrs:[[[SaxAttributes alloc] init] autorelease]];
523     [self->cardStack addObject:tag];
524   } 
525   else if ([tagName isEqualToString:@"END"]) {
526     NSString *mtName;
527     
528     mtName = [self _mapTagName:tagValue];
529     if([self->cardStack count] > 0) {
530       NSString *expectedName;
531
532       expectedName = [[self->cardStack lastObject] objectAtIndex:1];
533       if(![expectedName isEqualToString:mtName]) {
534         if(debugOn) {
535           NSLog(@"%s found end tag '%@' which doesn't match expected name "
536                 @"'%@'! Tag '%@' hasn't been closed properly. Given iCal "
537                 @"document contains errors!",
538                 __PRETTY_FUNCTION__,
539                 mtName,
540                 expectedName,
541                 expectedName);
542         }
543         /* probably futile attempt to parse anyways */
544         if(debugOn) {
545           NSLog(@"%s trying to fix previous error by inserting bogus end "
546                 @"tag.",
547                 __PRETTY_FUNCTION__);
548         }
549         [self _endTag:expectedName];
550         [self->cardStack removeLastObject];
551       }
552     }
553     else {
554       if(debugOn) {
555         NSLog(@"%s found end tag '%@' without any open tags left?!",
556               __PRETTY_FUNCTION__,
557               mtName);
558       }
559     }
560     [self _endTag:mtName];
561     [self->cardStack removeLastObject];
562     if ([self->cardStack count] == 0)
563       [self _eventsForElements];
564   }
565   else {
566     [self _dataTag:[self _mapTagName:tagName]
567          withAttrs:[self _mapAttrs:tagAttributes forTag:tagName] 
568         andContent:tagValue];
569   }
570   [tagAttributes release];
571 }
572
573 - (void)_parseString:(NSString *)_rawString {
574   unsigned pos, length;
575   NSMutableString *line;
576   NSRange r;
577
578   [self->contentHandler startDocument];
579   [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
580   
581   length = [_rawString length];
582   /* RFC2445:
583      contentline        = name *(";" param ) ":" value CRLF
584      ; When parsing a content line, folded lines MUST first
585      ; be unfolded
586   */
587   r = NSMakeRange(0, 0);
588   /* probably too optimistic */ 
589   line = [[NSMutableString alloc] initWithCapacity:75 + 2];
590
591   for(pos = 0; pos < length; pos++) {
592     unichar c = [_rawString characterAtIndex:pos];
593     
594     if(c == '\r') {
595       if(((length - 1) - pos) >= 1) {
596         if([_rawString characterAtIndex:pos + 1] == '\n') {
597           BOOL isAtEndOfLine = YES;
598           /* test for folding first */
599           if(((length - 1) - pos) >= 2) {
600             unichar ws = [_rawString characterAtIndex:pos + 2];
601             isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
602                                                                      : YES;
603             if(!isAtEndOfLine) {
604               /* assemble part of line up to pos */
605               if(r.length > 0) {
606                 [line appendString:[_rawString substringWithRange:r]];
607               }
608               /* unfold */
609               pos += 2;
610               r = NSMakeRange(pos + 1, 0); /* begin new range */
611             }
612           }
613           if(isAtEndOfLine) {
614             /* assemble part of line up to pos */
615             if(r.length > 0) {
616               [line appendString:[_rawString substringWithRange:r]];
617             }
618             [self _parseLine:line];
619             /* reset line */
620             [line deleteCharactersInRange:NSMakeRange(0, [line length])];
621             pos += 1;
622             r = NSMakeRange(pos + 1, 0); /* begin new range */
623           }
624         }
625       }
626       else {
627         /* garbled last line! */
628         if(debugOn) {
629           NSLog(@"%s Last line is truncated, trying to parse anyways!",
630                 __PRETTY_FUNCTION__);
631         }
632       }
633     }
634     else if(c == '\n') { /* broken, non-standard */
635       BOOL isAtEndOfLine = YES;
636       /* test for folding first */
637       if(((length - 1) - pos) >= 1) {
638         unichar ws = [_rawString characterAtIndex:pos + 1];
639         isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
640                                                                  : YES;
641         if(!isAtEndOfLine) {
642           /* assemble part of line up to pos */
643           if(r.length > 0) {
644             [line appendString:[_rawString substringWithRange:r]];
645           }
646           /* unfold */
647           pos += 1;
648           r = NSMakeRange(pos + 1, 0); /* begin new range */
649         }
650       }
651       if(isAtEndOfLine) {
652         /* assemble part of line up to pos */
653         if(r.length > 0) {
654           [line appendString:[_rawString substringWithRange:r]];
655         }
656         [self _parseLine:line];
657         /* reset line */
658         [line deleteCharactersInRange:NSMakeRange(0, [line length])];
659         r = NSMakeRange(pos + 1, 0); /* begin new range */
660       }
661     }
662     else {
663       r.length += 1;
664     }
665   }
666   if(r.length > 0) {
667     if(debugOn) {
668       NSLog(@"%s Last line of iCal string is not properly terminated!",
669             __PRETTY_FUNCTION__);
670     }
671     [line appendString:[_rawString substringWithRange:r]];
672     [self _parseLine:line];
673   }
674
675   if([self->cardStack count] != 0) {
676     if(debugOn) {
677       NSLog(@"%s found elements on cardStack. This indicates an improper "
678             @"iCal structure! Not all required events will have been "
679             @"generated, leading to unpredictable results!",
680             __PRETTY_FUNCTION__);
681     }
682   }
683
684   [line release];
685   [self->contentHandler endPrefixMapping:@""];
686   [self->contentHandler endDocument];
687 }
688
689 - (void)parseFromSource:(id)_source {
690   if (debugOn)
691     NSLog(@"%s: parse: %@", __PRETTY_FUNCTION__, _source);
692   
693   if ([_source isKindOfClass:[NSURL class]]) {
694     if (debugOn) 
695       NSLog(@"%s: trying to load URL...",__PRETTY_FUNCTION__);
696     _source = [_source resourceDataUsingCache:NO];
697   }
698   
699   if ([_source isKindOfClass:[NSData class]]) {
700     // FIXME: Data is not always utf-8.....
701     if (debugOn) 
702       NSLog(@"%s: trying to decode data...",__PRETTY_FUNCTION__);
703     _source = [[[NSString alloc]
704                  initWithData:_source encoding:NSUTF8StringEncoding]
705                  autorelease];
706   }
707   
708   if ([_source isKindOfClass:[NSString class]]) {
709     if (debugOn) 
710       NSLog(@"%s: trying to parse string...",__PRETTY_FUNCTION__);
711     [self _parseString:_source];
712   } 
713   else {
714     if (debugOn) 
715       NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
716     // FIXME: Return Error
717   }
718 }
719
720 - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
721   [self parseFromSource:_source];
722 }
723
724 - (void)parseFromSystemId:(NSString *)_sysId {
725   NSURL *url;
726   
727   if ((url = [NSURL URLWithString:_sysId]))
728     [self parseFromSource:url systemId:_sysId];
729 }
730
731 /* debugging */
732
733 - (BOOL)isDebuggingEnabled {
734   return debugOn;
735 }
736
737 @end /* VersitSaxDriver */