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
5 changes: 3 additions & 2 deletions cjs/html/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ const {customElements} = require('../interface/custom-element-registry.js');
const {HTMLElement} = require('./element.js');

const createHTMLElement = (ownerDocument, builtin, localName, options) => {
if (!builtin && htmlClasses.has(localName)) {
if (!builtin) {
const Class = htmlClasses.get(localName);
return new Class(ownerDocument, localName);
if (Class)
return new Class(ownerDocument, localName);
}
const {[CUSTOM_ELEMENTS]: {active, registry}} = ownerDocument;
if (active) {
Expand Down
13 changes: 12 additions & 1 deletion cjs/shared/parse-from-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,23 @@ const append = (self, node, active) => {
return node;
};

/** Same tokens as a DOMTokenList would hold: no empty entries, no duplicates, single spaces. */
const classTokens = value => {
const tokens = [];
for (const token of value.split(/\s+/)) {
if (token && !tokens.includes(token))
tokens.push(token);
}
return tokens.join(' ');
};

const attribute = (element, end, attribute, value, active) => {
attribute[VALUE] = value;
attribute.ownerElement = element;
knownSiblings(end[PREV], attribute, end);
// the token list is built on demand: while it does not exist, only the value needs the tokens
if (attribute.name === 'class')
element.className = value;
attribute[VALUE] = classTokens(value);
if (active)
attributeChangedCallback(element, attribute.name, null, value);
};
Expand Down
5 changes: 3 additions & 2 deletions esm/html/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {customElements} from '../interface/custom-element-registry.js';
import {HTMLElement} from './element.js';

const createHTMLElement = (ownerDocument, builtin, localName, options) => {
if (!builtin && htmlClasses.has(localName)) {
if (!builtin) {
const Class = htmlClasses.get(localName);
return new Class(ownerDocument, localName);
if (Class)
return new Class(ownerDocument, localName);
}
const {[CUSTOM_ELEMENTS]: {active, registry}} = ownerDocument;
if (active) {
Expand Down
13 changes: 12 additions & 1 deletion esm/shared/parse-from-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,23 @@ const append = (self, node, active) => {
return node;
};

/** Same tokens as a DOMTokenList would hold: no empty entries, no duplicates, single spaces. */
const classTokens = value => {
const tokens = [];
for (const token of value.split(/\s+/)) {
if (token && !tokens.includes(token))
tokens.push(token);
}
return tokens.join(' ');
};

const attribute = (element, end, attribute, value, active) => {
attribute[VALUE] = value;
attribute.ownerElement = element;
knownSiblings(end[PREV], attribute, end);
// the token list is built on demand: while it does not exist, only the value needs the tokens
if (attribute.name === 'class')
element.className = value;
attribute[VALUE] = classTokens(value);
if (active)
attributeChangedCallback(element, attribute.name, null, value);
};
Expand Down
15 changes: 15 additions & 0 deletions test/html/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,18 @@ assert(node.innerHTML, '<video src="" controls></video>');
node.innerHTML = '<div>The <strong>quick</strong> brown fox</div><div>Jumped over<br>The lazy\ndog</div>';
assert(node.innerText, 'The quick brown fox\nJumped over\nThe lazy dog', 'innerText newlines');
assert(node.textContent, 'The quick brown foxJumped overThe lazy\ndog', 'textContent no newlines');

// a parsed class attribute keeps every character of its tokens, drops empty and duplicate tokens,
// and is escaped only when it is serialized
{
const source = '<!DOCTYPE html><html><body><div class=" a<b &amp;c a<b d&quot;e "></div></body></html>';
const {document} = parseHTML(source);
const div = document.querySelector('div');
assert(div.getAttribute('class'), 'a<b &c d"e', 'class value with markup characters');
assert(div.className, 'a<b &c d"e', 'className with markup characters');
assert(div.classList.length, 3, 'empty and duplicate class tokens dropped');
assert([...div.classList].join('|'), 'a<b|&c|d"e', 'class tokens with markup characters');
assert(div.outerHTML, '<div class="a<b &c d&quot;e"></div>', 'only the quote is escaped in html');
const {document: reparsed} = parseHTML(`<!DOCTYPE html><html><body>${div.outerHTML}</body></html>`);
assert(reparsed.querySelector('div').getAttribute('class'), 'a<b &c d"e', 'class value round trip');
}
7 changes: 7 additions & 0 deletions test/xml/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ document.documentElement.innerHTML = `

assert(document.querySelectorAll('Element').length, 2, 'case sesntivive 2');
assert(document.querySelectorAll('element').length, 0, 'case sesntivive 0');

// in xml every character of a class value is escaped on output, as for any other attribute
{
const xml = (new DOMParser).parseFromString('<root class="a&lt;b &amp;c a&lt;b"/>', 'text/xml');
assert(xml.documentElement.getAttribute('class'), 'a<b &c', 'class value with markup characters');
assert(xml.documentElement.toString(), '<root class="a&lt;b &amp;c" />', 'class value escaped in xml');
}