URL 片段文本指令

社区组草案报告,

此版本:
https://wicg.github.io/scroll-to-text-fragment/
问题追踪:
GitHub
规范内标注
编辑者:
(Google)
(Google)
测试套件:
https://wpt.fyi/results/scroll-to-text-fragment/

摘要

文本指令为 URL 片段增加了指定文本片段的支持。 当导航到带有此类片段的 URL 时,用户代理可以快速突出显示并/或将其引起用户的注意。

本文件状态

本规范由Web 平台孵化社区组发布。 它不是 W3C 标准,也不在 W3C 标准轨道上。 请注意,根据W3C 社区贡献者许可协议(CLA),存在有限的选择退出权和其它适用条件。 详细了解W3C 社区与业务组

1. 基础结构

本规范依赖于 Infra 标准。[INFRA]

2. 简介

本节为非规范性内容

2.1. 使用场景

2.1.1. 网页文本引用

文本片段的核心用例是允许 URL 作为跨网页的精确文本引用。例如,维基百科的引用可以链接到它们所引用页面上的确切文本。同样,搜索引擎可以提供 URL,将用户直接定位到页面中他们所寻找的答案,而不是链接到页面顶部。

2.1.2. 用户分享

借助文本指令,浏览器可以实现“复制到此处 URL”的选项,当用户在文本选区上打开上下文菜单时,浏览器即可生成带有适当文本选区指定的 URL,URL 的接收者将方便地看到指定的文本被指示出来。如果没有文本片段,用户若想分享页面上一段文字,通常只是复制并粘贴该段文字,这样接收者就失去了页面的上下文。

本规范力图最大化文本指令链接的有效期,例如,使用实际文本内容作为 URL 载荷,并允许使用元素 id 片段作为备用。然而,网页经常会更新和变更其内容。因此,此类链接可能会“失效”,即它们指向的文本内容在目标页面上不再存在。

尽管有这个问题,文本指令链接依然有用。在用户分享场景中,这类链接通常是临时的,仅用于发送后的短时间内。对于更长期的引用和网页链接等用例,文本指令依然有价值,因为它们能优雅降级为普通链接。此外,过期文本指令的存在也可以为用户展现有用的信息,以帮助用户理解原作者的用意,并提示页面内容自链接创建以来可能已发生更改。

关于如何创建健壮的文本指令链接,请参见§ 4 生成文本片段指令中的最佳实践。

3. 描述

3.1. 指示

本节为非规范性内容

本规范有意不定义用户代理如何“指示”文本匹配。有多种不同的体验和权衡,用户代理可以做出。可能的操作示例包括:

操作方式的选择可能对用户安全与隐私产生影响。详细内容见§ 3.5 安全与隐私

3.2. 语法

本节为非规范性内容

文本指令片段指令中指定(见§ 3.3 片段指令),格式如下:

#:~:text=[prefix-,]start[,end][,-suffix]
          context  |--match--|  context

(方括号表示可选参数)

文本参数在匹配前会进行百分号解码。连字符(-)、和号(&)以及逗号(,)等字符在文本参数中会被百分号转义,以避免被解析为文本指令语法的一部分。

唯一必选的参数是start。如果只指定了start,则以该精确字符串首次出现处为目标文本。

#:~:text=an%20example%20text%20fragment 表明精确文本“an example text fragment”为目标文本。

如果还指定了end参数,则文本指令指向页面中的一段文本范围。目标文本范围是从首次出现的start开始,到其后首次出现的end为止。这等价于在start参数中指定完整的文本范围,但可避免因过长的文本指令导致URL膨胀。

#:~:text=an%20example,text%20fragment 表示从首次出现的“an example”到接下来的首次出现的“text fragment”为目标文本。

3.2.1. 上下文术语

本节为非规范性内容

另外两个可选参数即为上下文术语。它们由连字符(-)连接在前缀和后缀处,以区别于startend参数,可任意组合指定。

上下文术语用于消歧目标文本片段。上下文术语可分别指定文本片段之前(前缀)和之后(后缀)紧邻的文本内容,允许存在空白字符。

只有在上下文术语包围目标文本片段时匹配才算成功,且上下文术语与文本片段之间可有任意数量空白字符。这允许上下文术语跨元素边界,比如当目标文本片段位于段落开头且需要以上一个元素的文本作为前缀消歧时。

上下文术语不是目标文本片段的一部分,也不会被视觉指示。

#:~:text=this%20is-,an%20example,-text%20fragment 可匹配“this is an example text fragment”中的“an example”,但不会匹配“here is an example text”中的“an example”。

3.2.2. 双向文本注意事项

本节为非规范性内容
参见 Unicode 双向算法基础,了解双向文本的工作原理。

由于 URL 字符串为 ASCII 编码,其本身不支持双向文本。但我们想要在页面上定位的内容可能是 LTR(从左到右)、RTL(从右到左)或双向文本。本节直观描述了规范中进一步章节隐含的行为。

文本片段中每个术语的字符都处于逻辑顺序,即母语读者阅读的顺序(也是内存中字符的存储顺序)。

同样,prefixstart表示另一个术语前的内容,suffixend表示后续内容,均为逻辑顺序。

注意:用户代理可将 URL 以更符合本地读者习惯的方式呈现,比如显示为 Unicode。但 URL 的字符串表达始终是纯 ASCII 字符。

假设我们要选择مِصر‎(埃及,阿拉伯语),前面是البحرين‎(巴林,阿拉伯语)。先对各个术语进行百分号编码:

مِصر‎ 变为 "%D9%85%D8%B5%D8%B1"(注:UTF-8 字符 [0xD9,0x85] 为该阿拉伯词的第一个(最右)字符)。

البحرين‎ 变为 "%D8%A7%D9%84%D8%A8%D8%AD%D8%B1%D9%8A%D9%86"

文本片段将如下:

:~:text=%D8%A7%D9%84%D8%A8%D8%AD%D8%B1%D9%8A%D9%86-,%D9%85%D8%B5%D8%B1

浏览器地址栏可按自然 RTL 方向渲染文本,呈现如下:

:~:text=البحرين-,مِصر

3.3. 片段指令

为避免与现有 URL 片段的使用发生兼容性问题,本规范引入了片段指令的概念。它是 URL 片段中位于片段指令分隔符之后的部分,也可能为 null(如果片段中未出现分隔符)。

片段指令分隔符为字符串 ":~:",即连续三个码点 U+003A (:)、U+007E (~)、U+003A (:)。

