CVE-2021-21366: Misinterpretation of Input (CWE-115)
value: xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. xmldom versions 0.4.0 and older do not correctly preserve system identifiers, FPIs or namespaces when repeatedly parsing and serializing maliciously crafted documents. This may lead to unexpected syntactic changes during XML processing in some downstream applications. This is fixed in version 0.5.0. As a workaround downstream applications can validate the input and reject the maliciously crafted documents.
CWE description: The product misinterprets an input, whether from an attacker or another product, in a security-relevant fashion.
CVSS v3 base score: 4.3
Published: 2021-03-12T17:15Z
Repository: https://github.com/xmldom/xmldom
Language: JavaScript
Files changed: lib/dom-parser.js, lib/dom.js, lib/sax.js, test/error/__snapshots__/reported-levels.test.js.snap, test/error/__snapshots__/xml-error.test.js.snap, test/error/error-handler.test.js, test/error/reported-levels.test.js, test/error/reported.js, test/error/xml-reader-dom-handler-errors.test.js, test/html/__snapshots__/normalize.test.js.snap, test/parse/doctype.test.js, test/parse/parse-error.test.js, test/xmltest/__snapshots__/not-wf.test.js.snap
Commit date: 2021-03-08 22:21:56 -0500
Commit message: Merge pull request from GHSA-h6q6-9hqw-rwfv

* fix!: Preserve quotes in DOCTYPE declaration

Since the only purpose of parsing the DOCTYPE is to be able to restore it when serializing,
we decided that it would be best to leave the parsed publicId and systemId as is,
including any quotes.

BREAKING CHANGE: If somebody relies on the actual unquoted values of those ids,
they will need to take care of either single or double quotes and the right escaping.

(Without this change this would not have been possible because the SAX parser already
dropped the information about the quotes that have been used in the source.)

https://www.w3.org/TR/2006/REC-xml11-20060816/#dtd
https://www.w3.org/TR/2006/REC-xml11-20060816/#IDAX1KS (External Entity Declaration)

Co-authored-by: Christian Bewernitz <coder@karfau.de>
Co-authored-by: Chris Brody <chris.brody+brodybits@gmail.com>

* feat(security): Improve error reporting; throw on duplicate attribute

BREAKING CHANGE: It is currently not clear how to consistently deal with duplicate attributes,
so it is also safer for our users to fail when detecting them.

It is possible to configure the `DOMParser.errorHandler` before parsing, to handle those errors differently.

To accomplish this and also be able to verify it in tests we needed to:

- create a new `Error` type `ParseError` and export it
- Throw `ParseError` from `errorHandler.fatalError` and prevent those from being caught in `XMLReader`.
- export `DOMHandler` constructor as `__DOMHandler`

Co-authored-by: Christian Bewernitz <coder@karfau.de>
Co-authored-by: Chris Brody <chris.brody+brodybits@gmail.com>

Co-authored-by: Christian Bewernitz <coder@karfau.de>
Diff: diff --git a/lib/dom-parser.js b/lib/dom-parser.js
index bb4dda7..2ad090d 100644
--- a/lib/dom-parser.js
+++ b/lib/dom-parser.js
@@ -176,12 +176,11 @@ DOMHandler.prototype = {
 	},
 	error:function(error) {
 		console.error('[xmldom error]\t'+error,_locator(this.locator));
 	},
 	fatalError:function(error) {
-		console.error('[xmldom fatalError]\t'+error,_locator(this.locator));
-	    throw error;
+		throw new ParseError(error, this.locator);
 	}
 }
 function _locator(l){
 	if(l){
 		return '\n@'+(l.systemId ||'')+'#[line:'+l.lineNumber+',col:'+l.columnNumber+']'
@@ -242,10 +241,13 @@ function appendElement (hander,node) {
     }
 }//appendChild and setAttributeNS are preformance key
 
 //if(typeof require == 'function'){
 var htmlEntity = require('./entities');
-var XMLReader = require('./sax').XMLReader;
+var sax = require('./sax');
+var XMLReader = sax.XMLReader;
+var ParseError = sax.ParseError;
 var DOMImplementation = exports.DOMImplementation = require('./dom').DOMImplementation;
 exports.XMLSerializer = require('./dom').XMLSerializer ;
 exports.DOMParser = DOMParser;
+exports.__DOMHandler = DOMHandler;
 //}
diff --git a/lib/dom.js b/lib/dom.js
index 266efeb..78ab770 100644
--- a/lib/dom.js
+++ b/lib/dom.js
@@ -1092,17 +1092,17 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
 	case DOCUMENT_TYPE_NODE:
 		var pubid = node.publicId;
 		var sysid = node.systemId;
 		buf.push('<!DOCTYPE ',node.name);
 		if(pubid){
-			buf.push(' PUBLIC "',pubid);
+			buf.push(' PUBLIC ', pubid);
 			if (sysid && sysid!='.') {
-				buf.push( '" "',sysid);
+				buf.push(' ', sysid);
 			}
-			buf.push('">');
+			buf.push('>');
 		}else if(sysid && sysid!='.'){
-			buf.push(' SYSTEM "',sysid,'">');
+			buf.push(' SYSTEM ', sysid, '>');
 		}else{
 			var sub = node.internalSubset;
 			if(sub){
 				buf.push(" [",sub,"]");
 			}
diff --git a/lib/sax.js b/lib/sax.js
index c46d146..395b13f 100644
--- a/lib/sax.js
+++ b/lib/sax.js
@@ -16,10 +16,25 @@ var S_EQ = 3;//=space?
 var S_ATTR_NOQUOT_VALUE = 4;//attr value(no quot value only)
 var S_ATTR_END = 5;//attr value end and no space(quot end)
 var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer)
 var S_TAG_CLOSE = 7;//closed el<el />
 
+/**
+ * Creates an error that will not be caught by XMLReader aka the SAX parser.
+ *
+ * @param {string} message
+ * @param {any?} locator Optional, can provide details about the location in the source
+ * @constructor
+ */
+function ParseError(message, locator) {
+	this.message = message
+	this.locator = locator
+	if(Error.captureStackTrace) Error.captureStackTrace(this, ParseError);
+}
+ParseError.prototype = new Error();
+ParseError.prototype.name = ParseError.name
+
 function XMLReader(){
 	
 }
 
 XMLReader.prototype = {
@@ -124,11 +139,11 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){
 						for(var prefix in localNSMap){
 							domBuilder.endPrefixMapping(prefix) ;
 						}
 					}
 					if(!endMatch){
-		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName );
+		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName ); // No known test case
 					}
 		        }else{
 		        	parseStack.push(config)
 		        }
 				
@@ -185,14 +200,15 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){
 				}else{
 					end++;
 				}
 			}
 		}catch(e){
+			if (e instanceof ParseError) {
+				throw e;
+			}
 			errorHandler.error('element parse error: '+e)
-			//errorHandler.error('element parse error: '+e);
 			end = -1;
-			//throw e;
 		}
 		if(end>start){
 			start = end;
 		}else{
 			//TODO: 这里有可能sax回退，有位置错误风险
@@ -209,10 +225,20 @@ function copyLocator(f,t){
 /**
  * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack);
  * @return end of the elementStartPart(end of elementEndPart for selfClosed el)
  */
 function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){
+
+	/**
+	 * @param {string} qname
+	 * @param {string} value
+	 * @param {number} startIndex
+	 */
+	function addAttribute(qname, value, startIndex) {
+		if (qname in el.attributeNames) errorHandler.fatalError('Attribute ' + qname + ' redefined')
+		el.addValue(qname, value, startIndex)
+	}
 	var attrName;
 	var value;
 	var p = ++start;
 	var s = S_TAG;//status
 	while(true){
@@ -224,11 +250,11 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				s = S_EQ;
 			}else if(s === S_ATTR_SPACE){
 				s = S_EQ;
 			}else{
 				//fatalError: equal must after attrName or space after attrName
-				throw new Error('attribute equal must after attrName');
+				throw new Error('attribute equal must after attrName'); // No known test case
 			}
 			break;
 		case '\'':
 		case '"':
 			if(s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE
@@ -239,27 +265,27 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				}
 				start = p+1;
 				p = source.indexOf(c,start)
 				if(p>0){
 					value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
-					el.add(attrName,value,start-1);
+					addAttribute(attrName, value, start-1);
 					s = S_ATTR_END;
 				}else{
 					//fatalError: no end quot match
 					throw new Error('attribute value no end \''+c+'\' match');
 				}
 			}else if(s == S_ATTR_NOQUOT_VALUE){
 				value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
 				//console.log(attrName,value,start,p)
-				el.add(attrName,value,start);
+				addAttribute(attrName, value, start);
 				//console.dir(el)
 				errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+')!!');
 				start = p+1;
 				s = S_ATTR_END
 			}else{
 				//fatalError: no equal before
-				throw new Error('attribute value must after "="');
+				throw new Error('attribute value must after "="'); // No known test case
 			}
 			break;
 		case '/':
 			switch(s){
 			case S_TAG:
@@ -273,15 +299,14 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 			case S_ATTR:
 			case S_ATTR_SPACE:
 				break;
 			//case S_EQ:
 			default:
-				throw new Error("attribute invalid close char('/')")
+				throw new Error("attribute invalid close char('/')") // No known test case
 			}
 			break;
 		case ''://end document
