Skip to content

Commit

Permalink
Add serializable shadow roots and getHTML()
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=271343
rdar://125513986

Reviewed by Ryosuke Niwa.

This implements whatwg/html#10139 as amended by
whatwg/html#10260.

This also changes the way a shadow root is serialized to avoid creating
a template element.

The API is guarded by DeclarativeShadowRootsSerializerAPIsEnabled just
in case there is a need to disable it without removing the code.

Thanks to Chris Dumez for his help with Vector<RefPtr<ShadowRoot>>.

WPT tests are synchronized up to and including this commit:
web-platform-tests/wpt@dd2e51d

This also adjusts the copyright lines to match the commit log for the
relevant files.

* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/declarative-shadow-dom-repeats-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/declarative-shadow-dom-repeats.html:
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml-ordering-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml-ordering.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml.tentative-expected.txt: Removed.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/gethtml.tentative.html: Removed.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/declarative/w3c-import.log:
* LayoutTests/tests-options.json:
* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/CMakeLists.txt:
* Source/WebCore/DerivedSources-input.xcfilelist:
* Source/WebCore/DerivedSources-output.xcfilelist:
* Source/WebCore/DerivedSources.make:
* Source/WebCore/Headers.cmake:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/dom/Element.cpp:
(WebCore::Element::getHTML const):
* Source/WebCore/dom/Element.h:
* Source/WebCore/dom/GetHTMLOptions.h: Added.
* Source/WebCore/dom/GetHTMLOptions.idl: Added.
* Source/WebCore/dom/InnerHTML.idl:
* Source/WebCore/dom/ShadowRoot.cpp:
(WebCore::ShadowRoot::ShadowRoot):
(WebCore::ShadowRoot::getHTML const):
(WebCore::ShadowRoot::cloneNodeInternal):
* Source/WebCore/dom/ShadowRoot.h:
* Source/WebCore/dom/ShadowRoot.idl:
* Source/WebCore/dom/ShadowRootInit.h:
* Source/WebCore/dom/ShadowRootInit.idl:
* Source/WebCore/editing/MarkupAccumulator.cpp:
(WebCore::MarkupAccumulator::MarkupAccumulator):
(WebCore::MarkupAccumulator::shouldIncludeShadowRoots const):
(WebCore::MarkupAccumulator::includeShadowRoot const):
(WebCore::MarkupAccumulator::serializeNodesWithNamespaces):
(WebCore::MarkupAccumulator::replacementElement):
* Source/WebCore/editing/MarkupAccumulator.h:
(WebCore::MarkupAccumulator::MarkupAccumulator):
* Source/WebCore/editing/markup.cpp:
(WebCore::serializeFragment):
* Source/WebCore/editing/markup.h:
(WebCore::serializeFragment):
* Source/WebCore/html/HTMLAttributeNames.in:
* Source/WebCore/html/HTMLTemplateElement.cpp:
(WebCore::HTMLTemplateElement::attachAsDeclarativeShadowRootIfNeeded):
* Source/WebCore/html/HTMLTemplateElement.idl:
* Source/WebCore/html/parser/HTMLConstructionSite.cpp:
(WebCore::HTMLConstructionSite::insertHTMLTemplateElement):
* Source/WebCore/loader/archive/cf/LegacyWebArchive.cpp:
(WebCore::LegacyWebArchive::create):

Canonical link: https://commits.webkit.org/277374@main
  • Loading branch information
annevk committed Apr 11, 2024
1 parent 619a9da commit de64e62
Show file tree
Hide file tree
Showing 37 changed files with 4,152 additions and 2,038 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

PASS Repeated declarative shadow roots keep only the first
PASS Calling attachShadow() on declarative shadow root must match type
PASS Calling attachShadow() on declarative shadow root must match mode
PASS Calling attachShadow() on declarative shadow root must match all parameters

Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,48 @@
test((t) => {
assert_throws_dom("NotSupportedError",() => {
open1.attachShadow({mode: "closed"});
},'Mismatched shadow root type should throw');
},'Mismatched shadow root mode should throw');
const initialShadow = open1.shadowRoot;
const shadow = open1.attachShadow({mode: "open"}); // Shouldn't throw
assert_equals(shadow,initialShadow,'Same shadow should be returned');
assert_equals(shadow.textContent,'','Shadow should be empty');
},'Calling attachShadow() on declarative shadow root must match type');
},'Calling attachShadow() on declarative shadow root must match mode');
</script>

<div id=open2>
<template shadowrootmode=open shadowrootdelegatesfocus shadowrootclonable>
<template shadowrootmode=open shadowrootdelegatesfocus shadowrootclonable shadowrootserializable>
Open, delegates focus (not the default), clonable (not the default)
named slot assignment (the default)
serializable (not the default), named slot assignment (the default)
</template>
</div>

