Bibliothek / SDK
chenglou/pretext avatar
chenglou/pretext

Pretext: mehrzeiligen Text messen und setzen, ohne das DOM zu berühren

Pretext misst und gestaltet mehrsprachigen Text in Webanwendungen und verwendet Canvas-, DOM- und OpenType-Daten, um vorhersehbare Zeilenumbrüche und Abmessungen zu erzeugen.

50.362 Sterne2.737 ForksTypeScriptMIT

Auf einen Blick

Was ist das?
Die TypeScript-Bibliothek chenglou/pretext misst Absätze über die Schrift-Engine des Browsers und berechnet Umbrüche per Arithmetik. Wofür prepare und layout gut sind, wo die Grenzen liegen und wann man prepareWithSegments braucht.
Für wen ist es gedacht?
Pretext passt für UI-Code, der Absatzhöhen vorab wissen muss, etwa Virtualisierung, Masonry-Layouts oder das Wiederverankern einer Scrollposition, und für Canvas- oder SVG-Renderer, die ihre Zeilen selbst setzen wollen. Es passt nicht in Projekte, die automatische Silbentrennung, komplexe CSS-Inline-Formatierung oder Laufzeiten ohne Intl.Segmenter brauchen.
Darf ich es kommerziell nutzen?
Ja. MIT ist eine freizügige Lizenz: Sie dürfen darauf aufbauende Software nutzen, verändern und verkaufen, solange Sie die Urheberrechts- und Lizenzhinweise beibehalten.
Wird es noch gepflegt?
Ja. Das Repository hat innerhalb des letzten Tages neue Commits erhalten.
In welcher Sprache ist es geschrieben?
Hauptsächlich TypeScript, laut der Sprachstatistik von GitHub.

Die Antworten beruhen auf den GitHub-Daten des Projekts (zuletzt abgeglichen am 15. September 2026) und auf unserer Analyse. Sie sind keine Rechtsberatung.

TIEFGEHENDE OPEN-SOURCE-ANALYSE

Warum @chenglou/pretext auf getBoundingClientRect verzichtet

Pretext ist eine reine JavaScript- und TypeScript-Bibliothek für mehrzeilige Textmessung und Layout. Ihre Ausgangsfrage klingt unspektacular, ist aber im Browser eine harte Nuss: Wie hoch wird dieser Absatz, bevor man ihn einfügt? Der übliche Weg über getBoundingClientRect oder offsetHeight erzwingt einen Layout-Reflow, der im README als eine der teuersten Operationen des Browsers beschrieben wird.

Die Bibliothek schlägt stattdessen einen eigenen Weg ein. Sie implementiert ihre Messlogik selbst und benutzt die Schrift-Engine des Browsers als Maßstab, also genau dieselbe Instanz, die später auch den Text rendert. Als Ausgabeformate nennt das README DOM, Canvas und SVG, eine serverseitige Variante ist angekündigt. Mit rund 50000 Sternen auf GitHub ist das Projekt inzwischen breit beobachtet, lizenziert unter MIT, und die Credits führen den ursprünglichen Entwurf auf Sebastian Markbages frühere Arbeit an text-layout zurück.

prepare() einmal, layout() oft: die Vorabrechnung richtig nutzen

Der erste Anwendungsfall misst die Höhe eines Absatzes, ohne das DOM anzufassen. prepare() erledigt die Einmalkosten: Whitespace normalisieren, Text segmentieren, Glue-Regeln anwenden, die Segmente über Canvas messen und ein undurchsichtiges Handle zurückgeben. Danach ist layout() der billige heiße Pfad, reine Arithmetik über gecachte Breiten, mit maxWidth und lineHeight als Eingabe und height plus lineCount als Ergebnis.

Diese Aufteilung hat eine Regel: prepare() für denselben Text mit derselben Konfiguration nicht erneut aufrufen, denn das vernichtet die Vorabrechnung. Beim Resize gehört nur layout() in den Handler. Optionen von prepare() sind whiteSpace: 'pre-wrap' für sichtbare Leerzeichen, Tabulatoren und harte Umbrüche im Stil einer Textarea, wordBreak: 'keep-all' nach Art von CSS word-break, und letterSpacing als Pixelwert, der zur CSS-Passung synchron bleiben muss. Ein Randfall ist dokumentiert: Für einen leeren String liefert layout() lineCount 0 und height 0, während ein Browser einen leeren Block auf eine Zeile dimensioniert; wer das angleichen will, klemmt mit Math.max(1, lineCount) * lineHeight.

Mit prepareWithSegments() und layoutNextLineRange() um ein Bild fließen

Der zweite Anwendungsfall übergibt das Zeilensetzen an den Aufrufer. prepareWithSegments() liefert eine reichere Struktur als prepare(), und layoutWithLines() gibt alle Zeilen bei fester Breite zurück, die sich einzeln auf ein Canvas zeichnen lassen. Für dynamischere Fälle übernimmt layoutNextLineRange() die Steuerung: Er liefert Zeile für Zeile bei jeweils wechselnder Breite und gibt einen Cursor zurück, den materializeLineRange() bei Bedarf wieder in den Zeilentext auflöst.

