URLフラグメントテキストディレクティブ

ドラフトコミュニティグループレポート,

このバージョン:
https://wicg.github.io/scroll-to-text-fragment/
課題の追跡:
GitHub
仕様内インライン
編集者:
(Google)
(Google)
テストスイート:
https://wpt.fyi/results/scroll-to-text-fragment/

概要

テキストディレクティブはURLのフラグメントでテキストスニペットを指定する機能を追加します。 このようなフラグメントを含むURLに移動すると、ユーザーエージェントはその部分を強調表示したり、ユーザーの注意を引いたりすることができます。

この文書のステータス

この仕様はWeb Platform Incubator Community Groupによって公開されました。 これはW3C標準ではなく、W3C標準トラックにもありません。 W3Cコミュニティ貢献者ライセンス契約(CLA)の下で、制限付きのオプトアウトおよびその他の条件が適用されることにご留意ください。 W3Cコミュニティおよびビジネスグループの詳細をご覧ください。

1. インフラストラクチャ

この仕様はInfra Standardに依存しています。[INFRA]

2. はじめに

このセクションは規範的ではありません

2.1. ユースケース

2.1.1. Webテキスト参照

テキストフラグメントの主なユースケースは、URLがWeb全体で正確なテキスト参照として機能することです。例えば、Wikipediaの参照はページから引用している正確なテキストにリンクできます。同様に、検索エンジンはページの先頭ではなく、ユーザーが探している答えがある場所へ誘導するURLを提供することができます。

2.1.2. ユーザー共有

テキストディレクティブを使うことで、ブラウザはユーザーがテキスト選択部分でコンテキストメニューを開いたときに「ここへのURLをコピー」というオプションを実装できるかもしれません。ブラウザは選択したテキストを適切に指定したURLを生成し、そのURLの受信者に指定されたテキストが分かりやすく示されます。テキストフラグメントがなければ、ユーザーがページ内のテキストを共有したい時は、通常そのテキストをコピー&ペーストすることになりますが、その場合、受信者はページのコンテキストを失ってしまいます。

この仕様は、例えば実際のテキスト内容をURLのペイロードとして使用したり、フォールバックとして要素IDのフラグメントを許可することにより、テキストディレクティブリンクの有用な有効期間を最大化しようとしています。しかし、Web上のページはしばしば更新され、内容が変化します。そのため、このようなリンクは参照先のテキスト内容が目的のページから無くなってしまい「リンク切れ」を起こす場合があります。

この問題があってもテキストディレクティブリンクは有用です。ユーザー共有のユースケースでは、リンクは送信してから短期間のみ利用されることが多く、一時的なものです。参照やWebページのリンクのような長期間にわたって利用される場合でも、テキストディレクティブは通常のリンクに緩やかに劣化するので価値があります。また、古くなったテキストディレクティブがあることで、ユーザーがリンク作成者の意図やその後ページ内容が変わったことを理解する助けになります。

強固なテキストディレクティブリンクの作成方法については§ 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. コンテキスト用語

このセクションは規範的ではありません

他の2つのオプションパラメータはコンテキスト用語です。プレフィックスの後やサフィックスの前にダッシュ(-)を置くことで指定し、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. 双方向(BiDi)の考慮事項

このセクションは規範的ではありません
Unicode 双方向アルゴリズムの基本を参照すると、双方向テキストの仕組みがよくわかります。

URL文字列はASCIIエンコードなので、双方向テキストを組み込むサポートはありません。ただし、対象とするページ内容はLTR(左から右)、RTL(右から左)、あるいは両方(双方向/BiDi)となり得ます。このセクションは、仕様の規範的セクションで暗黙的に記述された挙動の直感的な説明を提供します。

テキストフラグメント内の各項目の文字は論理順序です。つまり、母語話者が読む順番(かつメモリ上の文字の格納順)です。

同様に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になり得ます。

フラグメントディレクティブ区切り子は":~:"という文字列、すなわち3連続するU+003A(:)、U+007E(~)、U+003A(:)のコードポイントです。

フラグメントディレクティブはURLフラグメントの一部です。そのため、常にURLにU+0023(#)コードポイントの後ろに現れます。
https://example.com のようなURLにフラグメントディレクティブを追加するには、まずフラグメントを付与します: 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から見えなくなり、ページ動作に影響を与えずにディレクティブを指定できます。

ディレクティブは生の文字列ではなくディレクティブ状態オブジェクト内に格納されます。これは、同一のディレクティブ状態が複数の連続した履歴エントリで共有できるようにするためです。トラバース時、2つのエントリ間でディレクティブ状態が変化した場合のみ(=新規ハイライト等が必要な時)ディレクティブが処理されます。

セッション履歴エントリの定義に、次を追加します:

HTML § 7.4.1.1 セッション履歴エントリへのモンキーパッチ:

セッション履歴エントリは以下の項目を持つ構造体です:

URLからフラグメントディレクティブ文字列を削除・返却するヘルパーアルゴリズムを追加します:

[HTML]へのモンキーパッチ:

このアルゴリズムはURLのフラグメントをフラグメントディレクティブ区切り子手前までにします。戻り値のフラグメントディレクティブは区切り子以降すべての文字を含み、区切り子自体は含みません。
TODO: フラグメントが':~:'で終わる(空ディレクティブ)の場合、nullを返します。これは明示的ディレクティブ未指定扱い(既存を上書きしない)ですが、空文字列を返すべき? '#:~:'へのnavigate/pushStateで明示的にディレクティブやハイライトを解除できるよう配慮する案。

フラグメントディレクティブを除去するには、URL url から、次の手順を実行する:

  1. raw fragmenturlフラグメントを代入

  2. fragment directive をnullにする

  3. raw fragmentが非nullかつフラグメントディレクティブ区切り子を含む場合:

    1. position に、raw fragment内で区切り子が最初に現れるコードポイントの位置変数を代入。なければ末尾+1。

    2. new fragment に、raw fragmentの先頭からposition直前までの部分文字列を代入。

    3. position区切り子のコードポイント長だけ進める。

    4. positionraw fragment末尾を超えていなければ:

      1. fragment directiveに、raw fragmentposition以降の部分文字列をセット

    5. urlフラグメントnew fragmentにする

  4. fragment directive を返す

https://example.org/#test:~:text=foo はフラグメントが"test"、フラグメントディレクティブが"text=foo"になるようにパースされます。

以降の4つのモンキーパッチは、フラグメントディレクティブが含まれる可能性があるURLでセッション履歴エントリ作成時に、フラグメントディレクティブを除去してディレクティブ状態へ保存する点を修正します。

navigateの定義では:

HTML § 7.4.2.2 ナビゲーション開始へのモンキーパッチ:

ナビゲーブルなnavigableをURLurlにナビゲートするには...:
  1. ...

  2. navigableのongoing navigationをnavigationIdに設定
  3. urlのスキームが"javascript"なら...

  4. 並列で次の手順を実行:

    1. ...

    2. urlがabout:blankならdocumentStateのoriginにinitiator originを設定
    3. そうでなくabout:srcdocならdocumentStateのoriginにnavigableの親のアクティブドキュメントoriginを設定

    4. historyEntryを新規セッション履歴エントリにし、そのURLをurl、document stateをdocumentStateに設定。
    5. fragment directiveフラグメントディレクティブを削除urlで実行した結果を代入。
    6. directive stateを新しいディレクティブ状態valuefragment directiveセット)として作成

    7. historyEntryを新規セッション履歴エントリ(URLはurl、document stateはdocumentState、ディレクティブ状態directive state)に設定

    8. navigationParamsをnullに設定

    9. ...

navigate to a fragmentの定義では:

HTML § 7.4.2.3.3 フラグメントナビゲーションへのモンキーパッチ:

ナビゲーブルnavigableでフラグメントにナビゲートするには...:
  1. directive stateにnavigableのアクティブセッション履歴エントリのディレクティブ状態を代入

  2. fragment directiveフラグメントディレクティブを削除urlで実行した結果を代入

  3. fragment directiveがnullでなければ:

    フラグメントのみ変更かつディレクティブ未指定なら、アクティブエントリのディレクティブ状態を再利用(=ハイライト維持)。
    1. directive stateに新規ディレクティブ状態valuefragment directive)を設定

  4. historyEntryを新規セッション履歴エントリとして:

    • URL: url

    • document state: navigableのアクティブセッション履歴エントリのdocument state

    • scroll restoration mode: navigableのアクティブセッション履歴エントリのscroll restoration mode

    • ディレクティブ状態: directive state

  5. historyHandlingが"replace"ならentryToReplaceをnavigableのアクティブセッション履歴エントリに、そうでなければnullに設定

  6. ...

