🌈

Lexicalでシンタックスハイライトする

2022/06/19に公開

LexicalはDraft.jsの後継となるテキストエディタフレームワークです。
元々Metaで開発されていたものがOSSとして公開されました。

https://github.com/facebook/lexical

Lexicalは主にリッチテキストを実装するためのフレームワークですが、
高度に抽象化されているためソースコードエディタを実装するのにも利用できます。
ただコードに関するモジュールである@lexical/codeはドキュメントが虚無なため、
本記事で使い方の紹介ができればと思います。

https://lexical.dev/docs/api/lexical-code

@lexical/code

Lexicalで既存言語のソースコードをシンタックスハイライトするために必要なものは
以下の3つで、全て@lexical/codeからエクスポートされています。

  • CodeNode
  • CodeHighlightNode
  • registerCodeHighlighting

CodeNodeはコードブロックを表すElementNodeの子クラス、
CodeHighlightNodeはハイライト用のTextNodeの子クラス、
registerCodeHighlightingは上記のNode達やTextNodeの変換関数を登録するための関数です。

ここまでの説明でNodeやNodeの変換にピンと来ていない方にはこちらの記事が
とても参考になります。
https://zenn.dev/stin/articles/getting-started-with-lexical

Lexical自体は特にReact専用ではないですが、本記事のコードはReactで書いていきます。

Step.1 変換処理の登録

まずはregisterCodeHighlightingでの登録用コンポーネントです。
このコンポーネントはLexical中のサンプルにありますが、エクスポートされていないため、
サンプルを利用しない場合はコピペする必要があります。
※著作権はMetaに帰属するはずなので注意しましょう。

https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/CodeHighlightPlugin.ts

Step.2 NodeとPluginの利用

上記のCodeHighlightPluginをLexicalComposer内で利用するのと、
CodeNode, CodeHighlightNodeを利用するためにLexicalComposerの設定として渡します。
他は適当です。

CodeEditor.tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import CodeHighlightPlugin from "./CodeHighlightPlugin";

const CodeEditor = () => {
  return (
    <LexicalComposer
      initialConfig={{
        namespace: "Editor",
        nodes: [CodeNode, CodeHighlightNode],
        onError: console.error,
      }}
    >
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={null}
      />
      <CodeHighlightPlugin />
    </LexicalComposer>
  );
};

export default CodeEditor;

ここまででCodeEditorをエラーなく利用できるようになっていますが、適当なコードを入力しても
ハイライトされません。
理由の1つは初期値を与えていないため、ParagraphNodeが挿入され、プレーンテキストとして
入力が処理されるからです。
もう1つはCodeHighlightNodeに対してのテーマ設定をしていないからです。

Step.3 初期値の登録

ParagraphNodeの代わりにCodeNodeを挿入すれば良いです。

editorState.ts
import { $getRoot } from "lexical";
import { $createCodeNode } from "@lexical/code";

export const $getInitialState = () => {
  const code = $createCodeNode();
  $getRoot().append(code).selectEnd();
};

なお$createCodeNodeの引数では言語を設定できます(デフォルトはJavaScriptです)。

CodeEditor.tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import CodeHighlightPlugin from "./CodeHighlightPlugin";
+import { $getInitialState } from "./editorState";

const CodeEditor = () => {
  return (
    <LexicalComposer
      initialConfig={{
        namespace: "Editor",
        nodes: [CodeNode, CodeHighlightNode],
        onError: console.error,
      }}
    >
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={null}
+	initialEditorState={$getInitialState}
      />
      <CodeHighlightPlugin />
    </LexicalComposer>
  );
};

export default CodeEditor;

Step.4 テーマ設定

CodeNode, CodeHightlightNodeへのクラス名の設定とCSSを書きます。

CodeEditor.tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import CodeHighlightPlugin from "./CodeHighlightPlugin";
import { $getInitialState } from "./editorState";

const CodeEditor = () => {
  return (
    <LexicalComposer
      initialConfig={{
        namespace: "Editor",
        nodes: [CodeNode, CodeHighlightNode],
        onError: console.error,
+       theme: {
+         code: "editor-code",
+         codeHighlight: {
+           function: "editor-tokenFunction",
+           keyword: "editor-tokenAttr",
+           number: "editor-tokenProperty",
+           string: "editor-tokenSelector",
+         },
+       },
      }}
    >
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={null}
	initialEditorState={$getInitialState}
      />
      <CodeHighlightPlugin />
    </LexicalComposer>
  );
};

export default CodeEditor;

色は適当です。

codeTheme.css
.editor-code {
  font-family: Consolas, Menlo, monospace;
}

.editor-tokenAttr {
  color: red;
}

.editor-tokenFunction {
  color: violet;
}

.editor-tokenProperty {
  color: blue;
}

.editor-tokenSelector {
  color: green;
}

Themeに設定できるものは実際はもっと多いです。
https://lexical.dev/docs/getting-started/theming

これでシンタックスハイライトが実現できました。

最後に

執筆にあたってLexicalをいろいろ触ってみましたが、汎用的に作られているので
いろいろ使い道がありそうです。
今回は既存言語のハイライトを取り上げましたがNodeを自作すれば未対応の言語も
ハイライトすることができます。

ただLexicalは成熟しておらず、その旨がREADMEにも一番初めに書かれています。
今回利用したregisterCodeHighlightingも機能の分割が進められているようなので
本記事のコードはそのうち動かなくなるでしょう。

https://github.com/facebook/lexical/issues/2380

今後に期待!!

Discussion