summaryrefslogtreecommitdiffstats
path: root/dom/base
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base')
-rw-r--r--dom/base/AbstractRange.cpp204
-rw-r--r--dom/base/AbstractRange.h35
-rw-r--r--dom/base/BodyUtil.cpp7
-rw-r--r--dom/base/ChromeUtils.cpp18
-rw-r--r--dom/base/ContentIterator.cpp283
-rw-r--r--dom/base/ContentIterator.h50
-rw-r--r--dom/base/ContentProcessMessageManager.cpp13
-rw-r--r--dom/base/ContentProcessMessageManager.h4
-rw-r--r--dom/base/DirectionalityUtils.cpp6
-rw-r--r--dom/base/Document.cpp156
-rw-r--r--dom/base/Document.h32
-rw-r--r--dom/base/Element.cpp87
-rw-r--r--dom/base/Element.h14
-rw-r--r--dom/base/EventSource.cpp10
-rw-r--r--dom/base/FragmentDirective.cpp879
-rw-r--r--dom/base/FragmentDirective.h111
-rw-r--r--dom/base/Link.cpp160
-rw-r--r--dom/base/Link.h38
-rw-r--r--dom/base/Location.cpp24
-rw-r--r--dom/base/PlacesEventCounts.cpp58
-rw-r--r--dom/base/PlacesEventCounts.h39
-rw-r--r--dom/base/PlacesObservers.cpp22
-rw-r--r--dom/base/PlacesObservers.h4
-rw-r--r--dom/base/RangeBoundary.h29
-rw-r--r--dom/base/RangeUtils.cpp7
-rw-r--r--dom/base/ScriptableContentIterator.cpp16
-rw-r--r--dom/base/Selection.cpp273
-rw-r--r--dom/base/Selection.h51
-rw-r--r--dom/base/StaticRange.cpp10
-rw-r--r--dom/base/StaticRange.h16
-rw-r--r--dom/base/ThirdPartyUtil.cpp14
-rw-r--r--dom/base/UseCounters.conf5
-rw-r--r--dom/base/crashtests/1697256.html3
-rw-r--r--dom/base/crashtests/1887930.html7
-rw-r--r--dom/base/crashtests/1887963_1.html15
-rw-r--r--dom/base/crashtests/1887963_2.html15
-rw-r--r--dom/base/crashtests/1887974.html18
-rw-r--r--dom/base/crashtests/1890888.html13
-rw-r--r--dom/base/crashtests/crashtests.list5
-rw-r--r--dom/base/fragmentdirectives/Cargo.toml13
-rw-r--r--dom/base/fragmentdirectives/cbindgen.toml15
-rw-r--r--dom/base/fragmentdirectives/fragment_directive_impl.rs342
-rw-r--r--dom/base/fragmentdirectives/lib.rs158
-rw-r--r--dom/base/fragmentdirectives/test.rs599
-rw-r--r--dom/base/moz.build10
-rw-r--r--dom/base/nsContentUtils.cpp30
-rw-r--r--dom/base/nsContentUtils.h12
-rw-r--r--dom/base/nsFocusManager.cpp67
-rw-r--r--dom/base/nsFocusManager.h16
-rw-r--r--dom/base/nsFrameLoader.cpp3
-rw-r--r--dom/base/nsFrameLoaderOwner.cpp8
-rw-r--r--dom/base/nsFrameMessageManager.cpp5
-rw-r--r--dom/base/nsGlobalWindowInner.cpp4
-rw-r--r--dom/base/nsGlobalWindowInner.h4
-rw-r--r--dom/base/nsGlobalWindowOuter.cpp83
-rw-r--r--dom/base/nsGlobalWindowOuter.h4
-rw-r--r--dom/base/nsIContentInlines.h32
-rw-r--r--dom/base/nsIEventSourceEventService.idl2
-rw-r--r--dom/base/nsINode.cpp66
-rw-r--r--dom/base/nsINode.h21
-rw-r--r--dom/base/nsIScriptableContentIterator.idl7
-rw-r--r--dom/base/nsISelectionController.idl7
-rw-r--r--dom/base/nsJSEnvironment.cpp67
-rw-r--r--dom/base/nsMimeTypeArray.cpp4
-rw-r--r--dom/base/nsMimeTypeArray.h4
-rw-r--r--dom/base/nsRange.cpp406
-rw-r--r--dom/base/nsRange.h149
-rw-r--r--dom/base/nsWrapperCache.h6
-rw-r--r--dom/base/rust/lib.rs2
-rw-r--r--dom/base/test/browser.toml2
-rw-r--r--dom/base/test/browser_aboutnewtab_process_selection.js4
-rw-r--r--dom/base/test/file_window_close.html2
-rw-r--r--dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js4
-rw-r--r--dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js4
-rw-r--r--dom/base/test/fullscreen/browser_fullscreen-navigation-history.js4
-rw-r--r--dom/base/test/fullscreen/browser_fullscreen-window-open-race.js2
-rw-r--r--dom/base/test/fullscreen/file_MozDomFullscreen.html2
-rw-r--r--dom/base/test/fullscreen/file_fullscreen-iframe-middle.html2
-rw-r--r--dom/base/test/fullscreen/file_fullscreen-iframe-top.html2
-rw-r--r--dom/base/test/fullscreen/fullscreen_helpers.js7
-rw-r--r--dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml2
-rw-r--r--dom/base/test/jsmodules/chrome.toml12
-rw-r--r--dom/base/test/jsmodules/export_star_ambiguous.mjs1
-rw-r--r--dom/base/test/jsmodules/import_ambiguous_export.mjs1
-rw-r--r--dom/base/test/jsmodules/import_ambiguous_export_star.mjs1
-rw-r--r--dom/base/test/jsmodules/import_circular.mjs1
-rw-r--r--dom/base/test/jsmodules/import_circular_1.mjs1
-rw-r--r--dom/base/test/jsmodules/import_no_export.mjs2
-rw-r--r--dom/base/test/jsmodules/import_no_indirect_export.mjs2
-rw-r--r--dom/base/test/jsmodules/importmaps/classic_script.js1
-rw-r--r--dom/base/test/jsmodules/importmaps/mochitest.toml7
-rw-r--r--dom/base/test/jsmodules/importmaps/module_chain_1.mjs2
-rw-r--r--dom/base/test/jsmodules/importmaps/module_chain_2.mjs1
-rw-r--r--dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs4
-rw-r--r--dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html33
-rw-r--r--dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html81
-rw-r--r--dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html25
-rw-r--r--dom/base/test/jsmodules/module_a.mjs2
-rw-r--r--dom/base/test/jsmodules/module_b.mjs2
-rw-r--r--dom/base/test/jsmodules/module_c.mjs3
-rw-r--r--dom/base/test/jsmodules/module_d.mjs1
-rw-r--r--dom/base/test/jsmodules/module_e.mjs1
-rw-r--r--dom/base/test/jsmodules/test_import_errorMessage.html40
-rw-r--r--dom/base/test/jsmodules/test_import_errorMessage2.html35
-rw-r--r--dom/base/test/mochitest.toml2
-rw-r--r--dom/base/test/test_bug564863-2.xhtml6
-rw-r--r--dom/base/test/test_content_iterator_subtree_shadow_tree.html290
-rw-r--r--dom/base/test/test_embed_xorigin_document.html5
-rw-r--r--dom/base/test/test_range_bounds.html48
-rw-r--r--dom/base/test/unit/test_xhr_standalone.js5
-rw-r--r--dom/base/use_counter_metrics.yaml146
111 files changed, 4888 insertions, 807 deletions
diff --git a/dom/base/AbstractRange.cpp b/dom/base/AbstractRange.cpp
index 91234bf0a7..c9138a19d2 100644
--- a/dom/base/AbstractRange.cpp
+++ b/dom/base/AbstractRange.cpp
@@ -10,6 +10,7 @@
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/RangeUtils.h"
+#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/StaticRange.h"
#include "mozilla/dom/Selection.h"
@@ -87,6 +88,29 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractRange)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRegisteredClosestCommonInclusiveAncestor)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+// When aMarkDesendants is true, Set
+// DescendantOfClosestCommonInclusiveAncestorForRangeInSelection flag for the
+// flattened children of aNode. When aMarkDesendants is false, unset that flag
+// for the flattened children of aNode.
+void UpdateDescendantsInFlattenedTree(const nsIContent& aNode,
+ bool aMarkDesendants) {
+ if (!aNode.IsElement() || aNode.IsHTMLElement(nsGkAtoms::slot)) {
+ return;
+ }
+
+ FlattenedChildIterator iter(&aNode);
+ for (nsIContent* child = iter.GetNextChild(); child;
+ child = iter.GetNextChild()) {
+ if (aMarkDesendants) {
+ child->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection();
+ } else {
+ child
+ ->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection();
+ }
+ UpdateDescendantsInFlattenedTree(*child, aMarkDesendants);
+ }
+}
+
void AbstractRange::MarkDescendants(const nsINode& aNode) {
// Set NodeIsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection on
// aNode's descendants unless aNode is already marked as a range common
@@ -95,10 +119,22 @@ void AbstractRange::MarkDescendants(const nsINode& aNode) {
if (!aNode.IsMaybeSelected()) {
// don't set the Descendant bit on |aNode| itself
nsINode* node = aNode.GetNextNode(&aNode);
+ if (!node) {
+ if (aNode.GetShadowRootForSelection()) {
+ UpdateDescendantsInFlattenedTree(*aNode.AsContent(), true);
+ }
+ return;
+ }
while (node) {
node->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection();
if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) {
- node = node->GetNextNode(&aNode);
+ if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ UpdateDescendantsInFlattenedTree(*node->AsContent(), true);
+ // sub-tree of node has been marked already
+ node = node->GetNextNonChildNode(&aNode);
+ } else {
+ node = node->GetNextNode(&aNode);
+ }
} else {
// optimize: skip this sub-tree since it's marked already.
node = node->GetNextNonChildNode(&aNode);
@@ -116,10 +152,22 @@ void AbstractRange::UnmarkDescendants(const nsINode& aNode) {
.IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) {
// we know |aNode| doesn't have any bit set
nsINode* node = aNode.GetNextNode(&aNode);
+ if (!node) {
+ if (aNode.GetShadowRootForSelection()) {
+ UpdateDescendantsInFlattenedTree(*aNode.AsContent(), false);
+ }
+ return;
+ }
while (node) {
node->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection();
if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) {
- node = node->GetNextNode(&aNode);
+ if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ UpdateDescendantsInFlattenedTree(*node->AsContent(), false);
+ // sub-tree has been marked already
+ node = node->GetNextNonChildNode(&aNode);
+ } else {
+ node = node->GetNextNode(&aNode);
+ }
} else {
// We found an ancestor of an overlapping range, skip its descendants.
node = node->GetNextNonChildNode(&aNode);
@@ -185,10 +233,62 @@ bool AbstractRange::MaybeCacheToReuse(RangeType& aInstance) {
return true;
}
-nsINode* AbstractRange::GetClosestCommonInclusiveAncestor() const {
- return mIsPositioned ? nsContentUtils::GetClosestCommonInclusiveAncestor(
- mStart.Container(), mEnd.Container())
- : nullptr;
+nsINode* AbstractRange::GetClosestCommonInclusiveAncestor(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const {
+ if (!mIsPositioned) {
+ return nullptr;
+ }
+ nsINode* startContainer =
+ aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? GetMayCrossShadowBoundaryStartContainer()
+ : GetStartContainer();
+ nsINode* endContainer =
+ aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? GetMayCrossShadowBoundaryEndContainer()
+ : GetEndContainer();
+
+ if (MayCrossShadowBoundary() &&
+ aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) {
+ // Since both the start container and the end container are
+ // guaranteed to be in the same composed document.
+ // If one of the boundary is a document, use that document
+ // as the common ancestor since both nodes.
+ const bool oneBoundaryIsDocument =
+ (startContainer && startContainer->IsDocument()) ||
+ (endContainer && endContainer->IsDocument());
+ if (oneBoundaryIsDocument) {
+ MOZ_ASSERT_IF(
+ startContainer && startContainer->IsDocument(),
+ !endContainer || endContainer->GetComposedDoc() == startContainer);
+ MOZ_ASSERT_IF(
+ endContainer && endContainer->IsDocument(),
+ !startContainer || startContainer->GetComposedDoc() == endContainer);
+
+ return startContainer ? startContainer->GetComposedDoc()
+ : endContainer->GetComposedDoc();
+ }
+
+ const auto rescope = [](nsINode*& aContainer) {
+ if (!aContainer) {
+ return;
+ }
+ // RangeBoundary allows the container to be shadow roots; When
+ // this happens, we should use the shadow host here.
+ if (auto* shadowRoot = ShadowRoot::FromNode(aContainer)) {
+ aContainer = shadowRoot->GetHost();
+ return;
+ }
+ };
+
+ rescope(startContainer);
+ rescope(endContainer);
+
+ return nsContentUtils::GetCommonFlattenedTreeAncestorForSelection(
+ startContainer ? startContainer->AsContent() : nullptr,
+ endContainer ? endContainer->AsContent() : nullptr);
+ }
+ return nsContentUtils::GetClosestCommonInclusiveAncestor(startContainer,
+ endContainer);
}
// static
@@ -237,9 +337,28 @@ nsresult AbstractRange::SetStartAndEndInternal(
return NS_ERROR_DOM_INDEX_SIZE_ERR;
}
- // If they have different root, this should be collapsed at the end point.
+ // Different root
if (newStartRoot != newEndRoot) {
- aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot);
+ if (aRange->IsStaticRange()) {
+ // StaticRange allows nodes in different trees, so set start and end
+ // accordingly
+ aRange->DoSetRange(aStartBoundary, aEndBoundary, newEndRoot);
+ } else {
+ MOZ_ASSERT(aRange->IsDynamicRange());
+ // In contrast, nsRange keeps both. It has a pair of start and end
+ // which they have been collapsed to one end, and it also may have a pair
+ // of start and end which are the original value.
+ aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot);
+
+ // Don't create the cross shadow bounday range if the one of the roots is
+ // an UA widget regardless whether the boundaries are allowed to cross
+ // shadow boundary or not.
+ if (!IsRootUAWidget(newStartRoot) && !IsRootUAWidget(newEndRoot)) {
+ aRange->AsDynamicRange()
+ ->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(aStartBoundary,
+ aEndBoundary);
+ }
+ }
return NS_OK;
}
@@ -274,7 +393,10 @@ void AbstractRange::RegisterSelection(Selection& aSelection) {
bool isFirstSelection = mSelections.IsEmpty();
mSelections.AppendElement(&aSelection);
if (isFirstSelection && !mRegisteredClosestCommonInclusiveAncestor) {
- nsINode* commonAncestor = GetClosestCommonInclusiveAncestor();
+ nsINode* commonAncestor = GetClosestCommonInclusiveAncestor(
+ StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()
+ ? AllowRangeCrossShadowBoundary::Yes
+ : AllowRangeCrossShadowBoundary::No);
MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes");
RegisterClosestCommonInclusiveAncestor(commonAncestor);
}
@@ -358,7 +480,8 @@ void AbstractRange::UnregisterClosestCommonInclusiveAncestor(
void AbstractRange::UpdateCommonAncestorIfNecessary() {
nsINode* oldCommonAncestor = mRegisteredClosestCommonInclusiveAncestor;
- nsINode* newCommonAncestor = GetClosestCommonInclusiveAncestor();
+ nsINode* newCommonAncestor =
+ GetClosestCommonInclusiveAncestor(AllowRangeCrossShadowBoundary::Yes);
if (newCommonAncestor != oldCommonAncestor) {
if (oldCommonAncestor) {
UnregisterClosestCommonInclusiveAncestor(oldCommonAncestor, false);
@@ -379,6 +502,59 @@ void AbstractRange::UpdateCommonAncestorIfNecessary() {
}
}
+const RangeBoundary& AbstractRange::MayCrossShadowBoundaryStartRef() const {
+ return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryStartRef()
+ : mStart;
+}
+
+const RangeBoundary& AbstractRange::MayCrossShadowBoundaryEndRef() const {
+ return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryEndRef()
+ : mEnd;
+}
+
+nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtStartOffset() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtStartOffset()
+ : mStart.GetChildAtOffset();
+}
+
+nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtEndOffset() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtEndOffset()
+ : mEnd.GetChildAtOffset();
+}
+
+nsINode* AbstractRange::GetMayCrossShadowBoundaryStartContainer() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->GetMayCrossShadowBoundaryStartContainer()
+ : mStart.Container();
+}
+
+nsINode* AbstractRange::GetMayCrossShadowBoundaryEndContainer() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->GetMayCrossShadowBoundaryEndContainer()
+ : mEnd.Container();
+}
+
+bool AbstractRange::MayCrossShadowBoundary() const {
+ return IsDynamicRange() ? !!AsDynamicRange()->GetCrossShadowBoundaryRange()
+ : false;
+}
+
+uint32_t AbstractRange::MayCrossShadowBoundaryStartOffset() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->MayCrossShadowBoundaryStartOffset()
+ : static_cast<uint32_t>(*mStart.Offset(
+ RangeBoundary::OffsetFilter::kValidOrInvalidOffsets));
+}
+
+uint32_t AbstractRange::MayCrossShadowBoundaryEndOffset() const {
+ return IsDynamicRange()
+ ? AsDynamicRange()->MayCrossShadowBoundaryEndOffset()
+ : static_cast<uint32_t>(*mEnd.Offset(
+ RangeBoundary::OffsetFilter::kValidOrInvalidOffsets));
+}
+
nsINode* AbstractRange::GetParentObject() const { return mOwner; }
JSObject* AbstractRange::WrapObject(JSContext* aCx,
@@ -395,4 +571,12 @@ void AbstractRange::ClearForReuse() {
mCalledByJS = false;
}
+/*static*/
+bool AbstractRange::IsRootUAWidget(const nsINode* aRoot) {
+ MOZ_ASSERT(aRoot);
+ if (const ShadowRoot* shadowRoot = ShadowRoot::FromNode(aRoot)) {
+ return shadowRoot->IsUAWidget();
+ }
+ return false;
+}
} // namespace mozilla::dom
diff --git a/dom/base/AbstractRange.h b/dom/base/AbstractRange.h
index c70aaf19ec..2f9b59158a 100644
--- a/dom/base/AbstractRange.h
+++ b/dom/base/AbstractRange.h
@@ -31,10 +31,15 @@ class Document;
class Selection;
class StaticRange;
+enum class AllowRangeCrossShadowBoundary : bool { No, Yes };
+
class AbstractRange : public nsISupports,
public nsWrapperCache,
// For linking together selection-associated ranges.
public mozilla::LinkedListElement<AbstractRange> {
+ using AllowRangeCrossShadowBoundary =
+ mozilla::dom::AllowRangeCrossShadowBoundary;
+
protected:
explicit AbstractRange(nsINode* aNode, bool aIsDynamicRange);
virtual ~AbstractRange();
@@ -51,18 +56,33 @@ class AbstractRange : public nsISupports,
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AbstractRange)
+ /**
+ * All of the MayCrossShadowBoundary* methods are used to get the boundary
+ * endpoints that cross shadow boundaries. They would return
+ * the same value as the non-MayCrossShadowBoundary* methods if the range
+ * boundaries don't cross shadow boundaries.
+ */
const RangeBoundary& StartRef() const { return mStart; }
+ const RangeBoundary& MayCrossShadowBoundaryStartRef() const;
+
const RangeBoundary& EndRef() const { return mEnd; }
+ const RangeBoundary& MayCrossShadowBoundaryEndRef() const;
nsIContent* GetChildAtStartOffset() const {
return mStart.GetChildAtOffset();
}
+ nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const;
+
nsIContent* GetChildAtEndOffset() const { return mEnd.GetChildAtOffset(); }
+ nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const;
+
bool IsPositioned() const { return mIsPositioned; }
/**
* https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor
*/
- nsINode* GetClosestCommonInclusiveAncestor() const;
+ nsINode* GetClosestCommonInclusiveAncestor(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No) const;
// WebIDL
@@ -75,7 +95,12 @@ class AbstractRange : public nsISupports,
// `IsPositioned()` directly.
nsINode* GetStartContainer() const { return mStart.Container(); }
+ nsINode* GetMayCrossShadowBoundaryStartContainer() const;
+
nsINode* GetEndContainer() const { return mEnd.Container(); }
+ nsINode* GetMayCrossShadowBoundaryEndContainer() const;
+
+ bool MayCrossShadowBoundary() const;
Document* GetComposedDocOfContainers() const {
return mStart.Container() ? mStart.Container()->GetComposedDoc() : nullptr;
@@ -86,12 +111,15 @@ class AbstractRange : public nsISupports,
return static_cast<uint32_t>(
*mStart.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets));
}
+ uint32_t MayCrossShadowBoundaryStartOffset() const;
// FYI: Returns 0 if it's not positioned.
uint32_t EndOffset() const {
return static_cast<uint32_t>(
*mEnd.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets));
}
+ uint32_t MayCrossShadowBoundaryEndOffset() const;
+
bool Collapsed() const {
return !mIsPositioned || (mStart.Container() == mEnd.Container() &&
StartOffset() == EndOffset());
@@ -132,6 +160,11 @@ class AbstractRange : public nsISupports,
*/
bool IsInSelection(const mozilla::dom::Selection& aSelection) const;
+ /**
+ * Return true if aRoot is a UA shadow root.
+ */
+ static bool IsRootUAWidget(const nsINode* aRoot);
+
protected:
template <typename SPT, typename SRT, typename EPT, typename ERT,
typename RangeType>
diff --git a/dom/base/BodyUtil.cpp b/dom/base/BodyUtil.cpp
index e8de3d18ec..45ca58e2d9 100644
--- a/dom/base/BodyUtil.cpp
+++ b/dom/base/BodyUtil.cpp
@@ -422,9 +422,10 @@ already_AddRefed<FormData> BodyUtil::ConsumeFormData(
if (isValidUrlEncodedMimeType) {
RefPtr<FormData> fd = new FormData(aParent);
DebugOnly<bool> status = URLParams::Parse(
- aStr, true, [&fd](const nsAString& aName, const nsAString& aValue) {
- ErrorResult rv;
- fd->Append(aName, aValue, rv);
+ aStr, true, [&fd](const nsACString& aName, const nsACString& aValue) {
+ IgnoredErrorResult rv;
+ fd->Append(NS_ConvertUTF8toUTF16(aName),
+ NS_ConvertUTF8toUTF16(aValue), rv);
MOZ_ASSERT(!rv.Failed());
return true;
});
diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp
index 0df1cd3c9b..407f33e044 100644
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -1289,9 +1289,10 @@ void ChromeUtils::GetBaseDomainFromPartitionKey(dom::GlobalObject& aGlobal,
nsString scheme;
nsString pkBaseDomain;
int32_t port;
+ bool ancestor;
- if (!mozilla::OriginAttributes::ParsePartitionKey(aPartitionKey, scheme,
- pkBaseDomain, port)) {
+ if (!mozilla::OriginAttributes::ParsePartitionKey(
+ aPartitionKey, scheme, pkBaseDomain, port, ancestor)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
@@ -1317,7 +1318,10 @@ void ChromeUtils::GetPartitionKeyFromURL(dom::GlobalObject& aGlobal,
}
mozilla::OriginAttributes attrs;
- attrs.SetPartitionKey(uri);
+ // For now, uses assume the partition key is cross-site.
+ // We will need to not make this assumption to allow access
+ // to same-site partitioned cookies in the cookie extension API.
+ attrs.SetPartitionKey(uri, false);
aPartitionKey = attrs.mPartitionKey;
}
@@ -1515,10 +1519,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal,
// DOM windows.
/* aUtilityInfo = */ std::move(utilityActors),
/* aChild = */ 0 // Without a ContentProcess, no ChildId.
-#ifdef XP_MACOSX
+#ifdef XP_DARWIN
,
/* aChildTask = */ aGeckoProcess->GetChildTask()
-#endif // XP_MACOSX
+#endif // XP_DARWIN
);
});
@@ -1617,10 +1621,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal,
/* aWindowInfo = */ std::move(windows),
/* aUtilityInfo = */ nsTArray<UtilityInfo>(),
/* aChild = */ contentParent->ChildID()
-#ifdef XP_MACOSX
+#ifdef XP_DARWIN
,
/* aChildTask = */ contentParent->Process()->GetChildTask()
-#endif // XP_MACOSX
+#endif // XP_DARWIN
);
}
diff --git a/dom/base/ContentIterator.cpp b/dom/base/ContentIterator.cpp
index 6819353520..0b405f0348 100644
--- a/dom/base/ContentIterator.cpp
+++ b/dom/base/ContentIterator.cpp
@@ -7,6 +7,7 @@
#include "ContentIterator.h"
#include "mozilla/Assertions.h"
+#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/RangeBoundary.h"
#include "mozilla/RangeUtils.h"
@@ -26,6 +27,69 @@ using namespace dom;
__VA_ARGS__); \
template aResultType ContentIteratorBase<nsINode*>::aMethodName(__VA_ARGS__)
+/**
+ * IteratorHelpers contains the static methods to help extra values
+ * based on whether or not the iterator allows to iterate nodes cross the shadow
+ * boundary.
+ */
+struct IteratorHelpers {
+ IteratorHelpers() = delete;
+
+ static nsINode* GetStartContainer(AbstractRange* aRange,
+ bool aAllowCrossShadowBoundary) {
+ MOZ_ASSERT(aRange);
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aRange->GetMayCrossShadowBoundaryStartContainer()
+ : aRange->GetStartContainer();
+ }
+
+ static int32_t StartOffset(AbstractRange* aRange,
+ bool aAllowCrossShadowBoundary) {
+ MOZ_ASSERT(aRange);
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aRange->MayCrossShadowBoundaryStartOffset()
+ : aRange->StartOffset();
+ }
+
+ static nsINode* GetEndContainer(AbstractRange* aRange,
+ bool aAllowCrossShadowBoundary) {
+ MOZ_ASSERT(aRange);
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aRange->GetMayCrossShadowBoundaryEndContainer()
+ : aRange->GetEndContainer();
+ }
+
+ static int32_t EndOffset(AbstractRange* aRange,
+ bool aAllowCrossShadowBoundary) {
+ MOZ_ASSERT(aRange);
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aRange->MayCrossShadowBoundaryEndOffset()
+ : aRange->EndOffset();
+ }
+
+ // FIXME(sefeng): This doesn't work with slots / flattened tree.
+ static nsINode* GetParentNode(nsINode& aNode,
+ bool aAllowCrossShadowBoundary) {
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aNode.GetParentOrShadowHostNode()
+ : aNode.GetParentNode();
+ }
+
+ static ShadowRoot* GetShadowRoot(const nsINode* aNode,
+ bool aAllowCrossShadowBoundary) {
+ MOZ_ASSERT(aNode);
+ return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aAllowCrossShadowBoundary)
+ ? aNode->GetShadowRootForSelection()
+ : nullptr;
+ }
+};
+
static bool ComparePostMode(const RawRangeBoundary& aStart,
const RawRangeBoundary& aEnd, nsINode& aNode) {
nsINode* parent = aNode.GetParentNode();
@@ -541,17 +605,45 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepFirstChild(nsINode* aRoot) {
// static
template <typename NodeType>
nsIContent* ContentIteratorBase<NodeType>::GetDeepFirstChild(
- nsIContent* aRoot) {
+ nsIContent* aRoot, bool aAllowCrossShadowBoundary) {
if (NS_WARN_IF(!aRoot)) {
return nullptr;
}
nsIContent* node = aRoot;
- nsIContent* child = node->GetFirstChild();
+ nsIContent* child = nullptr;
+
+ if (ShadowRoot* shadowRoot =
+ IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) {
+ // When finding the deepest child of node, if this node has a
+ // web exposed shadow root, we use this shadow root to find the deepest
+ // child.
+ // If the first candidate should be a slotted content,
+ // shadowRoot->GetFirstChild() should be able to return the <slot> element.
+ // It's probably correct I think. Then it's up to the caller of this
+ // iterator to decide whether to use the slot's assigned nodes or not.
+ MOZ_ASSERT(aAllowCrossShadowBoundary);
+ child = shadowRoot->GetFirstChild();
+ } else {
+ child = node->GetFirstChild();
+ }
while (child) {
node = child;
- child = node->GetFirstChild();
+ if (ShadowRoot* shadowRoot =
+ IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) {
+ // When finding the deepest child of node, if this node has a
+ // web exposed shadow root, we use this shadow root to find the deepest
+ // child.
+ // If the first candidate should be a slotted content,
+ // shadowRoot->GetFirstChild() should be able to return the <slot>
+ // element. It's probably correct I think. Then it's up to the caller of
+ // this iterator to decide whether to use the slot's assigned nodes or
+ // not.
+ child = shadowRoot->GetFirstChild();
+ } else {
+ child = node->GetFirstChild();
+ }
}
return node;
@@ -569,23 +661,41 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepLastChild(nsINode* aRoot) {
// static
template <typename NodeType>
-nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild(nsIContent* aRoot) {
+nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild(
+ nsIContent* aRoot, bool aAllowCrossShadowBoundary) {
if (NS_WARN_IF(!aRoot)) {
return nullptr;
}
nsIContent* node = aRoot;
- while (node->HasChildren()) {
- nsIContent* child = node->GetLastChild();
- node = child;
+
+ ShadowRoot* shadowRoot =
+ IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary);
+ // FIXME(sefeng): This doesn't work with slots / flattened tree.
+ while (node->HasChildren() || (shadowRoot && shadowRoot->HasChildren())) {
+ if (node->HasChildren()) {
+ node = node->GetLastChild();
+ } else {
+ MOZ_ASSERT(shadowRoot);
+ // If this node doesn't have a child, but it's also a shadow host
+ // that can be selected, we go into this shadow tree.
+ node = shadowRoot->GetLastChild();
+ }
+ shadowRoot =
+ IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary);
}
return node;
}
-// Get the next sibling, or parent's next sibling, or grandpa's next sibling...
+// Get the next sibling, or parent's next sibling, or shadow host's next
+// sibling (when aAllowCrossShadowBoundary is true), or grandpa's next
+// sibling...
+//
// static
+//
template <typename NodeType>
-nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) {
+nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(
+ nsINode* aNode, bool aAllowCrossShadowBoundary) {
if (NS_WARN_IF(!aNode)) {
return nullptr;
}
@@ -594,18 +704,32 @@ nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) {
return next;
}
- nsINode* parent = aNode->GetParentNode();
+ nsINode* parent =
+ IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary);
if (NS_WARN_IF(!parent)) {
return nullptr;
}
- return ContentIteratorBase::GetNextSibling(parent);
+ if (aAllowCrossShadowBoundary) {
+ // This is temporary solution.
+ // For shadow root, instead of getting to the sibling of the parent
+ // directly, we need to get into the light tree of the parent to handle
+ // slotted contents.
+ if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(aNode)) {
+ if (nsIContent* child = parent->GetFirstChild()) {
+ return child;
+ }
+ }
+ }
+
+ return ContentIteratorBase::GetNextSibling(parent, aAllowCrossShadowBoundary);
}
-// Get the prev sibling, or parent's prev sibling, or grandpa's prev sibling...
-// static
+// Get the prev sibling, or parent's prev sibling, or shadow host's prev sibling
+// (when aAllowCrossShadowBoundary is true), or grandpa's prev sibling... static
template <typename NodeType>
-nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) {
+nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(
+ nsINode* aNode, bool aAllowCrossShadowBoundary) {
if (NS_WARN_IF(!aNode)) {
return nullptr;
}
@@ -614,12 +738,13 @@ nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) {
return prev;
}
- nsINode* parent = aNode->GetParentNode();
+ nsINode* parent =
+ IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary);
if (NS_WARN_IF(!parent)) {
return nullptr;
}
- return ContentIteratorBase::GetPrevSibling(parent);
+ return ContentIteratorBase::GetPrevSibling(parent, aAllowCrossShadowBoundary);
}
template <typename NodeType>
@@ -860,19 +985,41 @@ nsresult ContentSubtreeIterator::Init(const RawRangeBoundary& aStartBoundary,
return InitWithRange();
}
+nsresult ContentSubtreeIterator::InitWithAllowCrossShadowBoundary(
+ AbstractRange* aRange) {
+ MOZ_ASSERT(aRange);
+
+ if (NS_WARN_IF(!aRange->IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mRange = aRange;
+
+ mAllowCrossShadowBoundary = AllowRangeCrossShadowBoundary::Yes;
+ return InitWithRange();
+}
+
void ContentSubtreeIterator::CacheInclusiveAncestorsOfEndContainer() {
mInclusiveAncestorsOfEndContainer.Clear();
- nsINode* const endContainer = mRange->GetEndContainer();
+ nsINode* const endContainer =
+ IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary());
nsIContent* endNode =
endContainer->IsContent() ? endContainer->AsContent() : nullptr;
while (endNode) {
mInclusiveAncestorsOfEndContainer.AppendElement(endNode);
- endNode = endNode->GetParent();
+ // Cross the boundary for contents in shadow tree.
+ nsINode* parent = IteratorHelpers::GetParentNode(
+ *endNode, IterAllowCrossShadowBoundary());
+ if (!parent || !parent->IsContent()) {
+ break;
+ }
+ endNode = parent->AsContent();
}
}
nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const {
- nsINode* startContainer = mRange->GetStartContainer();
+ nsINode* startContainer = IteratorHelpers::GetStartContainer(
+ mRange, IterAllowCrossShadowBoundary());
nsIContent* firstCandidate = nullptr;
// find first node in range
nsINode* node = nullptr;
@@ -880,9 +1027,14 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const {
// no children, start at the node itself
node = startContainer;
} else {
- nsIContent* child = mRange->GetChildAtStartOffset();
- MOZ_ASSERT(child ==
- startContainer->GetChildAt_Deprecated(mRange->StartOffset()));
+ nsIContent* child =
+ IterAllowCrossShadowBoundary()
+ ? mRange->GetMayCrossShadowBoundaryChildAtStartOffset()
+ : mRange->GetChildAtStartOffset();
+
+ MOZ_ASSERT(child == startContainer->GetChildAt_Deprecated(
+ IteratorHelpers::StartOffset(
+ mRange, IterAllowCrossShadowBoundary())));
if (!child) {
// offset after last child
node = startContainer;
@@ -893,11 +1045,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const {
if (!firstCandidate) {
// then firstCandidate is next node after node
- firstCandidate = ContentIteratorBase::GetNextSibling(node);
+ firstCandidate = ContentIteratorBase::GetNextSibling(
+ node, IterAllowCrossShadowBoundary());
}
if (firstCandidate) {
- firstCandidate = ContentIteratorBase::GetDeepFirstChild(firstCandidate);
+ firstCandidate = ContentIteratorBase::GetDeepFirstChild(
+ firstCandidate, IterAllowCrossShadowBoundary());
}
return firstCandidate;
@@ -926,9 +1080,12 @@ nsIContent* ContentSubtreeIterator::DetermineFirstContent() const {
nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const {
nsIContent* lastCandidate{nullptr};
- nsINode* endContainer = mRange->GetEndContainer();
+ nsINode* endContainer =
+ IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary());
// now to find the last node
- int32_t offset = mRange->EndOffset();
+ int32_t offset =
+ IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary());
+
int32_t numChildren = endContainer->GetChildCount();
nsINode* node = nullptr;
@@ -939,7 +1096,9 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const {
if (!offset || !numChildren) {
node = endContainer;
} else {
- lastCandidate = mRange->EndRef().Ref();
+ lastCandidate = IterAllowCrossShadowBoundary()
+ ? mRange->MayCrossShadowBoundaryEndRef().Ref()
+ : mRange->EndRef().Ref();
MOZ_ASSERT(lastCandidate == endContainer->GetChildAt_Deprecated(--offset));
NS_ASSERTION(lastCandidate,
"tree traversal trouble in ContentSubtreeIterator::Init");
@@ -947,11 +1106,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const {
if (!lastCandidate) {
// then lastCandidate is prev node before node
- lastCandidate = ContentIteratorBase::GetPrevSibling(node);
+ lastCandidate = ContentIteratorBase::GetPrevSibling(
+ node, IterAllowCrossShadowBoundary());
}
if (lastCandidate) {
- lastCandidate = ContentIteratorBase::GetDeepLastChild(lastCandidate);
+ lastCandidate = ContentIteratorBase::GetDeepLastChild(
+ lastCandidate, IterAllowCrossShadowBoundary());
}
return lastCandidate;
@@ -962,11 +1123,17 @@ nsresult ContentSubtreeIterator::InitWithRange() {
MOZ_ASSERT(mRange->IsPositioned());
// get the start node and offset, convert to nsINode
- mClosestCommonInclusiveAncestor = mRange->GetClosestCommonInclusiveAncestor();
- nsINode* startContainer = mRange->GetStartContainer();
- const int32_t startOffset = mRange->StartOffset();
- nsINode* endContainer = mRange->GetEndContainer();
- const int32_t endOffset = mRange->EndOffset();
+ mClosestCommonInclusiveAncestor =
+ mRange->GetClosestCommonInclusiveAncestor(mAllowCrossShadowBoundary);
+
+ nsINode* startContainer = IteratorHelpers::GetStartContainer(
+ mRange, IterAllowCrossShadowBoundary());
+ const int32_t startOffset =
+ IteratorHelpers::StartOffset(mRange, IterAllowCrossShadowBoundary());
+ nsINode* endContainer =
+ IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary());
+ const int32_t endOffset =
+ IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary());
MOZ_ASSERT(mClosestCommonInclusiveAncestor && startContainer && endContainer);
// Bug 767169
MOZ_ASSERT(uint32_t(startOffset) <= startContainer->Length() &&
@@ -1044,14 +1211,23 @@ void ContentSubtreeIterator::Next() {
return;
}
- nsINode* nextNode = ContentIteratorBase::GetNextSibling(mCurNode);
+ nsINode* nextNode = ContentIteratorBase::GetNextSibling(
+ mCurNode, IterAllowCrossShadowBoundary());
+
NS_ASSERTION(nextNode, "No next sibling!?! This could mean deadlock!");
int32_t i = mInclusiveAncestorsOfEndContainer.IndexOf(nextNode);
while (i != -1) {
// as long as we are finding ancestors of the endpoint of the range,
// dive down into their children
- nextNode = nextNode->GetFirstChild();
+ ShadowRoot* root = IteratorHelpers::GetShadowRoot(
+ Element::FromNode(nextNode), IterAllowCrossShadowBoundary());
+ if (!root) {
+ nextNode = nextNode->GetFirstChild();
+ } else {
+ nextNode = mRange->MayCrossShadowBoundary() ? root->GetFirstChild()
+ : nextNode->GetFirstChild();
+ }
NS_ASSERTION(nextNode, "Iterator error, expected a child node!");
// should be impossible to get a null pointer. If we went all the way
@@ -1098,7 +1274,8 @@ nsresult ContentSubtreeIterator::PositionAt(nsINode* aCurNode) {
nsIContent* ContentSubtreeIterator::GetTopAncestorInRange(
nsINode* aNode) const {
- if (!aNode || !aNode->GetParentNode()) {
+ if (!aNode ||
+ !IteratorHelpers::GetParentNode(*aNode, IterAllowCrossShadowBoundary())) {
return nullptr;
}
@@ -1114,15 +1291,23 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange(
return nullptr;
}
+ nsIContent* lastContentInShadowTree = nullptr;
while (content) {
- nsIContent* parent = content->GetParent();
+ nsINode* parent = IteratorHelpers::GetParentNode(
+ *content, IterAllowCrossShadowBoundary());
+
// content always has a parent. If its parent is the root, however --
// i.e., either it's not content, or it is content but its own parent is
// null -- then we're finished, since we don't go up to the root.
//
+ // Caveat: If iteration crossing shadow boundary is allowed
+ // and the root is a shadow root, we keep going up to the
+ // shadow host and continue.
+ //
// We have to special-case this because CompareNodeToRange treats the root
// node differently -- see bug 765205.
- if (!parent || !parent->GetParentNode()) {
+ if (!parent || !IteratorHelpers::GetParentNode(
+ *parent, IterAllowCrossShadowBoundary())) {
return content;
}
@@ -1130,10 +1315,28 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange(
RangeUtils::IsNodeContainedInRange(*parent, mRange);
MOZ_ALWAYS_TRUE(isNodeContainedInRange);
if (!isNodeContainedInRange.value()) {
+ if (IterAllowCrossShadowBoundary() && content->IsShadowRoot()) {
+ MOZ_ASSERT(parent->GetShadowRoot() == content);
+ // host element is not in range, the last content in tree
+ // should be the ancestor.
+ MOZ_ASSERT(lastContentInShadowTree);
+ return lastContentInShadowTree;
+ }
return content;
}
- content = parent;
+ // When we cross the boundary, we keep a reference to the
+ // last content that is in tree, because if we later
+ // find the shadow host element is not in the range, that means
+ // the last content in the tree should be top ancestor in range.
+ //
+ // Using shadow root doesn't make sense here because it doesn't
+ // represent a actual content.
+ if (IterAllowCrossShadowBoundary() && parent->IsShadowRoot()) {
+ lastContentInShadowTree = content;
+ }
+
+ content = parent->AsContent();
}
MOZ_CRASH("This should only be possible if aNode was null");
diff --git a/dom/base/ContentIterator.h b/dom/base/ContentIterator.h
index b645c4147e..67962d41d5 100644
--- a/dom/base/ContentIterator.h
+++ b/dom/base/ContentIterator.h
@@ -82,15 +82,26 @@ class ContentIteratorBase {
// Recursively get the deepest first/last child of aRoot. This will return
// aRoot itself if it has no children.
static nsINode* GetDeepFirstChild(nsINode* aRoot);
- static nsIContent* GetDeepFirstChild(nsIContent* aRoot);
+ // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree
+ // when it reaches to a shadow host.
+ static nsIContent* GetDeepFirstChild(nsIContent* aRoot,
+ bool aAllowCrossShadowBoundary);
static nsINode* GetDeepLastChild(nsINode* aRoot);
- static nsIContent* GetDeepLastChild(nsIContent* aRoot);
+ // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree
+ // when it reaches to a shadow host.
+ static nsIContent* GetDeepLastChild(nsIContent* aRoot,
+ bool aAllowCrossShadowBoundary);
// Get the next/previous sibling of aNode, or its parent's, or grandparent's,
// etc. Returns null if aNode and all its ancestors have no next/previous
// sibling.
- static nsIContent* GetNextSibling(nsINode* aNode);
- static nsIContent* GetPrevSibling(nsINode* aNode);
+ //
+ // If aAllowCrossShadowBoundary is true, it'll continue with the shadow host
+ // when it reaches to a shadow root.
+ static nsIContent* GetNextSibling(nsINode* aNode,
+ bool aAllowCrossShadowBoundary = false);
+ static nsIContent* GetPrevSibling(nsINode* aNode,
+ bool aAllowCrossShadowBoundary = false);
nsINode* NextNode(nsINode* aNode);
nsINode* PrevNode(nsINode* aNode);
@@ -219,6 +230,29 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase {
virtual nsresult Init(nsINode* aRoot) override;
virtual nsresult Init(dom::AbstractRange* aRange) override;
+
+ /**
+ * Initialize the iterator with aRange that does correct things
+ * when the aRange's start and/or the end containers are
+ * in shadow dom.
+ *
+ * If both start and end containers are in light dom, the iterator
+ * won't do anything special.
+ *
+ * When the start container is in shadow dom, the iterator can
+ * find the correct start node by crossing the shadow
+ * boundary when needed.
+ *
+ * When the end container is in shadow dom, the iterator can find
+ * the correct end node by crossing the shadow boundary when
+ * needed. Also when the next node is an ancestor of
+ * the end node, it can correctly iterate into the
+ * subtree of it by crossing the shadow boundary.
+ *
+ * Examples of what nodes will be returned can be found
+ * at test_content_iterator_subtree_shadow_tree.html.
+ */
+ nsresult InitWithAllowCrossShadowBoundary(dom::AbstractRange* aRange);
virtual nsresult Init(nsINode* aStartContainer, uint32_t aStartOffset,
nsINode* aEndContainer, uint32_t aEndOffset) override;
virtual nsresult Init(const RawRangeBoundary& aStartBoundary,
@@ -276,10 +310,18 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase {
// the range's start and end nodes will never be considered "in" it.
nsIContent* GetTopAncestorInRange(nsINode* aNode) const;
+ bool IterAllowCrossShadowBoundary() const {
+ return mAllowCrossShadowBoundary == dom::AllowRangeCrossShadowBoundary::Yes;
+ }
+
RefPtr<dom::AbstractRange> mRange;
// See <https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor>.
AutoTArray<nsIContent*, 8> mInclusiveAncestorsOfEndContainer;
+
+ // Whether this iterator allows to iterate nodes across shadow boundary.
+ dom::AllowRangeCrossShadowBoundary mAllowCrossShadowBoundary =
+ dom::AllowRangeCrossShadowBoundary::No;
};
} // namespace mozilla
diff --git a/dom/base/ContentProcessMessageManager.cpp b/dom/base/ContentProcessMessageManager.cpp
index 7661d1036f..9723341782 100644
--- a/dom/base/ContentProcessMessageManager.cpp
+++ b/dom/base/ContentProcessMessageManager.cpp
@@ -104,6 +104,7 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() {
jsapi.Init();
if (!GetOrCreateDOMReflectorNoWrap(jsapi.cx(), this, &val)) {
+ JS_ClearPendingException(jsapi.cx());
return nullptr;
}
}
@@ -111,11 +112,15 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() {
return &val.toObject();
}
-void ContentProcessMessageManager::LoadScript(const nsAString& aURL) {
+bool ContentProcessMessageManager::LoadScript(const nsAString& aURL) {
Init();
- JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(),
- GetOrCreateWrapper());
- LoadScriptInternal(messageManager, aURL, true);
+ JSObject* wrapper = GetOrCreateWrapper();
+ if (wrapper) {
+ JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(), wrapper);
+ LoadScriptInternal(messageManager, aURL, true);
+ return true;
+ }
+ return false;
}
void ContentProcessMessageManager::SetInitialProcessData(
diff --git a/dom/base/ContentProcessMessageManager.h b/dom/base/ContentProcessMessageManager.h
index b7c54ba452..0d437b5e50 100644
--- a/dom/base/ContentProcessMessageManager.h
+++ b/dom/base/ContentProcessMessageManager.h
@@ -58,7 +58,7 @@ class ContentProcessMessageManager : public nsIMessageSender,
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
- JSObject* GetOrCreateWrapper();
+ [[nodiscard]] JSObject* GetOrCreateWrapper();
using MessageManagerGlobal::AddMessageListener;
using MessageManagerGlobal::AddWeakMessageListener;
@@ -84,7 +84,7 @@ class ContentProcessMessageManager : public nsIMessageSender,
return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
}
- virtual void LoadScript(const nsAString& aURL);
+ [[nodiscard]] virtual bool LoadScript(const nsAString& aURL);
bool IsProcessScoped() const override { return true; }
diff --git a/dom/base/DirectionalityUtils.cpp b/dom/base/DirectionalityUtils.cpp
index dd427c61b1..2e4ada4800 100644
--- a/dom/base/DirectionalityUtils.cpp
+++ b/dom/base/DirectionalityUtils.cpp
@@ -191,10 +191,8 @@ static bool ParticipatesInAutoDirection(const nsIContent* aContent) {
if (aContent->IsShadowRoot()) {
return true;
}
- dom::NodeInfo* ni = aContent->NodeInfo();
- return ni->NamespaceID() == kNameSpaceID_XHTML &&
- !ni->Equals(nsGkAtoms::script) && !ni->Equals(nsGkAtoms::style) &&
- !ni->Equals(nsGkAtoms::input) && !ni->Equals(nsGkAtoms::textarea);
+ return !aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style,
+ nsGkAtoms::input, nsGkAtoms::textarea);
}
/**
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index 4e9286a91e..8cbf8b8075 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -172,6 +172,8 @@
#include "mozilla/dom/FeaturePolicy.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
#include "mozilla/dom/FontFaceSet.h"
+#include "mozilla/dom/FragmentDirective.h"
+#include "mozilla/dom/fragmentdirectives_ffi_generated.h"
#include "mozilla/dom/FromParser.h"
#include "mozilla/dom/HighlightRegistry.h"
#include "mozilla/dom/HTMLAllCollection.h"
@@ -198,6 +200,7 @@
#include "mozilla/dom/NetErrorInfoBinding.h"
#include "mozilla/dom/NodeInfo.h"
#include "mozilla/dom/NodeIterator.h"
+#include "mozilla/dom/nsHTTPSOnlyUtils.h"
#include "mozilla/dom/PContentChild.h"
#include "mozilla/dom/PWindowGlobalChild.h"
#include "mozilla/dom/PageTransitionEvent.h"
@@ -1437,7 +1440,6 @@ Document::Document(const char* aContentType)
mThrowOnDynamicMarkupInsertionCounter(0),
mIgnoreOpensDuringUnloadCounter(0),
mSavedResolution(1.0f),
- mSavedResolutionBeforeMVM(1.0f),
mGeneration(0),
mCachedTabSizeGeneration(0),
mNextFormNumber(0),
@@ -2484,6 +2486,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyForIdle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFragmentDirective)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHighlightRegistry)
// Traverse all Document nsCOMPtrs.
@@ -2631,6 +2634,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFragmentDirective)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mHighlightRegistry)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOnloadBlocker)
@@ -4065,6 +4069,21 @@ void Document::StopDocumentLoad() {
void Document::SetDocumentURI(nsIURI* aURI) {
nsCOMPtr<nsIURI> oldBase = GetDocBaseURI();
mDocumentURI = aURI;
+ // This loosely implements §3.4.1 of Text Fragments
+ // https://wicg.github.io/scroll-to-text-fragment/#invoking-text-directives
+ // Unlike specified in the spec, the fragment directive is not stripped from
+ // the URL in the session history entry. Instead it is removed when the URL is
+ // set in the `Document`. Also, instead of storing the `uninvokedDirective` in
+ // `Document` as mentioned in the spec, the extracted directives are moved to
+ // the `FragmentDirective` object which deals with finding the ranges to
+ // highlight in `ScrollToRef()`.
+ // XXX(:jjaschke): This is only a temporary solution.
+ // https://bugzil.la/1881429 is filed for revisiting this.
+ nsTArray<TextDirective> textDirectives;
+ FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(
+ mDocumentURI, &textDirectives);
+ FragmentDirective()->SetTextDirectives(std::move(textDirectives));
+
nsIURI* newBase = GetDocBaseURI();
mChromeRulesEnabled = URLExtraData::ChromeRulesEnabled(aURI);
@@ -4103,10 +4122,11 @@ void Document::SetDocumentURI(nsIURI* aURI) {
}
}
-static void GetFormattedTimeString(PRTime aTime,
+static void GetFormattedTimeString(PRTime aTime, bool aUniversal,
nsAString& aFormattedTimeString) {
PRExplodedTime prtime;
- PR_ExplodeTime(aTime, PR_LocalTimeParameters, &prtime);
+ PR_ExplodeTime(aTime, aUniversal ? PR_GMTParameters : PR_LocalTimeParameters,
+ &prtime);
// "MM/DD/YYYY hh:mm:ss"
char formatedTime[24];
if (SprintfLiteral(formatedTime, "%02d/%02d/%04d %02d:%02d:%02d",
@@ -4124,7 +4144,9 @@ void Document::GetLastModified(nsAString& aLastModified) const {
if (!mLastModified.IsEmpty()) {
aLastModified.Assign(mLastModified);
} else {
- GetFormattedTimeString(PR_Now(), aLastModified);
+ GetFormattedTimeString(PR_Now(),
+ ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC),
+ aLastModified);
}
}
@@ -6401,7 +6423,7 @@ void Document::SetLastFocusTime(const TimeStamp& aFocusTime) {
mLastFocusTime = aFocusTime;
}
-void Document::GetReferrer(nsAString& aReferrer) const {
+void Document::GetReferrer(nsACString& aReferrer) const {
aReferrer.Truncate();
if (!mReferrerInfo) {
return;
@@ -6412,13 +6434,7 @@ void Document::GetReferrer(nsAString& aReferrer) const {
return;
}
- nsAutoCString uri;
- nsresult rv = URLDecorationStripper::StripTrackingIdentifiers(referrer, uri);
- if (NS_WARN_IF(NS_FAILED(rv))) {
- return;
- }
-
- CopyUTF8toUTF16(uri, aReferrer);
+ URLDecorationStripper::StripTrackingIdentifiers(referrer, aReferrer);
}
void Document::GetCookie(nsAString& aCookie, ErrorResult& aRv) {
@@ -7683,6 +7699,10 @@ static void NotifyActivityChangedCallback(nsISupports* aSupports) {
void Document::NotifyActivityChanged() {
EnumerateActivityObservers(NotifyActivityChangedCallback);
+ // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-full-activity
+ if (!IsActive()) {
+ UnlockAllWakeLocks(WakeLockType::Screen);
+ }
}
void Document::SetContainer(nsDocShell* aContainer) {
@@ -11111,7 +11131,9 @@ void Document::RetrieveRelevantHeaders(nsIChannel* aChannel) {
mLastModified.Truncate();
if (modDate != 0) {
- GetFormattedTimeString(modDate, mLastModified);
+ GetFormattedTimeString(modDate,
+ ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC),
+ mLastModified);
}
}
@@ -12537,14 +12559,6 @@ void Document::UpdateDocumentStates(DocumentState aMaybeChangedStates,
}
}
- if (aMaybeChangedStates.HasAtLeastOneOfStates(DocumentState::LWTHEME)) {
- if (ComputeDocumentLWTheme()) {
- mState |= DocumentState::LWTHEME;
- } else {
- mState &= ~DocumentState::LWTHEME;
- }
- }
-
if (aMaybeChangedStates.HasState(DocumentState::WINDOW_INACTIVE)) {
BrowsingContext* bc = GetBrowsingContext();
if (!bc || !bc->GetIsActiveBrowserWindow()) {
@@ -13089,25 +13103,29 @@ void Document::SetScrollToRef(nsIURI* aDocumentURI) {
// https://html.spec.whatwg.org/#scrolling-to-a-fragment
void Document::ScrollToRef() {
- if (mScrolledToRefAlready) {
- RefPtr<PresShell> presShell = GetPresShell();
- if (presShell) {
- presShell->ScrollToAnchor();
- }
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (!presShell) {
return;
}
-
- // 2. If fragment is the empty string, then return the special value top of
- // the document.
- if (mScrollToRef.IsEmpty()) {
+ if (mScrolledToRefAlready) {
+ presShell->ScrollToAnchor();
return;
}
- RefPtr<PresShell> presShell = GetPresShell();
- if (!presShell) {
+ // If text directives is non-null, then highlight the text directives and
+ // scroll to the last one.
+ // XXX(:jjaschke): Document policy integration should happen here
+ // as soon as https://bugzil.la/1860915 lands.
+ // XXX(:jjaschke): Same goes for User Activation and security aspects,
+ // tracked in https://bugzil.la/1888756.
+ const bool didScrollToTextFragment =
+ presShell->HighlightAndGoToTextFragment(true);
+
+ // 2. If fragment is the empty string and no text directives have been
+ // scrolled to, then return the special value top of the document.
+ if (didScrollToTextFragment || mScrollToRef.IsEmpty()) {
return;
}
-
// 3. Let potentialIndicatedElement be the result of finding a potential
// indicated element given document and fragment.
NS_ConvertUTF8toUTF16 ref(mScrollToRef);
@@ -15089,11 +15107,6 @@ void Document::HideAllPopoversUntil(nsINode& aEndpoint,
} while (repeatingHide);
}
-MOZ_CAN_RUN_SCRIPT_BOUNDARY void
-Document::HideAllPopoversWithoutRunningScript() {
- return HideAllPopoversUntil(*this, false, false);
-}
-
void Document::HidePopover(Element& aPopover, bool aFocusPreviousElement,
bool aFireEvents, ErrorResult& aRv) {
RefPtr<nsGenericHTMLElement> popoverHTMLEl =
@@ -15548,13 +15561,21 @@ bool Document::HasPendingFullscreenRequests() {
return !iter.AtEnd();
}
+MOZ_CAN_RUN_SCRIPT_BOUNDARY
bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) {
if (!FullscreenElementReadyCheck(*aRequest)) {
return false;
}
+ Element* elem = aRequest->Element();
+
+ RefPtr<nsINode> hideUntil = elem->GetTopmostPopoverAncestor(nullptr, false);
+ if (!hideUntil) {
+ hideUntil = OwnerDoc();
+ }
+
RefPtr<Document> doc = aRequest->Document();
- doc->HideAllPopoversWithoutRunningScript();
+ doc->HideAllPopoversUntil(*hideUntil, false, true);
// Stash a reference to any existing fullscreen doc, we'll use this later
// to detect if the origin which is fullscreen has changed.
@@ -15580,7 +15601,6 @@ bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) {
// Set the fullscreen element. This sets the fullscreen style on the
// element, and the fullscreen-ancestor styles on ancestors of the element
// in this document.
- Element* elem = aRequest->Element();
SetFullscreenElement(*elem);
// Set the iframe fullscreen flag.
if (auto* iframe = HTMLIFrameElement::FromNode(elem)) {
@@ -15698,6 +15718,11 @@ void Document::UpdateVisibilityState(DispatchVisibilityChange aDispatchEvent) {
for (auto* listener : mWorkerListeners) {
listener->OnVisible(visible);
}
+
+ // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-visibility
+ if (!visible) {
+ UnlockAllWakeLocks(WakeLockType::Screen);
+ }
}
}
@@ -16558,16 +16583,6 @@ void Document::SetStateObject(nsIStructuredCloneContainer* scContainer) {
mCachedStateObjectValid = false;
}
-bool Document::ComputeDocumentLWTheme() const {
- if (!NodePrincipal()->IsSystemPrincipal()) {
- return false;
- }
-
- Element* element = GetRootElement();
- return element && element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::lwtheme,
- nsGkAtoms::_true, eCaseMatters);
-}
-
already_AddRefed<Element> Document::CreateHTMLElement(nsAtom* aTag) {
RefPtr<mozilla::dom::NodeInfo> nodeInfo;
nodeInfo = mNodeInfoManager->GetNodeInfo(aTag, nullptr, kNameSpaceID_XHTML,
@@ -17525,6 +17540,18 @@ Document::CreatePermissionGrantPromise(
p = new StorageAccessAPIHelper::StorageAccessPermissionGrantPromise::
Private(__func__);
+ // Before we prompt, see if we are same-site
+ if (aFrameOnly) {
+ nsIChannel* channel = self->GetChannel();
+ if (channel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ if (!loadInfo->GetIsThirdPartyContextToTopWindow()) {
+ p->Resolve(StorageAccessAPIHelper::eAllow, __func__);
+ return p;
+ }
+ }
+ }
+
RefPtr<PWindowGlobalChild::GetStorageAccessPermissionPromise> promise;
// Test the permission
MOZ_ASSERT(XRE_IsContentProcess());
@@ -18357,9 +18384,13 @@ class UnlockAllWakeLockRunnable final : public Runnable {
void Document::UnlockAllWakeLocks(WakeLockType aType) {
// Perform unlock in a runnable to prevent UnlockAll being MOZ_CAN_RUN_SCRIPT
- RefPtr<UnlockAllWakeLockRunnable> runnable =
- MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this);
- NS_DispatchToMainThread(runnable);
+ if (!ActiveWakeLocks(aType).IsEmpty()) {
+ RefPtr<UnlockAllWakeLockRunnable> runnable =
+ MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this);
+ nsresult rv = NS_DispatchToMainThread(runnable);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
}
RefPtr<Document::AutomaticStorageAccessPermissionGrantPromise>
@@ -18637,12 +18668,24 @@ nsICookieJarSettings* Document::CookieJarSettings() {
net::CookieJarSettings::Cast(mCookieJarSettings)
->SetFingerprintingRandomizationKey(randomKey);
}
+
+ // Inerit the top level windowContext id from the parent.
+ net::CookieJarSettings::Cast(mCookieJarSettings)
+ ->SetTopLevelWindowContextId(
+ net::CookieJarSettings::Cast(inProcessParent->CookieJarSettings())
+ ->GetTopLevelWindowContextId());
} else {
mCookieJarSettings = net::CookieJarSettings::Create(NodePrincipal());
+
+ if (IsTopLevelContentDocument()) {
+ net::CookieJarSettings::Cast(mCookieJarSettings)
+ ->SetTopLevelWindowContextId(InnerWindowID());
+ }
}
if (auto* wgc = GetWindowGlobalChild()) {
net::CookieJarSettingsArgs csArgs;
+
net::CookieJarSettings::Cast(mCookieJarSettings)->Serialize(csArgs);
// Update cookie settings in the parent process
if (!wgc->SendUpdateCookieJarSettings(csArgs)) {
@@ -19042,6 +19085,13 @@ HighlightRegistry& Document::HighlightRegistry() {
return *mHighlightRegistry;
}
+FragmentDirective* Document::FragmentDirective() {
+ if (!mFragmentDirective) {
+ mFragmentDirective = MakeRefPtr<class FragmentDirective>(this);
+ }
+ return mFragmentDirective;
+}
+
RadioGroupContainer& Document::OwnedRadioGroupContainer() {
if (!mRadioGroupContainer) {
mRadioGroupContainer = MakeUnique<RadioGroupContainer>();
diff --git a/dom/base/Document.h b/dom/base/Document.h
index a52c61addf..0b0d0ca3d0 100644
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -244,6 +244,7 @@ class EventListener;
struct FailedCertSecurityInfo;
class FeaturePolicy;
class FontFaceSet;
+class FragmentDirective;
class FrameRequestCallback;
class ImageTracker;
class HighlightRegistry;
@@ -2992,15 +2993,6 @@ class Document : public nsINode,
SetStateObject(aDocument->mStateObjectContainer);
}
- /**
- * Returns true if there is a lightweight theme specified. This is used to
- * determine the state of the :-moz-lwtheme pseudo-class.
- */
- bool ComputeDocumentLWTheme() const;
- void ResetDocumentLWTheme() {
- UpdateDocumentStates(DocumentState::LWTHEME, true);
- }
-
// Whether we're a media document or not.
enum class MediaDocumentKind {
NotMedia,
@@ -3281,7 +3273,7 @@ class Document : public nsINode,
void SetDomain(const nsAString& aDomain, mozilla::ErrorResult& rv);
void GetCookie(nsAString& aCookie, mozilla::ErrorResult& rv);
void SetCookie(const nsAString& aCookie, mozilla::ErrorResult& rv);
- void GetReferrer(nsAString& aReferrer) const;
+ void GetReferrer(nsACString& aReferrer) const;
void GetLastModified(nsAString& aLastModified) const;
void GetReadyState(nsAString& aReadyState) const;
@@ -3377,7 +3369,6 @@ class Document : public nsINode,
bool aFocusPreviousElement,
bool aFireEvents);
- MOZ_CAN_RUN_SCRIPT_BOUNDARY void HideAllPopoversWithoutRunningScript();
// Hides the given popover element, see
// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm
MOZ_CAN_RUN_SCRIPT void HidePopover(Element& popover,
@@ -4100,6 +4091,13 @@ class Document : public nsINode,
*/
class HighlightRegistry& HighlightRegistry();
+ /**
+ * @brief Returns the `FragmentDirective` object which contains information
+ * and functionality to extract or create text directives.
+ * Guaranteed to be non-null.
+ */
+ class FragmentDirective* FragmentDirective();
+
bool ShouldResistFingerprinting(RFPTarget aTarget) const;
bool IsInPrivateBrowsing() const;
@@ -4159,7 +4157,8 @@ class Document : public nsINode,
// Apply the fullscreen state to the document, and trigger related
// events. It returns false if the fullscreen element ready check
// fails and nothing gets changed.
- bool ApplyFullscreen(UniquePtr<FullscreenRequest>);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ApplyFullscreen(
+ UniquePtr<FullscreenRequest>);
void RemoveDocStyleSheetsFromStyleSets();
void ResetStylesheetsToURI(nsIURI* aURI);
@@ -5314,9 +5313,6 @@ class Document : public nsINode,
// Pres shell resolution saved before entering fullscreen mode.
float mSavedResolution;
- // Pres shell resolution saved before creating a MobileViewportManager.
- float mSavedResolutionBeforeMVM;
-
nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
bool mHasStoragePermission;
@@ -5381,6 +5377,7 @@ class Document : public nsINode,
nsTArray<CanvasUsage> mCanvasUsage;
uint64_t mLastCanvasUsage = 0;
+ RefPtr<class FragmentDirective> mFragmentDirective;
UniquePtr<RadioGroupContainer> mRadioGroupContainer;
public:
@@ -5392,11 +5389,6 @@ class Document : public nsINode,
nsRefPtrHashtable<nsRefPtrHashKey<Element>, nsXULPrototypeElement>
mL10nProtoElements;
- float GetSavedResolutionBeforeMVM() { return mSavedResolutionBeforeMVM; }
- void SetSavedResolutionBeforeMVM(float aResolution) {
- mSavedResolutionBeforeMVM = aResolution;
- }
-
void LoadEventFired();
RadioGroupContainer& OwnedRadioGroupContainer();
diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp
index be31000278..b6f5d5c3be 100644
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1055,6 +1055,14 @@ already_AddRefed<nsIScreen> Element::GetScreen() {
return nullptr;
}
+double Element::CurrentCSSZoom() {
+ nsIFrame* f = GetPrimaryFrame(FlushType::Frames);
+ if (!f) {
+ return 1.0;
+ }
+ return f->Style()->EffectiveZoom().ToFloat();
+}
+
already_AddRefed<DOMRect> Element::GetBoundingClientRect() {
RefPtr<DOMRect> rect = new DOMRect(ToSupports(OwnerDoc()));
@@ -1233,15 +1241,14 @@ bool Element::CanAttachShadowDOM() const {
return true;
}
-// https://dom.spec.whatwg.org/commit-snapshots/1eadf0a4a271acc92013d1c0de8c730ac96204f9/#dom-element-attachshadow
-already_AddRefed<ShadowRoot> Element::AttachShadow(
- const ShadowRootInit& aInit, ErrorResult& aError,
- ShadowRootDeclarative aNewShadowIsDeclarative) {
+// https://dom.spec.whatwg.org/#dom-element-attachshadow
+already_AddRefed<ShadowRoot> Element::AttachShadow(const ShadowRootInit& aInit,
+ ErrorResult& aError) {
/**
* Step 1, 2, and 3.
*/
if (!CanAttachShadowDOM()) {
- aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ aError.ThrowNotSupportedError("Unable to attach ShadowDOM");
return nullptr;
}
@@ -1249,21 +1256,27 @@ already_AddRefed<ShadowRoot> Element::AttachShadow(
* 4. If element is a shadow host, then:
*/
if (RefPtr<ShadowRoot> root = GetShadowRoot()) {
- /*
- * 1. If element’s shadow root’s declarative is false, then throw an
- * "NotSupportedError" DOMException.
+ /**
+ * 1. Let currentShadowRoot be element’s shadow root.
+ *
+ * 2. If any of the following are true:
+ * currentShadowRoot’s declarative is false; or
+ * currentShadowRoot’s mode is not mode,
+ * then throw a "NotSupportedError" DOMException.
*/
- if (!root->IsDeclarative()) {
- aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ if (!root->IsDeclarative() || root->Mode() != aInit.mMode) {
+ aError.ThrowNotSupportedError(
+ "Unable to re-attach to existing ShadowDOM");
return nullptr;
}
- // https://github.com/whatwg/dom/issues/1235
- root->SetIsDeclarative(aNewShadowIsDeclarative);
- /*
- * 2. Otherwise, remove all of element’s shadow root’s children, in tree
- * order, and return.
+ /**
+ * 3. Otherwise:
+ * 1. Remove all of currentShadowRoot’s children, in tree order.
+ * 2. Set currentShadowRoot’s declarative to false.
+ * 3. Return.
*/
root->ReplaceChildren(nullptr, aError);
+ root->SetIsDeclarative(ShadowRootDeclarative::No);
return root.forget();
}
@@ -1273,14 +1286,12 @@ already_AddRefed<ShadowRoot> Element::AttachShadow(
return AttachShadowWithoutNameChecks(
aInit.mMode, DelegatesFocus(aInit.mDelegatesFocus), aInit.mSlotAssignment,
- ShadowRootClonable(aInit.mClonable),
- ShadowRootDeclarative(aNewShadowIsDeclarative));
+ ShadowRootClonable(aInit.mClonable));
}
already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks(
ShadowRootMode aMode, DelegatesFocus aDelegatesFocus,
- SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable,
- ShadowRootDeclarative aDeclarative) {
+ SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable) {
nsAutoScriptBlocker scriptBlocker;
auto* nim = mNodeInfo->NodeInfoManager();
@@ -1304,9 +1315,9 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks(
* context object's node document, host is context object,
* and mode is init's mode.
*/
- RefPtr<ShadowRoot> shadowRoot =
- new (nim) ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment,
- aClonable, aDeclarative, nodeInfo.forget());
+ RefPtr<ShadowRoot> shadowRoot = new (nim)
+ ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment, aClonable,
+ ShadowRootDeclarative::No, nodeInfo.forget());
if (NodeOrAncestorHasDirAuto()) {
shadowRoot->SetAncestorHasDirAuto();
@@ -1336,6 +1347,22 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks(
dispatcher->PostDOMEvent();
}
+ const LinkedList<AbstractRange>* ranges =
+ GetExistingClosestCommonInclusiveAncestorRanges();
+ if (ranges) {
+ for (const AbstractRange* range : *ranges) {
+ if (range->MayCrossShadowBoundary()) {
+ MOZ_ASSERT(range->IsDynamicRange());
+ StaticRange* crossBoundaryRange =
+ range->AsDynamicRange()->GetCrossShadowBoundaryRange();
+ MOZ_ASSERT(crossBoundaryRange);
+ // We may have previously selected this node before it
+ // becomes a shadow host, so we need to reset the values
+ // in RangeBoundaries to accommodate the change.
+ crossBoundaryRange->NotifyNodeBecomesShadowHost(this);
+ }
+ }
+ }
/**
* 10. Return shadow.
*/
@@ -1460,13 +1487,7 @@ void Element::GetAttribute(const nsAString& aName, DOMString& aReturn) {
if (val) {
val->ToString(aReturn);
} else {
- if (IsXULElement()) {
- // XXX should be SetDOMStringToNull(aReturn);
- // See bug 232598
- // aReturn is already empty
- } else {
- aReturn.SetNull();
- }
+ aReturn.SetNull();
}
}
@@ -1930,9 +1951,7 @@ nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) {
// This has to be here, rather than in nsGenericHTMLElement::BindToTree,
// because it has to happen after updating the parent pointer, but before
// recursively binding the kids.
- if (IsHTMLElement()) {
- SetDirOnBind(this, nsIContent::FromNode(aParent));
- }
+ SetDirOnBind(this, nsIContent::FromNode(aParent));
UpdateEditableState(false);
@@ -2145,9 +2164,7 @@ void Element::UnbindFromTree(UnbindContext& aContext) {
// This has to be here, rather than in nsGenericHTMLElement::UnbindFromTree,
// because it has to happen after unsetting the parent pointer, but before
// recursively unbinding the kids.
- if (IsHTMLElement()) {
- ResetDir(this);
- }
+ ResetDir(this);
for (nsIContent* child = GetFirstChild(); child;
child = child->GetNextSibling()) {
diff --git a/dom/base/Element.h b/dom/base/Element.h
index 40a8052aef..fb88274a88 100644
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -257,7 +257,8 @@ class Element : public FragmentOrElement {
#ifdef MOZILLA_INTERNAL_API
explicit Element(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
: FragmentOrElement(std::move(aNodeInfo)),
- mState(ElementState::READONLY | ElementState::DEFINED) {
+ mState(ElementState::READONLY | ElementState::DEFINED |
+ ElementState::LTR) {
MOZ_ASSERT(mNodeInfo->NodeType() == ELEMENT_NODE,
"Bad NodeType in aNodeInfo");
SetIsElement();
@@ -1352,10 +1353,8 @@ class Element : public FragmentOrElement {
enum class ShadowRootDeclarative : bool { No, Yes };
MOZ_CAN_RUN_SCRIPT_BOUNDARY
- already_AddRefed<ShadowRoot> AttachShadow(
- const ShadowRootInit& aInit, ErrorResult& aError,
- ShadowRootDeclarative aNewShadowIsDeclarative =
- ShadowRootDeclarative::No);
+ already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
+ ErrorResult& aError);
bool CanAttachShadowDOM() const;
enum class DelegatesFocus : bool { No, Yes };
@@ -1364,8 +1363,7 @@ class Element : public FragmentOrElement {
already_AddRefed<ShadowRoot> AttachShadowWithoutNameChecks(
ShadowRootMode aMode, DelegatesFocus = DelegatesFocus::No,
SlotAssignmentMode aSlotAssignmentMode = SlotAssignmentMode::Named,
- ShadowRootClonable aClonable = ShadowRootClonable::No,
- ShadowRootDeclarative aDeclarative = ShadowRootDeclarative::No);
+ ShadowRootClonable aClonable = ShadowRootClonable::No);
// Attach UA Shadow Root if it is not attached.
enum class NotifyUAWidgetSetup : bool { No, Yes };
@@ -1499,6 +1497,8 @@ class Element : public FragmentOrElement {
return CSSPixel::FromAppUnits(GetClientAreaRect().Width());
}
+ MOZ_CAN_RUN_SCRIPT double CurrentCSSZoom();
+
// This function will return the block size of first line box, no matter if
// the box is 'block' or 'inline'. The return unit is pixel. If the element
// can't get a primary frame, we will return be zero.
diff --git a/dom/base/EventSource.cpp b/dom/base/EventSource.cpp
index f70db487dd..def3c90ec0 100644
--- a/dom/base/EventSource.cpp
+++ b/dom/base/EventSource.cpp
@@ -568,7 +568,15 @@ nsresult EventSourceImpl::ParseURL(const nsAString& aURL) {
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> srcURI;
- rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI);
+ nsCOMPtr<Document> doc =
+ mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr;
+ if (doc) {
+ rv = NS_NewURI(getter_AddRefs(srcURI), aURL, doc->GetDocumentCharacterSet(),
+ baseURI);
+ } else {
+ rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI);
+ }
+
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
nsAutoString origin;
diff --git a/dom/base/FragmentDirective.cpp b/dom/base/FragmentDirective.cpp
new file mode 100644
index 0000000000..3300a85751
--- /dev/null
+++ b/dom/base/FragmentDirective.cpp
@@ -0,0 +1,879 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FragmentDirective.h"
+#include <cstdint>
+#include "RangeBoundary.h"
+#include "mozilla/Assertions.h"
+#include "Document.h"
+#include "mozilla/dom/FragmentDirectiveBinding.h"
+#include "mozilla/dom/FragmentOrElement.h"
+#include "mozilla/dom/NodeBinding.h"
+#include "mozilla/dom/Text.h"
+#include "mozilla/intl/WordBreaker.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsDOMAttributeMap.h"
+#include "nsGkAtoms.h"
+#include "nsICSSDeclaration.h"
+#include "nsIFrame.h"
+#include "nsINode.h"
+#include "nsIURIMutator.h"
+#include "nsRange.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+static LazyLogModule sFragmentDirectiveLog("FragmentDirective");
+
+/** Converts a `TextDirective` into a percent-encoded string. */
+nsCString ToString(const TextDirective& aTextDirective) {
+ nsCString str;
+ create_text_directive(&aTextDirective, &str);
+ return str;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FragmentDirective, mDocument)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FragmentDirective)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FragmentDirective)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FragmentDirective)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+FragmentDirective::FragmentDirective(Document* aDocument)
+ : mDocument(aDocument) {}
+
+JSObject* FragmentDirective::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FragmentDirective_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(
+ nsCOMPtr<nsIURI>& aURI, nsTArray<TextDirective>* aTextDirectives) {
+ if (!aURI || !StaticPrefs::dom_text_fragments_enabled()) {
+ return;
+ }
+ bool hasRef = false;
+ aURI->GetHasRef(&hasRef);
+ if (!hasRef) {
+ return;
+ }
+
+ nsAutoCString hash;
+ aURI->GetRef(hash);
+
+ ParsedFragmentDirectiveResult fragmentDirective;
+ const bool hasRemovedFragmentDirective =
+ parse_fragment_directive(&hash, &fragmentDirective);
+ if (!hasRemovedFragmentDirective) {
+ return;
+ }
+ Unused << NS_MutateURI(aURI)
+ .SetRef(fragmentDirective.url_without_fragment_directive)
+ .Finalize(aURI);
+ if (aTextDirectives) {
+ aTextDirectives->SwapElements(fragmentDirective.text_directives);
+ }
+}
+
+nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() {
+ MOZ_ASSERT(mDocument);
+ mDocument->FlushPendingNotifications(FlushType::Frames);
+ nsTArray<RefPtr<nsRange>> textDirectiveRanges;
+ for (const TextDirective& textDirective : mUninvokedTextDirectives) {
+ if (RefPtr<nsRange> range = FindRangeForTextDirective(textDirective)) {
+ textDirectiveRanges.AppendElement(range);
+ }
+ }
+ mUninvokedTextDirectives.Clear();
+ return textDirectiveRanges;
+}
+
+/**
+ * @brief Determine if `aNode` should be considered when traversing the DOM.
+ *
+ * A node is "search invisible" if it is an element in the HTML namespace and
+ * 1. The computed value of its `display` property is `none`
+ * 2. It serializes as void
+ * 3. It is one of the following types:
+ * - HTMLIFrameElement
+ * - HTMLImageElement
+ * - HTMLMeterElement
+ * - HTMLObjectElement
+ * - HTMLProgressElement
+ * - HTMLStyleElement
+ * - HTMLScriptElement
+ * - HTMLVideoElement
+ * - HTMLAudioElement
+ * 4. It is a `select` element whose `multiple` content attribute is absent
+ *
+ * see https://wicg.github.io/scroll-to-text-fragment/#search-invisible
+ */
+bool NodeIsSearchInvisible(nsINode& aNode) {
+ if (!aNode.IsElement()) {
+ return false;
+ }
+ // 2. If the node serializes as void.
+ nsAtom* nodeNameAtom = aNode.NodeInfo()->NameAtom();
+ if (FragmentOrElement::IsHTMLVoid(nodeNameAtom)) {
+ return true;
+ }
+ // 3. Is any of the following types: HTMLIFrameElement, HTMLImageElement,
+ // HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement,
+ // HTMLScriptElement, HTMLVideoElement, HTMLAudioElement
+ if (aNode.IsAnyOfHTMLElements(
+ nsGkAtoms::iframe, nsGkAtoms::image, nsGkAtoms::meter,
+ nsGkAtoms::object, nsGkAtoms::progress, nsGkAtoms::style,
+ nsGkAtoms::script, nsGkAtoms::video, nsGkAtoms::audio)) {
+ return true;
+ }
+ // 4. Is a select element whose multiple content attribute is absent.
+ if (aNode.IsHTMLElement(nsGkAtoms::select)) {
+ return aNode.GetAttributes()->GetNamedItem(u"multiple"_ns) == nullptr;
+ }
+ // This is tested last because it's the most expensive check.
+ // 1. The computed value of its 'display' property is 'none'.
+ const Element* nodeAsElement = Element::FromNode(aNode);
+ const RefPtr<const ComputedStyle> computedStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement);
+ return !computedStyle ||
+ computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None;
+}
+
+/**
+ * @brief Returns true if `aNode` has block-level display.
+ * A node has block-level display if it is an element and the computed value
+ * of its display property is any of
+ * - block
+ * - table
+ * - flow-root
+ * - grid
+ * - flex
+ * - list-item
+ *
+ * See https://wicg.github.io/scroll-to-text-fragment/#has-block-level-display
+ */
+bool NodeHasBlockLevelDisplay(nsINode& aNode) {
+ if (!aNode.IsElement()) {
+ return false;
+ }
+ const Element* nodeAsElement = Element::FromNode(aNode);
+ const RefPtr<const ComputedStyle> computedStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement);
+ if (!computedStyle) {
+ return false;
+ }
+ const StyleDisplay& styleDisplay = computedStyle->StyleDisplay()->mDisplay;
+ return styleDisplay == StyleDisplay::Block ||
+ styleDisplay == StyleDisplay::Table ||
+ styleDisplay == StyleDisplay::FlowRoot ||
+ styleDisplay == StyleDisplay::Grid ||
+ styleDisplay == StyleDisplay::Flex || styleDisplay.IsListItem();
+}
+
+/**
+ * @brief Get the Block Ancestor For `aNode`.
+ *
+ * see https://wicg.github.io/scroll-to-text-fragment/#nearest-block-ancestor
+ */
+nsINode* GetBlockAncestorForNode(nsINode* aNode) {
+ // 1. Let curNode be node.
+ RefPtr<nsINode> curNode = aNode;
+ // 2. While curNode is non-null
+ while (curNode) {
+ // 2.1. If curNode is not a Text node and it has block-level display then
+ // return curNode.
+ if (!curNode->IsText() && NodeHasBlockLevelDisplay(*curNode)) {
+ return curNode;
+ }
+ // 2.2. Otherwise, set curNode to curNode’s parent.
+ curNode = curNode->GetParentNode();
+ }
+ // 3.Return node’s node document's document element.
+ return aNode->GetOwnerDocument();
+}
+
+/**
+ * @brief Returns true if `aNode` is part of a non-searchable subtree.
+ *
+ * A node is part of a non-searchable subtree if it is or has a shadow-including
+ * ancestor that is search invisible.
+ *
+ * see https://wicg.github.io/scroll-to-text-fragment/#non-searchable-subtree
+ */
+bool NodeIsPartOfNonSearchableSubTree(nsINode& aNode) {
+ nsINode* node = &aNode;
+ do {
+ if (NodeIsSearchInvisible(*node)) {
+ return true;
+ }
+ } while ((node = node->GetParentOrShadowHostNode()));
+ return false;
+}
+
+/**
+ * @brief Return true if `aNode` is a visible Text node.
+ *
+ * A node is a visible text node if it is a Text node, the computed value of
+ * its parent element's visibility property is visible, and it is being
+ * rendered.
+ *
+ * see https://wicg.github.io/scroll-to-text-fragment/#visible-text-node
+ */
+bool NodeIsVisibleTextNode(const nsINode& aNode) {
+ const Text* text = Text::FromNode(aNode);
+ if (!text) {
+ return false;
+ }
+ const nsIFrame* frame = text->GetPrimaryFrame();
+ return frame && frame->StyleVisibility()->IsVisible();
+}
+
+enum class TextScanDirection { Left = -1, Right = 1 };
+
+/**
+ * @brief Tests if there is whitespace at the given position and direction.
+ *
+ * This algorithm tests for whitespaces and `&nbsp;` at `aPos`.
+ * It returns the size of the whitespace found at the position, i.e. 5/6 for
+ * `&nbsp/;` and 1 otherwise.
+ *
+ * This function follows a subsection of this section of the spec, but has been
+ * adapted to be able to scan in both directions:
+ * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position
+ */
+uint32_t IsWhitespaceAtPosition(nsString& aText, uint32_t aPos,
+ TextScanDirection aDirection) {
+ if (aText.Length() == 0) {
+ return 0;
+ }
+ if (aDirection == TextScanDirection::Right) {
+ if (aText.Length() > (aPos + 5)) {
+ if (Substring(aText, aPos, 5).Equals(u"&nbsp")) {
+ return aText.Length() > (aPos + 6) && aText.CharAt(aPos + 6) == u';'
+ ? 6
+ : 5;
+ }
+ }
+ } else {
+ if (aPos > 6 && Substring(aText, aPos - 6, 6).Equals(u"&nbsp;")) {
+ return 6;
+ }
+ if (aPos > 5 && Substring(aText, aPos - 5, 5).Equals(u"&nbsp")) {
+ return 5;
+ }
+ }
+ return uint32_t(IsSpaceCharacter(aText.CharAt(aPos)));
+}
+
+/** Advances the start of `aRange` to the next non-whitespace position.
+ * The function follows this section of the spec:
+ * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position
+ */
+void AdvanceStartToNextNonWhitespacePosition(nsRange& aRange) {
+ // 1. While range is not collapsed:
+ while (!aRange.Collapsed()) {
+ // 1.1. Let node be range's start node.
+ RefPtr<nsINode> node = aRange.GetStartContainer();
+ MOZ_ASSERT(node);
+ // 1.2. Let offset be range's start offset.
+ const uint32_t offset = aRange.StartOffset();
+ // 1.3. If node is part of a non-searchable subtree or if node is not a
+ // visible text node or if offset is equal to node's length then:
+ if (NodeIsPartOfNonSearchableSubTree(*node) ||
+ !NodeIsVisibleTextNode(*node) || offset == node->Length()) {
+ // 1.3.1. Set range's start node to the next node, in shadow-including
+ // tree order.
+ // 1.3.2. Set range's start offset to 0.
+ if (NS_FAILED(aRange.SetStart(node->GetNextNode(), 0))) {
+ return;
+ }
+ // 1.3.3. Continue.
+ continue;
+ }
+ const Text* text = Text::FromNode(node);
+ nsAutoString textData;
+ text->GetData(textData);
+ // These steps are moved to `IsWhitespaceAtPosition()`.
+ // 1.4. If the substring data of node at offset offset and count 6 is equal
+ // to the string "&nbsp;" then:
+ // 1.4.1. Add 6 to range’s start offset.
+ // 1.5. Otherwise, if the substring data of node at offset offset and count
+ // 5 is equal to the string "&nbsp" then:
+ // 1.5.1. Add 5 to range’s start offset.
+ // 1.6. Otherwise:
+ // 1.6.1 Let cp be the code point at the offset index in node’s data.
+ // 1.6.2 If cp does not have the White_Space property set, return.
+ // 1.6.3 Add 1 to range’s start offset.
+ const uint32_t whitespace =
+ IsWhitespaceAtPosition(textData, offset, TextScanDirection::Right);
+ if (whitespace == 0) {
+ return;
+ }
+
+ aRange.SetStart(node, offset + whitespace);
+ }
+}
+
+/**
+ * @brief Moves `aRangeBoundary` one word in `aDirection`.
+ *
+ * Word boundaries are determined using `intl::WordBreaker::FindWord()`.
+ *
+ *
+ * @param aRangeBoundary[in] The range boundary that should be moved.
+ * Must be set and valid.
+ * @param aDirection[in] The direction into which to move.
+ * @return A new `RangeBoundary` which is moved to the next word.
+ */
+RangeBoundary MoveRangeBoundaryOneWord(const RangeBoundary& aRangeBoundary,
+ TextScanDirection aDirection) {
+ MOZ_ASSERT(aRangeBoundary.IsSetAndValid());
+ RefPtr<nsINode> curNode = aRangeBoundary.Container();
+ uint32_t offset = *aRangeBoundary.Offset(
+ RangeBoundary::OffsetFilter::kValidOrInvalidOffsets);
+
+ const int offsetIncrement = int(aDirection);
+ // Get the text node of the start of the range and the offset.
+ // This is the current position of the start of the range.
+ nsAutoString text;
+ if (NodeIsVisibleTextNode(*curNode)) {
+ const Text* textNode = Text::FromNode(curNode);
+ textNode->GetData(text);
+
+ // Assuming that the current position might not be at a word boundary,
+ // advance to the word boundary at word begin/end.
+ if (!IsWhitespaceAtPosition(text, offset, aDirection)) {
+ const intl::WordRange wordRange =
+ intl::WordBreaker::FindWord(text, offset);
+ if (aDirection == TextScanDirection::Right &&
+ offset != wordRange.mBegin) {
+ offset = wordRange.mEnd;
+ } else if (aDirection == TextScanDirection::Left &&
+ offset != wordRange.mEnd) {
+ // The additional -1 is necessary to move to offset to *before* the
+ // start of the word.
+ offset = wordRange.mBegin - 1;
+ }
+ }
+ }
+ // Now, skip any whitespace, so that `offset` points to the word boundary of
+ // the next word (which is the one this algorithm actually aims to move over).
+ while (curNode) {
+ if (!NodeIsVisibleTextNode(*curNode) || NodeIsSearchInvisible(*curNode) ||
+ offset >= curNode->Length()) {
+ curNode = aDirection == TextScanDirection::Left ? curNode->GetPrevNode()
+ : curNode->GetNextNode();
+ if (!curNode) {
+ break;
+ }
+ offset =
+ aDirection == TextScanDirection::Left ? curNode->Length() - 1 : 0;
+ if (const Text* textNode = Text::FromNode(curNode)) {
+ textNode->GetData(text);
+ }
+ continue;
+ }
+ if (const uint32_t whitespace =
+ IsWhitespaceAtPosition(text, offset, aDirection)) {
+ offset += offsetIncrement * whitespace;
+ continue;
+ }
+
+ // At this point, the caret has been moved to the next non-whitespace
+ // position.
+ // find word boundaries at the current position
+ const intl::WordRange wordRange = intl::WordBreaker::FindWord(text, offset);
+ offset = aDirection == TextScanDirection::Left ? wordRange.mBegin
+ : wordRange.mEnd;
+
+ return {curNode, offset};
+ }
+ return {};
+}
+
+RefPtr<nsRange> FragmentDirective::FindRangeForTextDirective(
+ const TextDirective& aTextDirective) {
+ MOZ_LOG(sFragmentDirectiveLog, LogLevel::Info,
+ ("FragmentDirective::%s(): Find range for text directive '%s'.",
+ __FUNCTION__, ToString(aTextDirective).Data()));
+ // 1. Let searchRange be a range with start (document, 0) and end (document,
+ // document’s length)
+ ErrorResult rv;
+ RefPtr<nsRange> searchRange =
+ nsRange::Create(mDocument, 0, mDocument, mDocument->Length(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ // 2. While searchRange is not collapsed:
+ while (!searchRange->Collapsed()) {
+ // 2.1. Let potentialMatch be null.
+ RefPtr<nsRange> potentialMatch;
+ // 2.2. If parsedValues’s prefix is not null:
+ if (!aTextDirective.prefix.IsEmpty()) {
+ // 2.2.1. Let prefixMatch be the the result of running the find a string
+ // in range steps with query parsedValues’s prefix, searchRange
+ // searchRange, wordStartBounded true and wordEndBounded false.
+ RefPtr<nsRange> prefixMatch =
+ FindStringInRange(searchRange, aTextDirective.prefix, true, false);
+ // 2.2.2. If prefixMatch is null, return null.
+ if (!prefixMatch) {
+ return nullptr;
+ }
+ // 2.2.3. Set searchRange’s start to the first boundary point after
+ // prefixMatch’s start
+ const RangeBoundary boundaryPoint = MoveRangeBoundaryOneWord(
+ {prefixMatch->GetStartContainer(), prefixMatch->StartOffset()},
+ TextScanDirection::Right);
+ if (!boundaryPoint.IsSetAndValid()) {
+ return nullptr;
+ }
+ searchRange->SetStart(boundaryPoint.AsRaw(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+
+ // 2.2.4. Let matchRange be a range whose start is prefixMatch’s end and
+ // end is searchRange’s end.
+ RefPtr<nsRange> matchRange = nsRange::Create(
+ prefixMatch->GetEndContainer(), prefixMatch->EndOffset(),
+ searchRange->GetEndContainer(), searchRange->EndOffset(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ // 2.2.5. Advance matchRange’s start to the next non-whitespace position.
+ AdvanceStartToNextNonWhitespacePosition(*matchRange);
+ // 2.2.6. If matchRange is collapsed return null.
+ // (This can happen if prefixMatch’s end or its subsequent non-whitespace
+ // position is at the end of the document.)
+ if (matchRange->Collapsed()) {
+ return nullptr;
+ }
+ // 2.2.7. Assert: matchRange’s start node is a Text node.
+ // (matchRange’s start now points to the next non-whitespace text data
+ // following a matched prefix.)
+ MOZ_ASSERT(matchRange->GetStartContainer()->IsText());
+
+ // 2.2.8. Let mustEndAtWordBoundary be true if parsedValues’s end is
+ // non-null or parsedValues’s suffix is null, false otherwise.
+ const bool mustEndAtWordBoundary =
+ !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty();
+ // 2.2.9. Set potentialMatch to the result of running the find a string in
+ // range steps with query parsedValues’s start, searchRange matchRange,
+ // wordStartBounded false, and wordEndBounded mustEndAtWordBoundary.
+ potentialMatch = FindStringInRange(matchRange, aTextDirective.start,
+ false, mustEndAtWordBoundary);
+ // 2.2.10. If potentialMatch is null, return null.
+ if (!potentialMatch) {
+ return nullptr;
+ }
+ // 2.2.11. If potentialMatch’s start is not matchRange’s start, then
+ // continue.
+ // (In this case, we found a prefix but it was followed by something other
+ // than a matching text so we’ll continue searching for the next instance
+ // of prefix.)
+ if (potentialMatch->GetStartContainer() !=
+ matchRange->GetStartContainer()) {
+ continue;
+ }
+ }
+ // 2.3. Otherwise:
+ else {
+ // 2.3.1. Let mustEndAtWordBoundary be true if parsedValues’s end is
+ // non-null or parsedValues’s suffix is null, false otherwise.
+ const bool mustEndAtWordBoundary =
+ !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty();
+ // 2.3.2. Set potentialMatch to the result of running the find a string in
+ // range steps with query parsedValues’s start, searchRange searchRange,
+ // wordStartBounded true, and wordEndBounded mustEndAtWordBoundary.
+ potentialMatch = FindStringInRange(searchRange, aTextDirective.start,
+ true, mustEndAtWordBoundary);
+ // 2.3.3. If potentialMatch is null, return null.
+ if (!potentialMatch) {
+ return nullptr;
+ }
+ // 2.3.4. Set searchRange’s start to the first boundary point after
+ // potentialMatch’s start
+ RangeBoundary newRangeBoundary = MoveRangeBoundaryOneWord(
+ {potentialMatch->GetStartContainer(), potentialMatch->StartOffset()},
+ TextScanDirection::Right);
+ if (!newRangeBoundary.IsSetAndValid()) {
+ return nullptr;
+ }
+ searchRange->SetStart(newRangeBoundary.AsRaw(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ }
+ // 2.4. Let rangeEndSearchRange be a range whose start is potentialMatch’s
+ // end and whose end is searchRange’s end.
+ RefPtr<nsRange> rangeEndSearchRange = nsRange::Create(
+ potentialMatch->GetEndContainer(), potentialMatch->EndOffset(),
+ searchRange->GetEndContainer(), searchRange->EndOffset(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ // 2.5. While rangeEndSearchRange is not collapsed:
+ while (!rangeEndSearchRange->Collapsed()) {
+ // 2.5.1. If parsedValues’s end item is non-null, then:
+ if (!aTextDirective.end.IsEmpty()) {
+ // 2.5.1.1. Let mustEndAtWordBoundary be true if parsedValues’s suffix
+ // is null, false otherwise.
+ const bool mustEndAtWordBoundary = aTextDirective.suffix.IsEmpty();
+ // 2.5.1.2. Let endMatch be the result of running the find a string in
+ // range steps with query parsedValues’s end, searchRange
+ // rangeEndSearchRange, wordStartBounded true, and wordEndBounded
+ // mustEndAtWordBoundary.
+ RefPtr<nsRange> endMatch =
+ FindStringInRange(rangeEndSearchRange, aTextDirective.end, true,
+ mustEndAtWordBoundary);
+ // 2.5.1.3. If endMatch is null then return null.
+ if (!endMatch) {
+ return nullptr;
+ }
+ // 2.5.1.4. Set potentialMatch’s end to endMatch’s end.
+ potentialMatch->SetEnd(endMatch->GetEndContainer(),
+ endMatch->EndOffset());
+ }
+ // 2.5.2. Assert: potentialMatch is non-null, not collapsed and represents
+ // a range exactly containing an instance of matching text.
+ MOZ_ASSERT(potentialMatch && !potentialMatch->Collapsed());
+
+ // 2.5.3. If parsedValues’s suffix is null, return potentialMatch.
+ if (aTextDirective.suffix.IsEmpty()) {
+ return potentialMatch;
+ }
+ // 2.5.4. Let suffixRange be a range with start equal to potentialMatch’s
+ // end and end equal to searchRange’s end.
+ RefPtr<nsRange> suffixRange = nsRange::Create(
+ potentialMatch->GetEndContainer(), potentialMatch->EndOffset(),
+ searchRange->GetEndContainer(), searchRange->EndOffset(), rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ // 2.5.5. Advance suffixRange's start to the next non-whitespace position.
+ AdvanceStartToNextNonWhitespacePosition(*suffixRange);
+
+ // 2.5.6. Let suffixMatch be result of running the find a string in range
+ // steps with query parsedValue's suffix, searchRange suffixRange,
+ // wordStartBounded false, and wordEndBounded true.
+ RefPtr<nsRange> suffixMatch =
+ FindStringInRange(suffixRange, aTextDirective.suffix, false, true);
+
+ // 2.5.7. If suffixMatch is null, return null.
+ // (If the suffix doesn't appear in the remaining text of the document,
+ // there's no possible way to make a match.)
+ if (!suffixMatch) {
+ return nullptr;
+ }
+ // 2.5.8. If suffixMatch's start is suffixRange's start, return
+ // potentialMatch.
+ if (suffixMatch->GetStartContainer() ==
+ suffixRange->GetStartContainer() &&
+ suffixMatch->StartOffset() == suffixRange->StartOffset()) {
+ return potentialMatch;
+ }
+ // 2.5.9. If parsedValue's end item is null then break;
+ // (If this is an exact match and the suffix doesn’t match, start
+ // searching for the next range start by breaking out of this loop without
+ // rangeEndSearchRange being collapsed. If we’re looking for a range
+ // match, we’ll continue iterating this inner loop since the range start
+ // will already be correct.)
+ if (aTextDirective.end.IsEmpty()) {
+ break;
+ }
+ // 2.5.10. Set rangeEndSearchRange's start to potentialMatch's end.
+ // (Otherwise, it is possible that we found the correct range start, but
+ // not the correct range end. Continue the inner loop to keep searching
+ // for another matching instance of rangeEnd.)
+ rangeEndSearchRange->SetStart(potentialMatch->GetEndContainer(),
+ potentialMatch->EndOffset());
+ }
+ // 2.6. If rangeEndSearchRange is collapsed then:
+ if (rangeEndSearchRange->Collapsed()) {
+ // 2.6.1. Assert parsedValue's end item is non-null.
+ // (This can only happen for range matches due to the break for exact
+ // matches in step 9 of the above loop. If we couldn’t find a valid
+ // rangeEnd+suffix pair anywhere in the doc then there’s no possible way
+ // to make a match.)
+ // XXX(:jjaschke): should this really assert?
+ MOZ_ASSERT(!aTextDirective.end.IsEmpty());
+ }
+ }
+ // 3. Return null.
+ return nullptr;
+}
+
+/**
+ * @brief Convenience function that returns true if the given position in a
+ * string is a word boundary.
+ *
+ * This is a thin wrapper around the `WordBreaker::FindWord()` function.
+ *
+ * @param aText The text input.
+ * @param aPosition The position to check.
+ * @return true if there is a word boundary at `aPosition`.
+ * @return false otherwise.
+ */
+bool IsAtWordBoundary(const nsAString& aText, uint32_t aPosition) {
+ const intl::WordRange wordRange =
+ intl::WordBreaker::FindWord(aText, aPosition);
+ return wordRange.mBegin == aPosition || wordRange.mEnd == aPosition;
+}
+
+enum class IsEndIndex : bool { No, Yes };
+RangeBoundary GetBoundaryPointAtIndex(
+ uint32_t aIndex, const nsTArray<RefPtr<Text>>& aTextNodeList,
+ IsEndIndex aIsEndIndex) {
+ // 1. Let counted be 0.
+ uint32_t counted = 0;
+ // 2. For each curNode of nodes:
+ for (Text* curNode : aTextNodeList) {
+ // 2.1. Let nodeEnd be counted + curNode’s length.
+ uint32_t nodeEnd = counted + curNode->Length();
+ // 2.2. If isEnd is true, add 1 to nodeEnd.
+ if (aIsEndIndex == IsEndIndex::Yes) {
+ ++nodeEnd;
+ }
+ // 2.3. If nodeEnd is greater than index then:
+ if (nodeEnd > aIndex) {
+ // 2.3.1. Return the boundary point (curNode, index − counted).
+ return RangeBoundary(curNode->AsNode(), aIndex - counted);
+ }
+ // 2.4. Increment counted by curNode’s length.
+ counted += curNode->Length();
+ }
+ return {};
+}
+
+RefPtr<nsRange> FindRangeFromNodeList(
+ nsRange* aSearchRange, const nsAString& aQuery,
+ const nsTArray<RefPtr<Text>>& aTextNodeList, bool aWordStartBounded,
+ bool aWordEndBounded) {
+ // 1. Let searchBuffer be the concatenation of the data of each item in nodes.
+ // XXX(:jjaschke): There's an open issue here that deals with what
+ // data is supposed to be (text data vs. rendered text)
+ // https://github.com/WICG/scroll-to-text-fragment/issues/98
+ uint32_t bufferLength = 0;
+ for (const Text* text : aTextNodeList) {
+ bufferLength += text->Length();
+ }
+ // bail out if the search query is longer than the text data.
+ if (bufferLength < aQuery.Length()) {
+ return nullptr;
+ }
+ nsAutoString searchBuffer;
+ searchBuffer.SetCapacity(bufferLength);
+ for (Text* text : aTextNodeList) {
+ text->AppendTextTo(searchBuffer);
+ }
+ // 2. Let searchStart be 0.
+ // 3. If the first item in nodes is searchRange’s start node then set
+ // searchStart to searchRange’s start offset.
+ uint32_t searchStart =
+ aTextNodeList.SafeElementAt(0) == aSearchRange->GetStartContainer()
+ ? aSearchRange->StartOffset()
+ : 0;
+
+ // 4. Let start and end be boundary points, initially null.
+ RangeBoundary start, end;
+ // 5. Let matchIndex be null.
+ // "null" here doesn't mean 0, instead "not set". 0 would be a valid index.
+ // Therefore, "null" is represented by the value -1.
+ int32_t matchIndex = -1;
+
+ // 6. While matchIndex is null
+ // As explained above, "null" == -1 in this algorithm.
+ while (matchIndex == -1) {
+ // 6.1. Set matchIndex to the index of the first instance of queryString in
+ // searchBuffer, starting at searchStart. The string search must be
+ // performed using a base character comparison, or the primary level, as
+ // defined in [UTS10].
+ // [UTS10]
+ // Ken Whistler; Markus Scherer.Unicode Collation Algorithm.26 August 2022.
+ // Unicode Technical Standard #10.
+ // URL : https://www.unicode.org/reports/tr10/tr10-47.html
+
+ // XXX(:jjaschke): For the initial implementation, a standard case-sensitive
+ // find-in-string is used.
+ // See: https://github.com/WICG/scroll-to-text-fragment/issues/233
+ matchIndex = searchBuffer.Find(aQuery, searchStart);
+ // 6.2. If matchIndex is null, return null.
+ if (matchIndex == -1) {
+ return nullptr;
+ }
+
+ // 6.3. Let endIx be matchIndex + queryString’s length.
+ // endIx is the index of the last character in the match + 1.
+ const uint32_t endIx = matchIndex + aQuery.Length();
+
+ // 6.4. Set start to the boundary point result of get boundary point at
+ // index matchIndex run over nodes with isEnd false.
+ start = GetBoundaryPointAtIndex(matchIndex, aTextNodeList, IsEndIndex::No);
+ // 6.5. Set end to the boundary point result of get boundary point at index
+ // endIx run over nodes with isEnd true.
+ end = GetBoundaryPointAtIndex(endIx, aTextNodeList, IsEndIndex::Yes);
+
+ // 6.6. If wordStartBounded is true and matchIndex is not at a word boundary
+ // in searchBuffer, given the language from start’s node as the locale; or
+ // wordEndBounded is true and matchIndex + queryString’s length is not at a
+ // word boundary in searchBuffer, given the language from end’s node as the
+ // locale:
+ if ((aWordStartBounded && !IsAtWordBoundary(searchBuffer, matchIndex)) ||
+ (aWordEndBounded && !IsAtWordBoundary(searchBuffer, endIx))) {
+ // 6.6.1. Set searchStart to matchIndex + 1.
+ searchStart = matchIndex + 1;
+ // 6.6.2. Set matchIndex to null.
+ matchIndex = -1;
+ }
+ }
+ // 7. Let endInset be 0.
+ // 8. If the last item in nodes is searchRange’s end node then set endInset
+ // to (searchRange’s end node's length − searchRange’s end offset)
+ // (endInset is the offset from the last position in the last node in the
+ // reverse direction. Alternatively, it is the length of the node that’s not
+ // included in the range.)
+ uint32_t endInset =
+ aTextNodeList.LastElement() == aSearchRange->GetEndContainer()
+ ? aSearchRange->GetEndContainer()->Length() -
+ aSearchRange->EndOffset()
+ : 0;
+
+ // 9. If matchIndex + queryString’s length is greater than searchBuffer’s
+ // length − endInset return null.
+ // (If the match runs past the end of the search range, return null.)
+ if (matchIndex + aQuery.Length() > searchBuffer.Length() - endInset) {
+ return nullptr;
+ }
+
+ // 10. Assert: start and end are non-null, valid boundary points in
+ // searchRange.
+ MOZ_ASSERT(start.IsSetAndValid());
+ MOZ_ASSERT(end.IsSetAndValid());
+
+ // 11. Return a range with start start and end end.
+ ErrorResult rv;
+ RefPtr<nsRange> range = nsRange::Create(start, end, rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+
+ return range;
+}
+
+RefPtr<nsRange> FragmentDirective::FindStringInRange(nsRange* aSearchRange,
+ const nsAString& aQuery,
+ bool aWordStartBounded,
+ bool aWordEndBounded) {
+ MOZ_ASSERT(aSearchRange);
+ RefPtr<nsRange> searchRange = aSearchRange->CloneRange();
+ // 1. While searchRange is not collapsed
+ while (searchRange && !searchRange->Collapsed()) {
+ // 1.1. Let curNode be searchRange’s start node.
+ RefPtr<nsINode> curNode = searchRange->GetStartContainer();
+
+ // 1.2. If curNode is part of a non-searchable subtree:
+ if (NodeIsPartOfNonSearchableSubTree(*curNode)) {
+ // 1.2.1. Set searchRange’s start node to the next node, in
+ // shadow-including tree order, that isn’t a shadow-including descendant
+ // of curNode.
+ RefPtr<nsINode> next = curNode;
+ while ((next = next->GetNextNode())) {
+ if (!next->IsShadowIncludingInclusiveDescendantOf(curNode)) {
+ break;
+ }
+ }
+ if (!next) {
+ return nullptr;
+ }
+ // 1.2.2. Set `searchRange`s `start offset` to 0
+ searchRange->SetStart(next, 0);
+ // 1.2.3. continue.
+ continue;
+ }
+ // 1.3. If curNode is not a visible TextNode:
+ if (!NodeIsVisibleTextNode(*curNode)) {
+ // 1.3.1. Set searchRange’s start node to the next node, in
+ // shadow-including tree order, that is not a doctype.
+ RefPtr<nsINode> next = curNode;
+ while ((next = next->GetNextNode())) {
+ if (next->NodeType() != Node_Binding::DOCUMENT_TYPE_NODE) {
+ break;
+ }
+ }
+ if (!next) {
+ return nullptr;
+ }
+ // 1.3.2. Set searchRange’s start offset to 0.
+ searchRange->SetStart(next, 0);
+ // 1.3.3. continue.
+ continue;
+ }
+ // 1.4. Let blockAncestor be the nearest block ancestor of `curNode`
+ RefPtr<nsINode> blockAncestor = GetBlockAncestorForNode(curNode);
+
+ // 1.5. Let textNodeList be a list of Text nodes, initially empty.
+ nsTArray<RefPtr<Text>> textNodeList;
+ // 1.6. While curNode is a shadow-including descendant of blockAncestor and
+ // the position of the boundary point (curNode,0) is not after searchRange's
+ // end:
+ while (curNode &&
+ curNode->IsShadowIncludingInclusiveDescendantOf(blockAncestor)) {
+ Maybe<int32_t> comp = nsContentUtils::ComparePoints(
+ curNode, 0, searchRange->GetEndContainer(), searchRange->EndOffset());
+ if (comp) {
+ if (*comp >= 0) {
+ break;
+ }
+ } else {
+ // This means that the compared nodes are disconnected.
+ return nullptr;
+ }
+ // 1.6.1. If curNode has block-level display, then break.
+ if (NodeHasBlockLevelDisplay(*curNode)) {
+ break;
+ }
+ // 1.6.2. If curNode is search invisible:
+ if (NodeIsSearchInvisible(*curNode)) {
+ // 1.6.2.1. Set curNode to the next node, in shadow-including tree
+ // order, that isn't a shadow-including descendant of curNode.
+ curNode = curNode->GetNextNode();
+ // 1.6.2.2. Continue.
+ continue;
+ }
+ // 1.6.3. If curNode is a visible text node then append it to
+ // textNodeList.
+ if (NodeIsVisibleTextNode(*curNode)) {
+ textNodeList.AppendElement(curNode->AsText());
+ }
+ // 1.6.4. Set curNode to the next node in shadow-including
+ // tree order.
+ curNode = curNode->GetNextNode();
+ }
+ // 1.7. Run the find a range from a node list steps given
+ // query, searchRange, textNodeList, wordStartBounded, wordEndBounded as
+ // input. If the resulting Range is not null, then return it.
+ if (RefPtr<nsRange> range =
+ FindRangeFromNodeList(searchRange, aQuery, textNodeList,
+ aWordStartBounded, aWordEndBounded)) {
+ return range;
+ }
+
+ // 1.8. If curNode is null, then break.
+ if (!curNode) {
+ break;
+ }
+
+ // 1.9. Assert: curNode follows searchRange's start node.
+
+ // 1.10. Set searchRange's start to the boundary point (curNode,0).
+ searchRange->SetStart(curNode, 0);
+ }
+
+ // 2. Return null.
+ return nullptr;
+}
+} // namespace mozilla::dom
diff --git a/dom/base/FragmentDirective.h b/dom/base/FragmentDirective.h
new file mode 100644
index 0000000000..8972556d6c
--- /dev/null
+++ b/dom/base/FragmentDirective.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FRAGMENTDIRECTIVE_H_
+#define DOM_FRAGMENTDIRECTIVE_H_
+
+#include "js/TypeDecls.h"
+#include "mozilla/dom/BindingDeclarations.h"
+
+#include "mozilla/dom/fragmentdirectives_ffi_generated.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsStringFwd.h"
+#include "nsWrapperCache.h"
+
+class nsINode;
+class nsIURI;
+class nsRange;
+namespace mozilla::dom {
+class Document;
+class Text;
+
+/**
+ * @brief The `FragmentDirective` class is the C++ representation of the
+ * `Document.fragmentDirective` webidl property.
+ *
+ * This class also serves as the main interface to interact with the fragment
+ * directive from the C++ side. It allows to find text fragment ranges from a
+ * given list of `TextDirective`s using
+ * `FragmentDirective::FindTextFragmentsInDocument()`.
+ * To avoid Text Directives being applied multiple times, this class implements
+ * the `uninvoked directive` mechanism, which in the spec is defined to be part
+ * of the `Document` [0].
+ *
+ * [0]
+ * https://wicg.github.io/scroll-to-text-fragment/#document-uninvoked-directives
+ */
+class FragmentDirective final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FragmentDirective)
+
+ public:
+ explicit FragmentDirective(Document* aDocument);
+ FragmentDirective(Document* aDocument,
+ nsTArray<TextDirective>&& aTextDirectives)
+ : mDocument(aDocument),
+ mUninvokedTextDirectives(std::move(aTextDirectives)) {}
+
+ protected:
+ ~FragmentDirective() = default;
+
+ public:
+ Document* GetParentObject() const { return mDocument; };
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ /**
+ * @brief Sets Text Directives as "uninvoked directive".
+ */
+ void SetTextDirectives(nsTArray<TextDirective>&& aTextDirectives) {
+ mUninvokedTextDirectives = std::move(aTextDirectives);
+ }
+
+ /** Returns true if there are Text Directives that have not been applied to
+ * the `Document`.
+ */
+ bool HasUninvokedDirectives() const {
+ return !mUninvokedTextDirectives.IsEmpty();
+ };
+
+ /** Searches for the current uninvoked text directives and creates a range for
+ * each one that is found.
+ *
+ * When this method returns, the uninvoked directives for this document are
+ * cleared.
+ *
+ * This method tries to follow the specification as close as possible in how
+ * to find a matching range for a text directive. However, instead of using
+ * collator-based search, a standard case-insensitive search is used
+ * (`nsString::find()`).
+ */
+ nsTArray<RefPtr<nsRange>> FindTextFragmentsInDocument();
+
+ /** Utility function which parses the fragment directive and removes it from
+ * the hash of the given URI. This operation happens in-place.
+ *
+ * If aTextDirectives is nullptr, the parsed fragment directive is discarded.
+ */
+ static void ParseAndRemoveFragmentDirectiveFromFragment(
+ nsCOMPtr<nsIURI>& aURI,
+ nsTArray<TextDirective>* aTextDirectives = nullptr);
+
+ private:
+ RefPtr<nsRange> FindRangeForTextDirective(
+ const TextDirective& aTextDirective);
+ RefPtr<nsRange> FindStringInRange(nsRange* aSearchRange,
+ const nsAString& aQuery,
+ bool aWordStartBounded,
+ bool aWordEndBounded);
+
+ RefPtr<Document> mDocument;
+ nsTArray<TextDirective> mUninvokedTextDirectives;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FRAGMENTDIRECTIVE_H_
diff --git a/dom/base/Link.cpp b/dom/base/Link.cpp
index 296a584ed1..1465ea5773 100644
--- a/dom/base/Link.cpp
+++ b/dom/base/Link.cpp
@@ -6,26 +6,21 @@
#include "Link.h"
-#include "mozilla/MemoryReporting.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
#include "mozilla/dom/SVGAElement.h"
#include "mozilla/dom/HTMLDNSPrefetch.h"
#include "mozilla/IHistory.h"
-#include "mozilla/StaticPrefs_layout.h"
#include "nsLayoutUtils.h"
-#include "nsIURL.h"
#include "nsIURIMutator.h"
#include "nsISizeOf.h"
-#include "nsEscape.h"
#include "nsGkAtoms.h"
#include "nsString.h"
-#include "mozAutoDocUpdate.h"
#include "mozilla/Components.h"
#include "nsAttrValueInlines.h"
-#include "HTMLLinkElement.h"
namespace mozilla::dom {
@@ -133,7 +128,7 @@ nsIURI* Link::GetURI() const {
return mCachedURI;
}
-void Link::SetProtocol(const nsAString& aProtocol) {
+void Link::SetProtocol(const nsACString& aProtocol) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
@@ -146,111 +141,100 @@ void Link::SetProtocol(const nsAString& aProtocol) {
SetHrefAttribute(uri);
}
-void Link::SetPassword(const nsAString& aPassword) {
+void Link::SetPassword(const nsACString& aPassword) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv = NS_MutateURI(uri)
- .SetPassword(NS_ConvertUTF16toUTF8(aPassword))
- .Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetPassword(aPassword).Finalize(uri);
if (NS_SUCCEEDED(rv)) {
SetHrefAttribute(uri);
}
}
-void Link::SetUsername(const nsAString& aUsername) {
+void Link::SetUsername(const nsACString& aUsername) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv = NS_MutateURI(uri)
- .SetUsername(NS_ConvertUTF16toUTF8(aUsername))
- .Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetUsername(aUsername).Finalize(uri);
if (NS_SUCCEEDED(rv)) {
SetHrefAttribute(uri);
}
}
-void Link::SetHost(const nsAString& aHost) {
+void Link::SetHost(const nsACString& aHost) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv =
- NS_MutateURI(uri).SetHostPort(NS_ConvertUTF16toUTF8(aHost)).Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetHostPort(aHost).Finalize(uri);
if (NS_FAILED(rv)) {
return;
}
SetHrefAttribute(uri);
}
-void Link::SetHostname(const nsAString& aHostname) {
+void Link::SetHostname(const nsACString& aHostname) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv =
- NS_MutateURI(uri).SetHost(NS_ConvertUTF16toUTF8(aHostname)).Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetHost(aHostname).Finalize(uri);
if (NS_FAILED(rv)) {
return;
}
SetHrefAttribute(uri);
}
-void Link::SetPathname(const nsAString& aPathname) {
+void Link::SetPathname(const nsACString& aPathname) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv = NS_MutateURI(uri)
- .SetFilePath(NS_ConvertUTF16toUTF8(aPathname))
- .Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetFilePath(aPathname).Finalize(uri);
if (NS_FAILED(rv)) {
return;
}
SetHrefAttribute(uri);
}
-void Link::SetSearch(const nsAString& aSearch) {
+void Link::SetSearch(const nsACString& aSearch) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv =
- NS_MutateURI(uri).SetQuery(NS_ConvertUTF16toUTF8(aSearch)).Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetQuery(aSearch).Finalize(uri);
if (NS_FAILED(rv)) {
return;
}
SetHrefAttribute(uri);
}
-void Link::SetPort(const nsAString& aPort) {
+void Link::SetPort(const nsACString& aPort) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv;
- nsAutoString portStr(aPort);
-
// nsIURI uses -1 as default value.
+ nsresult rv;
int32_t port = -1;
if (!aPort.IsEmpty()) {
- port = portStr.ToInteger(&rv);
+ port = aPort.ToInteger(&rv);
if (NS_FAILED(rv)) {
return;
}
@@ -263,15 +247,14 @@ void Link::SetPort(const nsAString& aPort) {
SetHrefAttribute(uri);
}
-void Link::SetHash(const nsAString& aHash) {
+void Link::SetHash(const nsACString& aHash) {
nsCOMPtr<nsIURI> uri(GetURI());
if (!uri) {
// Ignore failures to be compatible with NS4.
return;
}
- nsresult rv =
- NS_MutateURI(uri).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(uri);
+ nsresult rv = NS_MutateURI(uri).SetRef(aHash).Finalize(uri);
if (NS_FAILED(rv)) {
return;
}
@@ -279,121 +262,102 @@ void Link::SetHash(const nsAString& aHash) {
SetHrefAttribute(uri);
}
-void Link::GetOrigin(nsAString& aOrigin) {
+void Link::GetOrigin(nsACString& aOrigin) {
aOrigin.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
return;
}
- nsString origin;
- nsContentUtils::GetWebExposedOriginSerialization(uri, origin);
- aOrigin.Assign(origin);
+ nsContentUtils::GetWebExposedOriginSerialization(uri, aOrigin);
}
-void Link::GetProtocol(nsAString& _protocol) {
- nsCOMPtr<nsIURI> uri(GetURI());
- if (uri) {
- nsAutoCString scheme;
- (void)uri->GetScheme(scheme);
- CopyASCIItoUTF16(scheme, _protocol);
+void Link::GetProtocol(nsACString& aProtocol) {
+ if (nsIURI* uri = GetURI()) {
+ (void)uri->GetScheme(aProtocol);
}
- _protocol.Append(char16_t(':'));
+ aProtocol.Append(':');
}
-void Link::GetUsername(nsAString& aUsername) {
+void Link::GetUsername(nsACString& aUsername) {
aUsername.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
return;
}
- nsAutoCString username;
- uri->GetUsername(username);
- CopyASCIItoUTF16(username, aUsername);
+ uri->GetUsername(aUsername);
}
-void Link::GetPassword(nsAString& aPassword) {
+void Link::GetPassword(nsACString& aPassword) {
aPassword.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
return;
}
- nsAutoCString password;
- uri->GetPassword(password);
- CopyASCIItoUTF16(password, aPassword);
+ uri->GetPassword(aPassword);
}
-void Link::GetHost(nsAString& _host) {
- _host.Truncate();
+void Link::GetHost(nsACString& aHost) {
+ aHost.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI should result in an empty string.
return;
}
- nsAutoCString hostport;
- nsresult rv = uri->GetHostPort(hostport);
- if (NS_SUCCEEDED(rv)) {
- CopyUTF8toUTF16(hostport, _host);
- }
+ uri->GetHostPort(aHost);
}
-void Link::GetHostname(nsAString& _hostname) {
- _hostname.Truncate();
+void Link::GetHostname(nsACString& aHostname) {
+ aHostname.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI should result in an empty string.
return;
}
- nsContentUtils::GetHostOrIPv6WithBrackets(uri, _hostname);
+ nsContentUtils::GetHostOrIPv6WithBrackets(uri, aHostname);
}
-void Link::GetPathname(nsAString& _pathname) {
- _pathname.Truncate();
+void Link::GetPathname(nsACString& aPathname) {
+ aPathname.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI should result in an empty string.
return;
}
- nsAutoCString file;
- nsresult rv = uri->GetFilePath(file);
- if (NS_SUCCEEDED(rv)) {
- CopyUTF8toUTF16(file, _pathname);
- }
+ uri->GetFilePath(aPathname);
}
-void Link::GetSearch(nsAString& _search) {
- _search.Truncate();
+void Link::GetSearch(nsACString& aSearch) {
+ aSearch.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI or URL should result in an empty
// string.
return;
}
- nsAutoCString search;
- nsresult rv = uri->GetQuery(search);
- if (NS_SUCCEEDED(rv) && !search.IsEmpty()) {
- _search.Assign(u'?');
- AppendUTF8toUTF16(search, _search);
+ nsresult rv = uri->GetQuery(aSearch);
+ if (NS_SUCCEEDED(rv) && !aSearch.IsEmpty()) {
+ aSearch.Insert('?', 0);
}
}
-void Link::GetPort(nsAString& _port) {
- _port.Truncate();
+void Link::GetPort(nsACString& aPort) {
+ aPort.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI should result in an empty string.
return;
@@ -404,27 +368,23 @@ void Link::GetPort(nsAString& _port) {
// Note that failure to get the port from the URI is not necessarily a bad
// thing. Some URIs do not have a port.
if (NS_SUCCEEDED(rv) && port != -1) {
- nsAutoString portStr;
- portStr.AppendInt(port, 10);
- _port.Assign(portStr);
+ aPort.AppendInt(port, 10);
}
}
-void Link::GetHash(nsAString& _hash) {
- _hash.Truncate();
+void Link::GetHash(nsACString& aHash) {
+ aHash.Truncate();
- nsCOMPtr<nsIURI> uri(GetURI());
+ nsIURI* uri = GetURI();
if (!uri) {
// Do not throw! Not having a valid URI should result in an empty
// string.
return;
}
- nsAutoCString ref;
- nsresult rv = uri->GetRef(ref);
- if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) {
- _hash.Assign(char16_t('#'));
- AppendUTF8toUTF16(ref, _hash);
+ nsresult rv = uri->GetRef(aHash);
+ if (NS_SUCCEEDED(rv) && !aHash.IsEmpty()) {
+ aHash.Insert('#', 0);
}
}
diff --git a/dom/base/Link.h b/dom/base/Link.h
index b604457126..cee1e1c5ca 100644
--- a/dom/base/Link.h
+++ b/dom/base/Link.h
@@ -64,25 +64,25 @@ class Link : public nsISupports {
/**
* Helper methods for modifying and obtaining parts of the URI of the Link.
*/
- void SetProtocol(const nsAString& aProtocol);
- void SetUsername(const nsAString& aUsername);
- void SetPassword(const nsAString& aPassword);
- void SetHost(const nsAString& aHost);
- void SetHostname(const nsAString& aHostname);
- void SetPathname(const nsAString& aPathname);
- void SetSearch(const nsAString& aSearch);
- void SetPort(const nsAString& aPort);
- void SetHash(const nsAString& aHash);
- void GetOrigin(nsAString& aOrigin);
- void GetProtocol(nsAString& _protocol);
- void GetUsername(nsAString& aUsername);
- void GetPassword(nsAString& aPassword);
- void GetHost(nsAString& _host);
- void GetHostname(nsAString& _hostname);
- void GetPathname(nsAString& _pathname);
- void GetSearch(nsAString& _search);
- void GetPort(nsAString& _port);
- void GetHash(nsAString& _hash);
+ void SetProtocol(const nsACString& aProtocol);
+ void SetUsername(const nsACString& aUsername);
+ void SetPassword(const nsACString& aPassword);
+ void SetHost(const nsACString& aHost);
+ void SetHostname(const nsACString& aHostname);
+ void SetPathname(const nsACString& aPathname);
+ void SetSearch(const nsACString& aSearch);
+ void SetPort(const nsACString& aPort);
+ void SetHash(const nsACString& aHash);
+ void GetOrigin(nsACString& aOrigin);
+ void GetProtocol(nsACString& aProtocol);
+ void GetUsername(nsACString& aUsername);
+ void GetPassword(nsACString& aPassword);
+ void GetHost(nsACString& aHost);
+ void GetHostname(nsACString& aHostname);
+ void GetPathname(nsACString& aPathname);
+ void GetSearch(nsACString& aSearch);
+ void GetPort(nsACString& aPort);
+ void GetHash(nsACString& aHash);
/**
* Invalidates any link caching, and resets the state to the default.
diff --git a/dom/base/Location.cpp b/dom/base/Location.cpp
index cdf25abf56..9edb9e0b6f 100644
--- a/dom/base/Location.cpp
+++ b/dom/base/Location.cpp
@@ -34,6 +34,7 @@
#include "mozilla/Unused.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/FragmentDirective.h"
#include "mozilla/dom/LocationBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "ReferrerInfo.h"
@@ -105,6 +106,9 @@ nsresult Location::GetURI(nsIURI** aURI, bool aGetInnermostURI) {
}
NS_ASSERTION(uri, "nsJARURI screwed up?");
+
+ // Remove the fragment directive from the url hash.
+ FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(uri);
nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri);
exposableURI.forget(aURI);
return NS_OK;
@@ -549,26 +553,6 @@ void Location::Reload(bool aForceget, nsIPrincipal& aSubjectPrincipal,
return aRv.Throw(NS_ERROR_FAILURE);
}
- if (StaticPrefs::dom_block_reload_from_resize_event_handler()) {
- nsCOMPtr<nsPIDOMWindowOuter> window = docShell->GetWindow();
- if (window && window->IsHandlingResizeEvent()) {
- // location.reload() was called on a window that is handling a
- // resize event. Sites do this since Netscape 4.x needed it, but
- // we don't, and it's a horrible experience for nothing. In stead
- // of reloading the page, just clear style data and reflow the
- // page since some sites may use this trick to work around gecko
- // reflow bugs, and this should have the same effect.
- RefPtr<Document> doc = window->GetExtantDoc();
-
- nsPresContext* pcx;
- if (doc && (pcx = doc->GetPresContext())) {
- pcx->RebuildAllStyleData(NS_STYLE_HINT_REFLOW,
- RestyleHint::RestyleSubtree());
- }
- return;
- }
- }
-
RefPtr<BrowsingContext> bc = GetBrowsingContext();
if (!bc || bc->IsDiscarded()) {
return;
diff --git a/dom/base/PlacesEventCounts.cpp b/dom/base/PlacesEventCounts.cpp
new file mode 100644
index 0000000000..be02e4c39b
--- /dev/null
+++ b/dom/base/PlacesEventCounts.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PlacesEventCounts.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/PlacesEventCounts.h"
+#include "mozilla/dom/PlacesEventBinding.h"
+#include "mozilla/dom/PlacesObserversBinding.h"
+
+namespace mozilla::dom {
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(PlacesEventCounts)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PlacesEventCounts)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PlacesEventCounts)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlacesEventCounts)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+PlacesEventCounts::PlacesEventCounts() {
+ ErrorResult rv;
+ for (auto eventType : MakeWebIDLEnumeratedRange<PlacesEventType>()) {
+ PlacesEventCounts_Binding::MaplikeHelpers::Set(
+ this, NS_ConvertUTF8toUTF16(GetEnumString(eventType)), 0, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return;
+ }
+ }
+}
+
+nsresult PlacesEventCounts::Increment(PlacesEventType aEventType) {
+ ErrorResult rv;
+ nsAutoCString eventName(GetEnumString(aEventType));
+ uint64_t count = PlacesEventCounts_Binding::MaplikeHelpers::Get(
+ this, NS_ConvertUTF8toUTF16(eventName), rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+ PlacesEventCounts_Binding::MaplikeHelpers::Set(
+ this, NS_ConvertUTF8toUTF16(eventName), ++count, rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+ return NS_OK;
+}
+
+JSObject* PlacesEventCounts::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PlacesEventCounts_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/base/PlacesEventCounts.h b/dom/base/PlacesEventCounts.h
new file mode 100644
index 0000000000..69abeac7b3
--- /dev/null
+++ b/dom/base/PlacesEventCounts.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_PLACESEVENTCOUNTS_H_
+#define DOM_PLACESEVENTCOUNTS_H_
+
+#include "js/TypeDecls.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/PlacesEventBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+class PlacesEventCounts final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PlacesEventCounts)
+
+ public:
+ PlacesEventCounts();
+
+ nsresult Increment(PlacesEventType aEventType);
+
+ nsISupports* GetParentObject() const { return nullptr; };
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ ~PlacesEventCounts() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_PLACESEVENTCOUNTS_H_
diff --git a/dom/base/PlacesObservers.cpp b/dom/base/PlacesObservers.cpp
index bb92e4a072..b5211733ea 100644
--- a/dom/base/PlacesObservers.cpp
+++ b/dom/base/PlacesObservers.cpp
@@ -22,7 +22,7 @@ struct Flagged {
Flagged(const Flagged& aOther) = default;
~Flagged() = default;
- uint32_t flags;
+ uint32_t flags = 0;
T value;
};
@@ -123,6 +123,14 @@ MOZ_CAN_RUN_SCRIPT void CallListeners(
}
}
+StaticRefPtr<PlacesEventCounts> PlacesObservers::sCounts;
+static void EnsureCountsInitialized() {
+ if (!PlacesObservers::sCounts) {
+ PlacesObservers::sCounts = new PlacesEventCounts();
+ ClearOnShutdown(&PlacesObservers::sCounts);
+ }
+}
+
void PlacesObservers::AddListener(GlobalObject& aGlobal,
const nsTArray<PlacesEventType>& aEventTypes,
PlacesEventCallback& aCallback,
@@ -304,7 +312,11 @@ void PlacesObservers::NotifyListeners(
if (aEvents.Length() == 0) {
return;
}
-
+ EnsureCountsInitialized();
+ for (const auto& event : aEvents) {
+ DebugOnly<nsresult> rv = sCounts->Increment(event->Type());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
#ifdef DEBUG
if (!gNotificationQueue.IsEmpty()) {
NS_WARNING(
@@ -389,4 +401,10 @@ void PlacesObservers::NotifyNext() {
}
}
+already_AddRefed<PlacesEventCounts> PlacesObservers::Counts(
+ const GlobalObject& global) {
+ EnsureCountsInitialized();
+ return do_AddRef(sCounts);
+};
+
} // namespace mozilla::dom
diff --git a/dom/base/PlacesObservers.h b/dom/base/PlacesObservers.h
index e7aca305d8..434380e11f 100644
--- a/dom/base/PlacesObservers.h
+++ b/dom/base/PlacesObservers.h
@@ -11,6 +11,7 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/PlacesObserversBinding.h"
#include "mozilla/dom/PlacesEvent.h"
+#include "mozilla/dom/PlacesEventCounts.h"
#include "mozilla/places/INativePlacesEventCallback.h"
#include "nsIWeakReferenceUtils.h"
@@ -49,6 +50,9 @@ class PlacesObservers {
static void NotifyListeners(
const Sequence<OwningNonNull<PlacesEvent>>& aEvents);
+ static StaticRefPtr<PlacesEventCounts> sCounts;
+ static already_AddRefed<PlacesEventCounts> Counts(const GlobalObject& global);
+
private:
static void RemoveListener(uint32_t aFlags, PlacesEventCallback& aCallback);
static void RemoveListener(uint32_t aFlags,
diff --git a/dom/base/RangeBoundary.h b/dom/base/RangeBoundary.h
index 2e4ab42397..4c5a70fb6a 100644
--- a/dom/base/RangeBoundary.h
+++ b/dom/base/RangeBoundary.h
@@ -9,6 +9,7 @@
#include "nsCOMPtr.h"
#include "nsIContent.h"
+#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/Assertions.h"
#include "mozilla/Maybe.h"
@@ -351,6 +352,34 @@ class RangeBoundaryBase {
}
public:
+ void NotifyParentBecomesShadowHost() {
+ MOZ_ASSERT(mParent);
+ MOZ_ASSERT(mParent->IsContainerNode(),
+ "Range is positioned on a text node!");
+ if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ return;
+ }
+
+ if (!mIsMutationObserved) {
+ // RangeBoundaries that are not used in the context of a
+ // `MutationObserver` use the offset as main source of truth to compute
+ // `mRef`. Therefore, it must not be updated or invalidated.
+ return;
+ }
+
+ if (!mRef) {
+ MOZ_ASSERT(mOffset.isSome() && mOffset.value() == 0,
+ "Invalidating offset of invalid RangeBoundary?");
+ return;
+ }
+
+ if (dom::ShadowRoot* shadowRoot = mParent->GetShadowRootForSelection()) {
+ mParent = shadowRoot;
+ }
+
+ mOffset = Some(0);
+ }
+
bool IsSet() const { return mParent && (mRef || mOffset.isSome()); }
bool IsSetAndValid() const {
diff --git a/dom/base/RangeUtils.cpp b/dom/base/RangeUtils.cpp
index 28283054b8..2383cc18aa 100644
--- a/dom/base/RangeUtils.cpp
+++ b/dom/base/RangeUtils.cpp
@@ -147,9 +147,10 @@ nsresult RangeUtils::CompareNodeToRange(nsINode* aNode,
NS_WARN_IF(!aAbstractRange->IsPositioned())) {
return NS_ERROR_INVALID_ARG;
}
- return CompareNodeToRangeBoundaries(aNode, aAbstractRange->StartRef(),
- aAbstractRange->EndRef(),
- aNodeIsBeforeRange, aNodeIsAfterRange);
+ return CompareNodeToRangeBoundaries(
+ aNode, aAbstractRange->MayCrossShadowBoundaryStartRef(),
+ aAbstractRange->MayCrossShadowBoundaryEndRef(), aNodeIsBeforeRange,
+ aNodeIsAfterRange);
}
template <typename SPT, typename SRT, typename EPT, typename ERT>
nsresult RangeUtils::CompareNodeToRangeBoundaries(
diff --git a/dom/base/ScriptableContentIterator.cpp b/dom/base/ScriptableContentIterator.cpp
index d10c4e68f8..ab04053c29 100644
--- a/dom/base/ScriptableContentIterator.cpp
+++ b/dom/base/ScriptableContentIterator.cpp
@@ -109,6 +109,22 @@ ScriptableContentIterator::InitWithRange(IteratorType aType, nsRange* aRange) {
}
NS_IMETHODIMP
+ScriptableContentIterator::InitWithRangeAllowCrossShadowBoundary(
+ IteratorType aType, nsRange* aRange) {
+ if (aType == NOT_INITIALIZED ||
+ (mIteratorType != NOT_INITIALIZED && aType != mIteratorType) ||
+ aType != SUBTREE_ITERATOR) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mIteratorType = aType;
+ MOZ_ASSERT(mIteratorType == SUBTREE_ITERATOR);
+ EnsureContentIterator();
+ return static_cast<ContentSubtreeIterator*>(mContentIterator.get())
+ ->InitWithAllowCrossShadowBoundary(aRange);
+}
+
+NS_IMETHODIMP
ScriptableContentIterator::InitWithPositions(IteratorType aType,
nsINode* aStartContainer,
uint32_t aStartOffset,
diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp
index 69986e6b78..7983ef98f9 100644
--- a/dom/base/Selection.cpp
+++ b/dom/base/Selection.cpp
@@ -22,9 +22,11 @@
#include "mozilla/CaretAssociationHint.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/SelectionBinding.h"
#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/StaticRange.h"
+#include "mozilla/dom/ShadowIncludingTreeIterator.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IntegerRange.h"
@@ -778,30 +780,39 @@ NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(Selection)
NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Selection, Disconnect())
-const RangeBoundary& Selection::AnchorRef() const {
+const RangeBoundary& Selection::AnchorRef(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmpty;
return sEmpty;
}
if (GetDirection() == eDirNext) {
- return mAnchorFocusRange->StartRef();
+ return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef()
+ : mAnchorFocusRange->StartRef();
}
- return mAnchorFocusRange->EndRef();
+ return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef()
+ : mAnchorFocusRange->EndRef();
}
-const RangeBoundary& Selection::FocusRef() const {
+const RangeBoundary& Selection::FocusRef(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmpty;
return sEmpty;
}
if (GetDirection() == eDirNext) {
- return mAnchorFocusRange->EndRef();
+ return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef()
+ : mAnchorFocusRange->EndRef();
}
-
- return mAnchorFocusRange->StartRef();
+ return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef()
+ : mAnchorFocusRange->StartRef();
}
void Selection::SetAnchorFocusRange(size_t aIndex) {
@@ -818,8 +829,8 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode,
uint32_t aCompareOffset,
const AbstractRange& aRange,
nsContentUtils::NodeIndexCache* aCache) {
- MOZ_ASSERT(aRange.GetStartContainer());
- nsINode* start = aRange.GetStartContainer();
+ MOZ_ASSERT(aRange.GetMayCrossShadowBoundaryStartContainer());
+ nsINode* start = aRange.GetMayCrossShadowBoundaryStartContainer();
// If the nodes that we're comparing are not in the same document, assume that
// aCompareNode will fall at the end of the ranges.
if (aCompareNode.GetComposedDoc() != start->GetComposedDoc() ||
@@ -830,8 +841,9 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode,
}
// The points are in the same subtree, hence there has to be an order.
- return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, start,
- aRange.StartOffset(), aCache);
+ return *nsContentUtils::ComparePoints(
+ &aCompareNode, aCompareOffset, start,
+ aRange.MayCrossShadowBoundaryStartOffset(), aCache);
}
static int32_t CompareToRangeStart(const nsINode& aCompareNode,
@@ -844,7 +856,7 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode,
uint32_t aCompareOffset,
const AbstractRange& aRange) {
MOZ_ASSERT(aRange.IsPositioned());
- nsINode* end = aRange.GetEndContainer();
+ nsINode* end = aRange.GetMayCrossShadowBoundaryEndContainer();
// If the nodes that we're comparing are not in the same document or in the
// same subtree, assume that aCompareNode will fall at the end of the ranges.
if (aCompareNode.GetComposedDoc() != end->GetComposedDoc() ||
@@ -855,8 +867,9 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode,
}
// The points are in the same subtree, hence there has to be an order.
- return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, end,
- aRange.EndOffset());
+ return *nsContentUtils::ComparePoints(
+ &aCompareNode, aCompareOffset, end,
+ aRange.MayCrossShadowBoundaryEndOffset());
}
// static
@@ -1323,7 +1336,18 @@ nsresult Selection::RemoveCollapsedRanges() {
nsresult Selection::StyledRanges::RemoveCollapsedRanges() {
uint32_t i = 0;
while (i < mRanges.Length()) {
- if (mRanges[i].mRange->Collapsed()) {
+ const AbstractRange* range = mRanges[i].mRange;
+ // If nsRange::mCrossShadowBoundaryRange exists, it means
+ // there's a cross boundary selection, so obviously
+ // we shouldn't remove this range.
+ const bool collapsed =
+ range->Collapsed() && !range->MayCrossShadowBoundary();
+ // Cross boundary range should always be uncollapsed.
+ MOZ_ASSERT_IF(
+ range->MayCrossShadowBoundary(),
+ !range->AsDynamicRange()->CrossShadowBoundaryRangeCollapsed());
+
+ if (collapsed) {
nsresult rv = RemoveRangeAndUnregisterSelection(*mRanges[i].mRange);
NS_ENSURE_SUCCESS(rv, rv);
} else {
@@ -1616,7 +1640,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval(
// the given interval's start point, but that range isn't collapsed (a
// collapsed range should be included in the returned results).
const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange;
- if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset) &&
+ if (beginRange->MayCrossShadowBoundaryEndRef().Equals(aBeginNode,
+ aBeginOffset) &&
!beginRange->Collapsed()) {
beginsAfterIndex++;
}
@@ -1627,7 +1652,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval(
// included
if (endsBeforeIndex < mRanges.Length()) {
const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange;
- if (endRange->StartRef().Equals(aEndNode, aEndOffset) &&
+ if (endRange->MayCrossShadowBoundaryStartRef().Equals(aEndNode,
+ aEndOffset) &&
endRange->Collapsed()) {
endsBeforeIndex++;
}
@@ -1710,6 +1736,16 @@ nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent(
return NS_OK;
}
+void Selection::SelectFramesOfShadowIncludingDescendantsOfContent(
+ nsIContent* aContent, bool aSelected) const {
+ MOZ_ASSERT(aContent);
+ MOZ_ASSERT(StaticPrefs::dom_shadowdom_selection_across_boundary_enabled());
+ for (nsINode* node : ShadowIncludingTreeIterator(*aContent)) {
+ nsIContent* innercontent = node->IsContent() ? node->AsContent() : nullptr;
+ SelectFramesOf(innercontent, aSelected);
+ }
+}
+
void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) {
// this method is currently only called in a user-initiated context.
// therefore it is safe to assume that we are not in a Highlight selection
@@ -1748,16 +1784,22 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
if (mFrameSelection->IsInTableSelectionMode()) {
const nsIContent* const commonAncestorContent =
- nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor());
+ nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor(
+ StaticPrefs::dom_select_events_textcontrols_selectstart_enabled()
+ ? AllowRangeCrossShadowBoundary::Yes
+ : AllowRangeCrossShadowBoundary::No));
nsIFrame* const frame = commonAncestorContent
? commonAncestorContent->GetPrimaryFrame()
: aPresContext->PresShell()->GetRootFrame();
if (frame) {
if (frame->IsTextFrame()) {
- MOZ_ASSERT(commonAncestorContent == aRange.GetStartContainer());
- MOZ_ASSERT(commonAncestorContent == aRange.GetEndContainer());
+ MOZ_ASSERT(commonAncestorContent ==
+ aRange.GetMayCrossShadowBoundaryStartContainer());
+ MOZ_ASSERT(commonAncestorContent ==
+ aRange.GetMayCrossShadowBoundaryEndContainer());
static_cast<nsTextFrame*>(frame)->SelectionStateChanged(
- aRange.StartOffset(), aRange.EndOffset(), aSelect, mSelectionType);
+ aRange.MayCrossShadowBoundaryStartOffset(),
+ aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType);
} else {
frame->SelectionStateChanged();
}
@@ -1768,8 +1810,8 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
// Loop through the content iterator for each content node; for each text
// node, call SetSelected on it:
- nsIContent* const startContent =
- nsIContent::FromNodeOrNull(aRange.GetStartContainer());
+ nsIContent* const startContent = nsIContent::FromNodeOrNull(
+ aRange.GetMayCrossShadowBoundaryStartContainer());
if (MOZ_UNLIKELY(!startContent)) {
// Don't warn, bug 1055722
// XXX The range can start from a document node and such range can be
@@ -1780,7 +1822,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
MOZ_DIAGNOSTIC_ASSERT(startContent->IsInComposedDoc());
// We must call first one explicitly
- nsINode* const endNode = aRange.GetEndContainer();
+ nsINode* const endNode = aRange.GetMayCrossShadowBoundaryEndContainer();
if (NS_WARN_IF(!endNode)) {
// We null-checked start node above, therefore, end node should also be
// non-null here.
@@ -1792,10 +1834,10 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
// The frame could be an SVG text frame, in which case we don't treat it
// as a text frame.
if (frame->IsTextFrame()) {
- const uint32_t startOffset = aRange.StartOffset();
- const uint32_t endOffset = endNode == startContent
- ? aRange.EndOffset()
- : startContent->Length();
+ const uint32_t startOffset = aRange.MayCrossShadowBoundaryStartOffset();
+ const uint32_t endOffset =
+ endNode == startContent ? aRange.MayCrossShadowBoundaryEndOffset()
+ : startContent->Length();
static_cast<nsTextFrame*>(frame)->SelectionStateChanged(
startOffset, endOffset, aSelect, mSelectionType);
} else {
@@ -1806,7 +1848,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
// If the range is in a node and the node is a leaf node, we don't need to
// walk the subtree.
- if (aRange.Collapsed() ||
+ if ((aRange.Collapsed() && !aRange.MayCrossShadowBoundary()) ||
(startContent == endNode && !startContent->HasChildren())) {
if (!isFirstContentTextNode) {
SelectFramesOf(startContent, aSelect);
@@ -1815,7 +1857,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
}
ContentSubtreeIterator subtreeIter;
- subtreeIter.Init(&aRange);
+ subtreeIter.InitWithAllowCrossShadowBoundary(&aRange);
if (isFirstContentTextNode && !subtreeIter.IsDone() &&
subtreeIter.GetCurrentNode() == startContent) {
subtreeIter.Next(); // first content has already been handled.
@@ -1825,8 +1867,12 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
MOZ_DIAGNOSTIC_ASSERT(subtreeIter.GetCurrentNode());
if (nsIContent* const content =
nsIContent::FromNodeOrNull(subtreeIter.GetCurrentNode())) {
- SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content,
- aSelect);
+ if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ SelectFramesOfShadowIncludingDescendantsOfContent(content, aSelect);
+ } else {
+ SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content,
+ aSelect);
+ }
}
}
@@ -1839,7 +1885,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext,
// The frame could be an SVG text frame, in which case we'll ignore it.
if (frame->IsTextFrame()) {
static_cast<nsTextFrame*>(frame)->SelectionStateChanged(
- 0, aRange.EndOffset(), aSelect, mSelectionType);
+ 0, aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType);
}
}
return NS_OK;
@@ -1901,10 +1947,11 @@ UniquePtr<SelectionDetails> Selection::LookUpSelection(
if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
continue;
}
- nsINode* startNode = range->GetStartContainer();
- nsINode* endNode = range->GetEndContainer();
- uint32_t startOffset = range->StartOffset();
- uint32_t endOffset = range->EndOffset();
+
+ nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer();
+ nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer();
+ uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset();
+ uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset();
Maybe<uint32_t> start, end;
if (startNode == aContent && endNode == aContent) {
@@ -2184,6 +2231,67 @@ void Selection::RemoveAllRanges(ErrorResult& aRv) {
RemoveAllRangesInternal(aRv);
}
+already_AddRefed<StaticRange> Selection::GetComposedRange(
+ const AbstractRange* aRange,
+ const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const {
+ // If aIsEndNode is true, this method does the Step 5.1 and 5.2
+ // in https://www.w3.org/TR/selection-api/#dom-selection-getcomposedranges,
+ // otherwise it does the Step 3.1 and 3.2.
+ auto reScope = [&aShadowRoots](nsINode*& aNode, uint32_t& aOffset,
+ bool aIsEndNode) {
+ MOZ_ASSERT(aNode);
+ while (aNode) {
+ const ShadowRoot* shadowRootOfNode = aNode->GetContainingShadow();
+ if (!shadowRootOfNode) {
+ return;
+ }
+
+ for (const OwningNonNull<ShadowRoot>& shadowRoot : aShadowRoots) {
+ if (shadowRoot->IsShadowIncludingInclusiveDescendantOf(
+ shadowRootOfNode)) {
+ return;
+ }
+ }
+
+ const nsIContent* host = aNode->GetContainingShadowHost();
+ const Maybe<uint32_t> maybeIndex = host->ComputeIndexInParentContent();
+ MOZ_ASSERT(maybeIndex.isSome(), "not parent or anonymous child?");
+ if (MOZ_UNLIKELY(maybeIndex.isNothing())) {
+ // Unlikely to happen, but still set aNode to nullptr to avoid
+ // leaking information about the shadow tree.
+ aNode = nullptr;
+ return;
+ }
+ aOffset = maybeIndex.value();
+ if (aIsEndNode) {
+ aOffset += 1;
+ }
+ aNode = host->GetParentNode();
+ }
+ };
+
+ nsINode* startNode = aRange->GetMayCrossShadowBoundaryStartContainer();
+ uint32_t startOffset = aRange->MayCrossShadowBoundaryStartOffset();
+ nsINode* endNode = aRange->GetMayCrossShadowBoundaryEndContainer();
+ uint32_t endOffset = aRange->MayCrossShadowBoundaryEndOffset();
+
+ reScope(startNode, startOffset, false /* aIsEndNode */);
+ reScope(endNode, endOffset, true /* aIsEndNode */);
+
+ RefPtr<StaticRange> composedRange = StaticRange::Create(
+ startNode, startOffset, endNode, endOffset, IgnoreErrors());
+ return composedRange.forget();
+}
+
+void Selection::GetComposedRanges(
+ const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots,
+ nsTArray<RefPtr<StaticRange>>& aComposedRanges) {
+ aComposedRanges.SetCapacity(mStyledRanges.mRanges.Length());
+ for (const auto& range : mStyledRanges.mRanges) {
+ aComposedRanges.AppendElement(GetComposedRange(range.mRange, aShadowRoots));
+ }
+}
+
void Selection::RemoveAllRangesInternal(ErrorResult& aRv) {
if (!mFrameSelection) {
aRv.Throw(NS_ERROR_NOT_INITIALIZED);
@@ -2501,11 +2609,15 @@ void Selection::CollapseInternal(InLimiter aInLimiter,
// Hack to display the caret on the right line (bug 1237236).
if (frameSelection->GetHint() == CaretAssociationHint::Before &&
aPoint.Container()->IsContent()) {
- int32_t frameOffset;
- nsTextFrame* f = do_QueryFrame(nsCaret::GetFrameAndOffset(
- this, aPoint.Container(),
- *aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets),
- &frameOffset));
+ const nsCaret::CaretPosition pos{
+ aPoint.Container(),
+ int32_t(*aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)),
+ frameSelection->GetHint(), frameSelection->GetCaretBidiLevel()};
+ CaretFrameData frameData = nsCaret::GetFrameAndOffset(pos);
+ if (frameData.mFrame) {
+ frameSelection->SetHint(frameData.mHint);
+ }
+ nsTextFrame* f = do_QueryFrame(frameData.mFrame);
if (f && f->IsAtEndOfLine() && f->HasSignificantTerminalNewline()) {
// RawRangeBounary::Offset() causes computing offset if it's not been
// done yet. However, it's called only when the container is a text
@@ -2672,6 +2784,19 @@ AbstractRange* Selection::GetAbstractRangeAt(uint32_t aIndex) const {
return mStyledRanges.mRanges.SafeElementAt(aIndex, empty).mRange;
}
+void Selection::GetDirection(nsAString& aDirection) const {
+ if (mStyledRanges.mRanges.IsEmpty() ||
+ (mFrameSelection && (mFrameSelection->IsDoubleClickSelection() ||
+ mFrameSelection->IsTripleClickSelection()))) {
+ // Empty range and double/triple clicks result a directionless selection.
+ aDirection.AssignLiteral("none");
+ } else if (mDirection == nsDirection::eDirPrevious) {
+ aDirection.AssignLiteral("backward");
+ } else {
+ aDirection.AssignLiteral("forward");
+ }
+}
+
nsRange* Selection::GetRangeAt(uint32_t aIndex) const {
// This method per IDL spec returns a dynamic range.
// Therefore, it must be ensured that it is only called
@@ -2836,17 +2961,17 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
#ifdef DEBUG_SELECTION
nsDirection oldDirection = GetDirection();
#endif
- nsINode* anchorNode = GetAnchorNode();
- nsINode* focusNode = GetFocusNode();
- const uint32_t anchorOffset = AnchorOffset();
- const uint32_t focusOffset = FocusOffset();
+ nsINode* anchorNode = GetMayCrossShadowBoundaryAnchorNode();
+ nsINode* focusNode = GetMayCrossShadowBoundaryFocusNode();
+ const uint32_t anchorOffset = MayCrossShadowBoundaryAnchorOffset();
+ const uint32_t focusOffset = MayCrossShadowBoundaryFocusOffset();
RefPtr<nsRange> range = mAnchorFocusRange->CloneRange();
- nsINode* startNode = range->GetStartContainer();
- nsINode* endNode = range->GetEndContainer();
- const uint32_t startOffset = range->StartOffset();
- const uint32_t endOffset = range->EndOffset();
+ nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer();
+ nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer();
+ const uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset();
+ const uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset();
bool shouldClearRange = false;
const Maybe<int32_t> anchorOldFocusOrder = nsContentUtils::ComparePoints(
@@ -2882,7 +3007,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
(*anchorOldFocusOrder <= 0 &&
*oldFocusNewFocusOrder < 0)) { // a1,2 a,1,2
// select from 1 to 2 unless they are collapsed
- range->SetEnd(aContainer, aOffset, aRv);
+ range->SetEnd(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -2903,7 +3029,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
*anchorNewFocusOrder > 0) { // 2, a1
// select from 2 to 1a
SetDirection(eDirPrevious);
- range->SetStart(aContainer, aOffset, aRv);
+ range->SetStart(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -2923,7 +3050,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
return;
}
- range->SetEnd(aContainer, aOffset, aRv);
+ range->SetEnd(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -2933,27 +3061,33 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
return;
}
SelectFrames(presContext, *difRange, false); // deselect now
- difRange->SetEnd(range->GetEndContainer(), range->EndOffset());
+ difRange->SetEnd(range->GetMayCrossShadowBoundaryEndContainer(),
+ range->MayCrossShadowBoundaryEndOffset(),
+ AllowRangeCrossShadowBoundary::Yes);
SelectFrames(presContext, *difRange, true); // must reselect last node
// maybe more
} else if (*anchorOldFocusOrder >= 0 &&
*anchorNewFocusOrder <= 0) { // 1,a,2 or 1a,2 or 1,a2 or 1a2
if (GetDirection() == eDirPrevious) {
- res = range->SetStart(endNode, endOffset);
+ res = range->SetStart(endNode, endOffset,
+ AllowRangeCrossShadowBoundary::Yes);
if (NS_FAILED(res)) {
aRv.Throw(res);
return;
}
}
SetDirection(eDirNext);
- range->SetEnd(aContainer, aOffset, aRv);
+ range->SetEnd(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
if (focusNode != anchorNode ||
focusOffset != anchorOffset) { // if collapsed diff dont do anything
- res = difRange->SetStart(focusNode, focusOffset);
- nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset);
+ res = difRange->SetStart(focusNode, focusOffset,
+ AllowRangeCrossShadowBoundary::Yes);
+ nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset,
+ AllowRangeCrossShadowBoundary::Yes);
if (NS_FAILED(tmp)) {
res = tmp;
}
@@ -2987,7 +3121,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
return;
}
SetDirection(eDirPrevious);
- range->SetStart(aContainer, aOffset, aRv);
+ range->SetStart(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -2998,15 +3133,19 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
return;
}
SelectFrames(presContext, *difRange, false);
- difRange->SetStart(range->GetStartContainer(), range->StartOffset());
+ difRange->SetStart(range->GetMayCrossShadowBoundaryStartContainer(),
+ range->MayCrossShadowBoundaryStartOffset(),
+ AllowRangeCrossShadowBoundary::Yes);
SelectFrames(presContext, *difRange, true); // must reselect last node
} else if (*anchorNewFocusOrder >= 0 &&
*anchorOldFocusOrder <= 0) { // 2,a,1 or 2a,1 or 2,a1 or 2a1
if (GetDirection() == eDirNext) {
- range->SetEnd(startNode, startOffset);
+ range->SetEnd(startNode, startOffset,
+ AllowRangeCrossShadowBoundary::Yes);
}
SetDirection(eDirPrevious);
- range->SetStart(aContainer, aOffset, aRv);
+ range->SetStart(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -3036,7 +3175,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset,
} else if (*oldFocusNewFocusOrder >= 0 &&
*anchorOldFocusOrder >= 0) { // 2,1,a or 21,a or 2,1a or 21a
// select from 2 to 1
- range->SetStart(aContainer, aOffset, aRv);
+ range->SetStart(aContainer, aOffset, aRv,
+ AllowRangeCrossShadowBoundary::Yes);
if (aRv.Failed()) {
return;
}
@@ -3604,9 +3744,10 @@ void Selection::NotifySelectionListeners() {
RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
- // This flag will be set to true if a selection by double click is detected.
- // As soon as the selection is modified, it needs to be set to false.
- frameSelection->SetIsDoubleClickSelection(false);
+ // This flag will be set to Double or Triple if a selection by double click or
+ // triple click is detected. As soon as the selection is modified, it needs to
+ // be reset to NotApplicable.
+ frameSelection->SetClickSelectionType(ClickSelectionType::NotApplicable);
if (frameSelection->IsBatching()) {
frameSelection->SetChangesDuringBatchingFlag();
diff --git a/dom/base/Selection.h b/dom/base/Selection.h
index 9f031ab3cf..08563993ac 100644
--- a/dom/base/Selection.h
+++ b/dom/base/Selection.h
@@ -64,6 +64,9 @@ namespace dom {
class Selection final : public nsSupportsWeakReference,
public nsWrapperCache,
public SupportsWeakPtr {
+ using AllowRangeCrossShadowBoundary =
+ mozilla::dom::AllowRangeCrossShadowBoundary;
+
protected:
virtual ~Selection();
@@ -205,6 +208,10 @@ class Selection final : public nsSupportsWeakReference,
nsRange* aRange, Maybe<size_t>* aOutIndex,
DispatchSelectstartEvent aDispatchSelectstartEvent);
+ already_AddRefed<StaticRange> GetComposedRange(
+ const AbstractRange* aRange,
+ const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const;
+
public:
nsresult RemoveCollapsedRanges();
void Clear(nsPresContext* aPresContext);
@@ -246,6 +253,8 @@ class Selection final : public nsSupportsWeakReference,
// anchor and which end is focus.
const nsRange* GetAnchorFocusRange() const { return mAnchorFocusRange; }
+ void GetDirection(nsAString& aDirection) const;
+
nsDirection GetDirection() const { return mDirection; }
void SetDirection(nsDirection aDir) { mDirection = aDir; }
@@ -321,6 +330,30 @@ class Selection final : public nsSupportsWeakReference,
return offset ? *offset : 0;
}
+ nsINode* GetMayCrossShadowBoundaryAnchorNode() const {
+ const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes);
+ return anchor.IsSet() ? anchor.Container() : nullptr;
+ }
+
+ uint32_t MayCrossShadowBoundaryAnchorOffset() const {
+ const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes);
+ const Maybe<uint32_t> offset =
+ anchor.Offset(RangeBoundary::OffsetFilter::kValidOffsets);
+ return offset ? *offset : 0;
+ }
+
+ nsINode* GetMayCrossShadowBoundaryFocusNode() const {
+ const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes);
+ return focus.IsSet() ? focus.Container() : nullptr;
+ }
+
+ uint32_t MayCrossShadowBoundaryFocusOffset() const {
+ const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes);
+ const Maybe<uint32_t> offset =
+ focus.Offset(RangeBoundary::OffsetFilter::kValidOffsets);
+ return offset ? *offset : 0;
+ }
+
nsIContent* GetChildAtAnchorOffset() {
const RangeBoundary& anchor = AnchorRef();
return anchor.IsSet() ? anchor.GetChildAtOffset() : nullptr;
@@ -330,8 +363,12 @@ class Selection final : public nsSupportsWeakReference,
return focus.IsSet() ? focus.GetChildAtOffset() : nullptr;
}
- const RangeBoundary& AnchorRef() const;
- const RangeBoundary& FocusRef() const;
+ const RangeBoundary& AnchorRef(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No) const;
+ const RangeBoundary& FocusRef(
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No) const;
/*
* IsCollapsed -- is the whole selection just one point, or unset?
@@ -385,6 +422,10 @@ class Selection final : public nsSupportsWeakReference,
MOZ_CAN_RUN_SCRIPT void RemoveAllRanges(mozilla::ErrorResult& aRv);
+ void GetComposedRanges(
+ const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots,
+ nsTArray<RefPtr<StaticRange>>& aComposedRanges);
+
/**
* Whether Stringify should flush layout or not.
*/
@@ -810,6 +851,12 @@ class Selection final : public nsSupportsWeakReference,
PostContentIterator& aPostOrderIter, nsIContent* aContent,
bool aSelected) const;
+ /**
+ * https://dom.spec.whatwg.org/#concept-shadow-including-descendant
+ */
+ void SelectFramesOfShadowIncludingDescendantsOfContent(nsIContent* aContent,
+ bool aSelected) const;
+
nsresult SelectFrames(nsPresContext* aPresContext, AbstractRange& aRange,
bool aSelect) const;
diff --git a/dom/base/StaticRange.cpp b/dom/base/StaticRange.cpp
index 0946e8f9bf..73ff04c038 100644
--- a/dom/base/StaticRange.cpp
+++ b/dom/base/StaticRange.cpp
@@ -100,6 +100,13 @@ bool StaticRange::IsValid() const {
return false;
}
+ MOZ_ASSERT(mAreStartAndEndInSameTree ==
+ (RangeUtils::ComputeRootNode(mStart.Container()) ==
+ RangeUtils::ComputeRootNode(mEnd.Container())));
+ if (!mAreStartAndEndInSameTree) {
+ return false;
+ }
+
const Maybe<int32_t> pointOrder = nsContentUtils::ComparePoints(mStart, mEnd);
return pointOrder.isSome() && *pointOrder <= 0;
}
@@ -119,6 +126,9 @@ void StaticRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
if (checkCommonAncestor) {
UpdateCommonAncestorIfNecessary();
}
+
+ mAreStartAndEndInSameTree = RangeUtils::ComputeRootNode(mStart.Container()) ==
+ RangeUtils::ComputeRootNode(mEnd.Container());
}
/* static */
diff --git a/dom/base/StaticRange.h b/dom/base/StaticRange.h
index a6f677130d..af7054f843 100644
--- a/dom/base/StaticRange.h
+++ b/dom/base/StaticRange.h
@@ -8,6 +8,7 @@
#define mozilla_dom_StaticRange_h
#include "mozilla/RangeBoundary.h"
+#include "mozilla/RangeUtils.h"
#include "mozilla/dom/AbstractRange.h"
#include "mozilla/dom/StaticRangeBinding.h"
#include "nsTArray.h"
@@ -70,6 +71,21 @@ class StaticRange final : public AbstractRange {
*/
bool IsValid() const;
+ void NotifyNodeBecomesShadowHost(nsINode* aNode) {
+ if (aNode == mStart.Container()) {
+ mStart.NotifyParentBecomesShadowHost();
+ }
+
+ if (aNode == mEnd.Container()) {
+ mEnd.NotifyParentBecomesShadowHost();
+ }
+ }
+
+ private:
+ // Whether the start and end points are in the same tree.
+ // They could be in different trees, i.e, cross shadow boundaries.
+ bool mAreStartAndEndInSameTree = false;
+
protected:
explicit StaticRange(nsINode* aNode)
: AbstractRange(aNode, /* aIsDynamicRange = */ false) {}
diff --git a/dom/base/ThirdPartyUtil.cpp b/dom/base/ThirdPartyUtil.cpp
index 2c71ab3d94..0ced8aa7d8 100644
--- a/dom/base/ThirdPartyUtil.cpp
+++ b/dom/base/ThirdPartyUtil.cpp
@@ -200,9 +200,9 @@ ThirdPartyUtil::IsThirdPartyWindow(mozIDOMWindowProxy* aWindow, nsIURI* aURI,
bool result;
- // Ignore about:blank URIs here since they have no domain and attempting to
- // compare against them will fail.
- if (aURI && !NS_IsAboutBlank(aURI)) {
+ // Ignore about:blank and about:srcdoc URIs here since they have no domain
+ // and attempting to compare against them will fail.
+ if (aURI && !NS_IsAboutBlank(aURI) && !NS_IsAboutSrcdoc(aURI)) {
nsCOMPtr<nsIPrincipal> prin;
nsresult rv = GetPrincipalFromWindow(aWindow, getter_AddRefs(prin));
NS_ENSURE_SUCCESS(rv, rv);
@@ -320,10 +320,10 @@ ThirdPartyUtil::IsThirdPartyChannel(nsIChannel* aChannel, nsIURI* aURI,
}
}
- // Special consideration must be done for about:blank URIs because those
- // inherit the principal from the parent context. For them, let's consider the
- // principal URI.
- if (NS_IsAboutBlank(channelURI)) {
+ // Special consideration must be done for about:blank and about:srcdoc URIs
+ // because those inherit the principal from the parent context. For them,
+ // let's consider the principal URI.
+ if (NS_IsAboutBlank(channelURI) || NS_IsAboutSrcdoc(channelURI)) {
nsCOMPtr<nsIPrincipal> principalToInherit =
loadInfo->FindPrincipalToInherit(aChannel);
if (!principalToInherit) {
diff --git a/dom/base/UseCounters.conf b/dom/base/UseCounters.conf
index 86d782b476..66507b6d49 100644
--- a/dom/base/UseCounters.conf
+++ b/dom/base/UseCounters.conf
@@ -62,11 +62,6 @@ method DataTransfer.mozGetDataAt
attribute DataTransfer.mozUserCancelled
attribute DataTransfer.mozSourceNode
-// Marquee events
-custom onstart sets a <marquee> onstart event listener
-custom onbounce sets a <marquee> onbounce event listener
-custom onfinish sets a <marquee> onfinish event listener
-
// Element non-standard events
custom onoverflow sets an element onoverflow event listener
custom onunderflow sets an element onunderflow event listener
diff --git a/dom/base/crashtests/1697256.html b/dom/base/crashtests/1697256.html
index 25024083e3..3d6634e952 100644
--- a/dom/base/crashtests/1697256.html
+++ b/dom/base/crashtests/1697256.html
@@ -4,8 +4,9 @@
<script>
window.onload = () => {
window.requestIdleCallback(() => {
- SpecialPowers.wrap(self).printPreview()
+ let pp = SpecialPowers.wrap(self).printPreview()
setTimeout(() => {
+ try { pp.close(); } catch (e) { }
document.documentElement.classList.remove("reftest-wait");
}, 250)
})
diff --git a/dom/base/crashtests/1887930.html b/dom/base/crashtests/1887930.html
new file mode 100644
index 0000000000..04a89de8d2
--- /dev/null
+++ b/dom/base/crashtests/1887930.html
@@ -0,0 +1,7 @@
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ document.getSelection().extend(a)
+})
+</script>
+<dialog id="a"></dialog>
+<input type="datetime-local" autofocus="autofocus">
diff --git a/dom/base/crashtests/1887963_1.html b/dom/base/crashtests/1887963_1.html
new file mode 100644
index 0000000000..fbc30dc587
--- /dev/null
+++ b/dom/base/crashtests/1887963_1.html
@@ -0,0 +1,15 @@
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ let c = a.attachShadow({mode: "open"})
+ let b = document.getElementById("host").shadowRoot.getElementById("b");
+ window.parent.getSelection().setBaseAndExtent(b, 0, c, 0)
+})
+</script>
+<div id="a">A</div>
+<video>
+ <div id="host">
+ <template shadowrootmode="open">
+ <video id="b">
+ </template>
+ </div>
+</video>
diff --git a/dom/base/crashtests/1887963_2.html b/dom/base/crashtests/1887963_2.html
new file mode 100644
index 0000000000..53f512173b
--- /dev/null
+++ b/dom/base/crashtests/1887963_2.html
@@ -0,0 +1,15 @@
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ let c = a.attachShadow({mode: "open"})
+ let b = document.getElementById("not-slotted");
+ window.parent.getSelection().setBaseAndExtent(b, 0, c, 0)
+})
+</script>
+<div id="a">A</div>
+<video>
+ <div id="host">
+ <template shadowrootmode="open">
+ </template>
+ <span id="not-slotted">NotSlotted</span>
+ </div>
+</video>
diff --git a/dom/base/crashtests/1887974.html b/dom/base/crashtests/1887974.html
new file mode 100644
index 0000000000..85ffd2b02a
--- /dev/null
+++ b/dom/base/crashtests/1887974.html
@@ -0,0 +1,18 @@
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ c.add(d, 1)
+ b.addEventListener("DOMNodeRemoved", () => {
+ a.appendChild(f)
+ })
+ let r = document.createRange()
+ r.setEndBefore(d)
+ r.deleteContents()
+})
+</script>
+<audio>
+<canvas id="b">
+</canvas>
+<select id="c">
+<option id="d">A</option>
+<optgroup id="f">
+<input id="a">
diff --git a/dom/base/crashtests/1890888.html b/dom/base/crashtests/1890888.html
new file mode 100644
index 0000000000..006768ae14
--- /dev/null
+++ b/dom/base/crashtests/1890888.html
@@ -0,0 +1,13 @@
+<script>
+window.addEventListener("DOMContentLoaded", () => {
+ o1.prepend(o4)
+ document.getSelection().setBaseAndExtent(o5.attachShadow({mode: "closed"}), 0, o3, 0)
+ o5.replaceWith(o2)
+ setTimeout(window.close, 500)
+})
+</script>
+<iframe id="o1"></iframe>
+<picture id="o2"></picture>
+<area id="o3">
+<meter id="o4">
+<p id="o5">
diff --git a/dom/base/crashtests/crashtests.list b/dom/base/crashtests/crashtests.list
index 864538ddf5..22aaf50e6b 100644
--- a/dom/base/crashtests/crashtests.list
+++ b/dom/base/crashtests/crashtests.list
@@ -271,3 +271,8 @@ load 1835886.html
load 1836824.html
skip-if(Android) load 1838484.html
load 1840191.html
+load 1887930.html
+load 1887963_1.html
+load 1887963_2.html
+asserts(0-1) load 1887974.html
+load 1890888.html
diff --git a/dom/base/fragmentdirectives/Cargo.toml b/dom/base/fragmentdirectives/Cargo.toml
new file mode 100644
index 0000000000..7b3b589668
--- /dev/null
+++ b/dom/base/fragmentdirectives/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "dom_fragmentdirectives"
+version = "0.1.0"
+authors = ["Jan Jaeschke <jjaschke@mozilla.com>"]
+edition = "2021"
+license = "MPL-2.0"
+
+[dependencies]
+nsstring = { path = "../../../xpcom/rust/nsstring/" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+percent-encoding = { version = "2.3.1" }
+[lib]
+path = "lib.rs"
diff --git a/dom/base/fragmentdirectives/cbindgen.toml b/dom/base/fragmentdirectives/cbindgen.toml
new file mode 100644
index 0000000000..ec54ebc02d
--- /dev/null
+++ b/dom/base/fragmentdirectives/cbindgen.toml
@@ -0,0 +1,15 @@
+header = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+include_guard = "fragmentdirectives_ffi_generated_h"
+includes = ["nsStringFwd.h", "nsTArrayForwardDeclare.h"]
+
+[export.rename]
+"ThinVec" = "nsTArray"
diff --git a/dom/base/fragmentdirectives/fragment_directive_impl.rs b/dom/base/fragmentdirectives/fragment_directive_impl.rs
new file mode 100644
index 0000000000..dfbdb37415
--- /dev/null
+++ b/dom/base/fragmentdirectives/fragment_directive_impl.rs
@@ -0,0 +1,342 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use percent_encoding::{percent_decode, percent_encode, NON_ALPHANUMERIC};
+use std::str;
+
+/// The `FragmentDirectiveParameter` represents one of
+/// `[prefix-,]start[,end][,-suffix]` without any surrounding `-` or `,`.
+///
+/// The token is stored as percent-decoded string.
+/// Therefore, interfaces exist to
+/// - create a `FragmentDirectiveParameter` from a percent-encoded string.
+/// This function will determine from occurrence and position of a dash
+/// if the token represents a `prefix`, `suffix` or either `start` or `end`.
+/// - create a percent-encoded string from the value the token holds.
+pub enum TextDirectiveParameter {
+ Prefix(String),
+ StartOrEnd(String),
+ Suffix(String),
+}
+
+impl TextDirectiveParameter {
+ /// Creates a token from a percent-encoded string.
+ /// Based on position of a dash the correct token type is determined.
+ /// Returns `None` in case of an ill-formed token:
+ /// - starts and ends with a dash (i.e. `-token-`)
+ /// - only consists of a dash (i.e. `-`) or is empty
+ /// - conversion from percent-encoded string to utf8 fails.
+ pub fn from_percent_encoded(token: &[u8]) -> Option<Self> {
+ if token.is_empty() {
+ return None;
+ }
+ let starts_with_dash = *token.first().unwrap() == b'-';
+ let ends_with_dash = *token.last().unwrap() == b'-';
+ if starts_with_dash && ends_with_dash {
+ // `-token-` is not valid.
+ return None;
+ }
+ if token.len() == 1 && starts_with_dash {
+ // `-` is not valid.
+ return None;
+ }
+ // Note: Trimming of the raw strings is currently not mentioned in the spec.
+ // However, it looks as it is implicitly expected.
+ if starts_with_dash {
+ if let Ok(decoded_suffix) = percent_decode(&token[1..]).decode_utf8() {
+ return Some(TextDirectiveParameter::Suffix(String::from(
+ decoded_suffix.trim(),
+ )));
+ }
+ return None;
+ }
+ if ends_with_dash {
+ if let Ok(decoded_prefix) = percent_decode(&token[..token.len() - 1]).decode_utf8() {
+ return Some(TextDirectiveParameter::Prefix(String::from(
+ decoded_prefix.trim(),
+ )));
+ }
+ return None;
+ }
+ if let Ok(decoded_text) = percent_decode(&token).decode_utf8() {
+ return Some(TextDirectiveParameter::StartOrEnd(String::from(
+ decoded_text.trim(),
+ )));
+ }
+ None
+ }
+
+ /// Returns the value of the token as percent-decoded `String`.
+ pub fn value(&self) -> &String {
+ match self {
+ TextDirectiveParameter::Prefix(value) => &value,
+ TextDirectiveParameter::StartOrEnd(value) => &value,
+ TextDirectiveParameter::Suffix(value) => &value,
+ }
+ }
+
+ /// Creates a percent-encoded string of the token's value.
+ /// This includes placing a dash appropriately
+ /// to indicate whether this token is prefix, suffix or start/end.
+ ///
+ /// This method always returns a new object.
+ pub fn to_percent_encoded_string(&self) -> String {
+ let encode = |text: &String| percent_encode(text.as_bytes(), NON_ALPHANUMERIC).to_string();
+ match self {
+ Self::Prefix(text) => encode(text) + "-",
+ Self::StartOrEnd(text) => encode(text),
+ Self::Suffix(text) => {
+ let encoded = encode(text);
+ let mut result = String::with_capacity(encoded.len() + 1);
+ result.push_str("-");
+ result.push_str(&encoded);
+ result
+ }
+ }
+ }
+}
+
+/// This struct represents one parsed text directive using Rust types.
+///
+/// A text fragment is encoded into a URL fragment like this:
+/// `text=[prefix-,]start[,end][,-suffix]`
+///
+/// The text directive is considered valid if at least `start` is not None.
+/// (see `Self::is_valid()`).
+#[derive(Default)]
+pub struct TextDirective {
+ prefix: Option<TextDirectiveParameter>,
+ start: Option<TextDirectiveParameter>,
+ end: Option<TextDirectiveParameter>,
+ suffix: Option<TextDirectiveParameter>,
+}
+impl TextDirective {
+ /// Creates an instance from string parts.
+ /// This function is intended to be used when a fragment directive string should be created.
+ /// Returns `None` if `start` is empty.
+ pub fn from_parts(prefix: String, start: String, end: String, suffix: String) -> Option<Self> {
+ if !start.is_empty() {
+ Some(Self {
+ prefix: if !prefix.is_empty() {
+ Some(TextDirectiveParameter::Prefix(prefix.trim().into()))
+ } else {
+ None
+ },
+ start: Some(TextDirectiveParameter::StartOrEnd(start.trim().into())),
+ end: if !end.is_empty() {
+ Some(TextDirectiveParameter::StartOrEnd(end.trim().into()))
+ } else {
+ None
+ },
+ suffix: if !suffix.is_empty() {
+ Some(TextDirectiveParameter::Suffix(suffix.trim().into()))
+ } else {
+ None
+ },
+ })
+ } else {
+ None
+ }
+ }
+
+ /// Creates an instance from a percent-encoded string
+ /// that originates from a fragment directive.
+ ///
+ /// `text_fragment` is supposed to have this format:
+ /// ```
+ /// text=[prefix-,]start[,end][,-suffix]
+ /// ```
+ /// This function returns `None` if `text_fragment`
+ /// does not start with `text=`, it contains 0 or more
+ /// than 4 elements or prefix/suffix/start or end
+ /// occur too many times.
+ /// It also returns `None` if any of the tokens parses to fail.
+ pub fn from_percent_encoded_string(text_directive: &str) -> Option<Self> {
+ // first check if the string starts with `text=`
+ if text_directive.len() < 6 {
+ return None;
+ }
+ if !text_directive.starts_with("text=") {
+ return None;
+ }
+
+ let mut parsed_text_directive = Self::default();
+ let valid = text_directive[5..]
+ .split(",")
+ // Parse the substrings into `TextDirectiveParameter`s. This will determine
+ // for each substring if it is a Prefix, Suffix or Start/End,
+ // or if it is invalid.
+ .map(|token| TextDirectiveParameter::from_percent_encoded(token.as_bytes()))
+ // populate `parsed_text_directive` and check its validity by inserting the parameters
+ // one by one. Given that the parameters are sorted by their position in the source,
+ // the validity of the text directive can be determined while adding the parameters.
+ .map(|token| match token {
+ Some(TextDirectiveParameter::Prefix(..)) => {
+ if !parsed_text_directive.is_empty() {
+ // `prefix-` must be the first result.
+ return false;
+ }
+ parsed_text_directive.prefix = token;
+ return true;
+ }
+ Some(TextDirectiveParameter::StartOrEnd(..)) => {
+ if parsed_text_directive.suffix.is_some() {
+ // start or end must come before `-suffix`.
+ return false;
+ }
+ if parsed_text_directive.start.is_none() {
+ parsed_text_directive.start = token;
+ return true;
+ }
+ if parsed_text_directive.end.is_none() {
+ parsed_text_directive.end = token;
+ return true;
+ }
+ // if `start` and `end` is already filled,
+ // this is invalid as well.
+ return false;
+ }
+ Some(TextDirectiveParameter::Suffix(..)) => {
+ if parsed_text_directive.start.is_some()
+ && parsed_text_directive.suffix.is_none()
+ {
+ // `start` must be present and `-suffix` must not be present.
+ // `end` may be present.
+ parsed_text_directive.suffix = token;
+ return true;
+ }
+ return false;
+ }
+ // empty or invalid token renders the whole text directive invalid.
+ None => false,
+ })
+ .all(|valid| valid);
+ if valid {
+ return Some(parsed_text_directive);
+ }
+ None
+ }
+
+ /// Creates a percent-encoded string for the current `TextDirective`.
+ /// In the unlikely case that the `TextDirective` is invalid (i.e. `start` is None),
+ /// which should have been caught earlier,this method returns an empty string.
+ pub fn to_percent_encoded_string(&self) -> String {
+ if !self.is_valid() {
+ return String::default();
+ }
+ String::from("text=")
+ + &[&self.prefix, &self.start, &self.end, &self.suffix]
+ .iter()
+ .filter_map(|&token| token.as_ref())
+ .map(|token| token.to_percent_encoded_string())
+ .collect::<Vec<_>>()
+ .join(",")
+ }
+
+ pub fn start(&self) -> &Option<TextDirectiveParameter> {
+ &self.start
+ }
+
+ pub fn end(&self) -> &Option<TextDirectiveParameter> {
+ &self.end
+ }
+
+ pub fn prefix(&self) -> &Option<TextDirectiveParameter> {
+ &self.prefix
+ }
+
+ pub fn suffix(&self) -> &Option<TextDirectiveParameter> {
+ &self.suffix
+ }
+
+ fn is_empty(&self) -> bool {
+ self.prefix.is_none() && self.start.is_none() && self.end.is_none() && self.suffix.is_none()
+ }
+
+ /// A `TextDirective` object is valid if it contains the `start` token.
+ /// All other tokens are optional.
+ fn is_valid(&self) -> bool {
+ self.start.is_some()
+ }
+}
+/// Parses a fragment directive into a list of `TextDirective` objects and removes
+/// the fragment directive from the input url.
+///
+/// If the hash does not contain a fragment directive, `url` is not modified
+/// and this function returns `None`.
+/// Otherwise, the fragment directive is removed from `url` and parsed.
+/// If parsing fails, this function returns `None`.
+pub fn parse_fragment_directive_and_remove_it_from_hash(
+ url: &str,
+) -> Option<(&str, &str, Vec<TextDirective>)> {
+ // The Fragment Directive is preceded by a `:~:`,
+ // which is only allowed to appear in the hash once.
+ // However (even if unlikely), it might appear outside of the hash,
+ // so this code only considers it when it is after the #.
+ let maybe_first_hash_pos = url.find("#");
+ // If there is no # in url, it is considered to be only the hash (and not a full url).
+ let first_hash_pos = maybe_first_hash_pos.unwrap_or_default();
+ let mut fragment_directive_iter = url[first_hash_pos..].split(":~:");
+ let url_with_stripped_fragment_directive =
+ &url[..first_hash_pos + fragment_directive_iter.next().unwrap_or_default().len()];
+
+ if let Some(fragment_directive) = fragment_directive_iter.next() {
+ if fragment_directive_iter.next().is_some() {
+ // There are multiple occurrences of `:~:`, which is not allowed.
+ return None;
+ }
+ // - fragments are separated by `&`.
+ // - if a fragment does not start with `text=`, it is not a text fragment and will be ignored.
+ // - if parsing of the text fragment fails (for whatever reason), it will be ignored.
+ let text_directives: Vec<_> = fragment_directive
+ .split("&")
+ .map(|maybe_text_fragment| {
+ TextDirective::from_percent_encoded_string(&maybe_text_fragment)
+ })
+ .filter_map(|maybe_text_directive| maybe_text_directive)
+ .collect();
+ if !text_directives.is_empty() {
+ return Some((
+ url_with_stripped_fragment_directive
+ .strip_suffix("#")
+ .unwrap_or(url_with_stripped_fragment_directive),
+ fragment_directive,
+ text_directives,
+ ));
+ }
+ }
+ None
+}
+
+/// Creates a percent-encoded text fragment string.
+///
+/// The returned string starts with `:~:`, so that it can be appended
+/// to a normal fragment.
+/// Text directives which are not valid (ie., they are missing the `start` parameter),
+/// are skipped.
+///
+/// Returns `None` if `fragment_directives` is empty.
+pub fn create_fragment_directive_string(text_directives: &Vec<TextDirective>) -> Option<String> {
+ if text_directives.is_empty() {
+ return None;
+ }
+ let encoded_fragment_directives: Vec<_> = text_directives
+ .iter()
+ .filter(|&fragment_directive| fragment_directive.is_valid())
+ .map(|fragment_directive| fragment_directive.to_percent_encoded_string())
+ .filter(|text_directive| !text_directive.is_empty())
+ .collect();
+ if encoded_fragment_directives.is_empty() {
+ return None;
+ }
+ Some(String::from(":~:") + &encoded_fragment_directives.join("&"))
+}
+
+/// Creates the percent-encoded text directive string for a single text directive.
+pub fn create_text_directive_string(text_directive: &TextDirective) -> Option<String> {
+ if text_directive.is_valid() {
+ Some(text_directive.to_percent_encoded_string())
+ } else {
+ None
+ }
+}
diff --git a/dom/base/fragmentdirectives/lib.rs b/dom/base/fragmentdirectives/lib.rs
new file mode 100644
index 0000000000..0003849eb7
--- /dev/null
+++ b/dom/base/fragmentdirectives/lib.rs
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use nsstring::{nsCString, nsString};
+use thin_vec::ThinVec;
+pub mod fragment_directive_impl;
+mod test;
+
+/// This struct contains the percent-decoded parts of a text directive.
+/// All parts besides `start` are optional (which is indicated by an empty string).
+///
+/// This struct uses Gecko String types, whereas the parser internally uses Rust types.
+/// Therefore, conversion functions are provided.
+#[repr(C)]
+pub struct TextDirective {
+ prefix: nsString,
+ start: nsString,
+ end: nsString,
+ suffix: nsString,
+}
+
+impl TextDirective {
+ /// Creates a `FragmentDirectiveElement` object from a `FragmentDirectiveElementInternal` object
+ /// (which uses Rust string types).
+ fn from_rust_type(element: &fragment_directive_impl::TextDirective) -> Self {
+ Self {
+ prefix: element
+ .prefix()
+ .as_ref()
+ .map_or_else(nsString::new, |token| nsString::from(token.value())),
+ start: element
+ .start()
+ .as_ref()
+ .map_or_else(nsString::new, |token| nsString::from(token.value())),
+ end: element
+ .end()
+ .as_ref()
+ .map_or_else(nsString::new, |token| nsString::from(token.value())),
+ suffix: element
+ .suffix()
+ .as_ref()
+ .map_or_else(nsString::new, |token| nsString::from(token.value())),
+ }
+ }
+
+ /// Converts the contents of this object into Rust types.
+ /// Returns `None` if the given fragment is not valid.
+ /// The only invalid condition is a fragment that is missing the `start` token.
+ fn to_rust_type(&self) -> Option<fragment_directive_impl::TextDirective> {
+ fragment_directive_impl::TextDirective::from_parts(
+ self.prefix.to_string(),
+ self.start.to_string(),
+ self.end.to_string(),
+ self.suffix.to_string(),
+ )
+ }
+}
+
+/// Result of the `parse_fragment_directive()` function.
+///
+/// The result contains the original given URL without the fragment directive,
+/// a unsanitized string version of the extracted fragment directive,
+/// and an array of the parsed text directives.
+#[repr(C)]
+pub struct ParsedFragmentDirectiveResult {
+ url_without_fragment_directive: nsCString,
+ fragment_directive: nsCString,
+ text_directives: ThinVec<TextDirective>,
+}
+
+/// Parses the fragment directive from a given URL.
+///
+/// This function writes the result data into `result`.
+/// The result consists of
+/// - the input url without the fragment directive,
+/// - the fragment directive as unparsed string,
+/// - a list of the parsed and percent-decoded text directives.
+///
+/// Directives which are unknown will be ignored.
+/// If new directive types are added in the future, they should also be considered here.
+/// This function returns false if no fragment directive is found, or it could not be parsed.
+#[no_mangle]
+pub extern "C" fn parse_fragment_directive(
+ url: &nsCString,
+ result: &mut ParsedFragmentDirectiveResult,
+) -> bool {
+ // sanitize inputs
+ result.url_without_fragment_directive = nsCString::new();
+ result.fragment_directive = nsCString::new();
+ result.text_directives.clear();
+
+ let url_as_rust_string = url.to_utf8();
+ if let Some((stripped_url, fragment_directive, text_directives)) =
+ fragment_directive_impl::parse_fragment_directive_and_remove_it_from_hash(
+ &url_as_rust_string,
+ )
+ {
+ result
+ .url_without_fragment_directive
+ .assign(&stripped_url);
+ result.fragment_directive.assign(&fragment_directive);
+ result.text_directives.extend(
+ text_directives
+ .iter()
+ .map(|text_directive| TextDirective::from_rust_type(text_directive)),
+ );
+ return true;
+ }
+ false
+}
+
+/// Creates a percent-encoded fragment directive string from a given list of `FragmentDirectiveElement`s.
+///
+/// The returned string has this form:
+/// `:~:text=[prefix1-,]start1[,end1][,-suffix1]&text=[prefix2-,]start2[,end2][,-suffix2]`
+///
+/// Invalid `FragmentDirectiveElement`s are ignored, where "invalid" means that no `start` token is provided.
+/// If there are no valid `FragmentDirectiveElement`s, an empty string is returned.
+#[no_mangle]
+pub extern "C" fn create_fragment_directive(
+ text_directives: &ThinVec<TextDirective>,
+ fragment_directive: &mut nsCString,
+) -> bool {
+ let directives_rust = Vec::from_iter(
+ text_directives
+ .iter()
+ .filter_map(|fragment| fragment.to_rust_type()),
+ );
+ if let Some(fragment_directive_rust) =
+ fragment_directive_impl::create_fragment_directive_string(&directives_rust)
+ {
+ fragment_directive.assign(&fragment_directive_rust);
+ return true;
+ }
+
+ false
+}
+
+/// Creates a percent-encoded text directive string for a single text directive.
+/// The returned string has the form `text=[prefix-,]start[,end][,-suffix]`.
+/// If the provided `TextDirective` is invalid (i.e. it has no `start` attribute),
+/// the outparam `directive_string` is empty and the function returns false.
+#[no_mangle]
+pub extern "C" fn create_text_directive(
+ text_directive: &TextDirective,
+ directive_string: &mut nsCString,
+) -> bool {
+ if let Some(text_directive_rust) = text_directive.to_rust_type() {
+ if let Some(text_directive_string_rust) =
+ fragment_directive_impl::create_text_directive_string(&text_directive_rust)
+ {
+ directive_string.assign(&text_directive_string_rust);
+ return true;
+ }
+ }
+ false
+}
diff --git a/dom/base/fragmentdirectives/test.rs b/dom/base/fragmentdirectives/test.rs
new file mode 100644
index 0000000000..d4509cb033
--- /dev/null
+++ b/dom/base/fragmentdirectives/test.rs
@@ -0,0 +1,599 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[cfg(test)]
+mod test {
+ use crate::fragment_directive_impl::{
+ create_fragment_directive_string, parse_fragment_directive_and_remove_it_from_hash,
+ TextDirective,
+ };
+
+ /// This test verifies that valid combinations of [prefix-,]start[,end][,-suffix] are parsed correctly.
+ #[test]
+ fn test_parse_fragment_directive_with_one_text_directive() {
+ let test_cases = vec![
+ ("#:~:text=start", (None, Some("start"), None, None)),
+ (
+ "#:~:text=start,end",
+ (None, Some("start"), Some("end"), None),
+ ),
+ (
+ "#:~:text=prefix-,start",
+ (Some("prefix"), Some("start"), None, None),
+ ),
+ (
+ "#:~:text=prefix-,start,end",
+ (Some("prefix"), Some("start"), Some("end"), None),
+ ),
+ (
+ "#:~:text=prefix-,start,end,-suffix",
+ (Some("prefix"), Some("start"), Some("end"), Some("suffix")),
+ ),
+ (
+ "#:~:text=start,-suffix",
+ (None, Some("start"), None, Some("suffix")),
+ ),
+ (
+ "#:~:text=start,end,-suffix",
+ (None, Some("start"), Some("end"), Some("suffix")),
+ ),
+ ("#:~:text=text=", (None, Some("text="), None, None)),
+ ];
+ for (url, (prefix, start, end, suffix)) in test_cases {
+ let (stripped_url, fragment_directive, result) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ fragment_directive,
+ &url[4..],
+ "The extracted fragment directive string
+ should be unsanitized and therefore match the input string."
+ );
+ assert_eq!(result.len(), 1, "There must be one parsed text fragment.");
+ assert_eq!(
+ stripped_url, "",
+ "The fragment directive must be removed from the url hash."
+ );
+ let text_directive = result.first().unwrap();
+ if prefix.is_none() {
+ assert!(
+ text_directive.prefix().is_none(),
+ "There must be no `prefix` token (test case `{}`).",
+ url
+ );
+ } else {
+ assert!(
+ text_directive
+ .prefix()
+ .as_ref()
+ .expect("There must be a `prefix` token.")
+ .value()
+ == prefix.unwrap(),
+ "Wrong value for `prefix` (test case `{}`).",
+ url
+ );
+ }
+ if start.is_none() {
+ assert!(
+ text_directive.start().is_none(),
+ "There must be no `start` token (test case `{}`).",
+ url
+ );
+ } else {
+ assert!(
+ text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value()
+ == start.unwrap(),
+ "Wrong value for `start` (test case `{}`).",
+ url
+ );
+ }
+ if end.is_none() {
+ assert!(
+ text_directive.end().is_none(),
+ "There must be no `end` token (test case `{}`).",
+ url
+ );
+ } else {
+ assert!(
+ text_directive
+ .end()
+ .as_ref()
+ .expect("There must be a `end` token.")
+ .value()
+ == end.unwrap(),
+ "Wrong value for `end` (test case `{}`).",
+ url
+ );
+ }
+ if suffix.is_none() {
+ assert!(
+ text_directive.suffix().is_none(),
+ "There must be no `suffix` token (test case `{}`).",
+ url
+ );
+ } else {
+ assert!(
+ text_directive
+ .suffix()
+ .as_ref()
+ .expect("There must be a `suffix` token.")
+ .value()
+ == suffix.unwrap(),
+ "Wrong value for `suffix` (test case `{}`).",
+ url
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn test_parse_full_url() {
+ for (url, stripped_url_ref) in [
+ ("https://example.com#:~:text=foo", "https://example.com"),
+ (
+ "https://example.com/some/page.html?query=answer#:~:text=foo",
+ "https://example.com/some/page.html?query=answer",
+ ),
+ (
+ "https://example.com/some/page.html?query=answer#fragment:~:text=foo",
+ "https://example.com/some/page.html?query=answer#fragment",
+ ),
+ (
+ "http://example.com/page.html?query=irrelevant:~:#bar:~:text=foo",
+ "http://example.com/page.html?query=irrelevant:~:#bar"
+ )
+ ] {
+ let (stripped_url, fragment_directive, _) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result");
+ assert_eq!(stripped_url, stripped_url_ref, "The stripped url is not correct.");
+ assert_eq!(fragment_directive, "text=foo");
+ }
+ }
+
+ /// This test verifies that a text fragment is parsed correctly if it is preceded
+ /// or followed by a fragment (i.e. `#foo:~:text=bar`).
+ #[test]
+ fn test_parse_text_fragment_after_fragments() {
+ let url = "#foo:~:text=start";
+ let (stripped_url, fragment_directive, result) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ result.len(),
+ 1,
+ "There must be exactly one parsed text fragment."
+ );
+ assert_eq!(
+ stripped_url, "#foo",
+ "The fragment directive was not removed correctly."
+ );
+ assert_eq!(
+ fragment_directive, "text=start",
+ "The fragment directive was not extracted correctly."
+ );
+ let fragment = result.first().unwrap();
+ assert!(fragment.prefix().is_none(), "There is no `prefix` token.");
+ assert_eq!(
+ fragment
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "start"
+ );
+ assert!(fragment.end().is_none(), "There is no `end` token.");
+ assert!(fragment.suffix().is_none(), "There is no `suffix` token.");
+ }
+
+ /// Ensure that multiple text fragments are parsed correctly.
+ #[test]
+ fn test_parse_multiple_text_fragments() {
+ let url = "#:~:text=prefix-,start,-suffix&text=foo&text=bar,-suffix";
+ let (_, _, text_directives) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ text_directives.len(),
+ 3,
+ "There must be exactly two parsed text fragments."
+ );
+ let first_text_directive = &text_directives[0];
+ assert_eq!(
+ first_text_directive
+ .prefix()
+ .as_ref()
+ .expect("There must be a `prefix` token.")
+ .value(),
+ "prefix"
+ );
+ assert_eq!(
+ first_text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "start"
+ );
+ assert!(
+ first_text_directive.end().is_none(),
+ "There is no `end` token."
+ );
+ assert_eq!(
+ first_text_directive
+ .suffix()
+ .as_ref()
+ .expect("There must be a `suffix` token.")
+ .value(),
+ "suffix"
+ );
+
+ let second_text_directive = &text_directives[1];
+ assert!(
+ second_text_directive.prefix().is_none(),
+ "There is no `prefix` token."
+ );
+ assert_eq!(
+ second_text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "foo"
+ );
+ assert!(
+ second_text_directive.end().is_none(),
+ "There is no `end` token."
+ );
+ assert!(
+ second_text_directive.suffix().is_none(),
+ "There is no `suffix` token."
+ );
+ let third_text_directive = &text_directives[2];
+ assert!(
+ third_text_directive.prefix().is_none(),
+ "There is no `prefix` token."
+ );
+ assert_eq!(
+ third_text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "bar"
+ );
+ assert!(
+ third_text_directive.end().is_none(),
+ "There is no `end` token."
+ );
+ assert_eq!(
+ third_text_directive
+ .suffix()
+ .as_ref()
+ .expect("There must be a `suffix` token.")
+ .value(),
+ "suffix"
+ );
+ }
+
+ /// Multiple text directives should be parsed correctly
+ /// if they are surrounded or separated by unknown directives.
+ #[test]
+ fn test_parse_multiple_text_directives_with_unknown_directive_in_between() {
+ for url in [
+ "#:~:foo&text=start1&text=start2",
+ "#:~:text=start1&foo&text=start2",
+ "#:~:text=start1&text=start2&foo",
+ ] {
+ let (_, fragment_directive, text_directives) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ fragment_directive,
+ &url[4..],
+ "The extracted fragment directive string is unsanitized
+ and should contain the unknown directive."
+ );
+ assert_eq!(
+ text_directives.len(),
+ 2,
+ "There must be exactly two parsed text fragments."
+ );
+ let first_text_directive = &text_directives[0];
+ assert_eq!(
+ first_text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "start1"
+ );
+ let second_text_directive = &text_directives[1];
+ assert_eq!(
+ second_text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` token.")
+ .value(),
+ "start2"
+ );
+ }
+ }
+
+ /// Ensures that input that doesn't contain a text fragment does not produce a result.
+ /// This includes the use of partial identifying tokens necessary for a text fragment
+ /// (e.g. `:~:` without `text=`, `text=foo` without the `:~:` or multiple occurrences of `:~:`)
+ /// In these cases, the parser must return `None` to indicate that there are no valid text fragments.
+ #[test]
+ fn test_parse_invalid_or_unknown_fragment_directive() {
+ for url in [
+ "#foo",
+ "#foo:",
+ "#foo:~:",
+ "#foo:~:bar",
+ "text=prefix-,start",
+ "#:~:text=foo-,bar,-baz:~:text=foo",
+ ] {
+ let text_directives =
+ parse_fragment_directive_and_remove_it_from_hash(&url);
+ assert!(
+ text_directives.is_none(),
+ "The fragment `{}` does not contain a valid or known fragment directive.",
+ url
+ );
+ }
+ }
+
+ /// Ensures that ill-formed text directives (but valid fragment directives)
+ /// (starting correctly with `:~:text=`) are not parsed.
+ /// Instead `None` must be returned.
+ /// Test cases include invalid combinations of `prefix`/`suffix`es,
+ /// additional `,`s, too many `start`/`end` tokens, or empty text fragments.
+ #[test]
+ fn test_parse_invalid_text_fragments() {
+ for url in [
+ "#:~:text=start,start,start",
+ "#:~:text=prefix-,prefix-",
+ "#:~:text=prefix-,-suffix",
+ "#:~:text=prefix-,start,start,start",
+ "#:~:text=prefix-,start,start,start,-suffix",
+ "#:~:text=start,start,start,-suffix",
+ "#:~:text=prefix-,start,end,-suffix,foo",
+ "#:~:text=foo,prefix-,start",
+ "#:~:text=prefix-,,start,",
+ "#:~:text=,prefix,start",
+ "#:~:text=",
+ ] {
+ let text_directives =
+ parse_fragment_directive_and_remove_it_from_hash(&url);
+ assert!(
+ text_directives.is_none(),
+ "The fragment directive `{}` does not contain a valid text directive.",
+ url
+ );
+ }
+ }
+
+ /// Ensure that out of multiple text fragments only the invalid ones are ignored
+ /// while valid text fragments are still returned.
+ /// Since correct parsing of multiple text fragments as well as
+ /// several forms of invalid text fragments are already tested in
+ /// `test_parse_multiple_text_fragments` and `test_parse_invalid_text_fragments()`,
+ /// it should be enough to test this with only one fragment directive
+ /// that contains two text fragments, one of them being invalid.
+ #[test]
+ fn test_valid_and_invalid_text_directives() {
+ for url in [
+ "#:~:text=start&text=,foo,",
+ "#:~:text=foo,foo,foo&text=start",
+ ] {
+ let (_, fragment_directive, text_directives) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ fragment_directive,
+ &url[4..],
+ "The extracted fragment directive string is unsanitized
+ and should contain invalid text directives."
+ );
+ assert_eq!(
+ text_directives.len(),
+ 1,
+ "There must be exactly one parsed text fragment."
+ );
+ let text_directive = text_directives.first().unwrap();
+ assert_eq!(
+ text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a `start` value.")
+ .value(),
+ "start",
+ "The `start` value of the text directive has the wrong value."
+ );
+ }
+ }
+
+ /// Ensures that a fragment directive that contains percent-encoded characters
+ /// is decoded correctly. This explicitly includes characters which are used
+ /// for identifying text fragments, i.e. `#`, `, `, `&`, `:`, `~` and `-`.
+ #[test]
+ fn test_parse_percent_encoding_tokens() {
+ let url = "#:~:text=prefix%26-,start%20and%2C,end%23,-%26suffix%2D";
+ let (_, fragment_directive, text_directives) =
+ parse_fragment_directive_and_remove_it_from_hash(&url)
+ .expect("The parser must find a result.");
+ assert_eq!(
+ fragment_directive,
+ &url[4..],
+ "The extracted fragment directive string is unsanitized
+ and should contain the original and percent-decoded string."
+ );
+ let text_directive = text_directives.first().unwrap();
+ assert_eq!(
+ text_directive
+ .prefix()
+ .as_ref()
+ .expect("There must be a prefix.")
+ .value(),
+ "prefix&",
+ ""
+ );
+ assert_eq!(
+ text_directive
+ .start()
+ .as_ref()
+ .expect("There must be a prefix.")
+ .value(),
+ "start and,",
+ ""
+ );
+ assert_eq!(
+ text_directive
+ .end()
+ .as_ref()
+ .expect("There must be a prefix.")
+ .value(),
+ "end#",
+ ""
+ );
+ assert_eq!(
+ text_directive
+ .suffix()
+ .as_ref()
+ .expect("There must be a prefix.")
+ .value(),
+ "&suffix-",
+ ""
+ );
+ }
+
+ /// Ensures that a text fragment is created correctly,
+ /// based on a given combination of tokens.
+ /// This includes all sorts of combinations of
+ /// `prefix`, `suffix`, `start` and `end`,
+ /// als well as values for these tokens which contain
+ /// characters that need to be encoded because they are
+ /// identifiers for text fragments
+ /// (#`, `, `, `&`, `:`, `~` and `-`).
+ #[test]
+ fn test_create_fragment_directive() {
+ for (text_directive, expected_fragment_directive) in [
+ (
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start"),
+ String::new(),
+ String::new(),
+ )
+ .unwrap(),
+ ":~:text=start",
+ ),
+ (
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start"),
+ String::from("end"),
+ String::new(),
+ )
+ .unwrap(),
+ ":~:text=start,end",
+ ),
+ (
+ TextDirective::from_parts(
+ String::from("prefix"),
+ String::from("start"),
+ String::from("end"),
+ String::new(),
+ )
+ .unwrap(),
+ ":~:text=prefix-,start,end",
+ ),
+ (
+ TextDirective::from_parts(
+ String::from("prefix"),
+ String::from("start"),
+ String::from("end"),
+ String::from("suffix"),
+ )
+ .unwrap(),
+ ":~:text=prefix-,start,end,-suffix",
+ ),
+ (
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start"),
+ String::from("end"),
+ String::from("suffix"),
+ )
+ .unwrap(),
+ ":~:text=start,end,-suffix",
+ ),
+ (
+ TextDirective::from_parts(
+ String::from("prefix"),
+ String::from("start"),
+ String::new(),
+ String::from("suffix"),
+ )
+ .unwrap(),
+ ":~:text=prefix-,start,-suffix",
+ ),
+ (
+ TextDirective::from_parts(
+ String::from("prefix-"),
+ String::from("start and,"),
+ String::from("&end"),
+ String::from("#:~:suffix"),
+ )
+ .unwrap(),
+ ":~:text=prefix%2D-,start%20and%2C,%26end,-%23%3A%7E%3Asuffix",
+ ),
+ ] {
+ let fragment_directive = create_fragment_directive_string(&vec![text_directive])
+ .expect("The given input must produce a valid fragment directive.");
+ assert_eq!(fragment_directive, expected_fragment_directive);
+ }
+ }
+
+ /// Ensures that a fragment directive is created correctly if multiple text fragments are given.
+ /// The resulting fragment must start with `:~:`
+ /// and each text fragment must be separated using `&text=`.
+ #[test]
+ fn test_create_fragment_directive_from_multiple_text_directives() {
+ let text_directives = vec![
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start1"),
+ String::new(),
+ String::new(),
+ )
+ .unwrap(),
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start2"),
+ String::new(),
+ String::new(),
+ )
+ .unwrap(),
+ TextDirective::from_parts(
+ String::new(),
+ String::from("start3"),
+ String::new(),
+ String::new(),
+ )
+ .unwrap(),
+ ];
+ let fragment_directive = create_fragment_directive_string(&text_directives)
+ .expect("The given input must produce a valid fragment directive.");
+ assert_eq!(
+ fragment_directive, ":~:text=start1&text=start2&text=start3",
+ "The created fragment directive is wrong for multiple fragments."
+ );
+ }
+}
diff --git a/dom/base/moz.build b/dom/base/moz.build
index ef1780f161..ffcfb0aaf6 100644
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -194,6 +194,7 @@ EXPORTS.mozilla.dom += [
"External.h",
"FilteredNodeIterator.h",
"FormData.h",
+ "FragmentDirective.h",
"FragmentOrElement.h",
"FromParser.h",
"GeneratedImageContent.h",
@@ -237,6 +238,7 @@ EXPORTS.mozilla.dom += [
"PlacesBookmarkTitle.h",
"PlacesBookmarkUrl.h",
"PlacesEvent.h",
+ "PlacesEventCounts.h",
"PlacesFavicon.h",
"PlacesHistoryCleared.h",
"PlacesObservers.h",
@@ -296,6 +298,7 @@ if CONFIG["FUZZING"]:
if CONFIG["COMPILE_ENVIRONMENT"]:
EXPORTS.mozilla.dom += [
+ "!fragmentdirectives_ffi_generated.h",
"!GeneratedElementDocumentState.h",
"RustTypes.h",
]
@@ -305,6 +308,11 @@ if CONFIG["COMPILE_ENVIRONMENT"]:
inputs=["rust"],
)
+ CbindgenHeader(
+ "fragmentdirectives_ffi_generated.h",
+ inputs=["fragmentdirectives"],
+ )
+
UNIFIED_SOURCES += [
"!UseCounterMetrics.cpp",
"AbstractRange.cpp",
@@ -349,6 +357,7 @@ UNIFIED_SOURCES += [
"EventSourceEventService.cpp",
"External.cpp",
"FormData.cpp",
+ "FragmentDirective.cpp",
"FragmentOrElement.cpp",
"GeneratedImageContent.cpp",
"GlobalTeardownObserver.cpp",
@@ -489,6 +498,7 @@ if CONFIG["FUZZING"]:
if CONFIG["MOZ_PLACES"]:
UNIFIED_SOURCES += [
"PlacesEvent.cpp",
+ "PlacesEventCounts.cpp",
"PlacesObservers.cpp",
"PlacesWeakCallbackWrapper.cpp",
]
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index c6f1687f73..d2c863bd65 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -1767,6 +1767,17 @@ bool nsContentUtils::IsAlphanumericOrSymbol(uint32_t aChar) {
cat == nsUGenCategory::kSymbol;
}
+// static
+bool nsContentUtils::IsHyphen(uint32_t aChar) {
+ // Characters treated as hyphens for the purpose of "emergency" breaking
+ // when the content would otherwise overflow.
+ return aChar == uint32_t('-') || // HYPHEN-MINUS
+ aChar == 0x2010 || // HYPHEN
+ aChar == 0x2012 || // FIGURE DASH
+ aChar == 0x2013 || // EN DASH
+ aChar == 0x058A; // ARMENIAN HYPHEN
+}
+
/* static */
bool nsContentUtils::IsHTMLWhitespace(char16_t aChar) {
return aChar == char16_t(0x0009) || aChar == char16_t(0x000A) ||
@@ -3008,6 +3019,15 @@ nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorHelper(
}
/* static */
+nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorForSelection(
+ nsIContent* aContent1, nsIContent* aContent2) {
+ return GetCommonAncestorInternal(
+ aContent1, aContent2, [](nsIContent* aContent) {
+ return aContent->GetFlattenedTreeParentNodeForSelection();
+ });
+}
+
+/* static */
Element* nsContentUtils::GetCommonFlattenedTreeAncestorForStyle(
Element* aElement1, Element* aElement2) {
return GetCommonAncestorInternal(aElement1, aElement2, [](Element* aElement) {
@@ -11341,7 +11361,7 @@ int32_t nsContentUtils::CompareTreePosition(const nsINode* aNode1,
MOZ_ASSERT(aNode1, "aNode1 must not be null");
MOZ_ASSERT(aNode2, "aNode2 must not be null");
- if (MOZ_UNLIKELY(NS_WARN_IF(aNode1 == aNode2))) {
+ if (NS_WARN_IF(aNode1 == aNode2)) {
return 0;
}
@@ -11439,7 +11459,8 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost,
bool aIsClonable,
bool aDelegatesFocus) {
RefPtr<Element> host = mozilla::dom::Element::FromNodeOrNull(aHost);
- if (!host) {
+ if (!host || host->GetShadowRoot()) {
+ // https://html.spec.whatwg.org/#parsing-main-inhead:shadow-host
return nullptr;
}
@@ -11449,9 +11470,10 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost,
init.mSlotAssignment = SlotAssignmentMode::Named;
init.mClonable = aIsClonable;
- RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors(),
- Element::ShadowRootDeclarative::Yes);
+ RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors());
if (shadowRoot) {
+ shadowRoot->SetIsDeclarative(
+ nsGenericHTMLFormControlElement::ShadowRootDeclarative::Yes);
// https://html.spec.whatwg.org/#parsing-main-inhead:available-to-element-internals
shadowRoot->SetAvailableToElementInternals();
}
diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h
index 338fc097de..4291d2c5d1 100644
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -225,7 +225,6 @@ enum EventNameType {
EventNameType_SVGSVG = 0x0008, // the svg element
EventNameType_SMIL = 0x0010, // smil elements
EventNameType_HTMLBodyOrFramesetOnly = 0x0020,
- EventNameType_HTMLMarqueeOnly = 0x0040,
EventNameType_HTMLXUL = 0x0003,
EventNameType_All = 0xFFFF
@@ -516,6 +515,13 @@ class nsContentUtils {
}
/**
+ * Returns the common flattened tree ancestor from the point of view of
+ * the selection system, if any, for two given content nodes.
+ */
+ static nsIContent* GetCommonFlattenedTreeAncestorForSelection(
+ nsIContent* aContent1, nsIContent* aContent2);
+
+ /**
* Returns the common flattened tree ancestor from the point of view of the
* style system, if any, for two given content nodes.
*/
@@ -781,6 +787,10 @@ class nsContentUtils {
* Returns true if aChar is of class L*, N* or S* (for first-letter).
*/
static bool IsAlphanumericOrSymbol(uint32_t aChar);
+ /**
+ * Returns true if aChar is a kind of hyphen.
+ */
+ static bool IsHyphen(uint32_t aChar);
/*
* Is the character an HTML whitespace character?
diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp
index 5a4cf78d65..4e2d604693 100644
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -846,24 +846,27 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument,
NS_ENSURE_ARG(aDocument);
NS_ENSURE_ARG(aContent);
- RefPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
- if (!window) {
+ nsPIDOMWindowOuter* windowPtr = aDocument->GetWindow();
+ if (!windowPtr) {
return NS_OK;
}
// if the content is currently focused in the window, or is an
// shadow-including inclusive ancestor of the currently focused element,
// reset the focus within that window.
- RefPtr<Element> previousFocusedElement = window->GetFocusedElement();
- if (!previousFocusedElement) {
+ Element* previousFocusedElementPtr = windowPtr->GetFocusedElement();
+ if (!previousFocusedElementPtr) {
return NS_OK;
}
if (!nsContentUtils::ContentIsHostIncludingDescendantOf(
- previousFocusedElement, aContent)) {
+ previousFocusedElementPtr, aContent)) {
return NS_OK;
}
+ RefPtr<nsPIDOMWindowOuter> window = windowPtr;
+ RefPtr<Element> previousFocusedElement = previousFocusedElementPtr;
+
RefPtr<Element> newFocusedElement = [&]() -> Element* {
if (auto* sr = ShadowRoot::FromNode(aContent)) {
if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) {
@@ -1802,7 +1805,8 @@ Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent,
// focus, update the node in the window, and raise the window if desired.
if (allowFrameSwitch) {
AdjustWindowFocus(newBrowsingContext, true, IsWindowVisible(newWindow),
- actionId);
+ actionId, false /* aShouldClearAncestorFocus */,
+ nullptr /* aAncestorBrowsingContextToFocus */);
}
// set the focus node and method as needed
@@ -1975,7 +1979,9 @@ mozilla::dom::BrowsingContext* nsFocusManager::GetCommonAncestor(
bool nsFocusManager::AdjustInProcessWindowFocus(
BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible,
- uint64_t aActionId) {
+ uint64_t aActionId, bool aShouldClearAncestorFocus,
+ BrowsingContext* aAncestorBrowsingContextToFocus) {
+ MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus);
if (ActionIdComparableAndLower(aActionId,
mActionIdForFocusedBrowsingContextInContent)) {
LOGFOCUS(
@@ -2026,6 +2032,17 @@ bool nsFocusManager::AdjustInProcessWindowFocus(
break;
}
+ if (aShouldClearAncestorFocus) {
+ // This is the BrowsingContext that receives the focus, no need to clear
+ // its focused element and the rest of the ancestors.
+ if (window->GetBrowsingContext() == aAncestorBrowsingContextToFocus) {
+ break;
+ }
+
+ window->SetFocusedElement(nullptr);
+ continue;
+ }
+
if (frameElement != window->GetFocusedElement()) {
window->SetFocusedElement(frameElement);
@@ -2041,18 +2058,22 @@ bool nsFocusManager::AdjustInProcessWindowFocus(
return needToNotifyOtherProcess;
}
-void nsFocusManager::AdjustWindowFocus(BrowsingContext* aBrowsingContext,
- bool aCheckPermission, bool aIsVisible,
- uint64_t aActionId) {
+void nsFocusManager::AdjustWindowFocus(
+ BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible,
+ uint64_t aActionId, bool aShouldClearAncestorFocus,
+ BrowsingContext* aAncestorBrowsingContextToFocus) {
+ MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus);
if (AdjustInProcessWindowFocus(aBrowsingContext, aCheckPermission, aIsVisible,
- aActionId)) {
+ aActionId, aShouldClearAncestorFocus,
+ aAncestorBrowsingContextToFocus)) {
// Some ancestors of aBrowsingContext isn't in this process, so notify other
// processes to adjust their focused element.
mozilla::dom::ContentChild* contentChild =
mozilla::dom::ContentChild::GetSingleton();
MOZ_ASSERT(contentChild);
- contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible,
- aActionId);
+ contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, aActionId,
+ aShouldClearAncestorFocus,
+ aAncestorBrowsingContextToFocus);
}
}
@@ -2423,6 +2444,22 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
if (ancestorWindowToFocus) {
ancestorWindowToFocus->SetFocusedElement(nullptr, 0, true);
}
+
+ // When the focus of aBrowsingContextToClear is cleared, it should
+ // also clear its ancestors's focus because ancestors should no longer
+ // be considered aBrowsingContextToClear is focused.
+ //
+ // We don't need to do this when aBrowsingContextToClear and
+ // aAncestorBrowsingContextToFocus is equal because ancestors don't
+ // care about this.
+ if (aBrowsingContextToClear &&
+ aBrowsingContextToClear != aAncestorBrowsingContextToFocus) {
+ AdjustWindowFocus(
+ aBrowsingContextToClear, false,
+ IsWindowVisible(aBrowsingContextToClear->GetDOMWindow()), aActionId,
+ true /* aShouldClearAncestorFocus */,
+ aAncestorBrowsingContextToFocus);
+ }
}
SetFocusedWindowInternal(nullptr, aActionId);
@@ -2569,7 +2606,9 @@ void nsFocusManager::Focus(
// focus can be traversed from the top level down to the newly focused
// window.
RefPtr<BrowsingContext> bc = aWindow->GetBrowsingContext();
- AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId);
+ AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId,
+ false /* aShouldClearAncestorFocus */,
+ nullptr /* aAncestorBrowsingContextToFocus */);
}
// indicate that the window has taken focus.
diff --git a/dom/base/nsFocusManager.h b/dom/base/nsFocusManager.h
index 4fb9d05e1c..9815ab9b98 100644
--- a/dom/base/nsFocusManager.h
+++ b/dom/base/nsFocusManager.h
@@ -349,17 +349,21 @@ class nsFocusManager final : public nsIFocusManager,
nsPIDOMWindowOuter* aWindow, mozilla::dom::BrowsingContext* aContext);
/**
- * When aBrowsingContext is focused, adjust the ancestors of aBrowsingContext
- * so that they also have their corresponding frames focused. Thus, one can
- * start at the active top-level window and navigate down the currently
- * focused elements for each frame in the tree to get to aBrowsingContext.
+ * When aBrowsingContext is focused or blurred, adjust the ancestors of
+ * aBrowsingContext so that they also have their corresponding frames focused
+ * or blurred. Thus, one can start at the active top-level window and navigate
+ * down the currently focused elements for each frame in the tree to get to
+ * aBrowsingContext.
*/
MOZ_CAN_RUN_SCRIPT bool AdjustInProcessWindowFocus(
mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission,
- bool aIsVisible, uint64_t aActionId);
+ bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus,
+ mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus);
+
MOZ_CAN_RUN_SCRIPT void AdjustWindowFocus(
mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission,
- bool aIsVisible, uint64_t aActionId);
+ bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus,
+ mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus);
/**
* Returns true if aWindow is visible.
diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp
index eca528f258..1e3fb93aa8 100644
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -3388,7 +3388,8 @@ already_AddRefed<Promise> nsFrameLoader::PrintPreview(
/* aListener = */ nullptr, docShellToCloneInto,
nsGlobalWindowOuter::IsPreview::Yes,
nsGlobalWindowOuter::IsForWindowDotPrint::No,
- [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, rv);
+ [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); },
+ nullptr, rv);
if (NS_WARN_IF(rv.Failed())) {
promise->MaybeReject(std::move(rv));
}
diff --git a/dom/base/nsFrameLoaderOwner.cpp b/dom/base/nsFrameLoaderOwner.cpp
index 03945975dd..0e67f723d2 100644
--- a/dom/base/nsFrameLoaderOwner.cpp
+++ b/dom/base/nsFrameLoaderOwner.cpp
@@ -81,13 +81,7 @@ nsFrameLoaderOwner::ShouldPreserveBrowsingContext(
}
}
- // We will preserve our browsing context if either fission is enabled, or the
- // `preserve_browsing_contexts` pref is active.
- if (UseRemoteSubframes() ||
- StaticPrefs::fission_preserve_browsing_contexts()) {
- return ChangeRemotenessContextType::PRESERVE;
- }
- return ChangeRemotenessContextType::DONT_PRESERVE;
+ return ChangeRemotenessContextType::PRESERVE;
}
void nsFrameLoaderOwner::ChangeRemotenessCommon(
diff --git a/dom/base/nsFrameMessageManager.cpp b/dom/base/nsFrameMessageManager.cpp
index da275095b1..4c110cc55e 100644
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -506,7 +506,7 @@ void nsFrameMessageManager::SendSyncMessage(JSContext* aCx,
"Should not have parent manager in content!");
AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING(
- "nsFrameMessageManager::SendMessage", OTHER, aMessageName);
+ "nsFrameMessageManager::SendSyncMessage", OTHER, aMessageName);
profiler_add_marker("SendSyncMessage", geckoprofiler::category::IPC, {},
FrameMessageMarker{}, aMessageName, true);
@@ -1428,8 +1428,7 @@ class SameParentProcessMessageManagerCallback : public MessageManagerCallback {
bool aRunInGlobalScope) override {
auto* global = ContentProcessMessageManager::Get();
MOZ_ASSERT(!aRunInGlobalScope);
- global->LoadScript(aURL);
- return true;
+ return global && global->LoadScript(aURL);
}
nsresult DoSendAsyncMessage(const nsAString& aMessage,
diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp
index 5337e1588f..7dcd265ca4 100644
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -3703,7 +3703,7 @@ bool nsGlobalWindowInner::Confirm(const nsAString& aMessage,
}
already_AddRefed<Promise> nsGlobalWindowInner::Fetch(
- const RequestOrUSVString& aInput, const RequestInit& aInit,
+ const RequestOrUTF8String& aInput, const RequestInit& aInit,
CallerType aCallerType, ErrorResult& aRv) {
return FetchRequest(this, aInput, aInit, aCallerType, aRv);
}
@@ -3752,7 +3752,7 @@ Nullable<WindowProxyHolder> nsGlobalWindowInner::PrintPreview(
/* aRemotePrintJob = */ nullptr, aListener, aDocShellToCloneInto,
nsGlobalWindowOuter::IsPreview::Yes,
nsGlobalWindowOuter::IsForWindowDotPrint::No,
- /* aPrintPreviewCallback = */ nullptr, aError),
+ /* aPrintPreviewCallback = */ nullptr, nullptr, aError),
aError, nullptr);
}
diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h
index 15fbc4259f..215e362dad 100644
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -120,7 +120,7 @@ class OwningExternalOrWindowProxy;
class Promise;
class PostMessageEvent;
struct RequestInit;
-class RequestOrUSVString;
+class RequestOrUTF8String;
class SharedWorker;
class Selection;
struct SizeToContentConstraints;
@@ -671,7 +671,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
already_AddRefed<mozilla::dom::cache::CacheStorage> GetCaches(
mozilla::ErrorResult& aRv);
already_AddRefed<mozilla::dom::Promise> Fetch(
- const mozilla::dom::RequestOrUSVString& aInput,
+ const mozilla::dom::RequestOrUTF8String& aInput,
const mozilla::dom::RequestInit& aInit,
mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aRv);
MOZ_CAN_RUN_SCRIPT void Print(mozilla::ErrorResult& aError);
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index e28dcdb092..c678a0a941 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -2414,6 +2414,11 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument,
MOZ_RELEASE_ASSERT(newInnerWindow->mDoc == aDocument);
+ if (mBrowsingContext->IsTopContent()) {
+ net::CookieJarSettings::Cast(aDocument->CookieJarSettings())
+ ->SetTopLevelWindowContextId(aDocument->InnerWindowID());
+ }
+
newInnerWindow->RefreshReduceTimerPrecisionCallerType();
if (!aState) {
@@ -3468,9 +3473,17 @@ nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) {
aSize = CSSPixel::FromAppUnits(viewportSize);
- if (StaticPrefs::dom_innerSize_rounded()) {
- aSize.width = std::roundf(aSize.width);
- aSize.height = std::roundf(aSize.height);
+ switch (StaticPrefs::dom_innerSize_rounding()) {
+ case 1:
+ aSize.width = std::roundf(aSize.width);
+ aSize.height = std::roundf(aSize.height);
+ break;
+ case 2:
+ aSize.width = std::truncf(aSize.width);
+ aSize.height = std::truncf(aSize.height);
+ break;
+ default:
+ break;
}
return NS_OK;
@@ -4993,7 +5006,7 @@ void nsGlobalWindowOuter::PrintOuter(ErrorResult& aError) {
const bool forPreview = !StaticPrefs::print_always_print_silent();
Print(nullptr, nullptr, nullptr, nullptr, IsPreview(forPreview),
- IsForWindowDotPrint::Yes, nullptr, aError);
+ IsForWindowDotPrint::Yes, nullptr, nullptr, aError);
#endif
}
@@ -5015,7 +5028,8 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
nsIPrintSettings* aPrintSettings, RemotePrintJobChild* aRemotePrintJob,
nsIWebProgressListener* aListener, nsIDocShell* aDocShellToCloneInto,
IsPreview aIsPreview, IsForWindowDotPrint aForWindowDotPrint,
- PrintPreviewResolver&& aPrintPreviewCallback, ErrorResult& aError) {
+ PrintPreviewResolver&& aPrintPreviewCallback,
+ RefPtr<BrowsingContext>* aCachedBrowsingContext, ErrorResult& aError) {
#ifdef NS_PRINTING
nsCOMPtr<nsIPrintSettingsService> printSettingsService =
do_GetService("@mozilla.org/gfx/printsettings-service;1");
@@ -5051,16 +5065,36 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
nsCOMPtr<nsIDocumentViewer> viewer;
RefPtr<BrowsingContext> bc;
bool hasPrintCallbacks = false;
- if (docToPrint->IsStaticDocument()) {
+ bool wasStaticDocument = docToPrint->IsStaticDocument();
+ bool usingCachedBrowsingContext = false;
+ if (aCachedBrowsingContext && *aCachedBrowsingContext) {
+ MOZ_ASSERT(!wasStaticDocument,
+ "Why pass in non-empty aCachedBrowsingContext if original "
+ "document is already static?");
+ if (!wasStaticDocument) {
+ // The passed in document is not a static clone and the caller passed in a
+ // static clone to reuse, so swap it in.
+ docToPrint = (*aCachedBrowsingContext)->GetDocument();
+ MOZ_ASSERT(docToPrint);
+ MOZ_ASSERT(docToPrint->IsStaticDocument());
+ wasStaticDocument = true;
+ usingCachedBrowsingContext = true;
+ }
+ }
+ if (wasStaticDocument) {
if (aForWindowDotPrint == IsForWindowDotPrint::Yes) {
aError.ThrowNotSupportedError(
"Calling print() from a print preview is unsupported, did you intend "
"to call printPreview() instead?");
return nullptr;
}
- // We're already a print preview window, just reuse our browsing context /
- // content viewer.
- bc = sourceBC;
+ if (usingCachedBrowsingContext) {
+ bc = docToPrint->GetBrowsingContext();
+ } else {
+ // We're already a print preview window, just reuse our browsing context /
+ // content viewer.
+ bc = sourceBC;
+ }
nsCOMPtr<nsIDocShell> docShell = bc->GetDocShell();
if (!docShell) {
aError.ThrowNotSupportedError("No docshell");
@@ -5102,6 +5136,10 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
if (NS_WARN_IF(aError.Failed())) {
return nullptr;
}
+ if (aCachedBrowsingContext) {
+ MOZ_ASSERT(!*aCachedBrowsingContext);
+ *aCachedBrowsingContext = bc;
+ }
}
if (!bc) {
aError.ThrowNotAllowedError("No browsing context");
@@ -5157,6 +5195,24 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print(
"Content viewer didn't implement nsIWebBrowserPrint");
return nullptr;
}
+ bool closeWindowAfterPrint;
+ if (wasStaticDocument) {
+ // Here the document was a static clone to begin with that this code did not
+ // create, so we should not clean it up.
+ // The exception is if we're using the passed-in aCachedBrowsingContext, in
+ // which case this is the second print with this static document clone that
+ // we created the first time through, and we are responsible for cleaning it
+ // up.
+ closeWindowAfterPrint = usingCachedBrowsingContext;
+ } else {
+ // In this case the document was not a static clone, so we made a static
+ // clone for printing purposes and must clean it up after the print is done.
+ // The exception is if aCachedBrowsingContext is non-NULL, meaning the
+ // caller is intending to print this document again, so we need to defer the
+ // cleanup until after the second print.
+ closeWindowAfterPrint = !aCachedBrowsingContext;
+ }
+ webBrowserPrint->SetCloseWindowAfterPrint(closeWindowAfterPrint);
// For window.print(), we postpone making these calls until the round-trip to
// the parent process (triggered by the OpenInternal call above) calls us
@@ -5953,10 +6009,11 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
if (!allowClose) {
// We're blocking the close operation
// report localized error msg in JS console
- nsContentUtils::ReportToConsole(
- nsIScriptError::warningFlag, "DOM Window"_ns,
- mDoc, // Better name for the category?
- nsContentUtils::eDOM_PROPERTIES, "WindowCloseBlockedWarning");
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ "DOM Window"_ns,
+ mDoc, // Better name for the category?
+ nsContentUtils::eDOM_PROPERTIES,
+ "WindowCloseByScriptBlockedWarning");
return;
}
diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h
index 8337a7353f..3c26344c3d 100644
--- a/dom/base/nsGlobalWindowOuter.h
+++ b/dom/base/nsGlobalWindowOuter.h
@@ -105,7 +105,6 @@ class PostMessageData;
class PostMessageEvent;
class PrintPreviewResultInfo;
struct RequestInit;
-class RequestOrUSVString;
class Selection;
struct SizeToContentConstraints;
class SpeechSynthesis;
@@ -581,7 +580,8 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget,
Print(nsIPrintSettings*,
mozilla::layout::RemotePrintJobChild* aRemotePrintJob,
nsIWebProgressListener*, nsIDocShell*, IsPreview, IsForWindowDotPrint,
- PrintPreviewResolver&&, mozilla::ErrorResult&);
+ PrintPreviewResolver&&, RefPtr<mozilla::dom::BrowsingContext>*,
+ mozilla::ErrorResult&);
mozilla::dom::Selection* GetSelectionOuter();
already_AddRefed<mozilla::dom::Selection> GetSelection() override;
nsScreen* GetScreen();
diff --git a/dom/base/nsIContentInlines.h b/dom/base/nsIContentInlines.h
index 89cc7b9c30..04e73b75e1 100644
--- a/dom/base/nsIContentInlines.h
+++ b/dom/base/nsIContentInlines.h
@@ -82,10 +82,27 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) {
return parent;
}
- if (parentAsContent->GetShadowRoot()) {
- // If it's not assigned to any slot it's not part of the flat tree, and thus
- // we return null.
- return content->GetAssignedSlot();
+ // Use GetShadowRootForSelection for the selection case such that
+ // if the content is slotted into a UA shadow tree, use
+ // the parent of content as the flattened tree parent (instead of
+ // the slot element).
+ const nsINode* shadowRootForParent =
+ aType == nsINode::eForSelection
+ ? parentAsContent->GetShadowRootForSelection()
+ : parentAsContent->GetShadowRoot();
+
+ if (shadowRootForParent) {
+ // When aType is not nsINode::eForSelection, If it's not assigned to any
+ // slot it's not part of the flat tree, and thus we return null.
+ auto* assignedSlot = content->GetAssignedSlot();
+ if (assignedSlot || aType != nsINode::eForSelection) {
+ return assignedSlot;
+ }
+
+ MOZ_ASSERT(aType == nsINode::eForSelection);
+ // When aType is nsINode::eForSelection, we use the parent of the
+ // content even if it's not assigned to any slot.
+ return parent;
}
if (parentAsContent->IsInShadowTree()) {
@@ -106,7 +123,7 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) {
}
inline nsINode* nsINode::GetFlattenedTreeParentNode() const {
- return ::GetFlattenedTreeParentNode<nsINode::eNotForStyle>(this);
+ return ::GetFlattenedTreeParentNode<nsINode::eNormal>(this);
}
inline nsIContent* nsIContent::GetFlattenedTreeParent() const {
@@ -127,6 +144,11 @@ inline nsINode* nsINode::GetFlattenedTreeParentNodeForStyle() const {
return ::GetFlattenedTreeParentNode<nsINode::eForStyle>(this);
}
+inline nsIContent* nsINode::GetFlattenedTreeParentNodeForSelection() const {
+ nsINode* parent = ::GetFlattenedTreeParentNode<nsINode::eForSelection>(this);
+ return (parent && parent->IsContent()) ? parent->AsContent() : nullptr;
+}
+
inline bool nsINode::NodeOrAncestorHasDirAuto() const {
return AncestorHasDirAuto() || (IsElement() && AsElement()->HasDirAuto());
}
diff --git a/dom/base/nsIEventSourceEventService.idl b/dom/base/nsIEventSourceEventService.idl
index 6131424669..be96d58260 100644
--- a/dom/base/nsIEventSourceEventService.idl
+++ b/dom/base/nsIEventSourceEventService.idl
@@ -31,5 +31,5 @@ interface nsIEventSourceEventService : nsISupports
[must_use] void removeListener(in unsigned long long aInnerWindowID,
in nsIEventSourceEventListener aListener);
- [must_use] bool hasListenerFor(in unsigned long long aInnerWindowID);
+ [must_use] boolean hasListenerFor(in unsigned long long aInnerWindowID);
};
diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp
index d5455e5596..6c77574df7 100644
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -287,11 +287,17 @@ static const nsINode* GetClosestCommonInclusiveAncestorForRangeInSelection(
const nsINode* aNode) {
while (aNode &&
!aNode->IsClosestCommonInclusiveAncestorForRangeInSelection()) {
+ const bool isNodeInShadowTree =
+ StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() &&
+ aNode->IsInShadowTree();
if (!aNode
- ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) {
+ ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection() &&
+ !isNodeInShadowTree) {
return nullptr;
}
- aNode = aNode->GetParentNode();
+ aNode = StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()
+ ? aNode->GetParentOrShadowHostNode()
+ : aNode->GetParentNode();
}
return aNode;
}
@@ -315,12 +321,12 @@ class IsItemInRangeComparator {
int operator()(const AbstractRange* const aRange) const {
int32_t cmp = nsContentUtils::ComparePoints_Deprecated(
- &mNode, mEndOffset, aRange->GetStartContainer(), aRange->StartOffset(),
- nullptr, mCache);
+ &mNode, mEndOffset, aRange->GetMayCrossShadowBoundaryStartContainer(),
+ aRange->MayCrossShadowBoundaryStartOffset(), nullptr, mCache);
if (cmp == 1) {
cmp = nsContentUtils::ComparePoints_Deprecated(
- &mNode, mStartOffset, aRange->GetEndContainer(), aRange->EndOffset(),
- nullptr, mCache);
+ &mNode, mStartOffset, aRange->GetMayCrossShadowBoundaryEndContainer(),
+ aRange->MayCrossShadowBoundaryEndOffset(), nullptr, mCache);
if (cmp == -1) {
return 0;
}
@@ -386,6 +392,18 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
return true;
}
+ if (range->MayCrossShadowBoundary()) {
+ MOZ_ASSERT(range->IsDynamicRange(),
+ "range->MayCrossShadowBoundary() can only return true for "
+ "dynamic range");
+ StaticRange* crossBoundaryRange =
+ range->AsDynamicRange()->GetCrossShadowBoundaryRange();
+ MOZ_ASSERT(crossBoundaryRange);
+ if (!crossBoundaryRange->Collapsed()) {
+ return true;
+ }
+ }
+
const AbstractRange* middlePlus1;
const AbstractRange* middleMinus1;
// if node end > start of middle+1, result = 1
@@ -552,7 +570,8 @@ static nsIContent* GetRootForContentSubtree(nsIContent* aContent) {
return nsIContent::FromNode(aContent->SubtreeRoot());
}
-nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) {
+nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell,
+ bool aAllowCrossShadowBoundary) {
NS_ENSURE_TRUE(aPresShell, nullptr);
if (IsDocument()) return AsDocument()->GetRootElement();
@@ -596,7 +615,7 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) {
}
RefPtr<nsFrameSelection> fs = aPresShell->FrameSelection();
- nsIContent* content = fs->GetLimiter();
+ nsCOMPtr<nsIContent> content = fs->GetLimiter();
if (!content) {
content = fs->GetAncestorLimiter();
if (!content) {
@@ -616,6 +635,10 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) {
// Use the host as the root.
if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(content)) {
content = shadowRoot->GetHost();
+ if (content && aAllowCrossShadowBoundary) {
+ content = content->GetSelectionRootContent(aPresShell,
+ aAllowCrossShadowBoundary);
+ }
}
}
@@ -3826,6 +3849,33 @@ void nsINode::FireNodeRemovedForChildren() {
}
}
+ShadowRoot* nsINode::GetShadowRoot() const {
+ return IsContent() ? AsContent()->GetShadowRoot() : nullptr;
+}
+
+ShadowRoot* nsINode::GetShadowRootForSelection() const {
+ if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ return nullptr;
+ }
+
+ ShadowRoot* shadowRoot = GetShadowRoot();
+ if (!shadowRoot) {
+ return nullptr;
+ }
+
+ // ie. <details> and <video>
+ if (shadowRoot->IsUAWidget()) {
+ return nullptr;
+ }
+
+ // ie. <use> element
+ if (IsElement() && !AsElement()->CanAttachShadowDOM()) {
+ return nullptr;
+ }
+
+ return shadowRoot;
+}
+
NS_IMPL_ISUPPORTS(nsNodeWeakReference, nsIWeakReference)
nsNodeWeakReference::nsNodeWeakReference(nsINode* aNode)
diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h
index 3a47992cc8..d2a2fd008d 100644
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -1099,7 +1099,7 @@ class nsINode : public mozilla::dom::EventTarget {
: nullptr;
}
- enum FlattenedParentType { eNotForStyle, eForStyle };
+ enum FlattenedParentType { eNormal, eForStyle, eForSelection };
/**
* Returns the node that is the parent of this node in the flattened
@@ -1121,6 +1121,15 @@ class nsINode : public mozilla::dom::EventTarget {
*/
inline nsINode* GetFlattenedTreeParentNodeForStyle() const;
+ /**
+ * Similar to GetFlattenedTreeParentNode, it does two things differently
+ * 1. For contents that are not in the flattened tree, use its
+ * parent rather than nullptr.
+ * 2. For contents that are slotted into a UA shadow tree, use its
+ * parent rather than the slot element.
+ */
+ inline nsIContent* GetFlattenedTreeParentNodeForSelection() const;
+
inline mozilla::dom::Element* GetFlattenedTreeParentElement() const;
inline mozilla::dom::Element* GetFlattenedTreeParentElementForStyle() const;
@@ -1629,7 +1638,7 @@ class nsINode : public mozilla::dom::EventTarget {
* not in same subtree, this returns the root content of the closeset subtree.
*/
MOZ_CAN_RUN_SCRIPT nsIContent* GetSelectionRootContent(
- mozilla::PresShell* aPresShell);
+ mozilla::PresShell* aPresShell, bool aAllowCrossShadowBoundary = false);
nsINodeList* ChildNodes();
@@ -2074,6 +2083,14 @@ class nsINode : public mozilla::dom::EventTarget {
ClearBoolFlag(ElementCreatedFromPrototypeAndHasUnmodifiedL10n);
}
+ mozilla::dom::ShadowRoot* GetShadowRoot() const;
+
+ // Return the shadow root of the node if it is a shadow host and
+ // it meets the requirements for being a shadow host of a selection.
+ // For example, <details>, <video> and <use> elements are not valid
+ // shadow host for selection.
+ mozilla::dom::ShadowRoot* GetShadowRootForSelection() const;
+
protected:
void SetParentIsContent(bool aValue) { SetBoolFlag(ParentIsContent, aValue); }
void SetIsInDocument() { SetBoolFlag(IsInDocument); }
diff --git a/dom/base/nsIScriptableContentIterator.idl b/dom/base/nsIScriptableContentIterator.idl
index 370cd8c8a7..80208ac983 100644
--- a/dom/base/nsIScriptableContentIterator.idl
+++ b/dom/base/nsIScriptableContentIterator.idl
@@ -38,6 +38,11 @@ interface nsIScriptableContentIterator : nsISupports
void initWithRange(in nsIScriptableContentIterator_IteratorType aType,
in Range aRange);
+ // See ContentSubtreeIterator::InitWithAllowCrossShadowBoundary(nsRange*)
+ void initWithRangeAllowCrossShadowBoundary(
+ in nsIScriptableContentIterator_IteratorType aType,
+ in Range aRange);
+
// See ContentIteratorBase::Init(nsINode*, uint32_t, nsINode*, uint32_t)
void initWithPositions(in nsIScriptableContentIterator_IteratorType aType,
in Node aStartContainer, in unsigned long aStartOffset,
@@ -59,7 +64,7 @@ interface nsIScriptableContentIterator : nsISupports
readonly attribute Node currentNode;
// See ContentIteratorBase::IsDone()
- readonly attribute bool isDone;
+ readonly attribute boolean isDone;
// See ContentIteratorBase::PositionAt(nsINode*)
void positionAt(in Node aNode);
diff --git a/dom/base/nsISelectionController.idl b/dom/base/nsISelectionController.idl
index f2d5ebe65f..9c76e45f43 100644
--- a/dom/base/nsISelectionController.idl
+++ b/dom/base/nsISelectionController.idl
@@ -41,11 +41,12 @@ interface nsISelectionController : nsISelectionDisplay
const short SELECTION_FIND = 8;
const short SELECTION_URLSECONDARY = 9;
const short SELECTION_URLSTRIKEOUT = 10;
+ const short SELECTION_TARGET_TEXT = 11;
// Custom Highlight API
// (see https://drafts.csswg.org/css-highlight-api-1/#enumdef-highlighttype)
- const short SELECTION_HIGHLIGHT = 11;
+ const short SELECTION_HIGHLIGHT = 12;
// End of RawSelectionType values.
- const short NUM_SELECTIONTYPES = 12;
+ const short NUM_SELECTIONTYPES = 13;
// SelectionRegion values:
const short SELECTION_ANCHOR_REGION = 0;
@@ -311,6 +312,7 @@ enum class SelectionType : RawSelectionType
eFind = nsISelectionController::SELECTION_FIND,
eURLSecondary = nsISelectionController::SELECTION_URLSECONDARY,
eURLStrikeout = nsISelectionController::SELECTION_URLSTRIKEOUT,
+ eTargetText = nsISelectionController::SELECTION_TARGET_TEXT,
eHighlight = nsISelectionController::SELECTION_HIGHLIGHT,
};
@@ -327,6 +329,7 @@ static const SelectionType kPresentSelectionTypes[] = {
SelectionType::eFind,
SelectionType::eURLSecondary,
SelectionType::eURLStrikeout,
+ SelectionType::eTargetText,
SelectionType::eHighlight,
};
diff --git a/dom/base/nsJSEnvironment.cpp b/dom/base/nsJSEnvironment.cpp
index 1397bd25b5..a14a22bcf0 100644
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -1707,38 +1707,38 @@ void nsJSContext::MaybeRunNextCollectorSlice(nsIDocShell* aDocShell,
return;
}
- if (!sScheduler->IsUserActive()) {
- if (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles()) {
- Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint();
- if (next.isSome()) {
- // Try to not delay the next RefreshDriver tick, so give a reasonable
- // deadline for collectors.
- sScheduler->RunNextCollectorTimer(aReason, next.value());
- }
- } else {
- nsCOMPtr<nsIDocShell> shell = aDocShell;
- NS_DispatchToCurrentThreadQueue(
- NS_NewRunnableFunction(
- "nsJSContext::MaybeRunNextCollectorSlice",
- [shell] {
- nsIDocShell::BusyFlags busyFlags = nsIDocShell::BUSY_FLAGS_NONE;
- shell->GetBusyFlags(&busyFlags);
- if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) {
- return;
- }
-
- // In order to improve performance on the next page, run a minor
- // GC. The 16ms limit ensures it isn't called all the time if
- // there are for example multiple iframes loading at the same
- // time.
- JS::RunNurseryCollection(
- CycleCollectedJSRuntime::Get()->Runtime(),
- JS::GCReason::PREPARE_FOR_PAGELOAD,
- mozilla::TimeDuration::FromMilliseconds(16));
- }),
- EventQueuePriority::Idle);
+ if (!sScheduler->IsUserActive() &&
+ (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles())) {
+ Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint();
+ if (next.isSome()) {
+ // Try to not delay the next RefreshDriver tick, so give a reasonable
+ // deadline for collectors.
+ sScheduler->RunNextCollectorTimer(aReason, next.value());
}
}
+
+ nsCOMPtr<nsIDocShell> shell = aDocShell;
+ NS_DispatchToCurrentThreadQueue(
+ NS_NewRunnableFunction("nsJSContext::MaybeRunNextCollectorSlice",
+ [shell] {
+ nsIDocShell::BusyFlags busyFlags =
+ nsIDocShell::BUSY_FLAGS_NONE;
+ shell->GetBusyFlags(&busyFlags);
+ if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) {
+ return;
+ }
+
+ // In order to improve performance on the next
+ // page, run a minor GC. The 16ms limit ensures
+ // it isn't called all the time if there are for
+ // example multiple iframes loading at the same
+ // time.
+ JS::RunNurseryCollection(
+ CycleCollectedJSRuntime::Get()->Runtime(),
+ JS::GCReason::PREPARE_FOR_PAGELOAD,
+ mozilla::TimeDuration::FromMilliseconds(16));
+ }),
+ EventQueuePriority::Idle);
}
// static
@@ -2121,6 +2121,13 @@ void nsJSContext::EnsureStatics() {
"javascript.options.mem.gc_compacting",
(void*)JSGC_COMPACTING_ENABLED);
+#ifdef NIGHTLY_BUILD
+ Preferences::RegisterCallbackAndCall(
+ SetMemoryPrefChangedCallbackBool,
+ "javascript.options.mem.gc_experimental_semispace_nursery",
+ (void*)JSGC_SEMISPACE_NURSERY_ENABLED);
+#endif
+
Preferences::RegisterCallbackAndCall(
SetMemoryPrefChangedCallbackBool,
"javascript.options.mem.gc_parallel_marking",
diff --git a/dom/base/nsMimeTypeArray.cpp b/dom/base/nsMimeTypeArray.cpp
index 4d15e1d8a2..376c7e1065 100644
--- a/dom/base/nsMimeTypeArray.cpp
+++ b/dom/base/nsMimeTypeArray.cpp
@@ -94,3 +94,7 @@ JSObject* nsMimeType::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return MimeType_Binding::Wrap(aCx, this, aGivenProto);
}
+
+already_AddRefed<nsPluginElement> nsMimeType::EnabledPlugin() const {
+ return do_AddRef(mPluginElement);
+}
diff --git a/dom/base/nsMimeTypeArray.h b/dom/base/nsMimeTypeArray.h
index aaeedba3ae..207f5dc542 100644
--- a/dom/base/nsMimeTypeArray.h
+++ b/dom/base/nsMimeTypeArray.h
@@ -83,9 +83,7 @@ class nsMimeType final : public nsWrapperCache {
retval.SetKnownLiveString(kMimeDescription);
}
- already_AddRefed<nsPluginElement> EnabledPlugin() const {
- return do_AddRef(mPluginElement);
- }
+ already_AddRefed<nsPluginElement> EnabledPlugin() const;
void GetSuffixes(mozilla::dom::DOMString& retval) const {
retval.SetKnownLiveString(kMimeSuffix);
diff --git a/dom/base/nsRange.cpp b/dom/base/nsRange.cpp
index 3121ed86c3..cf15f239c5 100644
--- a/dom/base/nsRange.cpp
+++ b/dom/base/nsRange.cpp
@@ -105,16 +105,30 @@ template nsresult nsRange::SetStartAndEnd(
template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary,
const RangeBoundary& aEndBoundary,
- nsINode* aRootNode, bool aNotInsertedYet);
+ nsINode* aRootNode, bool aNotInsertedYet,
+ CollapsePolicy aCollapsePolicy);
template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary,
const RawRangeBoundary& aEndBoundary,
- nsINode* aRootNode, bool aNotInsertedYet);
+ nsINode* aRootNode, bool aNotInsertedYet,
+ CollapsePolicy aCollapsePolicy);
template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary,
const RangeBoundary& aEndBoundary,
- nsINode* aRootNode, bool aNotInsertedYet);
+ nsINode* aRootNode, bool aNotInsertedYet,
+ CollapsePolicy aCollapsePolicy);
template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary,
const RawRangeBoundary& aEndBoundary,
- nsINode* aRootNode, bool aNotInsertedYet);
+ nsINode* aRootNode, bool aNotInsertedYet,
+ CollapsePolicy aCollapsePolicy);
+
+template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary);
+template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const RangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary);
+template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const RawRangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary);
+template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const RawRangeBoundary& aStartBoundary,
+ const RawRangeBoundary& aEndBoundary);
JSObject* nsRange::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
@@ -171,7 +185,7 @@ nsRange::nsRange(nsINode* aNode)
mNextEndRef(nullptr) {
// printf("Size of nsRange: %zu\n", sizeof(nsRange));
- static_assert(sizeof(nsRange) <= 240,
+ static_assert(sizeof(nsRange) <= 248,
"nsRange size shouldn't be increased as far as possible");
}
@@ -201,6 +215,109 @@ already_AddRefed<nsRange> nsRange::Create(
return range.forget();
}
+/*
+ * When a new boundary is given to a nsRange, compare its position with other
+ * existing boundaries to see if we need to collapse the end points.
+ *
+ * aRange: The nsRange that aNewBoundary is being set to.
+ * aNewRoot: The shadow-including root of the container of aNewBoundary
+ * aNewBoundary: The new boundary
+ * aIsSetStart: true if ShouldCollapseBoundary is called by nsRange::SetStart,
+ * false otherwise
+ * aAllowCrossShadowBoundary: Indicates whether the boundaries allowed to cross
+ * shadow boundary or not
+ */
+static CollapsePolicy ShouldCollapseBoundary(
+ const nsRange* aRange, const nsINode* aNewRoot,
+ const RawRangeBoundary& aNewBoundary, const bool aIsSetStart,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
+ if (!aRange->IsPositioned()) {
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ MOZ_ASSERT(aRange->GetRoot());
+ if (aNewRoot != aRange->GetRoot()) {
+ // Boundaries are in different document (or not connected), so collapse
+ // the both the default range and the crossBoundaryRange range.
+ if (aNewRoot->GetComposedDoc() != aRange->GetRoot()->GetComposedDoc()) {
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ // Always collapse both ranges if the one of the roots is an UA widget
+ // regardless whether the boundaries are allowed to cross shadow boundary
+ // or not.
+ if (AbstractRange::IsRootUAWidget(aNewRoot) ||
+ AbstractRange::IsRootUAWidget(aRange->GetRoot())) {
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ // Different root, but same document. So we only collapse the
+ // default range if boundaries are allowed to cross shadow boundary.
+ return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
+ ? CollapsePolicy::DefaultRange
+ : CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ const RangeBoundary& otherSideExistingBoundary =
+ aIsSetStart ? aRange->EndRef() : aRange->StartRef();
+
+ // Both bondaries are in the same root, now check for their position
+ const Maybe<int32_t> order =
+ aIsSetStart ? nsContentUtils::ComparePoints(aNewBoundary,
+ otherSideExistingBoundary)
+ : nsContentUtils::ComparePoints(otherSideExistingBoundary,
+ aNewBoundary);
+
+ if (order) {
+ if (*order != 1) {
+ // aNewBoundary is at a valid position.
+ //
+ // If aIsSetStart is true, this means
+ // aNewBoundary <= otherSideExistingBoundary which is
+ // good because aNewBoundary intends to be the start.
+ //
+ // If aIsSetStart is false, this means
+ // otherSideExistingBoundary <= aNewBoundary which is good because
+ // aNewBoundary intends to be the end.
+ //
+ // So no collapse for above cases.
+ return CollapsePolicy::No;
+ }
+
+ if (!aRange->MayCrossShadowBoundary() ||
+ aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::No) {
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ const RangeBoundary& otherSideExistingCrossShadowBoundaryBoundary =
+ aIsSetStart ? aRange->MayCrossShadowBoundaryEndRef()
+ : aRange->MayCrossShadowBoundaryStartRef();
+
+ // Please see the comment for (*order != 1) to see what "valid" means.
+ //
+ // We reach to this line when (*order == 1), it means aNewBoundary is
+ // at an invalid position, so we need to collapse aNewBoundary with
+ // otherSideExistingBoundary. However, it's possible that aNewBoundary
+ // is valid with the otherSideExistingCrossShadowBoundaryBoundary.
+ const Maybe<int32_t> withCrossShadowBoundaryOrder =
+ aIsSetStart
+ ? nsContentUtils::ComparePoints(
+ aNewBoundary, otherSideExistingCrossShadowBoundaryBoundary)
+ : nsContentUtils::ComparePoints(
+ otherSideExistingCrossShadowBoundaryBoundary, aNewBoundary);
+
+ // Valid to the cross boundary boundary.
+ if (withCrossShadowBoundaryOrder && *withCrossShadowBoundaryOrder != 1) {
+ return CollapsePolicy::DefaultRange;
+ }
+
+ // Not valid to both existing boundaries.
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+ }
+
+ MOZ_ASSERT_UNREACHABLE();
+ return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges;
+}
/******************************************************
* nsISupports
******************************************************/
@@ -219,11 +336,13 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange)
// `Reset()` unlinks `mStart`, `mEnd` and `mRoot`.
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrossShadowBoundaryRange);
tmp->Reset();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrossShadowBoundaryRange);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange)
@@ -231,6 +350,7 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_END
bool nsRange::MaybeInterruptLastRelease() {
bool interrupt = AbstractRange::MaybeCacheToReuse(*this);
+ ResetCrossShadowBoundaryRange();
MOZ_ASSERT(!interrupt || IsCleared());
return interrupt;
}
@@ -573,6 +693,21 @@ void nsRange::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) {
nsINode* startContainer = mStart.Container();
nsINode* endContainer = mEnd.Container();
+ // FIXME(sefeng): Temporary Solution for ContentRemoved
+ // editing/crashtests/removeformat-from-DOMNodeRemoved.html can be used to
+ // verify this.
+ if (mCrossShadowBoundaryRange) {
+ if (mCrossShadowBoundaryRange->GetStartContainer() == aChild ||
+ mCrossShadowBoundaryRange->GetEndContainer() == aChild) {
+ ResetCrossShadowBoundaryRange();
+ } else if (ShadowRoot* shadowRoot = aChild->GetShadowRoot()) {
+ if (mCrossShadowBoundaryRange->GetStartContainer() == shadowRoot ||
+ mCrossShadowBoundaryRange->GetEndContainer() == shadowRoot) {
+ ResetCrossShadowBoundaryRange();
+ }
+ }
+ }
+
RawRangeBoundary newStart;
RawRangeBoundary newEnd;
Maybe<bool> gravitateStart;
@@ -881,10 +1016,12 @@ void nsRange::AssertIfMismatchRootAndRangeBoundaries(
// Calling DoSetRange with either parent argument null will collapse
// the range to have both endpoints point to the other node
template <typename SPT, typename SRT, typename EPT, typename ERT>
-void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
- const RangeBoundaryBase<EPT, ERT>& aEndBoundary,
- nsINode* aRootNode,
- bool aNotInsertedYet /* = false */) {
+void nsRange::DoSetRange(
+ const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
+ const RangeBoundaryBase<EPT, ERT>& aEndBoundary, nsINode* aRootNode,
+ bool aNotInsertedYet /* = false */,
+ CollapsePolicy
+ aCollapsePolicy /* = DEFAULT_RANGE_AND_CROSS_BOUNDARY_RANGES */) {
mIsPositioned = aStartBoundary.IsSetAndValid() &&
aEndBoundary.IsSetAndValid() && aRootNode;
MOZ_ASSERT_IF(!mIsPositioned, !aStartBoundary.IsSet());
@@ -913,6 +1050,11 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
mStart.CopyFrom(aStartBoundary, RangeBoundaryIsMutationObserved::Yes);
mEnd.CopyFrom(aEndBoundary, RangeBoundaryIsMutationObserved::Yes);
+ if (aCollapsePolicy ==
+ CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges) {
+ ResetCrossShadowBoundaryRange();
+ }
+
if (checkCommonAncestor) {
UpdateCommonAncestorIfNecessary();
}
@@ -965,17 +1107,21 @@ bool nsRange::CanAccess(const nsINode& aNode) const {
return nsContentUtils::CanCallerAccess(&aNode);
}
-void nsRange::SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) {
+void nsRange::SetStart(
+ nsINode& aNode, uint32_t aOffset, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
if (!CanAccess(aNode)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return;
}
AutoInvalidateSelection atEndOfBlock(this);
- SetStart(RawRangeBoundary(&aNode, aOffset), aRv);
+ SetStart(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary);
}
-void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) {
+void nsRange::SetStart(
+ const RawRangeBoundary& aPoint, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container());
if (!newRoot) {
aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR);
@@ -987,28 +1133,46 @@ void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) {
return;
}
- // Collapse if not positioned yet, if positioned in another doc or
- // if the new start is after end.
- const bool collapse = [&]() {
- if (!mIsPositioned || (newRoot != mRoot)) {
- return true;
- }
-
- const Maybe<int32_t> order = nsContentUtils::ComparePoints(aPoint, mEnd);
- if (order) {
- return *order == 1;
- }
-
- MOZ_ASSERT_UNREACHABLE();
- return true;
- }();
+ CollapsePolicy policy =
+ ShouldCollapseBoundary(this, newRoot, aPoint, true /* aIsSetStart= */,
+ aAllowCrossShadowBoundary);
- if (collapse) {
- DoSetRange(aPoint, aPoint, newRoot);
- return;
+ switch (policy) {
+ case CollapsePolicy::No:
+ // EndRef(..) may be same as mStart or not, depends on
+ // the value of mCrossShadowBoundaryRange->mEnd, We need to update
+ // mCrossShadowBoundaryRange and the default boundaries separately
+ if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) {
+ if (MayCrossShadowBoundaryEndRef() != mEnd) {
+ CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ aPoint, MayCrossShadowBoundaryEndRef());
+ } else {
+ // The normal range is good enough for this case, just use that.
+ ResetCrossShadowBoundaryRange();
+ }
+ }
+ DoSetRange(aPoint, mEnd, mRoot, false, policy);
+ break;
+ case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges:
+ DoSetRange(aPoint, aPoint, newRoot, false, policy);
+ break;
+ case CollapsePolicy::DefaultRange:
+ MOZ_ASSERT(aAllowCrossShadowBoundary ==
+ AllowRangeCrossShadowBoundary::Yes);
+ CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ aPoint, MayCrossShadowBoundaryEndRef());
+ DoSetRange(aPoint, aPoint, newRoot, false, policy);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE();
}
+}
- DoSetRange(aPoint, mEnd, mRoot);
+void nsRange::SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset,
+ ErrorResult& aErr) {
+ AutoCalledByJSRestore calledByJSRestorer(*this);
+ mCalledByJS = true;
+ SetStart(aNode, aOffset, aErr, AllowRangeCrossShadowBoundary::Yes);
}
void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) {
@@ -1017,7 +1181,9 @@ void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) {
SetStartBefore(aNode, aErr);
}
-void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) {
+void nsRange::SetStartBefore(
+ nsINode& aNode, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
if (!CanAccess(aNode)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return;
@@ -1027,7 +1193,8 @@ void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) {
// If the node is being removed from its parent, GetRawRangeBoundaryBefore()
// returns unset instance. Then, SetStart() will throw
// NS_ERROR_DOM_INVALID_NODE_TYPE_ERR.
- SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv);
+ SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv,
+ aAllowCrossShadowBoundary);
}
void nsRange::SetStartAfterJS(nsINode& aNode, ErrorResult& aErr) {
@@ -1055,16 +1222,18 @@ void nsRange::SetEndJS(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr) {
SetEnd(aNode, aOffset, aErr);
}
-void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) {
+void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
if (!CanAccess(aNode)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return;
}
AutoInvalidateSelection atEndOfBlock(this);
- SetEnd(RawRangeBoundary(&aNode, aOffset), aRv);
+ SetEnd(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary);
}
-void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) {
+void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container());
if (!newRoot) {
aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR);
@@ -1076,28 +1245,47 @@ void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) {
return;
}
- // Collapse if not positioned yet, if positioned in another doc or
- // if the new end is before start.
- const bool collapse = [&]() {
- if (!mIsPositioned || (newRoot != mRoot)) {
- return true;
- }
+ CollapsePolicy policy =
+ ShouldCollapseBoundary(this, newRoot, aPoint, false /* aIsStartStart */,
+ aAllowCrossShadowBoundary);
- const Maybe<int32_t> order = nsContentUtils::ComparePoints(mStart, aPoint);
- if (order) {
- return *order == 1;
- }
-
- MOZ_ASSERT_UNREACHABLE();
- return true;
- }();
-
- if (collapse) {
- DoSetRange(aPoint, aPoint, newRoot);
- return;
+ switch (policy) {
+ case CollapsePolicy::No:
+ // StartRef(..) may be same as mStart or not, depends on
+ // the value of mCrossShadowBoundaryRange->mStart, so we need to update
+ // mCrossShadowBoundaryRange and the default boundaries separately
+ if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) {
+ if (MayCrossShadowBoundaryStartRef() != mStart) {
+ CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ MayCrossShadowBoundaryStartRef(), aPoint);
+ } else {
+ // The normal range is good enough for this case, just use that.
+ ResetCrossShadowBoundaryRange();
+ }
+ }
+ DoSetRange(mStart, aPoint, mRoot, false, policy);
+ break;
+ case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges:
+ DoSetRange(aPoint, aPoint, newRoot, false, policy);
+ break;
+ case CollapsePolicy::DefaultRange:
+ MOZ_ASSERT(aAllowCrossShadowBoundary ==
+ AllowRangeCrossShadowBoundary::Yes);
+ CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ MayCrossShadowBoundaryStartRef(), aPoint);
+ DoSetRange(aPoint, aPoint, newRoot, false, policy);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE();
}
+}
- DoSetRange(mStart, aPoint, mRoot);
+void nsRange::SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset,
+ ErrorResult& aErr) {
+ AutoCalledByJSRestore calledByJSRestorer(*this);
+ mCalledByJS = true;
+ SetEnd(aNode, aOffset, aErr,
+ AllowRangeCrossShadowBoundary::Yes /* aAllowCrossShadowBoundary */);
}
void nsRange::SelectNodesInContainer(nsINode* aContainer,
@@ -1127,7 +1315,9 @@ void nsRange::SetEndBeforeJS(nsINode& aNode, ErrorResult& aErr) {
SetEndBefore(aNode, aErr);
}
-void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) {
+void nsRange::SetEndBefore(
+ nsINode& aNode, ErrorResult& aRv,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
if (!CanAccess(aNode)) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
return;
@@ -1137,7 +1327,8 @@ void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) {
// If the node is being removed from its parent, GetRawRangeBoundaryBefore()
// returns unset instance. Then, SetEnd() will throw
// NS_ERROR_DOM_INVALID_NODE_TYPE_ERR.
- SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv);
+ SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv,
+ aAllowCrossShadowBoundary);
}
void nsRange::SetEndAfterJS(nsINode& aNode, ErrorResult& aErr) {
@@ -2201,6 +2392,11 @@ already_AddRefed<DocumentFragment> nsRange::CloneContents(ErrorResult& aRv) {
already_AddRefed<nsRange> nsRange::CloneRange() const {
RefPtr<nsRange> range = nsRange::Create(mOwner);
range->DoSetRange(mStart, mEnd, mRoot);
+ if (mCrossShadowBoundaryRange) {
+ range->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ mCrossShadowBoundaryRange->StartRef(),
+ mCrossShadowBoundaryRange->EndRef());
+ }
return range.forget();
}
@@ -2640,13 +2836,14 @@ static nsresult GetPartialTextRect(RectCallback* aCallback,
static void CollectClientRectsForSubtree(
nsINode* aNode, RectCallback* aCollector, Sequence<nsString>* aTextList,
nsINode* aStartContainer, uint32_t aStartOffset, nsINode* aEndContainer,
- uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout) {
+ uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout, bool aTextOnly) {
auto* content = nsIContent::FromNode(aNode);
if (!content) {
return;
}
- if (content->IsText()) {
+ const bool isText = content->IsText();
+ if (isText) {
if (aNode == aStartContainer) {
int32_t offset = aStartContainer == aEndContainer
? static_cast<int32_t>(aEndOffset)
@@ -2665,19 +2862,30 @@ static void CollectClientRectsForSubtree(
}
}
- if (aNode->IsElement() && aNode->AsElement()->IsDisplayContents()) {
- FlattenedChildIterator childIter(content);
-
- for (nsIContent* child = childIter.GetNextChild(); child;
- child = childIter.GetNextChild()) {
- CollectClientRectsForSubtree(child, aCollector, aTextList,
- aStartContainer, aStartOffset, aEndContainer,
- aEndOffset, aClampToEdge, aFlushLayout);
+ if (nsIFrame* frame = content->GetPrimaryFrame()) {
+ if (!aTextOnly || isText) {
+ nsLayoutUtils::GetAllInFlowRectsAndTexts(
+ frame, nsLayoutUtils::GetContainingBlockForClientRect(frame),
+ aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+ if (isText) {
+ return;
+ }
+ aTextOnly = true;
+ // We just get the text when calling GetAllInFlowRectsAndTexts, so we
+ // don't need to call it again when visiting the children.
+ aTextList = nullptr;
}
- } else if (nsIFrame* frame = content->GetPrimaryFrame()) {
- nsLayoutUtils::GetAllInFlowRectsAndTexts(
- frame, nsLayoutUtils::GetContainingBlockForClientRect(frame),
- aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+ } else if (!content->IsElement() ||
+ !content->AsElement()->IsDisplayContents()) {
+ return;
+ }
+
+ FlattenedChildIterator childIter(content);
+ for (nsIContent* child = childIter.GetNextChild(); child;
+ child = childIter.GetNextChild()) {
+ CollectClientRectsForSubtree(child, aCollector, aTextList, aStartContainer,
+ aStartOffset, aEndContainer, aEndOffset,
+ aClampToEdge, aFlushLayout, aTextOnly);
}
}
@@ -2745,7 +2953,7 @@ void nsRange::CollectClientRectsAndText(
CollectClientRectsForSubtree(node, aCollector, aTextList, aStartContainer,
aStartOffset, aEndContainer, aEndOffset,
- aClampToEdge, aFlushLayout);
+ aClampToEdge, aFlushLayout, false);
} while (!iter.IsDone());
}
@@ -2994,7 +3202,7 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) {
// This is the initial range and all its nodes until now are
// non-selectable so just trim them from the start.
IgnoredErrorResult err;
- range->SetStartBefore(*node, err);
+ range->SetStartBefore(*node, err, AllowRangeCrossShadowBoundary::Yes);
if (err.Failed()) {
return;
}
@@ -3008,7 +3216,8 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) {
// Truncate the current range before the first non-selectable node.
IgnoredErrorResult err;
- range->SetEndBefore(*firstNonSelectableContent, err);
+ range->SetEndBefore(*firstNonSelectableContent, err,
+ AllowRangeCrossShadowBoundary::Yes);
// Store it in the result (strong ref) - do this before creating
// a new range in |newRange| below so we don't drop the last ref
@@ -3252,3 +3461,52 @@ void nsRange::GetInnerTextNoFlush(DOMString& aValue, ErrorResult& aError,
// Do not flush trailing line breaks! Required breaks at the end of the text
// are suppressed.
}
+
+template <typename SPT, typename SRT, typename EPT, typename ERT>
+void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary,
+ const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary) {
+ if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
+ return;
+ }
+
+ MOZ_ASSERT(aStartBoundary.IsSetAndValid() && aEndBoundary.IsSetAndValid());
+
+ nsINode* startNode = aStartBoundary.Container();
+ nsINode* endNode = aEndBoundary.Container();
+
+ if (!startNode && !endNode) {
+ ResetCrossShadowBoundaryRange();
+ return;
+ }
+
+ auto CanBecomeCrossShadowBoundaryPoint = [](nsINode* aContainer) -> bool {
+ if (!aContainer) {
+ return true;
+ }
+
+ // Unlike normal ranges, shadow cross ranges don't work
+ // when the nodes aren't in document.
+ if (!aContainer->IsInComposedDoc()) {
+ return false;
+ }
+
+ // AbstractRange::GetClosestCommonInclusiveAncestor only supports
+ // Document and Content nodes.
+ return aContainer->IsDocument() || aContainer->IsContent();
+ };
+
+ if (!CanBecomeCrossShadowBoundaryPoint(startNode) ||
+ !CanBecomeCrossShadowBoundaryPoint(endNode)) {
+ ResetCrossShadowBoundaryRange();
+ return;
+ }
+
+ if (!mCrossShadowBoundaryRange) {
+ mCrossShadowBoundaryRange =
+ StaticRange::Create(aStartBoundary, aEndBoundary, IgnoreErrors());
+ return;
+ }
+
+ mCrossShadowBoundaryRange->SetStartAndEnd(aStartBoundary, aEndBoundary);
+}
diff --git a/dom/base/nsRange.h b/dom/base/nsRange.h
index 97756d3afc..94459087cb 100644
--- a/dom/base/nsRange.h
+++ b/dom/base/nsRange.h
@@ -13,6 +13,7 @@
#include "nsCOMPtr.h"
#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/dom/StaticRange.h"
#include "prmon.h"
#include "nsStubMutationObserver.h"
#include "nsWrapperCache.h"
@@ -31,6 +32,13 @@ class DOMRect;
class DOMRectList;
class InspectorFontFace;
class Selection;
+
+enum class CollapsePolicy : uint8_t {
+ No, // Don't need to collapse
+ DefaultRange, // Collapse the default range
+ DefaultRangeAndCrossShadowBoundaryRanges // Collapse both the default range
+ // and the cross boundary range
+};
} // namespace dom
} // namespace mozilla
@@ -43,6 +51,8 @@ class nsRange final : public mozilla::dom::AbstractRange,
using DOMRectList = mozilla::dom::DOMRectList;
using RangeBoundary = mozilla::RangeBoundary;
using RawRangeBoundary = mozilla::RawRangeBoundary;
+ using AllowRangeCrossShadowBoundary =
+ mozilla::dom::AllowRangeCrossShadowBoundary;
virtual ~nsRange();
explicit nsRange(nsINode* aNode);
@@ -111,14 +121,20 @@ class nsRange final : public mozilla::dom::AbstractRange,
* When you set both start and end of a range, you should use
* SetStartAndEnd() instead.
*/
- nsresult SetStart(nsINode* aContainer, uint32_t aOffset) {
+ nsresult SetStart(nsINode* aContainer, uint32_t aOffset,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No) {
ErrorResult error;
- SetStart(RawRangeBoundary(aContainer, aOffset), error);
+ SetStart(RawRangeBoundary(aContainer, aOffset), error,
+ aAllowCrossShadowBoundary);
return error.StealNSResult();
}
- nsresult SetEnd(nsINode* aContainer, uint32_t aOffset) {
+ nsresult SetEnd(nsINode* aContainer, uint32_t aOffset,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No) {
ErrorResult error;
- SetEnd(RawRangeBoundary(aContainer, aOffset), error);
+ SetEnd(RawRangeBoundary(aContainer, aOffset), error,
+ aAllowCrossShadowBoundary);
return error.StealNSResult();
}
@@ -224,6 +240,11 @@ class nsRange final : public mozilla::dom::AbstractRange,
void SetStartAfterJS(nsINode& aNode, ErrorResult& aErr);
void SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr);
+ void SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset,
+ ErrorResult& aErr);
+ void SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset,
+ ErrorResult& aErr);
+
void SurroundContents(nsINode& aNode, ErrorResult& aErr);
already_AddRefed<DOMRect> GetBoundingClientRect(bool aClampToEdge = true,
bool aFlushLayout = true);
@@ -235,14 +256,26 @@ class nsRange final : public mozilla::dom::AbstractRange,
// Following methods should be used for internal use instead of *JS().
void SelectNode(nsINode& aNode, ErrorResult& aErr);
void SelectNodeContents(nsINode& aNode, ErrorResult& aErr);
- void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr);
- void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr);
+ void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
+ void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
void SetEndAfter(nsINode& aNode, ErrorResult& aErr);
- void SetEndBefore(nsINode& aNode, ErrorResult& aErr);
- void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr);
- void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr);
+ void SetEndBefore(nsINode& aNode, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
+ void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
+ void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
void SetStartAfter(nsINode& aNode, ErrorResult& aErr);
- void SetStartBefore(nsINode& aNode, ErrorResult& aErr);
+ void SetStartBefore(nsINode& aNode, ErrorResult& aErr,
+ AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary =
+ AllowRangeCrossShadowBoundary::No);
void Collapse(bool aToStart);
static void GetInnerTextNoFlush(mozilla::dom::DOMString& aValue,
@@ -351,6 +384,82 @@ class nsRange final : public mozilla::dom::AbstractRange,
*/
nsINode* GetRegisteredClosestCommonInclusiveAncestor();
+ template <typename SPT, typename SRT, typename EPT, typename ERT>
+ void CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(
+ const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary,
+ const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary);
+
+ void ResetCrossShadowBoundaryRange() { mCrossShadowBoundaryRange = nullptr; }
+
+#ifdef DEBUG
+ bool CrossShadowBoundaryRangeCollapsed() const {
+ MOZ_ASSERT(mCrossShadowBoundaryRange);
+
+ return !mCrossShadowBoundaryRange->IsPositioned() ||
+ (mCrossShadowBoundaryRange->GetStartContainer() ==
+ mCrossShadowBoundaryRange->GetEndContainer() &&
+ mCrossShadowBoundaryRange->StartOffset() ==
+ mCrossShadowBoundaryRange->EndOffset());
+ }
+#endif
+
+ /*
+ * The methods marked with MayCrossShadowBoundary[..] additionally check for
+ * the existence of mCrossShadowBoundaryRange, which indicates a range that
+ * crosses a shadow DOM boundary (i.e. mStart and mEnd are in different
+ * trees). If the caller can guarantee that this does not happen, there are
+ * additional variants of these methods named without MayCrossShadowBoundary,
+ * which provide a slightly faster implementation.
+ * */
+
+ nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const {
+ return mCrossShadowBoundaryRange
+ ? mCrossShadowBoundaryRange->GetChildAtStartOffset()
+ : mStart.GetChildAtOffset();
+ }
+
+ nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const {
+ return mCrossShadowBoundaryRange
+ ? mCrossShadowBoundaryRange->GetChildAtEndOffset()
+ : mEnd.GetChildAtOffset();
+ }
+
+ mozilla::dom::StaticRange* GetCrossShadowBoundaryRange() const {
+ return mCrossShadowBoundaryRange;
+ }
+
+ nsINode* GetMayCrossShadowBoundaryStartContainer() const {
+ return mCrossShadowBoundaryRange
+ ? mCrossShadowBoundaryRange->GetStartContainer()
+ : mStart.Container();
+ }
+
+ nsINode* GetMayCrossShadowBoundaryEndContainer() const {
+ return mCrossShadowBoundaryRange
+ ? mCrossShadowBoundaryRange->GetEndContainer()
+ : mEnd.Container();
+ }
+
+ uint32_t MayCrossShadowBoundaryStartOffset() const {
+ return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartOffset()
+ : StartOffset();
+ }
+
+ uint32_t MayCrossShadowBoundaryEndOffset() const {
+ return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndOffset()
+ : EndOffset();
+ }
+
+ const RangeBoundary& MayCrossShadowBoundaryStartRef() const {
+ return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartRef()
+ : StartRef();
+ }
+
+ const RangeBoundary& MayCrossShadowBoundaryEndRef() const {
+ return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndRef()
+ : EndRef();
+ }
+
protected:
/**
* DoSetRange() is called when `AbstractRange::SetStartAndEndInternal()` sets
@@ -372,7 +481,9 @@ class nsRange final : public mozilla::dom::AbstractRange,
MOZ_CAN_RUN_SCRIPT_BOUNDARY void DoSetRange(
const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary,
const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary,
- nsINode* aRootNode, bool aNotInsertedYet = false);
+ nsINode* aRootNode, bool aNotInsertedYet = false,
+ mozilla::dom::CollapsePolicy aCollapsePolicy = mozilla::dom::
+ CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges);
// Assume that this is guaranteed that this is held by the caller when
// this is used. (Note that we cannot use AutoRestore for mCalledByJS
@@ -424,6 +535,22 @@ class nsRange final : public mozilla::dom::AbstractRange,
static nsTArray<RefPtr<nsRange>>* sCachedRanges;
+ // Used to keep track of the real start and end for a
+ // selection where the start and the end are in different trees.
+ // It's NULL when the nodes are in the same tree.
+ //
+ // mCrossShadowBoundaryRange doesn't deal with DOM mutations, because
+ // it's still an open question about how it should be handled.
+ // Spec: https://github.com/w3c/selection-api/issues/168.
+ // As a result, it'll be set to NULL if that happens.
+ //
+ // Theoretically, mCrossShadowBoundaryRange isn't really needed because
+ // we should be able to always store the real start and end, and
+ // just return one point when a collapse is needed.
+ // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1886028 is going
+ // to be used to improve mCrossShadowBoundaryRange.
+ RefPtr<mozilla::dom::StaticRange> mCrossShadowBoundaryRange;
+
friend class mozilla::dom::AbstractRange;
};
namespace mozilla::dom {
diff --git a/dom/base/nsWrapperCache.h b/dom/base/nsWrapperCache.h
index f1c4e17fa3..4410b64ddf 100644
--- a/dom/base/nsWrapperCache.h
+++ b/dom/base/nsWrapperCache.h
@@ -12,6 +12,7 @@
#include "mozilla/ServoUtils.h"
#include "mozilla/RustCell.h"
#include "js/HeapAPI.h"
+#include "js/RootingAPI.h"
#include "js/TracingAPI.h"
#include "js/TypeDecls.h"
#include "nsISupports.h"
@@ -196,6 +197,11 @@ class JS_HAZ_ROOTED nsWrapperCache {
if (mWrapper) {
MOZ_ASSERT(mWrapper == aOldObject);
mWrapper = aNewObject;
+ if (PreservingWrapper() && !JS::ObjectIsTenured(mWrapper)) {
+ // Can pass prevp as null here since a previous store buffer entry has
+ // been cleared by the current nursery collection.
+ JS::HeapObjectPostWriteBarrier(&mWrapper, nullptr, mWrapper);
+ }
}
}
diff --git a/dom/base/rust/lib.rs b/dom/base/rust/lib.rs
index 1c155e062b..467eeeb832 100644
--- a/dom/base/rust/lib.rs
+++ b/dom/base/rust/lib.rs
@@ -156,8 +156,6 @@ bitflags! {
const RTL_LOCALE = 1 << 1;
/// LTR locale: specific to the XUL localedir attribute
const LTR_LOCALE = 1 << 2;
- /// LWTheme status
- const LWTHEME = 1 << 3;
const ALL_LOCALEDIR_BITS = Self::LTR_LOCALE.bits() | Self::RTL_LOCALE.bits();
}
diff --git a/dom/base/test/browser.toml b/dom/base/test/browser.toml
index a68bd2e873..04eca5d1aa 100644
--- a/dom/base/test/browser.toml
+++ b/dom/base/test/browser.toml
@@ -104,7 +104,7 @@ skip-if = ["fission"] # Fails with Fission, and we're unlikely to spend time to
["browser_multiple_popups.js"]
skip-if = [
"os == 'win' && !debug", # Bug 1505235
- "os == 'mac' && !debug", # Bug 1661132 (osx)
+ "apple_catalina", # Bug 1661132 (osx), Bug 1866073
"socketprocess_networking",
]
support-files = ["browser_multiple_popups.html"]
diff --git a/dom/base/test/browser_aboutnewtab_process_selection.js b/dom/base/test/browser_aboutnewtab_process_selection.js
index ad59077105..db0e80acdf 100644
--- a/dom/base/test/browser_aboutnewtab_process_selection.js
+++ b/dom/base/test/browser_aboutnewtab_process_selection.js
@@ -33,7 +33,7 @@ add_task(async function () {
// Open 3 tabs using the preloaded browser.
let tabs = [];
for (let i = 0; i < 3; i++) {
- BrowserOpenTab();
+ BrowserCommands.openTab();
tabs.unshift(gBrowser.selectedTab);
await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser);
@@ -114,7 +114,7 @@ add_task(async function preloaded_state_attribute() {
"Sanity check that the first preloaded browser has the correct attribute"
);
- BrowserOpenTab();
+ BrowserCommands.openTab();
await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser);
// Now check that the tabs have the correct browser attributes set
diff --git a/dom/base/test/file_window_close.html b/dom/base/test/file_window_close.html
index 5adec04ec4..b3bb20d499 100644
--- a/dom/base/test/file_window_close.html
+++ b/dom/base/test/file_window_close.html
@@ -13,7 +13,7 @@
if (history.length == 4) {
// We're coming back from history.
function listener(m) {
- if (m.message.includes("Scripts may not close windows that were not opened by script.")) {
+ if (m.message.includes("Scripts may only close windows that were opened by a script.")) {
SpecialPowers.postConsoleSentinel();
SpecialPowers.pushPrefEnv({ set: [["dom.allow_scripts_to_close_windows", true]]}).then(
function() {
diff --git a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js
index 2aef23b042..6764ff2009 100644
--- a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js
+++ b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js
@@ -52,7 +52,7 @@ async function waitAndCheckFullscreenState(aWindow) {
add_task(async () => {
const URL =
- "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html";
+ "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html";
// We need this dummy tab which load the same URL as test tab to keep the
// original content process alive after test page navigates away.
let dummyTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
@@ -90,7 +90,7 @@ add_task(async () => {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html",
+ url: "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html",
},
async function (browser) {
// Open a new window to run the tests, the original window will keep the
diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js
index 49a48c3177..ab1651fd74 100644
--- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js
+++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js
@@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html",
+ url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html",
},
async function (browser) {
// Maybe prevent BFCache on initial page.
@@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) {
// Navigate to fullscreen page.
const url = crossOrigin
? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"
- : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html";
+ : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html";
const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
BrowserTestUtils.startLoadingURIString(browser, url);
await loaded;
diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js
index c4feb7f641..1b344b7a2f 100644
--- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js
+++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js
@@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html",
+ url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html",
},
async function (browser) {
// Maybe prevent BFCache on initial page.
@@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) {
// Navigate to fullscreen page.
const url = crossOrigin
? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"
- : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html";
+ : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html";
const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
BrowserTestUtils.startLoadingURIString(browser, url);
await loaded;
diff --git a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js
index 4cf8a3d8c7..95668db542 100644
--- a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js
+++ b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js
@@ -27,7 +27,7 @@ add_setup(async function () {
add_task(async () => {
const url =
- "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html";
+ "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html";
const name = "foo";
await BrowserTestUtils.withNewTab(
diff --git a/dom/base/test/fullscreen/file_MozDomFullscreen.html b/dom/base/test/fullscreen/file_MozDomFullscreen.html
index f954892706..600d335501 100644
--- a/dom/base/test/fullscreen/file_MozDomFullscreen.html
+++ b/dom/base/test/fullscreen/file_MozDomFullscreen.html
@@ -3,6 +3,6 @@
</head>
<body style="background-color: blue;">
<p>Outer doc</p>
-<iframe id="innerFrame" src="http://mochi.test:8888/"></iframe>
+<iframe id="innerFrame" src="https://example.com/"></iframe>
</body>
</html>
diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html
index b60dea43bf..35b07cfa11 100644
--- a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html
+++ b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html
@@ -1,5 +1,5 @@
<div name="div" id="div" style="width: 100px; height: 100px; background: blue;">
<iframe id="iframe" allowfullscreen="yes"
- src="http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html">
+ src="https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html">
</iframe>
</div><br>
diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html
index dddf4930c2..b61cdc02c3 100644
--- a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html
+++ b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html
@@ -1,5 +1,5 @@
<div name="div" id="div" style="width: 100px; height: 100px; background: red;">
<iframe id="iframe" allowfullscreen="yes"
- src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html">
+ src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html">
</iframe>
</div><br>
diff --git a/dom/base/test/fullscreen/fullscreen_helpers.js b/dom/base/test/fullscreen/fullscreen_helpers.js
index f097ae316e..41548cf8aa 100644
--- a/dom/base/test/fullscreen/fullscreen_helpers.js
+++ b/dom/base/test/fullscreen/fullscreen_helpers.js
@@ -8,15 +8,14 @@ const TEST_URLS = [
`data:text/html,
<div name="div" id="div" style="width: 100px; height: 100px; background: red;">
<iframe id="iframe" allowfullscreen="yes"
- src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe>
+ src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe>
</div>`,
// toplevel and inner most iframe are in same process, and middle iframe is
// in a different process.
- // eslint-disable-next-line @microsoft/sdl/no-insecure-url
- `http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`,
+ `https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`,
// toplevel and middle iframe are in same process, and inner most iframe is
// in a different process.
- `http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`,
+ `https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`,
];
function waitRemoteFullscreenExitEvents(aBrowsingContexts) {
diff --git a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml
index 3041d851ac..6b3d8fada2 100644
--- a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml
+++ b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml
@@ -17,7 +17,7 @@ var gPrevTrusted = SpecialPowers.getBoolPref("full-screen-api.allow-trusted-requ
var newwindow;
// Ensure "fullscreen" permissions are not present on the test URI.
-var uri = Services.io.newURI("http://mochi.test:8888");
+var uri = Services.io.newURI("https://example.com");
var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {});
Services.perms.removeFromPrincipal(principal, "fullscreen");
diff --git a/dom/base/test/jsmodules/chrome.toml b/dom/base/test/jsmodules/chrome.toml
index 82d02ad4df..8b8a614bfb 100644
--- a/dom/base/test/jsmodules/chrome.toml
+++ b/dom/base/test/jsmodules/chrome.toml
@@ -3,11 +3,21 @@ support-files = [
"ambiguous_export.mjs",
"import_ambiguous.mjs",
"import_ambiguous_indirect_export.mjs",
+ "import_ambiguous_export.mjs",
+ "import_ambiguous_export_star.mjs",
+ "import_circular.mjs",
+ "import_circular_1.mjs",
"import_no_export.mjs",
"import_no_indirect_export.mjs",
"exportA1.mjs",
"exportA2.mjs",
"export_ambiguous.mjs",
+ "export_star_ambiguous.mjs",
+ "module_a.mjs",
+ "module_b.mjs",
+ "module_c.mjs",
+ "module_d.mjs",
+ "module_e.mjs",
"module_setRan.mjs",
"module_testSyntax.mjs",
"module_badSyntax.mjs",
@@ -47,6 +57,8 @@ support-files = [
["test_import_errorMessage.html"]
+["test_import_errorMessage2.html"]
+
["test_import_meta_resolve.html"]
["test_importedModuleMemoization.html"]
diff --git a/dom/base/test/jsmodules/export_star_ambiguous.mjs b/dom/base/test/jsmodules/export_star_ambiguous.mjs
new file mode 100644
index 0000000000..35cc979dee
--- /dev/null
+++ b/dom/base/test/jsmodules/export_star_ambiguous.mjs
@@ -0,0 +1 @@
+export * from "./ambiguous_export.mjs";
diff --git a/dom/base/test/jsmodules/import_ambiguous_export.mjs b/dom/base/test/jsmodules/import_ambiguous_export.mjs
new file mode 100644
index 0000000000..f5c12ff086
--- /dev/null
+++ b/dom/base/test/jsmodules/import_ambiguous_export.mjs
@@ -0,0 +1 @@
+import { a } from "./ambiguous_export.mjs";
diff --git a/dom/base/test/jsmodules/import_ambiguous_export_star.mjs b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs
new file mode 100644
index 0000000000..1ee2a56d37
--- /dev/null
+++ b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs
@@ -0,0 +1 @@
+import { a } from "./export_star_ambiguous.mjs";
diff --git a/dom/base/test/jsmodules/import_circular.mjs b/dom/base/test/jsmodules/import_circular.mjs
new file mode 100644
index 0000000000..bb3a46bd5e
--- /dev/null
+++ b/dom/base/test/jsmodules/import_circular.mjs
@@ -0,0 +1 @@
+export { a } from "./import_circular_1.mjs";
diff --git a/dom/base/test/jsmodules/import_circular_1.mjs b/dom/base/test/jsmodules/import_circular_1.mjs
new file mode 100644
index 0000000000..eb7c038c77
--- /dev/null
+++ b/dom/base/test/jsmodules/import_circular_1.mjs
@@ -0,0 +1 @@
+export { a } from "./import_circular.mjs";
diff --git a/dom/base/test/jsmodules/import_no_export.mjs b/dom/base/test/jsmodules/import_no_export.mjs
index 47cabac557..d989498c88 100644
--- a/dom/base/test/jsmodules/import_no_export.mjs
+++ b/dom/base/test/jsmodules/import_no_export.mjs
@@ -1 +1 @@
-import x from "./no_export.mjs";
+import { x } from "./no_export.mjs";
diff --git a/dom/base/test/jsmodules/import_no_indirect_export.mjs b/dom/base/test/jsmodules/import_no_indirect_export.mjs
index dd1ca847fc..bfcdcedbc5 100644
--- a/dom/base/test/jsmodules/import_no_indirect_export.mjs
+++ b/dom/base/test/jsmodules/import_no_indirect_export.mjs
@@ -1,2 +1,2 @@
/* eslint-disable import/default */
-import x from "./no_indirect_export.mjs";
+import { a } from "./no_indirect_export.mjs";
diff --git a/dom/base/test/jsmodules/importmaps/classic_script.js b/dom/base/test/jsmodules/importmaps/classic_script.js
new file mode 100644
index 0000000000..d7ae0be054
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/classic_script.js
@@ -0,0 +1 @@
+// Empty script.
diff --git a/dom/base/test/jsmodules/importmaps/mochitest.toml b/dom/base/test/jsmodules/importmaps/mochitest.toml
index 4229455722..1f95b155ac 100644
--- a/dom/base/test/jsmodules/importmaps/mochitest.toml
+++ b/dom/base/test/jsmodules/importmaps/mochitest.toml
@@ -3,6 +3,9 @@ support-files = [
"bug_1865410_module_a.mjs",
"bug_1865410_module_b.mjs",
"bug_1873417.mjs",
+ "classic_script.js",
+ "module_chain_1.mjs",
+ "module_chain_2.mjs",
"module_importMap_with_external_script_0.mjs",
"module_importMap_with_external_script_1.mjs",
"module_importMap_with_external_script_2.mjs",
@@ -13,6 +16,7 @@ support-files = [
"module_importMap_with_external_script_6.mjs",
"module_importMap_with_external_script_6.mjs^headers^",
"module_importMap_with_external_script_7.mjs",
+ "module_importMap_with_nonexisting_module.mjs",
"bad/module_2.mjs",
"bad/module_3.mjs",
"bad/module_4.mjs",
@@ -31,3 +35,6 @@ support-files = [
["test_bug_1873417.html"]
["test_importMap_with_external_script.html"]
+["test_importMap_with_nonexisting_module.html"]
+["test_dynamic_importMap_with_external_script.html"]
+["test_dynamic_importMap_load_completes.html"]
diff --git a/dom/base/test/jsmodules/importmaps/module_chain_1.mjs b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs
new file mode 100644
index 0000000000..d9515fab7f
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/no-unassigned-import
+import {} from "./module_chain_2.mjs";
diff --git a/dom/base/test/jsmodules/importmaps/module_chain_2.mjs b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs
new file mode 100644
index 0000000000..ce12406a76
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs
@@ -0,0 +1 @@
+loaded = true;
diff --git a/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs
new file mode 100644
index 0000000000..4f9981bbe3
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs
@@ -0,0 +1,4 @@
+/* eslint-disable import/no-unassigned-import, import/no-unresolved */
+// Bareword specifier should be mapped to ./good/module_0.mjs.
+import {} from "bare";
+import * as test from "nonexistingmodule";
diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html
new file mode 100644
index 0000000000..da354c1ca0
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+<title>Test script loading complets when there's a dynamicly inserted import map</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<!--
+ This test case used to fail intermittently with some modules never
+ being loaded.
+-->
+
+<script>
+ let loaded = false; // Set by module_chain_2.mjs.
+</script>
+
+<script src="classic_script.js"></script>
+
+<script>
+ const script = document.createElement('script');
+ script.type = 'importmap';
+ script.textContent = `{}`;
+ document.head.appendChild(script);
+</script>
+
+<script src="module_chain_1.mjs" type="module"></script>
+
+<script type="module">
+ ok(loaded, "Expected all modules loaded");
+</script>
+
+<body></body>
diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html
new file mode 100644
index 0000000000..b78992fb87
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+<title>Test speculative preload of external script doesn't conflict with import map</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<!--
+ These tests check that speculative preloading, which could happen before
+ the import map is installed, doesn't load the wrong modules. This version
+ dynamically inserts the import map script element after speculative
+ preloading has started.
+-->
+
+<script>
+ const script = document.createElement('script');
+ script.type = 'importmap';
+ script.textContent =
+ `{
+ "imports": {
+ "bare": "./good/module_0.mjs",
+ "./bad/module_1.mjs": "./good/module_1.mjs",
+ "./bad/module_2.mjs": "./good/module_2.mjs",
+ "./bad/module_3.mjs": "./good/module_3.mjs",
+ "./bad/module_4.mjs": "./good/module_4.mjs",
+ "./bad/module_7.mjs": "./good/module_7.mjs"
+ }
+ }`;
+ document.head.appendChild(script);
+</script>
+
+<!--
+Test bareword import (not supported before import map installed).
+-->
+<script type="module" src="module_importMap_with_external_script_0.mjs"></script>
+
+<!--
+Test mapping from missing resource to existing resource (not found before
+import map installed).
+-->
+<script type="module" src="module_importMap_with_external_script_1.mjs"></script>
+
+<!--
+Test mapping from one existing resource to another (would load wrong resource before
+import map installed).
+-->
+<script type="module" src="module_importMap_with_external_script_2.mjs"></script>
+
+<!--
+Test mapping from one existing resource to another with circular dependency.
+-->
+<script type="module" src="module_importMap_with_external_script_3.mjs"></script>
+
+<!--
+Test with redirect, script_6.mjs -> script_5.mjs -> script_4.mjs.
+We redirect twice here, as sometimes one redirect can't reproduce the crash
+from bug 1835468.
+-->
+<script type="module" src="module_importMap_with_external_script_6.mjs"></script>
+
+<!--
+Test with async attribute
+-->
+<script type="module" async src="module_importMap_with_external_script_7.mjs"></script>
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ let passCount = 0;
+ const expectedCount = 6;
+
+ function success(name) {
+ ok(true, "Test passed, loaded " + name);
+ passCount++;
+ if (passCount == expectedCount) {
+ SimpleTest.finish();
+ }
+ }
+</script>
+<body></body>
diff --git a/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html
new file mode 100644
index 0000000000..57cfd5e5a1
--- /dev/null
+++ b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test an import map with an nonexisting module specifier</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script type="importmap">
+{
+ "imports": {
+ "bare": "./good/module_0.mjs"
+ }
+}
+</script>
+
+
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ window.onerror = (event, src, lineno, colno, error) => {
+ ok(error instanceof TypeError, "Should be a TypeError");
+ SimpleTest.finish();
+ };
+
+</script>
+<script type="module" src="module_importMap_with_nonexisting_module.mjs"></script>
diff --git a/dom/base/test/jsmodules/module_a.mjs b/dom/base/test/jsmodules/module_a.mjs
new file mode 100644
index 0000000000..0b0c3304ff
--- /dev/null
+++ b/dom/base/test/jsmodules/module_a.mjs
@@ -0,0 +1,2 @@
+export var a = 1;
+export var b = 2;
diff --git a/dom/base/test/jsmodules/module_b.mjs b/dom/base/test/jsmodules/module_b.mjs
new file mode 100644
index 0000000000..79da84736d
--- /dev/null
+++ b/dom/base/test/jsmodules/module_b.mjs
@@ -0,0 +1,2 @@
+export var b = 3;
+export var c = 4;
diff --git a/dom/base/test/jsmodules/module_c.mjs b/dom/base/test/jsmodules/module_c.mjs
new file mode 100644
index 0000000000..5a2a7e3e09
--- /dev/null
+++ b/dom/base/test/jsmodules/module_c.mjs
@@ -0,0 +1,3 @@
+/* eslint-disable import/export */
+export * from "./module_a.mjs";
+export * from "./module_b.mjs";
diff --git a/dom/base/test/jsmodules/module_d.mjs b/dom/base/test/jsmodules/module_d.mjs
new file mode 100644
index 0000000000..04dc02d27a
--- /dev/null
+++ b/dom/base/test/jsmodules/module_d.mjs
@@ -0,0 +1 @@
+import { a } from "./module_c.mjs";
diff --git a/dom/base/test/jsmodules/module_e.mjs b/dom/base/test/jsmodules/module_e.mjs
new file mode 100644
index 0000000000..544c424fcb
--- /dev/null
+++ b/dom/base/test/jsmodules/module_e.mjs
@@ -0,0 +1 @@
+import { b } from "./module_c.mjs";
diff --git a/dom/base/test/jsmodules/test_import_errorMessage.html b/dom/base/test/jsmodules/test_import_errorMessage.html
index 6ab0b1dd74..2330b46dd9 100644
--- a/dom/base/test/jsmodules/test_import_errorMessage.html
+++ b/dom/base/test/jsmodules/test_import_errorMessage.html
@@ -8,40 +8,38 @@
let count = 0;
window.onerror = function (event, src, lineno, colno, error) {
- info("window.onerror :" + error.message);
+ info("window.onerror: message: " + error.message);
+ info("window.onerror: src: " + src);
ok(error instanceof SyntaxError, "Should be a SyntaxError.");
- // import_no_indirect_export.mjs and import_ambiguous_indirect_export.mjs
- // are related to indirect import/export.
- if (count < 2) {
- ok(error.message.match("indirect"), "Should contain 'indirect'");
- }
-
- // import_ambiguous_indirect_export.mjs and import_ambiguous.mjs both
- // have ambiguous import/export.
- if (count % 2 === 1) {
- ok(error.message.match("ambiguous"), "Should contain 'ambiguous'");
- }
-
- if (count === 2) {
- ok(!error.message.match("ambiguous") && !error.message.match("indirect"),
- "Should NOT contain 'indirect' nor 'ambiguous'");
+ if (src.match("no_indirect_export.mjs") ||
+ src.match("import_no_export.mjs")) {
+ ok(error.message.match("doesn't provide an export named"));
+ } else if(src.match("export_ambiguous.mjs") ||
+ src.match("import_ambiguous_export_star.mjs") ||
+ src.match("import_ambiguous_export.mjs") ||
+ src.match("import_ambiguous.mjs")) {
+ ok(error.message.match("contains ambiguous star export"));
+ } else if (src.match("import_circular_1.mjs")) {
+ ok(error.message.match("contains circular import"));
+ } else {
+ ok(false, "unknown src " + src);
}
count++;
};
function testLoaded() {
- ok(count === 4, "Should have 4 SynaxErrors thrown.");
+ ok(count === 7, "Should have 7 SynaxErrors thrown.");
SimpleTest.finish();
}
+
</script>
-<!--
-In window.onerror will test the error messages, so if the order is changed,
-the code in window.onerror should be updated as well.
--->
<script type="module" src="import_no_indirect_export.mjs"></script>
<script type="module" src="import_ambiguous_indirect_export.mjs"></script>
+<script type="module" src="import_ambiguous_export_star.mjs"></script>
+<script type="module" src="import_ambiguous_export.mjs"></script>
<script type="module" src="import_no_export.mjs"></script>
<script type="module" src="import_ambiguous.mjs"></script>
+<script type="module" src="import_circular.mjs"></script>
<body onload='testLoaded()'></body>
diff --git a/dom/base/test/jsmodules/test_import_errorMessage2.html b/dom/base/test/jsmodules/test_import_errorMessage2.html
new file mode 100644
index 0000000000..ed4227362e
--- /dev/null
+++ b/dom/base/test/jsmodules/test_import_errorMessage2.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test to get the filename of the requested module after it has been evaluated</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ let count = 0;
+
+ window.onerror = function (event, src, lineno, colno, error) {
+ info("window.onerror: message: " + error.message);
+ info("window.onerror: src: " + src);
+ ok(error instanceof SyntaxError, "Should be a SyntaxError.");
+
+ if (src.match("module_e.mjs")) {
+ ok(error.message.match("contains ambiguous star export"));
+ } else {
+ ok(false, "unknown src " + src);
+ }
+ count++;
+ };
+
+ function testLoaded() {
+ ok(count === 1, "Should have 1 SynaxError thrown.");
+ SimpleTest.finish();
+ }
+
+</script>
+
+<!--module_c.mjs will be evaluated here-->
+<script type="module" src="module_d.mjs"></script>
+
+<!--module_c.mjs will be linked again here-->
+<script type="module" src="module_e.mjs"></script>
+<body onload='testLoaded()'></body>
diff --git a/dom/base/test/mochitest.toml b/dom/base/test/mochitest.toml
index fb6724e497..a1e7b31a19 100644
--- a/dom/base/test/mochitest.toml
+++ b/dom/base/test/mochitest.toml
@@ -1170,6 +1170,8 @@ skip-if = [
["test_content_iterator_subtree.html"]
+["test_content_iterator_subtree_shadow_tree.html"]
+
["test_copyimage.html"]
skip-if = [
"os == 'android'",
diff --git a/dom/base/test/test_bug564863-2.xhtml b/dom/base/test/test_bug564863-2.xhtml
index 6f338f612d..c860a7f6d7 100644
--- a/dom/base/test/test_bug564863-2.xhtml
+++ b/dom/base/test/test_bug564863-2.xhtml
@@ -55,11 +55,11 @@ SimpleTest.waitForExplicitFinish();
// not when run as a Mochitest plain.
//is(xul_cs.color, "rgb(0, 0, 0)", "xul color " + test);
- attrValue = removed ? null : "";
+ let attrValue = removed ? null : "";
is(xul.id, "", "xul id " + test);
- is(xul.getAttribute("id"), "", "xul getAttribute " + test);
+ is(xul.getAttribute("id"), attrValue, "xul getAttribute " + test);
is($("xul_id"), null, "xul getElementById " + test);
}
@@ -149,7 +149,7 @@ SimpleTest.waitForExplicitFinish();
await new Promise(SimpleTest.executeSoon);
if (mutation) {
is(mutation.target, xul, "target is xul");
- is(xul.getAttribute("id"), "", "xul no longer has id attr");
+ is(xul.getAttribute("id"), null, "xul no longer has id attr");
is(xul.id, "", "xul no longer has id");
xul.id = "other_xul_id";
} else {
diff --git a/dom/base/test/test_content_iterator_subtree_shadow_tree.html b/dom/base/test/test_content_iterator_subtree_shadow_tree.html
new file mode 100644
index 0000000000..033aaf80a7
--- /dev/null
+++ b/dom/base/test/test_content_iterator_subtree_shadow_tree.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for content subtree iterator with ShadowDOM involved</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+function finish() {
+ // The SimpleTest may require usual elements in the template, but they shouldn't be during test.
+ // So, let's create them at end of the test.
+ document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>';
+ SimpleTest.finish();
+}
+
+function createContentIterator() {
+ return Cc["@mozilla.org/scriptable-content-iterator;1"]
+ .createInstance(Ci.nsIScriptableContentIterator);
+}
+
+function getNodeDescription(aNode) {
+ if (aNode === undefined) {
+ return "undefine";
+ }
+ if (aNode === null) {
+ return "null";
+ }
+ function getElementDescription(aElement) {
+ if (aElement.host) {
+ aElement = aElement.host;
+ }
+ if (aElement.tagName === "BR") {
+ if (aElement.previousSibling) {
+ return `<br> element after ${getNodeDescription(aElement.previousSibling)}`;
+ }
+ return `<br> element in ${getElementDescription(aElement.parentElement)}`;
+ }
+ let hasHint = aElement == document.body;
+ let tag = `<${aElement.tagName.toLowerCase()}`;
+ if (aElement.getAttribute("id")) {
+ tag += ` id="${aElement.getAttribute("id")}"`;
+ hasHint = true;
+ }
+ if (aElement.getAttribute("class")) {
+ tag += ` class="${aElement.getAttribute("class")}"`;
+ hasHint = true;
+ }
+ if (aElement.getAttribute("type")) {
+ tag += ` type="${aElement.getAttribute("type")}"`;
+ }
+ if (aElement.getAttribute("name")) {
+ tag += ` name="${aElement.getAttribute("name")}"`;
+ }
+ if (aElement.getAttribute("value")) {
+ tag += ` value="${aElement.getAttribute("value")}"`;
+ hasHint = true;
+ }
+ if (aElement.getAttribute("style")) {
+ tag += ` style="${aElement.getAttribute("style")}"`;
+ hasHint = true;
+ }
+ if (hasHint) {
+ return tag + ">";
+ }
+
+ return `${tag}> in ${getElementDescription(aElement.parentElement || aElement.parentNode)}`;
+ }
+ switch (aNode.nodeType) {
+ case aNode.TEXT_NODE:
+ return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`;
+ case aNode.COMMENT_NODE:
+ return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`;
+ case aNode.ELEMENT_NODE:
+ return getElementDescription(SpecialPowers.unwrap(aNode));
+ default:
+ return "unknown node";
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function () {
+ let iter = createContentIterator();
+
+ function runTest() {
+ /**
+ * Basic tests with complicated tree.
+ */
+ function check(aIter, aExpectedResult, aDescription) {
+ if (aExpectedResult.length) {
+ is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+ `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+ ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`);
+
+ aIter.first();
+ is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+ `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+ ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`);
+
+ for (let expected of aExpectedResult) {
+ is(SpecialPowers.unwrap(aIter.currentNode), expected,
+ `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`);
+ ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`);
+ aIter.next();
+ }
+
+ is(SpecialPowers.unwrap(aIter.currentNode), null,
+ `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`);
+ ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`);
+ } else {
+ is(SpecialPowers.unwrap(aIter.currentNode), null,
+ `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`);
+ ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`);
+
+ aIter.first();
+ is(SpecialPowers.unwrap(aIter.currentNode), null,
+ `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`);
+ ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`);
+ }
+ }
+
+ // Structure
+ // <div>OuterText1</div>
+ // <div #host1>
+ // #ShadowRoot
+ // InnerText1
+ // <div>OuterText2</div>
+ // <div #host2>
+ // #ShadowRoot
+ // <div>InnerText2</div>
+ // <div>InnerText3</div>
+ // <div #host3>
+ // #ShadowRoot
+ // <div #host4>
+ // #ShadowRoot
+ // InnerText4
+ // OuterText3
+
+ document.body.innerHTML = `<div id="outerText1">OuterText1</div>` +
+ `<div id="host1"></div>` +
+ `<div id="outerText2">OuterText2</div>` +
+ `<div id="host2"></div>` +
+ `<div id="host3"></div>` +
+ `OuterText3`;
+ const outerText1 = document.getElementById("outerText1");
+ const outerText2 = document.getElementById("outerText2");
+
+ const host1 = document.getElementById("host1");
+ const root1 = host1.attachShadow({mode: "open"});
+ root1.innerHTML = "InnerText1";
+
+ const host2 = document.getElementById("host2");
+ const root2 = host2.attachShadow({mode: "open"});
+ root2.innerHTML = "<div>InnerText2</div><div>InnerText3</div>";
+
+ const host3 = document.getElementById("host3");
+ const root3 = host3.attachShadow({mode: "open"});
+ root3.innerHTML = `<div id="host4"></div>`;
+
+ const host4 = root3.getElementById("host4");
+ const root4 = host4.attachShadow({mode: "open"});
+ root4.innerHTML = "InnerText4";
+
+ /**
+ * Selects the <body> with a range.
+ */
+ range = document.createRange();
+ range.selectNode(document.body);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ check(iter, [document.body], "Initialized with range selecting the <body>");
+
+ /**
+ * Selects all children in the <body> with a range.
+ */
+ range = document.createRange();
+ range.selectNodeContents(document.body);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ check(iter, [outerText1, host1,
+ outerText2, host2,
+ host3, // host4 is a child of host3
+ document.body.lastChild],
+ "Initialized with range selecting all children in the <body>");
+
+ /**
+ * range around elements.
+ */
+ range = document.createRange();
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // outerText1.firstChild is a node without children, so the
+ // next candidate is root1.firstChild, given root1.firstChild
+ // is also the end container which isn't fully contained
+ // by this range, so the iterator returns nothing.
+ check(iter, [], "Initialized with range selecting 'OuterText1 and InnerText1'");
+
+ // From light DOM to Shadow DOM #1
+ range = document.createRange();
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] outerText1 is a container and it has children, so the first node
+ // is the topmost descendant, which is outerText.firstChild.
+ // [end] The end point of this iteration is also outerText1.firstChild because
+ // it is also the topmost element in the previous node of root1.firstChild.
+ // Iteration #1: outerText1.firstChild as it is the start node
+ check(iter, [outerText1.firstChild], "Initialized with range selecting 'OuterText1 and InnerText1'");
+
+ // From light DOM to Shadow DOM #2
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2, root2.childNodes.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] outerText1 is a container and it has children, so the first node
+ // is the topmost descendant, which is outerText.firstChild.
+ // [end] root2 is the container and it has children, so the end node is
+ // the last node of root2, which is root2.lastChild
+ // Iteration #1: outerText1.firstChild, as it's the start node
+ // Iteration #2: host1, as it's next available node after outerText1.firstChild
+ // Iteration #3: outerText2, as it's the next sibiling of host1
+ // Iteration #4: host2, as it's the next sibling of outerText2. Since it's
+ // the ancestor of the end node, so we get into this tree and returns
+ // root2.firstChild here.
+ // Iteration #5: root2.lastChild, as it's the next sibling of root2.firstChild
+ check(iter, [outerText1.firstChild, host1, outerText2, root2.firstChild, root2.lastChild],
+ "Initialized with range selecting 'OuterText1, InnerText1, OuterText2 and InnerText2'");
+
+ // From Shadow DOM to Shadow DOM #1
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] outerText2 is the start because root1.firstChild doesn't have children,
+ // so we look for next available node which is outerText2.
+ // [end] root2.lastChild is the end container, so we look for previous
+ // nodes and get root2.firstChild
+ // Iteration #1: outerText2, as it's the start node
+ // Iteration #2: host2, as it's the next sibling of outerText2. Since it's
+ // the ancestor of the end node, so we get into this tree and returns
+ // root2.firstChild here.
+ check(iter, [outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'");
+
+ // From Shadow DOM to Shadow DOM #2
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] root1 is the start container and it has children, so the first node
+ // is the topmost descendant, which is root1.firstChild.
+ // [end] root2.lastChild is the end container, so we look for previous
+ // nodes and get root2.firstChild
+ // Iteration #1: root1.firstChild, as it's the start node
+ // Iteration #2: outerText2, as it's the next available node
+ // Iteration #3: host2, as it's the next sibling of outerText2. Since it's
+ // the ancestor of the end node, so we get into this tree and returns
+ // root2.firstChild here.
+ check(iter, [root1.firstChild, outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'");
+
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 1);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root4.firstChild, root4.firstChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] outerText2 is the start because root1.firstChild doesn't have children,
+ // so we look for next available node which is outerText2.
+ // [end] host2 is the end container, so we look for previous
+ // nodes root4.firstChild and eventually get host2.
+ // Iteration #1: outerText2, as it's the start node
+ // Iteration #2: host2, as it's the next sibling of outerText2
+ check(iter, [outerText2, host2], "Initialized with range selecting 'InnerText1, OuterText2, InnerText2 and InnerText3'");
+
+ // From light to light
+ SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0);
+ SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(document.body.lastChild, document.body.lastChild.length);
+ iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+ // [start] host1 is the start because it's the next available node of
+ // outerText1.firstChild.
+ // [end] host3 is the end because the previous node of document.body.lastChild is host3.
+ // Iteration #1: host1, as it's the start node
+ // Iteration #2: outerText2, as it's the next sibling of host1
+ // Iteration #3: host2, as it's the next sibling of outerText2
+ // Iteration #4: host3, as it's the next sibling of host2
+ check(iter, [host1, outerText2, host2, host3],
+ "Initialized with range selecting 'OuterText1, InnerText1, OuterText2, InnerText2, InnerText3 and OuterText3'");
+
+ finish();
+ }
+
+ SpecialPowers.pushPrefEnv({"set": [["dom.shadowdom.selection_across_boundary.enabled", true]]}, runTest);
+});
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/base/test/test_embed_xorigin_document.html b/dom/base/test/test_embed_xorigin_document.html
index 7d4c29aacf..4566d48534 100644
--- a/dom/base/test/test_embed_xorigin_document.html
+++ b/dom/base/test/test_embed_xorigin_document.html
@@ -12,11 +12,6 @@ const testPath = window.location.href.replace("http://mochi.test:8888", "");
const testDir = testPath.substring(0, testPath.lastIndexOf('/') + 1);
add_task(async function() {
- // FIXME: Remove when bug 1658342 is fixed
- await SpecialPowers.pushPrefEnv({
- set: [["fission.remoteObjectEmbed", true]],
- });
-
info("Loading image in embed");
let embed = document.createElement("embed");
document.body.appendChild(embed);
diff --git a/dom/base/test/test_range_bounds.html b/dom/base/test/test_range_bounds.html
index 657d315198..dba687eead 100644
--- a/dom/base/test/test_range_bounds.html
+++ b/dom/base/test/test_range_bounds.html
@@ -38,6 +38,22 @@ function isEmptyRect(rect, name) {
is(rect.height, 0, name+'empty rect should have height = 0');
}
+function getTextBoundingClientRect(node) {
+ const quads = node.getBoxQuads()[0];
+ return DOMRect.fromRect({
+ x: quads.p1.x,
+ y: quads.p1.y,
+ width: quads.p2.x - quads.p1.x,
+ height: quads.p3.y - quads.p2.y
+ });
+}
+
+function sortRectList(rectlist) {
+ return Array.prototype.slice.call(rectlist, 0).sort(function(a, b) {
+ return a.top - b.top || a.left - b.left;
+ });
+}
+
function isEmptyRectList(rectlist, name) {
name = annotateName(name);
is(rectlist.length, 0, name + 'empty rectlist should have zero rects');
@@ -90,6 +106,9 @@ function runATest(obj) {
//convert RectList to a real array
obj.rectList=Array.prototype.slice.call(obj.rectList, 0);
}
+ if (obj.mustSortBeforeComparing) {
+ rectlist = sortRectList(rectlist);
+ }
obj.rectList.forEach(function(r,i) {
is(_getRect(rectlist[i]),_getRect(r),
annotateName(testname+": item at "+i));
@@ -109,11 +128,16 @@ function doTest(){
thirdDiv = root.childNodes[5];
var firstPRect = firstP.getBoundingClientRect(),
spanInFirstPRect = spanInFirstP.getBoundingClientRect(),
+ textInFirstPRect = getTextBoundingClientRect(firstP.firstChild),
+ textInSpanInFirstPRect = getTextBoundingClientRect(spanInFirstP.firstChild),
firstDivRect = firstDiv.getBoundingClientRect(),
+ textInFirstDivRect = getTextBoundingClientRect(firstDiv.firstChild),
spanInFirstDivRect = spanInFirstDiv.getBoundingClientRect(),
+ textInSpanInFirstDivRect = getTextBoundingClientRect(spanInFirstDiv.firstChild),
secondPRect = secondP.getBoundingClientRect(),
secondDivRect = secondDiv.getBoundingClientRect(),
spanInSecondPRect = spanInSecondP.getBoundingClientRect(),
+ textInSpanInSecondPRect = getTextBoundingClientRect(spanInSecondP.firstChild),
spanInSecondDivRect = spanInSecondDiv.getBoundingClientRect(),
spanInSecondDivRectList = spanInSecondDiv.getClientRects();
var widthPerchar = spanInSecondPRect.width / spanInSecondP.firstChild.length;
@@ -132,12 +156,14 @@ function doTest(){
{name:'collapsedAtEndOfTextNode', range:[firstP.firstChild, 6],
rect:[spanInFirstPRect.left, spanInFirstPRect.left,
spanInFirstPRect.top, spanInFirstPRect.bottom, 0, spanInFirstPRect.height]},
- {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect},
+ {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect,
+ rectList:[firstPRect, textInFirstPRect, spanInFirstPRect]},
{name:'twoBlockNodes', range:[root, 1, root, 3],
rect:[firstPRect.left, firstPRect.right, firstPRect.top,
firstDivRect.bottom, firstPRect.width,
firstDivRect.bottom - firstPRect.top],
- rectList:[firstPRect, firstDivRect]},
+ rectList:[firstPRect, textInFirstPRect, textInSpanInFirstPRect,
+ firstDivRect, textInFirstDivRect, textInSpanInFirstDivRect]},
{name:'endOfTextNodeToEndOfAnotherTextNodeInAnotherBlock',
range:[spanInFirstP.firstChild, 1, firstDiv.firstChild, 5],
rect:[spanInFirstDivRect.left - 5*widthPerchar, spanInFirstDivRect.left,
@@ -163,7 +189,7 @@ function doTest(){
rectList:[[spanInSecondPRect.left - 3*widthPerchar, spanInSecondPRect.left,
spanInSecondPRect.top, spanInSecondPRect.bottom, 3 * widthPerchar,
spanInSecondPRect.height],
- spanInSecondPRect,
+ spanInSecondPRect, textInSpanInSecondPRect,
[spanInSecondPRect.right, spanInSecondPRect.right + widthPerchar,
spanInSecondPRect.top, spanInSecondPRect.bottom, widthPerchar,
spanInSecondPRect.height]]}
@@ -228,10 +254,17 @@ function doTest(){
}
function testMixedDir(){
var root = document.getElementById('mixeddir');
+ var bdo = document.getElementById('bdo');
var firstSpan = root.firstElementChild, firstSpanRect=firstSpan.getBoundingClientRect(),
- firstSpanRectList = firstSpan.getClientRects();
+ firstSpanWithInnerTextRectList = Array.from(firstSpan.getClientRects());
+ firstSpanWithInnerTextRectList.push(...bdo.getClientRects());
+
+ // Depending on the font rendering, the order of the rects composing the bdo
+ // element may vary. We need to sort the list of rects before comparing it to
+ // the expected list.
+ firstSpanWithInnerTextRectList = sortRectList(firstSpanWithInnerTextRectList);
runATest({name:'mixeddir',range:[firstSpan.firstChild,0,firstSpan.lastChild,firstSpan.lastChild.length],
- rect: firstSpanRect, rectList:firstSpanRectList});
+ rect: firstSpanRect, rectList:firstSpanWithInnerTextRectList, mustSortBeforeComparing: true});
root = document.getElementById('mixeddir2');
firstSpan = root.firstElementChild;
@@ -271,7 +304,10 @@ function testShadowDOM() {
isnot(rect.height, 0, "Div element inside shadow shouldn't have zero size.");
}
-function test(){
+async function test(){
+ // We use getBoxQuads to get some text nodes bounding rects.
+ await SpecialPowers.pushPrefEnv({"set": [["layout.css.getBoxQuads.enabled", true]]});
+
//test ltr
doTest();
diff --git a/dom/base/test/unit/test_xhr_standalone.js b/dom/base/test/unit/test_xhr_standalone.js
index 94f2d7d642..100027f38a 100644
--- a/dom/base/test/unit/test_xhr_standalone.js
+++ b/dom/base/test/unit/test_xhr_standalone.js
@@ -6,6 +6,10 @@
// in non-window non-Worker context
function run_test() {
+ Services.prefs.setBoolPref(
+ "network.fetch.systemDefaultsToOmittingCredentials",
+ false
+ );
var xhr = new XMLHttpRequest();
xhr.open("GET", "data:,", false);
var exceptionThrown = false;
@@ -13,6 +17,7 @@ function run_test() {
xhr.responseType = "";
xhr.withCredentials = false;
} catch (e) {
+ console.error(e);
exceptionThrown = true;
}
Assert.equal(false, exceptionThrown);
diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml
index 211d3e1344..11b569e6d4 100644
--- a/dom/base/use_counter_metrics.yaml
+++ b/dom/base/use_counter_metrics.yaml
@@ -107,8 +107,8 @@ use.counter:
send_in_pings:
- use-counters
-# Total of 2307 use counter metrics (excludes denominators).
-# Total of 358 'page' use counters.
+# Total of 2301 use counter metrics (excludes denominators).
+# Total of 354 'page' use counters.
use.counter.page:
svgsvgelement_getelementbyid:
type: counter
@@ -450,57 +450,6 @@ use.counter.page:
send_in_pings:
- use-counters
- onstart:
- type: counter
- description: >
- Whether a page sets a <marquee> onstart event listener.
- Compare against `use.counter.top_level_content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
- onbounce:
- type: counter
- description: >
- Whether a page sets a <marquee> onbounce event listener.
- Compare against `use.counter.top_level_content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
- onfinish:
- type: counter
- description: >
- Whether a page sets a <marquee> onfinish event listener.
- Compare against `use.counter.top_level_content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
onoverflow:
type: counter
description: >
@@ -569,23 +518,6 @@ use.counter.page:
send_in_pings:
- use-counters
- js_late_weekday:
- type: counter
- description: >
- Whether a page parses a Date with day of week in an unexpected position.
- Compare against `use.counter.top_level_content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
js_wasm_legacy_exceptions:
type: counter
description: >
@@ -6196,7 +6128,7 @@ use.counter.page:
send_in_pings:
- use-counters
-# Total of 358 'document' use counters.
+# Total of 354 'document' use counters.
use.counter.doc:
svgsvgelement_getelementbyid:
type: counter
@@ -6538,57 +6470,6 @@ use.counter.doc:
send_in_pings:
- use-counters
- onstart:
- type: counter
- description: >
- Whether a document sets a <marquee> onstart event listener.
- Compare against `use.counter.content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
- onbounce:
- type: counter
- description: >
- Whether a document sets a <marquee> onbounce event listener.
- Compare against `use.counter.content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
- onfinish:
- type: counter
- description: >
- Whether a document sets a <marquee> onfinish event listener.
- Compare against `use.counter.content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
onoverflow:
type: counter
description: >
@@ -6657,23 +6538,6 @@ use.counter.doc:
send_in_pings:
- use-counters
- js_late_weekday:
- type: counter
- description: >
- Whether a document parses a Date with day of week in an unexpected position.
- Compare against `use.counter.content_documents_destroyed`
- to calculate the rate.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
- notification_emails:
- - dom-core@mozilla.com
- - emilio@mozilla.com
- expires: never
- send_in_pings:
- - use-counters
-
js_wasm_legacy_exceptions:
type: counter
description: >
@@ -15677,7 +15541,7 @@ use.counter.deprecated_ops.doc:
send_in_pings:
- use-counters
-# Total of 696 'CSS (page)' use counters.
+# Total of 697 'CSS (page)' use counters.
use.counter.css.page:
css_align_content:
type: counter
@@ -27528,7 +27392,7 @@ use.counter.css.page:
send_in_pings:
- use-counters
-# Total of 696 'CSS (document)' use counters.
+# Total of 697 'CSS (document)' use counters.
use.counter.css.doc:
css_align_content:
type: counter