URLと履歴更新手順の定義では:

HTML § 7.4.4 非フラグメント同期ナビゲーションへのモンキーパッチ:

Document documentでのURLと履歴更新手順は...:
  1. navigabledocumentのノードナビゲーブルを代入

  2. activeEntrynavigableのアクティブセッション履歴エントリを代入

  3. fragment directiveフラグメントディレクティブを削除newUrlで実行した結果を代入

  4. historyEntryを新規セッション履歴エントリ(

  5. documentのis initial about:blankがtrueならhistoryHandlingを"replace"にセット

  6. historyHandlingが"push"なら:

    1. documentのhistory objectのindexをインクリメント

    2. history objectのlengthをindex+1に

    3. newUrlactiveEntryのURL(fragment除去)またはfragment directiveがnullでなければ:

      フラグメントのみ変更&ディレクティブ未指定ならアクティブエントリのディレクティブ状態再利用、ハイライト維持
      1. historyEntryディレクティブ状態を新規ディレクティブ状態valuefragment directive)に設定

  7. そうでなく、fragment directiveがnullでなければ、historyEntryディレクティブ状態valuefragment directiveをセット

  8. serializedDataがnullでなければ、history object stateをdocumentとnewEntryで復元

create navigation params by fetchingの定義では:

HTML § 7.4.5 セッション履歴エントリの充填へのモンキーパッチ:

session history entry entryでcreate navigation params by fetchingを行うには...:
  1. これは並列実行であるassert

  2. ...

  3. currentURLにrequestのcurrent URLをセット
  4. commitEarlyHintsをnullに

  5. while (true):

    1. requestのreserved clientがnullでなく、currentURLのorigin≠reserved clientの生成URLのoriginなら:

    2. ...

    3. currentURL = locationURL
    4. fragment directiveフラグメントディレクティブを削除locationURLで実行した結果を代入

    5. entryのURLをcurrentURLに
    6. entryのURLをlocationURL

    7. entryディレクティブ状態valuefragment directiveをセット

    8. locationURLのスキームがfetch schemeでなければ、initiator originをrequestのcurrent URLのoriginとした新規non-fetch scheme navigation paramsを返す

    9. ...

Documentは履歴エントリから生成されるため、そのURL にはフラグメントディレクティブが含まれません。またwindowのLocation オブジェクトはアクティブドキュメントのURL表現であり、全getterでディレクティブを除去したバージョンが返ります。

さらに、HashChangeEventフラグメントの変更に応じて セッション履歴エントリ間のURLの変化で発火しますが、ナビゲーションやトラバースでフラグメントディレクティブのみ変化した場合は 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"の)ディレクティブ状態を持ちます。

この場合ページに見えるフラグメントは変わっていないためhashchangeは発火しません。hashchange判定は履歴エントリ間のディレクティブ除去済URLで行われるためです。

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

pushStateによる同一ドキュメントの新規履歴エントリ作成。非フラグメントURLが変化しているためこのエントリのディレクティブ状態は独立し、値は現時点でnull。

セッション履歴エントリにURLをセットしないその他ケース(例: 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から分離され、セッション履歴エントリに保存される仕組みについて説明しました。

このセクションでは、ナビゲーションおよびトラバーサル時に履歴エントリのディレクティブ状態を利用し、セッション履歴エントリに関連付けられたディレクティブをDocumentへ適用するタイミングと方法を定義します。

DOM § 4.5 Interface Documentへのモンキーパッチ:

各ドキュメントは関連付けられた保留中テキストディレクティブを持ちます。これはnullまたはリストテキストディレクティブのリスト)で、初期値はnullです。

履歴ステップ適用のためのドキュメント更新の定義にて:

HTML § 7.4.6.2 ドキュメントの更新へのモンキーパッチ:

Document documentとセッション履歴エントリ entry ... を与えて履歴ステップ適用のためのドキュメントを更新する:
  1. ...

  2. document の history object の length を scriptHistoryLength に設定
  3. documentsEntryChanged が true の場合:

    1. oldURLdocument の latest entry の URL を代入

    2. document の latest entry のディレクティブ状態entryディレクティブ状態と異なる場合:
      1. fragment directiveentryディレクティブ状態valueとする。

      2. document保留中テキストディレクティブに、フラグメントディレクティブのパースの結果をセット。

  4. document の latest entry を 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 code pointです。

TextDirectiveは次の生成規則を満たすとき有効と見なします:

ValidTextDirective ::=
"text=" TextDirectiveParameters
TextDirectiveParameters ::=
(TextDirectivePrefix ",")? TextDirectiveString ("," TextDirectiveString)? ("," TextDirectiveSuffix)?
TextDirectivePrefix ::=
TextDirectiveString"-"
TextDirectiveSuffix ::=
"-"TextDirectiveString
TextDirectiveString ::=
(TextDirectiveExplicitChar | PercentEncodedByte)+
TextDirectiveExplicitChar ::=
[a-zA-Z0-9] | "!" | "$" | "'" | "(" | ")" | "*" | "+" | "." | "/" | ":" | ";" | "=" | "?" | "@" | "_" | "~"
TextDirectiveExplicitCharは"&""-""、","で使われている以外の全てのURL code point。テキストフラグメントが"&"や"-"や","を参照する場合はパーセントエンコードする必要があります。
PercentEncodedByte ::=
"%" [a-zA-Z0-9][a-zA-Z0-9]

3.4. テキストディレクティブ

テキストディレクティブは、ユーザーに指示するためのテキスト範囲を示すディレクティブの一種です。構造体で、四つの文字列: start, end, prefix, suffix を持ちます。startはnull不可。他3つはnullでもよく、未指定時を示します。空文字列はどれにも許可されません。

各構成要素の意味や使い方は§ 3.2 構文を参照してください。

string term を引数に、「テキストディレクティブ項目のパーセントデコード」は次の手順
  1. termがnullならnullを返す。

  2. Assert: termASCII文字列である。

  3. decoded bytespercent-decoding term の結果とする。

  4. decoded bytesBOMなしのUTF-8デコードを実施して返す。

string text directive value を引数に「テキストディレクティブのパース」は次の手順:

このアルゴリズムは単一のテキストディレクティブ値(例:"prefix-,foo,bar")を入力とし、その内容をディレクティブの構成要素(例:("prefix", "foo", "bar", null))に分割しようとします。各構成要素の意味や使い方は§ 3.2 構文を参照。

入力が不正ならnullを返します。正しければテキストディレクティブを返します。

  1. prefix, suffix, start, end をすべてnullで初期化。

  2. Assert: text directive valueASCII文字列で、フラグメントパーセントエンコードセットのコードポイントやU+0026 (&) を含まない。

  3. tokens厳密分割text directive valueをU+002C(,)区切りで分割したリストを代入。

  4. tokensサイズが1未満または4より大ならnullを返す。

  5. tokensの最初の要素がU+002D(-)で終わる場合:

    1. prefixtokens[0]の0番目から長さ-1だけ切り出した部分文字列とする。

    2. tokensの先頭を削除。

    3. prefixが空文字列またはU+002D(-)を含む場合はnullを返す。

    4. tokensならnullを返す。

  6. tokensの最後の要素がU+002D(-)で始まる場合:

    1. suffixを最後の要素の1文字目以降の部分文字列とする。

    2. tokensの末尾を削除。

    3. suffixが空文字列またはU+002D(-)を含む場合はnullを返す。

    4. tokensならnullを返す。

  7. tokensサイズが2より大きければnullを返す。

  8. Assert: 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. directives厳密分割fragment directiveをU+0026 (&) 区切りで分割したリストを代入。

  2. outputに空のテキストディレクティブリストを初期化。

  3. string directive について:

    1. directiveが"text="で始まらない場合、スキップ

    2. text directive valuedirectiveの5文字目以降部分文字列を代入

      注: 空文字列もありうる
    3. parsed text directiveテキストディレクティブのパースの結果を代入

    4. parsed text directiveがnullでなければoutputに追加

  4. output を返す。

3.4.1. テキストディレクティブの呼び出し

このセクションでは、ドキュメントの保留中テキストディレクティブ内のテキストディレクティブがどのように処理・呼び出され、該当テキスト部分の指示が行われるかを説明します。

本セクションの変更点まとめ:

indicated partにおいて、フラグメントでrangeを指示できるようにします。以下のように修正:

HTML § 7.4.6.3 フラグメントへのスクロールへのモンキーパッチ:

HTML文書documentについて、その指示部分を決定するための処理モデルは次の通り:
  1. text directivesをdocumentの保留中テキストディレクティブとする。

  2. text directivesがnullでない場合:

    1. rangesリストをテキストディレクティブ呼び出し手順でdocumentとtext directivesから生成。

    2. rangesが空でなければ:

      1. firstRangerangesの最初の要素とする。

      2. ranges内の各range実装依存の方法で視覚的に指示する。指示は著者スクリプトから観測不可でなければならない。§ 3.7 テキスト一致の指示を参照。

        rangesの最初のrangeがスクロールされますが、全て指示は視覚的にユーザーに見せる必要があります。
      3. firstRangedocumentの指示部分としてセットし、return。

  3. fragmentをdocumentのURLフラグメントとする。

  4. fragmentが空文字列なら、特殊値「ドキュメント先頭」を返す。

  5. potentialIndicatedElementをdocumentとfragmentをもとに求める。

  6. ...

フラグメント識別子へのスクロールにおいて、指示部分がrangeの場合や、force-load-at-topポリシー時のスクロール制御を記述。次のように修正:

HTML § 7.4.6.3 フラグメントへのスクロールへのモンキーパッチ:

  1. documentの指示部分がnullなら、documentのtarget要素をnullに。

  2. documentの指示部分が「ドキュメント先頭」なら:

    1. documentのtarget要素をnullに。

    2. documentでドキュメント先頭へスクロール。

    3. return。

  3. その他の場合:

    1. アサーション: documentの指示部分はelementまたはrange

    2. scrollTargetをdocumentの指示部分とする。

    3. targetscrollTargetとする。

    4. targetrangeの場合:

      1. targetをstart node、end nodeの最小共通祖先にセット。

      2. targetがnullでなくelementでない間、targettargetに辿る。

        shadow tree内の場合のtarget設定はどうするか? #190
    5. アサート:targetはelement。

    6. documentのtarget要素にtargetをセット。

    7. ancestor details revealingアルゴリズムをtargetで実行。

    8. ancestor hidden-until-found revealingアルゴリズムをtargetで実行。

      revealingアルゴリズムの現状だとtargetが祖先またはドキュメントルートノードの場合うまくいかない場合がある。 #89 でcontain:style layoutブロックへの制限提案あり。
    9. blockPositionscrollTargetrangeなら"center"、それ以外は"start"。

      テキストディレクティブへのスクロールはブロック方向で中央寄せスクロールとなる。
    10. targetを自動・start・nearestでスクロール。
    11. scroll a target into viewtargetscrollTargetbehaviorは"auto"、blockblockPositioninlineは"nearest"で実行。

      実装はテキストディレクティブから生成されたtargetの場合スクロールしないことも可能。

    12. targetのfocusing stepsを実行(fallbackはDocumentのビューポート)。

      Blinkは現状テキストフラグメントでfocusを設定しない。対応要検討。
    13. 逐次focusナビゲーションの開始点をtargetに移動。

次の2つのモンキーパッチは、ユーザーエージェントがフラグメント検索完了時に保留中テキストディレクティブをクリアすること、およびテキストディレクティブ検索パース終了時は非テキストディレクティブのフラグメントもサーチすることを保証します。

try to scroll to the fragmentの定義にて:

HTML § 7.4.6.3 フラグメントへのスクロールへのモンキーパッチ:

Document documentについて、try to scroll to the fragmentのパラレル手順は以下:
  1. 実装依存の時間だけwait(パフォーマンス配慮のためのUX最適化)。

  2. documentのrelevant global objectでnavigation/traversal task sourceのグローバルタスクとして以下をキュー:

    1. documentにparserが存在しないか、パース完了か、スクロール不要ならabort
    2. ユーザーエージェントがスクロール不要と判断した場合
      1. 保留中テキストディレクティブをnullにセット

      2. abort

    3. documentにparserが存在しないかparserがstopなら:

      1. 保留中テキストディレクティブがnullでなければ:

        1. 保留中テキストディレクティブをnullに

        2. フラグメント識別子へのスクロールをdocumentで実行

      2. abort

    4. documentでフラグメントへスクロールを実行。

    5. ドキュメントの indicated part がまだ null の場合、ドキュメントのフラグメントへスクロールを試みる。それ以外の場合は、保留中のテキストディレクティブ を null に設定する。

navigate to a fragmentの定義では:

HTML § 7.4.2.3.3 フラグメントナビゲーションへのモンキーパッチ:

navigable navigableでフラグメントにナビゲートするには...:
  1. ...

  2. navigableのactive document, historyEntry, true, scriptHistoryIndex, scriptHistoryLengthで履歴ステップ適用のためのドキュメント更新を行う。
  3. navigableのactive documentでフラグメントにスクロールする。

  4. navigableのactive documentの保留中テキストディレクティブをnullに
  5. traversableをnavigableのtraversable navigableとする。

  6. ...

指示部分へのスクロールは「scroll to the fragment」から行われるものの1つにすぎない。名称と関連定義を「indicating a fragment」にリネーム:

HTML § 7.4.2.3.3 フラグメントナビゲーションへのモンキーパッチ:

HTML § 7.4.2.3.3 Fragment navigations およびその関連手順の名称を「indicating a fragment」(フラグメントの指示)にリネーム。

3.5. セキュリティとプライバシー

3.5.1. 動機

このセクションは規範的ではありません

テキストディレクティブを実装する際は、オリジン間で情報漏えいに使われないよう注意が必要です。スクリプトはクロスオリジンのテキストディレクティブ付きURLにナビゲートできます。悪意のある者が、被害者ページでテキストフラグメントが発見されたかを検出できれば、ページ内テキストの有無を推測できてしまいます。

次節以降の処理モデルでは、この攻撃ベクトルを軽減できるように制限を設けています。要点をまとめると、テキストディレクティブは次の条件下でのみ許可されます:

3.5.2. ナビゲーション時のスクロール

UAは一致したテキスト部分を自動スクロール表示できる場合があります。これはユーザーにとって便利ですが、実装UAはリスクに注意が必要です。

ナビゲーション時スクロールが通常のユーザースクロールと区別できる既知・未知の検出方法があります。

対象ページのiframe内にあるオリジンでIntersectionObserverを登録し、ロード500ms以内にスクロール発生を検出する。これにより、ページにテキストフラグメントが見つかったか判別できる。
2人のユーザーが同じネットワークで互いの通信を観測可能な場合、攻撃者がテキストフラグメント付きリンクを送信し、被害者がアクセス、フラグメント付近のリソースがユニークドメインにあり、DNSルックアップ順で一部一致成否を推定できる。
攻撃者が被害者にプライベートトークンを表示するページへのリンクを送り、テキストフラグメントで警告文をスクロールアウトし、トークンのみ読ませようとする。

このような漏洩方法は、ターゲットページ特有の状況に依存し一般化は難しいです。テキストフラグメントの発動に更なる制限をつけることで、攻撃者の行動範囲はさらに狭くなります。ただし、UAごとにリスク受容性判断が異なる可能性があります。UAはこうしたリスクを考慮し、テキストフラグメントでの自動スクロール実施可否を判断して下さい。

準拠UAはナビゲーション時の自動スクロールをしない選択もできます。その場合はUIでユーザーがスクロール開始できる仕組み(「クリックしてスクロール」など)や、指示部分が下部ページに存在する旨の通知をすることも可能です。

上記例は状況次第でページ内容1ビット漏洩可能となることを示していますが、攻撃を繰り返して任意のページ内容抽出が行えないよう、ユーザーアクティベーション制限やブラウジングコンテキスト分離などが重要かつ必須です。

ブラウジングコンテキスト分離は、他ドキュメントからのスクリプトによる攻撃面を減らします。

また、悪意ある利用も隠しにくくなります。グループ内唯一ならトップレベルコンテキスト(タブやウィンドウ)となるため。

UAが自動スクロール実施を選ぶ場合、ドキュメントがバックグラウンド(非アクティブタブ等)では一切スクロールしない必要があります。これにより、不正利用がユーザー可視となり、バックグラウンド自動化攻撃を防止します。

UAが自動スクロールを実施しない場合、テキストフラグメント有無に関わらず要素ID指定部分は必ずスクロールしてください。そうしないと要素IDがスクロールされたか否かで一致判別ができてしまいます。

3.5.3. 検索タイミング

ナイーブなテキスト検索アルゴリズムでは、一致/不一致時の実行時間差で情報漏えいの可能性があります。攻撃者がテキストディレクティブ呼び出しURLへ同期ナビゲートできれば、その実行時間からテキスト片の存在を推測可能です。

