Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cjs/interface/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
const {Facades, illegalConstructor} = require('../shared/facades.js');
const {HTMLClasses} = require('../shared/html-classes.js');
const {Mime} = require('../shared/mime.js');
const {knownSiblings} = require('../shared/utils.js');
const {asciiLowerCase, knownSiblings} = require('../shared/utils.js');
const {assign, create, defineProperties, setPrototypeOf} = require('../shared/object.js');

const {NonElementParentNode} = require('../mixin/non-element-parent-node.js');
Expand Down Expand Up @@ -169,7 +169,7 @@ class Document extends NonElementParentNode {
return this[EVENT_TARGET];
}

createAttribute(name) { return new Attr(this, name); }
createAttribute(name) { return new Attr(this, this[MIME].ignoreCase ? asciiLowerCase(name) : name); }
createCDATASection(data) { return new CDATASection(this, data); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
Expand Down
77 changes: 53 additions & 24 deletions cjs/interface/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ const {
} = require('../shared/symbols.js');

const {
asciiLowerCase,
hasUpperCase,
htmlToFragment,
ignoreCase,
inSvg,
knownAdjacent,
localCase,
String
Expand Down Expand Up @@ -71,6 +74,34 @@ const isVoid = ({localName, ownerDocument}) => {
return ownerDocument[MIME].voidElements.test(localName);
};

const findAttribute = (element, name) => {
let next = element[NEXT];
while (next.nodeType === ATTRIBUTE_NODE) {
if (next.name === name)
return next;
next = next[NEXT];
}
return null;
};

// the name an element stores: only HTML elements in HTML documents hold ASCII lowercase names
const attributeName = (element, name) => (
hasUpperCase(name) && ignoreCase(element) && !inSvg(element) ?
asciiLowerCase(name) :
name
);

// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name
// every attribute created for an HTML element is ASCII lowercase, so the name is looked up as
// given first, and only a non canonical one (from another document, for instance) is kept as is
const attributeByName = (element, name) => {
const attribute = findAttribute(element, name);
if (attribute)
return attribute;
const storedName = attributeName(element, name);
return storedName === name ? null : findAttribute(element, storedName);
};

// </utils>

/**
Expand Down Expand Up @@ -226,20 +257,17 @@ class Element extends ParentNode {
focus() { this.dispatchEvent(new Event('focus')); }

getAttribute(name) {
if (name === 'class')
const attribute = attributeByName(this, name);
if (!attribute)
return name === 'class' ? this.className : null;
// class is proxied through classList
if (attribute.name === 'class')
return this.className;
const attribute = this.getAttributeNode(name);
return attribute && (ignoreCase(this) ? attribute.value : escape(attribute.value));
return ignoreCase(this) ? attribute.value : escape(attribute.value);
}

getAttributeNode(name) {
let next = this[NEXT];
while (next.nodeType === ATTRIBUTE_NODE) {
if (next.name === name)
return next;
next = next[NEXT];
}
return null;
return attributeByName(this, name);
}

getAttributeNames() {
Expand All @@ -252,19 +280,15 @@ class Element extends ParentNode {
return attributes;
}

hasAttribute(name) { return !!this.getAttributeNode(name); }
hasAttribute(name) { return !!attributeByName(this, name); }
hasAttributes() { return this[NEXT].nodeType === ATTRIBUTE_NODE; }

removeAttribute(name) {
if (name === 'class' && this[CLASS_LIST])
const attribute = attributeByName(this, name);
if (attribute) {
if (attribute.name === 'class' && this[CLASS_LIST])
this[CLASS_LIST].clear();
let next = this[NEXT];
while (next.nodeType === ATTRIBUTE_NODE) {
if (next.name === name) {
removeAttribute(this, next);
return;
}
next = next[NEXT];
removeAttribute(this, attribute);
}
}

Expand All @@ -280,12 +304,17 @@ class Element extends ParentNode {
}

setAttribute(name, value) {
if (name === 'class')
this.className = value;
else {
const attribute = this.getAttributeNode(name);
if (attribute)
const attribute = attributeByName(this, name);
if (attribute) {
if (attribute.name === 'class')
this.className = value;
else
attribute.value = value;
}
else {
name = attributeName(this, name);
if (name === 'class')
this.className = value;
else
setAttribute(this, new Attr(this.ownerDocument, name, value));
}
Expand Down
5 changes: 3 additions & 2 deletions cjs/mixin/inner-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const {ELEMENT_NODE, DOCUMENT_FRAGMENT_NODE} = require('../shared/constants.js');
const {CUSTOM_ELEMENTS} = require('../shared/symbols.js');
const {parseFromString} = require('../shared/parse-from-string.js');
const {ignoreCase} = require('../shared/utils.js');
const {ignoreCase, inSvg} = require('../shared/utils.js');


/**
Expand All @@ -21,7 +21,8 @@ const setInnerHtml = (node, html) => {
const {constructor} = ownerDocument;
const document = new constructor;
document[CUSTOM_ELEMENTS] = ownerDocument[CUSTOM_ELEMENTS];
const {childNodes} = parseFromString(document, ignoreCase(node), html);
// an SVG context keeps attribute names as they are, like it does when parsing a whole document
const {childNodes} = parseFromString(document, ignoreCase(node), html, inSvg(node));

node.replaceChildren(...childNodes.map(setOwnerDocument, ownerDocument));
};
Expand Down
17 changes: 13 additions & 4 deletions cjs/shared/parse-from-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const {ELEMENT_NODE, SVG_NAMESPACE} = require('./constants.js');
const {CUSTOM_ELEMENTS, PREV, END, VALUE} = require('./symbols.js');
const {keys} = require('./object.js');

const {knownBoundaries, knownSiblings} = require('./utils.js');
const {asciiLowerCase, hasUpperCase, knownBoundaries, knownSiblings} = require('./utils.js');
const {attributeChangedCallback, connectedCallback} = require('../interface/custom-element-registry.js');

const {Attr} = require('../interface/attr.js');

const {Parser} = HTMLParser2;

// import {Mime} from './mime.js';
Expand Down Expand Up @@ -40,7 +42,7 @@ const attribute = (element, end, attribute, value, active) => {
const isNotParsing = () => notParsing;
exports.isNotParsing = isNotParsing;

const parseFromString = (document, isHTML, markupLanguage) => {
const parseFromString = (document, isHTML, markupLanguage, svg = false) => {
const {active, registry} = document[CUSTOM_ELEMENTS];

let node = document;
Expand All @@ -59,16 +61,19 @@ const parseFromString = (document, isHTML, markupLanguage) => {
// <tagName>
onopentag(name, attributes) {
let create = true;
let htmlAttributes = isHTML && !svg;
if (isHTML) {
if (ownerSVGElement) {
node = append(node, document.createElementNS(SVG_NAMESPACE, name), active);
node.ownerSVGElement = ownerSVGElement;
create = false;
htmlAttributes = false;
}
else if (name === 'svg' || name === 'SVG') {
ownerSVGElement = document.createElementNS(SVG_NAMESPACE, name);
node = append(node, ownerSVGElement, active);
create = false;
htmlAttributes = false;
}
else if (active) {
const ce = name.includes('-') ? name : (attributes.is || '');
Expand All @@ -85,8 +90,12 @@ const parseFromString = (document, isHTML, markupLanguage) => {
node = append(node, document.createElement(name), false);

let end = node[END];
for (const name of keys(attributes))
attribute(node, end, document.createAttribute(name), attributes[name], active);
// the HTML tokenizer lowercases attribute names, but SVG ones keep their case
// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
for (const name of keys(attributes)) {
const htmlName = htmlAttributes && hasUpperCase(name) ? asciiLowerCase(name) : name;
attribute(node, end, new Attr(document, htmlName), attributes[name], active);
}
},

// #text, #comment
Expand Down
5 changes: 3 additions & 2 deletions cjs/shared/parse-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {
const {END, PREV} = require('./symbols.js');

const {htmlClasses} = require('./register-html-class.js');
const {knownBoundaries, knownSiblings} = require('./utils.js');
const {asciiLowerCase, hasUpperCase, inSvg, knownBoundaries, knownSiblings} = require('./utils.js');

const {Attr} = require('../interface/attr.js');
const {CDATASection} = require('../interface/cdata-section.js');
Expand Down Expand Up @@ -75,7 +75,8 @@ const parseJSON = value => {
case ATTRIBUTE_NODE: {
const name = array[i++];
const value = typeof array[i] === 'string' ? array[i++] : '';
const attr = new Attr(document, name, value);
const htmlName = inSvg(parentNode) || !hasUpperCase(name) ? name : asciiLowerCase(name);
const attr = new Attr(document, htmlName, value);
attr.ownerElement = parentNode;
knownSiblings(end[PREV], attr, end);
break;
Expand Down
34 changes: 34 additions & 0 deletions cjs/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,43 @@ const {END, MIME, NEXT, PREV} = require('./symbols.js');
const $String = String;
exports.String = $String;

const allUpperCase = /[A-Z]/g;

// ASCII uppercase only: cheaper than a regular expression, as this runs on every lookup
const hasUpperCase = name => {
if (typeof name !== 'string')
return false;
for (let i = 0; i < name.length; i++) {
const code = name.charCodeAt(i);
if (64 < code && 91 > code)
return true;
}
return false;
};
exports.hasUpperCase = hasUpperCase;

// https://infra.spec.whatwg.org/#ascii-lowercase
const asciiLowerCase = name => $String(name).replace(
allUpperCase,
character => character.toLowerCase()
);
exports.asciiLowerCase = asciiLowerCase;

const getEnd = node => node.nodeType === ELEMENT_NODE ? node[END] : node;
exports.getEnd = getEnd;

// an element is in SVG when it is one, or when it lives inside one: svg.innerHTML builds
// HTML class elements (a pre-existing limitation), so checking ownerSVGElement is not enough
const inSvg = node => {
while (node) {
if (node.ownerSVGElement !== undefined)
return true;
node = node.parentNode;
}
return false;
};
exports.inSvg = inSvg;

const ignoreCase = ({ownerDocument}) => ownerDocument[MIME].ignoreCase;
exports.ignoreCase = ignoreCase;

Expand Down
4 changes: 2 additions & 2 deletions esm/interface/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import {Facades, illegalConstructor} from '../shared/facades.js';
import {HTMLClasses} from '../shared/html-classes.js';
import {Mime} from '../shared/mime.js';
import {knownSiblings} from '../shared/utils.js';
import {asciiLowerCase, knownSiblings} from '../shared/utils.js';
import {assign, create, defineProperties, setPrototypeOf} from '../shared/object.js';

import {NonElementParentNode} from '../mixin/non-element-parent-node.js';
Expand Down Expand Up @@ -169,7 +169,7 @@ export class Document extends NonElementParentNode {
return this[EVENT_TARGET];
}

createAttribute(name) { return new Attr(this, name); }
createAttribute(name) { return new Attr(this, this[MIME].ignoreCase ? asciiLowerCase(name) : name); }
createCDATASection(data) { return new CDATASection(this, data); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
Expand Down
Loading