-			//throw new Error('unexpected end of input')
 			errorHandler.error('unexpected end of input');
 			if(s == S_TAG){
 				el.setTagName(source.slice(start,p));
 			}
 			return p;
@@ -303,17 +328,17 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 			case S_ATTR_SPACE:
 				if(s === S_ATTR_SPACE){
 					value = attrName;
 				}
 				if(s == S_ATTR_NOQUOT_VALUE){
-					errorHandler.warning('attribute "'+value+'" missed quot(")!!');
-					el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start)
+					errorHandler.warning('attribute "'+value+'" missed quot(")!');
+					addAttribute(attrName, value.replace(/&#?\w+;/g,entityReplacer), start)
 				}else{
 					if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)){
 						errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!')
 					}
-					el.add(value,value,start)
+					addAttribute(value, value, start)
 				}
 				break;
 			case S_EQ:
 				throw new Error('attribute value missed!!');
 			}
@@ -334,11 +359,11 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 					s = S_ATTR_SPACE;
 					break;
 				case S_ATTR_NOQUOT_VALUE:
 					var value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
 					errorHandler.warning('attribute "'+value+'" missed quot(")!!');
-					el.add(attrName,value,start)
+					addAttribute(attrName, value, start)
 				case S_ATTR_END:
 					s = S_TAG_SPACE;
 					break;
 				//case S_TAG_SPACE:
 				//case S_EQ:
@@ -357,11 +382,11 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				case S_ATTR_SPACE:
 					var tagName =  el.tagName;
 					if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)){
 						errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!')
 					}
-					el.add(attrName,attrName,start);
+					addAttribute(attrName, attrName, start);
 					start = p;
 					s = S_ATTR;
 					break;
 				case S_ATTR_END:
 					errorHandler.warning('attribute space is required"'+attrName+'"!!')
@@ -540,12 +565,11 @@ function parseDCC(source,start,domBuilder,errorHandler){//sure start with '<!'
 				}else if(/^system$/i.test(matchs[2][0])){
 					sysid = matchs[3][0];
 				}
 			}
 			var lastMatch = matchs[len-1]