§ 3.5.4 テキストフラグメントの制限により、このケース(特に同一ドキュメント遷移抑止)は防がれますが、防御の多層化の一環です。

そのため、§ 3.6 テキストフラグメントへのナビゲーション手順の実行時間が一致の有無で変化しないように実装しなければなりません。

この仕様では、UAが達成する手法は問わず、多様な解法とトレードオフが存在します。例:UAはテキストディレクティブから範囲検索で一致が見つかってもツリー走査を継続したり、非同期タスクでドキュメント指示部分を設定するなど。

3.5.4. テキストフラグメントの制限

このセクションでは、HTMLナビゲーションとの統合により、指示されたテキストディレクティブがスクロールを許可されるタイミングを制限します。まとめ:

requestおよびDocumentの定義を修正し、新たなboolean型text directive user activationフィールドを含めます:

Monkeypatching [FETCH]:

requestには関連付けられたboolean型text directive user activation があり、初期値はfalse。

Monkeypatching [HTML]:

Documenttext directive user activation(boolean型、初期値はfalse)を持つ。

text directive user activationは、テキストフラグメントを一度だけアクティベート可能にするためのユーザー操作シグナルです。ドキュメント読み込み中、ユーザーアクティベーションによるナビゲート時のみtrueになり、クライアントサイドリダイレクトでも伝搬します。

Documenttext directive user activationがテキストフラグメントのアクティベートに使われなかった場合は、新しいナビゲーションrequesttext directive user activationにもtrueがセットされます。これにより、一つのtext directive user activationがナビゲーションをまたいで引き継がれます。

どちらのDocumenttext directive user activationも、requesttext directive user activationも、使用された時点で常にfalseになる。これにより、一度のユーザーアクティベーションで複数のテキストフラグメントをアクティベートできなくなる。

この仕組みにより、多くのWebサイトで用いられる一般的なリダイレクト経由でもテキストフラグメントをアクティベートできます。これらのサイトはスクリプトで window.location をセットする200レスポンスで目的地にリダイレクトします。

本当のHTTP( status 3xx) リダイレクトとは異なり、そのような「クライアントサイド」リダイレクトではNaavがユーザー操作の結果かを伝搬できません。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手順では、active documenttext directive user activation値をrequesttext directive user activationに伝搬します。

Monkeypatching [HTML]:

  1. これは並列実行である

  2. documentResourceをentryのdocument stateのresourceとする。

  3. requestを新規requestとして作成:

    url
    entryのURL
    ...
    ...
    referrer policy
    entryのdocument stateのrequest referrer policy
    text directive user activation
    navigableactive documenttext directive user activation
  4. navigableactive documenttext directive user activationをfalseに設定。

  5. documentResourceがPOST resourceであれば:

    1. ...

navigation params定義にも新しいフィールドを含める:

Monkeypatching [HTML]:

user involvement
user navigation involvement値。

user involvement値をナビゲーションparams作成時にセット。特にcreate navigation params by fetchingのケースでは初期値true:

Monkeypatching [HTML]:

To create navigation params by fetching given ... user navigation involvement user involvement, ...
  1. アサート:これは並列で実行されている。

  2. ...

  3. resultPolicyContainer を、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. 次の内容を持つ新しいナビゲーションパラメータを返す:

    id
    navigationId
    ...
    ...
    about base URL
    entry の document state の about base URL
    ユーザー関与
    user involvement

Documentオブジェクトの生成と初期化手順にてtext directive user activationフラグの計算と保存を行う:

Monkeypatching [HTML]:

  1. link headersをdocument, navigationParamsのresponse, "pre-media"で処理。

  2. documenttext directive user activationを、次条件のいずれかでtrue、そうでなければfalseに設定:
  3. documentを返す。

text directive allowing MIME typeは、MIME typeessenceが"text/html"または"text/plain"であるもの。

注: scrolling to a fragmentで述べられているように、フラグメント処理は各MIME Typeごとに定義されています。したがってscroll to the fragmentでのテキストディレクティブスクロールはtext/htmlメディアタイプのみが対象。ただし実際にはブラウザはtext/plainにもHTML方式のフラグメント処理を適用することがあり、そのためtext/plainへのテキストディレクティブ適用許可が有用です。text/css, application/json, application/javascript等は明示的に除外され、XS-Search攻撃等への配慮です。

この記述はHTML仕様として妥当か?

Document documentoriginまたはnull initiator originuser navigation involvementまたはnull user involvementを受け取って テキストディレクティブがスクロール可能か判定手順:
  1. documentの保留中テキストディレクティブがnullまたは空ならfalseを返す。

  2. is user involvedを次のいずれかでtrue、そうでなければfalseとする:documentのtext directive user activationがtrue、またはuser involvementが"activation"または"browser UI"

  3. documentのtext directive user activationをfalseに。

  4. documentのcontent typetext directive allowing MIME typeでなければfalse。

  5. user involvementが"browser UI"ならtrue。

    ナビゲーションがブラウザUIからのものなら常に許可、ユーザー起因でありページ/スクリプトがテキスト片を与えないため。

    この項の意図はアプリ/ページがURL制御可能なケースと完全にユーザー側制御なケースを区別することにある。前者では、宛先が別のブラウジングコンテキストグループ(元ページがテキスト片と副作用を両方制御できない)でない限りテキストフラグメントのスクロールを防ぐ必要がある。UIによる「新しいウィンドウで開く」等、グレーなケースもある。

    sec-fetch-siteも参照。

  6. is user involvedがfalseならfalse。

  7. documentのnode navigableparentがあればfalse。

  8. initiator originがnullでなく、documentのorigininitiator originsame originならtrue。

  9. documentのbrowsing contextgroupbrowsing context setが長さ1ならtrue。

    つまりクロスオリジン要素/スクリプトからのナビゲートは、documentがnoopenerな場合のみ許可。ナビがスクリプトアクセス不可で別プロセスに分離可能な新規トップレベルbrowsing contextであること。
  10. その他の場合false。

§ 3.4.1 テキストディレクティブの呼び出しですでに修正済みのscroll to the fragment手順にboolean型allow text directive scroll引数を追加:

Monkeypatching HTML § 7.4.6.3 フラグメントへのスクロール:

Document document、boolean allow text directive scroll引数で scroll to the fragmentを実行:
  1. ドキュメントの indicated part が null の場合、ドキュメントの target element を null に設定する。

  2. ...

  3. それ以外の場合:

    1. アサート:ドキュメントの indicated part は要素、または range である。

    2. ...

    3. targetrange の場合:
      1. allow text directive scroll が false なら、return。

      2. targettarget最初の共通祖先targetstart nodeend node)にする。

      3. ...

try to scroll to the fragmentを修正、boolean型allow text directive scrollを引数に持ち、手順2のタスク内容を差し替え:

Monkeypatching [HTML]:

Document documentとboolean allow text directive scroll でtry to scroll to the fragmentを並列実行:
  1. 実装依存の時間wait。

  2. documentのrelevant global objectでnavigation/traversal task sourceのグローバルタスクとして以下:

    1. documentにparserがない/停止済/ユーザーがスクロール希望でないならabort。

    2. documentallow text directive scrollでscroll to the fragmentを実行。

    3. documentの指示部分がnullなら再度try to scroll to the fragment(allow text directive scroll)。

update document for history step application手順でもboolean allow text directive scrollを引数に持ち、scroll to a fragment時に利用:

Monkeypatching [HTML]:

Document document, session history entry entry, boolean doNotReactivate, 数値scriptHistoryLengthとIndex、entriesForNavigationAPI(任意)、boolean allow text directive scroll引数でupdate document for history step application手順:
  1. documentIsNewはdocumentのlatest entryがnullならtrue。

  2. ...

  3. documentsEntryChangedがtrueの場合:
    1. oldURL = documentのlatest entryのURL

    2. ...

  4. documentIsNewがtrueなら:

    1. documentとallow text directive scrollでtry to scroll to the fragment実行

apply the history stepアルゴリズムもboolean allow text directive scroll引数を持ち、update document for history step application へ渡す:

Monkeypatching [HTML]:

apply the history step handoff... boolean allow text directive scroll(デフォルトfalse)で呼び出し

  1. completedChangeJobs が totalChangeJobs と等しくない間:

    1. ...

    2. 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. totalNonchangingJobs を nonchangingNavigablesThatStillNeedUpdates のサイズとする。

apply the push/replace history stepでもallow text directive scrollingをapply the history stepへ渡す:

Monkeypatching [HTML]:

