@Override
    public void endElement(String uri, String localName, String qName)
            throws SAXException {
        if (contentHandler == null) {
            return;
        }
        elementStack.pop();
        if (isWalkOver) {
            contentHandler.endElement(uri, localName, qName);
            return;
        }

        if ("hgroup".equals(localName)) {
            inHgroup = false;
            skipHeading = false;
        } else if (Arrays.binarySearch(H1_TO_H6_ELEMENTS, localName) > -1
                && inHgroup) {
            if (skipHeading) {
                // if skipHeading is true, we're in an hgroup subtree and
                // have reached the end tag of an h1-h6 that is not the
                // first descendant of the hgroup, so it provides a subhead
                currentSection.setIsMasked();
                currentHgroupSection.subheadSections.add(currentSection);
            } else {
                // otherwise, we're in an hgroup subtree & have reached the
                // end tag of the first descendant h1-h6  of the hgroup, so
                // we need make this the currentHgroupSectionto and treat
                // any subsequent h1-h6 as subheads associated with it
                skipHeading = true;
                currentHgroupSection = currentSection;
                currentHgroupSection.subheadSections = new LinkedList<>();
            }
        }

        int depth = currentWalkDepth--;

        if (inHeadingContentOrHiddenElement) {
            // When exiting an element, if that element is the element at the
            // top of the stack
            // Note: The element being exited is a heading content element or an
            // element with a hidden attribute.
            Element topElement = outlineStack.peek();
            assert topElement != null;
            if (topElement.equals(depth, localName)) {
                // Pop that element from the stack.
                outlineStack.pop();
                inHeadingContentOrHiddenElement = false;

                if (currentSection != null) {
                    StringBuilder headingTextBuilder = currentSection.getHeadingTextBuilder();
                    String heading = excerpt(
                            whitespacePattern.matcher(headingTextBuilder).replaceAll(
                                    " ").trim(), MAX_EXCERPT);
                    headingTextBuilder.setLength(0);
                    if (heading.length() == 0) {
                        StringBuilder headingImgAltTextBuilder = currentSection.getHeadingImgAltTextBuilder();
                        heading = excerpt(whitespacePattern.matcher(
                                headingImgAltTextBuilder).replaceAll(
                                        " ").trim(),
                                MAX_EXCERPT);
                        headingImgAltTextBuilder.setLength(0);
                    }
                    if (heading.length() > 0) {
                        headingTextBuilder.append(heading);
                    } else {
                        currentSection.createEmptyHeading();
                    }
                }
            }

            // If the top of the stack is a heading content element or an
            // element with a hidden attribute
            // Do nothing.
            contentHandler.endElement(uri, localName, qName);
            return;
        }

        if (Arrays.binarySearch(SECTIONING_CONTENT_ELEMENTS, localName) > -1) {
            // When exiting a sectioning content element, if the stack is not
            // empty
            if (!outlineStack.isEmpty()) {
                // If the current section has no heading,
                if (currentSection != null && !currentSection.hasHeading()) {
                    // create an implied heading and let that be the heading for
                    // the current section.
                    currentSection.createImpliedHeading();
                }
                Element exitedSectioningContentElement = currentOutlinee;
                assert exitedSectioningContentElement != null;

                // Pop the top element from the stack, and let the current
                // outlinee be that element.
                currentOutlinee = outlineStack.pop();

                // Let current section be the last section in the outline of the
                // current outlinee element.
                currentSection = currentOutlinee.getOutline().peekLast();
                assert currentSection != null;

                // Append the outline of the sectioning content element being
                // exited to the current section.
                // (This does not change which section is the last section in
                // the outline.)
                for (Section section : exitedSectioningContentElement.outline) {
                    section.setParent(currentSection);
                    currentSection.sections.add(section);
                }
            }
        } else if (Arrays.binarySearch(SECTIONING_ROOT_ELEMENTS.toArray(),
                localName) > -1) {
            // When exiting a sectioning root element, if the stack is not empty
            if (!outlineStack.isEmpty()) {
                // Run these steps:

                // If the current section has no heading,
                if (currentSection != null && !currentSection.hasHeading()) {
                    // create an implied heading and let that be the heading for
                    // the current section.
                    currentSection.createImpliedHeading();
                }

                // Pop the top element from the stack, and let the current
                // outlinee be that element.
                currentOutlinee = outlineStack.pop();

                // Let current section be the last section in the outline of the
                // current outlinee element.
                currentSection = currentOutlinee.getOutline().peekLast();

                // Finding the deepest child:
                // If current section has no child sections, stop these steps.
                while (!currentSection.sections.isEmpty())
                    // Let current section be the last child section of the
                    // current current section.
                    currentSection = currentSection.sections.peekLast();
                // Go back to the substep labeled finding the deepest child.
            }
        } else {
            // neither a sectioning content element nor a sectioning root
            // element
            contentHandler.endElement(uri, localName, qName);
            return;
        }

        // When exiting a sectioning content element or a sectioning root
        // element
        // Note: The current outlinee is the element being exited, and
        // it is the sectioning content element or a sectioning root element at
        // the root of the subtree for which an outline is being generated.

        // If the current section has no heading,
        if (currentSection != null && !currentSection.hasHeading()) {
            // create an implied heading and let that be the heading for the
            // current section.
            currentSection.createImpliedHeading();
        }

        // Skip to the next step in the overall set of steps.
        // (The walk is over.)
        // / isWalkOver = true;

        contentHandler.endElement(uri, localName, qName);
    }
@see org.xml.sax.helpers.XMLFilterImpl#endElement(java.lang.String,
     java.lang.String, java.lang.String)
src/nu/validator/servlet/OutlineBuildingXMLReaderWrapper.java