docs: private void parseDoctypedecl() throws Exception {
          String rootName;
          ExternalIdentifiers ids;
  
          // Read the document type name.
          requireWhitespace();
          rootName = readNmtoken(true);
  
          // Read the External subset's IDs
          skipWhitespace();
          ids = readExternalIds(false, true);
  
          // report (a) declaration of name, (b) lexical info (ids)
          handler.doctypeDecl(rootName, ids.publicId, ids.systemId);
  
          // Internal subset is parsed first, if present
          skipWhitespace();
          if (tryRead('[')) {
  
              // loop until the subset ends
              while (true) {
                  doReport = expandPE = true;
                  skipWhitespace();
                  doReport = expandPE = false;
                  if (tryRead(']')) {
                      break; // end of subset
                  } else {
                      // WFC, PEs in internal subset (only between decls)
                      peIsError = expandPE = true;
                      parseMarkupdecl();
                      peIsError = expandPE = false;
                  }
              }
          }
          skipWhitespace();
          require('>');
  
          // Read the external subset, if any
          InputSource subset;
  
          if (ids.systemId == null) {
              subset = handler.getExternalSubset(rootName, handler.getSystemId());
          } else {
              subset = null;
          }
          if ((ids.systemId != null) || (subset != null)) {
              pushString(null, ">");
  
              // NOTE: [dtd] is so we say what SAX2 expects,
              // though it's misleading (subset, not entire dtd)
              if (ids.systemId != null) {
                  pushURL(true, "[dtd]", ids, null, null, null, true);
              } else {
                  handler.warn("modifying document by adding external subset");
                  pushURL(true, "[dtd]", new ExternalIdentifiers(
                          subset.getPublicId(), subset.getSystemId(), null),
                          subset.getCharacterStream(), subset.getByteStream(),
                          subset.getEncoding(), false);
              }
  
              // Loop until we end up back at '>'
              while (true) {
                  doReport = expandPE = true;
                  skipWhitespace();
                  doReport = expandPE = false;
                  if (tryRead('>')) {
                      break;
                  } else {
                      expandPE = true;
                      parseMarkupdecl();
                      expandPE = false;
                  }
              }
  
              // the ">" string isn't popped yet
              if (inputStack.size() != 1) {
                  fatal("external subset has unmatched '>'");
              }
          }
  
          // done dtd
          handler.endDoctype();
          expandPE = false;
          doReport = true;
      }