apply the push/replace history step handoff... boolean allow text directive scroll(デフォルトfalse)で呼び出し

apply the history step内でfalse, null, null, null, allow text directive scrollを渡し実行。

注: allow text directive scrollはトラバースやリロード時には意図的にセットされません。イニシエーターオリジンやユーザー関与のチェック回避や履歴スクロール状態優先のためです。テキストディレクティブはドキュメントの指示部分として使われるため、ハイライトは復元される。

finalize a cross-document navigationuser involvement引数を渡し、allow text directive scrollingも計算・伝搬:

Monkeypatching [HTML]:

finalize a cross-document navigation handoff...user navigation involvement user involvement、...
  1. ...

  2. allow text directive scrollテキストディレクティブがスクロール可能か判定で計算し、historyEntryのdocument, entryのdocument state, initiator origin, user involvement引数で渡す
  3. apply the push/replace history step targetStep to traversable, with allow text directive scroll

navigateアルゴリズムにてuser involvementをfinalize a cross-document navigationへ伝播:

Monkeypatching [HTML]:

  1. ...

  2. . 並列で次の手順を実行する:
    1. ...

    2. . allowPOST を true、completionSteps を次の手順とし、navigable、"navigate"、sourceSnapshotParams、targetSnapshotParams、navigationId、navigationParams、cspNavigationType を指定して historyEntry のための履歴エントリの document を生成しようとする:
      1. navigable、historyHandling、historyEntry、および userInvolvement を指定して、セッションクロスドキュメントナビゲーションを完了するためのセッション履歴トラバーサル手順を navigable の traversable に追加する。

Navigate to a fragmentアルゴリズムもinitiator originを引数にとり、 allow text directive scrollをフラグメントスクロール時に渡す:

Monkeypatching [HTML]:

navigate to a fragment handoff... navigable, URL, ..., userInvolvement, navigationAPIState, navigationId, origin initiator origin...
  1. ...

  2. navigableのactive document, historyEntry, true, ...などでupdate document for history step application
  3. ...

  4. allow text directive scrollをcheck if a text directive can be scrolledで計算(active document, initiator origin, userInvolvement)

  5. active document, allow text directive scrollでscroll to the fragment呼び出し

navigateアルゴリズムでfragment navigationする際にinitiator originを渡す:

Monkeypatching [HTML]:

  1. navigation must be a replaceの判定(...)

  2. 次4条件がすべてtrueの場合:

    • documentResourceがnull

    • responseがnull

    • urlがnavigableのactive session history entryのURL(fragment除去)と等しい

    • urlのfragmentがnon-null

    なら:

    1. navigate to a fragment(navigable, url, historyHandling, userInvolvement, navigationAPIState, navigationId, initiatorOriginSnapshot)

    2. navigation = navigableのactive windowのnavigation API

3.5.5. ロード時のスクロール制限

このセクションでは、新しいドキュメントの読み込み時に、テキストディレクティブなどを含むすべての種類のスクロールを防ぐために、force-load-at-top ポリシーがどのように使用されるかを定義します。