Das README zeigt daraus ein konkretes Muster, Textfluss um ein schwebendes Bild: Solange die Zeile neben dem Bild liegt, wird sie mit columnWidth minus Bildbreite gesetzt, danach mit voller Spaltenbreite. Solche Layouts sind mit CSS-Floats nicht in dieser Präzision steuerbar, und genau hier liegt der Anwendungsfall für Canvas-, SVG- oder künftige serverseitige Renderer. Ein reicheres Beispiel dazu liegt im Repository unter /demos/dynamic-layout.

Shrink-Wrap mit measureLineStats() und walkLineRanges() nachbauen

Eine Eigenschaft, die es im Web laut README bislang nicht gab, ist das mehrzeilige Schrumpfverpacken: die engste Containerbreite finden, in die ein Absatz noch passt. measureLineStats() liefert dafür lineCount und maxLineWidth in einem Aufruf. Wer mehr Kontrolle will, nimmt walkLineRanges() und läuft über die Zeilenbereiche, ohne irgendwo Textstrings zu erzeugen, und merkt sich einfach die größte Zeilenbreite.

Beide Funktionen arbeiten auf derselben vorbereiteten Struktur und kosten damit kaum mehr als der eine layout()-Aufruf daneben. Praktisch wird das überall dort, wo Container mitwachsen sollen, etwa bei Karten, Tooltips oder Chatblasen, deren Breite sich am Inhalt orientieren soll. Statt Schätzwerte und Caches zu pflegen, fragt man die echte Zahl ab. Das README nennt als weitere Einsatzorte die Entwicklungszeit-Prüfung, dass Button-Labels nicht überlaufen, und das Halten der Scrollposition, wenn neuer Text nachlädt.

rich-inline für Chips und Mentions: extraWidth und break: never

Für Rich-Text im Inline-Fluss gibt es einen Zusatzpfad unter @chenglou/pretext/rich-inline mit den Einstiegen prepareRichInline(), walkRichInlineLineRanges() und materializeRichInlineLineRange(). Man übergibt eine Liste von Fragmenten, jedes mit eigenem Text, eigener Font und eigenen Optionen. Ein Fragment kann mit extraWidth Platz für die Pill-Dekoration beanspruchen, also den Rahmen um einen Chip, und mit break: 'never' als atomares Element gelten, das nie umbrochen wird, etwa eine Erwähnung wie @maya.

Der Helfer ist bewusst schmal geschnitten: roher Inline-Text mit Rand-Leerzeichen, nur white-space: normal, keine verschachtelte Markup-Baumstruktur und ausdrücklich keine allgemeine CSS-Inline-Formatierungs-Engine. Für Code-Spans, Mentions und Chips im Fließtext reicht das, für ein Dokumentformat mit Überschriften und Listen nicht. Wer Silbentrennung braucht, fügt weiche Trennzeichen vor prepare() oder prepareWithSegments() selbst ein; gewählte Umbrüche materialisieren als nachgestellter Bindestrich, nicht gewählte bleiben unsichtbar. Eine automatische Silbentrennung ist Stand heute nicht eingebaut.

Grenzen im Detail: Intl.Segmenter-Pflicht, system-ui unter macOS und die Tab-Größe 8

Das README listet abgedeckte Textfunktionen, und die Liste ist kurz genug, um sie zu prüfen: white-space normal und pre-wrap, word-break normal und keep-all, overflow-wrap break-word, line-break auto, letter-spacing als numerischer Pixelwert und Tabulatoren mit der Standard-Tab-Größe 8 des Browsers. Alles darüber hinaus gehört nicht zum Umfang.

Härtere Voraussetzungen betreffen die Laufzeit: Pretext braucht Intl.Segmenter und die Canvas-2D-Textmessung, Umgebungen ohne Intl.Segmenter werden nicht unterstützt. Eine explizite Warnung gilt für die Font-Angabe system-ui unter macOS, die als unsicher für die Genauigkeit von layout() markiert ist, mit Verweis auf ein Plattform-Fehlerprotokoll. Wie es weitergeht, zeigt die Umgebung des Projekts: Entwicklungshinweise stehen in DEVELOPMENT.md, die Demos starten nach bun install mit bun start unter /demos/index, unter Windows mit bun run start:windows, und live liegen sie unter chenglou.me/pretext sowie in einer zweiten Sammlung unter somnai-dreams.github.io/pretext-demos. Auf die installierbare Variante kommt man mit npm install @chenglou/pretext.

Redaktionelles Fazit

Pretext passt für UI-Code, der Absatzhöhen vorab wissen muss, etwa Virtualisierung, Masonry-Layouts oder das Wiederverankern einer Scrollposition, und für Canvas- oder SVG-Renderer, die ihre Zeilen selbst setzen wollen. Es passt nicht in Projekte, die automatische Silbentrennung, komplexe CSS-Inline-Formatierung oder Laufzeiten ohne Intl.Segmenter brauchen. Vor dem Einbau sollte man prüfen, ob die eigene Font-Zeichenkette exakt zur CSS-Passung stimmt, ob das Zielpublikum system-ui unter macOS nutzt, wo das README die Genauigkeit von layout() als unsicher markiert, und ob die abgedeckten Textfunktionen zum eigenen Inhalt reichen.

Offizielle Quellen

  1. Official documentation
  2. Official README
  3. Project repository
Community-Notizen

Community-Notizen