<script>
test((t) => {
t.add_cleanup(() => open2.remove());
assert_true(!!open2.shadowRoot);
// Changing the mode should throw.
assert_throws_dom("NotSupportedError",() => {
open2.attachShadow({mode: "closed", delegatesFocus: true, slotAssignment: "named", clonable: true});
open2.attachShadow({mode: "closed"});
},'Mismatched shadow root mode should throw');
assert_throws_dom("NotSupportedError",() => {
open2.attachShadow({mode: "closed", delegatesFocus: true, slotAssignment: "named", clonable: true, serializable: true});
},'Mismatched shadow root mode should throw (explicit args)');

// Changing other things should not throw, and should not change the shadow root's settings
const initialShadow = open2.shadowRoot;
const shadow = open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: true}); // Shouldn't throw
assert_equals(shadow,initialShadow,'Same shadow should be returned');
assert_equals(shadow.textContent,'','Shadow should be empty');

assert_equals(initialShadow.delegatesFocus,true);
assert_equals(initialShadow.slotAssignment,"named");
assert_true(initialShadow.clonable);
assert_true(initialShadow.serializable);
let newShadow = open2.attachShadow({mode: "open", delegatesFocus: false, slotAssignment: "manual", clonable: false, serializable: false});
assert_equals(newShadow,initialShadow,'Same shadow should be returned');
assert_equals(newShadow.textContent,'','Shadow should be empty');
assert_equals(newShadow.delegatesFocus,true);
assert_equals(newShadow.slotAssignment,"named");
assert_true(newShadow.clonable);
assert_true(newShadow.serializable);
assert_throws_dom("NotSupportedError",() => {
open2.attachShadow({mode: "open"});
},'Invoking attachShadow() on a non-declarative shadow root should throw');

},'Calling attachShadow() on declarative shadow root must match all parameters');
</script>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

PASS template position
PASS attribute position
PASS both template and attribute position

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<title>getHTML ordering behavior</title>
<link rel='author' href='mailto:masonf@chromium.org'>
<link rel='help' href='https://github.com/whatwg/html/pull/10139'>
<script src='/resources/testharness.js'></script>
<script src='/resources/testharnessreport.js'></script>


<div id=tests>
<div data-name="base">
<template shadowrootmode=open shadowrootdelegatesfocus shadowrootserializable shadowrootclonable>
<slot></slot>
</template>
<span class=content>Content 1</span>
<span class=content>Content 2</span>
</div>

<div data-name="template position">
<span class=content>Content 1</span>
<template shadowrootmode=open shadowrootdelegatesfocus shadowrootserializable shadowrootclonable>
<slot></slot>
</template>
<span class=content>Content 2</span>
</div>

<div data-name="attribute position">
<template shadowrootclonable shadowrootserializable shadowrootdelegatesfocus shadowrootmode=open>
<slot></slot>
</template>
<span class=content>Content 1</span>
<span class=content>Content 2</span>
</div>

<div data-name="both template and attribute position">
<span class=content>Content 1</span>
<span class=content>Content 2</span>
<template shadowrootclonable shadowrootserializable shadowrootdelegatesfocus shadowrootmode=open>
<slot></slot>
</template>
</div>
</div>

<script>
function removeWhitespaceNodes(el) {
el.shadowRoot && removeWhitespaceNodes(el.shadowRoot);
var iter = document.createNodeIterator(el, NodeFilter.SHOW_TEXT,
(node) => (node.data.replace(/\s/g,'').length === 0) ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT);
let node;
while (node = iter.nextNode()) {
node.remove();
}
return el;
}
const serialize = (host) => host.getHTML({shadowRoots: [host.shadowRoot]});

