エスケープ文字を含むテキストをリンク化する場合はlinkify-elementを使った方が良さそう
こんにちは!CastingONEの大沼です。
始めに
弊社ではURLテキストを自動でaタグに差し替える処理でlinkifyを使っております。その中でもlinkify-htmlを使っていましたが、色々触っていたところエスケープ文字がそのまま表示されず変換されてしまっていることに気づきました。
かなりエッジケースなので最悪未対応でも良いかとも思いましたが、色々調べてみて原因と対処法が見つかりましたので記事にまとめました。
他のリンク化ライブラリについて
他のリンク化ライブラリに乗り換えるという考えもあると思いますが、それは既にしていました。以前はAutolinker.jsを使っていましたが、 →https://google.com
のようなリンクテキスト前に記号文字がある時に上手くリンク化されない問題がありました。(oogle.com
だけリンク化しました)
こういった問題があってlinkify-html
に変えており、出来れば今のライブラリでなんとか解決したい思いがありました。
結論
結論から先に言うと、linkify-elementを使うことで解消されます。これはHTML文字列にリンク化処理を加えるlinkify-html
と違って、DOMに対してリンク化する処理加えます。従ってReactだと一度DOMに変換する処理を書く必要が出るため、この辺のロジックを閉じ込めたコンポーネントにすると使いやすくなると思います。
import { FC, useRef, useEffect } from 'react';
import sanitizeHtml from 'sanitize-html';
import linkifyElement from 'linkify-element';
const sanitize = (text: string) => {
return sanitizeHtml(text, {
disallowedTagsMode: 'escape',
});
};
const MyLinkify: FC<{ text: string; sanitizer?: (dirty: string) => string }> = ({
text,
sanitizer = sanitize,
}) => {
const elRootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const elDiv = document.createElement('div');
elDiv.innerHTML = sanitizer(text);
linkifyElement(elDiv);
elRootRef.current?.replaceChildren(...elDiv.childNodes);
}, [text, sanitizer]);
return <div ref={elRootRef} />;
};
linkify-htmlだとエスケープ文字が変換される理由
linkify-htmlを使った時にエスケープ文字が変換されてしまう問題ですが、エスケープ文字を更にエスケープ処理されていないことが問題でした。
現状
> → >
期待値
> → &gt;
linkifyによってaタグをDOMとして表示するにはReactではdangerouslySetInnerHTML
で渡す必要があるため、>
と言う文字列をそのままにしていたら>
に変換されてしまうのは必然です。&gt;
という文字列だと>
という文字で元と同じものになるため& → &
に変換する処理が必要なのですが、linkify-html
とsanitize-html
どちらもこの処理がされませんでした。>
という文字はキチンと>
に変換されていたため、既にエスケープ文字になっているので不要と判断されているんですかね🤔
& → &
に変換すれば良いため正規表現で自前で処理することも考えましたが、URLに&
が含まれている場合、そこは変換してしまうとまずく、それを考慮するとロジックが複雑になるため断念しました。
この問題はHTML文字列からDOM化するときに起きるので、最初からDOMにマウント済みのものに対してリンク化処理を適応すれば問題を回避できるため、前セクションで書いたように linkify-element
を使うことで解決しました😄
サニタイズしてdangerouslySetInnerHTML
でDOM表示するパターンとプレーンテキストとして扱えるように完全にエスケープするパターンで動作確認をしたところ、どちらもlinkify-html
だとエスケープ文字の解釈が1回分行われていました。
※エスケープ版で</b>
までリンク対象になってしまっていますが、本来はこういうURLテキストは入力しないと思うため気にしていません。
その他
linkify-react
を使うと良さそう
リンク以外のタグは認識させない場合は単純なプレーンテキストにURLだけリンク化したい場合、linkify-html
やlinkify-element
ではエスケープ処理を通してから実行する必要がありますが、linkify-react
を使うと最初からエスケープされているため楽に実装できます。
import { FC } from 'react';
import Linkify from 'linkify-react';
const App: FC = () => {
const text = "<b><div&gt;<br />https://google.com</b>";
// タグを一切受け付けない場合はlinkify-reactを使うと楽
return <Linkify>{text}</Linkify>;
};
linkify-html
では不正なタグは省略されてしまう
検証中に気づいたことですが、例えば<_<b>&lt;div&gt;<br />https://google.com</b>
みたいに解析不能なタグを含めると<_
という文字列が消えてしまいました。
ロジックを軽くみてみると、どうやらlinkifyする前にまずはhtmlTokenize
でトークン化するようで、この時にスキップされてしまうようです。
先ほどの文字列を htmlTokenize
した結果をconsole.logしたら<_
に関する情報が消えていました。
これは結構致命的な問題だと思いましたが、事前にサニタイズ処理をすることで<
は<
に置き換わって問題なくトークン化できていたので実運用では問題にならなそうだったのでその他に補足として載せました。
終わりに
以上がエスケープ文字を含むテキストをlinkifyする方法でした。リンク化する処理は細かいところで期待しない動作になって調整にかなり苦労しましたが、これでfixされたかなと思いました😊
リンク化処理の実装の参考になれれば幸いです。
検証コードは以下のStackBlitzに書きましたので、詳細のコードが気になる方はご参照ください。
弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!
Discussion