Bug 1867939, part 5: Integrate find-text-directive algorithm into Document load. r=peterv,farre,dom-core

This patch integrates the algorithm to find a text fragment range
into the document loading mechanism.
Unlike described in the spec, the fragment directive is not stripped
from the URL in the Session History Entry, instead it is stripped when
setting the URI into the Document using `Document::SetURI()`,
as well as when accessing the URL through `Location`.

The `PresShell` class is extended by a new method which sets the
ranges created from the text directives into the FrameSelection as
TargetText selection and scrolls it into view.

Security restrictions like force load at top and cross-origin iframes
are not yet considered in this patch.

Differential Revision: https://phabricator.services.mozilla.com/D195688
This commit is contained in:
Jan-Niklas Jaeschke 2024-04-04 14:39:33 +00:00
parent 81f5e14057
commit fda59c7c38
6 changed files with 130 additions and 14 deletions

View file

@ -173,6 +173,7 @@
#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"
@ -4067,6 +4068,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);
@ -13081,25 +13097,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();
}
return;
}
// 2. If fragment is the empty string, then return the special value top of
// the document.
if (mScrollToRef.IsEmpty()) {
return;
}
RefPtr<PresShell> presShell = GetPresShell();
if (!presShell) {
return;
}
if (mScrolledToRefAlready) {
presShell->ScrollToAnchor();
return;
}
// 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);

View file

@ -21,6 +21,7 @@
#include "nsICSSDeclaration.h"
#include "nsIFrame.h"
#include "nsINode.h"
#include "nsIURIMutator.h"
#include "nsRange.h"
#include "nsString.h"
@ -50,6 +51,34 @@ JSObject* FragmentDirective::WrapObject(JSContext* aCx,
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);

View file

@ -16,6 +16,7 @@
#include "nsWrapperCache.h"
class nsINode;
class nsIURI;
class nsRange;
namespace mozilla::dom {
class Document;
@ -84,6 +85,15 @@ class FragmentDirective final : public nsISupports, public nsWrapperCache {
*/
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);

View file

@ -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;

View file

@ -14,6 +14,7 @@
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/FontFaceSet.h"
#include "mozilla/dom/ElementBinding.h"
#include "mozilla/dom/FragmentDirective.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/PerformanceMainThread.h"
@ -3291,6 +3292,46 @@ nsresult PresShell::ScrollToAnchor() {
ScrollAxis(), ScrollFlags::AnchorScrollFlags);
}
bool PresShell::HighlightAndGoToTextFragment(bool aScrollToTextFragment) {
MOZ_ASSERT(mDocument);
if (!StaticPrefs::dom_text_fragments_enabled()) {
return false;
}
const RefPtr<FragmentDirective> fragmentDirective =
mDocument->FragmentDirective();
nsTArray<RefPtr<nsRange>> textDirectiveRanges =
fragmentDirective->FindTextFragmentsInDocument();
if (textDirectiveRanges.IsEmpty()) {
return false;
}
const RefPtr<Selection> targetTextSelection =
GetCurrentSelection(SelectionType::eTargetText);
if (!targetTextSelection) {
return false;
}
for (RefPtr<nsRange> range : textDirectiveRanges) {
targetTextSelection->AddRangeAndSelectFramesAndNotifyListeners(
*range, IgnoreErrors());
}
if (!aScrollToTextFragment) {
return false;
}
// Scroll the last text directive into view.
nsRange* lastRange = textDirectiveRanges.LastElement();
MOZ_ASSERT(lastRange);
if (RefPtr<nsIContent> lastRangeStartContent =
nsIContent::FromNode(lastRange->GetStartContainer())) {
return ScrollContentIntoView(
lastRangeStartContent,
ScrollAxis(WhereToScroll::Center, WhenToScroll::Always),
ScrollAxis(), ScrollFlags::AnchorScrollFlags) == NS_OK;
}
return false;
}
/*
* Helper (per-continuation) for ScrollContentIntoView.
*

View file

@ -1609,6 +1609,18 @@ class PresShell final : public nsStubDocumentObserver,
*/
MOZ_CAN_RUN_SCRIPT nsresult ScrollToAnchor();
/**
* Finds text fragments ranes in the document, highlights the ranges and
* scrolls to the last text fragment range on the page if
* `aScrollToTextFragment` is true.
*
* @param aScrollToTextFragment If true, scrolls the view to the last text
* fragment.
* @return True if scrolling happened.
*/
MOZ_CAN_RUN_SCRIPT bool HighlightAndGoToTextFragment(
bool aScrollToTextFragment);
/**
* When scroll anchoring adjusts positions in the root frame during page load,
* it may move our scroll position in the root frame.