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); }