Changeset 46

Show
Ignore:
Timestamp:
04/18/05 19:29:12 (4 years ago)
Author:
pragma
Message:

Refactored XML core lib:

- Exceptions now chain and provide token dump information
- DTD parsing now recognizes defined entities.
- Redefined Parser/Lexer relationship from inheritance to aggregation
- Added separate DTD parser and lexer to circumvent numerous bugs
- Added several more files from the conformance suite to xmltest.d

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/xml/ILexerStream.d

    r43 r46  
    3030 
    3131interface ILexerStream{ 
     32    char[] getStreamName(); 
    3233    bit hasMore(); 
    3334    char get(); 
     
    3536    char peek(); 
    3637    Position getPosition(); 
     38    Position getLastPosition(); 
    3739    bit isMatch(char[] match); 
    3840    char[] getUntil(char match); 
  • trunk/xml/IXMLConsumer.d

    r43 r46  
    3030interface IXMLConsumer{ 
    3131    void xmlProlog(XMLAttributes attribs); 
    32     void xmlStartDoctype(char[] name,char[] pubidLiteral,char[] systemLiteral); 
    33     void xmlEndDoctype(char[] name,char[] pubidLiteral,char[] systemLiteral); 
     32    void xmlStartDoctype(char[] name); 
     33    void xmlEndDoctype(char[] name); 
    3434     
    3535    void xmlProcessingInstruction(char[] name,char[] data); 
  • trunk/xml/IXMLConsumerAdapter.d

    r43 r46  
    3131class IXMLConsumerAdapter : IXMLConsumer{ 
    3232    void xmlProlog(XMLAttributes attribs){} 
    33     void xmlStartDoctype(char[] name,char[] pubidLiteral,char[] systemLiteral){} 
    34     void xmlEndDoctype(char[] name,char[] pubidLiteral,char[] systemLiteral){} 
     33    void xmlStartDoctype(char[] name){} 
     34    void xmlEndDoctype(char[] name){} 
    3535     
    3636    void xmlProcessingInstruction(char[] name,char[] data){} 
  • trunk/xml/IXMLProvider.d

    r43 r46  
    2929 
    3030interface IXMLProvider{ 
    31     ILexerStream resolveURI(char[] pubid,char[] system); 
     31    ILexerStream resolveExternalID(char[] pubid,char[] system); 
    3232} 
  • trunk/xml/OfflineProvider.d

    r43 r46  
    2727 
    2828private import xml.ILexerStream; 
     29private import xml.SimpleStream; 
    2930private import xml.IXMLProvider; 
     31private import xml.XMLException; 
    3032 
    3133private import std.file; 
     34private import std.utf; 
    3235 
    3336class OfflineProvider : IXMLProvider{ 
    34     ILexerStream resolveURI(char[] pubid,char[] system){ 
    35         //throw out the public identifier 
     37    char[] currentPath; 
     38     
     39    public this(){ 
     40        currentPath = ""; 
     41    } 
     42     
     43    public this(char[] currentPath){ 
     44        this.currentPath = currentPath; 
     45    } 
     46     
     47    public void setCurrentPath(char[] currentPath){ 
     48        this.currentPath = currentPath; 
     49    } 
     50     
     51    ILexerStream resolveExternalID(char[] pubid,char[] system){ 
     52        //throw out the public identifier (not useful here) 
    3653         
    37         if(system[0..5] == "http:"){ 
    38             return null; 
     54        if(system.length == 0) return new SimpleStream(""); 
     55         
     56         
     57         
     58        if(system.length < 5 || system[0..5] == "http:"){ 
     59            return null; // can't do this offline 
    3960        } 
    4061         
    4162        // attempt to load the systemURI as a file 
    4263         
     64        char[] filename; 
     65         
     66        if(system[0] != '/' || system[0] != '\\'){ 
     67            filename = currentPath ~ system; 
     68        } 
     69        else{ 
     70            filename = system; 
     71        } 
     72                 
     73        try{ 
     74            return new SimpleStream(toUTF8(cast(char[])std.file.read(filename))); 
     75        } 
     76        catch(Exception e){ 
     77            throw new XMLException("failure to resolve file: " ~ system); 
     78        } 
     79        return null; 
    4380    } 
    4481} 
  • trunk/xml/SimpleStream.d

    r43 r46  
    3232class SimpleStream : ILexerStream{ 
    3333    char[] data; 
     34    char[] name; 
    3435    uint position; 
    3536    uint lastPosition; 
    36  
    37     public this(char[] utf8data){ 
     37     
     38    public this(char[] name,char[] utf8data){ 
     39        this.name = name; 
    3840        this.data = utf8data; 
    3941        reset(); 
     42    } 
     43     
     44    public this(char[] utf8data){ 
     45        this("",utf8data); 
     46    }    
     47     
     48    public char[] getStreamName(){ 
     49        return name; 
    4050    } 
    4151     
     
    7080    } 
    7181     
     82    Position getLastPosition(){ 
     83        Position pos; 
     84        pos.row = 1; 
     85        pos.col = 0; 
     86         
     87        for(int i=0; i<data.length && i<=lastPosition; i++){ 
     88            if(data[i] == '\n' || data[i] == '\r'){ 
     89                pos.row++; 
     90                pos.col = 1; 
     91                while(i<data.length && (data[i] == '\n' || data[i] == '\r')) i++; 
     92            } 
     93            else{ 
     94                pos.col++; 
     95            } 
     96        } 
     97        return pos; 
     98    } 
     99     
    72100    public Position getPosition(){ 
    73101        Position pos; 
  • trunk/xml/XMLException.d

    r43 r46  
    2727 
    2828private import xml.Position; 
     29private import xml.BaseParser; 
     30private import xml.BaseLexer; 
     31private import xml.ILexerStream; 
    2932 
    3033private import std.string; 
    3134 
     35//TODO: add chaining, named streams and references to originating lexer 
     36 
    3237class XMLException : Exception{ 
     38    XMLException cause; 
     39     
    3340    public this(char[] reason){ 
    3441        super(reason); 
    3542    } 
    3643     
    37     public this(Position pos,char[] reason){ 
    38         super(format("(%d,%d) %s",pos.row,pos.col,reason)); 
     44    public this(XMLException cause,char[] reason){ 
     45        super(reason); 
     46        this.cause = cause;  
     47    } 
     48     
     49    public char[] toString(){ 
     50        if(!cause) return super.toString(); 
     51        return super.toString() ~ "\n" ~ cause.toString(); 
    3952    } 
    4053} 
     54 
     55class XMLLexerException : XMLException{ 
     56    public BaseLexer lex; 
     57    public Position pos; 
     58    public char[] streamName; 
     59     
     60    public this(BaseLexer lex,ILexerStream stream,char[] reason){ 
     61        super(reason); 
     62        this.lex = lex; 
     63        pos = stream.getPosition(); 
     64        streamName = stream.getStreamName(); 
     65    } 
     66     
     67    public this(XMLException cause,BaseLexer lex,ILexerStream stream,char[] reason){ 
     68        super(cause,reason); 
     69        this.lex = lex; 
     70        pos = stream.getPosition(); 
     71        streamName = stream.getStreamName(); 
     72    }    
     73     
     74    public char[] toString(){ 
     75        return format("%s (%d,%d): %s\nToken Dump:\n%s",streamName,pos.row,pos.col,super.toString(),lex.toString()); 
     76    } 
     77} 
     78 
     79class XMLParserException : XMLException{ 
     80    public BaseParser parser; 
     81    public Position pos; 
     82    public char[] streamName; 
     83     
     84    public this(BaseParser parser,Position pos,char[] reason){ 
     85        super(reason); 
     86        this.parser = parser; 
     87        this.pos = pos; 
     88        streamName = parser.getStreamName(); 
     89    } 
     90     
     91    public this(XMLException cause,BaseParser parser,Position pos,char[] reason){ 
     92        super(cause,reason); 
     93        this.parser = parser; 
     94        this.pos = pos; 
     95        streamName = parser.getStreamName(); 
     96    }    
     97     
     98    public char[] toString(){ 
     99        return format("%s (%d,%d): %s\nToken Dump:\n%s",streamName,pos.row,pos.col,super.toString(),parser.toString()); 
     100    } 
     101} 
  • trunk/xml/XMLLexer.d

    r43 r46  
    2626module xml.XMLLexer; 
    2727 
     28private import xml.BaseLexer; 
    2829private import xml.ILexerStream; 
    2930private import xml.XMLToken; 
     
    3536private import std.string; 
    3637 
    37 class XMLLexer{ 
    38     private XMLToken[] tokens; 
    39     private uint position; 
    40     private ILexerStream stream; // used while tokenizing 
    41      
    42     public this(){ 
    43         reset(); 
    44     } 
    45      
    46     public void reset(){ 
    47         position = 0; 
    48         tokens.length = 0; 
    49     } 
    50      
    51     public XMLToken[] getTokens(){ 
    52         return tokens; 
    53     } 
    54  
    55     protected bit hasMore(){ 
    56         return position < tokens.length; 
    57     } 
    58      
    59     protected XMLToken getNext(){ 
    60         if(position == tokens.length) throw new XMLException("unexpected end of file"); 
    61         XMLToken token = tokens[position]; 
    62         position++; 
    63         return token; 
    64     } 
    65      
    66     protected XMLToken getNext(uint type){ 
    67         if(position == tokens.length) throw new XMLException("unexpected end of file"); 
    68         XMLToken token = tokens[position]; 
    69         if(token.type != type) throw new XMLException(token.pos,"invalid token " ~ token.toName() ~ ", expected " ~ XMLToken.toName(type)); 
    70         position++; 
    71         return token; 
    72     } 
    73      
    74     protected XMLToken getNextOptional(uint type){ 
    75         if(position == tokens.length) return XMLToken.Null; 
    76         XMLToken token = tokens[position]; 
    77         if(token.type != type) return XMLToken.Null; 
    78         position++; 
    79         return token; 
    80     }    
    81      
    82     protected XMLToken getNext(uint type,char[] value){ 
    83         if(position == tokens.length) throw new XMLException("unexpected end of file"); 
    84         XMLToken token = tokens[position]; 
    85         if(token.type != type) throw new XMLException(token.pos,"invalid token " ~ token.toName() ~ ", expected " ~ XMLToken.toName(type)); 
    86         position++; 
    87         return token; 
    88     } 
    89     /* 
    90     protected void unget(uint times=1){ 
    91         position-=times; 
    92         if(position < 0) position = 0; 
    93     } 
    94     */ 
    95     protected XMLToken peek(){ 
    96         if(position == tokens.length) throw new XMLException("unexpected end of file"); 
    97         return tokens[position]; 
    98     } 
    99      
    100     /*  
    101         used to allow parsed entities to inject themselves into the token stream *at* the read position  
    102         note: this will have the side effect of changing the result of peek() before and after the call, 
    103             since the position will point to a different token 
    104     */ 
    105     protected void insertTokens(XMLToken[] newTokens){ 
    106         tokens = tokens[0..position] ~ newTokens ~ tokens[position..$]; 
    107     } 
    108      
    109     private void addToken(char value){ 
    110         addToken(value,toString(value)); 
    111     } 
    112      
    113     private void addToken(uint type,char[] value){ 
    114         XMLToken token; 
    115          
    116         token.pos = stream.getPosition(); 
    117         token.type = type; 
    118         token.value = value; 
    119          
    120         tokens.length = tokens.length + 1; 
    121         tokens[$-1] = token; 
    122          
    123         //printf("%.*s\n",token.toString()); 
    124     } 
    125      
    126     protected void tokenize(ILexerStream stream){ 
     38class XMLLexer : BaseLexer{ 
     39    public void tokenize(ILexerStream stream){ 
    12740        assert(this.stream == null); 
    12841        this.stream = stream; 
     
    15669                 
    15770            case '>': 
    158                 throw new XMLException(stream.getPosition(),"illegal token '>' (missing '<')"); 
     71                throw new XMLLexerException(this,stream,"illegal token '>' (missing '<')"); 
    15972                break; 
    16073                 
     
    17184        this.stream = null; 
    17285    } 
    173  
    174     /* hook to allow parsed entities to function */  
    175     public void tokenizeDTD(ILexerStream stream){ 
    176         assert(this.stream == null); 
    177         this.stream = stream; 
    178         reset(); 
    179         tokenizeDTDElement(); 
    180         stream = null; 
    181     }    
    18286     
    183     private void tokenizePI(){ 
    184         tokenizeName(); 
    185         addToken(TokSpace,stream.getWhileIn(" \t\r\n")); 
    186              
    187         // use element rules to lex xml directives 
    188         if(tolower(tokens[$-2].value) == "xml"){         
    189             tokenizeElement();  
    190         } 
    191         // use raw character data instead 
    192         else{ 
    193             addToken(TokChars,stream.getUntil("?>")); 
    194             addToken(stream.get()); 
    195             addToken(stream.get()); 
    196         } 
    197     } 
    198      
    199     private void tokenizeElement(){ 
     87    protected void tokenizeElement(){ 
    20088        while(stream.hasMore()){ 
    20189            switch(stream.peek()){       
     
    20391                addToken(stream.get()); 
    20492                return; 
    205              
     93                     
     94            case '=': 
    20695            case '!': 
     96            case '/': 
    20797            case '?': 
    208             case '/': 
    209             case '=': 
    21098                addToken(stream.get()); 
    21199                break; 
    212100                 
    213101            case '[': 
    214                 addToken(stream.get()); 
     102                stream.get(); 
    215103                tokenizeDTD(); 
    216104                break; 
     
    225113            case '\r': 
    226114            case '\n': 
    227                 addToken(TokSpace,stream.getWhileIn(" /t/r/n")); 
    228                 break; 
    229                  
    230             case '&': 
    231                 tokenizeEntity(); 
     115                addToken(TokSpace,stream.getWhileIn(" \t\r\n")); 
    232116                break; 
    233117                 
     
    240124    } 
    241125     
    242     private void tokenizeDTD(){ 
    243         while(stream.hasMore()){ 
    244             switch(stream.peek()){ 
    245             case '<':  
    246                 if(stream.isMatch("<!--")){ 
    247                     //TODO: the spec maintains that '-' is also illegal inside the comment - fix this 
    248                     stream.get(4); 
    249                     addToken(TokComment,stream.getUntil("-->")); 
    250                     stream.get(3); 
    251                 } 
    252                 else if(stream.isMatch("<?")){ 
    253                     addToken(stream.get()); // eat '<' 
    254                     addToken(stream.get()); // eat '?' 
    255                     tokenizePI(); 
    256                 } 
    257                 else{ 
    258                     addToken(stream.get()); 
    259                     tokenizeDTDElement();  
    260                 } 
    261                 break; 
    262                  
    263             case '%': 
    264                 // handle parsed entity 
    265                 //TODO: break into separate function 
    266                 addToken(stream.get()); 
    267                 addToken(TokName,stream.getUntil(';')); 
    268                 addToken(stream.get()); 
    269                 break; 
    270                  
    271             case ']': 
    272                 addToken(stream.get()); 
    273                 return; 
    274                  
    275             case ' ': 
    276             case '\t': 
    277             case '\r': 
    278             case '\n': 
    279                 addToken(TokSpace,stream.getWhileIn(" \t\r\n")); 
    280                 break; 
    281                  
    282             default: 
    283                 throw new XMLException(stream.getPosition(),"illegal token '" ~ toString(stream.get) ~ "'"); 
    284                 break; 
    285             } 
    286         } 
    287     } 
    288  
    289     private void tokenizeDTDElement(){ 
    290         while(stream.hasMore()){ 
    291             switch(stream.peek()){       
    292             case '>': 
    293                 addToken(stream.get()); 
    294                 return; 
    295              
    296             case '!': 
    297             case '?':        
    298             case '/':            
    299             case '=':    
    300             case '|': 
    301             case ',': 
    302             case '+': 
    303             case '-': 
    304             case '(': 
    305             case ')': 
    306             case '*': 
    307             case '%':    
    308             case ';': 
    309                 addToken(stream.get()); 
    310                 break;                   
    311                          
    312             case '\'': 
    313             case '\"': 
    314                 tokenizeString(stream.get()); 
    315                 break; 
    316                  
    317             case ' ': 
    318             case '\t': 
    319             case '\r': 
    320             case '\n': 
    321                 addToken(TokSpace,stream.getWhileIn(" \t\r\n")); 
    322                 break; 
    323                  
    324             case '&': 
    325                 tokenizeEntity(); 
    326                 break; 
    327                                              
    328             case '#': 
    329                 tokenizeNmtoken(); 
    330                 break; 
    331                                  
    332             default: 
    333                 tokenizeName(); 
    334                 break; 
    335             } 
    336         } 
    337     } 
    338  
    339     private bit verifyNameChar(char ch){ 
    340         return  
    341             ch == '_' ||  
    342             ch == ':' ||   
    343             ch == '.' || 
    344             (ch >= 'a' && ch <= 'z') || 
    345             (ch >= 'A' && ch <= 'Z') || 
    346             (ch >= '0' && ch <= '9') 
    347             ; 
    348     } 
    349      
    350     private bit verifyNameStartChar(char ch){ 
    351         return  
    352             ch == '_' ||  
    353             ch == ':' ||   
    354             (ch >= 'a' && ch <= 'z') || 
    355             (ch >= 'A' && ch <= 'Z') 
    356             ; 
    357     } 
    358      
    359     private void tokenizeName(){ 
    360          
    361         if(!verifyNameStartChar(stream.peek())){ 
    362             throw new XMLException(stream.getPosition(),"illegal token '" ~ toString(stream.peek()) ~ "'"); 
    363         } 
    364      
    365         addToken(TokName,stream.getWhile(&verifyNameChar)); 
    366     } 
    367      
    368     private void tokenizeNmtoken(){ 
    369         stream.get(); //eat '#' token 
    370         addToken(TokName,stream.getWhile(&verifyNameChar));  
    371     } 
    372      
    373     private void tokenizeString(char terminator){ 
    374         char[] value; 
    375         char[4] term; 
    376         term[0] = terminator; 
    377         term[1] = '&'; 
    378         term[2] = '%'; // capture tokens useful for PE-sensitive areas 
    379         term[3] = ';'; 
    380          
    381         addToken(TokString,toString(terminator)); 
    382          
    383         while(stream.hasMore()){ 
    384             addToken(TokChars,stream.getUntilIn(term)); 
    385             char ch = stream.peek(); 
    386             if(ch == terminator){ 
    387                 addToken(TokString,toString(stream.get())); 
    388                 return; 
    389             } 
    390             else if(ch == '&'){ 
    391                 tokenizeEntity(); 
    392             } 
    393             else{ 
    394                 addToken(stream.get); // plug '%' and ';' in as separate bits 
    395             } 
    396         } 
    397         throw new XMLException(stream.getPosition(),"unexpected end of file"); 
    398     } 
    399      
    400     private void tokenizeEntity(){ 
    401         stream.get(); //eat '&' token 
    402         if(stream.peek() == '#'){ 
    403             char[] characterValue; 
    404             ulong value; 
    405          
    406             stream.get(); //eat '#' token 
    407             if(stream.peek() == 'x'){ 
    408                 stream.get(); //eat 'x' token 
    409                 characterValue = stream.getUntil(';'); 
    410                                  
    411                 //convert using radix16 
    412                 value = 0; 
    413                 for(uint i=0; i<characterValue.length; i++){ 
    414                     char ch = characterValue[i]; 
    415                     value *= 16; 
    416                      
    417                     if(ch >= 'a' && ch <= 'z'){ 
    418                         value += (ch - 'a') + 10; 
    419                     } 
    420                     else if(ch >= 'A' && ch <= 'Z'){ 
    421                         value += (ch - 'A') + 10; 
    422                     } 
    423                     else if(ch >= '0' && ch <= '9'){ 
    424                         value += (ch - '0'); 
    425                     } 
    426                     else{ 
    427                         throw new XMLException(stream.getPosition(),"invalid entity (not valid hex number)"); 
    428                     } 
    429                 } 
    430             } 
    431             else{ 
    432                 characterValue = stream.getUntil(';'); 
    433                 value = toUlong(characterValue);         
    434             } 
    435             dchar[1] newValue; 
    436             newValue[0] = cast(dchar)value; 
    437              
    438             addToken(TokChars,toUTF8(newValue)); 
    439         } 
    440         else{ 
    441             addToken(TokEntity,stream.getUntil(';')); 
    442         } 
    443         stream.get(); //eat ';' token 
    444     } 
    445      
    446     private static char[] toString(char ch){ 
    447         char[] val = new char[1]; 
    448         val[0] = ch; 
    449         return val; 
    450     } 
    451      
    452     public char[] dumpTokens(){ 
    453         char[] output = ""; 
    454         foreach(XMLToken tok; tokens){ 
    455             output ~= tok.toString() ~ "\n"; 
    456         } 
    457         return output; 
     126    protected void tokenizeDTD(){ 
     127        stream.getWhileIn(" \t\r\n"); 
     128        addToken(TokDTD,stream.getUntil(']')); 
     129        stream.get(); 
    458130    } 
    459131} 
  • trunk/xml/XMLParser.d

    r43 r46  
    3131private import xml.XMLLexer; 
    3232private import xml.XMLToken; 
     33private import xml.BaseLexer; 
     34private import xml.BaseParser; 
    3335private import xml.XMLException; 
    34  
     36private import xml.DTDParser; 
    3537private import xml.SimpleStream; 
    3638private import xml.OfflineProvider; 
     
    4648*/ 
    4749 
    48 class XMLParser : XMLLexer{ 
    49     private IXMLProvider provider; 
    50     private IXMLConsumer consumer; 
    51      
    52     char[][char[]] entities; 
    53     XMLToken[][char[]] parsedEntities; 
    54  
    55     public this(){ 
    56         entities["apos"] = "'"; 
    57         entities["quot"] = "\""; 
    58         entities["lt"] = "<"; 
    59         entities["gt"] = ">"; 
    60         entities["amp"] = "&"; 
    61     } 
    62      
    63     public void parse(IXMLConsumer consumer, IXMLProvider provider, ILexerStream stream){ 
    64         this.provider = provider; 
    65         this.consumer = consumer; 
    66          
    67         tokenize(stream); 
    68         startParse(); 
    69     } 
    70      
    71     public void parse(IXMLConsumer consumer, ILexerStream stream){ 
    72         parse(consumer, new OfflineProvider(), stream); 
    73     } 
    74      
    75     public void parse(IXMLConsumer consumer, char[] utf8data){ 
    76         parse(consumer, new OfflineProvider(), new SimpleStream(utf8data)); 
    77     } 
    78          
    79     private void startParse(){ 
    80         parseProlog(); 
     50class XMLParser : BaseParser{ 
     51    protected BaseLexer getLexer(){ 
     52        return new XMLLexer(); 
     53    } 
     54         
     55    protected void startParse(){ 
     56        getNext(TokStartElem); 
     57         
     58        if(peek().type == TokQuestion){ 
     59            parseProlog(); 
     60        } 
     61        parseMisc(); 
    8162         
    8263        if(peek().type == TokName){ 
     
    8667    } 
    8768     
    88     private void parseProlog(){ 
     69    protected void parseProlog(){ 
    8970        /* 
    9071            [22]    prolog     ::=      XMLDecl? Misc* (doctypedecl Misc*)? 
     
    9576        */ 
    9677         
    97         //<?xml 
    98         getNext(TokStartElem); 
     78        //?xml 
    9979        getNext(TokQuestion); 
    100         if(getNext().value != "xml"){ 
    101             throw new XMLException(peek().pos,"document prolog must begin with an 'xml' processing instruction"); 
     80        if(std.string.tolower(getNext().value) != "xml"){ 
     81            throw new XMLParserException(this,peek().pos,"document prolog must begin with an 'xml' processing instruction"); 
    10282        } 
    10383         
     
    11696            getNext(); // eat token 
    11797            parseDoctype(); 
    118             parseMisc(); 
    119         } 
    120     } 
    121      
    122     private void parseDoctype(){ 
     98        } 
     99    } 
     100     
     101    protected void parseMisc(){ 
     102        /* 
     103        [27]    Misc       ::=      Comment | PI | S     
     104        */ 
     105        while(hasMore()){ 
     106            switch(peek().type){ 
     107            case TokSpace: 
     108                getNext(); // eat whitespace 
     109                break; 
     110                 
     111            case TokComment: 
     112                consumer.xmlComment(getNext().value); 
     113                break; 
     114                 
     115            case TokStartElem: 
     116                getNext(); // eat '<' token 
     117                if(peek().type == TokQuestion){ 
     118                    getNext(); // eat '?' token 
     119                    parseProcessingInstruction(); 
     120                    break; 
     121                } 
     122                else return; // something other than a PI, let parent worry about it 
     123                 
     124            default: 
     125                throw new XMLParserException(this,peek().pos,"expected processing instruction"); 
     126            } 
     127        } 
     128    }    
     129     
     130    protected void parseDoctype(){ 
    123131        /* 
    124132        [28]    doctypedecl    ::=      '<!DOCTYPE' S Name (S ExternalID)? S? ('[' intSubset ']' S?)? '>'   [VC: Root Element Type][WFC: External Subset] 
     
    134142         
    135143        name = getNext(TokName).value; 
    136         getNextOptional(TokSpace); 
     144        getNextOptional(TokSpace);      
    137145         
    138146        if(peek().type == TokName){ 
    139             if(peek().value == "SYSTEM"){ 
    140                 getNext(); // eat token 
    141                 getNext(TokSpace); 
    142                 systemLiteral = parseString();       
    143             } 
    144             else if(peek().value == "PUBLIC"){ 
    145                 getNext(); // eat token 
    146                 getNext(TokSpace); 
    147                 pubidLiteral = parseString(); 
    148                 getNext(TokSpace); 
    149                 systemLiteral = parseString();               
     147            ILexerStream newStream = parseExternalID(); 
     148             
     149            if(newStream){ 
     150                // check for non-empty stream 
     151                if(newStream.hasMore()){ 
     152                    //parse against DTD 
     153                    DTDParser parser = new DTDParser(); 
     154                    parser.parse(consumer,provider,newStream); 
     155                     
     156                    //copy any useful bits 
     157                    copyParserData(parser); 
     158                } 
    150159            } 
    151160            else{ 
    152                 throw new XMLException(peek().pos,"document external identity must begin with 'PUBLIC' or 'SYSTEM'"); 
    153             } 
    154             getNextOptional(TokSpace); 
    155         } 
    156          
    157         consumer.xmlStartDoctype(name,pubidLiteral,systemLiteral); 
     161                throw new XMLParserException(this,peek().pos,"could not resolve external DTD"); 
     162            } 
     163             
     164            getNextOptional(TokSpace); 
     165        } 
     166         
     167        consumer.xmlStartDoctype(name); 
    158168     
    159169        // parse all the elements under the doctype 
    160         if(peek().type == TokStartDTD){ 
    161             getNext(); // eat '[' token 
    162             parseDTD();  
     170        if(peek().type == TokDTD){ 
     171            SimpleStream newStream = new SimpleStream(getNext().value); 
     172            DTDParser parser; 
     173            try{ 
     174                parser = new DTDParser(); 
     175                parser.parse(consumer,provider,newStream); 
     176            } 
     177            catch(XMLException e){ 
     178                throw new XMLParserException(e,this,peek().pos,"Error in parsing DTD"); 
     179            } 
     180 
     181            //copy any useful bits 
     182            copyParserData(parser); 
    163183            getNextOptional(TokSpace); 
    164184        } 
     
    166186        getNext(TokEndElem); 
    167187         
    168         consumer.xmlEndDoctype(name,pubidLiteral,systemLiteral); 
    169     } 
    170      
    171     private void parseDTD(){ 
    172         while(hasMore()){ 
    173             switch(peek().type){ 
    174             case TokSpace: 
    175                 getNext(); // eat token; 
    176                 break; 
    177                  
    178             case TokComment: 
    179                 consumer.xmlComment(getNext().value); 
    180                 break; 
    181              
    182             case TokPercent: 
    183                 getNext(); // eat token 
    184                 parseParsedEntity(); 
    185                 break; 
    186                              
    187             case TokStartElem: 
    188                 getNext(); // eat token; 
    189                  
    190                 if(peek().type == TokQuestion){ 
    191                     getNext(); // eat token; 
    192                     parseProcesingInstruction(); 
    193                 } 
    194                 else{ 
    195                     getNext(TokBang); // next must be a '!' 
    196  
    197                     // check out the element name 
    198                     XMLToken tok = getNext(TokName); 
    199                     switch(tok.value){ 
    200                     case "ELEMENT": 
    201                         parseDTDElement(); 
    202                         break; 
    203                          
    204                     case "ATTLIST": 
    205                         parseDTDAttlist(); 
    206                         break;       
    207                          
    208                     case "ENTITY": 
    209                         parseDTDEntity(); 
    210                         break;       
    211                          
    212                     case "NOTATION": 
    213                         parseDTDNotation(); 
    214                         break;       
    215                          
    216                     default: 
    217                         throw new XMLException(tok.pos,"invalid DTD element: '" ~ tok.value ~ "'"); 
    218                     } 
    219                 } 
    220                 break; 
    221              
    222             case TokEndDTD: 
    223                 getNext(); // eat ']' token 
    224                 return; 
    225                  
    226             default: 
    227                 // shouldn't ever get here 
    228                 throw new XMLException(peek().pos,"expected DTD element or closing ']'"); 
    229                 break;  
    230             } 
    231         } 
    232     } 
    233      
    234     private void parseDTDElement(){ 
    235         /* 
    236         [45]    elementdecl    ::=      '<!ELEMENT' S Name S contentspec S? '>' [VC: Unique Element Type Declaration] 
    237         [46]    contentspec    ::=      'EMPTY' | 'ANY' | Mixed | children 
    238         */ 
    239          
    240         char[] name; 
    241      
    242         getNext(TokSpace); 
    243         name = getNext(TokName).value; 
    244          
    245         //TODO: unbreak me 
    246         while(hasMore()){    
    247             if(getNext().type == TokEndElem){ 
    248                 break; 
    249             } 
    250         } 
    251     } 
    252      
    253     private void parseDTDAttlist(){ 
    254         /* 
    255         [45]    elementdecl    ::=      '<!ELEMENT' S Name S contentspec S? '>' [VC: Unique Element Type Declaration] 
    256         [46]    contentspec    ::=      'EMPTY' | 'ANY' | Mixed | children 
    257         */ 
    258      
    259         //TODO: unbreak me 
    260         while(hasMore()){ 
    261             if(getNext().type == TokEndElem){ 
    262                 break; 
    263             } 
    264         } 
    265     } 
    266      
    267     private void parseDTDEntity(){ 
    268         /* 
    269         [70]    EntityDecl     ::=      GEDecl | PEDecl 
    270         [71]    GEDecl     ::=      '<!ENTITY' S Name S EntityDef S? '>' 
    271         [72]    PEDecl     ::=      '<!ENTITY' S '%' S Name S PEDef S? '>' 
    272         [73]    EntityDef      ::=      EntityValue| (ExternalID NDataDecl?) 
    273         [74]    PEDef      ::=      EntityValue | ExternalID 
    274          
    275         [9]     EntityValue    ::=      '"' ([^%&"] | PEReference | Reference)* '"' 
    276             |  "'" ([^%&'] | PEReference | Reference)* "'"       
    277              
    278         [67]    Reference      ::=      EntityRef | CharRef 
    279         [68]    EntityRef      ::=      '&' Name ';'             
    280         [69]    PEReference    ::=      '%' Name ';' 
    281          
    282         [75]    ExternalID     ::=      'SYSTEM' S SystemLiteral 
    283          
    284         [76]    NDataDecl      ::=      S 'NDATA' S Name 
    285             | 'PUBLIC' S PubidLiteral S SystemLiteral        
    286         */ 
    287         char[] name, value; 
    288          
    289         getNext(TokSpace); 
    290          
    291         switch(peek().type){ 
    292         case TokPercent:  
    293             // handle parsed entity 
    294             getNext(); // eat token; 
     188        consumer.xmlEndDoctype(name); 
     189    } 
     190     
     191    protected ILexerStream parseExternalID(){ 
     192        char[] pubidLiteral; 
     193        char[] systemLiteral; 
     194         
     195        if(peek().value == "SYSTEM"){ 
     196            pubidLiteral = ""; // empty value 
     197            getNext(); // eat token 
    295198            getNext(TokSpace); 
    296              
    297             name = getNext(TokName).value; 
     199            systemLiteral = parseString();   
     200        } 
     201        else if(peek().value == "PUBLIC"){ 
     202            getNext(); // eat token 
    298203            getNext(TokSpace); 
    299