force-load-at-top がナビゲーションAPIとどのように連携するかを決定する必要があります。[Issue #WICG/scroll-to-text-fragment#242]

保存状態の復元ステップを修正し、スクロールの復元を抑制する新しいブールパラメーターを受け取るようにします:

Monkeypatching [HTML]:

セッション履歴エントリ entry , およびブール値 suppressScrolling から保存状態を復元するには:
  1. entryのスクロール復元モードが "auto"、 suppressScrolling がfalse、かつentryのドキュメントの関連グローバルオブジェクトのナビゲーションAPIの ongoing navigation中の通常のスクロール復元抑制がfalseであれば、entryを指定してスクロール位置データを復元する。

  2. ...

履歴ステップ適用のためのドキュメント更新ステップを修正し、 force-load-at-top ポリシーを確認することで、新しいドキュメントで設定されていればスクロールを回避します。

Monkeypatching [HTML]:

  1. ...

  2. documentの履歴オブジェクトのlengthを scriptHistoryLength に設定する。
  3. scrollingBlockedInNewDocumentポリシー値の取得 を使って force-load-at-topdocument に対して取得した結果とする。

  4. documentsEntryChanged が true の場合、次を行う:

    1. oldURL を document の latest entry の URL とする。

    2. ...

    3. documentIsNew が false の場合:
      1. ナビゲーション、entry、"traverse" を指定して、同一ドキュメントナビゲーション用のナビゲーションAPIエントリを更新する。

      2. popstate という名前のイベントを発火する...

      3. entry および suppressScrolling をfalseにして保存状態を復元する。

      4. oldURLのフラグメントが...でなければ

    4. それ以外の場合、

      1. Assert: entriesForNavigationAPI が与えられている。

      2. entry および scrollingBlockedInNewDocument で保存状態を復元する。

      3. ナビゲーション、entriesForNavigationAPI、entry を指定して新しいドキュメント用のナビゲーションAPIエントリを初期化する。

  5. documentIsNew が true の場合、

    1. scrollingBlockedInNewDocument がfalseであれば、ドキュメントのフラグメントへスクロールを試みる。

    2. この時点でスクリプトが新たに生成された document 用に実行される場合がある。

  6. それ以外で、documentsEntryChanged が false かつ doNotReactivate が false なら:

    1. ...

テキストフラグメント仕様は HTML § 7.4.2.3.3 フラグメントナビゲーション への修正を提案しています。要約すると、テキストディレクティブ が存在し、ページ内で一致が見つかった場合、テキストフラグメントが要素フラグメントよりも優先され表示部分として扱われます。HTMLドキュメントの indicated part 処理モデルを修正し、range を返すようにします(これまでの element ではなく)。このrangeがスクロールインビューされます。
2つのノード nodeAnodeB最初の共通祖先を見つけるには、次の手順を実行します:
  1. commonAncestornodeA にする。

  2. commonAncestor がnullでなく、かつshadow-including inclusive ancestor でない場合、commonAncestorcommonAncestorshadow-including parent にする。

  3. commonAncestor を返す。

nodeshadow-including parent を見つけるには次の手順を実行します:
  1. nodeshadow root の場合、 nodehost を返す。

  2. それ以外の場合、nodeparent を返す。

3.6.1. ドキュメント内の範囲の特定

このセクションでは、フラグメントディレクティブ文字列をドキュメント内のRange のリストに変換する方法を規定する複数のアルゴリズムと定義を示します。

概略では、次のようなフラグメントディレクティブ文字列を受け取ります:

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

これを個々のテキストディレクティブに分けます:

text=prefix-,foo
text=bar,baz

各テキストディレクティブごとに、ディレクティブ内の制限条件に一致するレンダリング済みテキストの最初のインスタンスをドキュメント内で検索します。 検索はそれぞれ独立しており、他にいくつディレクティブがあっても、また一致結果にかかわらず、同じ結果となります。

ディレクティブが文書内テキストと一致した場合、ドキュメント内の一致する部分を示すrangeを返します。テキストディレクティブの実行ステップがこのセクションで提供される上位APIです。 これらは、個々のディレクティブ毎のマッチングステップで一致したrangeリスト を(ディレクティブ文字列に指定された順に)返します。

ディレクティブが一致しなかった場合、その戻り値リストにはアイテムを追加しません。

テキストディレクティブの実行を行うには、入力としてリストテキストディレクティブ text directivesDocument document を受け取り、次のステップを実行します:
このアルゴリズムは表示指示用の rangeリストを返します。 最初のrangeは(UAが自動スクロールする場合)インビューとなります。
  1. rangesrangeリスト(初期値は空)にする。

  2. テキストディレクティブ directive について:

    1. テキストディレクティブから範囲を探すdirectivedocument で実行した結果が null でなければ、それを ranges に追加 する。

  3. ranges を返す。

テキストディレクティブから範囲を探すには、 テキストディレクティブ parsedValuesDocument document を受け取り、次の手順を実行する:
このアルゴリズムはパース済みテキストディレクティブと検索対象ドキュメントを入力とし、 条件に一致し周辺コンテキストも満たす最初のテキスト部分に該当するrangeを返します。 該当部分が存在しなければnullを返します。

end はnull可です。省略した場合「完全一致」検索となり、返される rangestart と完全一致する文字列となります。 end があれば「範囲検索」となり、返される rangestartend で始まります。正規な条件に合致するテキスト部分を、 以下「matching text」と呼びます。

prefixsuffixは両方またはいずれかがnull可で、その場合片側のコンテキストはチェックしません。例:prefixがnullなら前方のテキスト制約なしで一致。

一致するテキストおよびprefix/suffixはブロック境界をまたぐことができますが、個々のパラメーター自体はまたげません。 すなわち、prefixstartendsuffixは単一ブロック内のみ一致。
:~:text=The quick,lazy dog
次の例では一致しません
<div>The<div> </div>quick brown fox</div>
<div>jumped over the lazy dog</div>

なぜなら "The quick" という開始文字列が同一かつ途切れないブロック内に登場しないからです。ドキュメント内の"The quick" は"The"と"quick"の間にブロック要素が挟まっています。

ただしこの例なら一致します:

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>
  1. searchRangeRange(開始:document, 0、終了:document の長さ)とする

  2. searchRangecollapsed でないかぎり:

    1. potentialMatch を null に。

    2. parsedValuesprefixが null でない場合:

      1. prefixMatch範囲内文字列検索 を、 query parsedValuesprefixsearchRange searchRangewordStartBounded true、wordEndBounded false で実行した結果とする。

      2. prefixMatch が null であれば null を返す。

      3. searchRangestartprefixMatchboundary point の後ろ after にセット

      4. matchRangeRange(開始:prefixMatchend、終了:searchRangeend)とする。

      5. matchRangestart次の非空白位置へ進める。

      6. matchRangecollapsed なら null を返す。

        この場合、prefixMatchend 、またはその後の非空白場所がドキュメント末尾にあるときに発生します。
      7. Assert: matchRangestart nodeText ノード。

        matchRangestart は一致したprefixに続く次の非空白文字位置。
      8. mustEndAtWordBoundary を、parsedValuesend が null でなければ true、suffixが null なら true、そうでなければfalse。

      9. potentialMatch範囲内文字列検索を ( parsedValuesstart, matchRange, false, mustEndAtWordBoundary)で実行した結果をセット

      10. potentialMatch が null なら null を返す。

      11. potentialMatchstartmatchRangestart でなければ、次のループ を続ける。

        この場合、一致するprefixは見つかりましたが、それに続くテキストが一致せず、ループを継続して次のprefix出現箇所を探します。
    3. そうでなければ:

      1. mustEndAtWordBoundaryparsedValuesend が null でなければ true、suffixが null なら true、それ以外はfalse。

      2. potentialMatch範囲内文字列検索(parsedValuesstart, searchRange, true, mustEndAtWordBoundary)の結果をセット

      3. potentialMatch がnullならnullを返す。

      4. searchRangestartpotentialMatchstart の直後へ

    4. rangeEndSearchRangeRange(開始:potentialMatchend、終了:searchRangeend)とする。

    5. rangeEndSearchRangecollapsedでないかぎり:

      1. parsedValuesendがnullでなければ、

        1. mustEndAtWordBoundaryparsedValuessuffixが null なら true、そうでなければfalse。

        2. endMatch範囲内文字列検索( parsedValuesend, rangeEndSearchRange, true, mustEndAtWordBoundary ) の結果とする。

        3. endMatch が null なら null を返す。

        4. potentialMatchendendMatchend にセット。

      2. Assert: potentialMatchは非nullでcollapsedした範囲でなく、一致するテキスト範囲を表す。

      3. parsedValuessuffix がnullなら、potentialMatch を返す。

      4. suffixRangeRange (開始:potentialMatchend 、終了:searchRangeend)とする。

      5. suffixRangestart次の非空白位置 に進める。

      6. suffixMatch範囲内文字列検索( parsedValuessuffix, suffixRange, false, true) の結果

      7. suffixMatch が null なら null を返す。

        suffixが文書残りテキストに現れない場合、一致できない。
      8. suffixMatchstartsuffixRangestart なら potentialMatch を返す。

      9. parsedValuesendがnullなら break

        これは一致がexactでsuffixが一致しなければ、ループを抜けて次の範囲検索に進む。範囲一致の場合はinner loopを続行。
      10. rangeEndSearchRangestartpotentialMatchend

        この場合、正しいrange startはみつかったがrange end が一致しない場合で、inner loop を継続する。
    6. rangeEndSearchRangecollapsedなら:

      1. Assert: parsedValuesend がnullでない

      2. null を返す

        これはexact一致のbreakによるものを除き、rangeEnd+suffixが見つからなかった場合。
  3. null を返す

テスト
range range開始位置次の非空白位置へ進めるには、以下の手順に従う:
  1. range が collapsed(折りたたまれていない)間:

    1. noderange開始ノードとする。

    2. offsetrange開始オフセットとする。

    3. node検索不可部分木の一部である、または node可視テキストノード でない、または offsetnode長さと等しければ:

      1. range開始ノードshadow-including tree順で次のノードに設定する。

      2. range開始オフセット を 0 に設定する。

      3. 続ける

    4. node部分文字列データのオフセット offset から 6 文字分が文字列 "&nbsp;" に等しければ:

      1. range開始オフセット に6を加算。

    5. その他の場合、部分文字列データのオフセット offset から5文字分が文字列 "&nbsp" に等しければ:

      1. range開始オフセット に5を加算。

    6. それ以外の場合:

      1. cp を、コードポイントとして、nodeデータoffset 番目とする。

      2. cp空白属性を持たない場合、処理を終了する。

      3. range開始オフセット に1を加算。

範囲内で文字列を検索には、文字列 query範囲 searchRange、およびブール値 wordStartBoundedwordEndBounded を指定し、次の手順を実行:
このアルゴリズムは query テキストの最初の出現を searchRange 内で完全に含む 範囲 を返します。単語区切り(§ 3.6.2 Word Boundaries参照)で開始または終了位置を制限することもできます。一致がなければnullを返します。

このアルゴリズムの基本は、ブロック内の検索可能な全テキストノードをリストに収集し、それらを連結して検索し、ノードリストでオフセットを解釈して戻り値の範囲を作ること。

ブロックノードに到達するとリスト化を中断。例:

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

この場合は"abc"・"d"・"e"と分割して検索。

よって query は、ブロックレベルのコンテナ内で途切れのない連続したテキストのみに一致する。

  1. searchRangecollapsed でない間:

    1. curNodesearchRange開始ノード とする。

    2. curNode検索不可部分木であれば:

      1. searchRange開始ノード を、shadow-including tree順で curNodeshadow-including descendant でない次のノードに設定。

      2. searchRange開始オフセット を 0 に設定。

      3. 続ける

    3. curNode可視テキストノード でなければ:

      1. searchRange開始ノードshadow-including tree順で次の doctype でないノードに設定する。

      2. searchRange開始オフセット を 0 に設定。

      3. 続ける

    4. blockAncestor直近のブロック祖先 とする。

    5. textNodeListリスト(初期値は空)の Text ノードとする。

    6. curNodeshadow-including descendant かつ 境界点 (curNode, 0) が searchRangeの終了より後でない間:

      1. curNodeブロックレベル表示を持つ場合、break

      2. curNode検索不可であれば:

        1. curNode を、shadow-including tree順で curNodeshadow-including descendant でない次のノードに設定。

        2. 続ける

      3. curNode可視テキストノード であれば textNodeList に追加。

      4. curNode をshadow-including tree順で次のノードに更新。

    7. ノードリストから範囲を得る を引数 query, searchRange, textNodeList, wordStartBounded, wordEndBounded で実行し、結果が非nullならそれを返す。

    8. curNode が null なら break

    9. Assert: curNodesearchRangeの開始ノードに続くノードである。

    10. searchRangestart を (curNode, 0) に設定。

  2. null を返す。

ノードは、検索不可 である条件: 要素であり、HTML名前空間内にあり、以下いずれかを満たすとき:

  1. computed value(算出値)の display プロパティが none である。

  2. そのノードが voidとしてシリアライズされるとき。

  3. 次の型のいずれかである場合:HTMLIFrameElementHTMLImageElementHTMLMeterElementHTMLObjectElementHTMLProgressElementHTMLStyleElementHTMLScriptElementHTMLVideoElementHTMLAudioElement

  4. select 要素で multiple 属性がない場合

ノードが 検索不可部分木 の一部となる条件は、自身または shadow-including ancestor検索不可 である場合です。

ノードが 可視テキストノード と判定される条件は、Text ノードであり、親要素の算出値による親要素visibility プロパティが visible であり、描画されていることです。

ノードがブロックレベル表示を持つとは、 要素であり、 算出値による display プロパティが blocktableflow-rootgridflexlist-item のいずれかであることです。

node直近のブロック祖先を求めるには、次の手順:
  1. curNodenode にする。

  2. curNode が null でない間:

    1. curNodeText ノードでなく、かつ ブロックレベル表示を持つ場合は curNode を返す。

    2. それ以外は curNode を親ノードに更新。

  3. nodeノードドキュメントドキュメント要素 を返す。

ノードリストから範囲を得るには、検索文字列 queryString範囲 searchRangeTextノードリスト nodes、 ブール値 wordStartBounded および wordEndBounded を使い次の手順に従う:
オプションで、条件一致したテキストが単語境界で始まる/終わる場合のみ返せます。 例:
“range”は “mountain range” には必ず一致、
  1. 単語開始境界必須なら “color orange” には一致しません。

  2. 単語終了境界必須なら “forest ranger” には一致しません。

詳細・他例は § 3.6.2 Word Boundaries を参照。

  1. searchBuffernodes 内各要素の データ連結 して作る。

    データはこの文脈では正確でなく、DOM上のテキストデータなので注意。本来は描画済みテキストを対象にし、その後DOMに戻す必要あり。[Issue #WICG/scroll-to-text-fragment#98]

  2. searchStart を 0 に初期化。

  3. nodes の先頭が searchRange開始ノードなら searchStartsearchRange開始オフセットに設定。

  4. start/end境界点(null初期値)とする。

  5. matchIndex をnullに初期化。

  6. matchIndex がnullの間:

    1. searchBuffer で文字列 queryString の最初の出現インデックス matchIndexsearchStart から探す。比較は 第一水準(大文字小文字・アクセント無視)で。

      直感的には大文字小文字・濁点など無視の検索。
    2. matchIndex がnullなら null を返す。

    3. endIxmatchIndexqueryString長さとする。

      endIx は一致文字列の末尾+1の位置。
    4. start境界点として get boundary point at index matchIndexnodes、isEnd=false で実行した結果に。

    5. end境界点として get boundary point at index endIxnodes、isEnd=true で実行した結果に。

    6. wordStartBounded が true かつ matchIndex単語境界でない場合、または wordEndBounded が true かつ matchIndexqueryString の長さが 単語境界でない場合:

      1. searchStartmatchIndex+1に。

      2. matchIndex を nullに。

  7. endInset を0に初期化。

  8. nodes の末尾が searchRange終了ノードなら endInset を( searchRange終了ノード長さsearchRange終了オフセット )に設定。

    endInsetは最後のノードの逆方向のオフセット分。すなわち含まれない長さ。
  9. matchIndexqueryString長さsearchBuffer の長さ−endInset を超える場合、nullを返す。

    範囲末尾をまたいだマッチは無効。
  10. Assert: startendsearchRange 内の非null・正当な 境界点 である。

  11. 範囲(開始:start、終了:end)を返す。

インデックスから境界点を得るには、整数 indexTextノードリスト nodes、ブール値 isEnd を受け、次の手順:

これは上記アルゴリズムで、連結文字列上のインデックスがどのノードに所属するか確定するための補助ルーチンです。

isEndは始端と終端を区別のため利用。終端インデックスは一致文字列の「次の位置」を指す。マッチがノード境界上なら、次ノード頭ではなく、そのノード末としてほしい。

  1. counted を0に初期化。

  2. nodes の各 curNode について:

    1. nodeEnd を (countedcurNode長さ) とする。

    2. isEnd が true なら nodeEnd に1加算。

    3. nodeEndindex なら:

      1. 境界点 (curNode, indexcounted)を返す。

    4. countedcurNode長さ を加算。

  3. nullを返す。

3.6.2. 単語境界

単語境界への一致限定は、クロスオリジン情報漏洩を抑制するための対策の1つです。
Intl.Segmenter(Unicodeセグメンテーション規定を提案するもの)を参照。単語セグメント化も含む。将来的に規定されれば、このアルゴリズムもIntl.Segmenter APIを活用して単語境界一致を改善できる。

単語境界は、[UAX29]およびUnicode Text Segmentation § Word_Boundariesで定義されます。Unicode Text Segmentation § Default_Word_Boundariesでは デフォルトの単語境界規則セットを規定していますが、仕様でも述べている通り、より高度なアルゴリズムをロケールに応じて使うべきです。

辞書ベースの単語境界判定は、単語区切り文字のないロケールでは特に注意が必要です。英語ではスペース(' ')で単語区切りですが、日本語には単語間を区切る文字がありません。そのような場合、かつアルファベットが100文字未満の場合、辞書には有効な1文字単語をアルファベットの20%以内に抑える必要があります。

ロケールとは、有効な文字列[BCP47]言語タグ)または空文字列です。空文字列は主言語が不明であることを示します。

部分文字列が単語境界で区切られているとは、文字列 textロケール startLocaleendLocale が与えられたとき、その最初の文字の位置が 単語境界であるかつ、末尾(最後の文字の直後の位置)が 単語境界である場合を指します。

数値 position単語境界であるのは、文字列 text、および ロケール locale が与えられたとき、localeに従い 単語境界が直前にある、または text の長さが0より大きく、positionが0 または text長と等しい場合を指します。

直観的に、部分文字列が 単語境界に区切られているとは、その始端・終端とも単語の途中ではないということです。

区切り記号(例: 半角スペース)がある言語では(ほぼ)簡単ですが、技術資料で扱う改行・ハイフネーション・引用符等の詳細な規定があります。

区切り記号のない言語(中・日・韓等)は辞書ベースで有効な単語を認識する必要があります。

テキストフラグメントの一致語句は、隣接する文脈語と連結したときに単語境界となるように制限されます。たとえば exact検索 prefix,start,suffix では、"prefix+start+suffix"全体が単語境界で区切られているときのみマッチします。一方range検索 prefix,start,end,suffix では、"prefix+start""end+suffix"の両方が単語境界である必要があります。

目的は、第三者が一致トークンの全体を既知であるケースのみ閲覧できること。start,endのrangeマッチで内側が単語境界でなければ、第三者が繰り返し一致探索してトークン復元を試行(例:"Balance: 123,456 $"なら prefix="Balance: ", end="$" かつ start を1文字ずつ試す)できてしまう。

詳細はセキュリティレビュー文書 参照。

文字列 "An impressive mountain range" の中では "mountain range" は単語境界で区切られていますが、"An impressive mountain ranger" 内では区切られていません。
日本語の "ウィキペディアへようこそ" では "ようこそ" は単語境界となりますが、"ようこ" は単語途中とみなされます。

3.7. 一致したテキストの指示

UAは、try to scroll to the fragment の手順や他のメカニズムの一環として、テキストフラグメントをインビューにスクロールしても構いませんが、必ずしも一致箇所を表示する義務はありません。

UAはユーザに一致箇所を認識させるため、ハイコントラストなハイライトなど視覚的に強調して示すべきです。

UAはユーザーの操作で一致表示状態を解除し、強調を消す手段も提示すべきです。

その見た目やUI動作はUA依存です。ただし、オーサスクリプトで観測可能な手法(例:Documentのselection)によって表示してはなりません。そうするとコンテント抽出の攻撃ベクトルとなります。

UAは提供されたコンテキスト語句には視覚的強調を表示してはいけません。

強調はドキュメントの内容の一部ではないため、UAはユーザがページ内容と区別できる工夫も推奨されます。

UAは初回数回だけ、マーカーがリンク元によるUAによる目印である旨のヘルプをインプロダクトUI(例: ポップアップ)で提示してもよいでしょう。

3.7.1. UA機能のURL表示

UAは、ドキュメントURLを消費する様々な場所を提供します(window.locationのようなAPIを除く)。例としては、ロケーションバーで現在表示中ドキュメントのURLを見せたり、ブックマーク作成時のURLなどがあります。

ユーザの混乱を避けるため、こうしたURLにフラグメントディレクティブを含めるかどうかは統一すべきです。本節で推奨する標準動作例を示します。

ここでの推奨は統一的なユーザ体験を基準としますが、UA間相互運用性には影響せず厳密な適合要件ではありません。

細かい挙動は実装UAに委ねられ、デフォルト設定を変えたり抽出UIを出すのも可。例えばユーザがURL内のフラグメントディレクティブ公開を選べるようにも。

UX向上実験も許容されます。例えばテキストフラグメントが画面外にスクロールされた場合表示URLから省略してもよいでしょう。

原則として、URLはビジュアル指示(マーカー)が表示中のみフラグメントディレクティブを含めるべきです。マーカーが解除されたらURLからも同ディレクティブを除外すべきです。

URLがテキストフラグメントを含むが実際には一致しなかった場合、UAは公開URLからそれを省くこともできます。

ページ内にフラグメントが見つからないのは、リンク作成以降ページが変わったなどをユーザに示す有用な情報となります。

ただしブックマーク目的ではその情報はあまり役に立ちません。

よくある具体例は以下のとおりです。

ここでは「テキストフラグメント」「フラグメントディレクティブ」を区別せず同義に扱います。将来別種のディレクティブが登場した場合は、これらUXを個別に見直すことが必要です。
3.7.1.1. ロケーションバー

ロケーションバーのURLには、マーカー表示中はテキストフラグメントを含めるべきです。マーカー解除時はロケーションバーのURLから同ディレクティブを消してください。

たとえ一致しなくても、ロケーションバーにはフラグメント情報を含めることが推奨されます。

3.7.1.2. ブックマーク

多くのUAは「ブックマーク」機能で現在ページへのリンクを保存できます。

新たに作成されたブックマークURLは、デフォルトで一致がありかつマーカー表示中であればフラグメントディレクティブを含めるべきです。

ブックマークからの遷移時は通常と同様、同ディレクティブを処理します。

3.7.1.3. 共有

一部UAはURL共有(他アプリ・メッセージ転送)機能を提供します。

この場合も、一致がありマーカーが未解除ならフラグメントディレクティブをURLに含めるべきです。

3.8. Document Policyとの統合

本仕様は Document Policy の設定点として "force-load-at-top" という名前で定義します。boolean で、初期値falseです。

有効化時、このポリシーは全自動ページ読込時スクロール機能(テキストフラグメント・要素フラグメント・履歴復元スクロール)を無効化します。
例:ユーザが https://example.com#:~:text=foo に遷移、 example.comはレスポンスヘッダで
Document-Policy: force-load-at-top
と返した場合、

ページロード時、"foo"を含む要素は indicated partとしてマークされ、target elementとなります。しかし "foo" はスクロール表示されません。

このポリシーによるフラグメント由来のスクロール抑制は、本書scroll to the fragmentアルゴリズム修正として§ 3.6 テキストフラグメントへのナビゲート内で規定されています。

履歴スクロール復元の抑制は、restore persisted state ステップの2の直後に下記処理を挿入することで規定します:

  1. "force-load-at-top" 機能のDocument policy値を取得し、Documentについて trueなら、ユーザエージェントは Documentやそのスクロール可能領域のスクロール位置を復元してはならない。

3.9. 機能検出について

機能検出のため、新しいFragmentDirectiveインターフェイスを追加し、UAが対応していればdocument.fragmentDirectiveで公開する提案です。

[Exposed=Window]
interface FragmentDirective {
};

このため Document インターフェイスに fragmentDirective プロパティを追加します:

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

このオブジェクトは将来テキストフラグメントやその他ディレクティブに関する追加情報を提供する目的にも使えるでしょう。

4. テキストフラグメントディレクティブ生成

この節は規範的でありません。

本節ではUAがテキストディレクティブ付きURLを自動生成する際の推奨を示します。必須仕様ではありませんが、生成URLの安定性・利便性を高める観点でまとめています。

4.1. 範囲一致より完全一致を優先

一致テキストは exact型(例: "text=foo%20bar%20baz")または range型(例: "text=foo,bar")いずれでも指定できます。

可能なら全体を1つの文字列として指定するほうが望ましいです。これにより遷移先ページが削除・変動しても意図したリンク先がURLから分かります。

例: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" が追加されると違う箇所がターゲットとなってしまいます。

また、範囲一致は意味的な価値も小さいです。当該文自体がなくなった場合、ユーザーは意図したターゲット文を知り得ません。exactタイプならUAやユーザーが「探しても出ない」旨明示できます。

範囲一致は引用テキストが非常に長く、全体をエンコードするとURLが極端に長大になる場合は有用です。

300文字未満のテキスト断片は完全一致を推奨、超える場合はrange型も可。

今後この上限値について合理的な指標を検討予定。

4.2. 文脈指定は必要時のみ

文脈指定はテキストディレクティブによるページ内断片の曖昧性解消に使えますが、構造変化などで 脆くなりやすいです。多くの場合、断片テキストは要素境界で始まったり終わることが多いので文脈側が隣接別要素に現れます。そのためページ構造の変更で文脈と一致断片が隣接しなくなりURLが無効になりがちです。

例:次のテキストへのURL化の場合
<div class="section">HEADER</div>
<div class="content">Text to quote</div>

ディレクティブ例:

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

しかし、ページが [edit] リンクをヘッダー隣に追加した場合、このURLは壊れてしまいます。

断片テキストが十分長くユニークな場合、UAは無用な文脈語追加を避けるべきです。

文脈指定が必要な条件:

この数値基準も今後検討予定。

4.3. フラグメントIDの要否判定

UAがテキストディレクティブ付きURLへ遷移時、テキストフラグメントが見つからなければ通常の要素IDフラグメントにフォールバックしてスクロールします。

これは、ページテキストが変化してテキストディレクティブが無効化された場合の保険として有用です。

例:URL 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)が開いてしまい混乱の元となります。

