/*
 * Copyright (c) 2005, 2006, 2007 Henri Sivonen
 * Copyright (c) 2007-2019 Mozilla Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package nu.validator.messages;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.Set;

import jakarta.servlet.http.HttpServletRequest;

import nu.validator.checker.InfoAwareErrorHandler;
import nu.validator.checker.NormalizationChecker;
import nu.validator.checker.DatatypeMismatchException;
import nu.validator.checker.VnuBadAttrValueException;
import nu.validator.checker.VnuBadElementNameException;
import nu.validator.datatype.Html5DatatypeException;
import nu.validator.io.DataUri;
import nu.validator.io.SystemIdIOException;
import nu.validator.messages.types.MessageType;
import nu.validator.saxtree.DocumentFragment;
import nu.validator.saxtree.TreeParser;
import nu.validator.servlet.imagereview.Image;
import nu.validator.servlet.imagereview.ImageCollector;
import nu.validator.source.Location;
import nu.validator.source.SourceCode;
import nu.validator.source.SourceHandler;
import nu.validator.spec.EmptySpec;
import nu.validator.spec.Spec;
import nu.validator.spec.html5.Html5AttributeDatatypeBuilder;
import nu.validator.spec.html5.ImageReportAdviceBuilder;
import nu.validator.xml.AttributesImpl;
import nu.validator.xml.CharacterUtil;
import nu.validator.xml.XhtmlSaxEmitter;

import nu.validator.vendor.relaxng.datatype.DatatypeException;

import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import nu.validator.vendor.thaiopensource.relaxng.exceptions.AbstractValidationException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.BadAttributeValueException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.ImpossibleAttributeIgnoredException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.OnlyTextNotAllowedException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.OutOfContextElementException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.RequiredAttributesMissingException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.RequiredAttributesMissingOneOfException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.RequiredElementsMissingException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.RequiredElementsMissingOneOfException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.StringNotAllowedException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.TextNotAllowedException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.UnfinishedElementException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.UnfinishedElementOneOfException;
import nu.validator.vendor.thaiopensource.relaxng.exceptions.UnknownElementException;
import nu.validator.vendor.thaiopensource.xml.util.Name;

import org.apache.log4j.Logger;
import com.ibm.icu.text.Normalizer;

@SuppressWarnings("unchecked")
public class MessageEmitterAdapter implements InfoAwareErrorHandler {

    private static final Logger log4j = Logger.getLogger(MessageEmitterAdapter.class);

    private final static Map<String, char[]> WELL_KNOWN_NAMESPACES = new HashMap<>();

    static {
        WELL_KNOWN_NAMESPACES.put("", "unnamespaced".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/xhtml",
                "XHTML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2000/svg",
                "SVG".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1998/Math/MathML",
                "MathML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2005/Atom",
                "Atom".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/xlink",
                "XLink".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://docbook.org/ns/docbook",
                "DocBook".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://relaxng.org/ns/structure/1.0",
                "RELAX NG".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/XML/1998/namespace",
                "XML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/XSL/Transform",
                "XSLT".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/ns/xbl",
                "XBL2".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
                "XUL".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
                "RDF".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://purl.org/dc/elements/1.1/",
                "Dublin Core".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2001/XMLSchema-instance",
                "XML Schema Instance".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2002/06/xhtml2/",
                "XHTML2".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.ascc.net/xml/schematron",
                "Schematron".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://purl.oclc.org/dsdl/schematron",
                "ISO Schematron".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.inkscape.org/namespaces/inkscape",
                "Inkscape".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
                "Sodipodi".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.openmath.org/OpenMath",
                "OpenMath".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
                "Adobe SVG Viewer 3.0 extension".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/AdobeIllustrator/10.0/",
                "Adobe Illustrator 10.0".toCharArray());
        WELL_KNOWN_NAMESPACES.put("adobe:ns:meta/",
                "XMP Container".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/xap/1.0/",
                "XMP".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/pdf/1.3/",
                "Adobe PDF 1.3".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/tiff/1.0/",
                "Adobe TIFF".toCharArray());
    }

    @SuppressWarnings("rawtypes")
    private final static Map<Class, DocumentFragment> HTML5_DATATYPE_ADVICE = new HashMap<>();

    private final static DocumentFragment IMAGE_REPORT_GENERAL;

    private final static DocumentFragment IMAGE_REPORT_EMPTY;

    private final static DocumentFragment NO_ALT_NO_LINK_ADVICE;

    private final static DocumentFragment NO_ALT_LINK_ADVICE;

    private final static DocumentFragment EMPTY_ALT_ADVICE;

    private final static DocumentFragment HAS_ALT_ADVICE;

    private final static DocumentFragment IMAGE_REPORT_FATAL;

    private static final String SPEC_LINK_URI = System.getProperty(
            "nu.validator.spec.html5-link",
            "https://html.spec.whatwg.org/multipage/");

    private static final long MAX_MESSAGES = Integer.parseInt(System.getProperty(
            "nu.validator.messages.limit", "1000"));

    private static final Map<String, String[]> validInputTypesByAttributeName = new TreeMap<>();

    static {
        validInputTypesByAttributeName.put("accept", new String[] {
                "#attr-input-accept", "file" });
        validInputTypesByAttributeName.put("alt", new String[] {
                "#attr-input-alt", "image" });
        validInputTypesByAttributeName.put("autocomplete", new String[] {
                "#attr-input-autocomplete", "text", "search", "url", "tel",
                "email", "password", "date", "month", "week",
                "time", "datetime-local", "number", "range", "color" });
        validInputTypesByAttributeName.put("autofocus",
                new String[] { "#attr-fe-autofocus" });
        validInputTypesByAttributeName.put("checked", new String[] {
                "#attr-input-checked", "checkbox", "radio" });
        validInputTypesByAttributeName.put("dirname", new String[] {
                "#attr-input-dirname", "text", "search" });
        validInputTypesByAttributeName.put("disabled",
                new String[] { "#attr-fe-disabled" });
        validInputTypesByAttributeName.put("form",
                new String[] { "#attr-fae-form" });
        validInputTypesByAttributeName.put("formaction", new String[] {
                "#attr-fs-formaction", "submit", "image" });
        validInputTypesByAttributeName.put("formenctype", new String[] {
                "#attr-fs-formenctype", "submit", "image" });
        validInputTypesByAttributeName.put("formmethod", new String[] {
                "#attr-fs-formmethod", "submit", "image" });
        validInputTypesByAttributeName.put("formnovalidate", new String[] {
                "#attr-fs-formnovalidate", "submit", "image" });
        validInputTypesByAttributeName.put("formtarget", new String[] {
                "#attr-fs-formtarget", "submit", "image" });
        validInputTypesByAttributeName.put("height", new String[] {
                "#attr-dim-height", "image" });
        validInputTypesByAttributeName.put("list", new String[] {
                "#attr-input-list", "text", "search", "url", "tel", "email",
                "date", "month", "week", "time", "datetime-local",
                "number", "range", "color" });
        validInputTypesByAttributeName.put("max", new String[] {
                "#attr-input-max", "date", "month", "week", "time",
                "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("maxlength", new String[] {
                "#attr-input-maxlength", "text", "search", "url", "tel",
                "email", "password" });
        validInputTypesByAttributeName.put("min", new String[] {
                "#attr-input-min", "date", "month", "week", "time",
                "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("multiple", new String[] {
                "#attr-input-multiple", "email", "file" });
        validInputTypesByAttributeName.put("name",
                new String[] { "#attr-fe-name" });
        validInputTypesByAttributeName.put("pattern", new String[] {
                "#attr-input-pattern", "text", "search", "url", "tel",
                "email", "password" });
        validInputTypesByAttributeName.put("placeholder", new String[] {
                "#attr-input-placeholder", "text", "search", "url", "tel",
                "email", "password", "number" });
        validInputTypesByAttributeName.put("readonly", new String[] {
                "#attr-input-readonly", "text", "search", "url", "tel",
                "email", "password", "date", "month", "week",
                "time", "datetime-local", "number" });
        validInputTypesByAttributeName.put("required",
                new String[] { "#attr-input-required", "text", "search", "url",
                        "tel", "email", "password", "date",
                        "month", "week", "time", "datetime-local", "number",
                        "checkbox", "radio", "file" });
        validInputTypesByAttributeName.put("size", new String[] {
                "#attr-input-size", "text", "search", "url", "tel", "email",
                "password" });
        validInputTypesByAttributeName.put("src", new String[] {
                "#attr-input-src", "image" });
        validInputTypesByAttributeName.put("step", new String[] {
                "#attr-input-step", "date", "month", "week",
                "time", "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("type",
                new String[] { "#attr-input-type" });
        validInputTypesByAttributeName.put("value",
                new String[] { "#attr-input-value" });
        validInputTypesByAttributeName.put("width", new String[] {
                "#attr-dim-width", "image" });
    }

    private static final Map<String, String> fragmentIdByInputType = new TreeMap<>();

    static {
        fragmentIdByInputType.put("hidden", "#hidden-state-(type=hidden)");
        fragmentIdByInputType.put("text",
                "#text-(type=text)-state-and-search-state-(type=search)");
        fragmentIdByInputType.put("search",
                "#text-(type=text)-state-and-search-state-(type=search)");
        fragmentIdByInputType.put("tel", "#telephone-state-(type=tel)");
        fragmentIdByInputType.put("url", "#url-state-(type=url)");
        fragmentIdByInputType.put("email", "#e-mail-state-(type=email)");
        fragmentIdByInputType.put("password", "#password-state-(type=password)");
        fragmentIdByInputType.put("date", "#date-state-(type=date)");
        fragmentIdByInputType.put("month", "#month-state-(type=month)");
        fragmentIdByInputType.put("week", "#week-state-(type=week)");
        fragmentIdByInputType.put("time", "#time-state-(type=time)");
        fragmentIdByInputType.put("datetime-local",
                "#local-date-and-time-state-(type=datetime-local)");
        fragmentIdByInputType.put("number", "#number-state-(type=number)");
        fragmentIdByInputType.put("range", "#range-state-(type=range)");
        fragmentIdByInputType.put("color", "#color-state-(type=color)");
        fragmentIdByInputType.put("checkbox", "#checkbox-state-(type=checkbox)");
        fragmentIdByInputType.put("radio", "#radio-button-state-(type=radio)");
        fragmentIdByInputType.put("file", "#file-upload-state-(type=file)");
        fragmentIdByInputType.put("submit", "#submit-button-state-(type=submit)");
        fragmentIdByInputType.put("image", "#image-button-state-(type=image)");
        fragmentIdByInputType.put("reset", "#reset-button-state-(type=reset)");
        fragmentIdByInputType.put("button", "#button-state-(type=button)");
    }

    static {
        try {
            HTML5_DATATYPE_ADVICE.putAll(Html5AttributeDatatypeBuilder.parseSyntaxDescriptions());
            List<DocumentFragment> list = ImageReportAdviceBuilder.parseAltAdvice();
            IMAGE_REPORT_GENERAL = list.get(0);
            NO_ALT_NO_LINK_ADVICE = list.get(1);
            NO_ALT_LINK_ADVICE = list.get(2);
            EMPTY_ALT_ADVICE = list.get(3);
            HAS_ALT_ADVICE = list.get(4);
            IMAGE_REPORT_EMPTY = list.get(5);
            IMAGE_REPORT_FATAL = list.get(6);
        } catch (IOException | SAXException e) {
            throw new RuntimeException(e);
        }
    }

    private final static char[] INDETERMINATE_MESSAGE = "The result cannot be determined due to a non-document-error.".toCharArray();

    private final static char[] ELEMENT_SPECIFIC_ATTRIBUTES_BEFORE = "Attributes for element ".toCharArray();

    private final static char[] ELEMENT_SPECIFIC_ATTRIBUTES_AFTER = ":".toCharArray();

    private final static char[] CONTENT_MODEL_BEFORE = "Content model for element ".toCharArray();

    private final static char[] CONTENT_MODEL_AFTER = ":".toCharArray();

    private final static char[] CONTEXT_BEFORE = "Contexts in which element ".toCharArray();

    private final static char[] CONTEXT_AFTER = " may be used:".toCharArray();

    private final static char[] BAD_VALUE = "Bad value ".toCharArray();

    private final static char[] POTENTIALLY_BAD_VALUE = "Potentially bad value ".toCharArray();

    private final static char[] BAD_ELEMENT_NAME = "Bad element name".toCharArray();

    private final static char[] POTENTIALLY_BAD_ELEMENT_NAME = "Potentially bad element name".toCharArray();

    private final static char[] FOR = " for ".toCharArray();

    private final static char[] ATTRIBUTE = "attribute ".toCharArray();

    private final static char[] FROM_NAMESPACE = " from namespace ".toCharArray();

    private final static char[] SPACE = " ".toCharArray();

    private final static char[] ON = " on ".toCharArray();

    private final static char[] ELEMENT = "element ".toCharArray();

    private final static char[] PERIOD = ".".toCharArray();

    private final static char[] COMMA = ", ".toCharArray();

    private final static char[] COLON = ":".toCharArray();

    private final static char[] NOT_ALLOWED_ON = " not allowed on ".toCharArray();

    private final static char[] AT_THIS_POINT = " at this point.".toCharArray();

    private final static char[] ONLY_TEXT = " is not allowed to have content that consists solely of text.".toCharArray();

    private final static char[] NOT_ALLOWED = " not allowed".toCharArray();

    private final static char[] AS_CHILD_OF = " as child of ".toCharArray();

    private final static char[] IN_THIS_CONTEXT_SUPPRESSING = " in this context. (Suppressing further errors from this subtree.)".toCharArray();

    private final static char[] REQUIRED_ATTRIBUTES_MISSING = " is missing required attribute ".toCharArray();

    private final static char[] REQUIRED_ATTRIBUTES_MISSING_ONE_OF = " is missing one or more of the following attributes: ".toCharArray();

    private final static char[] REQUIRED_ELEMENTS_MISSING = "Required elements missing.".toCharArray();

    private final static char[] IS_MISSING_A_REQUIRED_CHILD = " is missing a required child element".toCharArray();

    private final static char[] REQUIRED_CHILDREN_MISSING_FROM = " is missing a required instance of child element ".toCharArray();

    private final static char[] REQUIRED_CHILDREN_MISSING_ONE_OF_FROM = " is missing a required instance of one or more of the following child elements: ".toCharArray();

    private final static char[] BAD_CHARACTER_CONTENT = "Bad character content ".toCharArray();

    private final static char[] IN_THIS_CONTEXT = " in this context.".toCharArray();

    /**
     * Elements with transparent content models per HTML spec.
     * These elements inherit content restrictions from their ancestors.
     */
    private static final Set<String> TRANSPARENT_ELEMENTS = Set.of(
            "a", "ins", "del", "object", "video", "audio", "canvas", "map",
            "slot");

    private final static char[] TEXT_NOT_ALLOWED_IN = "Text not allowed in ".toCharArray();

    private final static char[] UNKNOWN = "Unknown ".toCharArray();

    private static final char[] NO_ALT_NO_LINK_HEADING = "No textual alternative available, not linked".toCharArray();

    private static final char[] NO_ALT_LINK_HEADING = "No textual alternative available, image linked".toCharArray();

    private static final char[] EMPTY_ALT = "Empty textual alternative\u2014Omitted from non-graphical presentation".toCharArray();

    private static final char[] HAS_ALT = "Images with textual alternative".toCharArray();

    private static final String[] DEFAULT_FILTER_STRINGS = { //
            // Salvation messages that are a little bit ahead of their time yet
            ".*Authors who wish to regulate nested browsing contexts.*", //
            ".*\\s+“report-uri”\\s+in\\s+favour\\s+of\\s+a\\s+new\\s+“report-to”.*", //
            ".*is only used for backwards compatibility with older CSP.*", //
            // (non-)errors for features the CSS checker doesn’t support yet
            ".*Property “scale” doesn't exist.*", //
    };

    protected static final Pattern DEFAULT_FILTER_PATTERN = Pattern.compile(
            String.join("|", DEFAULT_FILTER_STRINGS));

    protected static final Pattern FILE_NOT_CHECKED = Pattern.compile(
            ".*File was not checked.*");

    private final AttributesImpl attributesImpl = new AttributesImpl();

    private final char[] oneChar = { '\u0000' };

    private int warnings = 0;

    private int errors = 0;

    private int fatalErrors = 0;

    private final boolean batchMode;

    private int nonDocumentErrors = 0;

    private final Pattern filterPattern;

    private final SourceCode sourceCode;

    private final MessageEmitter emitter;

    private final ExactErrorHandler exactErrorHandler;

    private final boolean showSource;

    private final ImageCollector imageCollector;

    private final HttpServletRequest request;

    private int lineOffset;

    private Spec spec = EmptySpec.THE_INSTANCE;

    private boolean html = false;

    private boolean loggingOk = false;

    private boolean errorsOnly = false;

    private boolean skipInfoMessages = false;

    @SuppressWarnings("deprecation")
    protected static String scrub(String s) throws SAXException {
        if (s == null) {
            return null;
        }
        s = CharacterUtil.prudentlyScrubCharacterData(s);
        if (NormalizationChecker.startsWithComposingChar(s)) {
            s = " " + s;
        }
        return Normalizer.normalize(s, Normalizer.NFC, 0);
    }

    private StringBuilder zapLf(StringBuilder builder) {
        int len = builder.length();
        for (int i = 0; i < len; i++) {
            char c = builder.charAt(i);
            if (c == '\n' || c == '\r') {
                builder.setCharAt(i, ' ');
            }
        }
        return builder;
    }

    private void throwIfTooManyMessages() throws SAXException {
        if (!batchMode && (warnings + errors > MAX_MESSAGES)) {
            throw new TooManyErrorsException("Too many messages.");
        }
    }

    public MessageEmitterAdapter(HttpServletRequest request, Pattern
            filterPattern, SourceCode sourceCode, boolean showSource,
            ImageCollector imageCollector, int lineOffset, boolean batchMode,
            MessageEmitter messageEmitter) {
        super();
        this.filterPattern = filterPattern;
        this.sourceCode = sourceCode;
        this.emitter = messageEmitter;
        this.exactErrorHandler = new ExactErrorHandler(this);
        this.showSource = showSource;
        this.lineOffset = lineOffset;
        this.batchMode = batchMode;
        this.imageCollector = imageCollector;
        this.request = request;
    }

    public MessageEmitterAdapter(Pattern filterPattern, SourceCode sourceCode,
            boolean showSource, ImageCollector imageCollector, int lineOffset,
            boolean batchMode, MessageEmitter messageEmitter) {
        super();
        this.filterPattern = filterPattern;
        this.sourceCode = sourceCode;
        this.emitter = messageEmitter;
        this.exactErrorHandler = new ExactErrorHandler(this);
        this.showSource = showSource;
        this.lineOffset = lineOffset;
        this.batchMode = batchMode;
        this.imageCollector = imageCollector;
        this.request = null;
    }

    /**
     * For nu.validator.client.TestRunner
     */
    public MessageEmitterAdapter() {
        super();
        this.filterPattern = null;
        this.sourceCode = null;
        this.emitter = null;
        this.exactErrorHandler = null;
        this.showSource = false;
        this.lineOffset = 0;
        this.batchMode = false;
        this.imageCollector = null;
        this.request = null;
    }

    /**
     * @return Returns the errors.
     */
    public int getErrors() {
        return errors;
    }

    /**
     * @return Returns the fatalErrors.
     */
    public int getFatalErrors() {
        return fatalErrors;
    }

    /**
     * @return Returns the warnings.
     */
    public int getWarnings() {
        return warnings;
    }

    private boolean isErrors() {
        return !(errors == 0 && fatalErrors == 0);
    }

    /**
     * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
     */
    @Override
    public void warning(SAXParseException e) throws SAXException {
        warning(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void warning(SAXParseException e, boolean exact)
            throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        if (e.getMessage().contains("cannot be represented as XML 1.0")
                || e.getMessage().contains(
                        "is not serializable as XML 1.0")) {
            return;
        }
        if (!FILE_NOT_CHECKED.matcher(e.getMessage()).matches()) {
            this.warnings++;
        }
        throwIfTooManyMessages();
        messageFromSAXParseException(MessageType.WARNING, e, exact, null);
    }

    /**
     * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
     */
    @Override
    public void error(SAXParseException e) throws SAXException {
        error(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void error(SAXParseException e, boolean exact) throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        Map<String, DatatypeException> datatypeErrors = null;
        if (e instanceof BadAttributeValueException) {
            datatypeErrors = ((BadAttributeValueException) e).getExceptions();
        }
        if (e instanceof VnuBadAttrValueException) {
            datatypeErrors = ((VnuBadAttrValueException) e).getExceptions();
        }
        if (e instanceof VnuBadElementNameException) {
            datatypeErrors = ((VnuBadElementNameException) e).getExceptions();
        }
        if (e instanceof DatatypeMismatchException) {
            datatypeErrors = ((DatatypeMismatchException) e).getExceptions();
        }
        if (datatypeErrors != null) {
            for (Map.Entry<String, DatatypeException> entry : datatypeErrors.entrySet()) {
                DatatypeException dex = entry.getValue();
                if (dex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                    if (ex5.isWarning()) {
                        String message = ex5.getMessage();
                        if (message != null && message.contains("Typo for")) {
                            messageFromSAXParseException(MessageType.INFO, e,
                                    exact, null);
                            return;
                        }
                        this.warnings++;
                        throwIfTooManyMessages();
                        messageFromSAXParseException(MessageType.WARNING, e,
                                exact, null);
                        return;
                    }
                }
            }
        }
        this.errors++;
        throwIfTooManyMessages();
        messageFromSAXParseException(MessageType.ERROR, e, exact, null);
    }

    public void errorWithStart(SAXParseException e, int[] start)
            throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        this.errors++;
        throwIfTooManyMessages();
        int startLine = start[0];
        int startColumn = start[1];
        int lastLine = e.getLineNumber();
        int lastColumn = e.getColumnNumber();
        boolean exact = (startLine == lastLine && startColumn == lastColumn);
        messageFromSAXParseException(MessageType.ERROR, e, exact, start);
    }

    /**
     * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
     */
    @Override
    public void fatalError(SAXParseException e) throws SAXException {
        fatalError(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void fatalError(SAXParseException e, boolean exact)
            throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        this.fatalErrors++;
        Exception wrapped = e.getException();
        String systemId = null;
        if (wrapped instanceof SystemIdIOException) {
            SystemIdIOException siie = (SystemIdIOException) wrapped;
            systemId = siie.getSystemId();
        }
        if (wrapped instanceof IOException) {
            message(MessageType.IO, wrapped, systemId, -1, -1, false, null);
        } else {
            messageFromSAXParseException(MessageType.FATAL, e, exact, null);
        }
    }

    @Override
    public void info(SAXParseException e) throws SAXException {
        info(e, false);
    }

    /**
     * Convenience method for emitting info messages with just a string.
     * This creates a SAXParseException without location information.
     * For messages with location info, use the info(SAXParseException) method
     * from InfoAwareErrorHandler interface.
     *
     * @param str the info message
     * @throws SAXException if something goes wrong
     */
    public void info(String str) throws SAXException {
        message(MessageType.INFO, new Exception(str), null, -1, -1, false,
                null);
    }

    private void info(SAXParseException e, boolean exact) throws SAXException {
        if (emitter == null) {
            return;
        }
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        throwIfTooManyMessages();
        messageFromSAXParseException(MessageType.INFO, e, exact, null);
    }

    public void ioError(IOException e) throws SAXException {
        this.nonDocumentErrors++;
        String systemId = null;
        if (e instanceof SystemIdIOException) {
            SystemIdIOException siie = (SystemIdIOException) e;
            systemId = siie.getSystemId();
        }
        message(MessageType.IO, e, systemId, -1, -1, false, null);
    }

    public void internalError(Throwable e, String message) throws SAXException {
        this.nonDocumentErrors++;
        message(MessageType.INTERNAL, new Exception(message), null, -1, -1,
                false, null);
    }

    public void schemaError(Exception e) throws SAXException {
        this.nonDocumentErrors++;
        message(MessageType.SCHEMA, e, null, -1, -1, false, null);
    }

    public void start(String documentUri) throws SAXException {
        emitter.startMessages(scrub(shortenDataUri(documentUri)), showSource);
    }

    private String shortenDataUri(String uri) {
        if (DataUri.startsWithData(uri)) {
            return "data:\u2026";
        } else {
            return uri;
        }
    }

    public void end(String successMessage, String failureMessage,
            String language) throws SAXException {
        ResultHandler resultHandler = emitter.startResult();
        if (resultHandler != null) {
            if (isIndeterminate()) {
                resultHandler.startResult(Result.INDETERMINATE);
                resultHandler.characters(INDETERMINATE_MESSAGE, 0,
                        INDETERMINATE_MESSAGE.length);
                resultHandler.endResult();
            } else if (isErrors()) {
                resultHandler.startResult(Result.FAILURE);
                resultHandler.characters(failureMessage.toCharArray(), 0,
                        failureMessage.length());
                resultHandler.endResult();
            } else {
                resultHandler.startResult(Result.SUCCESS);
                resultHandler.characters(successMessage.toCharArray(), 0,
                        successMessage.length());
                resultHandler.endResult();
            }
        }
        emitter.endResult();

        if (imageCollector != null) {
            DocumentFragment instruction = IMAGE_REPORT_GENERAL;
            boolean fatal = false;
            if (getFatalErrors() > 0) {
                fatal = true;
                instruction = IMAGE_REPORT_FATAL;
            } else if (imageCollector.isEmpty()) {
                instruction = IMAGE_REPORT_EMPTY;
            }

            ImageReviewHandler imageReviewHandler = emitter.startImageReview(
                    instruction, fatal);
            if (imageReviewHandler != null && !fatal) {
                emitImageReview(imageReviewHandler);
            }
            emitter.endImageReview();
        }

        if (showSource) {
            SourceHandler sourceHandler = emitter.startFullSource(lineOffset);
            if (sourceHandler != null) {
                sourceCode.emitSource(sourceHandler);
            }
            emitter.endFullSource();
        }
        emitter.endMessages(language);
    }

    private void emitImageReview(ImageReviewHandler imageReviewHandler)
            throws SAXException {
        List<Image> noAltNoLink = new LinkedList<>();
        List<Image> noAltLink = new LinkedList<>();
        List<Image> emptyAlt = new LinkedList<>();
        List<Image> hasAlt = new LinkedList<>();

        for (Image image : imageCollector) {
            String alt = image.getAlt();
            if (alt == null) {
                if (image.isLinked()) {
                    noAltLink.add(image);
                } else {
                    noAltNoLink.add(image);
                }
            } else if ("".equals(alt)) {
                emptyAlt.add(image);
            } else {
                hasAlt.add(image);
            }
        }

        emitImageList(imageReviewHandler, noAltLink, NO_ALT_LINK_HEADING,
                NO_ALT_LINK_ADVICE, false);
        emitImageList(imageReviewHandler, noAltNoLink, NO_ALT_NO_LINK_HEADING,
                NO_ALT_NO_LINK_ADVICE, false);
        emitImageList(imageReviewHandler, emptyAlt, EMPTY_ALT,
                EMPTY_ALT_ADVICE, false);
        emitImageList(imageReviewHandler, hasAlt, HAS_ALT, HAS_ALT_ADVICE, true);
    }

    private void emitImageList(ImageReviewHandler imageReviewHandler,
            List<Image> list, char[] heading, DocumentFragment instruction,
            boolean hasAlt) throws SAXException {
        if (!list.isEmpty()) {
            imageReviewHandler.startImageGroup(heading, instruction, hasAlt);
            for (Image image : list) {
                String systemId = image.getSystemId();
                int oneBasedLine = image.getLineNumber();
                int oneBasedColumn = image.getColumnNumber();
                Location rangeLast = sourceCode.newLocatorLocation(
                        oneBasedLine, oneBasedColumn);
                if (sourceCode.isWithinKnownSource(rangeLast)) {
                    Location rangeStart = sourceCode.rangeStartForRangeLast(rangeLast);
                    imageReviewHandler.image(image, hasAlt, systemId,
                            rangeStart.getLine() + 1,
                            rangeStart.getColumn() + 1, oneBasedLine,
                            oneBasedColumn);
                } else {
                    imageReviewHandler.image(image, hasAlt, systemId, -1, -1,
                            -1, -1);
                }
            }
            imageReviewHandler.endImageGroup();
        }
    }

    private boolean isIndeterminate() {
        return nonDocumentErrors > 0;
    }

    /**
     * Checks if an exception represents a role=directory error, which should
     * be treated as a warning (deprecated but not invalid).
     */
    protected static boolean isRoleDirectoryWarning(Exception e) {
        if (e instanceof BadAttributeValueException) {
            BadAttributeValueException ex = (BadAttributeValueException) e;
            return "directory".equals(ex.getAttributeValue())
                    && "role".equals(ex.getAttributeName().getLocalName());
        }
        return false;
    }

    protected String getDisplayMessage(Exception message) {
        if (message instanceof AbstractValidationException) {
            return getDisplayMessageForRng(
                    (AbstractValidationException) message);
        } else if (message instanceof VnuBadAttrValueException) {
            VnuBadAttrValueException e = (VnuBadAttrValueException) message;
            return "Bad value “" + e.getAttributeValue()
                    + "” for attribute “"
                    + e.getAttributeName().getLocalName()
                    + "” on element “"
                    + e.getCurrentElement().getLocalName() + "”.";
        } else if (message instanceof VnuBadElementNameException) {
            VnuBadElementNameException e = (VnuBadElementNameException) message;
            return "Element “" + e.getElementName()
                    + "” not allowed.";
        }
        return null;
    }

    private String getDisplayMessageForRng(AbstractValidationException e) {
        StringBuilder sb = new StringBuilder();
        if (e instanceof BadAttributeValueException) {
            BadAttributeValueException ex = (BadAttributeValueException) e;
            sb.append("Bad value “").append(ex.getAttributeValue()).append(
                    "” for attribute “").append(
                            ex.getAttributeName().getLocalName()).append(
                                    "” on element “").append(
                                            ex.getCurrentElement()
                                            .getLocalName()).append("”.");
        } else if (e instanceof ImpossibleAttributeIgnoredException) {
            ImpossibleAttributeIgnoredException ex =
                (ImpossibleAttributeIgnoredException) e;
            sb.append("Attribute “").append(
                    ex.getAttributeName().getLocalName()).append(
                    "” not allowed on element “").append(
                            ex.getCurrentElement().getLocalName()).append(
                                    "” at this point.");
        } else if (e instanceof OnlyTextNotAllowedException) {
            OnlyTextNotAllowedException ex = (OnlyTextNotAllowedException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” is not allowed to have content that"
                            + " consists solely of text.");
        } else if (e instanceof OutOfContextElementException) {
            OutOfContextElementException ex = (OutOfContextElementException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” not allowed");
            String parentName = null;
            if (ex.getParent() != null) {
                parentName = ex.getParent().getLocalName();
                sb.append(" as child of “").append(parentName).append(
                        "”");
            }
            sb.append(" in this context.");
            if (parentName != null
                    && TRANSPARENT_ELEMENTS.contains(parentName)) {
                sb.append(" Note: The “").append(parentName).append(
                        "” element has a transparent content model;")
                        .append(" its allowed content is inherited from its")
                        .append(" parent element.");
            }
        } else if (e instanceof RequiredAttributesMissingOneOfException) {
            RequiredAttributesMissingOneOfException ex =
                (RequiredAttributesMissingOneOfException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” is missing one or more of the following"
                            + " attributes: ");
            for (Iterator<String> iter =
                    ex.getAttributeLocalNames().iterator(); iter.hasNext();) {
                sb.append("“").append(iter.next()).append("”");
                if (iter.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append(".");
        } else if (e instanceof RequiredAttributesMissingException) {
            RequiredAttributesMissingException ex =
                (RequiredAttributesMissingException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” is missing required attribute “")
                    .append(ex.getAttributeLocalName()).append("”.");
        } else if (e instanceof RequiredElementsMissingException) {
            RequiredElementsMissingException ex =
                (RequiredElementsMissingException) e;
            if (ex.getParent() == null) {
                sb.append("Required elements missing.");
            } else {
                sb.append("Element “").append(
                        ex.getParent().getLocalName()).append("”");
                if (ex.getMissingElementName() == null) {
                    sb.append(" is missing a required child element");
                } else {
                    sb.append(
                            " is missing a required instance of child element"
                            + " “").append(ex.getMissingElementName())
                        .append("”");
                }
                sb.append(".");
            }
        } else if (e instanceof TextNotAllowedException) {
            TextNotAllowedException ex = (TextNotAllowedException) e;
            sb.append("Text not allowed in “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” in this context.");
        } else if (e instanceof UnfinishedElementException) {
            UnfinishedElementException ex = (UnfinishedElementException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append("”");
            if (ex.getMissingElementName() == null) {
                sb.append(" is missing a required child element");
            } else {
                sb.append(
                        " is missing a required instance of child element"
                        + " “").append(
                                ex.getMissingElementName()).append("”");
            }
            sb.append(".");
        } else if (e instanceof UnfinishedElementOneOfException) {
            UnfinishedElementOneOfException ex =
                (UnfinishedElementOneOfException) e;
            sb.append("Element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” is missing a required instance of one"
                            + " or more of the following child elements: ");
            for (Iterator<String> iter =
                    ex.getMissingElementNames().iterator(); iter.hasNext();) {
                sb.append("“").append(iter.next()).append("”");
                if (iter.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append(".");
        } else if (e instanceof RequiredElementsMissingOneOfException) {
            RequiredElementsMissingOneOfException ex =
                (RequiredElementsMissingOneOfException) e;
            sb.append("Element “").append(
                    ex.getParent().getLocalName()).append(
                            "” is missing a required instance of one or"
                            + " more of the following child elements: ");
            for (Iterator<String> iter =
                    ex.getMissingElementNames().iterator(); iter.hasNext();) {
                sb.append("“").append(iter.next()).append("”");
                if (iter.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append(".");
        } else if (e instanceof UnknownElementException) {
            UnknownElementException ex = (UnknownElementException) e;
            sb.append("Unknown element “").append(
                    ex.getCurrentElement().getLocalName()).append(
                            "” not allowed");
            if (ex.getParent() != null) {
                sb.append(" as child of “").append(
                        ex.getParent().getLocalName()).append("”");
            }
            sb.append(".");
        } else if (e instanceof StringNotAllowedException) {
            StringNotAllowedException ex = (StringNotAllowedException) e;
            sb.append("Bad character content “").append(
                    ex.getValue()).append("” for element “").append(
                            ex.getCurrentElement().getLocalName()).append(
                                    "”.");
        } else {
            return null;
        }
        return sb.toString();
    }

    private void messageFromSAXParseException(MessageType type,
            SAXParseException spe, boolean exact, int[] start)
            throws SAXException {
        int lineNumber = spe.getLineNumber() == 0 ? -1 : spe.getLineNumber();
        message(type, spe, spe.getSystemId(), lineNumber, spe.getColumnNumber(),
                exact, start);
    }

    private void message(MessageType type, Exception message, String systemId,
            int oneBasedLine, int oneBasedColumn, boolean exact, int[] start)
            throws SAXException {
        if (skipInfoMessages && type == MessageType.INFO)
            return;
        // Convert role=directory errors to warnings before filtering
        if (type == MessageType.ERROR && isRoleDirectoryWarning(message)) {
            if (this.errors > 0) {
                this.errors--;
            }
            this.warnings++;
            message(MessageType.WARNING, message, systemId, oneBasedLine,
                    oneBasedColumn, exact, start);
            return;
        }
        // Use display message for filtering (what users see in output).
        // Fall back to internal message for exception types not handled by
        // getDisplayMessage().
        String msg = getDisplayMessage(message);
        if (msg == null) {
            msg = message.getMessage();
        }
        if (msg != null && ((filterPattern != null
                && filterPattern.matcher(msg).matches())
                || DEFAULT_FILTER_PATTERN.matcher(msg).matches())) {
            if (type.getSuperType() == "error" && this.errors > 0) {
                this.errors--;
            } else if (type.getSubType() == "warning" && this.warnings > 0) {
                this.warnings--;
            }
            return;
        }
        if (msg != null && msg.contains(
                "Trailing slash on void elements has no effect")) {
            if (type != MessageType.INFO) {
                message(MessageType.INFO, message, systemId, oneBasedLine,
                        oneBasedColumn, exact, start);
                return;
            }
            if (request != null) {
                request.setAttribute(
                        "http://validator.nu/properties/self-closing-tag-found",
                        true);
            }
        }
        if (loggingOk
                && (type.getSuperType() == "error")
                && spec != EmptySpec.THE_INSTANCE
                && systemId != null
                && msg != null
                && (systemId.startsWith("http:") || systemId.startsWith("https:"))) {
            log4j.info(zapLf(new StringBuilder() //
                    .append(systemId).append('\t').append(msg)));
        }
        if (errorsOnly && type.getSuperType() == "info") {
            return;
        }
        String uri = sourceCode != null ? sourceCode.getUri() : null;
        if (oneBasedLine > -1
                && (uri == systemId || (uri != null && uri.equals(systemId)))) {
            if (oneBasedColumn > -1) {
                if (exact) {
                    messageWithExact(type, message, systemId, oneBasedLine,
                            oneBasedColumn, start);
                } else {
                    messageWithRange(type, message, systemId, oneBasedLine,
                            oneBasedColumn, start);
                }
            } else {
                messageWithLine(type, message, systemId, oneBasedLine);
            }
        } else {
            messageWithoutExtract(type, message, systemId, oneBasedLine,
                    oneBasedColumn);
        }
    }

    private void messageWithRange(MessageType type, Exception message,
            String systemId, int oneBasedLine, int oneBasedColumn, int[] start)
            throws SAXException {
        if (sourceCode == null) {
            messageWithoutExtract(type, message, systemId, oneBasedLine,
                    oneBasedColumn);
            return;
        }
        if (start != null && !sourceCode.getIsCss()) {
            oneBasedColumn = oneBasedColumn + start[2];
        }
        systemId = batchMode ? systemId : null;
        Location rangeLast = sourceCode.newLocatorLocation(oneBasedLine,
                oneBasedColumn);
        if (!sourceCode.isWithinKnownSource(rangeLast)) {
            messageWithoutExtract(type, message, null, oneBasedLine,
                    oneBasedColumn);
            return;
        }
        Location rangeStart = sourceCode.rangeStartForRangeLast(rangeLast);
        if (start != null) {
            if (sourceCode.getIsCss()) {
                rangeStart = sourceCode.newLocatorLocation(start[0], start[1]);
            } else {
                rangeStart = sourceCode.newLocatorLocation(start[0],
                        start[1] + start[2]);
            }
        }
        startMessage(type, scrub(shortenDataUri(systemId)),
                rangeStart.getLine() + 1, rangeStart.getColumn() + 1,
                oneBasedLine, oneBasedColumn, false);
        messageText(message);
        SourceHandler sourceHandler = emitter.startSource();
        if (sourceHandler != null) {
            if (start != null) {
                sourceCode.addLocatorLocation(rangeStart.getLine() + 1,
                        rangeStart.getColumn());
            }
            sourceCode.rangeEndError(rangeStart, rangeLast, sourceHandler);
        }
        emitter.endSource();
        elaboration(message);
        endMessage();
    }

    private void messageWithExact(MessageType type, Exception message,
            String systemId, int oneBasedLine, int oneBasedColumn, int[] start)
            throws SAXException {
        if (sourceCode == null) {
            messageWithoutExtract(type, message, systemId, oneBasedLine,
                    oneBasedColumn);
            return;
        }
        if (start != null && !sourceCode.getIsCss()) {
            oneBasedColumn = oneBasedColumn + start[2];
        }
        systemId = batchMode ? systemId : null;
        startMessage(type, scrub(shortenDataUri(systemId)), oneBasedLine,
                oneBasedColumn, oneBasedLine, oneBasedColumn, true);
        messageText(message);
        Location location = sourceCode.newLocatorLocation(oneBasedLine,
                oneBasedColumn);
        if (sourceCode.isWithinKnownSource(location)) {
            SourceHandler sourceHandler = emitter.startSource();
            if (sourceHandler != null) {
                sourceCode.exactError(location, sourceHandler);
            }
            emitter.endSource();
        } else {
            sourceCode.rememberExactError(location);
        }
        elaboration(message);
        endMessage();
    }

    private void messageWithLine(MessageType type, Exception message,
            String systemId, int oneBasedLine) throws SAXException {
        if (sourceCode == null) {
            messageWithoutExtract(type, message, systemId, oneBasedLine, -1);
            return;
        }
        systemId = batchMode ? systemId : null;
        if (!sourceCode.isWithinKnownSource(oneBasedLine)) {
            throw new RuntimeException("Bug. Line out of range!");
        }
        startMessage(type, scrub(shortenDataUri(systemId)), oneBasedLine, -1,
                oneBasedLine, -1, false);
        messageText(message);
        SourceHandler sourceHandler = emitter.startSource();
        if (sourceHandler != null) {
            sourceCode.lineError(oneBasedLine, sourceHandler);
        }
        emitter.endSource();
        elaboration(message);
        endMessage();
    }

    private void messageWithoutExtract(MessageType type, Exception message,
            String systemId, int oneBasedLine, int oneBasedColumn)
            throws SAXException {
        if (systemId == null && sourceCode != null) {
            systemId = sourceCode.getUri();
        }
        startMessage(type, scrub(shortenDataUri(systemId)), oneBasedLine,
                oneBasedColumn, oneBasedLine, oneBasedColumn, false);
        messageText(message);
        elaboration(message);
        endMessage();
    }

    /**
     * @param message
     * @throws SAXException
     */
    private void messageText(Exception message) throws SAXException {
        if (message instanceof AbstractValidationException) {
            AbstractValidationException ave = (AbstractValidationException) message;
            rngMessageText(ave);
        } else if (message instanceof VnuBadAttrValueException) {
            VnuBadAttrValueException e = (VnuBadAttrValueException) message;
            vnuBadAttrValueMessageText(e);
        } else if (message instanceof VnuBadElementNameException) {
            VnuBadElementNameException e = (VnuBadElementNameException) message;
            vnuElementNameMessageText(e);
        } else {
            String msg = message.getMessage();
            if (msg == null && message.getCause() != null) {
                msg = message.getCause().getMessage();
            }
            if (msg != null) {
                MessageTextHandler messageTextHandler = emitter.startText();
                if (messageTextHandler != null) {
                    emitStringWithQurlyQuotes(messageTextHandler, msg);
                }
                emitter.endText();
            }
        }
    }

    private void vnuBadAttrValueMessageText(VnuBadAttrValueException e)
            throws SAXException {
        MessageTextHandler messageTextHandler = emitter.startText();
        if (messageTextHandler != null) {
            boolean isWarning = false;
            Map<String, DatatypeException> datatypeErrors = e.getExceptions();
            for (Map.Entry<String, DatatypeException> entry : datatypeErrors.entrySet()) {
                DatatypeException dex = entry.getValue();
                if (dex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                    if (ex5.isWarning()) {
                        isWarning = true;
                    }
                }
            }
            if (isWarning) {
                messageTextString(messageTextHandler, POTENTIALLY_BAD_VALUE,
                        false);
            } else {
                messageTextString(messageTextHandler, BAD_VALUE, false);
            }
            if (e.getAttributeValue().length() < 200) {
                codeString(messageTextHandler, e.getAttributeValue());
            }
            messageTextString(messageTextHandler, FOR, false);
            attribute(messageTextHandler, e.getAttributeName(),
                    e.getCurrentElement(), false);
            messageTextString(messageTextHandler, ON, false);
            element(messageTextHandler, e.getCurrentElement(), false);
            emitDatatypeErrors(messageTextHandler, e.getExceptions());
        }
        emitter.endText();
    }

    private void vnuElementNameMessageText(VnuBadElementNameException e)
            throws SAXException {
        MessageTextHandler messageTextHandler = emitter.startText();
        if (messageTextHandler != null) {
            boolean isWarning = false;
            Map<String, DatatypeException> datatypeErrors = e.getExceptions();
            for (Map.Entry<String, DatatypeException> entry : datatypeErrors.entrySet()) {
                DatatypeException dex = entry.getValue();
                if (dex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                    if (ex5.isWarning()) {
                        isWarning = true;
                    }
                }
            }
            if (isWarning) {
                messageTextString(messageTextHandler,
                        POTENTIALLY_BAD_ELEMENT_NAME, false);
            } else {
                messageTextString(messageTextHandler, BAD_ELEMENT_NAME, false);
            }
            messageTextString(messageTextHandler, SPACE, false);
            codeString(messageTextHandler, e.getElementName());
            emitDatatypeErrors(messageTextHandler, e.getExceptions());
        }
        emitter.endText();
    }

    private void rngMessageText(
            AbstractValidationException e) throws SAXException {
        MessageTextHandler messageTextHandler = emitter.startText();
        if (messageTextHandler != null) {
            if (e instanceof BadAttributeValueException) {
                BadAttributeValueException ex = (BadAttributeValueException) e;
                boolean isWarning = false;
                Map<String, DatatypeException> datatypeErrors = ex.getExceptions();
                for (Map.Entry<String, DatatypeException> entry : datatypeErrors.entrySet()) {
                    DatatypeException dex = entry.getValue();
                    if (dex instanceof Html5DatatypeException) {
                        Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                        if (ex5.isWarning()) {
                            isWarning = true;
                        }
                    }
                }
                if (isWarning) {
                    messageTextString(messageTextHandler,
                            POTENTIALLY_BAD_VALUE, false);
                } else {
                    messageTextString(messageTextHandler, BAD_VALUE, false);
                }
                if (ex.getAttributeValue().length() < 200) {
                    codeString(messageTextHandler, ex.getAttributeValue());
                }
                messageTextString(messageTextHandler, FOR, false);
                attribute(messageTextHandler, ex.getAttributeName(),
                        ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, ON, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                emitDatatypeErrors(messageTextHandler, ex.getExceptions());
            } else if (e instanceof ImpossibleAttributeIgnoredException) {
                ImpossibleAttributeIgnoredException ex = (ImpossibleAttributeIgnoredException) e;
                attribute(messageTextHandler, ex.getAttributeName(),
                        ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, NOT_ALLOWED_ON, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, AT_THIS_POINT, false);
            } else if (e instanceof OnlyTextNotAllowedException) {
                OnlyTextNotAllowedException ex = (OnlyTextNotAllowedException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, ONLY_TEXT, false);
            } else if (e instanceof OutOfContextElementException) {
                OutOfContextElementException ex = (OutOfContextElementException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, NOT_ALLOWED, false);
                String parentName = null;
                if (ex.getParent() != null) {
                    parentName = ex.getParent().getLocalName();
                    messageTextString(messageTextHandler, AS_CHILD_OF, false);
                    element(messageTextHandler, ex.getParent(), false);
                }
                messageTextString(messageTextHandler,
                        IN_THIS_CONTEXT_SUPPRESSING, false);
                if (parentName != null
                        && TRANSPARENT_ELEMENTS.contains(parentName)) {
                    messageTextString(messageTextHandler, (" Note: The “"
                            + parentName + "” element has a transparent"
                            + " content model; its allowed content is"
                            + " inherited from its parent element.")
                                    .toCharArray(),
                            false);
                }
            } else if (e instanceof RequiredAttributesMissingOneOfException) {
                RequiredAttributesMissingOneOfException ex = (RequiredAttributesMissingOneOfException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_ATTRIBUTES_MISSING_ONE_OF, false);
                for (Iterator<String> iter = ex.getAttributeLocalNames().iterator(); iter.hasNext();) {
                    codeString(messageTextHandler, iter.next());
                    if (iter.hasNext()) {
                        messageTextString(messageTextHandler, COMMA, false);
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredAttributesMissingException) {
                RequiredAttributesMissingException ex = (RequiredAttributesMissingException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_ATTRIBUTES_MISSING, false);
                codeString(messageTextHandler, ex.getAttributeLocalName());
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredElementsMissingException) {
                RequiredElementsMissingException ex = (RequiredElementsMissingException) e;
                if (ex.getParent() == null) {
                    messageTextString(messageTextHandler,
                            REQUIRED_ELEMENTS_MISSING, false);
                } else {
                    element(messageTextHandler, ex.getParent(), true);
                    if (ex.getMissingElementName() == null) {
                        messageTextString(messageTextHandler,
                                IS_MISSING_A_REQUIRED_CHILD, false);
                    } else {
                        messageTextString(messageTextHandler,
                                REQUIRED_CHILDREN_MISSING_FROM, false);
                        codeString(messageTextHandler,
                                ex.getMissingElementName());
                    }
                    messageTextString(messageTextHandler, PERIOD, false);
                }
            } else if (e instanceof StringNotAllowedException) {
                StringNotAllowedException ex = (StringNotAllowedException) e;
                boolean isTypoInfo = false;
                Map<String, DatatypeException> datatypeErrors = ex.getExceptions();
                for (Map.Entry<String, DatatypeException> entry :
                        datatypeErrors.entrySet()) {
                    DatatypeException dex = entry.getValue();
                    if (dex instanceof Html5DatatypeException) {
                        Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                        if (ex5.isWarning() && ex5.getMessage() != null
                                && ex5.getMessage().contains("Typo for")) {
                            isTypoInfo = true;
                        }
                    }
                }
                if (!isTypoInfo) {
                    messageTextString(messageTextHandler, BAD_CHARACTER_CONTENT,
                            false);
                    codeString(messageTextHandler, ex.getValue());
                    messageTextString(messageTextHandler, FOR, false);
                    element(messageTextHandler, ex.getCurrentElement(), false);
                }
                emitDatatypeErrors(messageTextHandler, ex.getExceptions());
            } else if (e instanceof TextNotAllowedException) {
                TextNotAllowedException ex = (TextNotAllowedException) e;
                messageTextString(messageTextHandler, TEXT_NOT_ALLOWED_IN,
                        false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, IN_THIS_CONTEXT, false);
            } else if (e instanceof UnfinishedElementException) {
                UnfinishedElementException ex = (UnfinishedElementException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                if (ex.getMissingElementName() == null) {
                    messageTextString(messageTextHandler,
                            IS_MISSING_A_REQUIRED_CHILD, false);
                } else {
                    messageTextString(messageTextHandler,
                            REQUIRED_CHILDREN_MISSING_FROM, false);
                    codeString(messageTextHandler, ex.getMissingElementName());
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof UnfinishedElementOneOfException) {
                UnfinishedElementOneOfException ex = (UnfinishedElementOneOfException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_CHILDREN_MISSING_ONE_OF_FROM, false);
                for (Iterator<String> iter = ex.getMissingElementNames().iterator(); iter.hasNext();) {
                    String missingElementName = iter.next();
                    if (!("http://www.w3.org/1999/xhtml".equals(ex.getCurrentElement().getNamespaceUri()) && "frameset".equals(missingElementName))) {
                        codeString(messageTextHandler, missingElementName);
                        if (iter.hasNext()) {
                            messageTextString(messageTextHandler, COMMA, false);
                        }
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredElementsMissingOneOfException) {
                RequiredElementsMissingOneOfException ex = (RequiredElementsMissingOneOfException) e;
                element(messageTextHandler, ex.getParent(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_CHILDREN_MISSING_ONE_OF_FROM, false);
                for (Iterator<String> iter = ex.getMissingElementNames().iterator(); iter.hasNext();) {
                    String missingElementName = iter.next();
                    if (!("http://www.w3.org/1999/xhtml".equals(ex.getCurrentElement().getNamespaceUri()) && "frameset".equals(missingElementName))) {
                        codeString(messageTextHandler, missingElementName);
                        if (iter.hasNext()) {
                            messageTextString(messageTextHandler, COMMA, false);
                        }
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof UnknownElementException) {
                UnknownElementException ex = (UnknownElementException) e;
                messageTextString(messageTextHandler, UNKNOWN, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, NOT_ALLOWED, false);
                if (ex.getParent() != null) {
                    messageTextString(messageTextHandler, AS_CHILD_OF, false);
                    element(messageTextHandler, ex.getParent(), false);
                }
                messageTextString(messageTextHandler, PERIOD, false);
            }
        }
        emitter.endText();
    }

    /**
     * @param messageTextHandler
     * @param datatypeErrors
     * @throws SAXException
     */
    private void emitDatatypeErrors(MessageTextHandler messageTextHandler,
            Map<String, DatatypeException> datatypeErrors) throws SAXException {
        if (datatypeErrors.isEmpty()) {
            messageTextString(messageTextHandler, PERIOD, false);
        } else {
            boolean isTypoInfo = false;
            for (Map.Entry<String, DatatypeException> entry :
                    datatypeErrors.entrySet()) {
                DatatypeException ex = entry.getValue();
                if (ex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) ex;
                    if (ex5.isWarning() && ex5.getMessage() != null
                            && ex5.getMessage().contains("Typo for")) {
                        isTypoInfo = true;
                    }
                }
            }
            if (!isTypoInfo) {
                messageTextString(messageTextHandler, COLON, false);
            }
            for (Map.Entry<String, DatatypeException> entry :
                    datatypeErrors.entrySet()) {
                if (!isTypoInfo) {
                    messageTextString(messageTextHandler, SPACE, false);
                }
                DatatypeException ex = entry.getValue();
                if (ex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) ex;
                    String[] segments = ex5.getSegments();
                    for (int i = 0; i < segments.length; i++) {
                        String segment = segments[i];
                        if (i % 2 == 0) {
                            emitStringWithQurlyQuotes(messageTextHandler,
                                    segment);
                        } else {
                            String scrubbed = scrub(segment);
                            messageTextHandler.startCode();
                            messageTextHandler.characters(
                                    scrubbed.toCharArray(), 0,
                                    scrubbed.length());
                            messageTextHandler.endCode();
                        }
                    }
                } else {
                    emitStringWithQurlyQuotes(messageTextHandler,
                            ex.getMessage());
                }
            }
        }
    }

    private void element(MessageTextHandler messageTextHandler, Name element,
            boolean atSentenceStart) throws SAXException {
        if (html) {
            messageTextString(messageTextHandler, ELEMENT, atSentenceStart);
            linkedCodeString(messageTextHandler, element.getLocalName(),
                    spec.elementLink(element));
        } else {
            String ns = element.getNamespaceUri();
            char[] humanReadable = WELL_KNOWN_NAMESPACES.get(ns);
            if (humanReadable == null) {
                if (loggingOk) {
                    log4j.info(new StringBuilder().append("UNKNOWN_NS:\t").append(
                            ns));
                }
                messageTextString(messageTextHandler, ELEMENT, atSentenceStart);
                linkedCodeString(messageTextHandler, element.getLocalName(),
                        spec.elementLink(element));
                messageTextString(messageTextHandler, FROM_NAMESPACE, false);
                codeString(messageTextHandler, ns);
            } else {
                messageTextString(messageTextHandler, humanReadable,
                        atSentenceStart);
                messageTextString(messageTextHandler, SPACE, false);
                messageTextString(messageTextHandler, ELEMENT, false);
                linkedCodeString(messageTextHandler, element.getLocalName(),
                        spec.elementLink(element));
            }
        }
    }

    private void linkedCodeString(MessageTextHandler messageTextHandler,
            String str, String url) throws SAXException {
        if (url != null) {
            messageTextHandler.startLink(url, null);
        }
        codeString(messageTextHandler, str);
        if (url != null) {
            messageTextHandler.endLink();
        }

    }

    private void attribute(MessageTextHandler messageTextHandler,
            Name attributeName, Name elementName, boolean atSentenceStart)
            throws SAXException {
        String ns = attributeName.getNamespaceUri();
        if (html || "".equals(ns)) {
            messageTextString(messageTextHandler, ATTRIBUTE, atSentenceStart);
            codeString(messageTextHandler, attributeName.getLocalName());
        } else if ("http://www.w3.org/XML/1998/namespace".equals(ns)) {
            messageTextString(messageTextHandler, ATTRIBUTE, atSentenceStart);
            codeString(messageTextHandler,
                    "xml:" + attributeName.getLocalName());
        } else {
            char[] humanReadable = WELL_KNOWN_NAMESPACES.get(ns);
            if (humanReadable == null) {
                if (loggingOk) {
                    log4j.info(new StringBuilder().append("UNKNOWN_NS:\t").append(
                            ns));
                }
                messageTextString(messageTextHandler, ATTRIBUTE,
                        atSentenceStart);
                codeString(messageTextHandler, attributeName.getLocalName());
                messageTextString(messageTextHandler, FROM_NAMESPACE, false);
                codeString(messageTextHandler, ns);
            } else {
                messageTextString(messageTextHandler, humanReadable,
                        atSentenceStart);
                messageTextString(messageTextHandler, SPACE, false);
                messageTextString(messageTextHandler, ATTRIBUTE, false);
                codeString(messageTextHandler, attributeName.getLocalName());
            }
        }
    }

    private void codeString(MessageTextHandler messageTextHandler, String str)
            throws SAXException {
        messageTextHandler.startCode();
        messageTextHandler.characters(str.toCharArray(), 0, str.length());
        messageTextHandler.endCode();
    }

    private void messageTextString(MessageTextHandler messageTextHandler,
            char[] ch, boolean capitalize) throws SAXException {
        if (capitalize && ch[0] >= 'a' && ch[0] <= 'z') {
            oneChar[0] = (char) (ch[0] - 0x20);
            messageTextHandler.characters(oneChar, 0, 1);
            if (ch.length > 1) {
                messageTextHandler.characters(ch, 1, ch.length - 1);
            }
        } else {
            messageTextHandler.characters(ch, 0, ch.length);
        }
    }

    private void emitStringWithQurlyQuotes(
            MessageTextHandler messageTextHandler, String message)
            throws SAXException {
        if (message == null) {
            message = "";
        }
        message = scrub(message);
        int len = message.length();
        int start = 0;
        int startQuotes = 0;
        for (int i = 0; i < len; i++) {
            char c = message.charAt(i);
            if (c == '“') {
                startQuotes++;
                if (startQuotes == 1) {
                    char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
                    messageTextHandler.characters(scrubbed, 0, scrubbed.length);
                    start = i + 1;
                    messageTextHandler.startCode();
                }
            } else if (c == '”' && startQuotes > 0) {
                startQuotes--;
                if (startQuotes == 0) {
                    char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
                    messageTextHandler.characters(scrubbed, 0, scrubbed.length);
                    start = i + 1;
                    messageTextHandler.endCode();
                }
            }
        }
        if (start < len) {
            char[] scrubbed = scrub(message.substring(start, len)).toCharArray();
            messageTextHandler.characters(scrubbed, 0, scrubbed.length);
        }
        if (startQuotes > 0) {
            messageTextHandler.endCode();
        }
    }

    private void elaboration(Exception e)
            throws SAXException {
        if (!(e instanceof AbstractValidationException
                || e instanceof VnuBadAttrValueException
                || e instanceof VnuBadElementNameException
                || e instanceof DatatypeMismatchException)) {
            return;
        }

        if (e instanceof ImpossibleAttributeIgnoredException) {
            ImpossibleAttributeIgnoredException ex = (ImpossibleAttributeIgnoredException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt, ex.getAttributeName());
        } else if (e instanceof OnlyTextNotAllowedException) {
            OnlyTextNotAllowedException ex = (OnlyTextNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof OutOfContextElementException) {
            OutOfContextElementException ex = (OutOfContextElementException) e;
            Name parent = ex.getParent();
            Name child = ex.getCurrentElement();
            elaborateContentModelandContext(parent, child);
        } else if (e instanceof RequiredAttributesMissingException) {
            RequiredAttributesMissingException ex = (RequiredAttributesMissingException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt);
        } else if (e instanceof RequiredAttributesMissingOneOfException) {
            RequiredAttributesMissingOneOfException ex = (RequiredAttributesMissingOneOfException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt);
        } else if (e instanceof RequiredElementsMissingException) {
            RequiredElementsMissingException ex = (RequiredElementsMissingException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof RequiredElementsMissingOneOfException) {
            RequiredElementsMissingOneOfException ex = (RequiredElementsMissingOneOfException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof StringNotAllowedException) {
            StringNotAllowedException ex = (StringNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof TextNotAllowedException) {
            TextNotAllowedException ex = (TextNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnfinishedElementException) {
            UnfinishedElementException ex = (UnfinishedElementException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnfinishedElementOneOfException) {
            UnfinishedElementOneOfException ex = (UnfinishedElementOneOfException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnknownElementException) {
            UnknownElementException ex = (UnknownElementException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof BadAttributeValueException) {
            BadAttributeValueException ex = (BadAttributeValueException) e;
            Map<String, DatatypeException> map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof VnuBadAttrValueException) {
            VnuBadAttrValueException ex = (VnuBadAttrValueException) e;
            Map<String, DatatypeException> map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof VnuBadElementNameException) {
            VnuBadElementNameException ex = (VnuBadElementNameException) e;
            Map<String, DatatypeException> map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof DatatypeMismatchException) {
            DatatypeMismatchException ex = (DatatypeMismatchException) e;
            Map<String, DatatypeException> map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof StringNotAllowedException) {
            StringNotAllowedException ex = (StringNotAllowedException) e;
            Map<String, DatatypeException> map = ex.getExceptions();
            elaborateDatatypes(map);
        }
    }

    private void elaborateDatatypes(Map<String, DatatypeException> map)
            throws SAXException {
        Set<DocumentFragment> fragments = new HashSet<>();
        for (Map.Entry<String, DatatypeException> entry : map.entrySet()) {
            DatatypeException ex = entry.getValue();
            if (ex instanceof Html5DatatypeException) {
                Html5DatatypeException ex5 = (Html5DatatypeException) ex;
                DocumentFragment fragment = HTML5_DATATYPE_ADVICE.get(ex5.getDatatypeClass());
                if (fragment != null) {
                    fragments.add(fragment);
                }
            }
        }
        if (!fragments.isEmpty()) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                for (DocumentFragment fragment : fragments) {
                    treeParser.parse(fragment);
                }
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();

        }
    }

    /**
     * @param elt
     * @throws SAXException
     */
    private void elaborateContentModel(Name elt) throws SAXException {
        DocumentFragment dds = spec.contentModelDescription(elt);
        if (dds != null) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                emitContentModelDt(xhtmlSaxEmitter, elt);
                treeParser.parse(dds);
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();
        }
    }

    private void elaborateContentModelandContext(Name parent, Name child)
            throws SAXException {
        DocumentFragment contentModelDds = spec.contentModelDescription(parent);
        DocumentFragment contextDds = spec.contextDescription(child);
        if (contentModelDds != null || contextDds != null) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                if (contextDds != null) {
                    emitContextDt(xhtmlSaxEmitter, child);
                    treeParser.parse(contextDds);
                }
                if (contentModelDds != null) {
                    emitContentModelDt(xhtmlSaxEmitter, parent);
                    treeParser.parse(contentModelDds);
                }
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();
        }
    }

    /**
     * @param elt
     * @throws SAXException
     */
    private void elaborateElementSpecificAttributes(Name elt)
            throws SAXException {
        this.elaborateElementSpecificAttributes(elt, null);
    }

    private void elaborateElementSpecificAttributes(Name elt, Name attribute)
            throws SAXException {
        if ("input".equals(elt.getLocalName())) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                elaborateInputAttributes(xhtmlSaxEmitter, elt, attribute);
            }
            emitter.endElaboration();
        } else {
            DocumentFragment dds = spec.elementSpecificAttributesDescription(elt);
            if (dds != null) {
                ContentHandler ch = emitter.startElaboration();
                if (ch != null) {
                    TreeParser treeParser = new TreeParser(ch, null);
                    XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                    xhtmlSaxEmitter.startElement("dl");
                    emitElementSpecificAttributesDt(xhtmlSaxEmitter, elt);
                    treeParser.parse(dds);
                    xhtmlSaxEmitter.endElement("dl");
                }
                emitter.endElaboration();
            }
        }
    }

    private void emitElementSpecificAttributesDt(
            XhtmlSaxEmitter xhtmlSaxEmitter, Name elt) throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(ELEMENT_SPECIFIC_ATTRIBUTES_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(ELEMENT_SPECIFIC_ATTRIBUTES_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitContextDt(XhtmlSaxEmitter xhtmlSaxEmitter, Name elt)
            throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(CONTEXT_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(CONTEXT_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitContentModelDt(XhtmlSaxEmitter xhtmlSaxEmitter, Name elt)
            throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(CONTENT_MODEL_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(CONTENT_MODEL_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitLinkifiedLocalName(XhtmlSaxEmitter xhtmlSaxEmitter,
            Name elt) throws SAXException {
        String url = spec.elementLink(elt);
        if (url != null) {
            attributesImpl.clear();
            attributesImpl.addAttribute("href", url);
            xhtmlSaxEmitter.startElement("a", attributesImpl);
        }
        xhtmlSaxEmitter.startElement("code");
        xhtmlSaxEmitter.characters(elt.getLocalName());
        xhtmlSaxEmitter.endElement("code");
        if (url != null) {
            xhtmlSaxEmitter.endElement("a");
        }
    }

    private void elaborateInputAttributes(XhtmlSaxEmitter xhtmlSaxEmitter,
            Name elt, Name badAttribute) throws SAXException {
        attributesImpl.clear();
        attributesImpl.addAttribute("class", "inputattrs");
        xhtmlSaxEmitter.startElement("dl", attributesImpl);
        emitElementSpecificAttributesDt(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.startElement("dd");
        attributesImpl.clear();
        addHyperlink(xhtmlSaxEmitter, "Global attributes", SPEC_LINK_URI
                + "#global-attributes");
        attributesImpl.addAttribute("class", "inputattrtypes");
        xhtmlSaxEmitter.startElement("span", attributesImpl);
        xhtmlSaxEmitter.endElement("span");
        xhtmlSaxEmitter.endElement("dd");
        for (Map.Entry<String, String[]> entry : validInputTypesByAttributeName.entrySet()) {
            String attributeName = entry.getKey();
            xhtmlSaxEmitter.startElement("dd");
            attributesImpl.clear();
            attributesImpl.addAttribute("class", "inputattrname");
            xhtmlSaxEmitter.startElement("code", attributesImpl);
            attributesImpl.clear();
            attributesImpl.addAttribute("href",
                    SPEC_LINK_URI + entry.getValue()[0]);
            xhtmlSaxEmitter.startElement("a", attributesImpl);
            addText(xhtmlSaxEmitter, attributeName);
            xhtmlSaxEmitter.endElement("a");
            xhtmlSaxEmitter.endElement("code");
            attributesImpl.addAttribute("class", "inputattrtypes");
            if (badAttribute != null
                    && attributeName.equals(badAttribute.getLocalName())) {
                listInputTypesForAttribute(xhtmlSaxEmitter, attributeName, true);
            } else {
                listInputTypesForAttribute(xhtmlSaxEmitter, attributeName,
                        false);
            }
            xhtmlSaxEmitter.endElement("dd");
        }
        xhtmlSaxEmitter.endElement("dl");
    }

    private void listInputTypesForAttribute(XhtmlSaxEmitter xhtmlSaxEmitter,
            String attributeName, boolean bad) throws SAXException {
        String[] typeNames = validInputTypesByAttributeName.get(attributeName);
        int typeCount = typeNames.length;
        String wrapper = (bad ? "b" : "span");
        String highlight = (bad ? " highlight" : "");
        if (typeCount > 1 || "value".equals(attributeName)) {
            addText(xhtmlSaxEmitter, " ");
            AttributesImpl attributesImpl = new AttributesImpl();
            attributesImpl.addAttribute("class", "inputattrtypes" + highlight);
            xhtmlSaxEmitter.startElement(wrapper, attributesImpl);
            addText(xhtmlSaxEmitter, "when ");
            xhtmlSaxEmitter.startElement("code");
            addText(xhtmlSaxEmitter, "type");
            xhtmlSaxEmitter.endElement("code", "code");
            addText(xhtmlSaxEmitter, " is ");
            if ("value".equals(attributeName)) {
                addText(xhtmlSaxEmitter, "not ");
                addHyperlink(xhtmlSaxEmitter, "file", SPEC_LINK_URI
                        + fragmentIdByInputType.get("file"));
                addText(xhtmlSaxEmitter, " or ");
                addHyperlink(xhtmlSaxEmitter, "image", SPEC_LINK_URI
                        + fragmentIdByInputType.get("image"));
            } else {
                for (int i = 1; i < typeCount; i++) {
                    String typeName = typeNames[i];
                    if (i > 1) {
                        addText(xhtmlSaxEmitter, " ");
                    }
                    if (typeCount > 2 && i == typeCount - 1) {
                        addText(xhtmlSaxEmitter, "or ");
                    }
                    addHyperlink(xhtmlSaxEmitter, typeName, SPEC_LINK_URI
                            + fragmentIdByInputType.get(typeName));
                    if (i < typeCount - 1 && typeCount > 3) {
                        addText(xhtmlSaxEmitter, ",");
                    }
                }
            }
            xhtmlSaxEmitter.endElement(wrapper);
        } else {
            AttributesImpl attributesImpl = new AttributesImpl();
            attributesImpl.addAttribute("class", "inputattrtypes");
            xhtmlSaxEmitter.startElement("span", attributesImpl);
            xhtmlSaxEmitter.endElement("span");
        }
    }

    private void addText(XhtmlSaxEmitter xhtmlSaxEmitter, String text)
            throws SAXException {
        char[] ch = text.toCharArray();
        xhtmlSaxEmitter.characters(ch, 0, ch.length);
    }

    private void addHyperlink(XhtmlSaxEmitter xhtmlSaxEmitter, String text,
            String href) throws SAXException {
        AttributesImpl attributesImpl = new AttributesImpl();
        attributesImpl.addAttribute("href", href);
        xhtmlSaxEmitter.startElement("a", attributesImpl);
        addText(xhtmlSaxEmitter, text);
        xhtmlSaxEmitter.endElement("a");
    }

    private final class ExactErrorHandler implements ErrorHandler {

        private final MessageEmitterAdapter owner;

        /**
         * @param owner
         */
        ExactErrorHandler(final MessageEmitterAdapter owner) {
            this.owner = owner;
        }

        @Override
        public void error(SAXParseException exception) throws SAXException {
            owner.error(exception, true);
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
            owner.fatalError(exception, true);
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
            owner.warning(exception, true);
        }

    }

    /**
     * Returns the exactErrorHandler.
     *
     * @return the exactErrorHandler
     */
    public ErrorHandler getExactErrorHandler() {
        return exactErrorHandler;
    }

    /**
     * Sets the lineOffset.
     *
     * @param lineOffset
     *            the lineOffset to set
     */
    public void setLineOffset(int lineOffset) {
        this.lineOffset = lineOffset;
    }

    /**
     * Sets the spec.
     *
     * @param spec
     *            the spec to set
     */
    public void setSpec(Spec spec) {
        this.spec = spec;
    }

    /**
     * Sets the html.
     *
     * @param html
     *            the html to set
     */
    public void setHtml(boolean html) {
        this.html = html;
    }

    public void setLoggingOk(boolean ok) {
        this.loggingOk = ok;
    }

    /**
     * Sets the errorsOnly.
     *
     * @param errorsOnly
     *            the errorsOnly to set
     */
    public void setErrorsOnly(boolean errorsOnly) {
        this.errorsOnly = errorsOnly;
    }

    /**
     * Sets the skipInfoMessages.
     *
     * @param skipInfoMessages
     *            the skipInfoMessages to set
     */
    public void setSkipInfoMessages(boolean skipInfoMessages) {
        this.skipInfoMessages = skipInfoMessages;
    }

    /**
     * @throws SAXException
     * @see nu.validator.messages.MessageEmitter#endMessage()
     */
    public void endMessage() throws SAXException {
        emitter.endMessage();
    }

    /**
     * @param type
     * @param systemId
     * @param oneBasedFirstLine
     * @param oneBasedFirstColumn
     * @param oneBasedLastLine
     * @param oneBasedLastColumn
     * @param exact
     * @throws SAXException
     * @see nu.validator.messages.MessageEmitter#startMessage(nu.validator.messages.types.MessageType,
     *      java.lang.String, int, int, int, int, boolean)
     */
    public void startMessage(MessageType type, String systemId,
            int oneBasedFirstLine, int oneBasedFirstColumn,
            int oneBasedLastLine, int oneBasedLastColumn, boolean exact)
            throws SAXException {
        emitter.startMessage(type, systemId, (oneBasedFirstLine == -1) ? -1
                : oneBasedFirstLine + lineOffset, oneBasedFirstColumn,
                (oneBasedLastLine == -1) ? -1 : oneBasedLastLine + lineOffset,
                oneBasedLastColumn, exact);
    }
}
