🙆

エスケープ文字を含むテキストをリンク化する場合はlinkify-elementを使った方が良さそう

2024/07/31に公開

こんにちは!CastingONEの大沼です。

始めに

弊社ではURLテキストを自動でaタグに差し替える処理でlinkifyを使っております。その中でもlinkify-htmlを使っていましたが、色々触っていたところエスケープ文字がそのまま表示されず変換されてしまっていることに気づきました。

かなりエッジケースなので最悪未対応でも良いかとも思いましたが、色々調べてみて原因と対処法が見つかりましたので記事にまとめました。

他のリンク化ライブラリについて

他のリンク化ライブラリに乗り換えるという考えもあると思いますが、それは既にしていました。以前はAutolinker.jsを使っていましたが、 →https://google.com のようなリンクテキスト前に記号文字がある時に上手くリンク化されない問題がありました。(oogle.comだけリンク化しました)

こういった問題があってlinkify-htmlに変えており、出来れば今のライブラリでなんとか解決したい思いがありました。

結論

結論から先に言うと、linkify-elementを使うことで解消されます。これはHTML文字列にリンク化処理を加えるlinkify-htmlと違って、DOMに対してリンク化する処理加えます。従ってReactだと一度DOMに変換する処理を書く必要が出るため、この辺のロジックを閉じ込めたコンポーネントにすると使いやすくなると思います。

linkify-elementを使ってリンク化する
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; → &gt;

期待値
&gt; → &amp;gt;

linkifyによってaタグをDOMとして表示するにはReactではdangerouslySetInnerHTMLで渡す必要があるため、&gt;と言う文字列をそのままにしていたら>に変換されてしまうのは必然です。&amp;gt;という文字列だと&gt;という文字で元と同じものになるため& → &amp;に変換する処理が必要なのですが、linkify-htmlsanitize-htmlどちらもこの処理がされませんでした。>という文字はキチンと&gt;に変換されていたため、既にエスケープ文字になっているので不要と判断されているんですかね🤔
& → &amp;に変換すれば良いため正規表現で自前で処理することも考えましたが、URLに&が含まれている場合、そこは変換してしまうとまずく、それを考慮するとロジックが複雑になるため断念しました。
この問題はHTML文字列からDOM化するときに起きるので、最初からDOMにマウント済みのものに対してリンク化処理を適応すれば問題を回避できるため、前セクションで書いたように linkify-element を使うことで解決しました😄
サニタイズしてdangerouslySetInnerHTMLでDOM表示するパターンとプレーンテキストとして扱えるように完全にエスケープするパターンで動作確認をしたところ、どちらもlinkify-htmlだとエスケープ文字の解釈が1回分行われていました。

※エスケープ版で</b>までリンク対象になってしまっていますが、本来はこういうURLテキストは入力しないと思うため気にしていません。

その他

リンク以外のタグは認識させない場合はlinkify-reactを使うと良さそう

単純なプレーンテキストにURLだけリンク化したい場合、linkify-htmllinkify-elementではエスケープ処理を通してから実行する必要がありますが、linkify-reactを使うと最初からエスケープされているため楽に実装できます。

タグを一切受け付けないプレーンテキストの場合
import { FC } from 'react';
import Linkify from 'linkify-react';

const App: FC = () => {
  const text = "<b>&lt;div&amp;gt;<br />https://google.com</b>";
  // タグを一切受け付けない場合はlinkify-reactを使うと楽
  return <Linkify>{text}</Linkify>;
};

linkify-htmlでは不正なタグは省略されてしまう

検証中に気づいたことですが、例えば<_<b>&amp;lt;div&amp;gt;<br />https://google.com</b>みたいに解析不能なタグを含めると<_という文字列が消えてしまいました。
ロジックを軽くみてみると、どうやらlinkifyする前にまずはhtmlTokenizeでトークン化するようで、この時にスキップされてしまうようです。

https://github.com/Hypercontext/linkifyjs/blob/v4.1.3/packages/linkify-html/src/linkify-html.js#L16-L19

先ほどの文字列を htmlTokenize した結果をconsole.logしたら<_に関する情報が消えていました。

これは結構致命的な問題だと思いましたが、事前にサニタイズ処理をすることで<&lt;に置き換わって問題なくトークン化できていたので実運用では問題にならなそうだったのでその他に補足として載せました。

終わりに

以上がエスケープ文字を含むテキストをlinkifyする方法でした。リンク化する処理は細かいところで期待しない動作になって調整にかなり苦労しましたが、これでfixされたかなと思いました😊
リンク化処理の実装の参考になれれば幸いです。
検証コードは以下のStackBlitzに書きましたので、詳細のコードが気になる方はご参照ください。


弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://hrmos.co/pages/castingone/jobs/20240301_15
https://hrmos.co/pages/castingone/jobs/20240301_16
https://hrmos.co/pages/castingone/jobs/20240301_18

Discussion