これは人類最初の試みとして、驚異に満ちたリンクの物語である: テキストフラグメント
テキストフラグメントを使用すると、URLフラグメントでテキストスニペットを指定できます。 このようなテキストフラグメントを含むURLに移動する場合、ブラウザはそれを強調したりユーザーの注意を引いたりすることができます。
フラグメント識別子 #
Chrome80は大きなリリースでした。これには、WebワーカーのECMAScriptモジュール、 Null合体、オプションのチェーンなど、非常に期待されている機能が多数含まれていました。このリリースは、いつものように、Chromiumブログのブログ記事を通じて発表されました。下のスクリーンショットはそのブログ記事の抜粋です。
id属性を持つ要素の周りに赤いボックスが付いたChromiumのブログ記事。すべての赤いボックスが何を意味するのかを考えているのではないでしょうか。これらは、DevToolsで次のスニペットを実行した結果です。id属性を持つすべての要素が強調表示されています。
document.querySelectorAll('[id]').forEach((el) => {
el.style.border = 'solid 2px red';
});
フラグメント識別子のお陰で、赤いボックスで強調表示された要素にディープリンクを作成することができます。これは、後で、ページのURLのハッシュで使用できるようになります。横にある「プロダクトフォーラムにフィードバックを送信」ボックスにディープリンクを作成するとした場合、手動でURL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1とすることで作成することができます。DevToolsの要素パネルに表示されているとおり、対象の要素にはid属性と値HTML1があります。
idを表示するDevTools。このURLをJavaScriptのURL()コンストラクターで解析すると、さまざまなコンポーネントが表示されます。値が#HTML1を持つhashプロパティに注目してください。
new URL('https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1');
/* Creates a new `URL` object
URL {
hash: "#HTML1"
host: "blog.chromium.org"
hostname: "blog.chromium.org"
href: "https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1"
origin: "https://blog.chromium.org"
password: ""
pathname: "/2019/12/chrome-80-content-indexing-es-modules.html"
port: ""
protocol: "https:"
search: ""
searchParams: URLSearchParams {}
username: ""
}
*/
ただし、DevToolsを開かなければ要素のidを見つけられないということは、ブログ記事の作成者がページのこの特定のセクションにリンクする必要があることを示す可能性があります。
idを使わずに何かにリンクしたい場合はどうなるでしょうか。ECMAScript Modules in Web Workersヘッダーにリンクするとしましょう。下のスクリーンショットでわかるように、問題の<h1>にはid属性がありません。つまり、この見出しにリンクする方法はないということです。テキストフラグメントを使用すると、この問題を解決できます。
idのない見出しを表示するDevTools。テキストフラグメント #
テキストフラグメントの提案は、URLハッシュにテキストスニペットを指定するためのサポートを追加します。このようなテキストフラグメントを含むURLに移動する場合、ユーザーエージェントはそれを強調したり、ユーザーの注意を引くことができます。
ブラウザの互換性 #
テキストフラグメント機能は、バージョン80以降のChromiumベースのブラウザでサポートされています。これを書いている時点では、SafariとFirefoxはこの機能を実装する意図を公に示していません。SafariとFirefoxのディスカッションへのポインタは、関連リンクを参照してください。
セキュリティ上の理由から、この機能ではリンクをnoopenerコンテキストで開く必要があります。したがって、 <a>アンカーマークアップにrel="noopener"を含めるか、ウィンドウ機能フィーチャーのWindow.open()リストにnoopenerを追加してください。
textStart #
テキストフラグメントの構文、最も単純な形式では次のようになります。ハッシュ記号#の後に:~:text=が続き、最後にtextStartという、リンクするパーセントエンコードされたテキストを表すコードを入力します。
#:~:text=textStart
たとえば、Chrome 80の機能を発表するブログ記事のなかで、「ECMAScript Modules in Web Workers」という見出しにリンクするとした場合、このURLは次のようになります。
テキストフラグメントはこのようにが強調されます。Chromeなどのサポートブラウザでこのリンクをクリックすると、テキストフラグメントが強調表示され、スクロールして表示されます。
textStartとtextEnd #
では、「ECMAScript Modules in Web Workers」というタイトルの見出しだけでなく、セクション全体にリンクするにはどうすればよいでしょうか?セクションのテキスト全体をパーセントエンコードすると、URLがありえないほど長くなります。
幸いなことに、もっと良い方法があります。テキスト全体ではなく、textStart,textEnd構文を使用して目的のテキストをフレーム化する方法です。したがって、目的のテキストの先頭にパーセントエンコードされた単語と目的のテキストの終わりにパーセントエンコードされた単語をカンマ(,)区切りで指定します。
これは次のようになります。
textStartのECMAScript%20Modules%20in%20Web%20Workers、コンマ(,)、そしてtextEndとしてES%20Modules%20in%20Web%20Workers.となっています。Chromeなどのサポートされているブラウザでクリックすると、セクション全体が強調表示され、スクロールして表示されます。
textStartとtextEndの選択について疑問に思うかもしれません。実際には、わずかに短いURL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers.で、両側に2つの単語しか使用しなくてもよいでしょう。textStartとtextEndを以前の値と比較してください。
textStartとtextEndの両方に1つの単語しか使用しない場合、問題が発生していることがわかります。URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers.はさらに短くなりましたが、強調表示されたテキストフラグメントは本来求めていたものではなくなりました。強調表示は、最初に出現するWorkers.で停止し、これは正しい動作ではありますが、私が強調しようとしたものではありません。問題は、目的のセクションは、現在の一単語のみのtextStartとtextEndの値では一意に識別されていないということです。
prefix-と-suffix #
textStartとtextEndで十分な長さの値を使用することが、一意のリンクを取得するための1つのソリューションです。ただし、状況によっては、これが不可能な場合もあります。ちなみに、なぜChrome 80リリースのブログ記事を例として選んだのでしょうか?このリリースでテキストフラグメントが導入されたからです。
上のスクリーンショットで「テキスト」という単語が4回表示されていることに注目してください。 4番目の出現は緑色のコードフォントで書かれています。この特定の単語にリンクする場合は、 textStartをtextに設定します。「テキスト」という単語は1つの単語だけなので、 textEndは存在できません。どうすればよいでしょうか?URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=textとすると、見出しにも「Text」が存在するため、最初の出現で一致してしまいます。
幸いなことに解決策があります。このようなケースでは、prefix-と-suffixを指定することができます 。緑のコードフォントの「text」の前の単語は「the」で、後の単語は「parameter」です。ほかの3つの「Text」の出現には、これと同じ前後の単語がありません。この知識を使って、前のURLを微調整して、prefix-と-suffixを追加できます。他のパラメーターと同様に、これらもパーセントエンコードする必要があり、複数の単語を含めることができます。 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=the-,text,-parameter 。パーサーがprefix-と-suffixをはっきりと識別できるようにするに 、textStartから分離し、オプションのtextEndにはダッシュ-を使用します。

