diff --git a/cjs/interface/document.js b/cjs/interface/document.js
index 886f7766..e37113bb 100644
--- a/cjs/interface/document.js
+++ b/cjs/interface/document.js
@@ -34,6 +34,7 @@ const {NodeList} = require('./node-list.js');
const {Range} = require('./range.js');
const {Text} = require('./text.js');
const {TreeWalker} = require('./tree-walker.js');
+const {XMLAttr} = require('./xml-attr.js');
const query = (method, ownerDocument, selectors) => {
let {[NEXT]: next, [END]: end} = ownerDocument;
@@ -170,7 +171,7 @@ class Document extends NonElementParentNode {
return this[EVENT_TARGET];
}
- createAttribute(name) { return new Attr(this, name); }
+ createAttribute(name) { return this[MIME].isXML ? new XMLAttr(this, name) : new Attr(this, name); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
createDocumentType(name, publicId, systemId) { return new DocumentType(this, name, publicId, systemId); }
diff --git a/cjs/interface/xml-attr.js b/cjs/interface/xml-attr.js
new file mode 100644
index 00000000..6c2c975e
--- /dev/null
+++ b/cjs/interface/xml-attr.js
@@ -0,0 +1,21 @@
+'use strict';
+const {VALUE} = require('../shared/symbols.js');
+const {emptyAttributes} = require('../shared/attributes.js');
+const {escape} = require('../shared/text-escaper.js');
+const {Attr} = require('./attr.js');
+
+/**
+ * @implements globalThis.Attr
+ */
+class XMLAttr extends Attr {
+ constructor(ownerDocument, name, value = '') {
+ super(ownerDocument, name, value);
+ }
+
+ toString() {
+ const {name, [VALUE]: value} = this;
+ return emptyAttributes.has(name) && !value ?
+ name : `${name}="${escape(value)}"`;
+ }
+}
+exports.XMLAttr = XMLAttr
diff --git a/cjs/shared/mime.js b/cjs/shared/mime.js
index 1bb849d1..fac812c5 100644
--- a/cjs/shared/mime.js
+++ b/cjs/shared/mime.js
@@ -7,26 +7,31 @@ const Mime = {
'text/html': {
docType: '',
ignoreCase: true,
+ isXML: false,
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
},
'image/svg+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'text/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xhtml+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
}
};
diff --git a/esm/interface/document.js b/esm/interface/document.js
index 4e8b1c4a..259f946a 100644
--- a/esm/interface/document.js
+++ b/esm/interface/document.js
@@ -34,6 +34,7 @@ import {NodeList} from './node-list.js';
import {Range} from './range.js';
import {Text} from './text.js';
import {TreeWalker} from './tree-walker.js';
+import {XMLAttr} from './xml-attr.js';
const query = (method, ownerDocument, selectors) => {
let {[NEXT]: next, [END]: end} = ownerDocument;
@@ -170,7 +171,7 @@ export class Document extends NonElementParentNode {
return this[EVENT_TARGET];
}
- createAttribute(name) { return new Attr(this, name); }
+ createAttribute(name) { return this[MIME].isXML ? new XMLAttr(this, name) : new Attr(this, name); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
createDocumentType(name, publicId, systemId) { return new DocumentType(this, name, publicId, systemId); }
diff --git a/esm/interface/xml-attr.js b/esm/interface/xml-attr.js
new file mode 100644
index 00000000..0eae8798
--- /dev/null
+++ b/esm/interface/xml-attr.js
@@ -0,0 +1,19 @@
+import {VALUE} from '../shared/symbols.js';
+import {emptyAttributes} from '../shared/attributes.js';
+import {escape} from '../shared/text-escaper.js';
+import {Attr} from './attr.js';
+
+/**
+ * @implements globalThis.Attr
+ */
+export class XMLAttr extends Attr {
+ constructor(ownerDocument, name, value = '') {
+ super(ownerDocument, name, value);
+ }
+
+ toString() {
+ const {name, [VALUE]: value} = this;
+ return emptyAttributes.has(name) && !value ?
+ name : `${name}="${escape(value)}"`;
+ }
+}
diff --git a/esm/shared/mime.js b/esm/shared/mime.js
index 4390efe8..125d5513 100644
--- a/esm/shared/mime.js
+++ b/esm/shared/mime.js
@@ -6,26 +6,31 @@ export const Mime = {
'text/html': {
docType: '',
ignoreCase: true,
+ isXML: false,
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
},
'image/svg+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'text/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xhtml+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
}
};
diff --git a/test/xml/document.js b/test/xml/document.js
index deaa763c..7898f85b 100644
--- a/test/xml/document.js
+++ b/test/xml/document.js
@@ -2,20 +2,28 @@ const assert = require('../assert.js').for('XMLDocument');
const {DOMParser} = global[Symbol.for('linkedom')];
-const document = (new DOMParser).parseFromString('', 'text/xml');
+{
+ const document = (new DOMParser).parseFromString('', 'text/xml');
-assert(document.toString(), '');
+ assert(document.toString(), '');;
-assert(document.documentElement.tagName, 'root');
-assert(document.documentElement.nodeName, 'root');
+ assert(document.documentElement.tagName, 'root');
+ assert(document.documentElement.nodeName, 'root');
-document.documentElement.innerHTML = `
+ document.documentElement.innerHTML = `
Text
Text
`.trim();
-assert(document.querySelectorAll('Element').length, 2, 'case sesntivive 2');
-assert(document.querySelectorAll('element').length, 0, 'case sesntivive 0');
+ assert(document.querySelectorAll('Element').length, 2, 'case sensitive 2');
+ assert(document.querySelectorAll('element').length, 0, 'case sensitive 0');
+}
+
+{
+ const document = (new DOMParser).parseFromString('', 'text/xml');
+ assert(document.toString(), '');
+}
+
diff --git a/types/esm/interface/xml-attr.d.ts b/types/esm/interface/xml-attr.d.ts
new file mode 100644
index 00000000..848eb25c
--- /dev/null
+++ b/types/esm/interface/xml-attr.d.ts
@@ -0,0 +1,6 @@
+/**
+ * @implements globalThis.Attr
+ */
+export class XMLAttr extends Attr implements globalThis.Attr {
+}
+import { Attr } from "./attr.js";
diff --git a/types/esm/shared/mime.d.ts b/types/esm/shared/mime.d.ts
index fa7d537f..a206d6e5 100644
--- a/types/esm/shared/mime.d.ts
+++ b/types/esm/shared/mime.d.ts
@@ -2,11 +2,13 @@ export const Mime: {
'text/html': {
docType: string;
ignoreCase: boolean;
+ isXML: boolean;
voidElements: RegExp;
};
'image/svg+xml': {
docType: string;
ignoreCase: boolean;
+ isXML: boolean;
voidElements: {
test: () => boolean;
};
@@ -14,6 +16,7 @@ export const Mime: {
'text/xml': {
docType: string;
ignoreCase: boolean;
+ isXML: boolean;
voidElements: {
test: () => boolean;
};
@@ -21,6 +24,7 @@ export const Mime: {
'application/xml': {
docType: string;
ignoreCase: boolean;
+ isXML: boolean;
voidElements: {
test: () => boolean;
};
@@ -28,6 +32,7 @@ export const Mime: {
'application/xhtml+xml': {
docType: string;
ignoreCase: boolean;
+ isXML: boolean;
voidElements: {
test: () => boolean;
};
diff --git a/worker.js b/worker.js
index 3aaaede6..fa4823ba 100644
--- a/worker.js
+++ b/worker.js
@@ -11224,26 +11224,31 @@ const Mime = {
'text/html': {
docType: '',
ignoreCase: true,
+ isXML: false,
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
},
'image/svg+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'text/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
},
'application/xhtml+xml': {
docType: '',
ignoreCase: false,
+ isXML: true,
voidElements
}
};
@@ -11442,6 +11447,21 @@ class TreeWalker {
}
}
+/**
+ * @implements globalThis.Attr
+ */
+class XMLAttr extends Attr$1 {
+ constructor(ownerDocument, name, value = '') {
+ super(ownerDocument, name, value);
+ }
+
+ toString() {
+ const {name, [VALUE]: value} = this;
+ return emptyAttributes.has(name) && !value ?
+ name : `${name}="${escape(value)}"`;
+ }
+}
+
const query = (method, ownerDocument, selectors) => {
let {[NEXT]: next, [END]: end} = ownerDocument;
return method.call({ownerDocument, [NEXT]: next, [END]: end}, selectors);
@@ -11577,7 +11597,7 @@ let Document$1 = class Document extends NonElementParentNode {
return this[EVENT_TARGET];
}
- createAttribute(name) { return new Attr$1(this, name); }
+ createAttribute(name) { return this[MIME].isXML ? new XMLAttr(this, name) : new Attr$1(this, name); }
createComment(textContent) { return new Comment$1(this, textContent); }
createDocumentFragment() { return new DocumentFragment$1(this); }
createDocumentType(name, publicId, systemId) { return new DocumentType$1(this, name, publicId, systemId); }