💡

Editor.jsで特定の記号が特殊文字に変換される原因について調査

に公開

背景

Editor.jsを使った実装を行なっていると、<& といった文字列がエディタに入力されている際に、saveをして値を取得すると特殊文字に変換されていることに気がつきました

この原因を探るためにEditor.jsのコードを読んだ際の調査記録です

バージョン

@editorjs/editorjs: v2.31.1

発生した事象

以下のように、ボタンを押したら入力した値をコンソールに出力するコンポーネントを作成します

import { useEffect, useRef } from "react";
import type EditorJS from "@editorjs/editorjs";

export default function SampleEditor() {
  const refEditor = useRef<EditorJS | null>(null);
  useEffect(() => {
    import("@editorjs/editorjs").then((EditorJS) => {
      refEditor.current = new EditorJS.default({ holder: "editorjs" });
    });

    return () => {
      if (refEditor.current?.destroy) {
        refEditor.current.destroy();
      }
    };
  }, []);

  return (
    <div>
      <div id="editorjs"></div>
      <button
        type="button"
        onClick={() => {
          refEditor.current?.save().then((outputData) => {
            console.log(outputData);
          });
        }}
      >
        出力
      </button>
    </div>
  );
}

その後、以下のように <& といった文字列を含んだ入力を行いボタンをクリックします

すると、コンソール上には以下のような特殊文字を含んだ文字列が出力されます

{
    "time": 1768793204916,
    "blocks": [
        {
            "id": "TzpLrTCA2u",
            "type": "paragraph",
            "data": {
                "text": "&lt; や &amp; といった文字列を含んで入力をします"
            }
        }
    ],
    "version": "2.31.1"
}

エディタに文字を入力した際のDOMの変化

上記のような文字列を入力した際、DOM構造は以下のようになっていました

Editor.jsではdivタグに contenteditable=true を使うことでユーザーが編集できるようにしているようです

<div class="ce-block" data-id="TzpLrTCA2u">
  <div class="ce-block__content">
    <div class="ce-paragraph cdx-block" contenteditable="true" data-placeholder-active="" data-empty="false">
      < や & といった文字列を含んで入力をします
    </div>
  </div>
</div>

データを取得する際の内部処理

DOM上は <& は特殊文字になっていないのに、 save() で値を取得した際に特殊文字に変わっているため、Editor.jsの内部処理に原因がありそうです

内部実装を確認したところ、save() の実装箇所は saver.ts というファイルにありました

  public async save(): Promise<OutputData> {
    const { BlockManager, Tools } = this.Editor;
    const blocks = BlockManager.blocks,
        chainData = [];

    try {
      blocks.forEach((block: Block) => {
        chainData.push(this.getSavedData(block));
      });
      
    ...

処理を確認したところ、入力されたブロックごとにデータ取得を行なっているようです

前提として、Editor.jsは全ての入力されたデータをブロックとして扱います

デフォルトでは文字列を扱うParagraphというブロックのみですが、Toolsとして他のブロックを入れることにより、HeaderやImageといったデータを扱うことができます (参考: Getting started)

saver.tsに実装されている save() では、それらの入力されたブロックからデータの取得を行なっているようです

今回の問題は文字列を取得する際に起きているため、文字列を扱っているParagraphのブロックからデータを取得する際に原因がありそうです

そのため、続いてParagraphのデータ取得を見に行きます

Paragraphのデータは editor-js/paragraph というリポジトリの src/index.ts というファイルの save() メソッドで実装されていました

ここで、DOMからデータを取得するためにinnerHTMLが使用されているのがわかります

原因はこれのようです

  save(toolsContent: HTMLDivElement): ParagraphData {
    return {
      text: toolsContent.innerHTML,
    };
  }

innerHTMLの仕様

innerHTMLは、指定したHTML要素の子要素やテキストをHTMLコードとして取得することでがきます

その際、そのままHTMLファイルに貼り付けても元の構造を再現できる状態で取得します

そのため、HTMLのタグを構成する < や、特殊文字を構成するために使用する & は、 &lt;&amp; に変換されるようです

どのように対応するべきか

よくあるパターンとして、Editor.jsで入力した値をデータベースに入稿するというケースが考えられます

そのときに特殊文字で保存したくない場合は、特殊文字を通常の文字に変換すればよさそうです

innerHTMLを使用していることがわかったので、innerHTMLで特殊文字に変わったものを全て文字列に戻しましょう

export const unescape = (str: string) => {
  return str
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&nbsp;/g, " ")
}

Discussion