UAが妥当な要素IDフラグメントを自動判定できない場合、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]

本仕様の例は “for example” や class="example" の記述で導入し、本文と区別します。

これは情報提供目的の例です。

非規範的な注記(Note)は文頭が “Note” で始まるか、class="note" などで本文と区別します。

注:これは情報目的の注記です。

テスト

本仕様の内容に関するテストはこのような “Tests” ブロック内で示すことがあります。 その種のブロックは規範的ではありません。


索引

本仕様で定義される用語

参考規格によって定義される用語

参考文献

規範参考文献

[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS カスケーディングおよび継承 レベル5. URL: https://drafts.csswg.org/css-cascade-5/
[CSS-DISPLAY-3]
Elika Etemad; Tab Atkins Jr.. CSS Display モジュール レベル 3. URL: https://drafts.csswg.org/css-display/
[CSS-DISPLAY-4]
CSS Display モジュール レベル 4. Editor's Draft. URL: https://drafts.csswg.org/css-display-4/
[CSSOM-VIEW-1]
Simon Pieters. CSSOM View モジュール. URL: https://drafts.csswg.org/cssom-view/
[DOCUMENT-POLICY]
Ian Clelland. ドキュメントポリシー. ED. URL: https://wicg.github.io/document-policy
[DOM]
Anne van Kesteren. DOM 標準. Living Standard. URL: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. エンコーディング標準. Living Standard. URL: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch 標準. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML 標準. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. インフラ標準. Living Standard. URL: https://infra.spec.whatwg.org/
[MIMESNIFF]
Gordon P. Hemsley. MIME スニッフィング標準. Living Standard. URL: https://mimesniff.spec.whatwg.org/
[RFC2119]
S. Bradner. RFC で要件レベルを示すためのキーワード. 1997年3月. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[UAX29]
Josh Hadley. Unicode テキストセグメンテーション. 2023年8月16日. Unicode Standard Annex #29. URL: https://www.unicode.org/reports/tr29/tr29-43.html
[URL]
Anne van Kesteren. URL 標準. Living Standard. URL: https://url.spec.whatwg.org/
[UTS10]
Ken Whistler; Markus Scherer. Unicode 照合アルゴリズム. 2023年9月5日. Unicode Technical Standard #10. URL: https://www.unicode.org/reports/tr10/tr10-49.html
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 標準. Living Standard. URL: https://webidl.spec.whatwg.org/

参考情報

[BCP47]
A. Phillips, Ed.; M. Davis, Ed.. 言語識別用タグ. 2009年9月. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc5646
[FETCH-METADATA]
Mike West. Fetch メタデータリクエストヘッダ. WD. URL: https://w3c.github.io/webappsec-fetch-metadata/

IDL 索引

[Exposed=Window]
interface FragmentDirective {
};

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

課題索引

TODO: URL のフラグメントが ':~:' で終わる場合(つまり空ディレクティブ)、null を返し、明示的なディレクティブが指定されていない(既存ディレクティブの上書きを回避)扱いになる。ただし、空文字列を返す方が良いかもしれません?この場合、ページは '#:~:' へ navigate/pushState することで明示的にディレクティブ/ハイライトを消せるようになります。
シャドウツリー内の場合、ターゲットは何を設定すべきか? #190
これらの特定アルゴリズムはいまのままだと target が祖先やルートドキュメントノードになりうるためうまく動作しない。#89 では一致対象を contain:style layout ブロックに限定する提案があり、これが解決につながる。
実装ノート:現状Blinkはtext fragmentでフォーカスを設定しませんが、すべき?TODO: crbugをファイル化。
厳密には正しくなく、Chromeは same-origin イニシエーターには許容しています。この点の仕様修正が必要です。[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]