From cc592494c5ec648b95fac0d15cb0c9c22da2ee51 Mon Sep 17 00:00:00 2001 From: David Bokan Date: Thu, 20 Apr 2023 11:27:08 -0400 Subject: [PATCH] Improve integration with navigation (#225) The fragment directive is now stripped from the URL whenever it is set to a history entry and the directive is kept separately in the entry's `directive state`. The directive state may be shared between history entries where only the non-directive portion of the fragment changes. Moving between session history entries will cause the directive state, if it differs from the current entry, to be set into then `uninvoked directives` member on the Document. The "scroll to the fragment" steps will read directives from this member and attempt to perform the text search. When the fragment search ends, `uninvoked directives` is always cleared. The changes here are primarily in the "3.3 The Fragment Directive" and "3.4 Text Directives" sections. Most changes in other sections are minor and/or text that was moved out into these two section (and heavily rewritten). --------- Co-authored-by: Jake Archibald --- index.bs | 856 +++++++++++++++---------- index.html | 1778 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 1596 insertions(+), 1038 deletions(-) diff --git a/index.bs b/index.bs index b5b9c06..cf72522 100644 --- a/index.bs +++ b/index.bs @@ -59,6 +59,16 @@ spec:url; type:dfn; text:fragment } + +

Infrastructure

This specification depends on the Infra Standard. [[!INFRA]] @@ -237,12 +247,12 @@ The fragment directive delimiter is the string ":~:", that is the three consecutive code points U+003A (:), U+007E (~), U+003A (:).

- The [=/fragment directive=] is part of the URL fragment. This means it + The [=fragment directive=] is part of the URL fragment. This means it always appears after a U+0023 (#) code point in a URL.
- To add a [=/fragment directive=] to a URL like https://example.com, a fragment + To add a [=fragment directive=] to a URL like https://example.com, a fragment is first appended to the URL: https://example.com#:~:text=foo.
@@ -260,117 +270,241 @@ action. Multiple directives may appear in the fragment directive.

Contains 2 text directives and one unknown directive.

+To prevent impacting page operation, it is stripped from script-accessible APIs to prevent +interaction with author script. This also ensures future directives can be added without web +compatibility risk. -To prevent impacting page operation, it is stripped from a [=Document=]'s -[=Document/URL=] to prevent interaction with author script. This also ensures -future directives can be added without web compatibility risk. +### Extracting the fragment directive ### {#extracting-the-fragment-directive} -### Processing the fragment directive ### {#processing-the-fragment-directive} +This section describes the mechanism by which the fragment directive is hidden +from script and how it fits into [[HTML#navigation-and-session-history]]. -The [=/fragment directive=] is processed and removed from the fragment whenever the -UA sets the [=Document/URL=] on a [=Document=]. This is defined with the -following additions and changes. +
+ The summarized changes in this section: -To the definition of [=Document=], add: + * Session history entries now include a new "directive state" item + * All new entries are created with a directive state with an empty value. If the new URL includes + a fragment directive it will be written to the state's value (otherwise it remains null). + * Any time a URL potentially including a fragment directive is written to a session history entry, + extract the fragment directive from the URL and store it in a directive state item of the + entry. There are four such points where a URL can potentially include a directive: + * In the "navigate" steps for typical cross-document navigations + * In the "navigate to a fragment" steps for fragment based same-document navigations + * In the "URL and history update steps" for synchronous updates such as + pushState/replaceState. + * In the "create navigation params by fetching" steps for URLs coming from a redirect. + * Same-document navigations that change only the fragment, and the new URL doesn't specify a + directive, will create an entry whose directive state refers to the previous entry's directive + state. -> Monkeypatching [[DOM]]: -> -> -> Each document has an associated fragment -> directive which is either null or an ASCII string holding data used -> by the UA to process the resource. It is initially null. -> +
-
- To split the fragment from the fragment directive, given an ASCII string - |raw fragment| and returning a [=/tuple=] consisting of a fragment and a - fragment directive (both ASCII strings), run these steps: +In [[HTML#session-history-infrastructure]], define [=/directive state=]: -
    - 1. Let |position| be the [=string/position variable=] pointing to the first code - point of the first instance, if one exists, of the [=fragment directive delimiter=] in |raw - fragment|, or past the end of |raw fragment| otherwise. - 1. Let |fragment| be the [=code point substring by positions=] of |raw fragment| from the - start of |raw fragment| to |position|. - 1. Let |fragmentDirective| be an ASCII string, initially empty. - 1. Advance |position| by the [=string/code point length=] of the [=fragment directive - delimiter=]. - 1. If |position| does not point past the end of |raw fragment|: - 1. Set |fragmentDirective| to the [=code point substring to the end of the string=] - |raw fragment| starting from |position| - 1. Return the [=/tuple=] (|fragment|, |fragmentDirective|). -
-
+> Monkeypatching [[HTML#session-history-infrastructure]]: +> +> directive state holds the value of the [=fragment directive=] at the time the session +> history entry was created and is used to invoke directives, such as text highlighting, whenever +> the entry is traversed. It has: +> * value, the [=fragment directive=] [=ASCII string=] or null, +> initially null. +> +> A [=/directive state=] may be shared by multiple session history entries. +> +>
+>

The fragment directive is removed from the URL before the URL is set to the session +> history entry. It is instead stored in the directive state. This prevents it from being +> visible to script APIs so that a directive can be specified without interfering with a +> page's operation.

+> +>

The fragment directive is stored in the directive state object, rather than a raw string, +> since the same directive state can be shared across multiple contiguous session history +> entries. On a traversal, the directive is only processed (i.e. search text and highlight) if +> the directive state has changed between two entries.

+>
+To the definition of session history entry, add: -Whenever the fragment directive is stripped from the URL, the -Document's [=Document/fragment directive=] is set to the content of the fragment directive. +> Monkeypatching [[HTML#session-history-entries]]: +> +>
A session history entry is a struct with the following items: +> * ... +> * persisted user state, which is implementation-defined, initially null +> * directive state, a [=/directive state=], +> initially a new [=/directive state=] +>
-Add a series of steps that will process a fragment directive on a [=Document/URL=]: +Add a helper algorithm for removing and returning a fragment directive string from a [=/URL=]: -> Monkeypatching [[DOM]]: +> Monkeypatching [[HTML]]: +> +>
+> This algorithm makes a URL's fragment end at the [=fragment directive +> delimiter=]. The returned [=/fragment directive=] includes all characters that follow the +> delimiter but does not include the delimiter. +>
+> +>
+> TODO: If a URL's fragment ends with ':~:' (i.e. empty directive), this will return null which +> is treated as the URL not specifying an explicit directive (and avoids clobbering an existing +> one. But maybe in this case we should return the empty string? That way a page can explicitly +> clear directives/highlights by navigating/pushState to '#:~:'. +>
> -> To process and consume fragment directive from a [=/URL=] -> |url| and [=Document=] |document|, run these steps: +> To remove the fragment directive from a [=/URL=] |url|, run these steps: > 1. Let |raw fragment| be equal to |url|'s [=url/fragment=]. -> 1. If |raw fragment| is non-null and contains the [=fragment directive -> delimiter=] as a substring: -> 1. Let |components| be the result of running [=split the fragment from the fragment -> directive=] on |raw fragment|. -> 1. Set |url|'s [=url/fragment=] to |components|' fragment. -> 1. Set |document|'s [=Document/fragment directive=] to |components|' fragment directive. ->
This is stored on the document but currently not web-exposed
+> 1. Let |fragment directive| be null. +> 1. If |raw fragment| is non-null and contains the [=fragment directive delimiter=] as a +> substring: +> 1. Let |position| be the [=string/position variable=] pointing to the first code +> point of the first instance, if one exists, of the [=fragment directive delimiter=] in +> |raw fragment|, or past the end of |raw fragment| otherwise. +> 1. Let |new fragment| be the [=code point substring by positions=] of |raw fragment| from +> the start of |raw fragment| to |position|. +> 1. Advance |position| by the [=string/code point length=] of the [=fragment directive +> delimiter=]. +> 1. If |position| does not point past the end of |raw fragment|: +> 1. Set |fragment directive| to the [=code point substring to the end of the string=] +> |raw fragment| starting from |position| +> 1. Set |url|'s [=url/fragment=] to |new fragment|. +> 1. Return |fragment directive|. +> +>
+> https://example.org/#test:~:text=foo will be parsed such that +> the fragment is the string "test" and the [=/fragment directive=] is the string +> "text=foo". +>
-
- These changes make a URL's fragment end at the [=fragment directive - delimiter=]. The [=/fragment directive=] includes all characters that follow, - but not including, the delimiter. -
+The next four monkeypatches modify the creation of a session history entry, where the URL might +contain a fragment directive, to remove the fragment directive and store it in the [=/directive +state=]. -
-https://example.org/#test:~:text=foo will be parsed such that -the fragment is the string "test" and the [=/fragment directive=] is the string -"text=foo". -
+In the definition of [=navigate=]: + +> Monkeypatching [[HTML#beginning-navigation]]: +> +>
To navigate a navigable navigable to a URL |url|...: +> 1. ... +>
  • Set navigable's ongoing navigation to navigationId.
  • +> 15. If url's scheme is "javascript", then... +> 16. In parallel, run these steps: +> 1. ... +>
  • If url is about:blank, then set documentState's origin to documentState's initiator origin.
  • +> 6. Otherwise, if url is about:srcdoc, then set documentState's origin to navigable's parent's active document's origin. +> 7. Let historyEntry be a new session history entry, with its URL set to url and +> its document state set to documentState. +>
  • Let |fragment directive| be the result of running [=remove the +> fragment directive=] on |url|.
  • +> 8. Let |directive state| be a new [=/directive +> state=] with [=directive state/value=] set to |fragment directive|. +> 9. Let historyEntry be a new session history entry, with its URL +> set to |url|, its document state set to documentState, and its [=she/directive state=] +> set to |directive state|. +> 10. Let navigationParams be null. +> 11. ... +>
    +In the definition of navigate to a fragment: + +> Monkeypatching [[HTML#scroll-to-fragid]]: +> +>
    To navigate to a fragment given navigable |navigable|, ...: +> 1. Let |directive state| be navigable's active session history +> entry's [=she/directive state=]. +> 1. Let |fragment directive| be the result of running +> [=remove the fragment directive=] on |url|. +> 1. If |fragment directive| is not null: +>
    Otherwise, when only the fragment has changed and it did not specify +> a directive, the active entry's directive state is reused. This prevents a fragment +> change from clobbering highlights.
    +> 1. Let |directive state| be a new [=/directive state=] with +> [=directive state/value=] set to |fragment directive|. +> 2. Let historyEntry be a new session history entry, with +> * URL url +> * document state navigable's active session history entry's document state +> * scroll restoration mode navigable's active session history entry's scroll restoration +> mode +> * [=she/directive state=] |directive state| +> 2. Let entryToReplace be navigable's active session history entry if historyHandling is +> "replace", otherwise null. +> 3. ... +>
    -Amend the - -create and initialize a Document object steps to parse and remove the -[=/fragment directive=] by inserting the following steps right before the -setting |document|'s [=Document/URL=] -(currently -step 9): +In the definition of URL and history update steps: -> Monkeypatching [[HTML]]: +> Monkeypatching [[HTML#navigate-non-frag-sync]]: > -> 9. Run the [=process and consume fragment directive=] steps on -> |creationURL| and |document|. -> 10. Set |document|'s [=Document/URL=] to be |creationURL|. +>
    The URL and history update steps, given a Document |document|, ...: +> 1. Let |navigable| be |document|'s node navigable. +> 2. Let |activeEntry| be |navigable|'s active session history entry. +> 3. Let |fragment directive| be the result of running [=remove the +> fragment directive=] on |newUrl|. +> 5. Let |historyEntry| be a new session history entry, with +> * URL |newUrl| +> * ... +> * [=she/directive state=] |activeEntry|'s [=she/directive +> state=] +> 6. If |document|'s is initial about:blank is true, then set historyHandling to "replace". +> 7. If historyHandling is "push", then: +> 1. Increment document's history object's index. +> 2. Set document's history object's length to its index + 1. +> 3. If |newUrl| does not equal |activeEntry|'s URL with exclude +> fragments set to true OR |fragment directive| is not null, then: +>
    Otherwise, when only the fragment has changed and it did not specify +> a directive, the active entry's directive state is reused. This prevents a fragment +> change from clobbering highlights.
    +> 1. Let |historyEntry|'s [=she/directive state=] be a new +> [=/directive state=] with [=directive state/value=] set to |fragment +> directive|. +> 8. Otherwise, if |fragment directive| is not null, set +> |historyEntry|'s [=she/directive state=]'s [=directive state/value=] to |fragment +> directive|. +> 9. If serializedData is not null, then restore the history object state given document and +> newEntry. +>
    -Amend the - -traverse the history steps to process the [=/fragment directive=] -during a history navigation by inserting steps before setting the |newDocument|'s URL (currently -step 6). +In the definition of +create navigation params by fetching: -> Monkeypatching [[HTML]]: +> Monkeypatching [[HTML#populating-a-session-history-entry]]: > -> 6. Let |processedURL| be a copy of entry's URL. -> 7. Run the [=process and consume fragment directive=] steps on -> |processedURL| and |document|. -> 8. Set |newDocument|'s URL to |processedURL|. +>
    To create navigation params by fetching given a session history entry +> |entry|, ...: +> 1. Assert: this is running in parallel. +> 1. ... +>
  • Let currentURL be request's current URL.
  • +> 1. Let commitEarlyHints be null. +> 1. While true: +> 1. If request's reserved client is not null and currentURL's origin is not the same as request's reserved client's creation URL's origin, then: +> 1. ... +>
  • Set currentURL to |locationURL|.
  • +> 1. Let |fragment directive| be the result of running +> [=remove the fragment directive=] on |locationURL|. +> 1. Set |entry|'s URL to currentURL. +> 1. Set |entry|'s URL to |locationURL|. +> 1. Set |entry|'s [=she/directive state=]'s [=directive state/value=] to +> |fragment directive|. +> 1. If |locationURL| is a URL whose scheme is not a fetch scheme, then return a new non-fetch +> scheme navigation params, with initiator origin request's current URL's origin +> 1. ... +>

    - The changes in this section imply that a URL is only stripped of its fragment - directive when it is set on a Document. Notably, since a window's - {{Location}} object is a representation of the [=/URL=] of the [=active - document=], all getters on it will show a fragment-directive-stripped + Since a Document is populated from a history entry, its [=Document/URL=] will not include the + fragment directive. Similarly, since a window's {{Location}} object is a representation of the + [=/URL=] of the [=active document=], all getters on it will show a fragment-directive-stripped version of the URL.

    +

    + Additionally, since the {{HashChangeEvent}} is + + fired in response to a changed fragment between URLs of session history entries, + hashchange will not be fired if a navigation or traversal changes only the fragment + directive. +

    +

    Some examples are provided to help clarify various edge cases.

    @@ -378,41 +512,52 @@ step 6).
    ``` - window.location = 'https://example.com#foo:~:bar'; + window.location = "https://example.com#page1:~:hello"; + console.log(window.location.href); // 'https://example.com#page1' + console.log(window.location.hash); // '#page1' ``` - The page loads and when the document's URL is set the fragment directive is - stripped out during the "create and initialize a Document object" steps. + The initial navigation created a new session history entry. The entry's URL is stripped of the + fragment directive: "https://example.com#page1". The entry's directive state value is set to + "hello". Since the document is populated from the entry, web APIs don't include the fragment + directive in URLs. ``` - console.log(window.location.href); // 'https://example.com#foo' - console.log(window.location.hash); // '#foo' + location.hash = "page2"; + console.log(location.href); // 'https://example.com#page2' ``` - Since same document navigations are made by adding a new session history - entry and using the "traverse the history" steps, the the fragment directive - will be stripped here as well. + A same document navigation changed only the fragment. This adds a new session history entry in the + navigate to + a fragment steps. However, since only the fragment changed, the new entry's directive state + points to the same state as the first entry, with a value of "bar". ``` - window.location.hash = 'fizz:~:buzz'; - console.log(window.location.href); // 'https://example.com#fizz' - console.log(window.location.hash); // '#fizz' + onhashchange = () => console.assert(false, "hashchange doesn't fire."); + location.hash = "page2:~:world"; + console.log(location.href); // 'https://example.com#page2' + onhashchange = null; ``` - The hashchange event is dispatched when only the fragment directive changes - because the comparison for it is done on the URLs in the session history - entries, where the fragment directive hasn't been removed. + A same document navigation changes only the fragment but includes a fragment directive. Since an + explicit directive was provided, the new entry includes its own directive state with a value of + "fizz". + + The hashchange event is not fired since the page-visible fragment is unchanged; only the fragment + directive changed. This is because the comparison for hashchange is done on the URLs in the + session history entries, where the fragment directive has been removed. ``` - onhashchange = () => {console.log('HASHCHANGE');}; - window.location.hash = 'fizz:~:zillch'; // 'HASHCHANGE' - console.log(window.location.href); // 'https://example.com#fizz' - console.log(window.location.hash); // '#fizz' + history.pushState("", "", "page3"); + console.log(location.href); // 'https://example.com/page3' ``` + + pushState creates a new session history entry for the same document. However, since the + non-fragment URL has changed, this entry has its own directive state with value currently null.
    - In other cases where a Document's URL is not set by the UA, there is no + In other cases where a URL is not set to a session history entry, there is no fragment directive stripping. For URL objects: @@ -439,97 +584,39 @@ step 6). ```
    -
    - History pushState will create a session history entry where the URL's - fragment directive isn't stripped. However, traversing to the entry will - cause it to set its URL on the document which will process the fragment - directive before setting it on the Document (but the fragment directive - remains on the entry). +### Applying directives to a document ### {#applying-directives-to-a-document} +The section above described how the [=fragment directive=] is separated from the URL and stored in a +session history entry. - ``` - history.pushState({}, 'title', 'index.html#foo:~:bar'); - window.location = 'newpage.html'; - // on newpage.html - history.back(); - ``` - - Results in the current document having "bar" as the fragment directive. -
    - - -### Parsing the fragment directive ### {#parsing-the-fragment-directive} - -A text directive is a kind of [=directive=] representing a range of -text to be indicated to the user. It is a struct that consists of -four strings: start, -end, -prefix, and -suffix. [=text directive/start=] -is required to be non-null. The other three items may be set to null, -indicating they weren't provided. The empty string is not a valid value for any -of these items. +This section defines how and when navigations and traversals make use of history entry's [=she/directive +state=] to apply the directives associated with a session history entry to a [=Document=]. -See [[#syntax]] for the what each of these components means and how they're -used. - -
    +> Monkeypatching [[DOM#interface-document]]: +> +> Each document has an associated uninvoked directives which is either +> null or an ASCII string holding data used by the UA to process the resource. It is initially +> null. -To parse a text directive, on an ASCII string |text -directive input|, run these steps: +In the definition of +update document for history step application: -
    -

    - This algorithm takes a single text directive string as input (e.g. - "text=prefix-,foo,bar") and attempts to parse the string into the - components of the directive (e.g. ("prefix", "foo", "bar", null)). See - [[#syntax]] for the what each of these components means and how they're - used. -

    -

    - Returns null if the input is invalid or fails to parse in any way. - Otherwise, returns a [=text directive=]. -

    -
    +> Monkeypatching [[HTML#updating-the-document]]: +> +>
    To update document for history step application given a Document +> |document|, a session history entry |entry|,... +> 1. ... +>
  • Set |document|'s history object's length to scriptHistoryLength
  • +> 5. If documentsEntryChanged is true, then: +> 1. Let oldURL be |document|'s latest entry's URL. +> 2. If |document|'s latest entry's [=she/directive state=] is not |entry|'s +> [=she/directive state=] then set |document|'s [=Document/uninvoked directives=] to |entry|'s +> [=she/directive state=]'s [=directive state/value=]. +> 3. Set |document|'s latest entry to |entry| +> 4. ... +>
    -
      - 1. [=/Assert=]: |text directive input| matches the production [=TextDirective=]. - 1. Let |textDirectiveString| be the substring of |text directive - input| starting at index 5. -
      - This is the remainder of the |text directive input| following, - but not including, the "text=" prefix. -
      - 1. Let |tokens| be a list of strings that is the result of - splitting |textDirectiveString| on commas. - 1. If |tokens| has size less than 1 or greater than 4, return null. - 1. If any of |tokens|'s items are the empty string, return null. - 1. Let |retVal| be a [=text directive=] with each of its items initialized - to null. - 1. Let |potential prefix| be the first item of |tokens|. - 1. If the last character of |potential prefix| is U+002D (-), then: - 1. Set |retVal|'s [=text directive/prefix=] to the - [=string/percent-decode|percent-decoding=] of the result of removing the - last character from |potential prefix|. - 1. Remove the first item of the list |tokens|. - 1. Let |potential suffix| be the last item of |tokens|, if one exists, null - otherwise. - 1. If |potential suffix| is non-null and its first character is U+002D (-), - then: - 1. Set |retVal|'s [=text directive/suffix=] to the - [=string/percent-decode|percent-decoding=] of the result of removing the - first character from |potential suffix|. - 1. Remove the last item of the list |tokens|. - 1. If |tokens| has size not equal to 1 nor 2 then - return null. - 1. Set |retVal|'s [=text directive/start=] be the - [=string/percent-decode|percent-decoding=] of the first item of |tokens|. - 1. If |tokens| has size 2, then set |retVal|'s - [=text directive/end=] be the - [=string/percent-decode|percent-decoding=] of the last item of |tokens|. - 1. Return |retVal|. -
    -
    +### Parsing the fragment directive ### {#parsing-the-fragment-directive} ### Fragment directive grammar ### {#fragment-directive-grammar} @@ -607,6 +694,246 @@ it matches the production:
    "%" [a-zA-Z0-9]+
    +## Text Directives ## {#text-directives} + +A text directive is a kind of [=/directive=] representing a range of +text to be indicated to the user. It is a struct that consists of +four strings: start, +end, +prefix, and +suffix. [=text directive/start=] +is required to be non-null. The other three items may be set to null, +indicating they weren't provided. The empty string is not a valid value for any +of these items. + +See [[#syntax]] for the what each of these components means and how they're +used. + +
    + +To parse a text directive, on an ASCII string |text +directive input|, run these steps: + +
    +

    + This algorithm takes a single text directive string as input (e.g. + "text=prefix-,foo,bar") and attempts to parse the string into the + components of the directive (e.g. ("prefix", "foo", "bar", null)). See + [[#syntax]] for the what each of these components means and how they're + used. +

    +

    + Returns null if the input is invalid or fails to parse in any way. + Otherwise, returns a [=text directive=]. +

    +
    + +
      + 1. [=/Assert=]: |text directive input| matches the production [=TextDirective=]. + 1. Let |textDirectiveString| be the substring of |text directive + input| starting at index 5. +
      + This is the remainder of the |text directive input| following, + but not including, the "text=" prefix. +
      + 1. Let |tokens| be a list of strings that is the result of + splitting |textDirectiveString| on commas. + 1. If |tokens| has size less than 1 or greater than 4, return null. + 1. If any of |tokens|'s items are the empty string, return null. + 1. Let |retVal| be a [=text directive=] with each of its items initialized + to null. + 1. Let |potential prefix| be the first item of |tokens|. + 1. If the last character of |potential prefix| is U+002D (-), then: + 1. Set |retVal|'s [=text directive/prefix=] to the + [=string/percent-decode|percent-decoding=] of the result of removing the + last character from |potential prefix|. + 1. Remove the first item of the list |tokens|. + 1. Let |potential suffix| be the last item of |tokens|, if one exists, null + otherwise. + 1. If |potential suffix| is non-null and its first character is U+002D (-), + then: + 1. Set |retVal|'s [=text directive/suffix=] to the + [=string/percent-decode|percent-decoding=] of the result of removing the + first character from |potential suffix|. + 1. Remove the last item of the list |tokens|. + 1. If |tokens| has size not equal to 1 nor 2 then + return null. + 1. Set |retVal|'s [=text directive/start=] be the + [=string/percent-decode|percent-decoding=] of the first item of |tokens|. + 1. If |tokens| has size 2, then set |retVal|'s + [=text directive/end=] be the + [=string/percent-decode|percent-decoding=] of the last item of |tokens|. + 1. Return |retVal|. +
    +
    + +### Invoking Text Directives ### {#invoking-text-directives} + +This section describes how text directives in a document's [=Document/uninvoked directives=] are +processed and invoked to cause indication of the relevant text passages. + +
    + The summarized changes in this section: + + * Modify the indicated part processing model to try processing [=Document/uninvoked directives=] + into a [=range=] that will be returned as the indicated part. + * Modify "scrolling to a fragment" to correctly scroll and set the Document's target element in the case + of a [=range=] based indicated part. + * Ensure [=Document/uninvoked directives=] is reset to null when the user agent has finished the + fragment search for the current navigation/traversal. + * If the user agent finishes searching for a text directive, ensure it tries the regular + fragment as a fallback. +
    + +In +indicated part, enable a fragment to indicate a [=range=]. Make the following changes: + +> Monkeypatching [[HTML#scrolling-to-a-fragment]]: +> +>
    +> For an HTML document |document|, the following processing model must be followed to determine +> its indicated part: +> +> 1. Let |directives| be the document's [=Document/uninvoked directives=]. +> +> 1. If |directives| is non-null and |document|'s [=document/allow text +> fragment scroll=] is true then: +> 1. Let |ranges| be a list that is the result of running +> the [=invoke text directives=] steps with |directives| and the document. +> 1. If |ranges| is non-empty, then: +> 1. Let |firstRange| be the first item of |ranges|. +> 1. Visually indicate each [=range=] in |ranges| in an +> [=implementation-defined=] way. The indication must not be observable from author +> script. See [[#indicating-the-text-match]]. +>
    +> The first [=range=] in |ranges| is the one that gets scrolled into view but all +> ranges should be visually indicated to the user. +>
    +> 1. Set |firstRange| as |document|'s indicated part, return. +> 1. Let fragment be document's URL's fragment. +> 1. If fragment is the empty string, then return the special value top of the document. +> 1. Let potentialIndicatedElement be the result of finding a potential indicated element given +> document and fragment. +> 1. ... +> +>
    + +In scroll to the fragment, handle a indicated part that is a [=range=] and also +prevent fragment scrolling if the force-load-at-top policy is enabled. Make the following changes: + +> Monkeypatching [[HTML#scrolling-to-a-fragment]]: +> +>
    +> 1. If document's indicated part is null, then set document's target element to null. +> 2. Otherwise, if document's indicated part is top of the document, then: +> 1. Set document's target element to null. +> 2. Scroll to the beginning of the document for document. +> 3. Return. +> 3. Otherwise: +> 1. Assert: document's indicated part is an element or it is a [=range=]. +> 2. Let |scrollTarget| be |document|'s indicated part. +> 3. Let |target| be |scrollTarget|. +> 4. If |target| is a [=range=], then: +> 1. Set |target| to be the [=first common ancestor=] of |target|'s +> [=range/start node=] and [=range/end node=]. +> 2. While |target| is non-null and is not an [=element=], set |target| to +> |target|'s [=tree/parent=]. +>
    +> What should be set as target if inside a shadow tree? +> #190 +>
    +> 4. Assert: |target| is an [=element=]. +> 5. Set |document|'s target element to |target|. +> 6. Run the ancestor details revealing algorithm on |target|. +> 7. Run the ancestor hidden-until-found revealing algorithm on |target|. +>
    +> These revealing algorithms currently wont work well since |target| could be an +> ancestor or even the root document node. Issue +> #89 proposes +> restricting matches to `contain:style layout` blocks which would resolve this +> problem. +>
    +> 8. Get the policy +> value for `force-load-at-top` for |document|. If the result is false: +> 1. [=scroll a target into view=], +> with target set to |scrollTarget|, behavior set to "auto", block set to "center", and +> inline set to "nearest". +> +> Implementations MAY avoid scrolling to the target if it is +> produced from a [=text directive=]. +> +>
    +> force-load-at-top should be checked only when a new document is being +> loaded. +> #186 +>
    +> 9. Scroll target into view, with behavior set to "auto", block set to +> "start", and inline set to "nearest". +> 10. Run the focusing steps for target, with the Document's viewport as the fallback target. +>
    Implementation note: Blink doesn’t currently set focus for text +> fragments, it probably should? TODO: file crbug.
    +> 11. Move the sequential focus navigation starting point to target. +> +>
    + +The next two monkeypatches ensure the user agent clears [=Document/uninvoked directives=] when +the fragment search is complete. In the case where a text directive search finishes because parsing +has stopped, it tries one more search for a non-text directive fragment. + +In the definition of +try to scroll to the fragment: + +> Monkeypatching [[HTML#scrolling-to-a-fragment]]: +> +>
    +> To try to scroll to the fragment for a Document |document|, perform the following steps in +> parallel: +> 1. Wait for an implementation-defined amount of time. (This is intended to allow the user agent +> to optimize the user experience in the face of performance concerns.) +> 2. Queue a global task on the navigation and traversal task source given document's relevant +> global object to run these steps: +> 1. If document has no parser, or its parser has stopped parsing, or the user agent +> has reason to believe the user is no longer interested in scrolling to the fragment, then +> abort these steps. +>
  • If the user agent has reason to believe the user is no longer interested in scrolling to +> the fragment, then: +> 1. Set [=Document/uninvoked directives=] to null. +> 1. Abort these steps. +> 1. If the document has no parser, or its parser has stopped parsing, +> then:
  • +> 1. If [=Document/uninvoked directives=] is not null, then: +> 1. Set [=Document/uninvoked directives=] to null. +> 1. Scroll to the fragment given |document|. +> 1. Abort these steps. +> 2. Scroll to the fragment given document. +> 3. If document's indicated part is still null, then try to scroll to the fragment for +> document. Otherwise, set [=Document/uninvoked directives=] to +> null. + +In the definition of + +navigate to a fragment: + +> Monkeypatching [[HTML#scroll-to-fragid]]: +> +>
    To navigate to a fragment given navigable |navigable|, ...: +> 1. ... +>
  • Update document for history step application given navigable's active +> document, historyEntry, true, scriptHistoryIndex, and scriptHistoryLength.
  • +> 9. Scroll to the fragment given navigable's active document. +>
  • Set |navigable|'s active document's [=Document/uninvoked directives=] to +> null.
  • +> 11. Let traversable be navigable's traversable navigable. +> 12. ... + +Scrolling to the indicated part is only one of several things that happens from "scroll to the +fragment". Rename it and related definitions: + +> Monkeypatching [[HTML#scroll-to-fragid]]: +> +> Rename [[HTML#scroll-to-fragid]] and related steps to "indicating a fragment" to reflect it's +> broader effects. + ## Security and Privacy ## {#security-and-privacy} ### Motivation ### {#motivation} @@ -838,7 +1165,7 @@ and initialize a Document object steps by adding the following steps before > activated per user-activated navigation. >
    > 16. Set |document|'s [=document/allow text fragment scroll=] by following these sub-steps: -> 1. If |document|'s [=Document/fragment directive=] field is null or empty, set +> 1. If |document|'s [=Document/uninvoked directives=] field is null or empty, set > [=document/allow text fragment scroll=] to false and abort these sub-steps. > 1. Let |text directive user activation| be the value of |document|'s > [=document/text directive user activation=] and set |document|'s @@ -938,127 +1265,6 @@ indicated part processing model to return a [=range=], rather than an [=element=], that will be scrolled into view.
    -To enable the scroll to the fragment algorithm to operate on a -[=range=] indicated part, replace step 3 of this algorithm as follows: - - - -> Monkeypatching [[HTML]]: -> -> Replace: -> -> 1. Assert: document's indicated part is an element. -> 1. Let target be document's indicated part. -> 1. Set document's target element to target. -> 1. Run the ancestor details revealing algorithm on target. -> 1. Run the ancestor hidden-until-found revealing algorithm on target. -> 1. Scroll target into view, with behavior set to "auto", block set to "start", and inline set to "nearest". -> 1. Run the focusing steps for target, with the Document's viewport as the fallback target. -> 1. Move the sequential focus navigation starting point to target. -> -> With: -> -> 1. Assert: document's indicated part is a [=range=]. -> 1. Let |range| be the [=range=] that is |document|'s -> indicated part. -> 1. Let |target| be the [=first common ancestor=] of |range|'s -> [=range/start node=] and [=range/end node=]. -> 1. While |target| is non-null and is not an [=element=], set |target| to -> |target|'s [=tree/parent=]. ->
    -> What should be set as target if inside a shadow tree? -> #190 ->
    -> 1. Set |document|'s [=target element=] to |target|. -> 1. Run the ancestor details revealing algorithm on target. -> 1. Run the ancestor hidden-until-found revealing algorithm on target. ->
    -> These revealing algorithms currently wont work well since |target| -> could be an ancestor or even the root document node. Issue -> #89 -> proposes restricting matches to `contain:style layout` blocks which -> would resolve this problem. ->
    -> 1. Get -> the policy value for `force-load-at-top` in the -> [=Document=]. If the result is false: -> 1. If |range| wasn't produced as a result of a text fragment, or if the -> UA supports scrolling of text fragments on navigation, invoke -> -> scroll a target into view, with target set to |range|, -> containingElement |target|, behavior set to "auto", -> block set to "center", and inline set to -> "nearest". -> -> ->
    -> force-load-at-top should be checked only when a new -> document is being loaded. -> #186 ->
    -> 1. Let |start node| be |range|'s [=range/start node=]. -> 1. Run the focusing steps for |start node|, with the Document's viewport as the fallback target. ->
    -> Implementation note: Blink doesn't currently set focus for text -> fragments, it probably should? TODO: file crbug. ->
    -> 1. Move the sequential focus navigation starting point to |start node|. - -To enable a fragment to indicate a range of text, add the following steps to the -beginning of the processing model for the [=HTML Document=]'s -indicated part -so that the indicated part is a [=range=]: - -> Monkeypatching [[HTML]]: -> -> 1. Let |fragment directive string| be the document's [=Document/fragment directive=]. -> 1. If the document's [=document/allow text fragment scroll=] is true then: -> 1. Let |ranges| be a list that is the result of running -> the [=process a fragment directive=] steps with |fragment directive -> string| and the document. -> 1. If |ranges| is non-empty, then: -> 1. Let |range| be the first item of |ranges|. ->
    -> The first [=range=] in |ranges| is specifically -> scrolled into view. This [=range=], along with the -> remaining |ranges| should be visually indicated in a way that -> is not revealed to script, which is left as UA-defined behavior. ->
    -> 1. Set |range| as |document|'s indicated part, return. - -In order for the indicated part to return a [=range=] for regular element -fragments, modify the - -find a potential indicated element steps as follows: - -> Monkeypatching [[HTML]]: -> -> Replace: -> -> 1. If there is an element in the document tree whose root is -> document and that has an ID equal to fragment, then return the first -> such element in tree order. -> 1. If there is an a element in the document tree whose root is -> document that has a name attribute whose value is equal to fragment, -> then return the first such element in tree order. -> 1. Return null. -> -> With: -> -> 1. Let |element| be an [=Element=], initially null. -> 1. If there is an element in the document tree whose root is document and -> that has an ID equal to fragment, set |element| to the first such -> element in tree order. -> 1. Otherwise, if there is an element in the document tree whose root is -> document that has a name attribute whose value is equal to fragment, -> then set |element| to the first such element in tree order. -> 1. If |element| is null, return null. -> 1. Otherwise, return a [=range=] with [=range/start=] (|element|, 0) and -> [=range/end=] (|element|, |element|'s [=Node/length=]). -> -> And rename this algorithm and the returned variables. -
    To find the first common ancestor of two nodes |nodeA| and |nodeB|, follow these steps: @@ -1107,23 +1313,23 @@ To find the shadow-including parent of |node| follow these steps: regardless of how many other directives are provided or their match result. If a directive successfully matches to text in the document, it returns a - [=range=] indicating that match in the document. The [=process a fragment - directive=] steps are the high level API provided by this section. These - return a list of [=ranges=] that were matched by the - individual directive matching steps, in the order the directives were + [=range=] indicating that match in the document. The + [=invoke text directives=] steps are the high level API provided by this + section. These return a list of [=ranges=] that were matched + by the individual directive matching steps, in the order the directives were specified in the fragment directive string. If a directive was not matched, it does not add an item to the returned list.
    -
    -To process a fragment directive, given as input an ASCII string |fragment directive input| and a [=Document=] +
    +To invoke text directives, given as input an ASCII string |text directives| and a [=Document=] |document|, run these steps:
    - This algorithm takes as input a |fragment directive input|, that is the + This algorithm takes as input a |text directives|, that is the raw text of the fragment directive and the |document| over which it operates. It returns a list of [=ranges=] that are to be visually indicated, the first of which will be scrolled into view (if the UA scrolls @@ -1131,11 +1337,11 @@ spec=infra>ASCII string |fragment directive input| and a [=Document=]
      - 1. If |fragment directive input| is not a [=valid fragment directive=], then + 1. If |text directives| is not a [=valid fragment directive=], then return an empty list. 2. Let |directives| be a list of ASCII strings that is the result of [=strictly split a string|strictly splitting the - string=] |fragment directive input| on "&". + string=] |text directives| on "&". 3. Let |ranges| be a list of [=ranges=], initially empty. 4. For each ASCII string |directive| of |directives|: 1. If |directive| does not match the production [=TextDirective=], @@ -1687,10 +1893,10 @@ The UA should provide to the user some method of dismissing the match, such that the matched text no longer appears visually indicated. The exact appearance and mechanics of the indication are left as UA-defined. -However, the UA must not use the Document's selection to -indicate the text match as doing so could allow attack vectors for content -exfiltration. +However, the UA must not use any methods observable by author script, such as +the Document's +selection, to indicate the text match. Doing so could allow attack vectors +for content exfiltration. The UA must not visually indicate any provided context terms. diff --git a/index.html b/index.html index d65c0ab..3357820 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,15 @@ +