diff --git a/cjs/interface/attr.js b/cjs/interface/attr.js index eabb8028..66c67e16 100644 --- a/cjs/interface/attr.js +++ b/cjs/interface/attr.js @@ -1,5 +1,5 @@ 'use strict'; -const {ATTRIBUTE_NODE} = require('../shared/constants.js'); +const {ATTRIBUTE_NODE, QUOTE} = require('../shared/constants.js'); const {CHANGED, VALUE} = require('../shared/symbols.js'); const {String} = require('../shared/utils.js'); const {attrAsJSON} = require('../shared/jsdon.js'); @@ -10,8 +10,6 @@ const {attributeChangedCallback: ceAttributes} = require('./custom-element-regis const {Node} = require('./node.js'); -const QUOTE = /"/g; - /** * @implements globalThis.Attr */ diff --git a/cjs/interface/document.js b/cjs/interface/document.js index 886f7766..3af7d83e 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..6b93dbb9 --- /dev/null +++ b/cjs/interface/xml-attr.js @@ -0,0 +1,21 @@ +'use strict'; +const {VALUE} = require('../shared/symbols.js'); +const {AMPERSAND, QUOTE, LT, GT} = require('../shared/constants.js'); +const {emptyAttributes} = require('../shared/attributes.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}="${value.replace(AMPERSAND, '&').replace(QUOTE, '"').replace(LT, '<').replace(GT, '>')}"`; + } +} +exports.XmlAttr = XmlAttr diff --git a/cjs/shared/constants.js b/cjs/shared/constants.js index 1acc691e..988a69ea 100644 --- a/cjs/shared/constants.js +++ b/cjs/shared/constants.js @@ -50,3 +50,13 @@ exports.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = DOCUMENT_POSITION_IMPLEMENTA // SVG const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; exports.SVG_NAMESPACE = SVG_NAMESPACE; + +// Characters +const QUOTE = /"/g; +exports.QUOTE = QUOTE; +const LT = //g; +exports.GT = GT; +const AMPERSAND = /&/g; +exports.AMPERSAND = AMPERSAND; diff --git a/cjs/shared/mime.js b/cjs/shared/mime.js index 1bb849d1..c6ef83b6 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/attr.js b/esm/interface/attr.js index 4cf1cc7d..6054612c 100644 --- a/esm/interface/attr.js +++ b/esm/interface/attr.js @@ -1,4 +1,4 @@ -import {ATTRIBUTE_NODE} from '../shared/constants.js'; +import {ATTRIBUTE_NODE,QUOTE} from '../shared/constants.js'; import {CHANGED, VALUE} from '../shared/symbols.js'; import {String} from '../shared/utils.js'; import {attrAsJSON} from '../shared/jsdon.js'; @@ -9,8 +9,6 @@ import {attributeChangedCallback as ceAttributes} from './custom-element-registr import {Node} from './node.js'; -const QUOTE = /"/g; - /** * @implements globalThis.Attr */ diff --git a/esm/interface/document.js b/esm/interface/document.js index 4e8b1c4a..94248541 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..94c1b0fd --- /dev/null +++ b/esm/interface/xml-attr.js @@ -0,0 +1,19 @@ +import {VALUE} from '../shared/symbols.js'; +import {AMPERSAND,QUOTE,LT,GT} from '../shared/constants.js'; +import {emptyAttributes} from '../shared/attributes.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}="${value.replace(AMPERSAND, '&').replace(QUOTE, '"').replace(LT, '<').replace(GT, '>')}"`; + } +} diff --git a/esm/shared/constants.js b/esm/shared/constants.js index 062e85bd..cb1ea426 100644 --- a/esm/shared/constants.js +++ b/esm/shared/constants.js @@ -29,3 +29,9 @@ export const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; // SVG export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + +// Characters +export const QUOTE = /"/g; +export const LT = //g; +export const AMPERSAND = /&/g; diff --git a/esm/shared/mime.js b/esm/shared/mime.js index 4390efe8..f8f42ff2 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..83fab2d0 --- /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/constants.d.ts b/types/esm/shared/constants.d.ts index 3d746eec..1d7ea6b1 100644 --- a/types/esm/shared/constants.d.ts +++ b/types/esm/shared/constants.d.ts @@ -18,3 +18,7 @@ export const DOCUMENT_POSITION_CONTAINS: 8; export const DOCUMENT_POSITION_CONTAINED_BY: 16; export const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32; export const SVG_NAMESPACE: "http://www.w3.org/2000/svg"; +export const QUOTE: RegExp; +export const LT: RegExp; +export const GT: RegExp; +export const AMPERSAND: RegExp; diff --git a/types/esm/shared/mime.d.ts b/types/esm/shared/mime.d.ts index fa7d537f..f760992d 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..f23f7cde 100644 --- a/worker.js +++ b/worker.js @@ -3476,6 +3476,12 @@ const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; // SVG const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; +// Characters +const QUOTE = /"/g; +const LT = //g; +const AMPERSAND = /&/g; + const { assign, create: create$1, @@ -4418,8 +4424,6 @@ let Node$1 = class Node extends DOMEventTarget { } }; -const QUOTE = /"/g; - /** * @implements globalThis.Attr */ @@ -11224,26 +11228,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 +11451,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}="${value.replace(AMPERSAND, '&').replace(QUOTE, '"').replace(LT, '<').replace(GT, '>')}"`; + } +} + const query = (method, ownerDocument, selectors) => { let {[NEXT]: next, [END]: end} = ownerDocument; return method.call({ownerDocument, [NEXT]: next, [END]: end}, selectors); @@ -11577,7 +11601,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); }