Skip to content

Commit 2a7512b

Browse files
Escape characters on xml attributes
1 parent ec5ce84 commit 2a7512b

File tree

10 files changed

+101
-10
lines changed

10 files changed

+101
-10
lines changed

cjs/interface/document.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const {NodeList} = require('./node-list.js');
3434
const {Range} = require('./range.js');
3535
const {Text} = require('./text.js');
3636
const {TreeWalker} = require('./tree-walker.js');
37+
const {XmlAttr} = require('./xml-attr.js');
3738

3839
const query = (method, ownerDocument, selectors) => {
3940
let {[NEXT]: next, [END]: end} = ownerDocument;
@@ -170,7 +171,7 @@ class Document extends NonElementParentNode {
170171
return this[EVENT_TARGET];
171172
}
172173

173-
createAttribute(name) { return new Attr(this, name); }
174+
createAttribute(name) { return this[MIME].isXML ? new XmlAttr(this, name) : new Attr(this, name); }
174175
createComment(textContent) { return new Comment(this, textContent); }
175176
createDocumentFragment() { return new DocumentFragment(this); }
176177
createDocumentType(name, publicId, systemId) { return new DocumentType(this, name, publicId, systemId); }

cjs/interface/xml-attr.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
const {VALUE} = require('../shared/symbols.js');
3+
const {emptyAttributes} = require('../shared/attributes.js');
4+
const {escape} = require('../shared/text-escaper.js');
5+
const {Attr} = require('./attr.js');
6+
7+
/**
8+
* @implements globalThis.Attr
9+
*/
10+
class XmlAttr extends Attr {
11+
constructor(ownerDocument, name, value = '') {
12+
super(ownerDocument, name, value);
13+
}
14+
15+
toString() {
16+
const {name, [VALUE]: value} = this;
17+
return emptyAttributes.has(name) && !value ?
18+
name : `${name}="${escape(value)}"`;
19+
}
20+
}
21+
exports.XmlAttr = XmlAttr

cjs/shared/mime.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,31 @@ const Mime = {
77
'text/html': {
88
docType: '<!DOCTYPE html>',
99
ignoreCase: true,
10+
isXML: false,
1011
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
1112
},
1213
'image/svg+xml': {
1314
docType: '<?xml version="1.0" encoding="utf-8"?>',
1415
ignoreCase: false,
16+
isXML: true,
1517
voidElements
1618
},
1719
'text/xml': {
1820
docType: '<?xml version="1.0" encoding="utf-8"?>',
1921
ignoreCase: false,
22+
isXML: true,
2023
voidElements
2124
},
2225
'application/xml': {
2326
docType: '<?xml version="1.0" encoding="utf-8"?>',
2427
ignoreCase: false,
28+
isXML: true,
2529
voidElements
2630
},
2731
'application/xhtml+xml': {
2832
docType: '<?xml version="1.0" encoding="utf-8"?>',
2933
ignoreCase: false,
34+
isXML: true,
3035
voidElements
3136
}
3237
};

esm/interface/document.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {NodeList} from './node-list.js';
3434
import {Range} from './range.js';
3535
import {Text} from './text.js';
3636
import {TreeWalker} from './tree-walker.js';
37+
import {XmlAttr} from './xml-attr.js';
3738

3839
const query = (method, ownerDocument, selectors) => {
3940
let {[NEXT]: next, [END]: end} = ownerDocument;
@@ -170,7 +171,7 @@ export class Document extends NonElementParentNode {
170171
return this[EVENT_TARGET];
171172
}
172173

173-
createAttribute(name) { return new Attr(this, name); }
174+
createAttribute(name) { return this[MIME].isXML ? new XmlAttr(this, name) : new Attr(this, name); }
174175
createComment(textContent) { return new Comment(this, textContent); }
175176
createDocumentFragment() { return new DocumentFragment(this); }
176177
createDocumentType(name, publicId, systemId) { return new DocumentType(this, name, publicId, systemId); }

esm/interface/xml-attr.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {VALUE} from '../shared/symbols.js';
2+
import {emptyAttributes} from '../shared/attributes.js';
3+
import {escape} from '../shared/text-escaper.js';
4+
import {Attr} from './attr.js';
5+
6+
/**
7+
* @implements globalThis.Attr
8+
*/
9+
export class XmlAttr extends Attr {
10+
constructor(ownerDocument, name, value = '') {
11+
super(ownerDocument, name, value);
12+
}
13+
14+
toString() {
15+
const {name, [VALUE]: value} = this;
16+
return emptyAttributes.has(name) && !value ?
17+
name : `${name}="${escape(value)}"`;
18+
}
19+
}

esm/shared/mime.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ export const Mime = {
66
'text/html': {
77
docType: '<!DOCTYPE html>',
88
ignoreCase: true,
9+
isXML: false,
910
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
1011
},
1112
'image/svg+xml': {
1213
docType: '<?xml version="1.0" encoding="utf-8"?>',
1314
ignoreCase: false,
15+
isXML: true,
1416
voidElements
1517
},
1618
'text/xml': {
1719
docType: '<?xml version="1.0" encoding="utf-8"?>',
1820
ignoreCase: false,
21+
isXML: true,
1922
voidElements
2023
},
2124
'application/xml': {
2225
docType: '<?xml version="1.0" encoding="utf-8"?>',
2326
ignoreCase: false,
27+
isXML: true,
2428
voidElements
2529
},
2630
'application/xhtml+xml': {
2731
docType: '<?xml version="1.0" encoding="utf-8"?>',
2832
ignoreCase: false,
33+
isXML: true,
2934
voidElements
3035
}
3136
};

test/xml/document.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@ const assert = require('../assert.js').for('XMLDocument');
22

33
const {DOMParser} = global[Symbol.for('linkedom')];
44

5-
const document = (new DOMParser).parseFromString('<root></root>', 'text/xml');
5+
{
6+
const document = (new DOMParser).parseFromString('<root></root>', 'text/xml');
67

7-
assert(document.toString(), '<?xml version="1.0" encoding="utf-8"?><root />');
8+
assert(document.toString(), '<?xml version="1.0" encoding="utf-8"?><root />');;
89

9-
assert(document.documentElement.tagName, 'root');
10-
assert(document.documentElement.nodeName, 'root');
10+
assert(document.documentElement.tagName, 'root');
11+
assert(document.documentElement.nodeName, 'root');
1112

1213

13-
document.documentElement.innerHTML = `
14+
document.documentElement.innerHTML = `
1415
<Something>
1516
<Element>Text</Element>
1617
<Element>Text</Element>
1718
</Something>
1819
`.trim();
1920

20-
assert(document.querySelectorAll('Element').length, 2, 'case sesntivive 2');
21-
assert(document.querySelectorAll('element').length, 0, 'case sesntivive 0');
21+
assert(document.querySelectorAll('Element').length, 2, 'case sensitive 2');
22+
assert(document.querySelectorAll('element').length, 0, 'case sensitive 0');
23+
}
24+
25+
{
26+
const document = (new DOMParser).parseFromString('<root checked attr="&amp;&lt;&gt;"></root>', 'text/xml');
27+
assert(document.toString(), '<?xml version="1.0" encoding="utf-8"?><root checked attr="&amp;&lt;&gt;" />');
28+
}
29+

types/esm/interface/xml-attr.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @implements globalThis.Attr
3+
*/
4+
export class XmlAttr extends Attr implements globalThis.Attr {
5+
}
6+
import { Attr } from "./attr.js";

types/esm/shared/mime.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,37 @@ export const Mime: {
22
'text/html': {
33
docType: string;
44
ignoreCase: boolean;
5+
isXML: boolean;
56
voidElements: RegExp;
67
};
78
'image/svg+xml': {
89
docType: string;
910
ignoreCase: boolean;
11+
isXML: boolean;
1012
voidElements: {
1113
test: () => boolean;
1214
};
1315
};
1416
'text/xml': {
1517
docType: string;
1618
ignoreCase: boolean;
19+
isXML: boolean;
1720
voidElements: {
1821
test: () => boolean;
1922
};
2023
};
2124
'application/xml': {
2225
docType: string;
2326
ignoreCase: boolean;
27+
isXML: boolean;
2428
voidElements: {
2529
test: () => boolean;
2630
};
2731
};
2832
'application/xhtml+xml': {
2933
docType: string;
3034
ignoreCase: boolean;
35+
isXML: boolean;
3136
voidElements: {
3237
test: () => boolean;
3338
};

worker.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11224,26 +11224,31 @@ const Mime = {
1122411224
'text/html': {
1122511225
docType: '<!DOCTYPE html>',
1122611226
ignoreCase: true,
11227+
isXML: false,
1122711228
voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
1122811229
},
1122911230
'image/svg+xml': {
1123011231
docType: '<?xml version="1.0" encoding="utf-8"?>',
1123111232
ignoreCase: false,
11233+
isXML: true,
1123211234
voidElements
1123311235
},
1123411236
'text/xml': {
1123511237
docType: '<?xml version="1.0" encoding="utf-8"?>',
1123611238
ignoreCase: false,
11239+
isXML: true,
1123711240
voidElements
1123811241
},
1123911242
'application/xml': {
1124011243
docType: '<?xml version="1.0" encoding="utf-8"?>',
1124111244
ignoreCase: false,
11245+
isXML: true,
1124211246
voidElements
1124311247
},
1124411248
'application/xhtml+xml': {
1124511249
docType: '<?xml version="1.0" encoding="utf-8"?>',
1124611250
ignoreCase: false,
11251+
isXML: true,
1124711252
voidElements
1124811253
}
1124911254
};
@@ -11442,6 +11447,21 @@ class TreeWalker {
1144211447
}
1144311448
}
1144411449

11450+
/**
11451+
* @implements globalThis.Attr
11452+
*/
11453+
class XmlAttr extends Attr$1 {
11454+
constructor(ownerDocument, name, value = '') {
11455+
super(ownerDocument, name, value);
11456+
}
11457+
11458+
toString() {
11459+
const {name, [VALUE]: value} = this;
11460+
return emptyAttributes.has(name) && !value ?
11461+
name : `${name}="${escape(value)}"`;
11462+
}
11463+
}
11464+
1144511465
const query = (method, ownerDocument, selectors) => {
1144611466
let {[NEXT]: next, [END]: end} = ownerDocument;
1144711467
return method.call({ownerDocument, [NEXT]: next, [END]: end}, selectors);
@@ -11577,7 +11597,7 @@ let Document$1 = class Document extends NonElementParentNode {
1157711597
return this[EVENT_TARGET];
1157811598
}
1157911599

11580-
createAttribute(name) { return new Attr$1(this, name); }
11600+
createAttribute(name) { return this[MIME].isXML ? new XmlAttr(this, name) : new Attr$1(this, name); }
1158111601
createComment(textContent) { return new Comment$1(this, textContent); }
1158211602
createDocumentFragment() { return new DocumentFragment$1(this); }
1158311603
createDocumentType(name, publicId, systemId) { return new DocumentType$1(this, name, publicId, systemId); }

0 commit comments

Comments
 (0)