完全な構文 #
テキストフラグメントの完全な構文を以下に示します。(角括弧はオプションのパラメーターを示します。)すべてのパラメーターの値はパーセントエンコードする必要があります。これは特に、ダッシュ- 、アンパサンド& 、およびコンマ,文字の場合に特に重要であるため、テキストディレクティブ構文の一部として解釈されていません。
#:~:text=[prefix-,]textStart[,textEnd][,-suffix]
prefix-、textStart、textEnd、および-suffixはそれぞれ、単一のブロックレベル要素内のテキストに一致しますが、完全なtextStart,textEndの範囲は複数のブロックにまたがることができます。たとえば、 :~:text=The quick,lazy dogは次の例では失敗します。これは開始文字列の「The quick」が単一の中断されないブロックレベル要素内に表示されないためです。
<div>
The
<div></div>
quick brown fox
</div>
<div>jumped over the lazy dog</div>
ただし、この例では一致します。
<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>
ブラウザ拡張機能を使ってテキストフラグメントURLを作成する #
テキストフラグメントのURLを手動で作成することは、特にそれらが一意であることを確認する場合には面倒です。本当に必要な場合は、仕様にいくつかのヒントがあり、 テキストフラグメントURLを生成するための正確な手順がリストされています。 Link to Text Fragmentと呼ばれるオープンソースのブラウザ拡張機能を提供しています。この拡張機能を使用すると、テキストを選択し、コンテキストメニューの[選択したテキストにリンクをコピー]をクリックして任意のテキストにリンクできます。この拡張機能は、次のブラウザで使用できます。
- Google Chrome用Link to Text Fragment
- Microsoft Edge用Link to Text Fragment
- Mozilla Firefox用Link to Text Fragment
- Apple Safari用Link to Text Fragment
1つのURLに複数のテキストフラグメント #
1つのURLに複数のテキストフラグメントが表示される可能性があることに注意してください。特定のテキストフラグメントは、アンパサンド文字で&区切る必要があります。ここに、3つのテキストフラグメントを含むリンクの例を示します。https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=Text%20URL%20Fragments&text=text,-parameter&text=:~:text=On%20islands,%20birds%20can%20contribute%20as%20much%20as%2060%25%20of%20a%20cat's%20diet。
要素とテキストフラグメントの混合 #
従来の要素フラグメントをテキストフラグメントと組み合わせることができます。同一のURLに両方のフラグメントを含めることにまったく問題はないため、たとえばページの元のテキストが変更したことでテキストフラグメントが一致しなくなった場合の意味のあるフォールバックを提供することができます。「Give us feedback in our Product Forums」セクションにリンクするURL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1:~:text=Give%20us%20feedback%20in%20our%20Product%20Forums. には、要素フラグメント(HTML1)だけでなく、テキストフラグメント(text=Give%20us%20feedback%20in%20our%20Product%20Forums.)の両方が含まれています。
フラグメントディレクティブ #
まだ説明していない構文の要素が1つあります。それは、フラグメントディレクティブ:~:です。上記のような既存のURL要素フラグメントとの互換性の問題を回避するために、 Text Fragments仕様にはフラグメントディレクティブが導入されています。:~:というコードシーケンスで区切られたURLフラグメントの一部です。text=などのユーザーエージェント命令用に予約されており、作成者のスクリプトが直接対話できないように、読み込み中にURLから削除されます。ユーザーエージェント命令は、ディレクティブとも呼ばれます。そのため、具体的なケースでは、 text=はテキストディレクティブと呼ばれます。
機能の検出 #
サポートを検出するには、 documentにfragmentDirectiveプロパティがあるかをテストします。フラグメントディレクティブは、URLがドキュメントではなくブラウザに向けられた命令を指定するためのメカニズムです。これは、作成者スクリプトとの直接の対話を回避することを目的としているため、既存のコンテンツに重大な変更を加える心配なく、将来のユーザーエージェント命令を追加できます。たとえば、翻訳のヒントを追加する可能性があります。
if ('fragmentDirective' in document) {
// Text Fragments is supported.
}
機能検出は主に、リンクが動的に生成され(たとえば、検索エンジンによって)、それらをサポートしていないブラウザへのテキストフラグメントリンクの提供を回避する場合を対象としています。
テキストフラグメントのスタイル付け #
デフォルトでは、ブラウザはmarkのスタイル(通常、markのCSSシステムカラーである黄色に黒)と同じスタイルをテキストフラフメントにも設定します。ユーザーエージェントのスタイルシートには、次のようなCSSが含まれています。
:root::target-text {
color: MarkText;
background: Mark;
}
ご覧のとおり、ブラウザは、適用された強調表示をカスタマイズするために使用できる::target-textという疑似セレクターを公開します。たとえば、テキストフラグメントを赤い背景に黒いテキストになるようにデザインできます。いつものように、オーバーライドのスタイルがアクセシビリティの問題を引き起こさないように色のコントラストを確認し、ハイライトが実際に他のコンテンツから視覚的に目立つようにしてください。
:root::target-text {
color: black;
background-color: red;
}
ポリフィル可能性 #
テキストフラグメント機能は、ある程度ポリフィルすることができます。機能がJavaScriptで実装されているテキストフラグメントの組み込みサポートを提供しないブラウザ用に、拡張機能によって内部的に使用されるポリフィルを提供します。
プログラムによるテキストフラグメントリンクの生成 #
ポリフィルには、インポートしてテキストフラグメントリンクを生成するために使用できるfragment-generation-utils.jsが含まれています。これは、以下のコードサンプルで概説されています。
const { generateFragment } = await import('https://unpkg.com/text-fragments-polyfill/dist/fragment-generation-utils.js');
const result = generateFragment(window.getSelection());
if (result.status === 0) {
let url = `${location.origin}${location.pathname}${location.search}`;
const fragment = result.fragment;
const prefix = fragment.prefix ?
`${encodeURIComponent(fragment.prefix)}-,` :
'';
const suffix = fragment.suffix ?
`,-${encodeURIComponent(fragment.suffix)}` :
'';
const textStart = encodeURIComponent(fragment.textStart);
const textEnd = fragment.textEnd ?
`,${encodeURIComponent(fragment.textEnd)}` :
'';
url += `#:~:text=${prefix}${textStart}${textEnd}${suffix}`;
console.log(url);
}
分析目的でのテキストフラグメントの取得 #
多くのサイトがルーティングにフラグメントを使用しているため、ブラウザはそれらのページを壊さないようにテキストフラグメントを取り除きます。分析目的などでテキストフラグメントのリンクをページに公開する必要があることは認められていますが、提案されたソリューションはまだ実装されていません。今のところ回避策として、以下のコードを使用して必要な情報を抽出できます。
new URL(performance.getEntries().find(({ type }) => type === 'navigate').name).hash;
セキュリティ #
テキストフラグメントディレクティブは、ユーザーのアクティブ化の結果である完全な(同じページではない)ナビゲーションでのみ呼び出されます。さらに、宛先とは異なるオリジンから発信されたナビゲーションでは、宛先のページが十分に分離されていることがわかるようにnoopenerコンテキストで発生する必要があります。テキストフラグメントディレクティブは、メインフレームにのみ適用されます。つまり、テキストはiframe内で検索されず、iframeナビゲーションでテキストフラグメントを呼び出すことはできません。
プライバシー #
テキストフラグメントがページで見つかったかどうかに関係なく、テキストフラグメント仕様の実装がリークしないことが重要です。要素フラグメントは元のページ作成者の完全な制御下にありますが、テキストフラグメントは誰でも作成できます。上記の 例では、<h1>にidがなかったために、「ECMAScript Modules in Web Workers 」という見出しにリンクできなかったことを思い出してください。それでは、どうすれば、テキストフラグメントを慎重に作成することで、誰もがどこにでもリンクできるというのでしょうか。
悪意のあるevil-ads.example.comという広告ネットワークを運営しているとしましょう。さらに、広告用のiframeの1つでは、ユーザーが広告と対話したら、テキストフラグメントURL dating.example.com#:~:text=Log%20Outを使って、dating.example.comへの隠しクロスオリジンiframeを動的に作成したことを想像してください。「Log Out」というテキストがあれば、犠牲者は現在dating.example.comにログイン中であることになるため、プロファイリングに使用することができます。テキストフラグメントの実装は悪意があるなどを認識しないことから、正しく一致するとフォーカスが切り替わるようにできるため、evil-ads.example.comで、blurイベントをリスンして一致が発生したことを知ることができます。Chromeでは、このようなシナリオを実現できないようにテキストフラグメントが実装されています。
他には、スクロールの位置に応じてネットワークトラフィックを悪用する攻撃があります。会社のイントラネットの管理者のように、被害者のネットワークトラフィックログにアクセスできたとしましょう。次に、人事関連の「What to Do If You Suffer From…(次のような症状への対策)」という長いドキュメントがあり、burn out(燃え尽き)、anxiety(不安)などの症状のリストがあるとします。各項目の横に追跡用のピクセルを配置し、ドキュメントの読み込みが、たとえば「burn out」項目の追跡ピクセルの読み込みと一時的に同時に発生することがわかったなら、イントラネット管理者として、機密情報であり、誰にも表示されないと社員が思い込んでいる「:~:text=burn%20out」を使ったテキストフラグメントのリンクを社員がクリックしたと判定することができます。この例は、多少工夫されており、非常に具体的な前提条件が満たされることで悪用が成り立つため、Chromeのセキュリティチームはナビゲーションにスクロールを実装するリスクを管理可能と評価しました。他のユーザーエージェントは、代わりに手動のスクロールUI要素を表示するように決定している場合があります。
オプトアウトを希望するサイトの場合、Chromiumは送信可能なドキュメントポリシーヘッダー値をサポートしているため、ユーザーエージェントはテキストフラグメントURLを処理しません。
Document-Policy: force-load-at-top
テキストフラグメントの無効化 #
この機能を無効にする最も簡単な方法は、HTTPレスポンスヘッダーを注入できる拡張機能を使用することです。たとえば、ModHeader(非Google製品)を使用すると、次のようにレスポンス(リクエストではありません)ヘッダーを挿入することができます。
Document-Policy: force-load-at-top
オプトアウトするもう1つの方法は、より複雑ではありますが、エンタープライズ設定のScrollToTextFragmentEnabled を使用することです。macOSでこれを行うには、以下のコマンドをターミナルに貼り付けます。
defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false
Windowsの場合は、Google Chrome Enterpriseヘルプサポートサイトのドキュメントに従ってください。
Web検索のテキストフラグメント #
一部の検索では、Google検索エンジンは、関連するWebサイトのコンテンツスニペットによってクイックアンサーまたは要約を提供します。これらの注目のスニペットは、検索が質問の形式である場合に表示される可能性が最も高くなります。注目のスニペットをクリックすると、ユーザーはソースWebページの注目のスニペットテキストに直接移動します。この動作は、自動的に作成されたテキストフラグメントURLによるものです。
まとめ #
テキストフラグメントURLは、Webページ上の任意のテキストにリンクするための強力な機能です。これを使用すると、学術コミュニティは非常に正確な引用または参照リンクを提供でき、検索エンジンはページ上のテキスト結果にディープリンクを作成できます。また、ソーシャルネットワーキングサイトは、アクセスできないスクリーンショットではなく、ユーザーがWebページの特定の一節を共有できるようにすることができます。テキストフラグメントURLを使用し、便利であることに気づいていただければと思います。Link to Text Fragmentブラウザ拡張機能をぜひインストールしてください。
関連リンク #
- Spec draft
- TAG Review
- Chrome Platform Status ページ
- Chromeのバグトラッカー
- Intent to Shipスレッド
- WebKit-Devスレッド
- Mozilla standards positionスレッド
謝辞 #
テキストフラグメントは、Grant Wangの貢献とともに、Nick BurrisとDavid Bokanによって実装され、仕様に含まれました。この記事を徹底的にレビューしてくれたJoe Medleyに感謝しています。ヒーロー画像提供: Greg Rakozy(Unsplash)。