Skip to content

Commit 3e2579b

Browse files
Escape characters on xml attributes
1 parent ec5ce84 commit 3e2579b

File tree

15 files changed

+870
-227
lines changed

15 files changed

+870
-227
lines changed

cjs/interface/attr.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
const {ATTRIBUTE_NODE} = require('../shared/constants.js');
2+
const {ATTRIBUTE_NODE, QUOTE} = require('../shared/constants.js');
33
const {CHANGED, VALUE} = require('../shared/symbols.js');
44
const {String} = require('../shared/utils.js');
55
const {attrAsJSON} = require('../shared/jsdon.js');
@@ -10,8 +10,6 @@ const {attributeChangedCallback: ceAttributes} = require('./custom-element-regis
1010

1111
const {Node} = require('./node.js');
1212

13-
const QUOTE = /"/g;
14-
1513
/**
1614
* @implements globalThis.Attr
1715
*/

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 {AMPERSAND, QUOTE, LT, GT} = require('../shared/constants.js');
4+
const {emptyAttributes} = require('../shared/attributes.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}="${value.replace(AMPERSAND, '&').replace(QUOTE, '"').replace(LT, '<').replace(GT, '>')}"`;
19+
}
20+
}
21+
exports.XmlAttr = XmlAttr

cjs/shared/constants.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,13 @@ exports.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = DOCUMENT_POSITION_IMPLEMENTA
5050
// SVG
5151
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
5252
exports.SVG_NAMESPACE = SVG_NAMESPACE;
53+
54+
// Characters
55+
const QUOTE = /"/g;
56+
exports.QUOTE = QUOTE;
57+
const LT = /</g;
58+
exports.LT = LT;
59+
const GT = />/g;
60+
exports.GT = GT;
61+
const AMPERSAND = /&/g;
62+
exports.AMPERSAND = AMPERSAND;

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/attr.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ATTRIBUTE_NODE} from '../shared/constants.js';
1+
import {ATTRIBUTE_NODE,QUOTE} from '../shared/constants.js';
22
import {CHANGED, VALUE} from '../shared/symbols.js';
33
import {String} from '../shared/utils.js';
44
import {attrAsJSON} from '../shared/jsdon.js';
@@ -9,8 +9,6 @@ import {attributeChangedCallback as ceAttributes} from './custom-element-registr
99

1010
import {Node} from './node.js';
1111

12-
const QUOTE = /"/g;
13-
1412
/**
1513
* @implements globalThis.Attr
1614
*/

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 {AMPERSAND,QUOTE,LT,GT} from '../shared/constants.js';
3+
import {emptyAttributes} from '../shared/attributes.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}="${value.replace(AMPERSAND, '&amp;').replace(QUOTE, '&quot;').replace(LT, '&lt;').replace(GT, '&gt;')}"`;
18+
}
19+
}

esm/shared/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ export const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
2929

3030
// SVG
3131
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
32+
33+
// Characters
34+
export const QUOTE = /"/g;
35+
export const LT = /</g;
36+
export const GT = />/g;
37+
export const AMPERSAND = /&/g;

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

0 commit comments

Comments
 (0)