← ブログ

console.logへのReactの描画

Text.jsは、フレームごとに1回のスタイル付きconsole.log呼び出しで、DevToolsコンソールにReactアプリを描画します。

Adam

Text.jsはChrome DevToolsのコンソールでReactアプリを描画します。非表示の入力欄を除き、ページにUIはありません。アプリの各フレームは、1回の console.log 呼び出しです。

ディスプレイとしてのコンソール

console.log は書式文字列を受け取ります。各 %c は、次の引数にあるCSSを、その直後から次の %c までのテキストに適用します。

console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");

これらのスタイルで文字色、背景、太字、斜体、下線を設定でき、コンソールのフォントは等幅です(macOSではMenlo)。等幅フォントと、文字のまとまりごとに変更できる色によって、コンソールはスタイル付きの文字セルを並べたグリッドとして機能します。その上に枠線、パネル、入力欄、チャートを描画できます。

フレームはセルのグリッド

レンダラーは各フレームを CellBuffer に保持します。文字は1つの型付き配列に、前景色と背景色は別の2つの配列に、太字や下線などの属性は4つ目の配列に格納されます。配列は指定されたサイズに対して一度だけ割り当てられ、各フレームで再利用されます。

フレームをログに出力するために、ドライバーは各行を走査し、同じスタイルを持つ隣接セルをひとまとまりにします。各まとまりは1つの %c スパンになり、行は \n で連結されます。100×32、3,200セルのテスト用ダッシュボードは、約200スパンにエンコードされます。

1フレームにつき1回の呼び出し

各行を個別のメッセージとしてログに出力する方法はうまくいきません。DevToolsでは各メッセージに枠線とソースへのリンクが付き、同一のメッセージが連続すると繰り返し回数のカウンターにまとめられるため、表に同じ行が2つあると1つに統合されてしまいます。フレームを単一のメッセージとして出力すれば、分割されずに表示されます。

まず、テキスト内の2つの要素をエスケープする必要があります。文字としての % は %% と書くため、100% のようなテキストがスタイル引数を消費することはありません。また、DevToolsはURLをリンクに変換して独自の色で再描画するため、:// と www. には幅を持たないワードジョイナー(U+2060)を挿入します。

export function escapeConsoleText(text: string, allowLink = false): string {  let s = text;  if (s.includes("%")) s = s.replaceAll("%", "%%");  if (!allowLink) {    if (s.includes(LINK_SCHEME)) s = s.replaceAll(LINK_SCHEME, LINK_SCHEME_SAFE);    if (s.includes(WWW) || s.includes("WWW.")) s = s.replace(/www\./gi, (m) => `${m.slice(0, 3)}\u2060.`);  }  return s;}

アイドル時のフレームはログ出力なし

バッファーの各行のハッシュを計算します。前のフレームからどのハッシュも変わっていなければ、ドライバーは文字列を構築する前に処理を戻し、コンソールには一切触れません。入力待ちのアプリは呼び出しを発生させません。

行に変更があった場合、ドライバーはコンソールをクリアし、新しいフレームをログに出力します。

const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);

ハッシュが変わった行だけを再エンコードします。それ以外の行は、前のフレームのスパンを再利用します。

入力

コンソールはキー入力を受け取らないため、ページ側で収集します。非表示の <textarea> がキー押下、貼り付け、IMEの変換入力を受け取り、アプリ向けのキーイベントに変換します。ページをクリックすると、この入力欄にフォーカスが移ります。

今後の予定

npmにある @textjs/core 0.0.xのプロトタイプは、すべてを1つのパッケージにまとめています。Text.js 0.1では、ここで説明したレンダラーを @textjs/core に、Reactリコンサイラーを @textjs/react に移し、App Routerを追加します。