CodeMirror6では、style-modという独自のCSS-in-JSを用いてスクリプトファイルに直接スタイルを含めている。つまりスタイリングのためにCSSファイルを読み込む必要がない。

テーマについて

CodeMirror6では、エディタの機能が拡張(Extensions)という形で細分化されている。この背景には、必要に応じて拡張を組み合わて無駄を省き、サイズダウンを実現しよう、という意図がある(たぶん)。

スタイリングも拡張として提供されており、これらはテーマと呼ばれる。

テーマの作り方

テーマは、EditorView.theme()メソッドを用いて定義する。このメソッドの引数には、style-modの記法で書いたオブジェクトを指定する。

記法といっても、CSSをJavaScriptのオブジェクトに置き換えただけのシンプルなものである。

例えば以下のように書くと、エディタの文字を青くできる(必要なパッケージはインストール済みであるものとする)。

js
import { EditorView } from "@codemirror/view";

const myTheme = EditorView.theme(
  {
    "&": {
      color: "blue",
    },
  } /* {dark:true} ※後述 */
);

const view = new EditorView({
  doc: "Hello, CodeMirror!",
  extensions: [myTheme],
  parent: document.body,
});

エディタのスタイリングに使われているクラスは、デフォルトのスタイル公式のダークテーマなどを参照すれば把握できるかと思う。

なお、SASSと同じく、&は現在のセレクタを意味する。トップレベルで指定した場合は、そのドキュメントが選択される(CodeMirror内部で使う場合は、エディタのコンテナ要素が選択され

デフォルトテーマ

テーマの指定を省略すると、CodeMirrorのデフォルトのテーマが使われる。

CodeMirrorのEditorViewオブジェクトには、theme()メソッドとは別にbaseTheme()というメソッドが用意されている。使い方はtheme()メソッドと同じである。

baseTheme()は新しい要素をエディタに追加する場合に、その要素のデフォルトのスタイルを指定するためのものである。

このメソッドでエディタのデフォルトのスタイルを上書きすることもできる。ただ、作者のMarijn Haverbeke氏は、既存のスタイルの上書きにbaseTheme()を使うのは意図していないそうである(ソースはここ)。

テーマの開発者はbaseTheme()を使って、そのテーマを使う第三者はtheme()でカスタマイズせよ、みたいな感じで使い分けて欲しいのではないかと想像する。

ライトテーマとダークテーマ

CodeMirrorでは、ライトテーマとダークテーマの切り替えは、テーマを丸ごと切り替えることで行う。

微妙にややこしいのが、デフォルトのスタイルとの兼ね合いである。

baseTheme()でスタイルを記述する場合、例えば以下のように、ライトモードとダークモードのスタイルを記述できる。

js
{
	"&light .cm-content": { caretColor: "black" },
	"&dark .cm-content": { caretColor: "white" },
}

どちらが適用されるかは、適用されているテーマがライトモードかダークモードかによって異なる。

テーマがライトモードかダークモードかは、そのテーマを作成する際に、theme()メソッドの第2引数に指定した値によって決まる。

{dark:true}ならダークモード、{dark:false}ならライトモードである。

js
import { EditorView } from "@codemirror/view";

const myDarkTheme = EditorView.theme(
  {
    ".cm-content": {
      backgroundColor: "black",
      color: "white",
    },
  },
  { dark: true }
);

const view1 = new EditorView({
  doc: "Hello, LightTheme!",
  parent: document.body,
});

const view2 = new EditorView({
  doc: "Hello, DarkTheme!",
  extensions: [myDarkTheme],
  parent: document.body,
});

上記のサンプルコードを実行すると、背景色と文字色しか指定していないにも関わらず、ダークモードではキャレットの色が黒から白に変わっていることがわかる。

これは、baseTheme()メソッドを使うのと同じ形で、エディタ全体のライトモード・ダークモードのデフォルトのスタイルが定義されているためである。

どちらのモードのスタイルが適用されるかは、指定されたテーマに依存する。

構文ハイライトについて

テーマはエディタ全体のスタイリングを対象としており、構文ハイライトはまた別の方法でスタイリングを行う。

CodeMirror6の構文ハイライト(の前段となるトークナイズ)は、同じ作者のLezerというライブラリを使って行われる。そのためスタイリングも、このライブラリによるトークナイズに併せて行う。

Lezerでは、幅広い言語に対応できるトークン名のセットがあらかじめ用意されている。構文ハイライトのスタイルを指定する際は、このトークン名(Lezerではタグと呼ばれる)ごとにCSSプロパティを指定する。

以下は、マークダウン記法のハイライトをカスタムするサンプルである。

js
import { tags } from "@lezer/highlight";
import { EditorView } from "@codemirror/view";
import { markdown } from "@codemirror/lang-markdown";
import { syntaxHighlighting, HighlightStyle } from "@codemirror/language";

const myHighlightStyle = HighlightStyle.define([
  { tag: tags.heading1, color: "orange", fontWeight: "bold" },
]);

const view = new EditorView({
  doc: "# おみだし",
  extensions: [
    markdown(),
    syntaxHighlighting(myHighlightStyle, { fallback: false }),
  ],
  parent: document.body,
});

これを実行すると、見出しがオレンジ色で表示される。

まずHighlightStyle.define()メソッドに、Lezerのタグ名と対応づける形でCSSプロパティを指定する配列を渡す(なお、構文ハイライト用のデフォルトのスタイルは、@codemirror/languageモジュールにdefaultHighlightStyleという名称で定義されている。こだわらなければそれで十分かと思う)。

HighlightStyle.define()メソッドが返すハイライトスタイルを、syntaxHighlighting()関数の第1引数に渡すと、構文ハイライトを適用する拡張が返る。

ちなみに第2引数に{fallback: true}を渡すと、ほかにハイライトスタイルが指定されていない場合にそのスタイルが有効になる。

トークン名とLezer

上記の通り、任意のトークンにスタイルを割り当てるには、言語モジュール(上記の場合はlang-markdown)の中で使われるLezerのパーサーが、そのトークンにどのタグを割り振っているかを把握しなければならない。

CodeMirrorの言語モジュールではなく、その言語モジュールの中で使われているLezerのモジュールをさらに辿る必要がある点に留意したい。

「ͼ1」みたいなクラス名の正体

テーマを作成すると、内部でそのテーマ内のクラスをグループ化するための匿名クラスが生成される。

A theme is an extension created with EditorView.theme. It gets its own unique (generated) CSS class (which will be added to the editor when the theme extension is active) and defines styles scoped by that class.

テーマはEditorView.themeによって作られる拡張です。これは(テーマがアクティブになった時にエディタに追加される)ユニークなCSSクラスを持ちます。また、このクラスによって、スタイルの有効範囲を定義します。 引用元:CodeMirror System Guide

例えば先のサンプルコードでは、エディタのコンテナ要素にͼ4というクラスが付与され、styleタグには.ͼ4 {color: blue;}が出力されているはずである。この「ͼX」が匿名クラスである。

CodeMirrorが生成するエディタは、cm-接頭辞を関するクラスによってスタイリングされている。匿名クラスを自動生成してコンテナ要素に付与することで、これらのスタイルをテーマごとに効率的に分離・切り替えできる仕組みとなっている。

なので、開発者が直接匿名クラスを上書きするようなことは推奨されない。

参考資料