片段指令属于 URL 片段。这意味着它总是在 URL 中 U+0023 (#) 之后出现。
要为 https://example.com 添加片段指令,先加上片段:https://example.com#:~:text=foo。

片段指令会被解析并处理为单个指令,即指示用户代理执行某项操作的指令。片段指令中可出现多个指令。

本规范目前只引入了文本指令,未来可引入其他指令。
https://example.com#:~:text=foo&text=bar&unknownDirective

包含 2 个文本指令和 1 个未知指令。

为防止影响页面运行,片段指令会从脚本可访问的 API 中剥离,防止与作者脚本交互。这也确保将来可安全添加更多指令,而不会有 web 兼容性风险。

3.3.1. 提取片段指令

本节描述了如何将片段指令从脚本中隐藏,以及它如何适配 HTML § 7.4 导航和会话历史

本节变化汇总如下:

HTML § 7.4.1 会话历史中,定义 指令状态

猴补丁 HTML § 7.4.1 会话历史

指令状态 保存会创建会话历史条目时的片段指令值,并用于每次遍历此条目时执行指令(如文本高亮)。其结构为:

同一 指令状态可被多个会话历史条目共享。

片段指令在 URL 设置到会话历史条目前会被移除,而存储于指令状态对象。这避免片段指令暴露给脚本 API,使其可独立指定、不干扰页面业务。

指令状态是对象而非原始字符串,因为多个连续历史条目可共用同一状态。仅当遍历导致指令状态从一条切换到另一条时,指令(如查找/高亮)才会被处理。

会话历史条目 的定义处补充:

猴补丁 HTML § 7.4.1.1 会话历史条目

会话历史条目是如下成员的结构体:
  • ...

  • 持久化用户状态,实现细节定义,初值为 null

  • 指令状态,一种指令状态, 初值为一个新的指令状态

增加一个帮助算法,可从URL中移除并返回片段指令字符串:

猴补丁 [HTML]

本算法让 URL 片段以片段指令分隔符 处截断。返回的片段指令包含分隔符后的所有字符(不含分隔符本身)。
TODO: 若 URL 片段以 ':~:' 结尾(即空指令),此时会返回 null,等同于未显式指定指令(且不会覆盖已有指令)。但或许此时应该返回空字符串?这样页面即可通过导航/pushState 到 '#:~:' 来显式清除指令/高亮。

要从URL url移除片段指令,执行:

  1. raw fragmenturlfragment

  2. fragment directive为 null。

  3. raw fragment非空且包含片段指令分隔符

    1. position为字符串中首次出现片段指令分隔符第一个码点的位置变量;若无,则为尾部。

    2. new fragmentraw fragment从头至position之间的字符串。

    3. position增加分隔符码点长度

    4. 如果position未越界:

      1. fragment directiveraw fragmentposition起至末尾的子串。

    5. urlfragmentnew fragment

  4. 返回fragment directive

https://example.org/#test:~:text=foo 会被解析为 fragment="test",片段指令="text=foo"。

下述四个猴补丁修改了会话历史条目的创建。当 URL 可能包含片段指令时,将其移除并存于指令状态

navigate定义中:

猴补丁 HTML § 7.4.2.2 开始导航

导航 navigable 至 URL url...:
  1. ...

  2. 将 navigable 的 ongoing navigation 设为 navigationId。
  3. 若 url 的 scheme 为 "javascript",则...

  4. 并行执行以下步骤:

    1. ...

    2. 若 url 为 about:blank,则将 documentState 的 origin 设为 documentState 的 initiator origin。
    3. 否则,如 url 为 about:srcdoc,则 documentState 的 origin 设为 navigable 的父文档 origin。

    4. Let historyEntry be a new session history entry, with its URL set to url and its document state set to documentState.
    5. fragment directive为对url执行移除片段指令结果。
    6. directive state为新的指令状态,其设为fragment directive

    7. 创建会话历史条目 historyEntry,URL 设为url,文档状态设为 documentState,指令状态设为directive state

    8. 设 navigationParams 为 null。

    9. ...

navigate to a fragment定义中:

猴补丁 HTML § 7.4.2.3.3 片段导航

导航至片段 navigable navigable,...:
  1. directive state为 navigable 当前活动会话历史条目的指令状态

  2. fragment directive为执行移除片段指令url的结果。

  3. fragment directive非空:

    否则,仅片段变化且未指定指令则重用活动条目的指令状态,可防止片段变化后高亮丢失。
    1. directive state为新的指令状态fragment directive

  4. 创建会话历史条目 historyEntry:

    • URL url

    • 文档状态为 navigable 当前活动会话历史条目的文档状态

    • 滚动恢复模式为 navigable 当前活动会话历史条目的滚动恢复模式

    • 指令状态directive state

  5. 如 historyHandling 为"replace",则 entryToReplace 为 navigable 当前活动会话历史条目,否则为 null。

  6. ...

URL 和 历史更新步骤定义中:

猴补丁 HTML § 7.4.4 非片段同步“导航”

“URL 和历史更新步骤”,入参 Document document,...:
  1. navigabledocument的 node navigable。

  2. activeEntrynavigable当前活动会话历史条目。

  3. fragment directive为对newUrl执行移除片段指令得到的结果。

  4. 新建会话历史条目historyEntry,其中

  5. document 的 is initial about:blank 为 true,设 historyHandling 为"replace"。

  6. 如 historyHandling 为"push",则:

    1. 历史对象 index 加一。

    2. 历史对象长度设置为 index+1。

    3. newUrl不等于activeEntry的(排除 fragment 后)URL,或fragment directive非空,则:

      否则仅片段变更且未指定指令,则重用激活条目的指令状态,防止片段变更后高亮丢失。
      1. historyEntry指令状态设为新的指令状态, 其设为fragment directive

  7. 否则如fragment directive非空,将historyEntry指令状态设为fragment directive

  8. 如 serializedData 不为空,则恢复给定 document 和 newEntry 的历史对象状态。

create navigation params by fetching定义中:

猴补丁 HTML § 7.4.5 填充会话历史条目

“通过获取创建导航参数”,入参会话历史条目entry,...:
  1. 断言:此步骤在并行执行。

  2. ...

  3. 令 currentURL 为请求的当前 URL。
  4. 令 commitEarlyHints 为 null。

  5. 循环:

    1. 如请求的 reserved client 非空且 currentURL 的源与 reserved client 的创建 URL 源不同,则:

    2. ...

    3. 将 currentURL 设为 locationURL
    4. fragment directive为对locationURL执行移除片段指令的结果。

    5. entry 的 URL 设为 currentURL。
    6. entry 的 URL 设为 locationURL

    7. entry指令状态设为fragment directive

    8. locationURL 的 scheme 不是 fetch scheme,则返回新的非 fetch scheme 导航参数,initiator origin 为请求的 current URL 源

    9. ...

由于文档由历史条目填充,其URL 并不包含片段指令。同理,window 的 Location 对象是URL的显示(当前活动文档),其所有 getter 都返回去除片段指令的版本。

同时,HashChangeEvent片段变化时 会触发,但如果导航或遍历仅更改了片段指令,则不会触发 hashchange

下例用于说明各种边界情况。

window.location = "https://example.com#page1:~:hello";
console.log(window.location.href); // 'https://example.com#page1'
console.log(window.location.hash); // '#page1'

首次导航创建了新的会话历史条目,该条目的 URL 已去除片段指令:"https://example.com#page1"。条目的指令状态值为 "hello"。由于文档由条目填充,web API 返回的 URL 不含片段指令。

location.hash = "page2";
console.log(location.href); // 'https://example.com#page2'

同文档导航仅更改片段。这会在 navigate to a fragment 步骤中新建一个历史条目。但因为只变更片段,新条目的指令状态仍引用首个条目的状态,值为 "bar"。

onhashchange = () => console.assert(false, "hashchange doesn’t fire.");
location.hash = "page2:~:world";
console.log(location.href); // 'https://example.com#page2'
onhashchange = null;

同文档导航仅更改片段但包含片段指令。此时新条目含有自己的指令状态,值为 "fizz"。

由于页面可见 fragment 未变,仅片段指令变化,因此不会触发 hashchange。原因在于 hashchange 比较会话历史条目的 URL,已移除片段指令。

history.pushState("", "", "page3");
console.log(location.href); // 'https://example.com/page3'

pushState 为同一文档创建了新历史条目。由于非片段 URL 改变,该条目指令状态值初值为 null。

其他未设置到会话历史条目的 URL 不会移除片段指令。

对 URL 对象:

let url = new URL('https://example.com#foo:~:bar');
console.log(url.href); // 'https://example.com#foo:~:bar'
console.log(url.hash); // '#foo:~:bar'

document.url = url;
console.log(document.url.href); // 'https://example.com#foo:~:bar'
console.log(document.url.hash); // '#foo:~:bar'

<a><area> 元素:

<a id='anchor' href="https://example.com#foo:~:bar">Anchor</a>
<script>
  console.log(anchor.href); // 'https://example.com#foo:~:bar'
  console.log(anchor.hash); // '#foo:~:bar'
</script>

3.3.2. 将指令应用于文档

上节介绍了如何将片段指令从URL中分离出来,并存储在会话历史条目中。

本节定义了导航和遍历时,历史条目中的指令状态如何及何时被用来将会话历史条目关联的指令应用到文档上。

猴补丁 DOM § 4.5 文档接口

每个文档都有一个相关的待处理文本指令,它要么为null,要么为列表 (包含文本指令)。初始为null。

历史步骤应用时更新文档的定义中:

猴补丁 HTML § 7.4.6.2 更新文档

更新历史步骤应用时的文档,参数为 Document document,会话历史条目 entry,...
  1. ...

  2. document的历史对象 length 设为 scriptHistoryLength
  3. 如果documentsEntryChanged为真,则:

    1. oldURLdocument的最新条目的URL。

    2. document的最新条目的指令状态entry指令状态不同,则:
      1. fragment directiveentry指令状态

      2. document待处理文本指令设为解析fragment directive的结果。

  4. document的最新条目设为entry

  5. ...

3.3.3. 片段指令语法

注:本节为非规范性内容。

注:本语法仅供参考,解析的规则和步骤将在§ 3.4 文本指令节以命令方式给出。若实际行为与本语法描述有出入,应以那一节具体步骤为准。

FragmentDirective 可包含多个由 “&” 字符分隔的指令。目前允许页面存在多个文本指令以同时标示多段字符串,也为将来添加组合其它指令类型做扩展准备。为保持扩展性,遇到未知指令时不会导致整个解析失败。

若一个字符串匹配下述EBNF(扩展巴科斯范式)产生式,则为合法片段指令:

FragmentDirective ::=
(TextDirective | UnknownDirective) ("&" FragmentDirective)?
TextDirective ::=
"text="CharacterString
UnknownDirective ::=
CharacterString - TextDirective
CharacterString ::=
(ExplicitChar | PercentEncodedByte)*
ExplicitChar ::=
[a-zA-Z0-9] | "!" | "$" | "'" | "(" | ")" | "*" | "+" | "." | "/" | ":" | ";" | "=" | "?" | "@" | "_" | "~" | "," | "-"
ExplicitChar 可为任何URL 码点,除了“&”。

若一个TextDirective匹配下述产生式,则为合法:

ValidTextDirective ::=
"text=" TextDirectiveParameters
TextDirectiveParameters ::=
(TextDirectivePrefix ",")? TextDirectiveString ("," TextDirectiveString)? ("," TextDirectiveSuffix)?
TextDirectivePrefix ::=
TextDirectiveString"-"
TextDirectiveSuffix ::=
"-"TextDirectiveString
TextDirectiveString ::=
(TextDirectiveExplicitChar | PercentEncodedByte)+
TextDirectiveExplicitChar ::=
[a-zA-Z0-9] | "!" | "$" | "'" | "(" | ")" | "*" | "+" | "." | "/" | ":" | ";" | "=" | "?" | "@" | "_" | "~"
TextDirectiveExplicitChar 为任何未在FragmentDirectiveValidTextDirective语法中显式用到的 URL 码点,即“&”“-”“,”。文档中涉及“&”“-”“,”时需百分号编码。
PercentEncodedByte ::=
"%" [a-zA-Z0-9][a-zA-Z0-9]

3.4. 文本指令

文本指令是一种指令,用于代表要向用户指示的文本范围。它是一个包含四个字符串的结构体startendprefixsuffixstart要求非null。其余三项可为null,表示未提供。空字符串在这四项中都是无效值。

各部分含义和用法见§ 3.2 语法

对文本指令项进行百分号解码,输入 字符串 term,执行步骤:
  1. term为 null,则返回 null。

  2. 断言termASCII 字符串

  3. decoded bytes为对term执行百分号解码的结果。

  4. 返回对decoded bytes执行不带 BOM 的 UTF-8 解码结果。

解析文本指令,参数为 字符串 text directive value 时,执行如下步骤:

本算法以单一文本指令值字符串为输入(如 "prefix-,foo,bar"),尝试将其解析为指令组件(如 ("prefix", "foo", "bar", null))。各部分含义和用法见§ 3.2 语法

若输入无效则返回 null,否则返回文本指令

  1. prefixsuffixstartend均为null。

  2. 断言text directive value是无 片段百分号编码集合及U+0026(&)的 ASCII字符串

  3. tokens为对text directive value以U+002C(,)严格分割字符串列表

  4. tokens长度小于1或大于4,则返回null。

  5. tokens首项结尾为U+002D(-):

    1. prefixtokens[0]的从头截取长度=本身长度-1的子串

    2. 移除tokens首项。

    3. prefix为空或包含U+002D(-),则返回null。

    4. tokens,则返回null。

  6. tokens末项起始为U+002D(-):

    1. suffixtokens末项从第1位起至末尾的子串。

    2. 移除tokens末项。

    3. suffix为空或包含U+002D(-),则返回null。

    4. tokens,则返回null。

  7. tokens长度>2,则返回null。

  8. 断言tokens长度为1或2。

  9. starttokens首项。

  10. 移除tokens首项。

  11. start为空,或包含U+002D(-),则返回null。

  12. tokens

    1. endtokens首项。

    2. end为空或含U+002D(-),则返回null。

  13. 返回新文本指令

    prefix
    prefix进行百分号解码
    start
    start进行百分号解码
    end
    end进行百分号解码
    suffix
    suffix进行百分号解码

解析片段指令,参数为ASCII 字符串 fragment directive,执行如下步骤:

本算法将片段指令字符串(即 “:~:” 后的部分)解析为文本指令对象列表。可返回空列表。
  1. directivesfragment directive以U+0026(&)严格分割后的字符串数组。

  2. output为初始为空的文本指令列表

  3. 对每个字符串 directive

    1. directive以 “text=” 开头,则跳过

    2. text directive valuedirective下标5起至末尾的子串。

      注:此处可能为空字符串。
    3. parsed text directive解析文本指令所获。

    4. parsed text directive非空,将其添加output

  4. 返回output

3.4.1. 调用文本指令

本节描述如何处理和调用文档待处理文本指令,以指示相关的文本段落。

本节总结的变更:

指示部分中,允许片段指示范围。做如下修改:

猴补丁 HTML § 7.4.6.3 滚动到片段

对于HTML文档document,必须遵循如下处理模型,以确定其指示部分:
  1. text directives为文档的待处理文本指令

  2. text directives非null:

    1. ranges为对text directives及文档执行调用文本指令步骤所得的列表

    2. ranges非空:

      1. firstRangeranges的首项。

      2. range方式,对ranges内每个范围进行实现自定义的视觉指示。该指示不得从作者脚本中观察到。见§ 3.7 指示文本匹配

        ranges中的第一个范围会滚动至可视区,但所有范围都应高亮指示给用户。
      3. firstRange设为document的指示部分并返回。

  3. 令fragment为文档URL的片段。

  4. 如fragment为空字符串,则返回特殊值“文档顶端”。

  5. 令potentialIndicatedElement为给定文档和片段查找可能的指示元素结果。

  6. ...

滚动到片段中,处理指示部分为范围的情形,并在启用强制顶部加载策略时阻止片段滚动。做如下修改:

猴补丁 HTML § 7.4.6.3 滚动到片段

  1. 若document的指示部分为null,则将document的目标元素设为null。

  2. 否则,如document的指示部分为文档顶端,则:

    1. 将document的目标元素设为null。

    2. 将文档滚动至开头。

    3. 返回。

  3. 否则:

    1. 断言:document的指示部分为元素范围

    2. scrollTargetdocument的指示部分。

    3. targetscrollTarget

    4. target范围

      1. target为该范围的首个公共祖先,即target起始节点结束节点的祖先。

      2. target非null且不是元素时,循环将target设为其父节点

        若处于shadow tree内部目标应如何设?#190
    5. 断言:target元素

    6. document的目标元素设为target

    7. target运行祖先详情揭示算法。

    8. target运行祖先hidden-until-found揭示算法。

      这些揭示算法目前可能效果不好,因为target可能是祖先甚至根节点。#89 建议只匹配 contain:style layout 区块即可解决该问题。
    9. scrollTarget范围blockPosition设为"center",否则为"start"。

      滚动到文本指令时,在区块方向居中显示。
    10. target滚动到视图,行为设为"auto",block设为"start",inline设为"nearest"。
    11. 滚动目标到视图, 参数targetscrollTargetbehavior为"auto",blockblockPositioninline为"nearest"。

      实现 MAY(可选)避免对由文本指令产生的目标滚动。

    12. 为target运行聚焦步骤,文档视口作为后备目标。

      实现注:Blink当前不会为文本片段设置焦点,应当补充?TODO:提交crbug。
    13. 将顺序焦点导航起点移至target。

接下来两个猴补丁确保用户代理在片段查找完成时清理待处理文本指令。如因解析停止而结束文本指令查找,则再次尝试非文本片段。

尝试滚动到片段定义中:

猴补丁 HTML § 7.4.6.3 滚动到片段

要为文档document尝试滚动到片段,并行执行如下步骤:
  1. 等待一段实现自定义时间(用于优化用户体验和性能)。

  2. 在导航和遍历任务源上为document的相关全局对象排队全局任务,执行如下步骤:

    1. 如document没有解析器,或其解析器已停止解析,或用户代理认为用户不再关注片段滚动,则终止这些步骤。
    2. 如用户代理认为用户不再关注片段滚动,则:
      1. 待处理文本指令设为null。

      2. 终止这些步骤。

    3. 如文档没有解析器或解析器已停止解析,则:

      1. 待处理文本指令非null,则:

        1. 待处理文本指令设为null。

        2. 滚动到片段,参数为document

      2. 终止这些步骤。

    4. 滚动到片段,参数为document。

    5. 如document的指示部分仍为null,则为document再次尝试滚动到片段。否则,将待处理文本指令设为null。

导航到片段定义中:

猴补丁 HTML § 7.4.2.3.3 片段导航

导航到片段,入参navigable navigable,...:
  1. ...

  2. 以navigable的活动文档、historyEntry、true、scriptHistoryIndex和scriptHistoryLength调用“历史步骤应用时更新文档”。
  3. 为navigable的活动文档滚动到片段。

  4. navigable的活动文档的待处理文本指令设为null。
  5. 令traversable为navigable的可遍历navigable。

  6. ...

滚动到指示部分只是“滚动到片段”中的众多操作之一。需重命名该及相关定义:

猴补丁 HTML § 7.4.2.3.3 片段导航

HTML § 7.4.2.3.3 片段导航及相关步骤重命名为“指示片段”,以反映其更广泛的效果。

3.5. 安全与隐私

3.5.1. 动机

本节为非规范性内容

实现文本指令时必须确保无法被用于跨源窃取信息。脚本可以导航到带有文本指令的跨源URL。如果恶意行为者能判断导航导致目标页面上的文本片段被成功找到,则可推断出页面内容。

下述各子节给出的处理模型限制了该功能,缓解了已知的攻击向量。简而言之,文本指令限制如下:

3.5.2. 导航触发滚动

UA可以选择在匹配文本片段后自动滚动到可见区域。这对用户便捷,但也带来一些实现风险。

已知(或潜在未知)的方式可分辨导航触发滚动和用户自然滚动。

目标页iframe内嵌的其他源注册IntersectionObserver,检测前500ms内是否滚动发生。若有滚动,可能反映文本片段被成功命中。
两名用户处于同一网络并可监听彼此流量。恶意用户向受害者发送带文本片段的目标链接,并根据关联唯一域名的资源请求顺序,间接获知文本片段是否命中。
攻击者向受害者发送带有私密token的目标页面链接。通过文本片段让页面加载时将“请勿泄露该token”的警告滚动出可视区。

所有已知情景依赖于目标页的具体情况,因而并不普适。进一步对文本片段的调用条件加以限制,可进一步约束攻击者能力。但不同 UA 可根据自身判断风险是否可接受,确定是否在导航到文本片段时自动滚动。

符合规范的UA可选择导航时不自动滚动。此时可提供“点击滚动”等UI,或完全不滚动。此时 UA 应向用户提示有片段存在于页面下方。

上述例子展示在特殊情况下,攻击者可获取目标页面内容的1比特信息。但应防止可利用此途径持续攻击以获取任意内容。因此,基于用户激活与浏览上下文隔离的约束至关重要,必须严格实现。

浏览上下文隔离可确保其他页面无法通过脚本操作目标文档,降低攻击面。

但这同样确保恶意使用难以隐藏。一个组内唯一的浏览上下文将是顶级浏览上下文(即整个标签页/窗口)。

如UA选择自动滚动,必须确保文档在后台时(如处于非激活tab)不进行滚动,以防止攻击者在后台偷偷进行自动化搜索,确保恶意行为可见。

如UA选择不自动滚动,则必须无条件滚动后备元素id到可视区(如有),不论文本片段是否匹配。否则可用是否滚动该元素来判断文本片段是否成功匹配。

3.5.3. 查找时机

文本搜索算法的天真实现,可能因匹配与否导致运行时长差异,继而泄漏信息。攻击者如可同步导航到文本指令URL,即可通过导航耗时推断文本片段是否存在。

§ 3.5.4 限制文本片段中的限制可避免上述问题,尤其是禁止同文档导航。但多个防御层是必要的。

因此,实现必须确保§ 3.6 导航到文本片段步骤无论匹配与否运行时长无差异

本规范不规定 UA 如何实现。可选做法如:即使已找到匹配,仍继续遍历树(见从文本指令查找范围);也可调度异步任务查找并设定文档的指示部分。

3.5.4. 限制文本片段

本节与 HTML 导航集成,用于限制何时允许指示性文本指令自动滚动。总结如下:

补充请求文档 的定义,加入新的布尔值text directive user activation字段:

猴补丁 [FETCH]

一个请求关联一个布尔值text directive user activation, 初始为 false。

猴补丁 [HTML]

每个文档text directive user activation,为布尔型,初始为 false。

text directive user activation 提供了允许单次激活文本片段所需的用户手势信号。仅在导航因用户激活发生且跨客户端重定向时,于文档加载期间设为 true。

文档text directive user activation未用于激活文本片段,则其用于将新导航请求text directive user activation设为 true。因此,text directive user activation 可跨导航在文档间传递。

文档text directive user activation请求text directive user activation在被使用时总归为 false,因此一次用户激活不能多次激活文本片段。

此机制允许文本片段通过许多主流网站常用的重定向方式激活。这些站点通过 200 状态码响应脚本,设置 window.location 以重定向用户到期望目标。

与真实 HTTP ( status 3xx ) 重定向不同,这些“客户端” 重定向无法传递导航属于用户手势。text directive user activation 机制可穿越此次特定范围内的导航。因此页面可编程导航至文本片段一次,效果等同拥有用户操作。但此后会重置 text fragment user activation,没人为操作则后续文本片段导航不会激活。

下图展示了这一标志如何支持通过客户端重定向服务激活文本片段:

Diagram showing how a text fragment flag is set and used

更详讨论见 redirects.md

补充 create navigation params by fetching 步骤,将 活动文档text directive user activation 赋值给 requesttext directive user activation.

猴补丁 [HTML]

  1. 断言:本步骤在并发执行。

  2. 令 documentResource 为 entry 的 document state 的 resource。

  3. 新建 request,其属性包括:

    url
    entry 的 URL
    ...
    ...
    referrer policy
    entry 的 document state 的 request referrer policy
    text directive user activation
    navigable活动文档text directive user activation
  4. navigable活动文档text directive user activation 设为 false。

  5. 如 documentResource 为 POST 资源,则:

    1. ...

补充 navigation params,增加新字段:

猴补丁 [HTML]

user involvement
一个 用户导航介入 值。

所有创建 navigation params 的地方均初始化 user involvement 值。具体如 create navigation params by fetching 情况下初始化为 true:

猴补丁 [HTML]

要 create navigation params by fetching,参数:session history entry entry,navigable,source snapshot params sourceSnapshotParams,target snapshot params targetSnapshotParams,string cspNavigationType,navigation ID-or-null navigationId,NavigationTimingType navTimingType,user navigation involvement user involvement,步骤如下。它们返回 navigation params、non-fetch scheme navigation params 或 null。
  1. 断言:本步骤并发执行。

  2. ...

  3. 令 resultPolicyContainer 为 determine navigation params policy container 结果,其参数为 response 的 URL、entry 的 document state 的 history policy container、sourceSnapshotParams 的 source policy container、null 和 responsePolicyContainer。
  4. 如 navigable 的 container 为 iframe,且 response 的 timing allow passed 标志置位,则将 container 的 pending resource-timing start time 设为 null。

  5. 返回新 navigation params,属性为:

    id
    navigationId
    ...
    ...
    about base URL
    entry 的 document state 的 about base URL
    user involvement
    user involvement

补充 create and initialize a Document object 步骤,计算并存储 text directive user activation 标志:

猴补丁 [HTML]

  1. 调用 process link headers。

  2. 若满足下列任一条件,则设 documenttext directive user activation 为 true,否则为 false:
  3. 返回 document

一个text directive allowing MIME typeMIME 类型,其本质 为 "text/html" 或 "text/plain"。

注:滚动到片段所述, 片段处理由各 MIME 类型单独定义。因此滚动到片段中滚动文本指令只应应用于 text/html 类型的媒体。然而,实际上浏览器会对 text/plain 等其它类型应用 HTML 片段处理(如为 text/plain 文档加 id 后导航片段能滚动)。因此在 text/plain 文档启用文本指令有用。其余类型显式禁止,防止对敏感应用数据(如 text/css、application/json、application/javascript 等)发起 XS-Search 攻击。

这段内容是否适合写入 HTML 规范?

检查文本指令是否可被滚动;给定文档 documentorigin-或-null initiator origin,和 用户导航介入-或-null user involvement,执行如下步骤:
  1. document待处理文本指令为 null 或空,返回 false。

  2. documenttext directive user activation 为 true,或 user involvement 为 "activation" 或 "browser UI",则 is user involved 为 true,否则为 false。

  3. documenttext directive user activation 设为 false。

  4. 如果documentcontent typetext directive allowing MIME type,返回 false。

  5. user involvement 为 "browser UI",返回 true。

    如导航始于浏览器UI,始终允许(因用户触发且页面/脚本未提供文本片段)。

    本项意在区分由应用/页面控制URL还是由用户完全控制。如前者,则仅当目标加载于独立浏览上下文组,否则禁用文本片段滚动(防止同时控制片段且可观测副作用)。但如右键“新窗口中打开”属于灰色地带。

    相关讨论见 sec-fetch-site[FETCH-METADATA]

  6. is user involved 为 false,返回 false。

  7. documentnode navigable,返回 false。

  8. initiator origin 非 null 且 documentorigininitiator origin同源,返回true。

  9. documentbrowsing contextgroupbrowsing context set长度为1,返回true。

    即仅在跨 origin 元素/脚本导航目标文档于 noopener 上下文(即新顶级浏览上下文组)时允许滚动,确保发起方无法脚本控制目标文档(可多进程)。
  10. 否则返回 false。

补充(已在§ 3.4.1 调用文本指令饱和修改)滚动到片段,新增布尔参数 allow text directive scroll

猴补丁 HTML § 7.4.6.3 滚动到片段

要滚动到给定文档 document 和布尔变量 allow text directive scroll 的片段:
  1. 若 document 的指示部分为 null,则目标元素设为 null。

  2. ...

  3. 否则:

    1. 断言:document 的指示部分是元素或范围

    2. ...

    3. target范围,则:
      1. allow text directive scroll 为 false,返回。

      2. target 设为target第一个共同祖先

      3. ...

补充try to scroll to the fragment,新增布尔标志 allow text directive scroll,替换第2步任务内容:

猴补丁 [HTML]

要为文档 document (及布尔变量 allow text directive scroll尝试滚动到片段,并行执行如下步骤:
  1. 等待一段实现自定义时间。

  2. 在 navigation and traversal task source 上为 document 的关联全局对象排队全局任务,执行如下:

    1. 如 document 无解析器、解析器已停止、或 UA 认为用户已不关心滚动,则中止步骤。

    2. documentallow text directive scroll滚动到片段。

    3. 如 document 的指示部分依然为 null,再次用同参数 try to scroll to the fragment。

补充update document for history step application,新增布尔 allow text directive scroll,滚动时用之:

猴补丁 [HTML]

To update document for history step application given a Document document, a session history entry entry, a boolean doNotReactivate, integers scriptHistoryLength and scriptHistoryIndex, an optional list of session history entries entriesForNavigationAPI, and a boolean allow text directive scroll:
  1. Let documentIsNew be true if document’s latest entry is null; otherwise false.

  2. ...

  3. If documentsEntryChanged is true, then:
    1. Let oldURL be document’s latest entry’s URL.

    2. ...

  4. If documentIsNew is true, then:

    1. Try to scroll to the fragment with document and allow text directive scroll.

补充apply the history step 算法,传递 allow text directive scroll 调用 update document for history step application

猴补丁 [HTML]

apply the history step,参数:step、traversable、checkForCancelation、sourceSnapshotParams、initiatorToCheck、userInvolvementForNavigateEvents,allow text directive scroll(默认 false),步骤如下,返回 "initiator-disallowed"、"canceled-by-beforeunload"、"canceled-by-navigate" 或 "applied"。

  1. While completedChangeJobs does not equal totalChangeJobs:

    1. ...

    2. Queue a global task on navigation and traversal task source,入参:navigable 的 active window,执行如下步骤:
      1. 如 changingNavigableContinuation 的 update-only 为 false:

        1. ...

        2. 为 navigable 激活历史条目 targetEntry

      2. 定义 updateDocument:执 targetEntry 的 document、targetEntry、changingNavigableContinuation 的 update-only、scriptHistoryLength、scriptHistoryIndex、entriesForNavigationAPI、allow text directive scroll

      3. targetEntry 的 document 等于 displayedDocument,执 updateDocument。

  2. Let totalNonchangingJobs be the size of nonchangingNavigablesThatStillNeedUpdates.

补充apply the push/replace history step,传递 allow text directive scrolling 至 apply the history step:

猴补丁 [HTML]

apply the push/replace history step,参数:step、traversable,和布尔 allow text directive scroll(默认false)

返回 apply the history step(stap, traversable, false, null, null, null, allow text directive scroll).

注:allow text directive scroll 在遍历和重载时故意不设置。这样可避免大量传递和发起源、用户参与、历史滚动状态等检查。此时文本指令仍可作为文档指示部分(高亮可恢复)。

补充 finalize a cross-document navigation,增加user involvement参数并计算及传递 allow text directive scrolling

猴补丁 [HTML]

finalize a cross-document navigation,参数:navigable、historyHandling、historyEntry、user navigation involvement user involvement(默认 "none")

  1. 断言:执行于 navigable 的 traversable navigable 的会话历史遍历队列。

  2. ...

  3. allow text directive scroll检查文本指令是否可被滚动的结果,参数为 historyEntry 的document、historyEntry 的document stateinitiator originuser involvement
  4. apply the push/replace history step,参数 targetStep、traversable,allow text directive scroll

补充navigate算法,传递 user involvement 至 finalize a cross-document navigation:

猴补丁 [HTML]

  1. ...

  2. . 并发执行如下步骤:
    1. ...

    2. . 努力用下述参数填充历史条目的文档:navigable、"navigate"、sourceSnapshotParams、targetSnapshotParams、navigationId、navigationParams、cspNavigationType,并在步骤完成后执行:
      1. 为 navigable 的 traversable 附加会话历史遍历步骤,调用 finalize a cross-document navigation,参数:navigable、historyHandling、historyEntry、userInvolvement

补充navigate to a fragment 算法,添加 initiator origin 参数,且滚动时传递 allow text directive scroll

猴补丁 [HTML]

navigate to a fragment,参数:navigable、url、historyHandling、userInvolvement、navigationAPIState、navigationId,origin initiator origin

  1. 令 navigation 为 navigable 的 active window 的 navigation API。

  2. ...

  3. 用 navigable 的 active document、historyEntry、true、scriptHistoryIndex、scriptHistoryLength 调用 update document for history step application。
  4. 用 navigation、historyEntry、historyHandling,更新 navigation API 的 same-document navigation 条目。

  5. allow text directive scroll为调用检查文本指令是否可被滚动,参数为navigable 的活动文档、initiator origin、userInvolvement

  6. 用 navigable 的活动文档,allow text directive scroll,滚动到片段。

补充navigate算法,导航到片段时传递 initiator origin:

猴补丁 [HTML]

  1. 如导航须替换,historyHandling 设为 "replace"。

  2. 如下列全为真:

    • documentResource 为 null;

    • response 为 null;

    • url 等于 navigable 活动会话历史条目(排除 fragment 版本);

    • url 的 fragment 非 null,

    则:

    1. 使用参数 navigable、url、historyHandling、userInvolvement、navigationAPIState、navigationId、initiatorOriginSnapshot调用 navigate to a fragment。

    2. 令 navigation 为 navigable 的活动 window 的 navigation API。

3.5.5. 限制加载时滚动

本节定义如何使用 force-load-at-top 策略来阻止在加载新文档时发生所有类型的滚动,包括但不限于文本指令。

需要决定 force-load-at-top 与 Navigation API 的交互方式。[Issue #WICG/scroll-to-text-fragment#242]

修订 restore persisted state 步骤,增加能抑制滚动恢复的新布尔参数:

猴补丁 [HTML]

要 restore persisted state from a session history entry entry , and boolean suppressScrolling 执行:
  1. 如果 entry 的 scroll restoration mode 为 "auto",suppressScrolling 为 false,且 entry 的 document 的相关全局对象的导航 API 的 suppress normal scroll restoration during ongoing navigation 为 false,则根据 entry 恢复滚动位置信息。

  2. ...

修订 update document for history step application 步骤,检查 force-load-at-top 策略,若被设置则避免在新文档中滚动。

猴补丁 [HTML]

  1. ...

  2. 将 document 的历史对象长度设为 scriptHistoryLength。
  3. scrollingBlockedInNewDocument 为对 document 获取 force-load-at-top 策略值的结果。

  4. 如果 documentsEntryChanged 为 true,则:

    1. 设 oldURL 为 document 的最新 entry 的 URL。

    2. ...

    3. 如果 documentIsNew 为 false,则:
      1. 为同文档导航用 navigation、entry 和 "traverse" 更新导航 API 条目。

      2. 派发名为 popstate 的事件...

      3. 用 entry suppressScrolling 设为 false恢复持久化状态。

      4. 如果 oldURL 的 fragment 不等于...

    4. 否则,

      1. 断言:entriesForNavigationAPI 被传入。

      2. 用 entry scrollingBlockedInNewDocument恢复持久化状态。

      3. 用 navigation、entriesForNavigationAPI 和 entry 初始化新文档的导航 API 条目。

  5. 如果 documentIsNew 为 true,则:

    1. scrollingBlockedInNewDocument 为 false,为 document 尝试滚动到片段。

    2. 此时可为新建文档运行脚本。

  6. 否则,如果 documentsEntryChanged 为 false 且 doNotReactivate 为 false,则:

    1. ...

文本片段规范建议修订 HTML § 7.4.2.3.3 片段导航。概要:若页面中出现 文本指令 且命中,则文本片段优先作为指示部分而非元素片段。HTML Document 的 指示部分处理模型被修订为返回将滚动到视图的 范围,而不是 元素
寻找 首个公共祖先,输入两个节点 nodeAnodeB,执行:
  1. commonAncestornodeA

  2. commonAncestor 非 null 且不是 包含 shadow 的包容祖先时,将 commonAncestor 设为 commonAncestor包含 shadow 的父节点

  3. 返回 commonAncestor

获取 包含 shadow 的父节点,输入 node,步骤如下:
  1. nodeshadow root,返回 nodehost

  2. 否则,返回 node父节点

3.6.1. 在文档中查找范围

本节概述若干算法与定义,说明如何将片段指令字符串转换为文档中的 Range 列表。

高层来看,输入如:

text=prefix-,foo&unknown&text=bar,baz

我们拆分得到单独每条文本指令:

text=prefix-,foo
text=bar,baz

依次对每条文本指令,在文档内查找首次匹配其所有限制条件的渲染文本。每一次查找互不影响:即无论其他指令数量与匹配结果如何,结果都一致。

若指令在文档中成功匹配某段文本,则返回一个 范围 指示该匹配。invoke text directives 步骤是本节提供的高阶 API。返回由单条指令匹配出的 范围 组成的 列表,顺序与片段指令字符串一致。

如某条指令未命中,则不会在返回列表中加入项。

调用文本指令,输入为 列表 text directives(由 文本指令 组成)和 文档 document,步骤如下:
本算法返回要用于视觉指示的 范围 列表,第一个会自动滚动到视图(若UA支持自动滚动)。
  1. ranges范围的列表,初始为空。

  2. 依次遍历 text directives 中每个 文本指令 directive

    1. 从文本指令查找范围(参数:directivedocument)结果非 null,则将其添加ranges

  3. 返回 ranges

从文本指令查找范围,输入 文本指令 parsedValues文档 document,依次执行:
该算法输入为成功解析的文本指令和被查文档。返回值是文档中首次与搜索文本及上下文匹配的 范围。如无匹配返回null。

end 可为 null。未指定时为“精确”查找,返回的 范围只包含和 start 完全相等的字符串。如 end 指定,则是“范围”查找,返回 范围start 开始,以 end 结束。下文统称以 startend 匹配到的片段为"matching text"。

prefixsuffix 任一可为null。为null的方向无需额外上下文。例如 prefix 为null时,不要求被匹配文本前方内容。

匹配文本及其上下文可跨块,但算法各自单个参数不能跨块。即每个 prefixstartendsuffix 只会匹配单一块里的文本。
:~:text=The quick,lazy dog
在如下内容中无法匹配
<div>The<div> </div>quick brown fox</div>
<div>jumped over the lazy dog</div>

因为 "The quick" 这段并未在一整块内连续出现,中间有块级元素。

但如下例可以匹配:

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>
  1. searchRange 为范围 (document, 0) 到 (document, document长度)

  2. searchRange折叠时:

    1. potentialMatch 为 null。

    2. parsedValuesprefix 非null:

      1. prefixMatchfind a string in range(参数:query = parsedValuesprefixsearchRangewordStartBounded = true、wordEndBounded = false)

      2. prefixMatch 为 null,返回 null。

      3. searchRangestart 指向 prefixMatch边界点(即 prefixMatch 结尾后)。

      4. matchRange 为范围:起点 prefixMatchend,终点 searchRangeend

      5. matchRangestart 向前推进到 下一个非空白位置

      6. matchRange折叠,返回 null。

        如果 prefixMatch 的结尾或其后第一个非空白字符已在文档末尾,会发生此情况。
      7. 断言matchRangestart node 必为 Text 节点。

        matchRange 的起点现在就是 prefix 匹配后第一个非空白文本。
      8. mustEndAtWordBoundary 为 true 当 parsedValuesend 非 null 或 parsedValuessuffix 为 null,否则为 false。

      9. potentialMatchfind a string in range(参数:query = parsedValuesstartsearchRange = matchRangewordStartBounded = false,wordEndBounded = mustEndAtWordBoundary)。

      10. potentialMatch 为 null,返回 null。

      11. potentialMatchstart 不等于 matchRangestart,则 跳过本次 继续查找下一个 prefix 的实例。

        此时虽找到前缀,但其后并非匹配文本,故继续查下次前缀。
    3. 否则:

      1. mustEndAtWordBoundary 为 true 当 parsedValuesend 非 null 或 parsedValuessuffix 为 null,否则为 false。

      2. potentialMatchfind a string in range(参数:query = parsedValuesstartsearchRangewordStartBounded = true,wordEndBounded = mustEndAtWordBoundary)。

      3. potentialMatch 为 null,返回 null。

      4. searchRange 的起点设为 potentialMatchstart 之后的第一个边界点。

    4. rangeEndSearchRange 为范围,从 potentialMatchendsearchRangeend

    5. rangeEndSearchRange折叠时:

      1. parsedValuesend 非 null:

        1. mustEndAtWordBoundary 为 true 当 parsedValuessuffix 为 null,否则为 false。

        2. endMatchfind a string in range(参数:query = parsedValuesendsearchRange = rangeEndSearchRangewordStartBounded = true,wordEndBounded = mustEndAtWordBoundary)。

        3. endMatch 为 null 则返回 null。

        4. potentialMatchend 设为 endMatchend

      2. 断言potentialMatch 非null、且未折叠,且正好包含一段匹配文本。

      3. parsedValuessuffix 为 null,返回 potentialMatch

      4. suffixRange范围:起点等于 potentialMatchend,终点等于 searchRangeend

      5. suffixRange 的起点推进到下一个非空白位置

      6. suffixMatchfind a string in range(参数:query = parsedValuessuffixsearchRange = suffixRangewordStartBounded = false,wordEndBounded = true)。

      7. suffixMatch 为 null,则返回 null。

        如文档剩余文本中无 suffix,则无可能命中。
      8. suffixMatchstart 等于 suffixRangestart,返回 potentialMatch

      9. parsedValuesend 为 null,则 中断

        对精确匹配如 suffix 不符,则跳出循环寻下个 start。对区间匹配则会继续尝试后续 rangeEnd。
      10. rangeEndSearchRange 的起点设为 potentialMatchend

        否则,表明 start 选对但 end 错,继续尝试下次 rangeEnd。
    6. rangeEndSearchRange折叠,则:

      1. 断言parsedValuesend 非 null

      2. 返回 null

        只会在区间匹配中发生。若全 doc 无合法 end+suffix,则无可能命中。
  3. 返回 null

测试
要将range rangestart推进到 下一个非空白位置,按如下步骤:
  1. range 未折叠时:

    1. noderangestart node

    2. offsetrangestart offset

    3. 如果 node不可搜索子树的一部分,或 node 不是可见文本节点,或 offset 等于 node长度,则:

      1. rangestart node 设为下一个节点(按 包含shadow的树顺序)。

      2. rangestart offset 设为 0。

      3. 继续

    4. 如果 nodeoffset 处 6 个字符的子串数据为 "&nbsp;",则:

      1. rangestart offset加 6。

    5. 否则,若 nodeoffset 处 5 个字符的子串数据为 "&nbsp",则:

      1. rangestart offset加 5。

    6. 否则:

      1. cpnode 数据 码点中下标 offset 处的码点。

      2. cp 未设有 White_Space 属性,则返回。

      3. rangestart offset加 1。

在范围内查找字符串,给定 字符串 queryrange searchRange,以及布尔 wordStartBoundedwordEndBounded,步骤如下:
本算法返回表示 query 文本首次出现在 searchRange 内的 range,可选地要求其起止为词边界(详见 § 3.6.2 词边界)。如未找到则返回 null。

此算法基本思路是在块内遍历所有可搜索文本节点,将其收集为列表。将该列表拼为单一字符串,在其中搜索命中,并用节点列表确定与 DOM 的边界点,用于返回 range

一旦遇到块节点即终止收集,例如如下结构:

<div>
  a<em>b</em>c<div>d</div>e
</div>

会先在 "abc" 上查找,再查 "d",再查 "e"。

因此,query 只会命中同一块级容器内,未被其他块元素打断的连续文本。

  1. searchRange折叠时:

    1. curNodesearchRangestart node

    2. curNode不可搜索子树

      1. searchRangestart node 设为下一个不是 包含shadow的后代 的节点(按包含shadow的树顺序)。

      2. searchRangestart offset 设为 0。

      3. 继续

    3. curNode 不是可见文本节点

      1. searchRangestart node 设为下一个不是 doctype 的节点(按包含shadow的树顺序)。

      2. searchRangestart offset 设为 0。

      3. 继续

    4. blockAncestorcurNode最近块级祖先

    5. textNodeListText 节点列表, 初始为空。

    6. curNodeblockAncestor 的包含shadow的后代且 (curNode, 0) 边界点不是 searchRange 的 end 之后时:

      1. curNode 块级显示,则跳出

      2. curNode搜索不可见

        1. curNode 设为下一个不是其包含shadow的后代的节点(按包含shadow的树顺序)。

        2. 继续

      3. curNode可见文本节点则加入 textNodeList

      4. curNode 设为下一个节点(包含shadow树顺序)。

    7. 运行 find a range from a node list 步骤, 入参 query, searchRange, textNodeListwordStartBounded, wordEndBounded。如果结果 range 非 null,则返回。

    8. curNode 为 null,则 跳出

    9. 断言curNode 紧跟在 searchRangestart node之后。

    10. searchRangestart 设为 (curNode, 0)。

  2. 返回 null。

如果一个节点是元素,属于HTML 命名空间,并且满足下列任一条件,则该节点为搜索不可见

  1. 样式计算值display 属性为 none

  2. 该节点序列化后为空元素

  3. 类型为下列之一: HTMLIFrameElementHTMLImageElementHTMLMeterElementHTMLObjectElementHTMLProgressElementHTMLStyleElementHTMLScriptElementHTMLVideoElementHTMLAudioElement

  4. select 元素,且其 multiple 属性不存在。

如果节点本身或其任一包含shadow的祖先搜索不可见,则为 不可搜索子树

如果节点是 Text 节点,其父元素的样式计算值visibility 属性为 visible,且节点正在被渲染,则为 可见文本节点

如果节点是 element,且其 样式计算值display 属性为下述任意值: blocktableflow-rootgridflexlist-item,则有 块级显示

要获取 最近块级祖先,输入 node , 按如下步骤:
  1. curNode = node

  2. curNode 非 null 时

    1. curNodeText 节点,且 curNode 块级显示,则返回 curNode

    2. 否则,curNode 设为其父节点

  3. 返回 node所属文档文档元素

从节点列表查找范围,输入 search string queryStringrange searchRangeText 节点列表 nodes、布尔 wordStartBoundedwordEndBounded,如下:
可选:仅当匹配文本在词边界上起止时返回匹配。 例如:
查询字符串 “range” 总能在 “mountain range” 匹配,但
  1. 如需起始词边界,则不会在 “color orange” 匹配。

  2. 如需结束词边界,则不会在 “forest ranger” 匹配。

§ 3.6.2 词边界 了解详情与更多示例。

  1. searchBuffer拼接 nodes 内各节点的data

    data 不是所需的渲染文本。该算法需要针对渲染文本遍历(再映射回 DOM 范围)。[Issue #WICG/scroll-to-text-fragment#98]

  2. searchStart = 0。

  3. nodes 首项是 searchRangestart node ,则 searchStart 设为 searchRangestart offset

  4. startend 均为边界点,初值为 null。

  5. matchIndex 为 null。

  6. matchIndex 为 null 时,循环:

    1. searchStart 起,用基本字符对比(或 一级,见 [UTS10])在 searchBuffer 首次查找 queryString 的索引。

      即大小写不敏感且忽略重音、变音等的查找。
    2. matchIndex 为 null,返回 null。

    3. endIx = matchIndex + queryString长度

      endIx 即命中最后一字符的下标+1。
    4. 设置 start边界点,由 get boundary point at index(matchIndex, nodes, false)。

    5. 设置 end边界点,由 get boundary point at index(endIx, nodes, true)。

    6. wordStartBounded 为 true 且 matchIndexsearchBuffer词边界(使用 startnode语言);或 wordEndBounded 为 true 且 matchIndex + queryString长度searchBuffer词边界(用 endnode语言):

      1. searchStart = matchIndex + 1。

      2. matchIndex = null。

  7. endInset = 0。

  8. nodes 的最后一项为 searchRangeend node,则 endInset = searchRangeend node长度searchRangeend offset

    endInset 是最后一节点未被纳入范围的长度。
  9. matchIndex + queryString长度 大于 searchBuffer 的长度 − endInset,则返回 null。

    若匹配内容超出范围尾部则返回 null。
  10. 断言startend 均为非 null 且为 searchRange 内有效边界点

  11. 返回范围:range,其 start = startend = end

获取指定下标的边界点,输入整数 indexText 节点列表 nodes 和布尔 isEnd

这是上文用到的小工具,用于推断拼接字符串中的下标属于哪个节点。

isEnd 用于区分起止边界。终止边界指向匹配结果后一字符,如命中刚好在节点边界,则终止偏移也留在本节点末尾而不是下个节点头。

  1. counted = 0。

  2. 依次迭代 nodes

    1. nodeEnd = counted + curNode长度

    2. isEnd 为 true,nodeEnd 加 1。

    3. nodeEnd 大于 index

      1. 返回 边界点curNode, indexcounted)。

    4. countedcurNode长度

  3. 返回 null。

3.6.2. 词边界

将匹配限制在词边界是限制跨源信息泄露的缓解措施之一。
参见 Intl.Segmenter, 一个用于指定 Unicode 分割(包括单词分割)的提案。一旦规范化,本算法可以通过使用 Intl.Segmenter API 来改进词边界匹配。

词边界[UAX29]Unicode 文本分割 § Word_Boundaries 中定义。Unicode 文本分割 § 默认词边界 定义了默认的词边界集合,但如规范所述,应根据区域设置使用更复杂的算法。

基于词典的词界定在没有单词分隔字符的语言区域设置中应特别注意。例如在英语中,单词由空格字符(' ')分隔;然而在日语中没有将单词彼此分隔的字符。在此类情况中,且当字母表字符少于 100 个时,词典中被视为有效的一字词不得超过字母表的 20%。

区域设置 是一个字符串,包含一个有效的 [BCP47] 语言标签,或为空字符串。空字符串表示主要语言未知。

在给定 区域设置 startLocaleendLocale 的情况下,子字符串在 字符串 text 中被称为 词界定,当且仅当其首字符的位置在给定 startLocale处于词边界,并且其末字符之后的位置在给定 endLocale处于词边界

一个数字 position 在给定 区域设置 locale 的情况下,若在 字符串 text 中,按照 locale 的规则,要么在第 position 个代码单元之前立即存在一个 词边界,要么 text 的长度大于 0 且 position 等于 0 或等于 text 的长度,则称该 position 处于词边界

直观上,若子字符串既不在单词中间开始也不在单词中间结束,则称其为词界定

对于有单词分隔符(例如空格)的语言,这(大多)是直接的;尽管还有诸如换行、连字符、引号等细节由上面的技术报告覆盖。

有些语言没有这样的分隔符(尤其是中/日/韩)。此类语言需要词典来确定给定区域设置中什么才是有效单词。

文本片段受限于匹配项在与其相邻的上下文项结合时为词界定。例如,在像 prefix,start,suffix 的精确搜索中,只有当整个结果为词界定时,"prefix+start+suffix" 才会匹配。然而,在类似 prefix,start,end,suffix 的范围搜索中,当且仅当 "prefix+start""end+suffix" 均为词界定时才会找到匹配。

目标是第三方必须已经知道它们正在匹配的完整 token。像 start,end 的范围匹配必须在两术语内部为词界定;否则第三方可以重复使用此方法试图揭示一个 token(例如在页面包含 "Balance: 123,456 $" 的情况下,第三方可以设置 prefix="Balance: "end="$" 并通过改变 start 来逐位猜测数字 token)。

更多细节见 安全评审文档

子字符串 "mountain range" 在字符串 "An impressive mountain range" 中是词界定的,但在 "An impressive mountain ranger" 中则不是。
在日语字符串 "ウィキペディアへようこそ" (欢迎来到维基百科)中,"ようこそ"(欢迎)被视为词界定,但 "ようこ" 则不是。

3.7. 指示文本匹配

用户代理可以选择在 尝试滚动到片段 步骤中或通过其他机制将文本片段滚动到可见区域;但并不要求必须将匹配滚动到视图中。

用户代理应该以某种方式在视觉上指示被匹配的文本,使用户能够注意到该匹配,例如使用高对比度高亮。

用户代理应向用户提供某种取消匹配指示的方法,使得被匹配的文本不再以视觉方式被指示。

具体的指示外观和机制由用户代理定义。然而,用户代理不得使用任何作者脚本可观察的方式来指示匹配,例如使用文档的 selection。这样做可能允许内容外泄的攻击向量。

用户代理不得以任何方式对提供的上下文项进行可视指示。

由于指示器不是文档内容的一部分,用户代理应考虑将其与页面内容在用户感知上区分开来。

用户代理可以在指示器首次出现的前几次提供内置帮助提示,帮助训练用户识别该指示来自链接页面并由用户代理提供。

3.7.1. 用户代理功能中的 URL

用户代理为文档的 URL 提供了若干使用场景(不包括像 window.location 这样的程序化 API)。示例包括显示当前可见文档 URL 的地址栏,或当用户请求为当前页面创建书签时使用的 URL。

为避免用户困惑,用户代理在是否在这些 URL 中包含 片段指令 一事上应保持一致。本节为用户代理如何处理这些情况提供一组默认建议。

我们将这些作为一致行为的基线;然而,由于这些功能不影响跨用户代理互操作性,它们并非严格的一致性要求。

具体行为由实现的用户代理决定,用户代理可能有不同的约束或修改行为的理由。例如,用户代理可以允许用户配置默认值或在 UI 中提供选项,让用户选择是否希望在这些 URL 中包含片段指令。

也有利于允许用户代理试验以提供更好的体验。例如:也许当用户滚动并将文本片段移出视图时,用户代理显示的 URL 可以省略文本片段?

一般原则是:只有当视觉指示可见(即未被取消)时,URL 才应包含 片段指令。如果用户取消了指示,URL 应通过移除 片段指令 来反映该操作。

如果 URL 包含文本片段但在当前页面未找到匹配,用户代理可以选择在暴露的 URL 中省略它。

未在页面中找到的文本片段可以作为信息向用户展示,提示页面自链接创建以来已发生变化。

然而,对于书签来说,它通常并不太有用。

下面提供一些常见示例。

这里将“文本片段”和“片段指令”互换使用,因为假定文本片段是唯一的指令类型。若将来添加其它指令类型,则这些场景下的用户体验需针对新指令类型单独评估。
3.7.1.1. 地址栏

地址栏的 URL 在被视觉指示时应包含文本片段。当用户取消该指示时,片段指令应从地址栏 URL 中剥离。

建议即便在文档中未找到匹配,也在地址栏的 URL 中显示文本片段。

3.7.1.2. 书签

许多用户代理提供“书签”功能,允许用户在用户代理界面中存储指向当前页面的便捷链接。

新创建的书签默认应当在 URL 中包含 片段指令 当且仅当 已找到匹配并且视觉指示尚未取消。

从书签导航到 URL 应按典型导航的方式处理 片段指令

3.7.1.3. 分享

一些用户代理提供将当前页面共享给他人的方法,通常是将 URL 提供给另一个应用或消息服务。

在这些情况下提供 URL 时,只有当已找到匹配并且视觉指示尚未取消时,才应包含 片段指令

3.8. 文档策略集成

本规范在 Document Policy 中定义了名为 "force-load-at-top" 的配置点。其类型为 boolean默认值false

当启用时,此策略将禁用所有自动加载时滚动功能:文本片段、元素片段、历史滚动恢复。
假设用户导航到 https://example.com#:~:text=foo。example.com 服务器响应包含头:
Document-Policy: force-load-at-top

当页面加载时,包含 "foo" 的元素将被标记为指示部分并设为文档的目标元素。但是,"foo" 不会被滚动到可见区域。

该策略对基于片段的滚动阻止在对 滚动到片段 算法的修订中进行了规定,见本文件的 § 3.6 导航到文本片段 部分。

历史滚动恢复通过在 恢复持久化状态 步骤中插入新步骤(在步骤 2 之后)来被阻止:

  1. 获取文档策略值 中 "force-load-at-top" 特性的值,针对该 Document。如果结果为 true,则用户代理不应为该 Document 或其任何可滚动区域恢复滚动位置。

3.9. 特性可检测性

为特性可检测性,我们建议添加一个新的 FragmentDirective 接口,当用户代理支持该特性时,通过 document.fragmentDirective 暴露。

[Exposed=Window]
interface FragmentDirective {
};

我们将 Document 接口补充为包含一个 fragmentDirective 属性:

partial interface Document {
    [SameObject] readonly attribute FragmentDirective fragmentDirective;
};

该对象未来可用于暴露有关文本片段或其他片段指令的更多信息。

4. 生成文本片段指令

本节为非规范性内容。

本节包含建议,供用户代理自动生成带有 文本指令 的 URL 时参考。这些建议并非规范性,但用于确保生成的 URL 在稳定性和可用性上尽可能良好。

4.1. 优先精确匹配而非基于范围的匹配

匹配文本可以作为精确字符串 "text=foo%20bar%20baz" 提供,或作为范围 "text=foo,bar" 提供。

在可行情况下,优先指定整个字符串。这可以确保即使目标页面被删除或更改,预期的目标仍能从 URL 本身推断出来。

假设我们希望为 https://en.wikipedia.org/wiki/History_of_computing 构造引用句子:
The first recorded idea of using digital electronics for computing was the
1931 paper "The Use of Thyratrons for High Speed Automatic Counting of
Physical Phenomena" by C. E. Wynn-Williams.

我们可以创建一个基于范围的匹配,如下:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=The%20first%20recorded,Williams

或者我们可以使用精确匹配对整句进行编码:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=The%20first%20recorded%20idea%20of%20using%20digital%20electronics%20for%20computing%20was%20the%201931%20paper%20%22The%20Use%20of%20Thyratrons%20for%20High%20Speed%20Automatic%20Counting%20of%20Physical%20Phenomena%22%20by%20C.%20E.%20Wynn-Williams

基于范围的匹配稳定性较差,意味着如果页面在更早处加入另一个 "The first recorded" 的实例,链接将定位到非预期的文本片段。

基于范围的匹配在语义上也不够有用。如果页面被更改以删除该句子,用户将不知道原本想指向什么。在精确匹配情况下,用户可以读取或用户代理可以显示所搜索但未找到的文本。

当引用文本过长且对整个字符串编码会产生难以处理的 URL 时,基于范围的匹配可能有帮助。

建议将短于 300 字符的文本片段使用精确匹配编码。超过该限值时,用户代理可以将字符串编码为基于范围的匹配。

TODO: 是否能以不那么任意的方式确定上述限值?

4.2. 仅在必要时使用上下文

上下文项允许 文本指令 对页面上的文本片段进行消歧。然而,它们的使用在某些情况下会使 URL 更脆弱。通常,所需的字符串会在元素边界开始或结束。上下文因此可能存在于相邻元素中。页面结构的变化可能会使上下文与匹配文本不再相邻,从而使 文本指令 失效。

假设我们希望为以下文本构造 URL:
<div class="section">HEADER</div>
<div class="content">Text to quote</div>

我们可以这样构造 文本指令

text=HEADER-,Text%20to%20quote

但是,假设页面更改,在所有章节标题旁增加了一个 "[edit]" 链接。此时该 URL 将被破坏。

当文本片段足够长且唯一时,鼓励用户代理避免添加多余的上下文项。

仅在下列任一情况为真时使用上下文:

TODO: 以不那么任意的方式确定上述数字限制。

4.3. 确定是否需要片段 id

当用户代理导航到包含 文本指令 的 URL 时,如果存在基于元素 id 的常规片段且未找到文本片段,将回退为滚动该元素 id 到视图中。

在文档中更改文本使 文本指令 失效的情况下,这可以作为一种有用的后备。

假设我们希望为 https://en.wikipedia.org/wiki/History_of_computing 构造引用句子:
The earliest known tool for use in computation is the Sumerian abacus

通过指定文本出现的章节,我们确保如果文本被更改或删除,用户仍会被指向相关章节:

https://en.wikipedia.org/wiki/History_of_computing#Early_computation:~:text=The%20earliest%20known%20tool%20for%20use%20in%20computation%20is%20the%20Sumerian%20abacus

然而,用户代理应当确保回退的元素 id 片段是合适的:

假设用户导航到 https://en.wikipedia.org/wiki/History_of_computing#Early_computation。之后他们滚动到 Symbolic Computations 小节,并在该处选择了一段文本,选择为其创建 URL:
By the late 1960s, computer systems could perform symbolic algebraic
manipulations

尽管页面当前的 URL 为: https://en.wikipedia.org/wiki/History_of_computing#Early_computation,但使用 #Early_computation 作为回退并不合适。如果上面那句话被更改或删除,页面将加载到 #Early_computation 小节,可能会令用户感到困惑。

如果用户代理无法可靠地确定适当的回退片段,应该从 URL 中移除片段 id:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=By%20the%20late%201960s,%20computer%20systems%20could%20perform%20symbolic%20algebraic%20manipulations

一致性

文档惯例

一致性要求以描述性断言和 RFC 2119 术语的组合来表达。规范性的部分中关键词 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, 和 “OPTIONAL” 按照 RFC 2119 中的描述来解释。然而为提高可读性,这些词在本规范中并不全部以大写字母形式出现。

本规范的全部文本均为规范性,除非明确标注为非规范性的部分、示例和注释。[RFC2119]

本规范中的示例以“例如”引入或用 class="example" 与规范性文本区分开来,如下:

这是一个说明性示例。

说明性注释以 “Note” 开头,并用 class="note" 与规范性文本区分,如下:

注:这是一个说明性注释。

Tests

与本规范内容相关的测试可能记录在类似的“Tests”块中。任何此类块均为非规范性。


索引

本规范定义的术语

引用中定义的术语

参考文献

规范性引用

[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS 层叠与继承第5级. 链接: https://drafts.csswg.org/css-cascade-5/
[CSS-DISPLAY-3]
Elika Etemad; Tab Atkins Jr.. CSS Display 模块第3级. 链接: https://drafts.csswg.org/css-display/
[CSS-DISPLAY-4]
CSS Display 模块第4级. 编辑草案. 链接: https://drafts.csswg.org/css-display-4/
[CSSOM-VIEW-1]
Simon Pieters. CSSOM 视图模块. 链接: https://drafts.csswg.org/cssom-view/
[DOCUMENT-POLICY]
Ian Clelland. 文档策略. 编辑草案. 链接: https://wicg.github.io/document-policy
[DOM]
Anne van Kesteren. DOM 标准. 实时标准. 链接: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. 编码标准. 实时标准. 链接: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch 标准. 实时标准. 链接: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; 等. HTML 标准. 实时标准. 链接: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. 实时标准. 链接: https://infra.spec.whatwg.org/
[MIMESNIFF]
Gordon P. Hemsley. MIME 嗅探标准. 实时标准. 链接: https://mimesniff.spec.whatwg.org/
[RFC2119]
S. Bradner. RFC中用于表示需求等级的关键词. 1997年3月. 最佳当前实践. 链接: https://datatracker.ietf.org/doc/html/rfc2119
[UAX29]
Josh Hadley. Unicode 文本分割. 2023年8月16日. Unicode 标准附件#29. 链接: https://www.unicode.org/reports/tr29/tr29-43.html
[URL]
Anne van Kesteren. URL 标准. 实时标准. 链接: https://url.spec.whatwg.org/
[UTS10]
Ken Whistler; Markus Scherer. Unicode 排序算法. 2023年9月5日. Unicode 技术标准#10. 链接: https://www.unicode.org/reports/tr10/tr10-49.html
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 标准. 实时 标准. 链接: https://webidl.spec.whatwg.org/

参考性引用

[BCP47]
A. Phillips, Ed.; M. Davis, Ed.. 用于语言标识的标签. 2009年9月. 最佳当前实践. 链接: https://www.rfc-editor.org/rfc/rfc5646
[FETCH-METADATA]
Mike West. Fetch Metadata 请求头. WD. 链接: https://w3c.github.io/webappsec-fetch-metadata/

IDL 索引

[Exposed=Window]
interface FragmentDirective {
};

partial interface Document {
    [SameObject] readonly attribute FragmentDirective fragmentDirective;
};

问题索引

TODO: 如果 URL 的片段以':~:'结尾(即空指令),则会返回 null,视为该 URL 未显式指定指令(避免覆盖)。但或许此时应返回空字符串?这样页面可通过导航/pushState 到 '#:~:' 显式清除指令/高亮。
若在 shadow tree 中,应设置哪个为 target? #190
目前的揭示算法对于 target 是祖先或根节点等情形处理不好。#89 建议限制仅匹配 contain:style layout 区块可解决。
实现注:Blink 当前不会为文本片段设置焦点,或许应该?TODO: 提 crbug。
这并非绝对,Chrome 对同源发起者允许此操作。规范需更新。[Issue #WICG/scroll-to-text-fragment#240]
这一条能否写在 HTML 规范里?
需要决定 force-load-at-top 与 Navigation API 的交互方式。[Issue #WICG/scroll-to-text-fragment#242]
data 此处不正确,因为那是 DOM 里的文本数据。本算法意在用于渲染后的文本(再还原回 DOM 范围)。[Issue #WICG/scroll-to-text-fragment#98]