Copying content with shadow DOM doesn't copy any contents
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 3 Oct 2018 06:28:07 +0000 (06:28 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 3 Oct 2018 06:28:07 +0000 (06:28 +0000)
https://bugs.webkit.org/show_bug.cgi?id=157443

Reviewed by Wenson Hsieh.

Source/WebCore:

This patch adds the support for copying and pasting content across shadow boundaries in HTML and plain text,
which is enabled whenever selection across shadow boundaries is enabled.

To do this, TextIterator now has a constructor which takes two Positions, and the node traversal code in
StyledMarkupAccumulator has been abstracted via helper functions as done for TextIterator.

When serializing a HTMl slot element, serialize it as a span with "display: contents" to make sure when
the content is pasted into a shadow tree, it wouldn't affect the slot assignment of the shadow tree.

Tests: editing/pasteboard/copy-paste-across-shadow-boundaries-1.html
       editing/pasteboard/copy-paste-across-shadow-boundaries-2.html
       editing/pasteboard/copy-paste-across-shadow-boundaries-3.html
       editing/pasteboard/copy-paste-across-shadow-boundaries-4.html
       editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html
       editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html
       editing/pasteboard/copy-paste-with-shadow-content.html

* dom/ComposedTreeIterator.h:
(WebCore::assignedSlotIgnoringUserAgentShadow): Moved from TextIterator.cpp.
(WebCore::shadowRootIgnoringUserAgentShadow): Ditto.
(WebCore::firstChildInComposedTreeIgnoringUserAgentShadow): Ditto.
(WebCore::nextSiblingInComposedTreeIgnoringUserAgentShadow): Ditto.
* dom/Position.h:
(WebCore::Position::treeScope const): Added.
* editing/EditingStyle.cpp:
(WebCore::EditingStyle::addDisplayContents): Added.
* editing/EditingStyle.h:
* editing/Editor.cpp:
(WebCore::Editor::selectedText const): Use the new behavior when selectionAcrossShadowBoundariesEnabled is set.
(WebCore::Editor::selectedTextForDataTransfer const): Ditto.
* editing/MarkupAccumulator.cpp:
(WebCore::MarkupAccumulator::appendEndElement): Renamed from appendEndTag. Now takes StringBuilder.
* editing/MarkupAccumulator.h:
(WebCore::MarkupAccumulator::appendEndTag):
* editing/TextIterator.cpp:
(WebCore::TextIterator::TextIterator): Added a new variant which takes two positions.
(WebCore::TextIterator::init):
(WebCore::firstChild):
(WebCore::nextSibling):
(WebCore::plainText): Ditto.
* editing/TextIterator.h:
* editing/cocoa/EditorCocoa.mm:
(WebCore::Editor::selectionInHTMLFormat): Use the new behavior if selectionAcrossShadowBoundariesEnabled is set.
* editing/gtk/EditorGtk.cpp:
(WebCore::Editor::writeSelectionToPasteboard): Ditto.
* editing/markup.cpp:
(WebCore::StyledMarkupAccumulator::parentNode): Added.
(WebCore::StyledMarkupAccumulator::firstChild): Added.
(WebCore::StyledMarkupAccumulator::nextSibling): Added.
(WebCore::StyledMarkupAccumulator::nextSkippingChildren): Added.
(WebCore::StyledMarkupAccumulator::hasChildNodes): Added.
(WebCore::StyledMarkupAccumulator::isDescendantOf): Added.
(WebCore::StyledMarkupAccumulator::StyledMarkupAccumulator):
(WebCore::StyledMarkupAccumulator::appendElement): Serialize a slot element as a span with display: contents.
(WebCore::StyledMarkupAccumulator::appendEndElement): Added. Ditto.
(WebCore::StyledMarkupAccumulator::serializeNodes):
(WebCore::StyledMarkupAccumulator::traverseNodesForSerialization): Use the newly added helper functions to
traverse the composed tree when m_useComposedTree is set.
(WebCore::commonShadowIncludingAncestor): Added.
(WebCore::serializePreservingVisualAppearanceInternal): Added SerializeComposedTree as an argument. Also use
StyledMarkupAccumulator::parentNode to serialize special common ancestors; e.g. to preserve b, i, etc...
(WebCore::serializePreservingVisualAppearance): Ditto to the variant which takes VisibleSelection.
(WebCore::sanitizedMarkupForFragmentInDocument):
* editing/markup.h:
* editing/wpe/EditorWPE.cpp:
(WebCore::Editor::writeSelectionToPasteboard):
* loader/archive/cf/LegacyWebArchive.cpp:
(WebCore::LegacyWebArchive::createFromSelection):
* page/PageSerializer.cpp:
(WebCore::PageSerializer::SerializerMarkupAccumulator::appendEndElement):
* testing/Internals.cpp:
(WebCore::Internals::setSelectionWithoutValidation): Added. A helper function to create a selection across
shadow boundaries for testing purposes.
* testing/Internals.h:
* testing/Internals.idl:

LayoutTests:

Added tests for copying and pasting across shadow boundaries with HTML and plain text.

* editing/pasteboard/copy-paste-across-shadow-boundaries-1-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-1.html: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-2.html: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-3.html: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-4.html: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2-expected.txt: Added.
* editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html: Added.
* editing/pasteboard/copy-paste-with-shadow-content-expected.txt: Added.
* editing/pasteboard/copy-paste-with-shadow-content.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@236785 268f45cc-cd09-0410-ab3c-d52691b4dbfc

40 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-with-shadow-content-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/copy-paste-with-shadow-content.html [new file with mode: 0644]
LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt [new file with mode: 0644]
LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt [new file with mode: 0644]
LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt [new file with mode: 0644]
LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt [new file with mode: 0644]
LayoutTests/platform/ios/editing/pasteboard/copy-paste-with-shadow-content-expected.txt [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/ComposedTreeIterator.h
Source/WebCore/dom/Position.h
Source/WebCore/editing/EditingStyle.cpp
Source/WebCore/editing/EditingStyle.h
Source/WebCore/editing/Editor.cpp
Source/WebCore/editing/MarkupAccumulator.cpp
Source/WebCore/editing/MarkupAccumulator.h
Source/WebCore/editing/TextIterator.cpp
Source/WebCore/editing/TextIterator.h
Source/WebCore/editing/cocoa/EditorCocoa.mm
Source/WebCore/editing/gtk/EditorGtk.cpp
Source/WebCore/editing/markup.cpp
Source/WebCore/editing/markup.h
Source/WebCore/editing/wpe/EditorWPE.cpp
Source/WebCore/loader/archive/cf/LegacyWebArchive.cpp
Source/WebCore/page/PageSerializer.cpp
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl

index 3030554..65d1f31 100644 (file)
@@ -1,3 +1,27 @@
+2018-10-02  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Copying content with shadow DOM doesn't copy any contents
+        https://bugs.webkit.org/show_bug.cgi?id=157443
+
+        Reviewed by Wenson Hsieh.
+
+        Added tests for copying and pasting across shadow boundaries with HTML and plain text.
+
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-1-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-1.html: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-2.html: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-3.html: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-4.html: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2-expected.txt: Added.
+        * editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html: Added.
+        * editing/pasteboard/copy-paste-with-shadow-content-expected.txt: Added.
+        * editing/pasteboard/copy-paste-with-shadow-content.html: Added.
+
 2018-10-01  Ryosuke Niwa  <rniwa@webkit.org>
 
         GC can collect JS wrappers of nodes in the mutation records waiting to be delivered
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1-expected.txt
new file mode 100644 (file)
index 0000000..d2ee21a
--- /dev/null
@@ -0,0 +1,14 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| "hello "
+| <span>
+|   id="host"
+|   <span>
+|     style="display: contents;"
+|     "world"
+|   " Web<#selection-caret>"
+
+text/plain:
+| "hello world Web"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-1.html
new file mode 100644 (file)
index 0000000..0ddef48
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue">hello <span id="host">world</span> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = '<slot></slot> WebKit';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(source, 0, shadowRoot.lastChild, shadowRoot.lastChild.data.indexOf('Kit'));
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt
new file mode 100644 (file)
index 0000000..f7a21aa
--- /dev/null
@@ -0,0 +1,15 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "world" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <span>
+|   id="host"
+|   <span>
+|     style="display: contents;"
+|     "world"
+|   " "
+|   "WebKit"
+| " rocks<#selection-caret>"
+
+text/plain:
+| "world WebKit rocks"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-2.html
new file mode 100644 (file)
index 0000000..f4fe79c
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "world" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue">hello <span id="host">world</span> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = '<slot></slot> WebKit';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(host.firstChild, 0, source.lastChild, source.lastChild.data.length);
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt
new file mode 100644 (file)
index 0000000..b98bce9
--- /dev/null
@@ -0,0 +1,13 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "WebKit" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <span>
+|   id="host"
+|   <span>
+|     style="display: contents;"
+|     "world WebKit"
+| " rocks<#selection-caret>"
+
+text/plain:
+| "world WebKit rocks"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-3.html
new file mode 100644 (file)
index 0000000..6a62a3c
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "WebKit" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue"><span id="host">world WebKit</span> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = 'hello <slot></slot>';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(host.firstChild, 0, source.lastChild, source.lastChild.data.length);
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt
new file mode 100644 (file)
index 0000000..dceb111
--- /dev/null
@@ -0,0 +1,11 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| "hello "
+| <span>
+|   style="display: contents;"
+|   "world Web<#selection-caret>"
+
+text/plain:
+| "hello world Web"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-4.html
new file mode 100644 (file)
index 0000000..31dee13
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue"><span id="host">world WebKit</span> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = 'hello <slot></slot>';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(shadowRoot, 0, host.firstChild, host.firstChild.data.indexOf('Kit'));
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt
new file mode 100644 (file)
index 0000000..0485e60
--- /dev/null
@@ -0,0 +1,17 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <b>
+|   <span>
+|     id="host"
+|     "hello "
+|     <span>
+|       id="host"
+|       <span>
+|         style="display: contents; color: blue;"
+|         "world"
+|       " Web<#selection-caret>"
+
+text/plain:
+| "hello world Web"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html
new file mode 100644 (file)
index 0000000..473e91d
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue"><b>hello <span id="host">world</span></b> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = '<slot style="color: blue;"></slot> WebKit';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(source, 0, shadowRoot.lastChild, shadowRoot.lastChild.data.indexOf('Kit'));
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2-expected.txt
new file mode 100644 (file)
index 0000000..6ff5982
--- /dev/null
@@ -0,0 +1,21 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "WebKit", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <div>
+|   id="host1"
+|   style="font-style: italic;"
+|   <span>
+|     style="display: contents; color: blue;"
+|     <b>
+|       "hello"
+|   " "
+|   "world"
+| <div>
+|   id="host2"
+|   <u>
+|     "WebKit <#selection-caret>"
+
+text/plain:
+| "hello world
+WebKit "
diff --git a/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html b/LayoutTests/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html
new file mode 100644 (file)
index 0000000..4b323cb
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content across shadow boundaries.<br>
+To test manually, copy text blow starting from "hello" ending with "WebKit", and paste into the green box below. All the text shoul be copied & pasted.</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue"><div style="font-style: italic" id="host1"><b>hello</b></div><div id="host2">rocks</div></div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot1 = host1.attachShadow({mode: 'open'});
+shadowRoot1.innerHTML = '<slot style="color: blue;"></slot> world';
+
+const shadowRoot2 = host2.attachShadow({mode: 'open'});
+shadowRoot2.innerHTML = '<u>WebKit <slot></slot></u>';
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    internals.setSelectionWithoutValidation(source, 0, shadowRoot2.querySelector('u'), 1);
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/editing/pasteboard/copy-paste-with-shadow-content-expected.txt b/LayoutTests/editing/pasteboard/copy-paste-with-shadow-content-expected.txt
new file mode 100644 (file)
index 0000000..ce0d887
--- /dev/null
@@ -0,0 +1,31 @@
+This tests copying and pasting content with a shadow tree.
+To test manually, copy paste the content in the blue box to the green box below. All the text shoul be copied & pasted
+
+pasted:
+| <span>
+|   id="host"
+|   "hello "
+|   <span>
+|     id="host"
+|     <span>
+|       style="display: contents;"
+|       "world"
+|     " "
+|     "WebKit"
+|   " rocks<#selection-caret>"
+
+text/html:
+| <span>
+|   id="host"
+|   "hello "
+|   <span>
+|     id="host"
+|     <span>
+|       style="display: contents;"
+|       "world"
+|     " "
+|     "WebKit"
+|   " rocks<#selection-caret>"
+
+text/plain:
+| "hello world WebKit rocks"
diff --git a/LayoutTests/editing/pasteboard/copy-paste-with-shadow-content.html b/LayoutTests/editing/pasteboard/copy-paste-with-shadow-content.html
new file mode 100644 (file)
index 0000000..11d35ee
--- /dev/null
@@ -0,0 +1,40 @@
+<!DOCTYPE html><!-- webkit-test-runner [ internal:selectionAcrossShadowBoundariesEnabled=true ] -->
+<html>
+<body>
+<p id="description">This tests copying and pasting content with a shadow tree.<br>
+To test manually, copy paste the content in the blue box to the green box below. All the text shoul be copied & pasted</p>
+<style> .box { border: solid 1px; padding: 0.2rem; margin-bottom: 1rem; } </style>
+<div id="source" class="box" style="border-color: blue">hello <span id="host">world</span> rocks</div>
+<div id="destination" class="box" style="border-color: blue" contenteditable></div>
+<pre id="htmlResult"></pre>
+<pre id="textResult"></pre>
+<script src="../../resources/dump-as-markup.js"></script>
+<script>
+
+Markup.description(description.textContent);
+Markup.waitUntilDone();
+
+const shadowRoot = host.attachShadow({mode: 'open'});
+shadowRoot.innerHTML = '<slot></slot> WebKit';
+getSelection().selectAllChildren(source);
+
+destination.addEventListener('paste', (event) => {
+    htmlResult.textContent = event.clipboardData.getData('text/html');
+    textResult.textContent = event.clipboardData.getData('text/plain');
+    setTimeout(() => {
+        Markup.dump(destination, 'pasted');
+        Markup.dump(destination, 'text/html');
+        Markup.dump(textResult, 'text/plain');
+        Markup.notifyDone();
+    }, 0);
+});
+
+if (window.testRunner) {
+    document.execCommand('copy');
+    destination.focus();
+    document.execCommand('paste');
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt b/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-2-expected.txt
new file mode 100644 (file)
index 0000000..5223e4e
--- /dev/null
@@ -0,0 +1,18 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "world" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <span>
+|   id="host"
+|   style="-webkit-text-size-adjust: auto;"
+|   <span>
+|     style="display: contents;"
+|     "world"
+|   " "
+|   "WebKit"
+| <span>
+|   style="-webkit-text-size-adjust: auto;"
+|   " rocks<#selection-caret>"
+
+text/plain:
+| "world WebKit rocks"
diff --git a/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt b/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-3-expected.txt
new file mode 100644 (file)
index 0000000..c6fd560
--- /dev/null
@@ -0,0 +1,16 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "WebKit" ending with "rocks", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <span>
+|   id="host"
+|   style="-webkit-text-size-adjust: auto;"
+|   <span>
+|     style="display: contents;"
+|     "world WebKit"
+| <span>
+|   style="-webkit-text-size-adjust: auto;"
+|   " rocks<#selection-caret>"
+
+text/plain:
+| "world WebKit rocks"
diff --git a/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt b/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-4-expected.txt
new file mode 100644 (file)
index 0000000..a5c0dc0
--- /dev/null
@@ -0,0 +1,13 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <span>
+|   style="-webkit-text-size-adjust: auto;"
+|   "hello "
+| <span>
+|   style="-webkit-text-size-adjust: auto; display: contents;"
+|   "world Web<#selection-caret>"
+
+text/plain:
+| "hello world Web"
diff --git a/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt b/LayoutTests/platform/ios/editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1-expected.txt
new file mode 100644 (file)
index 0000000..42b3ab1
--- /dev/null
@@ -0,0 +1,18 @@
+This tests copying and pasting content across shadow boundaries.
+To test manually, copy text blow starting from "hello" ending with "Web", and paste into the green box below. All the text shoul be copied & pasted.
+
+pasted html:
+| <b>
+|   style="-webkit-text-size-adjust: auto;"
+|   <span>
+|     id="host"
+|     "hello "
+|     <span>
+|       id="host"
+|       <span>
+|         style="display: contents; color: blue;"
+|         "world"
+|       " Web<#selection-caret>"
+
+text/plain:
+| "hello world Web"
diff --git a/LayoutTests/platform/ios/editing/pasteboard/copy-paste-with-shadow-content-expected.txt b/LayoutTests/platform/ios/editing/pasteboard/copy-paste-with-shadow-content-expected.txt
new file mode 100644 (file)
index 0000000..b335d2f
--- /dev/null
@@ -0,0 +1,33 @@
+This tests copying and pasting content with a shadow tree.
+To test manually, copy paste the content in the blue box to the green box below. All the text shoul be copied & pasted
+
+pasted:
+| <span>
+|   id="host"
+|   style="-webkit-text-size-adjust: auto;"
+|   "hello "
+|   <span>
+|     id="host"
+|     <span>
+|       style="display: contents;"
+|       "world"
+|     " "
+|     "WebKit"
+|   " rocks<#selection-caret>"
+
+text/html:
+| <span>
+|   id="host"
+|   style="-webkit-text-size-adjust: auto;"
+|   "hello "
+|   <span>
+|     id="host"
+|     <span>
+|       style="display: contents;"
+|       "world"
+|     " "
+|     "WebKit"
+|   " rocks<#selection-caret>"
+
+text/plain:
+| "hello world WebKit rocks"
index c9d30a4..977fd99 100644 (file)
@@ -1,3 +1,86 @@
+2018-10-02  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Copying content with shadow DOM doesn't copy any contents
+        https://bugs.webkit.org/show_bug.cgi?id=157443
+
+        Reviewed by Wenson Hsieh.
+
+        This patch adds the support for copying and pasting content across shadow boundaries in HTML and plain text,
+        which is enabled whenever selection across shadow boundaries is enabled.
+
+        To do this, TextIterator now has a constructor which takes two Positions, and the node traversal code in
+        StyledMarkupAccumulator has been abstracted via helper functions as done for TextIterator.
+
+        When serializing a HTMl slot element, serialize it as a span with "display: contents" to make sure when
+        the content is pasted into a shadow tree, it wouldn't affect the slot assignment of the shadow tree.
+
+        Tests: editing/pasteboard/copy-paste-across-shadow-boundaries-1.html
+               editing/pasteboard/copy-paste-across-shadow-boundaries-2.html
+               editing/pasteboard/copy-paste-across-shadow-boundaries-3.html
+               editing/pasteboard/copy-paste-across-shadow-boundaries-4.html
+               editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-1.html
+               editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html
+               editing/pasteboard/copy-paste-with-shadow-content.html
+
+        * dom/ComposedTreeIterator.h:
+        (WebCore::assignedSlotIgnoringUserAgentShadow): Moved from TextIterator.cpp.
+        (WebCore::shadowRootIgnoringUserAgentShadow): Ditto.
+        (WebCore::firstChildInComposedTreeIgnoringUserAgentShadow): Ditto.
+        (WebCore::nextSiblingInComposedTreeIgnoringUserAgentShadow): Ditto.
+        * dom/Position.h:
+        (WebCore::Position::treeScope const): Added.
+        * editing/EditingStyle.cpp:
+        (WebCore::EditingStyle::addDisplayContents): Added.
+        * editing/EditingStyle.h:
+        * editing/Editor.cpp:
+        (WebCore::Editor::selectedText const): Use the new behavior when selectionAcrossShadowBoundariesEnabled is set.
+        (WebCore::Editor::selectedTextForDataTransfer const): Ditto.
+        * editing/MarkupAccumulator.cpp:
+        (WebCore::MarkupAccumulator::appendEndElement): Renamed from appendEndTag. Now takes StringBuilder.
+        * editing/MarkupAccumulator.h:
+        (WebCore::MarkupAccumulator::appendEndTag):
+        * editing/TextIterator.cpp:
+        (WebCore::TextIterator::TextIterator): Added a new variant which takes two positions.
+        (WebCore::TextIterator::init):
+        (WebCore::firstChild):
+        (WebCore::nextSibling):
+        (WebCore::plainText): Ditto.
+        * editing/TextIterator.h:
+        * editing/cocoa/EditorCocoa.mm:
+        (WebCore::Editor::selectionInHTMLFormat): Use the new behavior if selectionAcrossShadowBoundariesEnabled is set.
+        * editing/gtk/EditorGtk.cpp:
+        (WebCore::Editor::writeSelectionToPasteboard): Ditto.
+        * editing/markup.cpp:
+        (WebCore::StyledMarkupAccumulator::parentNode): Added.
+        (WebCore::StyledMarkupAccumulator::firstChild): Added.
+        (WebCore::StyledMarkupAccumulator::nextSibling): Added.
+        (WebCore::StyledMarkupAccumulator::nextSkippingChildren): Added.
+        (WebCore::StyledMarkupAccumulator::hasChildNodes): Added.
+        (WebCore::StyledMarkupAccumulator::isDescendantOf): Added.
+        (WebCore::StyledMarkupAccumulator::StyledMarkupAccumulator):
+        (WebCore::StyledMarkupAccumulator::appendElement): Serialize a slot element as a span with display: contents.
+        (WebCore::StyledMarkupAccumulator::appendEndElement): Added. Ditto.
+        (WebCore::StyledMarkupAccumulator::serializeNodes):
+        (WebCore::StyledMarkupAccumulator::traverseNodesForSerialization): Use the newly added helper functions to
+        traverse the composed tree when m_useComposedTree is set.
+        (WebCore::commonShadowIncludingAncestor): Added.
+        (WebCore::serializePreservingVisualAppearanceInternal): Added SerializeComposedTree as an argument. Also use
+        StyledMarkupAccumulator::parentNode to serialize special common ancestors; e.g. to preserve b, i, etc...
+        (WebCore::serializePreservingVisualAppearance): Ditto to the variant which takes VisibleSelection.
+        (WebCore::sanitizedMarkupForFragmentInDocument):
+        * editing/markup.h:
+        * editing/wpe/EditorWPE.cpp:
+        (WebCore::Editor::writeSelectionToPasteboard):
+        * loader/archive/cf/LegacyWebArchive.cpp:
+        (WebCore::LegacyWebArchive::createFromSelection):
+        * page/PageSerializer.cpp:
+        (WebCore::PageSerializer::SerializerMarkupAccumulator::appendEndElement):
+        * testing/Internals.cpp:
+        (WebCore::Internals::setSelectionWithoutValidation): Added. A helper function to create a selection across
+        shadow boundaries for testing purposes.
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2018-10-02  Chris Dumez  <cdumez@apple.com>
 
         MessageEvent.ports should return the same object
index 89740cf..2c3d76f 100644 (file)
@@ -26,6 +26,7 @@
 #pragma once
 
 #include "ElementAndTextDescendantIterator.h"
+#include "HTMLSlotElement.h"
 #include "ShadowRoot.h"
 
 namespace WebCore {
@@ -199,4 +200,49 @@ inline ComposedTreeChildAdapter composedTreeChildren(ContainerNode& parent)
 enum class ComposedTreeAsTextMode { Normal, WithPointers };
 WEBCORE_EXPORT String composedTreeAsText(ContainerNode& root, ComposedTreeAsTextMode = ComposedTreeAsTextMode::Normal);
 
+
+// Helper functions for walking the composed tree.
+// FIXME: Use ComposedTreeIterator instead. These functions are more expensive because they might do O(n) work.
+
+inline HTMLSlotElement* assignedSlotIgnoringUserAgentShadow(Node& node)
+{
+    auto* slot = node.assignedSlot();
+    if (!slot || slot->containingShadowRoot()->mode() == ShadowRootMode::UserAgent)
+        return nullptr;
+    return slot;
+}
+
+inline ShadowRoot* shadowRootIgnoringUserAgentShadow(Node& node)
+{
+    auto* shadowRoot = node.shadowRoot();
+    if (!shadowRoot || shadowRoot->mode() == ShadowRootMode::UserAgent)
+        return nullptr;
+    return shadowRoot;
+}
+
+inline Node* firstChildInComposedTreeIgnoringUserAgentShadow(Node& node)
+{
+    if (auto* shadowRoot = shadowRootIgnoringUserAgentShadow(node))
+        return shadowRoot->firstChild();
+    if (is<HTMLSlotElement>(node)) {
+        if (auto* assignedNodes = downcast<HTMLSlotElement>(node).assignedNodes())
+            return assignedNodes->at(0);
+    }
+    return node.firstChild();
+}
+
+inline Node* nextSiblingInComposedTreeIgnoringUserAgentShadow(Node& node)
+{
+    if (auto* slot = assignedSlotIgnoringUserAgentShadow(node)) {
+        auto* assignedNodes = slot->assignedNodes();
+        ASSERT(assignedNodes);
+        auto nodeIndex = assignedNodes->find(&node);
+        ASSERT(nodeIndex != notFound);
+        if (assignedNodes->size() > nodeIndex + 1)
+            return assignedNodes->at(nodeIndex + 1);
+        return nullptr;
+    }
+    return node.nextSibling();
+}
+
 } // namespace WebCore
index d94e354..2f15868 100644 (file)
@@ -117,6 +117,7 @@ public:
     Node* deprecatedNode() const { return m_anchorNode.get(); }
 
     Document* document() const { return m_anchorNode ? &m_anchorNode->document() : nullptr; }
+    TreeScope* treeScope() const { return m_anchorNode ? &m_anchorNode->treeScope() : nullptr; }
     Element* rootEditableElement() const
     {
         Node* container = containerNode();
index 622adf9..7c94f02 100644 (file)
@@ -1369,6 +1369,13 @@ void EditingStyle::forceInline()
     m_mutableStyle->setProperty(CSSPropertyDisplay, CSSValueInline, propertyIsImportant);
 }
 
+void EditingStyle::addDisplayContents()
+{
+    if (!m_mutableStyle)
+        m_mutableStyle = MutableStyleProperties::create();
+    m_mutableStyle->setProperty(CSSPropertyDisplay, CSSValueContents);
+}
+
 bool EditingStyle::convertPositionStyle()
 {
     if (!m_mutableStyle)
index 4419bd1..b7991ef 100644 (file)
@@ -149,6 +149,7 @@ public:
     void removeStyleFromRulesAndContext(StyledElement&, Node* context);
     void removePropertiesInElementDefaultStyle(Element&);
     void forceInline();
+    void addDisplayContents();
     bool convertPositionStyle();
     bool isFloating();
     int legacyFontSize(Document&) const;
index 67e7eee..b7f3e8c 100644 (file)
@@ -3182,18 +3182,25 @@ void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, O
 
 String Editor::selectedText() const
 {
-    return selectedText(TextIteratorDefaultBehavior);
+    TextIteratorBehavior behavior = TextIteratorDefaultBehavior;
+    if (m_frame.settings().selectionAcrossShadowBoundariesEnabled())
+        behavior |= TextIteratorTraversesFlatTree;
+    return selectedText(behavior);
 }
 
 String Editor::selectedTextForDataTransfer() const
 {
-    return selectedText(TextIteratorEmitsImageAltText);
+    TextIteratorBehavior behavior = TextIteratorEmitsImageAltText;
+    if (m_frame.settings().selectionAcrossShadowBoundariesEnabled())
+        behavior |= TextIteratorTraversesFlatTree;
+    return selectedText(behavior);
 }
 
 String Editor::selectedText(TextIteratorBehavior behavior) const
 {
     // We remove '\0' characters because they are not visibly rendered to the user.
-    return plainText(m_frame.selection().toNormalizedRange().get(), behavior).replaceWithLiteral('\0', "");
+    auto& selection = m_frame.selection().selection();
+    return plainText(selection.start(), selection.end(), behavior).replaceWithLiteral('\0', "");
 }
 
 static inline void collapseCaretWidth(IntRect& rect)
index 353dba6..45aee59 100644 (file)
@@ -192,9 +192,9 @@ void MarkupAccumulator::appendStartTag(const Node& node, Namespaces* namespaces)
         m_nodes->append(const_cast<Node*>(&node));
 }
 
-void MarkupAccumulator::appendEndTag(const Element& element)
+void MarkupAccumulator::appendEndElement(StringBuilder& out, const Element& element)
 {
-    appendEndMarkup(m_markup, element);
+    appendEndMarkup(out, element);
 }
 
 void MarkupAccumulator::appendTextSubstring(const Text& text, unsigned start, unsigned length)
index 3311c0c..47f0743 100644 (file)
@@ -76,10 +76,10 @@ protected:
     void appendEndTag(const Node& node)
     {
         if (is<Element>(node))
-            appendEndTag(downcast<Element>(node));
+            appendEndElement(m_markup, downcast<Element>(node));
     }
 
-    virtual void appendEndTag(const Element&);
+    virtual void appendEndElement(StringBuilder&, const Element&);
     virtual void appendCustomAttributes(StringBuilder&, const Element&, Namespaces*);
     virtual void appendText(StringBuilder&, const Text&);
     virtual void appendElement(StringBuilder&, const Element&, Namespaces*);
index 9dd88eb..e7ae75c 100644 (file)
@@ -27,6 +27,7 @@
 #include "config.h"
 #include "TextIterator.h"
 
+#include "ComposedTreeIterator.h"
 #include "Document.h"
 #include "Editing.h"
 #include "FontCascade.h"
@@ -339,10 +340,35 @@ void TextIteratorCopyableText::appendToStringBuilder(StringBuilder& builder) con
 
 // --------
 
+
+TextIterator::TextIterator(Position start, Position end, TextIteratorBehavior behavior)
+    : m_behavior(behavior)
+{
+    if (start.isNull() || end.isNull())
+        return;
+    ASSERT(comparePositions(start, end) <= 0);
+
+    RELEASE_ASSERT(behavior & TextIteratorTraversesFlatTree || start.treeScope() == end.treeScope());
+
+    start.document()->updateLayoutIgnorePendingStylesheets();
+
+    // FIXME: Use Position / PositionIterator instead to avoid offset computation.
+    m_startContainer = start.containerNode();
+    m_startOffset = start.computeOffsetInContainerNode();
+
+    m_endContainer = end.containerNode();
+    m_endOffset = end.computeOffsetInContainerNode();
+
+    m_node = start.firstNode().get();
+    if (!m_node)
+        return;
+
+    init();
+}
+
 TextIterator::TextIterator(const Range* range, TextIteratorBehavior behavior)
     : m_behavior(behavior)
 {
-    // FIXME: Only m_positionNode above needs to be initialized if range is null.
     if (!range)
         return;
 
@@ -358,11 +384,15 @@ TextIterator::TextIterator(const Range* range, TextIteratorBehavior behavior)
     m_endContainer = &range->endContainer();
     m_endOffset = range->endOffset();
 
-    // Set up the current node for processing.
     m_node = range->firstNode();
     if (!m_node)
         return;
 
+    init();
+}
+
+void TextIterator::init()
+{
     if (isClippedByFrameAncestor(m_node->document(), m_behavior))
         return;
 
@@ -379,59 +409,18 @@ TextIterator::TextIterator(const Range* range, TextIteratorBehavior behavior)
 
 TextIterator::~TextIterator() = default;
 
-static HTMLSlotElement* assignedAuthorSlot(Node& node)
-{
-    auto* slot = node.assignedSlot();
-    if (!slot || slot->containingShadowRoot()->mode() == ShadowRootMode::UserAgent)
-        return nullptr;
-    return slot;
-}
-
-static ShadowRoot* authorShadowRoot(Node& node)
-{
-    auto* shadowRoot = node.shadowRoot();
-    if (!shadowRoot || shadowRoot->mode() == ShadowRootMode::UserAgent)
-        return nullptr;
-    return shadowRoot;
-}
-
 // FIXME: Use ComposedTreeIterator instead. These functions are more expensive because they might do O(n) work.
-static inline Node* firstChildInFlatTreeIgnoringUserAgentShadow(Node& node)
-{
-    if (auto* shadowRoot = authorShadowRoot(node))
-        return shadowRoot->firstChild();
-    if (is<HTMLSlotElement>(node)) {
-        if (auto* assignedNodes = downcast<HTMLSlotElement>(node).assignedNodes())
-            return assignedNodes->at(0);
-    }
-    return node.firstChild();
-}
-
-static inline Node* nextSiblingInFlatTreeIgnoringUserAgentShadow(Node& node)
-{
-    if (auto* slot = assignedAuthorSlot(node)) {
-        auto* assignedNodes = slot->assignedNodes();
-        ASSERT(assignedNodes);
-        auto nodeIndex = assignedNodes->find(&node);
-        ASSERT(nodeIndex != notFound);
-        if (assignedNodes->size() > nodeIndex + 1)
-            return assignedNodes->at(nodeIndex + 1);
-        return nullptr;
-    }
-    return node.nextSibling();
-}
-
 static inline Node* firstChild(TextIteratorBehavior options, Node& node)
 {
     if (UNLIKELY(options & TextIteratorTraversesFlatTree))
-        return firstChildInFlatTreeIgnoringUserAgentShadow(node);
+        return firstChildInComposedTreeIgnoringUserAgentShadow(node);
     return node.firstChild();
 }
 
 static inline Node* nextSibling(TextIteratorBehavior options, Node& node)
 {
     if (UNLIKELY(options & TextIteratorTraversesFlatTree))
-        return nextSiblingInFlatTreeIgnoringUserAgentShadow(node);
+        return nextSiblingInComposedTreeIgnoringUserAgentShadow(node);
     return node.nextSibling();
 }
 
@@ -2654,19 +2643,23 @@ bool hasAnyPlainText(const Range& range, TextIteratorBehavior behavior)
     return false;
 }
 
-String plainText(const Range* r, TextIteratorBehavior defaultBehavior, bool isDisplayString)
+String plainText(Position start, Position end, TextIteratorBehavior defaultBehavior, bool isDisplayString)
 {
     // The initial buffer size can be critical for performance: https://bugs.webkit.org/show_bug.cgi?id=81192
     static const unsigned initialCapacity = 1 << 15;
 
+    if (!start.document())
+        return { };
+    auto document = makeRef(*start.document());
+
     unsigned bufferLength = 0;
     StringBuilder builder;
     builder.reserveCapacity(initialCapacity);
     TextIteratorBehavior behavior = defaultBehavior;
     if (!isDisplayString)
         behavior = static_cast<TextIteratorBehavior>(behavior | TextIteratorEmitsTextsWithoutTranscoding);
-    
-    for (TextIterator it(r, behavior); !it.atEnd(); it.advance()) {
+
+    for (TextIterator it(start, end, behavior); !it.atEnd(); it.advance()) {
         it.appendTextToStringBuilder(builder);
         bufferLength += it.text().length();
     }
@@ -2677,11 +2670,18 @@ String plainText(const Range* r, TextIteratorBehavior defaultBehavior, bool isDi
     String result = builder.toString();
 
     if (isDisplayString)
-        r->ownerDocument().displayStringModifiedByEncoding(result);
+        document->displayStringModifiedByEncoding(result);
 
     return result;
 }
 
+String plainText(const Range* range, TextIteratorBehavior defaultBehavior, bool isDisplayString)
+{
+    if (!range)
+        return emptyString();
+    return plainText(range->startPosition(), range->endPosition(), defaultBehavior, isDisplayString);
+}
+
 String plainTextReplacingNoBreakSpace(const Range* range, TextIteratorBehavior defaultBehavior, bool isDisplayString)
 {
     return plainText(range, defaultBehavior, isDisplayString).replace(noBreakSpace, ' ');
index 860e74c..c279ae5 100644 (file)
@@ -43,6 +43,8 @@ namespace SimpleLineLayout {
 class RunResolver;
 }
 
+WEBCORE_EXPORT String plainText(Position start, Position end, TextIteratorBehavior = TextIteratorDefaultBehavior, bool isDisplayString = false);
+
 WEBCORE_EXPORT String plainText(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior, bool isDisplayString = false);
 WEBCORE_EXPORT String plainTextReplacingNoBreakSpace(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior, bool isDisplayString = false);
 Ref<Range> findPlainText(const Range&, const String&, FindOptions);
@@ -99,6 +101,7 @@ private:
 
 class TextIterator {
 public:
+    explicit TextIterator(Position start, Position end, TextIteratorBehavior = TextIteratorDefaultBehavior);
     WEBCORE_EXPORT explicit TextIterator(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior);
     WEBCORE_EXPORT ~TextIterator();
 
@@ -118,6 +121,7 @@ public:
     WEBCORE_EXPORT static Ref<Range> subrange(Range& entireRange, int characterOffset, int characterCount);
 
 private:
+    void init();
     void exitNode(Node*);
     bool shouldRepresentNodeOffsetZero();
     bool shouldEmitSpaceBeforeAndAfterNode(Node&);
index af61f36..abe3db3 100644 (file)
@@ -49,6 +49,7 @@
 #import "Pasteboard.h"
 #import "RenderElement.h"
 #import "RenderStyle.h"
+#import "Settings.h"
 #import "Text.h"
 #import "WebContentReader.h"
 #import "WebCoreNSURLExtras.h"
@@ -75,7 +76,8 @@ static RefPtr<SharedBuffer> archivedDataForAttributedString(NSAttributedString *
 
 String Editor::selectionInHTMLFormat()
 {
-    return serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy);
+    return serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy,
+        m_frame.settings().selectionAcrossShadowBoundariesEnabled() ? SerializeComposedTree::Yes : SerializeComposedTree::No);
 }
 
 #if ENABLE(ATTACHMENT_ELEMENT)
index be5e2e3..819f674 100644 (file)
@@ -43,6 +43,7 @@
 #include "SVGElement.h"
 #include "SVGImageElement.h"
 #include "SelectionData.h"
+#include "Settings.h"
 #include "XLinkNames.h"
 #include "markup.h"
 #include <cairo.h>
@@ -146,7 +147,8 @@ void Editor::writeSelectionToPasteboard(Pasteboard& pasteboard)
     PasteboardWebContent pasteboardContent;
     pasteboardContent.canSmartCopyOrDelete = canSmartCopyOrDelete();
     pasteboardContent.text = selectedTextForDataTransfer();
-    pasteboardContent.markup = serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy);
+    pasteboardContent.markup = serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy,
+        m_frame.settings().selectionAcrossShadowBoundariesEnabled() ? SerializeComposedTree::Yes : SerializeComposedTree::No);
     pasteboard.write(pasteboardContent);
 }
 
index f0e0d86..1729e0d 100644 (file)
@@ -37,6 +37,7 @@
 #include "CacheStorageProvider.h"
 #include "ChildListMutationScope.h"
 #include "Comment.h"
+#include "ComposedTreeIterator.h"
 #include "DocumentFragment.h"
 #include "DocumentLoader.h"
 #include "DocumentType.h"
@@ -219,8 +220,8 @@ class StyledMarkupAccumulator final : public MarkupAccumulator {
 public:
     enum RangeFullySelectsNode { DoesFullySelectNode, DoesNotFullySelectNode };
 
-    StyledMarkupAccumulator(const Position& start, const Position& end, Vector<Node*>* nodes,
-        ResolveURLs, AnnotateForInterchange, MSOListMode, bool needsPositionStyleConversion, Node* highestNodeToBeSerialized = nullptr);
+    StyledMarkupAccumulator(const Position& start, const Position& end, Vector<Node*>* nodes, ResolveURLs, SerializeComposedTree,
+        AnnotateForInterchange, MSOListMode, bool needsPositionStyleConversion, Node* highestNodeToBeSerialized = nullptr);
 
     Node* serializeNodes(const Position& start, const Position& end);
     void wrapWithNode(Node&, bool convertBlocksToInlines = false, RangeFullySelectsNode = DoesFullySelectNode);
@@ -232,6 +233,13 @@ public:
 
     using MarkupAccumulator::appendString;
 
+    ContainerNode* parentNode(Node& node)
+    {
+        if (UNLIKELY(m_useComposedTree))
+            return node.parentInComposedTree();
+        return node.parentOrShadowHostNode();
+    }
+
 private:
     void appendStyleNodeOpenTag(StringBuilder&, StyleProperties*, Document&, bool isBlock = false);
     const String& styleNodeCloseTag(bool isBlock = false);
@@ -242,6 +250,7 @@ private:
     bool shouldPreserveMSOListStyleForElement(const Element&);
 
     void appendElement(StringBuilder& out, const Element&, bool addDisplayInline, RangeFullySelectsNode);
+    void appendEndElement(StringBuilder& out, const Element&) override;
     void appendCustomAttributes(StringBuilder&, const Element&, Namespaces*) override;
 
     void appendText(StringBuilder& out, const Text&) override;
@@ -250,6 +259,48 @@ private:
         appendElement(out, element, false, DoesFullySelectNode);
     }
 
+    Node* firstChild(Node& node)
+    {
+        if (UNLIKELY(m_useComposedTree))
+            return firstChildInComposedTreeIgnoringUserAgentShadow(node);
+        return node.firstChild();
+    }
+
+    Node* nextSibling(Node& node)
+    {
+        if (UNLIKELY(m_useComposedTree))
+            return nextSiblingInComposedTreeIgnoringUserAgentShadow(node);
+        return node.nextSibling();
+    }
+    
+    Node* nextSkippingChildren(Node& node)
+    {
+        if (UNLIKELY(m_useComposedTree)) {
+            if (auto* sibling = nextSiblingInComposedTreeIgnoringUserAgentShadow(node))
+                return sibling;
+            for (auto* ancestor = node.parentInComposedTree(); ancestor; ancestor = ancestor->parentInComposedTree()) {
+                if (auto* sibling = nextSiblingInComposedTreeIgnoringUserAgentShadow(*ancestor))
+                    return sibling;
+            }
+            return nullptr;
+        }
+        return NodeTraversal::nextSkippingChildren(node);
+    }
+
+    bool hasChildNodes(Node& node)
+    {
+        if (UNLIKELY(m_useComposedTree))
+            return firstChildInComposedTreeIgnoringUserAgentShadow(node);
+        return node.hasChildNodes();
+    }
+
+    bool isDescendantOf(Node& node, Node& possibleAncestor)
+    {
+        if (UNLIKELY(m_useComposedTree))
+            return node.isDescendantOrShadowDescendantOf(&possibleAncestor);
+        return node.isDescendantOf(&possibleAncestor);
+    }
+
     enum class NodeTraversalMode { EmitString, DoNotEmitString };
     Node* traverseNodesForSerialization(Node* startNode, Node* pastEnd, NodeTraversalMode);
 
@@ -271,6 +322,7 @@ private:
     const AnnotateForInterchange m_annotate;
     RefPtr<Node> m_highestNodeToBeSerialized;
     RefPtr<EditingStyle> m_wrappingStyle;
+    bool m_useComposedTree;
     bool m_needsPositionStyleConversion;
     bool m_needRelativeStyleWrapper { false };
     bool m_needClearingDiv { false };
@@ -278,13 +330,14 @@ private:
     bool m_inMSOList { false };
 };
 
-inline StyledMarkupAccumulator::StyledMarkupAccumulator(const Position& start, const Position& end, Vector<Node*>* nodes,
-    ResolveURLs urlsToResolve, AnnotateForInterchange annotate, MSOListMode msoListMode, bool needsPositionStyleConversion, Node* highestNodeToBeSerialized)
+inline StyledMarkupAccumulator::StyledMarkupAccumulator(const Position& start, const Position& end, Vector<Node*>* nodes, ResolveURLs urlsToResolve, SerializeComposedTree serializeComposedTree,
+    AnnotateForInterchange annotate, MSOListMode msoListMode, bool needsPositionStyleConversion, Node* highestNodeToBeSerialized)
     : MarkupAccumulator(nodes, urlsToResolve)
     , m_start(start)
     , m_end(end)
     , m_annotate(annotate)
     , m_highestNodeToBeSerialized(highestNodeToBeSerialized)
+    , m_useComposedTree(serializeComposedTree == SerializeComposedTree::Yes)
     , m_needsPositionStyleConversion(needsPositionStyleConversion)
     , m_shouldPreserveMSOList(msoListMode == MSOListMode::Preserve)
 {
@@ -445,12 +498,16 @@ bool StyledMarkupAccumulator::shouldPreserveMSOListStyleForElement(const Element
 void StyledMarkupAccumulator::appendElement(StringBuilder& out, const Element& element, bool addDisplayInline, RangeFullySelectsNode rangeFullySelectsNode)
 {
     const bool documentIsHTML = element.document().isHTMLDocument();
-    appendOpenTag(out, element, 0);
+    const bool isSlotElement = is<HTMLSlotElement>(element);
+    if (UNLIKELY(isSlotElement))
+        out.append("<span");
+    else
+        appendOpenTag(out, element, nullptr);
 
     appendCustomAttributes(out, element, nullptr);
 
     const bool shouldAnnotateOrForceInline = element.isHTMLElement() && (shouldAnnotate() || addDisplayInline);
-    bool shouldOverrideStyleAttr = (shouldAnnotateOrForceInline || shouldApplyWrappingStyle(element)) && !shouldPreserveMSOListStyleForElement(element);
+    bool shouldOverrideStyleAttr = (shouldAnnotateOrForceInline || shouldApplyWrappingStyle(element) || isSlotElement) && !shouldPreserveMSOListStyleForElement(element);
     if (element.hasAttributes()) {
         for (const Attribute& attribute : element.attributesIterator()) {
             // We'll handle the style attribute separately, below.
@@ -472,6 +529,9 @@ void StyledMarkupAccumulator::appendElement(StringBuilder& out, const Element& e
         } else
             newInlineStyle = EditingStyle::create();
 
+        if (isSlotElement)
+            newInlineStyle->addDisplayContents();
+
         if (is<StyledElement>(element) && downcast<StyledElement>(element).inlineStyle())
             newInlineStyle->overrideWithStyle(*downcast<StyledElement>(element).inlineStyle());
 
@@ -503,13 +563,21 @@ void StyledMarkupAccumulator::appendElement(StringBuilder& out, const Element& e
     appendCloseTag(out, element);
 }
 
+void StyledMarkupAccumulator::appendEndElement(StringBuilder& out, const Element& element)
+{
+    if (UNLIKELY(is<HTMLSlotElement>(element)))
+        out.append("</span>");
+    else
+        MarkupAccumulator::appendEndElement(out, element);
+}
+
 Node* StyledMarkupAccumulator::serializeNodes(const Position& start, const Position& end)
 {
     ASSERT(comparePositions(start, end) <= 0);
     auto startNode = start.firstNode();
     Node* pastEnd = end.computeNodeAfterPosition();
-    if (!pastEnd)
-        pastEnd = NodeTraversal::nextSkippingChildren(*end.containerNode());
+    if (!pastEnd && end.containerNode())
+        pastEnd = nextSkippingChildren(*end.containerNode());
 
     if (!m_highestNodeToBeSerialized) {
         Node* lastClosed = traverseNodesForSerialization(startNode.get(), pastEnd, NodeTraversalMode::DoNotEmitString);
@@ -535,7 +603,8 @@ Node* StyledMarkupAccumulator::traverseNodesForSerialization(Node* startNode, No
                 return false;
         }
 
-        if (!node.renderer() && !enclosingElementWithTag(firstPositionInOrBeforeNode(&node), selectTag))
+        bool isDisplayContents = is<Element>(node) && downcast<Element>(node).hasDisplayContents();
+        if (!node.renderer() && !isDisplayContents && !enclosingElementWithTag(firstPositionInOrBeforeNode(&node), selectTag))
             return false;
 
         ++depth;
@@ -561,20 +630,19 @@ Node* StyledMarkupAccumulator::traverseNodesForSerialization(Node* startNode, No
 
     Node* lastNode = nullptr;
     Node* next = nullptr;
-    for (Node* n = startNode; n != pastEnd; n = next) {
-        lastNode = n;
+    for (auto* n = startNode; n != pastEnd; lastNode = n, n = next) {
 
         Vector<Node*, 8> exitedAncestors;
         next = nullptr;
-        if (auto* firstChild = n->firstChild())
-            next = firstChild;
-        else if (auto* nextSibling = n->nextSibling())
-            next = nextSibling;
+        if (auto* child = firstChild(*n))
+            next = child;
+        else if (auto* sibling = nextSibling(*n))
+            next = sibling;
         else {
-            for (auto* ancestor = n->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
+            for (auto* ancestor = parentNode(*n); ancestor; ancestor = parentNode(*ancestor)) {
                 exitedAncestors.append(ancestor);
-                if (auto* nextSibling = ancestor->nextSibling()) {
-                    next = nextSibling;
+                if (auto* sibling = nextSibling(*ancestor)) {
+                    next = sibling;
                     break;
                 }
             }
@@ -588,10 +656,10 @@ Node* StyledMarkupAccumulator::traverseNodesForSerialization(Node* startNode, No
         if (!enterNode(*n)) {
             next = NodeTraversal::nextSkippingChildren(*n);
             // Don't skip over pastEnd.
-            if (pastEnd && pastEnd->isDescendantOf(*n))
+            if (pastEnd && isDescendantOf(*pastEnd, *n))
                 next = pastEnd;
         } else {
-            if (!n->hasChildNodes())
+            if (!hasChildNodes(*n))
                 exitNode(*n);
         }
 
@@ -601,9 +669,12 @@ Node* StyledMarkupAccumulator::traverseNodesForSerialization(Node* startNode, No
             exitNode(*ancestor);
         }
     }
-
-    for (auto* ancestor = (pastEnd ? pastEnd : lastNode)->parentNode(); ancestor && depth; ancestor = ancestor->parentNode())
-        exitNode(*ancestor);
+    
+    ASSERT(lastNode || !depth);
+    if (depth) {
+        for (auto* ancestor = parentNode(pastEnd ? *pastEnd : *lastNode); ancestor && depth; ancestor = parentNode(*ancestor))
+            exitNode(*ancestor);
+    }
 
     return lastClosed;
 }
@@ -754,15 +825,27 @@ static Node* highestAncestorToWrapMarkup(const Position& start, const Position&
     return specialCommonAncestor;
 }
 
-static String serializePreservingVisualAppearanceInternal(const Position& start, const Position& end, Vector<Node*>* nodes,
-    AnnotateForInterchange annotate, ConvertBlocksToInlines convertBlocksToInlines, ResolveURLs urlsToResolve, MSOListMode msoListMode)
+static RefPtr<Node> commonShadowIncludingAncestor(const Position& a, const Position& b)
+{
+    TreeScope* commonScope = commonTreeScope(a.containerNode(), b.containerNode());
+    if (!commonScope)
+        return nullptr;
+    auto* nodeA = commonScope->ancestorNodeInThisScope(a.containerNode());
+    ASSERT(nodeA);
+    auto* nodeB = commonScope->ancestorNodeInThisScope(b.containerNode());
+    ASSERT(nodeB);
+    return Range::commonAncestorContainer(nodeA, nodeB);
+}
+
+static String serializePreservingVisualAppearanceInternal(const Position& start, const Position& end, Vector<Node*>* nodes, ResolveURLs urlsToResolve, SerializeComposedTree serializeComposedTree,
+    AnnotateForInterchange annotate, ConvertBlocksToInlines convertBlocksToInlines, MSOListMode msoListMode)
 {
     static NeverDestroyed<const String> interchangeNewlineString(MAKE_STATIC_STRING_IMPL("<br class=\"" AppleInterchangeNewline "\">"));
 
     if (!comparePositions(start, end))
         return emptyString();
 
-    RefPtr<Node> commonAncestor = Range::commonAncestorContainer(start.containerNode(), end.containerNode());
+    RefPtr<Node> commonAncestor = commonShadowIncludingAncestor(start, end);
     if (!commonAncestor)
         return emptyString();
 
@@ -781,7 +864,7 @@ static String serializePreservingVisualAppearanceInternal(const Position& start,
 
     Node* specialCommonAncestor = highestAncestorToWrapMarkup(start, end, *commonAncestor, annotate);
 
-    StyledMarkupAccumulator accumulator(start, end, nodes, urlsToResolve, annotate, msoListMode, needsPositionStyleConversion, specialCommonAncestor);
+    StyledMarkupAccumulator accumulator(start, end, nodes, urlsToResolve, serializeComposedTree, annotate, msoListMode, needsPositionStyleConversion, specialCommonAncestor);
 
     Position startAdjustedForInterchangeNewline = start;
     if (annotate == AnnotateForInterchange::Yes && needInterchangeNewlineAfter(visibleStart)) {
@@ -799,7 +882,7 @@ static String serializePreservingVisualAppearanceInternal(const Position& start,
 
     if (specialCommonAncestor && lastClosed) {
         // Also include all of the ancestors of lastClosed up to this special ancestor.
-        for (ContainerNode* ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
+        for (ContainerNode* ancestor = accumulator.parentNode(*lastClosed); ancestor; ancestor = accumulator.parentNode(*ancestor)) {
             if (ancestor == fullySelectedRoot && convertBlocksToInlines == ConvertBlocksToInlines::No) {
                 RefPtr<EditingStyle> fullySelectedRootStyle = styleFromMatchedRulesAndInlineDecl(*fullySelectedRoot);
 
@@ -849,13 +932,14 @@ static String serializePreservingVisualAppearanceInternal(const Position& start,
 
 String serializePreservingVisualAppearance(const Range& range, Vector<Node*>* nodes, AnnotateForInterchange annotate, ConvertBlocksToInlines convertBlocksToInlines, ResolveURLs urlsToReslve)
 {
-    return serializePreservingVisualAppearanceInternal(range.startPosition(), range.endPosition(), nodes, annotate, convertBlocksToInlines, urlsToReslve, MSOListMode::DoNotPreserve);
+    return serializePreservingVisualAppearanceInternal(range.startPosition(), range.endPosition(), nodes, urlsToReslve, SerializeComposedTree::No,
+        annotate, convertBlocksToInlines, MSOListMode::DoNotPreserve);
 }
 
-String serializePreservingVisualAppearance(const VisibleSelection& selection, ResolveURLs resolveURLs, Vector<Node*>* nodes)
+String serializePreservingVisualAppearance(const VisibleSelection& selection, ResolveURLs resolveURLs, SerializeComposedTree serializeComposedTree, Vector<Node*>* nodes)
 {
-    return serializePreservingVisualAppearanceInternal(selection.start(), selection.end(), nodes,
-        AnnotateForInterchange::Yes, ConvertBlocksToInlines::No, resolveURLs, MSOListMode::DoNotPreserve);
+    return serializePreservingVisualAppearanceInternal(selection.start(), selection.end(), nodes, resolveURLs, serializeComposedTree,
+        AnnotateForInterchange::Yes, ConvertBlocksToInlines::No, MSOListMode::DoNotPreserve);
 }
 
 
@@ -880,8 +964,9 @@ String sanitizedMarkupForFragmentInDocument(Ref<DocumentFragment>&& fragment, Do
     ASSERT(bodyElement);
     bodyElement->appendChild(fragment.get());
 
+    // SerializeComposedTree::No because there can't be a shadow tree in the pasted fragment.
     auto result = serializePreservingVisualAppearanceInternal(firstPositionInNode(bodyElement.get()), lastPositionInNode(bodyElement.get()), nullptr,
-        AnnotateForInterchange::Yes, ConvertBlocksToInlines::No, ResolveURLs::YesExcludingLocalFileURLsForPrivacy, msoListMode);
+        ResolveURLs::YesExcludingLocalFileURLsForPrivacy, SerializeComposedTree::No, AnnotateForInterchange::Yes, ConvertBlocksToInlines::No,  msoListMode);
 
     if (msoListMode == MSOListMode::Preserve) {
         StringBuilder builder;
index b550a8f..0b8314b 100644 (file)
@@ -70,8 +70,9 @@ ExceptionOr<void> replaceChildrenWithFragment(ContainerNode&, Ref<DocumentFragme
 
 enum class ResolveURLs : uint8_t { No, Yes, YesExcludingLocalFileURLsForPrivacy };
 enum class ConvertBlocksToInlines : uint8_t { No, Yes };
+enum class SerializeComposedTree : uint8_t { No, Yes };
 WEBCORE_EXPORT String serializePreservingVisualAppearance(const Range&, Vector<Node*>* = nullptr, AnnotateForInterchange = AnnotateForInterchange::No, ConvertBlocksToInlines = ConvertBlocksToInlines::No, ResolveURLs = ResolveURLs::No);
-String serializePreservingVisualAppearance(const VisibleSelection&, ResolveURLs = ResolveURLs::No, Vector<Node*>* = nullptr);
+String serializePreservingVisualAppearance(const VisibleSelection&, ResolveURLs = ResolveURLs::No, SerializeComposedTree = SerializeComposedTree::No, Vector<Node*>* = nullptr);
 
 enum class SerializedNodes : uint8_t { SubtreeIncludingNode, SubtreesOfChildren };
 enum class SerializationSyntax : uint8_t { HTML, XML };
index 2dcf8c3..b50540a 100644 (file)
@@ -64,7 +64,8 @@ void Editor::writeSelectionToPasteboard(Pasteboard& pasteboard)
 {
     PasteboardWebContent pasteboardContent;
     pasteboardContent.text = selectedTextForDataTransfer();
-    pasteboardContent.markup = serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy);
+    pasteboardContent.markup = serializePreservingVisualAppearance(m_frame.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy,
+        m_frame.settings().selectionAcrossShadowBoundariesEnabled() ? SerializeComposedTree::Yes : SerializeComposedTree::No);
     pasteboard.write(pasteboardContent);
 }
 
index 6412262..0c58452 100644 (file)
@@ -554,7 +554,8 @@ RefPtr<LegacyWebArchive> LegacyWebArchive::createFromSelection(Frame* frame)
     builder.append(documentTypeString(*document));
 
     Vector<Node*> nodeList;
-    builder.append(serializePreservingVisualAppearance(frame->selection().selection(), ResolveURLs::No, &nodeList));
+    auto serializeComposedTree = frame->settings().selectionAcrossShadowBoundariesEnabled() ? SerializeComposedTree::Yes : SerializeComposedTree::No;
+    builder.append(serializePreservingVisualAppearance(frame->selection().selection(), ResolveURLs::No, serializeComposedTree, &nodeList));
 
     auto archive = create(builder.toString(), *frame, nodeList, nullptr);
     
index e564afc..3caa382 100644 (file)
@@ -105,7 +105,7 @@ private:
     void appendText(StringBuilder&, const Text&) override;
     void appendElement(StringBuilder&, const Element&, Namespaces*) override;
     void appendCustomAttributes(StringBuilder&, const Element&, Namespaces*) override;
-    void appendEndTag(const Element&) override;
+    void appendEndElement(StringBuilder&, const Element&) override;
 };
 
 PageSerializer::SerializerMarkupAccumulator::SerializerMarkupAccumulator(PageSerializer& serializer, Document& document, Vector<Node*>* nodes)
@@ -158,10 +158,10 @@ void PageSerializer::SerializerMarkupAccumulator::appendCustomAttributes(StringB
     appendAttribute(out, element, Attribute(frameOwnerURLAttributeName(frameOwner), url.string()), namespaces);
 }
 
-void PageSerializer::SerializerMarkupAccumulator::appendEndTag(const Element& element)
+void PageSerializer::SerializerMarkupAccumulator::appendEndElement(StringBuilder& out, const Element& element)
 {
     if (!shouldIgnoreElement(element))
-        MarkupAccumulator::appendEndTag(element);
+        MarkupAccumulator::appendEndElement(out, element);
 }
 
 PageSerializer::PageSerializer(Vector<PageSerializer::Resource>& resources)
index 67e06a1..266c6f8 100644 (file)
@@ -3449,6 +3449,13 @@ ExceptionOr<Ref<DOMRect>> Internals::selectionBounds()
     return DOMRect::create(document->frame()->selection().selectionBounds());
 }
 
+void Internals::setSelectionWithoutValidation(Ref<Node> baseNode, unsigned baseOffset, RefPtr<Node> extentNode, unsigned extentOffset)
+{
+    contextDocument()->frame()->selection().moveTo(
+        VisiblePosition { createLegacyEditingPosition(baseNode.ptr(), baseOffset) },
+        VisiblePosition { createLegacyEditingPosition(extentNode.get(), extentOffset) });
+}
+
 ExceptionOr<bool> Internals::isPluginUnavailabilityIndicatorObscured(Element& element)
 {
     if (!is<HTMLPlugInElement>(element))
index e825b6f..e6538eb 100644 (file)
@@ -534,6 +534,7 @@ public:
 #endif
 
     ExceptionOr<Ref<DOMRect>> selectionBounds();
+    void setSelectionWithoutValidation(Ref<Node> baseNode, unsigned baseOffset, RefPtr<Node> extentNode, unsigned extentOffset);
 
     ExceptionOr<bool> isPluginUnavailabilityIndicatorObscured(Element&);
     ExceptionOr<String> unavailablePluginReplacementText(Element&);
index cd3a7bb..fd96a72 100644 (file)
@@ -541,6 +541,7 @@ enum CompositingPolicy {
     boolean isPluginSnapshotted(Element element);
 
     [MayThrowException] DOMRect selectionBounds();
+    void setSelectionWithoutValidation(Node baseNode, unsigned long baseOffset, Node? extentNode, unsigned long extentOffset);
 
     [Conditional=MEDIA_SOURCE] void initializeMockMediaSource();
     [Conditional=MEDIA_SOURCE] sequence<DOMString> bufferedSamplesForTrackID(SourceBuffer buffer, DOMString trackID);