docs: private Object extendArray(Object array, int currentSize, int requiredSize) {
          if (requiredSize < currentSize) {
              return array;
          } else {
              Object newArray = null;
              int newSize = currentSize * 2;
  
              if (newSize <= requiredSize) {
                  newSize = requiredSize + 1;
              }
  
              if (array instanceof char[]) {
                  newArray = new char[newSize];
              } else if (array instanceof Object[]) {
                  newArray = new Object[newSize];
              } else {
                  throw new RuntimeException();
              }
  
              System.arraycopy(array, 0, newArray, 0, currentSize);
              return newArray;
          }
      }
  
      // ////////////////////////////////////////////////////////////////////
      // XML query routines.
      // ////////////////////////////////////////////////////////////////////
  
      boolean isStandalone() {
          return docIsStandalone;
      }
  
      //
      // Elements
      //
  
      private int getContentType(ElementDecl element, int defaultType) {
          int retval;
  
          if (element == null) {
              return defaultType;
          }
          retval = element.contentType;
          if (retval == CONTENT_UNDECLARED) {
              retval = defaultType;
          }
          return retval;
      }
  
      /**
       * Look up the content type of an element.
       * 
       * @param name
       *            The element type name.
       * @return An integer constant representing the content type.
       * @see #CONTENT_UNDECLARED
       * @see #CONTENT_ANY
       * @see #CONTENT_EMPTY
       * @see #CONTENT_MIXED
       * @see #CONTENT_ELEMENTS
       */
      public int getElementContentType(String name) {
          ElementDecl element = elementInfo.get(name);
          return getContentType(element, CONTENT_UNDECLARED);
      }
  
      /**
       * Register an element. Array format: [0] element type name [1] content
       * model (mixed, elements only) [2] attribute hash table
       */
      private void setElement(String name, int contentType, String contentModel,
              HashMap<String, AttributeDecl> attributes) throws SAXException {
          if (skippedPE) {
              return;
          }
  
          ElementDecl element = elementInfo.get(name);
  
          // first <!ELEMENT ...> or <!ATTLIST ...> for this type?
          if (element == null) {
              element = new ElementDecl();
              element.contentType = contentType;
              element.contentModel = contentModel;
              element.attributes = attributes;
              elementInfo.put(name, element);
              return;
          }
  
          // <!ELEMENT ...> declaration?
          if (contentType != CONTENT_UNDECLARED) {
              // ... following an associated <!ATTLIST ...>
              if (element.contentType == CONTENT_UNDECLARED) {
                  element.contentType = contentType;
                  element.contentModel = contentModel;
              } else {
                  // VC: Unique Element Type Declaration
                  handler.verror("multiple declarations for element type: "
                          + name);
              }
          }
  
          // first <!ATTLIST ...>, before <!ELEMENT ...> ?
          else if (attributes != null) {
              element.attributes = attributes;
          }
      }
  
      /**
       * Look up the attribute hash table for an element. The hash table is the
       * second item in the element array.
       */
      private HashMap<String, AttributeDecl> getElementAttributes(String name) {
          ElementDecl element = elementInfo.get(name);
          return (element == null) ? null : element.attributes;
      }
  
      //
      // Attributes
      //
  
      /**
       * Get the declared attributes for an element type.
       * 
       * @param elname
       *            The name of the element type.
       * @return An iterator over all the attributes declared for a specific
       *         element type. The results will be valid only after the DTD (if
       *         any) has been parsed.
       * @see #getAttributeType
       * @see #getAttributeEnumeration
       * @see #getAttributeDefaultValueType
       * @see #getAttributeDefaultValue
       * @see #getAttributeExpandedValue
       */
      private Iterator<String> declaredAttributes(ElementDecl element) {
          HashMap<String, AttributeDecl> attlist;
  
          if (element == null) {
              return null;
          }
          if ((attlist = element.attributes) == null) {
              return null;
          }
          return attlist.keySet().iterator();
      }
  
      /**
       * Get the declared attributes for an element type.
       * 
       * @param elname
       *            The name of the element type.
       * @return An iterator over all the attributes declared for a specific
       *         element type. The results will be valid only after the DTD (if
       *         any) has been parsed.
       * @see #getAttributeType
       * @see #getAttributeEnumeration
       * @see #getAttributeDefaultValueType
       * @see #getAttributeDefaultValue
       * @see #getAttributeExpandedValue
       */
      public Iterator<String> declaredAttributes(String elname) {
          return declaredAttributes(elementInfo.get(elname));
      }
  
      /**
       * Retrieve the declared type of an attribute.
       * 
       * @param name
       *            The name of the associated element.
       * @param aname
       *            The name of the attribute.
       * @return An interend string denoting the type, or null indicating an
       *         undeclared attribute.
       */
      public String getAttributeType(String name, String aname) {
          AttributeDecl attribute = getAttribute(name, aname);
          return (attribute == null) ? null : attribute.type;
      }
  
      /**
       * Retrieve the allowed values for an enumerated attribute type.
       * 
       * @param name
       *            The name of the associated element.
       * @param aname
       *            The name of the attribute.
       * @return A string containing the token list.
       */
      public String getAttributeEnumeration(String name, String aname) {
          AttributeDecl attribute = getAttribute(name, aname);
          // assert: attribute.enumeration is "ENUMERATION" or "NOTATION"
          return (attribute == null) ? null : attribute.enumeration;
      }
  
      /**
       * Retrieve the default value of a declared attribute.
       * 
       * @param name
       *            The name of the associated element.
       * @param aname
       *            The name of the attribute.
       * @return The default value, or null if the attribute was #IMPLIED or
       *         simply undeclared and unspecified.
       * @see #getAttributeExpandedValue
       */
      public String getAttributeDefaultValue(String name, String aname) {
          AttributeDecl attribute = getAttribute(name, aname);
          return (attribute == null) ? null : attribute.value;
      }
  
      /*
       * // FIXME: Leaving this in, until W3C finally resolves the confusion //
       * between parts of the XML 2nd REC about when entity declararations // are
       * guaranteed to be known. Current code matches what section 5.1 //
       * (conformance) describes, but some readings of the self-contradicting //
       * text in 4.1 (the "Entity Declared" WFC and VC) seem to expect that //
       * attribute expansion/normalization must be deferred in some cases // (just
       * TRY to identify them!).
       * 
       * Retrieve the expanded value of a declared attribute. <p>General entities
       * (and char refs) will be expanded (once). @param name The name of the
       * associated element. @param aname The name of the attribute. @return The
       * expanded default value, or null if the attribute was #IMPLIED or simply
       * undeclared
       * 
       * @see #getAttributeDefaultValue public String getAttributeExpandedValue
       *      (String name, String aname) throws Exception { AttributeDecl
       *      attribute = getAttribute (name, aname);
       * 
       * if (attribute == null) { return null; } else if (attribute.defaultValue ==
       * null && attribute.value != null) { // we MUST use the same buf for both
       * quotes else the literal // can't be properly terminated char buf [] = new
       * char [1]; int flags = LIT_ENTITY_REF | LIT_ATTRIBUTE; String type =
       * getAttributeType (name, aname);
       * 
       * if (type != "CDATA" && type != null) flags |= LIT_NORMALIZE; buf [0] =
       * '"'; pushCharArray (null, buf, 0, 1); pushString (null, attribute.value);
       * pushCharArray (null, buf, 0, 1); attribute.defaultValue = readLiteral
       * (flags); } return attribute.defaultValue; }
       */
  
      /**
       * Retrieve the default value mode of a declared attribute.
       * 
       * @see #ATTRIBUTE_DEFAULT_SPECIFIED
       * @see #ATTRIBUTE_DEFAULT_IMPLIED
       * @see #ATTRIBUTE_DEFAULT_REQUIRED
       * @see #ATTRIBUTE_DEFAULT_FIXED
       */
      public int getAttributeDefaultValueType(String name, String aname) {
          AttributeDecl attribute = getAttribute(name, aname);
          return (attribute == null) ? ATTRIBUTE_DEFAULT_UNDECLARED
                  : attribute.valueType;
      }
  
      /**
       * Register an attribute declaration for later retrieval. Format: - String
       * type - String default value - int value type - enumeration - processed
       * default value
       */
      private void setAttribute(String elName, String name, String type,
              String enumeration, String value, int valueType) throws Exception {
          HashMap<String, AttributeDecl> attlist;
  
          if (skippedPE) {
              return;
          }
  
          // Create a new hashtable if necessary.
          attlist = getElementAttributes(elName);
          if (attlist == null) {
              attlist = new HashMap<>();
          }
  
          // ignore multiple attribute declarations!
          if (attlist.get(name) != null) {
              // warn ...
              return;
          } else {
              AttributeDecl attribute = new AttributeDecl();
              attribute.type = type;
              attribute.value = value;
              attribute.valueType = valueType;
              attribute.enumeration = enumeration;
              attlist.put(name, attribute);
  
              // save; but don't overwrite any existing <!ELEMENT ...>
              setElement(elName, CONTENT_UNDECLARED, null, attlist);
          }
      }
  
      /**
       * Retrieve the attribute declaration for the given element name and name.
       */
      private AttributeDecl getAttribute(String elName, String name) {
          HashMap<String, AttributeDecl> attlist = getElementAttributes(elName);
          return (attlist == null) ? null : attlist.get(name);
      }
  
      //
      // Entities
      //
  
      /**
       * Find the type of an entity.
       * 
       * @returns An integer constant representing the entity type.
       * @see #ENTITY_UNDECLARED
       * @see #ENTITY_INTERNAL
       * @see #ENTITY_NDATA
       * @see #ENTITY_TEXT
       */
      public int getEntityType(String ename) {
          EntityInfo entity = entityInfo.get(ename);
          return (entity == null) ? ENTITY_UNDECLARED : entity.type;
      }
  
      /**
       * Return an external entity's identifiers.
       * 
       * @param ename
       *            The name of the external entity.
       * @return The entity's public identifier, system identifier, and base URI.
       *         Null if the entity was not declared as an external entity.
       * @see #getEntityType
       */
      public ExternalIdentifiers getEntityIds(String ename) {
          EntityInfo entity = entityInfo.get(ename);
          return (entity == null) ? null : entity.ids;
      }
  
      /**
       * Return an internal entity's replacement text.
       * 
       * @param ename
       *            The name of the internal entity.
       * @return The entity's replacement text, or null if the entity was not
       *         declared as an internal entity.
       * @see #getEntityType
       */
      public String getEntityValue(String ename) {
          EntityInfo entity = entityInfo.get(ename);
          return (entity == null) ? null : entity.value;
      }
  
      /**
       * Register an entity declaration for later retrieval.
       */
      private void setInternalEntity(String eName, String value)
              throws SAXException {
          if (skippedPE) {
              return;
          }
  
          if (entityInfo.get(eName) == null) {
              EntityInfo entity = new EntityInfo();
              entity.type = ENTITY_INTERNAL;
              entity.value = value;
              entityInfo.put(eName, entity);
          }
          if (handler.stringInterning) {
              if (("lt" == eName) || ("gt" == eName) || ("quot" == eName)
                      || ("apos" == eName) || ("amp" == eName)) {
                  return;
              }
          } else {
              if ("lt".equals(eName) || "gt".equals(eName)
                      || "quot".equals(eName) || "apos".equals(eName)
                      || "amp".equals(eName)) {
                  return;
              }
          }
          handler.getDeclHandler().internalEntityDecl(eName, value);
      }
  
      /**
       * Register an external entity declaration for later retrieval.
       */
      private void setExternalEntity(String eName, int eClass,
              ExternalIdentifiers ids, String nName) {
          if (entityInfo.get(eName) == null) {
              EntityInfo entity = new EntityInfo();
              entity.type = eClass;
              entity.ids = ids;
              entity.notationName = nName;
              entityInfo.put(eName, entity);
          }
      }
  
      //
      // Notations.
      //
  
      /**
       * Report a notation declaration, checking for duplicates.
       */
      private void setNotation(String nname, ExternalIdentifiers ids)
              throws SAXException {
          if (skippedPE) {
              return;
          }
  
          handler.notationDecl(nname, ids.publicId, ids.systemId, ids.baseUri);
          if (notationInfo.get(nname) == null) {
              notationInfo.put(nname, nname);
          } else {
              // VC: Unique Notation Name
              handler.verror("Duplicate notation name decl: " + nname);
          }
      }
  
      //
      // Location.
      //
  
      /**
       * Return the current line number.
       */
      public int getLineNumber() {
          if (line > 0) {
              return line;
          } else {
              return -1;
          }
      }
  
      /**
       * Return the current column number.
       */
      public int getColumnNumber() {
          if (column > 0) {
              return column;
          } else {
              return -1;
          }
      }
  
      // ////////////////////////////////////////////////////////////////////
      // High-level I/O.
      // ////////////////////////////////////////////////////////////////////
  
      /**
       * Read a single character from the readBuffer.
       * <p>
       * The readDataChunk () method maintains the buffer.
       * <p>
       * If we hit the end of an entity, try to pop the stack and keep going.
       * <p>
       * (This approach doesn't really enforce XML's rules about entity
       * boundaries, but this is not currently a validating parser).
       * <p>
       * This routine also attempts to keep track of the current position in
       * external entities, but it's not entirely accurate.
       * 
       * @return The next available input character.
       * @see #unread (char)
       * @see #readDataChunk
       * @see #readBuffer
       * @see #line
       * @return The next character from the current input source.
       */
      private char readCh() throws SAXException, IOException {
          // As long as there's nothing in the
          // read buffer, try reading more data
          // (for an external entity) or popping
          // the entity stack (for either).
          fillBuffer();
  
          char c = readBuffer[readBufferPos++];
          advanceLocation();
          // copied from fi.iki.hsivonen.htmlparser
          if ((c & 0xFC00) == 0xDC00) {
              // Got a low surrogate. See if prev was high surrogate
              if ((prev & 0xFC00) == 0xD800) {
                  int intVal = (prev << 10) + c + SURROGATE_OFFSET;
                  if (isNonCharacter(intVal)) {
                      handler.warn("Astral non-character.");
                  }
                  if (isAstralPrivateUse(intVal)) {
                      warnAboutPrivateUseChar();
                  }
              } else {
                  if (prev != 0) {
                      // the prev == 0 situation is bogus, but better not to fail
                      fatal("Unmatched low surrogate.");
                  }
              }
              prev = c;
          } else {
              // see if there was a lone high surrogate
              if ((prev & 0xFC00) == 0xD800) {
                  fatal("Unmatched high surrogate.");
              }
          }
  
          if (c == '\n') {
              nextCharOnNewLine = true;
          } else {
              if (c == '<') {
                  /* the most common return to parseContent () ... NOP */
              } else if ((((c < 0x0020) && (c != '\t') && (c != '\r')) || (c > 0xFFFD))
                      || ((c >= 0x007f) && (c <= 0x009f) && (c != 0x0085) && (xmlVersion == XML_11))) {
                  fatal("illegal XML character U+" + Integer.toHexString(c));
              } else if ((c >= '\u007F') && (c <= '\u009F')) // 2006-04-25 hsivonen
              {
                  handler.warn("Saw a control character: U+00"
                          + Integer.toHexString(c) + ".");
              }
  
              if (isPrivateUse(c)) {
                  warnAboutPrivateUseChar();
              }
              // If we're in the DTD and in a context where PEs get expanded,
              // do so ... 1/14/2000 errata identify those contexts. There
              // are also spots in the internal subset where PE refs are fatal
              // errors, hence yet another flag.
              else if ((c == '%') && expandPE) {
                  if (peIsError) {
                      fatal("PE reference within decl in internal subset.");
                  }
                  parsePEReference();
                  return readCh();
              }
          }
  
          return c;
      }
  
      /**
       * @throws SAXException
       * @throws IOException
       */
      private void fillBuffer() throws SAXException, IOException {
          while (readBufferPos >= readBufferLength) {
              switch (sourceType) {
                  case INPUT_READER:
                      readDataChunk();
                      while (readBufferLength < 1) {
                          popInput();
                          if (readBufferLength < 1) {
                              readDataChunk();
                          }
                      }
                      break;
  
                  default:
  
                      popInput();
                      break;
              }
          }
      }
  
      /**
       * Push a single character back onto the current input stream.
       * <p>
       * This method usually pushes the character back onto the readBuffer.
       * <p>
       * I don't think that this would ever be called with readBufferPos = 0,
       * because the methods always reads a character before unreading it, but
       * just in case, I've added a boundary condition.
       * 
       * @param c
       *            The character to push back.
       * @see #readCh
       * @see #unread (char[])
       * @see #readBuffer
       */
      private void unread(char c) throws SAXException {
          rollbackLocation();
          if (readBufferPos > 0) {
              readBuffer[--readBufferPos] = c;
          } else {
              pushString(null, Character.valueOf(c).toString());
          }
      }
  
      /**
       * 
       */
      private void rollbackLocation() {
          assert (column != columnPrev) || (line != linePrev);
          nextCharOnNewLine = (column == 1);
          line = linePrev;
          column = columnPrev;
      }
  
      /**
       * Push a char array back onto the current input stream.
       * <p>
       * NOTE: you must <em>never</em> push back characters that you haven't
       * actually read: use pushString () instead.
       * 
       * @see #readCh
       * @see #unread (char)
       * @see #readBuffer
       * @see #pushString
       */
      private void unread(char[] ch, int length) throws SAXException {
          if (length < readBufferPos) {
              readBufferPos -= length;
          } else {
              pushCharArray(null, ch, 0, length);
          }
      }
  
      /**
       * Push, or skip, a new external input source. The source will be some kind
       * of parsed entity, such as a PE (including the external DTD subset) or
       * content for the body.
       * 
       * @param url
       *            The java.net.URL object for the entity.
       * @see SAXDriver#resolveEntity
       * @see #pushString
       * @see #sourceType
       * @see #pushInput
       * @see #detectEncoding
       * @see #sourceType
       * @see #readBuffer
       */
      private void pushURL(boolean isPE, String ename, ExternalIdentifiers ids,
              Reader aReader, InputStream aStream, String aEncoding,
              boolean doResolve) throws SAXException, IOException {
          // removed boolean ignoreEncoding -- 2006-02-03 hsivonen
          String systemId;
          InputSource source;
          InputSource scratch = new InputSource();
  
          if (!isPE) {
              dataBufferFlush();
          }
  
          scratch.setPublicId(ids.publicId);
          scratch.setSystemId(ids.systemId);
  
          // See if we should skip or substitute the entity.
          // If we're not skipping, resolving reports startEntity()
          // and updates the (handler's) stack of URIs.
          if (doResolve) {
              // assert (stream == null && reader == null && encoding == null)
              source = handler.resolveEntity(isPE, ename, scratch, ids.baseUri);
              if (source == null) {
                  handler.skippedEntity(ename);
                  if (isPE) {
                      skippedPE = true;
                  }
                  return;
              }
  
              // we might be using alternate IDs/encoding
              systemId = source.getSystemId();
              // The following warning and setting systemId was deleted bcause
              // the application has the option of not setting systemId
              // provided that it has set the characte/byte stream.
              /*
               * if (systemId == null) { handler.warn ("missing system ID, using " +
               * ids.systemId); systemId = ids.systemId; }
               */
          } else {
              // "[document]", or "[dtd]" via getExternalSubset()
              scratch.setCharacterStream(aReader);
              scratch.setByteStream(aStream);
              scratch.setEncoding(aEncoding);
              source = scratch;
              systemId = ids.systemId;
              if (handler.stringInterning) {
                  handler.startExternalEntity(ename, systemId,
                          "[document]" == ename);
              } else {
                  handler.startExternalEntity(ename, systemId,
                          "[document]".equals(ename));
              }
          }
  
          // Push the existing status.
          pushInput(ename);
  
          // Create a new read buffer.
          // (Note the four-character margin)
          readBuffer = new char[READ_BUFFER_MAX + 4];
          readBufferPos = 0;
          readBufferLength = 0;
          readBufferOverflow = -1;
          is = null;
          reader = null;
          line = 0;
          column = 1;
          linePrev = 0;
          columnPrev = 1;
          nextCharOnNewLine = true;
          currentByteCount = 0;
  
          // If there's an explicit character stream, just
          // ignore encoding declarations.
          if (source.getCharacterStream() != null) {
              sourceType = INPUT_READER;
              this.reader = source.getCharacterStream();
              // swallow UTF-8 BOM -- 2006-02-03 hsivonen
              if ("UTF-8".equalsIgnoreCase(source.getEncoding())) {
                  char bom = readCh();
                  if (bom != '\uFEFF') {
                      unread(bom);
                  }
              }
              tryEncodingDecl(source.getEncoding() == null ? ""
                      : source.getEncoding());
              return;
          }
  
          // Else we handle the conversion, and need to ensure
          // it's done right.
          if (source.getByteStream() != null) {
              is = source.getByteStream();
          } else {
              // Stop -- 2006-11-10 hsivonen
              fatal("The entity resolver didn't properly resolve the entity.");
          }
  
          // If we get to here, there must be
          // an InputStream available.
          if (!is.markSupported()) {
              is = new BufferedInputStream(is);
          }
  
          // Zapped bogus external encoding label code -- 2006-11-10 hsivonen
  
          // if we got an external encoding label, use it ...
          if (source.getEncoding() != null) {
              draconianInputStreamReader(source.getEncoding(), is, false);
              if ("UTF-8".equalsIgnoreCase(source.getEncoding())) {
                  char bom = readCh();
                  if (bom != '\uFEFF') {
                      unread(bom);
                  }
              }
              tryEncodingDecl(source.getEncoding());
              // ... else autodetect from first bytes.
          } else {
              detectEncoding();
              // Read any XML or text declaration.
              String enc = tryEncodingDecl(null);
              if ((enc == null) && ("UTF-32" == characterEncoding)) {
                  fatal("UTF-32 was sniffed from the BOM, but there was no matching encoding declaration. The omission of explicit encoding declaration is only allowed with UTF-8 and UTF-16.");
              }
          }
      }