-			domBuilder.startDTD(name,pubid && pubid.replace(/^(['"])(.*?)\1$/,'$2'),
-					sysid && sysid.replace(/^(['"])(.*?)\1$/,'$2'));
+			domBuilder.startDTD(name, pubid, sysid);
 			domBuilder.endDTD();
 			
 			return lastMatch.index+lastMatch[0].length
 		}
 	}
@@ -567,27 +591,25 @@ function parseInstruction(source,start,domBuilder){
 		}
 	}
 	return -1;
 }
 
-/**
- * @param source
- */
-function ElementAttributes(source){
-	
+function ElementAttributes(){
+	this.attributeNames = {}
 }
 ElementAttributes.prototype = {
 	setTagName:function(tagName){
 		if(!tagNamePattern.test(tagName)){
 			throw new Error('invalid tagName:'+tagName)
 		}
 		this.tagName = tagName
 	},
-	add:function(qName,value,offset){
+	addValue:function(qName, value, offset) {
 		if(!tagNamePattern.test(qName)){
 			throw new Error('invalid attribute:'+qName)
 		}
+		this.attributeNames[qName] = this.length;
 		this[this.length++] = {qName:qName,value:value,offset:offset}
 	},
 	length:0,
 	getLocalName:function(i){return this[i].localName},
 	getLocator:function(i){return this[i].locator},
@@ -619,6 +641,6 @@ function split(source,start){
 		if(match[1])return buf;
 	}
 }
 
 exports.XMLReader = XMLReader;
-
+exports.ParseError = ParseError;
diff --git a/test/error/__snapshots__/reported-levels.test.js.snap b/test/error/__snapshots__/reported-levels.test.js.snap
new file mode 100644
index 0000000..529e08d
--- /dev/null
+++ b/test/error/__snapshots__/reported-levels.test.js.snap
@@ -0,0 +1,501 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
+    at parse (lib/sax.js:128)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
+    at parse (lib/sax.js:128)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
+    at parse (lib/sax.js:124)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
+    at parse (lib/sax.js:124)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
+    at parseDCC (lib/sax.js:538)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
+    at parseDCC (lib/sax.js:538)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:308)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:308)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:333)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:333)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:363)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:363)",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:281)",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:281)",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:337)",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:337)",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
+@#[line:1,col:1]",
+  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:385)",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
+@#[line:1,col:1]",
+  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:385)",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:263)",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:263)",
+]
+`;
+
+exports[`WF_DuplicateAttribute with mimeType text/html should be reported as fatalError 1`] = `
+Array [
+  "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_DuplicateAttribute with mimeType text/xml should be reported as fatalError 1`] = `
+Array [
+  "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
+    at replace (lib/sax.js:71)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
+    at replace (lib/sax.js:71)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_UnclosedXmlAttribute with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	unclosed xml attribute
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_UnclosedXmlAttribute with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	unclosed xml attribute||@#[line:1,col:1]
+    at parse (lib/sax.js:173)",
+]
+`;
diff --git a/test/error/__snapshots__/xml-error.test.js.snap b/test/error/__snapshots__/xml-error.test.js.snap
index d0a1bb1..5a9ac6e 100644
--- a/test/error/__snapshots__/xml-error.test.js.snap
+++ b/test/error/__snapshots__/xml-error.test.js.snap
@@ -2,11 +2,11 @@
 
 exports[`html vs xml: html attribute (miss quote) 1`] = `
 Object {
   "actual": "<img attr=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
@@ -35,25 +35,10 @@ Object {
     "[xmldom warning]	unclosed xml attribute
 @#[line:1,col:1]",
   ],
 }
 `;
-exports[`html vs xml: unclosed document in text/xml 1`] = `
-Object {
-  "actual": "<img/>",
-  "warning": Array [
-    "[xmldom warning]	unclosed xml attribute
-@#[line:1,col:1]",
-  ],
-}
-`;
-
-exports[`html vs xml: unclosed document in text/html 1`] = `
-Object {
-  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
-}
-`;
 
 exports[`html vs xml: text/html attribute (missing =) 1`] = `
 Object {
   "actual": "<scxml xmlns=\\"http://www.w3.org/2005/07/scxml\\" version=\\"1.0\\" profile=\\"ecmascript\\" id=\\"scxmlRoot\\" initial=\\"start\\">
 
@@ -92,5 +77,21 @@ Object {
     "[xmldom warning]	attribute value must after \\"=\\"
 @#[line:10,col:5]",
   ],
 }
 `;
+
+exports[`html vs xml: unclosed document in text/html 1`] = `
+Object {
+  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
+}
+`;
+
+exports[`html vs xml: unclosed document in text/xml 1`] = `
+Object {
+  "actual": "<img/>",
+  "warning": Array [
+    "[xmldom warning]	unclosed xml attribute
+@#[line:1,col:1]",
+  ],
+}
+`;
diff --git a/test/error/error-handler.test.js b/test/error/error-handler.test.js
new file mode 100644
index 0000000..8c4f50b
--- /dev/null
+++ b/test/error/error-handler.test.js
@@ -0,0 +1,51 @@
+'use strict'
+
+const { DOMParser } = require('../../lib/dom-parser')
+const { REPORTED } = require('./reported')
+
+describe('custom errorHandler', () => {
+	it('function with two args receives key and message', () => {
+		const errors = {}
+		const parser = new DOMParser({
+			// currently needs to be a `function` to make the test work,
+			// `jest.fn()` or using `() => {}` doesn't work
+			errorHandler: function (key, msg) {
+				errors[key] = msg
+			},
+		})
+
+		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
+		expect(errors).toHaveProperty('warning')
+		parser.parseFromString(
+			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
+			'text/xml'
+		)
+		expect(errors).toHaveProperty('error')
+		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')
+		expect(errors).toHaveProperty('fatalError')
+	})
+
+	it('function with one argument builds list', () => {
+		const errors = []
+		const parser = new DOMParser({
+			// currently needs to be a `function` to make the test work,
+			// `jest.fn()` or using `() => {}` doesn't work
+			errorHandler: function (msg) {
+				errors.push(msg)
+			},
+		})
+
+		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
+		parser.parseFromString(
+			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
+			'text/xml'
+		)
+		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')
+
+		expect(errors).toMatchObject([
+			/\[xmldom warning]/,
+			/\[xmldom error]/,
+			/\[xmldom fatalError]/,
+		])
+	})
+})
diff --git a/test/error/error.test.js b/test/error/error.test.js
deleted file mode 100644
index d92fcb4..0000000
--- a/test/error/error.test.js
+++ /dev/null
@@ -1,83 +0,0 @@
-'use strict'
-
-const { DOMParser } = require('../../lib/dom-parser')
-
-const XML_ERROR = '<html><body title="1<2"><table&lt;;test</body></body></html>'
-const XML_ERROR_AND_WARNING = '<html disabled><1 1="2"/></body></html>'
-
-describe('errorHandler', () => {
-	it('only single function with two args builds map', () => {
-		const errors = {}
-		const parser = new DOMParser({
-			errorHandler: function (key, msg) {
-				errors[key] = msg
-			},
-		})
-
-		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-		expect(errors).toMatchObject({
-			warning: expect.stringMatching(/.*/),
-			error: expect.stringMatching(/.*/),
-		})
-	})
-
-	it('only one function with one argument builds list', () => {
-		const errors = []
-		const parser = new DOMParser({
-			errorHandler: function (msg) {
-				errors.push(msg)
-			},
-		})
-
-		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-		expect(errors).toMatchObject([/\[xmldom warning]/, /\[xmldom error]/])
-	})
-
-	it.each(['warning', 'error'])(
-		'errorHandler for only one level: %s',
-		(level) => {
-			const errors = []
-			const parser = new DOMParser({
-				errorHandler: {
-					[level]: function (msg) {
-						errors.push(msg)
-					},
-				},
-			})
-
-			parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-			expect(errors).toHaveLength(1)
-		}
-	)
-	it.todo(
-		'errorHandler for only one level: fatalError'
-		/*
-		I was not able to create a test case for a fatalError.
-		It might need to be removed from the API, since all but one cases are in comments
-		and an error is thrown instead. The one case left I was not able to reproduce.
-		*/
-	)
-
-	it('error function throwing is not caught', () => {
-		const errors = []
-		const ERROR_MSG = 'FROM TEST'
-
-		const parser = new DOMParser({
-			locator: {}, // removing the locator makes the test fail!
-			errorHandler: {
-				error: function (msg) {
-					errors.push(msg)
-					throw new Error(ERROR_MSG)
-				},
-			},
-		})
-
-		expect(() => {
-			parser.parseFromString(XML_ERROR, 'text/html')
-		}).toThrow(ERROR_MSG)
-		expect(errors).toMatchObject([/\n@#\[line\:\d+,col\:\d+\]/])
-	})
-})
diff --git a/test/error/reported-levels.test.js b/test/error/reported-levels.test.js
new file mode 100644
index 0000000..63ba055
--- /dev/null
+++ b/test/error/reported-levels.test.js
@@ -0,0 +1,106 @@
+'use strict'
+
+const { REPORTED } = require('./reported')
+const { getTestParser } = require('../get-test-parser')
+const { ParseError } = require('../../lib/sax')
+const { DOMParser } = require('../../lib/dom-parser')
+
+describe.each(Object.entries(REPORTED))(
+	'%s',
+	(name, { source, level, match, skippedInHtml }) => {
+		describe.each(['text/xml', 'text/html'])('with mimeType %s', (mimeType) => {
+			const isHtml = mimeType === 'text/html'
+			if (isHtml && skippedInHtml) {
+				it(`should not be reported as ${level}`, () => {
+					const { errors, parser } = getTestParser()
+
+					parser.parseFromString(source, mimeType)
+
+					// if no report was triggered, the key is not present on `errors`
+					expect(errors[level]).toBeUndefined()
+				})
+			} else {
+				it(`should be reported as ${level}`, () => {
+					const { errors, parser } = getTestParser()
+
+					parser.parseFromString(source, mimeType)
+
+					const reported = errors[level]
+					// store the snapshot, so any change in message can be inspected in the git diff
+					expect(reported).toMatchSnapshot()
+					// if a match has been defined, filter messages
+					expect(
+						match ? (reported || []).filter(match) : reported
+					).toHaveLength(1)
+				})
+				if (level === 'fatalError') {
+					it(`should throw ParseError in errorHandler.fatalError`, () => {
+						const parser = new DOMParser()
+
+						expect(() => parser.parseFromString(source, mimeType)).toThrow(
+							ParseError
+						)
+					})
+				} else if (level === 'error') {
+					it(`should not catch Error thrown in errorHandler.${level}`, () => {
+						let thrown = []
+						const errorHandler = {
+							[level]: jest.fn((message) => {
+								const toThrow = new Error(message)
+								thrown.push(toThrow)
+								throw toThrow
+							}),
+						}
+						const { parser } = getTestParser({ errorHandler })
+
+						expect(() => parser.parseFromString(source, mimeType)).toThrow(
+							Error
+						)
+						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
+						match && expect(match(thrown[0].toString())).toBe(true)
+					})
+				} else if (level === 'warning') {
+					it('should escalate Error thrown in errorHandler.warning to errorHandler.error', () => {
+						let thrown = []
+						const errorHandler = {
+							warning: jest.fn((message) => {
+								const toThrow = new Error(message)
+								thrown.push(toThrow)
+								throw toThrow
+							}),
+							error: jest.fn(),
+						}
+						const { parser } = getTestParser({ errorHandler })
+
+						parser.parseFromString(source, mimeType)
+
+						expect(errorHandler.warning).toHaveBeenCalledTimes(1)
+						expect(errorHandler.error).toHaveBeenCalledTimes(1)
+						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
+						match && expect(match(thrown[0].message)).toBe(true)
+					})
+				}
+			}
+		})
+	}
+)
+
+/**
+ * Creates a string from an error that is easily readable in a snapshot
+ * - put's the message on one line as first line
+ * - picks the first line in the stack trace that is in `lib/sax.js`,
+ *   and strips absolute paths and character position from that stack entry
+ *   as second line
+ * @param {Error} error
+ */
+function toErrorSnapshot(error) {
+	const libSaxMatch = /\/.*\/(lib\/sax\.js)/
+	return `${error.message.replace(/([\n\r]+\s*)/g, '||')}\n${error.stack
+		.split(/[\n\r]+/)
+		// find first line that is from lib/sax.js
+		.filter((l) => libSaxMatch.test(l))[0]
+		// strip of absolute path
+		.replace(libSaxMatch, '$1')
+		// strip of position of character in line
+		.replace(/:\d+\)$/, ')')}`
+}
diff --git a/test/error/reported.js b/test/error/reported.js
new file mode 100644
index 0000000..2820684
--- /dev/null
+++ b/test/error/reported.js
@@ -0,0 +1,243 @@
+'use strict'
+
+/**
+ * @typedef ErrorReport
+ * @property {string} source the XML snippet
+ * @property {'error'|'warning'|'fatalError'} level the name of the method triggered
+ * @property {?function(msg:string):boolean} match to pick the relevant report when there are multiple
+ * @property {?boolean} skippedInHtml Is the error reported when parsing HTML?
+ */
+/**
+ * A collection of XML samples and related information that cause the XMLReader
+ * to call methods on `errorHandler`.
+ *
+ * @type {Record<string, ErrorReport>}}
+ */
+const REPORTED = {
+	/**
+	 * Entities need to be in the entityMap to be converted as part of parsing.
+	 * xmldom currently doesn't parse entities declared in DTD.
+	 *
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#wf-entdeclared
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#wf-entdeclared
+	 */
+	WF_EntityDeclared: {
+		source: '<xml>&e;</xml>',
+		level: 'error',
+		match: (msg) => /entity not found/.test(msg),
+	},
+	/**
+	 * Well-formedness constraint: Unique Att Spec
+	 *
+	 * An attribute name must not appear more than once
+	 * in the same start-tag or empty-element tag.
+	 *
+	 * In the browser:
+	 * - as XML it is reported as `error on line 1 at column 17: Attribute a redefined`
+	 * - as HTML only the first definition is considered
+	 *
+	 * In xmldom the behavior is different for namespaces (picks first)
+	 * than for other attributes (picks last),
+	 * which can be a security issue.
+	 *
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#uniqattspec
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#uniqattspec
+	 */
+	WF_DuplicateAttribute: {
+		source: '<xml a="1" a="2"></xml>',
+		level: 'fatalError',
+		match: (msg) => /Attribute .* redefined/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 14: expected '>'`,
+	 * but still adds the root level element to the dom.
+	 */
+	SYNTAX_EndTagNotComplete: {
+		source: '<xml></xml',
+		level: 'error',
+		match: (msg) => /end tag name/.test(msg) && /is not complete/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 21: expected '>'`,
+	 * but still adds the root level element and inner tag to the dom.
+	 */
+	SYNTAX_EndTagMaybeNotComplete: {
+		source: '<xml><inner></inner </xml>',
+		level: 'error',
+		match: (msg) => /end tag name/.test(msg) && /maybe not complete/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 6: Comment not terminated`.
+	 */
+	SYNTAX_UnclosedComment: {
+		source: '<!--',
+		level: 'error',
+		match: (msg) => /Unclosed comment/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:596, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - as XML it is reported as
+	 * `error on line 1 at column 2: StartTag: invalid element name`
+	 * - as HTML it is accepted as characters
+	 *
+	 */
+	SYNTAX_InvalidTagName: {
+		source: '<123 />',
+		level: 'error',
+		match: (msg) => /invalid tagName/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:602, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - as XML it is reported as
+	 * `error on line 1 at column 6: error parsing attribute name`
+	 * - as HTML it is accepted as attribute name
+	 */
+	SYNTAX_InvalidAttributeName: {
+		source: '<xml 123=""/>',
+		level: 'error',
+		match: (msg) => /invalid attribute/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 5: Couldn't find end of Start Tag xml`.
+	 */
+	SYNTAX_UnexpectedEndOfInput: {
+		source: '<xml',
+		level: 'error',
+		match: (msg) => /unexpected end of input/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:392, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - in XML it is reported as `error on line 1 at column 8: error parsing attribute name`
+	 * - in HTML it produces `<xml><a <="" xml=""></a></xml>` (invalid XML?)
+	 */
+	SYNTAX_ElementClosingNotConnected: {
+		source: '<xml><a/ </xml>',
+		level: 'error',
+		match: (msg) => /must be connected/.test(msg),
+	},
+	/**
+	 * In the Browser (for XML) this is reported as
+	 * `error on line 1 at column 6: Extra content at the end of the document`
+	 * for HTML it's added to the DOM without anything being reported.
+	 */
+	WF_UnclosedXmlAttribute: {
+		source: '<xml>',
+		level: 'warning',
+		skippedInHtml: true,
+		match: (msg) => /unclosed xml attribute/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as
+	 * `error on line 1 at column 10: Specification mandates value for attribute attr`
+	 * - for HTML is uses the attribute as one with no value and adds `"value"` to the attribute name
+	 *   and is not reporting any issue.
+	 */
+	WF_AttributeValueMustAfterEqual: {
+		source: '<xml attr"value" />',
+		level: 'warning',
+		match: (msg) => /attribute value must after "="/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
+	 * - for HTML is wraps `value"` with quotes and is not reporting any issue.
+	 */
+	WF_AttributeMissingStartingQuote: {
+		source: '<xml attr=value" />',
+		level: 'warning',
+		match: (msg) => /missed start quot/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:264, caught in 208.
+	 * TODO: Comment indicates fatalError, change to use errorHandler.fatalError?
+	 *
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 20: AttValue: ' expected`
+	 * - for HTML nothing is added to the DOM.
+	 */
+	SYNTAX_AttributeMissingEndingQuote: {
+		source: '<xml attr="value />',
+		level: 'error',
+		match: (msg) => /attribute value no end .* match/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:324
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
+	 * - for HTML is wraps `value/` with quotes and is not reporting any issue.
+	 */
+	WF_AttributeMissingQuote: {
+		source: '<xml attr=value/>',
+		level: 'warning',
+		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg) === false,
+	},
+	/**
+	 * Triggered by lib/sax.js:354
+	 * This is the only warning reported in this sample.
+	 * For some reason the "attribute" that is reported as missing quotes
+	 * has the name `&`.
+	 * This case is also present in 2 tests in test/html/normalize.test.js
+	 *
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 8: AttValue: " or ' expected`
+	 * - for HTML is yields `<xml a="&amp;" b="&amp;"></xml>` and is not reporting any issue.
+	 */
+	WF_AttributeMissingQuote2: {
+		source: `<xml a=& b="&"/>`,
+		level: 'warning',
+		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 9: AttValue: " or ' expected`
+	 * - for HTML is yields `<doc a1></xml>` and is not reporting any issue.
+	 *
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	SYNTAX_AttributeEqualMissingValue: {
+		source: '<doc a1=></doc>',
+		level: 'error',
+		match: (msg) => /attribute value missed!!/.test(msg),
+	},
+	/**
+	 * In the browser this is not an issue at all, but just add an attribute without a value.
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	WF_AttributeMissingValue: {
+		source: '<xml attr ></xml>',
+		level: 'warning',
+		match: (msg) => /missed value/.test(msg) && /instead!!/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:376
+	 * This seems to only be reached when there are two subsequent attributes with a missing value
+	 * In the browser this is not an issue at all, but just add an attribute without a value.
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	WF_AttributeMissingValue2: {
+		source: '<xml attr attr2 ></xml>',
+		level: 'warning',
+		match: (msg) => /missed value/.test(msg) && /instead2!!/.test(msg),
+	},
+}
+
+module.exports = {
+	REPORTED,
+}
diff --git a/test/error/xml-reader-dom-handler-errors.test.js b/test/error/xml-reader-dom-handler-errors.test.js
new file mode 100644
index 0000000..cb410e6
--- /dev/null
+++ b/test/error/xml-reader-dom-handler-errors.test.js
@@ -0,0 +1,140 @@
+'use strict'
+
+const { ParseError } = require('../../lib/sax')
+const { __DOMHandler, DOMParser } = require('../../lib/dom-parser')
+
+/**
+ * All methods implemented on the DOMHandler prototype.
+ *
+ * @type {string[]}
+ */
+const DOMHandlerMethods = Object.keys(__DOMHandler.prototype).sort()
+
+/**
+ * XMLReader is currently not calling all methods "implemented" by DOMHandler (some are just empty),
+ * If this changes the first test will fail.
+ *
+ * @type {Set<string>}
+ */
+const UNCALLED_METHODS = new Set([
+	'attributeDecl',
+	'elementDecl',
+	'endEntity',
+	'externalEntityDecl',
+	'getExternalSubset',
+	'ignorableWhitespace',
+	'internalEntityDecl',
+	'notationDecl',
+	'resolveEntity',
+	'skippedEntity',
+	'startEntity',
+	'unparsedEntityDecl',
+])
+
+/**
+ * Some of the methods DOMParser/XMLReader call during parsing are not guarded by try/catch,
+ * hence an error happening in those will stop the parsing process.
+ * There is a test to verify this error handling.
+ * If it changes this list might need to be changed as well
+ *
+ * @type {Set<string>}
+ */
+const UNCAUGHT_METHODS = new Set([
+	'characters',
+	'endDocument',
+	'error',
+	'setDocumentLocator',
+	'startDocument',
+])
+
+function noop() {}
+
+/**
+ * A subclass of DOMHandler that mocks all methods for later inspection.
+ * As part of the constructor it can be told which method is supposed to throw an error
+ * and which error constructor to use.
+ *
+ * The `methods` property provides the list of all mocks.
+ */
+class StubDOMHandler extends __DOMHandler {
+	constructor(throwingMethod, ErrorClass) {
+		super()
+		this.methods = []
+		DOMHandlerMethods.forEach((method) => {
+			const impl = jest.fn(
+				method === throwingMethod
+					? () => {
+							throw new (ErrorClass || ParseError)(
+								`StubDOMHandler throwing in ${throwingMethod}`
+							)
+					  }
+					: noop
+			)
+			impl.mockName(method)
+			this[method] = impl
+			this.methods.push(impl)
+		})
+	}
+}
+/**
+ * This sample is triggering all method calls from XMLReader to DOMHandler at least once.
+ * This is verified in a test.
+ *
+ * There is of course no guarantee that it triggers all the places where XMLReader calls DOMHandler.
+ * For example not all possible warning and error cases are present in this file,
+ * but some, so that the methods are triggered.
+ * For testing all the cases of the different error levels,
+ * there are samples per case in
+ * @see REPORTED
+ */
+const ALL_METHODS = `<?xml ?>
+<!DOCTYPE name >
+<![CDATA[ raw ]]>
+<root xmlns="namespace">
+  <!-- -->
+  <element xmlns:x="http://test" x:a="" warning>
+    character
+  </element>
+  <element duplicate="" duplicate="fatal"></mismatch>
+</root>
+<!--
+`
+
+describe('methods called in DOMHandler', () => {
+	it('should call "all possible" methods when using StubDOMHandler', () => {
+		const domBuilder = new StubDOMHandler()
+		const parser = new DOMParser({ domBuilder, locator: {} })
+		expect(domBuilder.methods).toHaveLength(DOMHandlerMethods.length)
+
+		parser.parseFromString(ALL_METHODS)
+
+		const uncalledMethodNames = domBuilder.methods
+			.filter((m) => m.mock.calls.length === 0)
+			.map((m) => m.getMockName())
+		expect(uncalledMethodNames).toEqual([...UNCALLED_METHODS.values()].sort())
+	})
+	describe.each(DOMHandlerMethods.filter((m) => !UNCALLED_METHODS.has(m)))(
+		'when DOMHandler.%s throws',
+		(throwing) => {
+			it('should not catch ParserError', () => {
+				const domBuilder = new StubDOMHandler(throwing, ParseError)
+				const parser = new DOMParser({ domBuilder, locator: {} })
+
+				expect(() => parser.parseFromString(ALL_METHODS)).toThrow(ParseError)
+			})
+			const isUncaughtMethod = UNCAUGHT_METHODS.has(throwing)
+			it(`${
+				isUncaughtMethod ? 'does not' : 'should'
+			} catch other Error`, () => {
+				const domBuilder = new StubDOMHandler(throwing, Error)
+				const parser = new DOMParser({ domBuilder, locator: {} })
+
+				if (isUncaughtMethod) {
+					expect(() => parser.parseFromString(ALL_METHODS)).toThrow()
+				} else {
+					expect(() => parser.parseFromString(ALL_METHODS)).not.toThrow()
+				}
+			})
+		}
+	)
+})
diff --git a/test/html/__snapshots__/normalize.test.js.snap b/test/html/__snapshots__/normalize.test.js.snap
index 510e3a9..6835182 100644
--- a/test/html/__snapshots__/normalize.test.js.snap
+++ b/test/html/__snapshots__/normalize.test.js.snap
@@ -11,10 +11,14 @@ Object {
 `;
 
 exports[`html normalizer <div a=& a="&''" b/> 1`] = `
 Object {
   "actual": "<div a=\\"&amp;''\\" b=\\"b\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></div>",
+  "fatalError": Array [
+    "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+  ],
   "warning": Array [
     "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
 @#[line:1,col:1]",
     "[xmldom warning]	attribute \\"b\\" missed value!! \\"b\\" instead!!
 @#[line:1,col:1]",
@@ -30,11 +34,11 @@ Object {
 @#[line:1,col:1]",
     "[xmldom warning]	attribute \\"bb\\" missed value!! \\"bb\\" instead2!!
 @#[line:1,col:1]",
     "[xmldom warning]	attribute \\"c\\" missed value!! \\"c\\" instead2!!
 @#[line:1,col:1]",
-    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
@@ -176,21 +180,21 @@ Object {
 
 exports[`html normalizer unclosed html <html title = 1/> 1`] = `
 Object {
   "actual": "<html title=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
 exports[`html normalizer unclosed html <html title =1/2></html> 1`] = `
 Object {
   "actual": "<html title=\\"1/2\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
@@ -206,11 +210,11 @@ Object {
 
 exports[`html normalizer unclosed html <html title= 1/> 1`] = `
 Object {
   "actual": "<html title=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
diff --git a/test/parse/doctype.test.js b/test/parse/doctype.test.js
new file mode 100644
index 0000000..70d81f4
--- /dev/null
+++ b/test/parse/doctype.test.js
@@ -0,0 +1,27 @@
+'use strict'
+
+const { getTestParser } = require('../get-test-parser')
+describe('doctype', () => {
+	describe.each(['SYSTEM', 'PUBLIC'])('%s', (idType) => {
+		test.each([
+			['outer single', `<!DOCTYPE x ${idType} '\"'><X/>`, "'\"'"],
+			['outer double', `<!DOCTYPE x ${idType} "\'"><X/>`, '"\'"'],
+		])(
+			'should parse single line DOCTYPE with mixed quotes (%s)',
+			(_, source, idValue) => {
+				const { errors, parser } = getTestParser()
+
+				const actual = parser.parseFromString(source).firstChild
+
+				expect({
+					[idType]: idType === 'SYSTEM' ? actual.systemId : actual.publicId,
+					name: actual.name,
+					...errors,
+				}).toEqual({
+					[idType]: idValue,
+					name: 'x',
+				})
+			}
+		)
+	})
+})
diff --git a/test/parse/parse-error.test.js b/test/parse/parse-error.test.js
new file mode 100644
index 0000000..4b10cd7
--- /dev/null
+++ b/test/parse/parse-error.test.js
@@ -0,0 +1,38 @@
+'use strict'
+const { ParseError } = require('../../lib/sax')
+
+describe('ParseError', () => {
+	it('should have name ParseError', () => {
+		expect(new ParseError('').name).toBe('ParseError')
+	})
+	it('should be an instance of Error', () => {
+		expect(new ParseError('') instanceof Error).toBe(true)
+	})
+
+	it('should be an instance of ParseError', () => {
+		expect(new ParseError('') instanceof ParseError).toBe(true)
+	})
+
+	it('should store first argument as message', () => {
+		const error = new ParseError('FROM TEST')
+		expect(error.message).toBe('FROM TEST')
+	})
+
+	it('should store second argument as locator', () => {
+		const locator = {}
+		const error = new ParseError('', locator)
+		expect(error.locator).toBe(locator)
+	})
+
+	it('should have correct StackTrace', () => {
+		const error = new ParseError('MESSAGE')
+		const stack = error.stack && error.stack.split(/[\n\r]+/)
+		expect(stack && stack.length).toBeGreaterThan(1)
+		expect(stack[0]).toBe('ParseError: MESSAGE')
+		expect(stack[1]).toContain(__filename)
+	})
+
+	it('Error should not be instanceof ParseError', () => {
+		expect(new Error() instanceof ParseError).toBe(false)
+	})
+})
diff --git a/test/xmltest/__snapshots__/not-wf.test.js.snap b/test/xmltest/__snapshots__/not-wf.test.js.snap
index b01e929..400dd6e 100644
--- a/test/xmltest/__snapshots__/not-wf.test.js.snap
+++ b/test/xmltest/__snapshots__/not-wf.test.js.snap
@@ -92,11 +92,11 @@ Object {
 
 exports[`xmltest/not-wellformed standalone should match 012.xml with snapshot 1`] = `
 Object {
   "actual": "<doc a1=\\"v1\\"/>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
 `;
 
@@ -298,10 +298,14 @@ Object {
 `;
 
 exports[`xmltest/not-wellformed standalone should match 038.xml with snapshot 1`] = `
 Object {
   "actual": "<doc x=\\"baz\\" y=\\"bar\\"/>",
+  "fatalError": Array [
+    "[xmldom fatalError]	Attribute x redefined
+@#[line:1,col:1]",
+  ],
 }
 `;
 
 exports[`xmltest/not-wellformed standalone should match 039.xml with snapshot 1`] = `
 Object {

Vulnerable code: 		console.error('[xmldom fatalError]\t'+error,_locator(this.locator));
	    throw error;
var XMLReader = require('./sax').XMLReader;
			buf.push(' PUBLIC "',pubid);
				buf.push( '" "',sysid);
			buf.push('">');
			buf.push(' SYSTEM "',sysid,'">');
		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName );
			//errorHandler.error('element parse error: '+e);
			//throw e;
				throw new Error('attribute equal must after attrName');
					el.add(attrName,value,start-1);
				el.add(attrName,value,start);
				throw new Error('attribute value must after "="');
				throw new Error("attribute invalid close char('/')")
			//throw new Error('unexpected end of input')
					errorHandler.warning('attribute "'+value+'" missed quot(")!!');
					el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start)
					el.add(value,value,start)
					el.add(attrName,value,start)
					el.add(attrName,attrName,start);
			domBuilder.startDTD(name,pubid && pubid.replace(/^(['"])(.*?)\1$/,'$2'),
					sysid && sysid.replace(/^(['"])(.*?)\1$/,'$2'));
/**
 * @param source
 */
function ElementAttributes(source){
	
	add:function(qName,value,offset){

    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
exports[`html vs xml: unclosed document in text/xml 1`] = `
Object {
  "actual": "<img/>",
  "warning": Array [
    "[xmldom warning]	unclosed xml attribute
@#[line:1,col:1]",
  ],
}
`;

exports[`html vs xml: unclosed document in text/html 1`] = `
Object {
  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
}
`;
'use strict'

const { DOMParser } = require('../../lib/dom-parser')

const XML_ERROR = '<html><body title="1<2"><table&lt;;test</body></body></html>'
const XML_ERROR_AND_WARNING = '<html disabled><1 1="2"/></body></html>'

describe('errorHandler', () => {
	it('only single function with two args builds map', () => {
		const errors = {}
		const parser = new DOMParser({
			errorHandler: function (key, msg) {
				errors[key] = msg
			},
		})

		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')

		expect(errors).toMatchObject({
			warning: expect.stringMatching(/.*/),
			error: expect.stringMatching(/.*/),
		})
	})

	it('only one function with one argument builds list', () => {
		const errors = []
		const parser = new DOMParser({
			errorHandler: function (msg) {
				errors.push(msg)
			},
		})

		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')

		expect(errors).toMatchObject([/\[xmldom warning]/, /\[xmldom error]/])
	})

	it.each(['warning', 'error'])(
		'errorHandler for only one level: %s',
		(level) => {
			const errors = []
			const parser = new DOMParser({
				errorHandler: {
					[level]: function (msg) {
						errors.push(msg)
					},
				},
			})

			parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')

			expect(errors).toHaveLength(1)
		}
	)
	it.todo(
		'errorHandler for only one level: fatalError'
		/*
		I was not able to create a test case for a fatalError.
		It might need to be removed from the API, since all but one cases are in comments
		and an error is thrown instead. The one case left I was not able to reproduce.
		*/
	)

	it('error function throwing is not caught', () => {
		const errors = []
		const ERROR_MSG = 'FROM TEST'

		const parser = new DOMParser({
			locator: {}, // removing the locator makes the test fail!
			errorHandler: {
				error: function (msg) {
					errors.push(msg)
					throw new Error(ERROR_MSG)
				},
			},
		})

		expect(() => {
			parser.parseFromString(XML_ERROR, 'text/html')
		}).toThrow(ERROR_MSG)
		expect(errors).toMatchObject([/\n@#\[line\:\d+,col\:\d+\]/])
	})
})
    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!!
    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!!
    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!!
Fixed code: 		throw new ParseError(error, this.locator);
var sax = require('./sax');
var XMLReader = sax.XMLReader;
var ParseError = sax.ParseError;
exports.__DOMHandler = DOMHandler;
			buf.push(' PUBLIC ', pubid);
				buf.push(' ', sysid);
			buf.push('>');
			buf.push(' SYSTEM ', sysid, '>');
/**
 * Creates an error that will not be caught by XMLReader aka the SAX parser.
 *
 * @param {string} message
 * @param {any?} locator Optional, can provide details about the location in the source
 * @constructor
 */
function ParseError(message, locator) {
	this.message = message
	this.locator = locator
	if(Error.captureStackTrace) Error.captureStackTrace(this, ParseError);
}
ParseError.prototype = new Error();
ParseError.prototype.name = ParseError.name

		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName ); // No known test case
			if (e instanceof ParseError) {
				throw e;
			}

	/**
	 * @param {string} qname
	 * @param {string} value
	 * @param {number} startIndex
	 */
	function addAttribute(qname, value, startIndex) {
		if (qname in el.attributeNames) errorHandler.fatalError('Attribute ' + qname + ' redefined')
		el.addValue(qname, value, startIndex)
	}
				throw new Error('attribute equal must after attrName'); // No known test case
					addAttribute(attrName, value, start-1);
				addAttribute(attrName, value, start);
				throw new Error('attribute value must after "="'); // No known test case
				throw new Error("attribute invalid close char('/')") // No known test case
					errorHandler.warning('attribute "'+value+'" missed quot(")!');
					addAttribute(attrName, value.replace(/&#?\w+;/g,entityReplacer), start)
					addAttribute(value, value, start)
					addAttribute(attrName, value, start)
					addAttribute(attrName, attrName, start);
			domBuilder.startDTD(name, pubid, sysid);
function ElementAttributes(){
	this.attributeNames = {}
	addValue:function(qName, value, offset) {
		this.attributeNames[qName] = this.length;
exports.ParseError = ParseError;
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value missed!!
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value missed!!
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
@#[line:1,col:6]",
]
`;

exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
@#[line:1,col:6]",
]
`;

exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	end tag name: inner maybe not complete
@#[line:1,col:6]",
]
`;

exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
    at parse (lib/sax.js:128)",
  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	end tag name: inner maybe not complete
@#[line:1,col:6]",
]
`;

exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
    at parse (lib/sax.js:128)",
  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_EndTagNotComplete with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	end tag name: xml is not complete:xml
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_EndTagNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
    at parse (lib/sax.js:124)",
  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	end tag name: xml is not complete:xml
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
    at parse (lib/sax.js:124)",
  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_InvalidAttributeName with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid attribute:123
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_InvalidAttributeName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid attribute:123
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_InvalidTagName with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid tagName:123
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_InvalidTagName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_InvalidTagName with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid tagName:123
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_InvalidTagName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_UnclosedComment with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	Unclosed comment
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_UnclosedComment with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
    at parseDCC (lib/sax.js:538)",
  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_UnclosedComment with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	Unclosed comment
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_UnclosedComment with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
    at parseDCC (lib/sax.js:538)",
  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	unexpected end of input
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:308)",
  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	unexpected end of input
@#[line:1,col:1]",
]
`;

exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:308)",
  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`WF_AttributeMissingQuote with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:333)",
]
`;

exports[`WF_AttributeMissingQuote with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:333)",
]
`;

exports[`WF_AttributeMissingQuote2 with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingQuote2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:363)",
]
`;

exports[`WF_AttributeMissingQuote2 with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingQuote2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:363)",
]
`;

exports[`WF_AttributeMissingStartingQuote with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingStartingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:281)",
]
`;

exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:281)",
]
`;

exports[`WF_AttributeMissingValue with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingValue with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:337)",
]
`;

exports[`WF_AttributeMissingValue with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingValue with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:337)",
]
`;

exports[`WF_AttributeMissingValue2 with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
@#[line:1,col:1]",
  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingValue2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:385)",
]
`;

exports[`WF_AttributeMissingValue2 with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
@#[line:1,col:1]",
  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeMissingValue2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:385)",
]
`;

exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute value must after \\"=\\"
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:263)",
]
`;

exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	attribute value must after \\"=\\"
@#[line:1,col:1]",
]
`;

exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
    at parseElementStartPart (lib/sax.js:263)",
]
`;

exports[`WF_DuplicateAttribute with mimeType text/html should be reported as fatalError 1`] = `
Array [
  "[xmldom fatalError]	Attribute a redefined
@#[line:1,col:1]",
]
`;

exports[`WF_DuplicateAttribute with mimeType text/xml should be reported as fatalError 1`] = `
Array [
  "[xmldom fatalError]	Attribute a redefined
@#[line:1,col:1]",
]
`;

exports[`WF_EntityDeclared with mimeType text/html should be reported as error 1`] = `
Array [
  "[xmldom error]	entity not found:&e;
@#[line:1,col:1]",
]
`;

exports[`WF_EntityDeclared with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
    at replace (lib/sax.js:71)",
  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`WF_EntityDeclared with mimeType text/xml should be reported as error 1`] = `
Array [
  "[xmldom error]	entity not found:&e;
@#[line:1,col:1]",
]
`;

exports[`WF_EntityDeclared with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
Array [
  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
    at replace (lib/sax.js:71)",
  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
    at parse (lib/sax.js:208)",
]
`;

exports[`WF_UnclosedXmlAttribute with mimeType text/xml should be reported as warning 1`] = `
Array [
  "[xmldom warning]	unclosed xml attribute
@#[line:1,col:1]",
]
`;

exports[`WF_UnclosedXmlAttribute with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
Array [
  "[xmldom warning]	unclosed xml attribute||@#[line:1,col:1]
    at parse (lib/sax.js:173)",
]
`;
    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!

exports[`html vs xml: unclosed document in text/html 1`] = `
Object {
  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
}
`;

exports[`html vs xml: unclosed document in text/xml 1`] = `
Object {
  "actual": "<img/>",
  "warning": Array [
    "[xmldom warning]	unclosed xml attribute
@#[line:1,col:1]",
  ],
}
`;
'use strict'

const { DOMParser } = require('../../lib/dom-parser')
const { REPORTED } = require('./reported')

describe('custom errorHandler', () => {
	it('function with two args receives key and message', () => {
		const errors = {}
		const parser = new DOMParser({
			// currently needs to be a `function` to make the test work,
			// `jest.fn()` or using `() => {}` doesn't work
			errorHandler: function (key, msg) {
				errors[key] = msg
			},
		})

		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
		expect(errors).toHaveProperty('warning')
		parser.parseFromString(
			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
			'text/xml'
		)
		expect(errors).toHaveProperty('error')
		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')
		expect(errors).toHaveProperty('fatalError')
	})

	it('function with one argument builds list', () => {
		const errors = []
		const parser = new DOMParser({
			// currently needs to be a `function` to make the test work,
			// `jest.fn()` or using `() => {}` doesn't work
			errorHandler: function (msg) {
				errors.push(msg)
			},
		})

		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
		parser.parseFromString(
			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
			'text/xml'
		)
		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')

		expect(errors).toMatchObject([
			/\[xmldom warning]/,
			/\[xmldom error]/,
			/\[xmldom fatalError]/,
		])
	})
})
'use strict'

const { REPORTED } = require('./reported')
const { getTestParser } = require('../get-test-parser')
const { ParseError } = require('../../lib/sax')
const { DOMParser } = require('../../lib/dom-parser')

describe.each(Object.entries(REPORTED))(
	'%s',
	(name, { source, level, match, skippedInHtml }) => {
		describe.each(['text/xml', 'text/html'])('with mimeType %s', (mimeType) => {
			const isHtml = mimeType === 'text/html'
			if (isHtml && skippedInHtml) {
				it(`should not be reported as ${level}`, () => {
					const { errors, parser } = getTestParser()

					parser.parseFromString(source, mimeType)

					// if no report was triggered, the key is not present on `errors`
					expect(errors[level]).toBeUndefined()
				})
			} else {
				it(`should be reported as ${level}`, () => {
					const { errors, parser } = getTestParser()

					parser.parseFromString(source, mimeType)

					const reported = errors[level]
					// store the snapshot, so any change in message can be inspected in the git diff
					expect(reported).toMatchSnapshot()
					// if a match has been defined, filter messages
					expect(
						match ? (reported || []).filter(match) : reported
					).toHaveLength(1)
				})
				if (level === 'fatalError') {
					it(`should throw ParseError in errorHandler.fatalError`, () => {
						const parser = new DOMParser()

						expect(() => parser.parseFromString(source, mimeType)).toThrow(
							ParseError
						)
					})
				} else if (level === 'error') {
					it(`should not catch Error thrown in errorHandler.${level}`, () => {
						let thrown = []
						const errorHandler = {
							[level]: jest.fn((message) => {
								const toThrow = new Error(message)
								thrown.push(toThrow)
								throw toThrow
							}),
						}
						const { parser } = getTestParser({ errorHandler })

						expect(() => parser.parseFromString(source, mimeType)).toThrow(
							Error
						)
						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
						match && expect(match(thrown[0].toString())).toBe(true)
					})
				} else if (level === 'warning') {
					it('should escalate Error thrown in errorHandler.warning to errorHandler.error', () => {
						let thrown = []
						const errorHandler = {
							warning: jest.fn((message) => {
								const toThrow = new Error(message)
								thrown.push(toThrow)
								throw toThrow
							}),
							error: jest.fn(),
						}
						const { parser } = getTestParser({ errorHandler })

						parser.parseFromString(source, mimeType)

						expect(errorHandler.warning).toHaveBeenCalledTimes(1)
						expect(errorHandler.error).toHaveBeenCalledTimes(1)
						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
						match && expect(match(thrown[0].message)).toBe(true)
					})
				}
			}
		})
	}
)

/**
 * Creates a string from an error that is easily readable in a snapshot
 * - put's the message on one line as first line
 * - picks the first line in the stack trace that is in `lib/sax.js`,
 *   and strips absolute paths and character position from that stack entry
 *   as second line
 * @param {Error} error
 */
function toErrorSnapshot(error) {
	const libSaxMatch = /\/.*\/(lib\/sax\.js)/
	return `${error.message.replace(/([\n\r]+\s*)/g, '||')}\n${error.stack
		.split(/[\n\r]+/)
		// find first line that is from lib/sax.js
		.filter((l) => libSaxMatch.test(l))[0]
		// strip of absolute path
		.replace(libSaxMatch, '$1')
		// strip of position of character in line
		.replace(/:\d+\)$/, ')')}`
}
'use strict'

/**
 * @typedef ErrorReport
 * @property {string} source the XML snippet
 * @property {'error'|'warning'|'fatalError'} level the name of the method triggered
 * @property {?function(msg:string):boolean} match to pick the relevant report when there are multiple
 * @property {?boolean} skippedInHtml Is the error reported when parsing HTML?
 */
/**
 * A collection of XML samples and related information that cause the XMLReader
 * to call methods on `errorHandler`.
 *
 * @type {Record<string, ErrorReport>}}
 */
const REPORTED = {
	/**
	 * Entities need to be in the entityMap to be converted as part of parsing.
	 * xmldom currently doesn't parse entities declared in DTD.
	 *
	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#wf-entdeclared
	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#wf-entdeclared
	 */
	WF_EntityDeclared: {
		source: '<xml>&e;</xml>',
		level: 'error',
		match: (msg) => /entity not found/.test(msg),
	},
	/**
	 * Well-formedness constraint: Unique Att Spec
	 *
	 * An attribute name must not appear more than once
	 * in the same start-tag or empty-element tag.
	 *
	 * In the browser:
	 * - as XML it is reported as `error on line 1 at column 17: Attribute a redefined`
	 * - as HTML only the first definition is considered
	 *
	 * In xmldom the behavior is different for namespaces (picks first)
	 * than for other attributes (picks last),
	 * which can be a security issue.
	 *
	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#uniqattspec
	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#uniqattspec
	 */
	WF_DuplicateAttribute: {
		source: '<xml a="1" a="2"></xml>',
		level: 'fatalError',
		match: (msg) => /Attribute .* redefined/.test(msg),
	},
	/**
	 * This sample doesn't follow the specified grammar.
	 * In the browser it is reported as `error on line 1 at column 14: expected '>'`,
	 * but still adds the root level element to the dom.
	 */
	SYNTAX_EndTagNotComplete: {
		source: '<xml></xml',
		level: 'error',
		match: (msg) => /end tag name/.test(msg) && /is not complete/.test(msg),
	},
	/**
	 * This sample doesn't follow the specified grammar.
	 * In the browser it is reported as `error on line 1 at column 21: expected '>'`,
	 * but still adds the root level element and inner tag to the dom.
	 */
	SYNTAX_EndTagMaybeNotComplete: {
		source: '<xml><inner></inner </xml>',
		level: 'error',
		match: (msg) => /end tag name/.test(msg) && /maybe not complete/.test(msg),
	},
	/**
	 * This sample doesn't follow the specified grammar.
	 * In the browser it is reported as `error on line 1 at column 6: Comment not terminated`.
	 */
	SYNTAX_UnclosedComment: {
		source: '<!--',
		level: 'error',
		match: (msg) => /Unclosed comment/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:596, caught in 208
	 * This sample doesn't follow the specified grammar.
	 * In the browser:
	 * - as XML it is reported as
	 * `error on line 1 at column 2: StartTag: invalid element name`
	 * - as HTML it is accepted as characters
	 *
	 */
	SYNTAX_InvalidTagName: {
		source: '<123 />',
		level: 'error',
		match: (msg) => /invalid tagName/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:602, caught in 208
	 * This sample doesn't follow the specified grammar.
	 * In the browser:
	 * - as XML it is reported as
	 * `error on line 1 at column 6: error parsing attribute name`
	 * - as HTML it is accepted as attribute name
	 */
	SYNTAX_InvalidAttributeName: {
		source: '<xml 123=""/>',
		level: 'error',
		match: (msg) => /invalid attribute/.test(msg),
	},
	/**
	 * This sample doesn't follow the specified grammar.
	 * In the browser it is reported as `error on line 1 at column 5: Couldn't find end of Start Tag xml`.
	 */
	SYNTAX_UnexpectedEndOfInput: {
		source: '<xml',
		level: 'error',
		match: (msg) => /unexpected end of input/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:392, caught in 208
	 * This sample doesn't follow the specified grammar.
	 * In the browser:
	 * - in XML it is reported as `error on line 1 at column 8: error parsing attribute name`
	 * - in HTML it produces `<xml><a <="" xml=""></a></xml>` (invalid XML?)
	 */
	SYNTAX_ElementClosingNotConnected: {
		source: '<xml><a/ </xml>',
		level: 'error',
		match: (msg) => /must be connected/.test(msg),
	},
	/**
	 * In the Browser (for XML) this is reported as
	 * `error on line 1 at column 6: Extra content at the end of the document`
	 * for HTML it's added to the DOM without anything being reported.
	 */
	WF_UnclosedXmlAttribute: {
		source: '<xml>',
		level: 'warning',
		skippedInHtml: true,
		match: (msg) => /unclosed xml attribute/.test(msg),
	},
	/**
	 * In the browser:
	 * - for XML it is reported as
	 * `error on line 1 at column 10: Specification mandates value for attribute attr`
	 * - for HTML is uses the attribute as one with no value and adds `"value"` to the attribute name
	 *   and is not reporting any issue.
	 */
	WF_AttributeValueMustAfterEqual: {
		source: '<xml attr"value" />',
		level: 'warning',
		match: (msg) => /attribute value must after "="/.test(msg),
	},
	/**
	 * In the browser:
	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
	 * - for HTML is wraps `value"` with quotes and is not reporting any issue.
	 */
	WF_AttributeMissingStartingQuote: {
		source: '<xml attr=value" />',
		level: 'warning',
		match: (msg) => /missed start quot/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:264, caught in 208.
	 * TODO: Comment indicates fatalError, change to use errorHandler.fatalError?
	 *
	 * In the browser:
	 * - for XML it is reported as `error on line 1 at column 20: AttValue: ' expected`
	 * - for HTML nothing is added to the DOM.
	 */
	SYNTAX_AttributeMissingEndingQuote: {
		source: '<xml attr="value />',
		level: 'error',
		match: (msg) => /attribute value no end .* match/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:324
	 * In the browser:
	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
	 * - for HTML is wraps `value/` with quotes and is not reporting any issue.
	 */
	WF_AttributeMissingQuote: {
		source: '<xml attr=value/>',
		level: 'warning',
		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg) === false,
	},
	/**
	 * Triggered by lib/sax.js:354
	 * This is the only warning reported in this sample.
	 * For some reason the "attribute" that is reported as missing quotes
	 * has the name `&`.
	 * This case is also present in 2 tests in test/html/normalize.test.js
	 *
	 * In the browser:
	 * - for XML it is reported as `error on line 1 at column 8: AttValue: " or ' expected`
	 * - for HTML is yields `<xml a="&amp;" b="&amp;"></xml>` and is not reporting any issue.
	 */
	WF_AttributeMissingQuote2: {
		source: `<xml a=& b="&"/>`,
		level: 'warning',
		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg),
	},
	/**
	 * In the browser:
	 * - for XML it is reported as `error on line 1 at column 9: AttValue: " or ' expected`
	 * - for HTML is yields `<doc a1></xml>` and is not reporting any issue.
	 *
	 * But the XML specifications does not allow that:
	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
	 */
	SYNTAX_AttributeEqualMissingValue: {
		source: '<doc a1=></doc>',
		level: 'error',
		match: (msg) => /attribute value missed!!/.test(msg),
	},
	/**
	 * In the browser this is not an issue at all, but just add an attribute without a value.
	 * But the XML specifications does not allow that:
	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
	 */
	WF_AttributeMissingValue: {
		source: '<xml attr ></xml>',
		level: 'warning',
		match: (msg) => /missed value/.test(msg) && /instead!!/.test(msg),
	},
	/**
	 * Triggered by lib/sax.js:376
	 * This seems to only be reached when there are two subsequent attributes with a missing value
	 * In the browser this is not an issue at all, but just add an attribute without a value.
	 * But the XML specifications does not allow that:
	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
	 */
	WF_AttributeMissingValue2: {
		source: '<xml attr attr2 ></xml>',
		level: 'warning',
		match: (msg) => /missed value/.test(msg) && /instead2!!/.test(msg),
	},
}

module.exports = {
	REPORTED,
}
'use strict'

const { ParseError } = require('../../lib/sax')
const { __DOMHandler, DOMParser } = require('../../lib/dom-parser')

/**
 * All methods implemented on the DOMHandler prototype.
 *
 * @type {string[]}
 */
const DOMHandlerMethods = Object.keys(__DOMHandler.prototype).sort()

/**
 * XMLReader is currently not calling all methods "implemented" by DOMHandler (some are just empty),
 * If this changes the first test will fail.
 *
 * @type {Set<string>}
 */
const UNCALLED_METHODS = new Set([
	'attributeDecl',
	'elementDecl',
	'endEntity',
	'externalEntityDecl',
	'getExternalSubset',
	'ignorableWhitespace',
	'internalEntityDecl',
	'notationDecl',
	'resolveEntity',
	'skippedEntity',
	'startEntity',
	'unparsedEntityDecl',
])

/**
 * Some of the methods DOMParser/XMLReader call during parsing are not guarded by try/catch,
 * hence an error happening in those will stop the parsing process.
 * There is a test to verify this error handling.
 * If it changes this list might need to be changed as well
 *
 * @type {Set<string>}
 */
const UNCAUGHT_METHODS = new Set([
	'characters',
	'endDocument',
	'error',
	'setDocumentLocator',
	'startDocument',
])

function noop() {}

/**
 * A subclass of DOMHandler that mocks all methods for later inspection.
 * As part of the constructor it can be told which method is supposed to throw an error
 * and which error constructor to use.
 *
 * The `methods` property provides the list of all mocks.
 */
class StubDOMHandler extends __DOMHandler {
	constructor(throwingMethod, ErrorClass) {
		super()
		this.methods = []
		DOMHandlerMethods.forEach((method) => {
			const impl = jest.fn(
				method === throwingMethod
					? () => {
							throw new (ErrorClass || ParseError)(
								`StubDOMHandler throwing in ${throwingMethod}`
							)
					  }
					: noop
			)
			impl.mockName(method)
			this[method] = impl
			this.methods.push(impl)
		})
	}
}
/**
 * This sample is triggering all method calls from XMLReader to DOMHandler at least once.
 * This is verified in a test.
 *
 * There is of course no guarantee that it triggers all the places where XMLReader calls DOMHandler.
 * For example not all possible warning and error cases are present in this file,
 * but some, so that the methods are triggered.
 * For testing all the cases of the different error levels,
 * there are samples per case in
 * @see REPORTED
 */
const ALL_METHODS = `<?xml ?>
<!DOCTYPE name >
<![CDATA[ raw ]]>
<root xmlns="namespace">
  <!-- -->
  <element xmlns:x="http://test" x:a="" warning>
    character
  </element>
  <element duplicate="" duplicate="fatal"></mismatch>
</root>
<!--
`

describe('methods called in DOMHandler', () => {
	it('should call "all possible" methods when using StubDOMHandler', () => {
		const domBuilder = new StubDOMHandler()
		const parser = new DOMParser({ domBuilder, locator: {} })
		expect(domBuilder.methods).toHaveLength(DOMHandlerMethods.length)

		parser.parseFromString(ALL_METHODS)

		const uncalledMethodNames = domBuilder.methods
			.filter((m) => m.mock.calls.length === 0)
			.map((m) => m.getMockName())
		expect(uncalledMethodNames).toEqual([...UNCALLED_METHODS.values()].sort())
	})
	describe.each(DOMHandlerMethods.filter((m) => !UNCALLED_METHODS.has(m)))(
		'when DOMHandler.%s throws',
		(throwing) => {
			it('should not catch ParserError', () => {
				const domBuilder = new StubDOMHandler(throwing, ParseError)
				const parser = new DOMParser({ domBuilder, locator: {} })

				expect(() => parser.parseFromString(ALL_METHODS)).toThrow(ParseError)
			})
			const isUncaughtMethod = UNCAUGHT_METHODS.has(throwing)
			it(`${
				isUncaughtMethod ? 'does not' : 'should'
			} catch other Error`, () => {
				const domBuilder = new StubDOMHandler(throwing, Error)
				const parser = new DOMParser({ domBuilder, locator: {} })

				if (isUncaughtMethod) {
					expect(() => parser.parseFromString(ALL_METHODS)).toThrow()
				} else {
					expect(() => parser.parseFromString(ALL_METHODS)).not.toThrow()
				}
			})
		}
	)
})
  "fatalError": Array [
    "[xmldom fatalError]	Attribute a redefined
@#[line:1,col:1]",
  ],
    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!
    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!
    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
'use strict'

const { getTestParser } = require('../get-test-parser')
describe('doctype', () => {
	describe.each(['SYSTEM', 'PUBLIC'])('%s', (idType) => {
		test.each([
			['outer single', `<!DOCTYPE x ${idType} '\"'><X/>`, "'\"'"],
			['outer double', `<!DOCTYPE x ${idType} "\'"><X/>`, '"\'"'],
		])(
			'should parse single line DOCTYPE with mixed quotes (%s)',
			(_, source, idValue) => {
				const { errors, parser } = getTestParser()

				const actual = parser.parseFromString(source).firstChild

				expect({
					[idType]: idType === 'SYSTEM' ? actual.systemId : actual.publicId,
					name: actual.name,
					...errors,
				}).toEqual({
					[idType]: idValue,
					name: 'x',
				})
			}
		)
	})
})
'use strict'
const { ParseError } = require('../../lib/sax')

describe('ParseError', () => {
	it('should have name ParseError', () => {
		expect(new ParseError('').name).toBe('ParseError')
	})
	it('should be an instance of Error', () => {
		expect(new ParseError('') instanceof Error).toBe(true)
	})

	it('should be an instance of ParseError', () => {
		expect(new ParseError('') instanceof ParseError).toBe(true)
	})

	it('should store first argument as message', () => {
		const error = new ParseError('FROM TEST')
		expect(error.message).toBe('FROM TEST')
	})

	it('should store second argument as locator', () => {
		const locator = {}
		const error = new ParseError('', locator)
		expect(error.locator).toBe(locator)
	})

	it('should have correct StackTrace', () => {
		const error = new ParseError('MESSAGE')
		const stack = error.stack && error.stack.split(/[\n\r]+/)
		expect(stack && stack.length).toBeGreaterThan(1)
		expect(stack[0]).toBe('ParseError: MESSAGE')
		expect(stack[1]).toContain(__filename)
	})

	it('Error should not be instanceof ParseError', () => {
		expect(new Error() instanceof ParseError).toBe(false)
	})
})
    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!
  "fatalError": Array [
    "[xmldom fatalError]	Attribute x redefined
@#[line:1,col:1]",
  ],