const testCases = Array.from(document.querySelectorAll('#tests>div'));
assert_true(testCases.length > 1);
const baseHost = removeWhitespaceNodes(testCases[0]);
const correctSerialization = serialize(baseHost);
baseHost.remove();
for(let i=1;i<testCases.length;++i) {
const thisHost = removeWhitespaceNodes(testCases[i]);
test(t => {
assert_equals(serialize(thisHost),correctSerialization,'Serialization should be identical');
thisHost.remove();
},thisHost.dataset.name);
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<title>getHTML behavior</title>
<meta name="timeout" content="long">
<link rel='author' href='mailto:masonf@chromium.org'>
<link rel='help' href='https://github.com/whatwg/html/issues/8867'>
<script src='/resources/testharness.js'></script>
<script src='/resources/testharnessreport.js'></script>
<script src='../../html/resources/common.js'></script>

<body>

<script>
function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot,
lightDOMContent, declarativeShadowDom, mode, delegatesFocus,
serializable, clonable) {
const t = test(t => {
// Create and attach element
let wrapper;
if (runGetHTMLOnShadowRoot) {
// This ensures we're testing both Element.getHTML() and ShadowRoot.getHTML().
const host = document.createElement('div');
t.add_cleanup(function() { host.remove(); });
document.body.appendChild(host);
wrapper = host.attachShadow({mode: 'open'});
} else {
wrapper = document.createElement('div');
t.add_cleanup(function() { wrapper.remove(); });
document.body.appendChild(wrapper);
}

let shadowRoot;
let initDict = {mode: mode, delegatesFocus: delegatesFocus, clonable};
let expectedSerializable = null;
switch (serializable) {
case undefined: expectedSerializable = false; break;
case "true": initDict.serializable = expectedSerializable = true; break;
case "false": initDict.serializable = expectedSerializable = false; break;
default: throw new Error(`Invalid serializable ${serializable}`);
}
const delegatesAttr = delegatesFocus ? ' shadowrootdelegatesfocus=""' : '';
const serializableAttr = expectedSerializable ? ' shadowrootserializable=""' : '';
const clonableAttr = clonable ? ' shadowrootclonable=""' : '';

if (allowsShadowDom && declarativeShadowDom) {
const html = `<${elementType}>${lightDOMContent}<template ` +
`shadowrootmode=${mode}${delegatesAttr}${serializableAttr}` +
`${clonableAttr}>`;
wrapper.setHTMLUnsafe(html);
// Get hold of the declarative shadow root in a way that works when its mode is "closed"
shadowRoot = wrapper.firstElementChild.attachShadow(initDict);
} else {
// Imperative shadow dom
const element = document.createElement(elementType);
wrapper.appendChild(element);
const temp = document.createElement('div');
temp.innerHTML = lightDOMContent;
element.append(...temp.childNodes);
if (allowsShadowDom) {
shadowRoot = element.attachShadow(initDict);
}
}
assert_true(!allowsShadowDom || !!shadowRoot);

if (allowsShadowDom) {
const correctShadowHtml = `<template shadowrootmode="${mode}"` +
`${delegatesAttr}${serializableAttr}${clonableAttr}><slot></slot>` +
`</template>`;
const correctHtml = `<${elementType}>${correctShadowHtml}` +
`${lightDOMContent}</${elementType}>`;
assert_equals(shadowRoot.mode,mode);
assert_equals(shadowRoot.delegatesFocus,delegatesFocus);
assert_equals(shadowRoot.serializable,expectedSerializable);
assert_equals(shadowRoot.clonable,clonable);
shadowRoot.appendChild(document.createElement('slot'));
const emptyElement = `<${elementType}>${lightDOMContent}</${elementType}>`;
if (expectedSerializable) {
assert_equals(wrapper.getHTML({serializableShadowRoots: true}),
correctHtml);
assert_equals(wrapper.firstElementChild.getHTML({
serializableShadowRoots: true}),
`${correctShadowHtml}${lightDOMContent}`);
} else {
assert_equals(wrapper.getHTML({serializableShadowRoots: true}), emptyElement);
}
// If we provide the shadow root, serialize it, regardless of serializableShadowRoots.
assert_equals(wrapper.getHTML({serializableShadowRoots: true, shadowRoots:
[shadowRoot]}),correctHtml);
assert_equals(wrapper.getHTML({serializableShadowRoots: false, shadowRoots:
[shadowRoot]}),correctHtml);
assert_equals(wrapper.getHTML({shadowRoots: [shadowRoot]}),correctHtml);
} else {
// For non-shadow hosts, getHTML() should also match .innerHTML
assert_equals(wrapper.getHTML({serializableShadowRoots: true}),wrapper.innerHTML);
}

// Either way, make sure getHTML({serializableShadowRoots: false}) matches .innerHTML
assert_equals(wrapper.getHTML({serializableShadowRoots: false}),wrapper.innerHTML,
'getHTML() with serializableShadowRoots false should return the same as .innerHTML');
// ...and that the default for serializableShadowRoots is false.
assert_equals(wrapper.getHTML(),wrapper.innerHTML,
'The default for serializableShadowRoots should be false');

}, `${runGetHTMLOnShadowRoot ? 'ShadowRoot' : 'Element'}.getHTML() on ` +
`<${elementType}>${lightDOMContent}${allowsShadowDom ?
`, with ${declarativeShadowDom ? 'declarative' : 'imperative'} shadow, ` +
`mode=${mode}, delegatesFocus=${delegatesFocus}, ` +
`serializable=${serializable}, clonable=${clonable}.` : ''}`);
}

function runAllTests() {
const allElements = [...HTML5_ELEMENTS, 'htmlunknown'];
const safelisted = HTML5_SHADOW_ALLOWED_ELEMENTS.filter(el => el != 'body');
for (const elementName of allElements) {
const allowsShadowDom = safelisted.includes(elementName);
for (const runGetHTMLOnShadowRoot of [false, true]) {
for (const lightDOMContent of ['','<span>light</span>']) {
if (allowsShadowDom) {
for (const declarativeShadowDom of [false, true]) {
for (const delegatesFocus of [false, true]) {
for (const clonable of [false, true]) {
for (const mode of ['open', 'closed']) {
for (const serializable of [undefined, 'false', 'true']) {
testElementType(true, elementName, runGetHTMLOnShadowRoot,
lightDOMContent, declarativeShadowDom, mode,
delegatesFocus, serializable, clonable);
}
}
}
}
}
} else {
testElementType(false, elementName, runGetHTMLOnShadowRoot,
lightDOMContent);
}
}
}
}
}

runAllTests();

</script>
Loading

0 comments on commit de64e62

Please sign in to comment.