React in console.log rendern
Text.js zeichnet eine React-App in der DevTools-Konsole als einen formatierten console.log-Aufruf pro Frame.
Text.js rendert React-Apps in der Konsole der Chrome DevTools. Abgesehen von einem versteckten Eingabefeld hat die Seite keine UI. Jeder Frame der App ist ein einzelner console.log-Aufruf.
Die Konsole als Anzeige
console.log akzeptiert einen Formatstring. Jedes %c wendet das CSS im nächsten Argument auf den folgenden Text an, bis zum nächsten %c:
console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");Diese Stile können Farben, Hintergründe, Fettdruck, Kursivschrift und Unterstreichung festlegen, und die Konsolenschrift ist nichtproportional (Menlo unter macOS). Mit einer nichtproportionalen Schrift und Farben, die sich für jede Zeichenfolge ändern können, funktioniert die Konsole als Raster aus formatierten Zeichenzellen. Darauf lassen sich Rahmen, Panels, Eingabefelder und Diagramme zeichnen.
Ein Frame als Raster aus Zellen
Der Renderer hält jeden Frame in einem CellBuffer. Zeichen liegen in einem typisierten Array, Vordergrund- und Hintergrundfarben in zwei weiteren und Attribute wie Fettdruck und Unterstreichung in einem vierten. Die Arrays werden für eine bestimmte Größe einmal angelegt und für jeden Frame wiederverwendet.
Um einen Frame zu protokollieren, durchläuft der Treiber jede Zeile und gruppiert benachbarte Zellen mit gleichem Stil zu Folgen. Jede Folge wird zu einem %c-Span, und die Zeilen werden mit \n verbunden. Unser Test-Dashboard mit 100×32 Zellen, insgesamt 3200 Zellen, wird in etwa 200 Spans kodiert.
Ein Aufruf pro Frame
Jede Zeile als eigene Nachricht zu protokollieren, funktioniert nicht. Jede Nachricht erhält in DevTools einen eigenen Rahmen und Quellverweis, und DevTools fasst identische aufeinanderfolgende Nachrichten hinter einem Wiederholungszähler zusammen, sodass zwei identische Tabellenzeilen zu einer verschmelzen würden. Ein Frame, der als einzelne Nachricht protokolliert wird, bleibt zusammen.
Zwei Dinge im Text müssen zuerst maskiert werden. Ein wörtliches % wird als %% geschrieben, damit Text wie 100% kein Stilargument verbrauchen kann. DevTools wandelt außerdem URLs in Links um und zeichnet sie in eigenen Farben neu, deshalb erhalten :// und www. einen Word Joiner (U+2060), der keine Breite hat:
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;}Keine Protokollausgabe für unveränderte Frames
Für jede Zeile des Puffers wird ein Hash berechnet. Wenn sich seit dem letzten Frame kein Hash geändert hat, kehrt der Treiber zurück, bevor er einen String erzeugt, und greift nicht auf die Konsole zu. Eine App, die auf Eingaben wartet, macht keine Aufrufe.
Wenn sich eine Zeile geändert hat, leert der Treiber die Konsole und protokolliert den neuen Frame:
const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);Nur Zeilen, deren Hash sich geändert hat, werden neu kodiert. Die anderen verwenden die Spans des vorherigen Frames wieder.
Eingabe
Die Konsole empfängt keine Tastenanschläge, deshalb erfasst die Seite sie. Ein verstecktes <textarea> nimmt Tastendrücke, eingefügten Text und IME-Komposition entgegen und wandelt sie in Tastenereignisse für die App um. Ein Klick auf die Seite gibt ihm den Fokus.
Ausblick
Die Prototypen von @textjs/core 0.0.x auf npm bündeln alles in einem Paket. Text.js 0.1 verschiebt den hier beschriebenen Renderer nach @textjs/core, den React-Reconciler nach @textjs/react und ergänzt einen App Router.