- Pretextは、DOM へアクセスせずに複数行テキストの高さと行配置を計算する純粋な JavaScript/TypeScript ライブラリで、ブラウザとサーバー環境の両方をサポート
- getBoundingClientRect のような DOM 計測 API を使わないため、レイアウトのリフローコストを排除し、フォントエンジンベースの独自計測ロジックで精度を確保
- prepare() / layout() API によりテキストを前処理し、キャッシュされた幅データを使って純粋な算術演算で高速な高さ計算を実行
- 絵文字、双方向テキスト(bidi)、さまざまな言語をサポートし、Canvas・SVG・WebGL・サーバーレンダリングでも同一の結果を提供
- 仮想化スクロール、テキストオーバーフロー検証、フローティングテキスト配置など、精密な UI レイアウト実装に活用できる高性能テキストエンジン
概要
- Pretextは、複数行テキストの計測とレイアウトのための純粋な JavaScript/TypeScript ライブラリで、DOM、Canvas、SVG、さらにサーバーサイドレンダリングまで対応
- DOM 計測 API(
getBoundingClientRect、offsetHeight など)を使わないため、レイアウトのリフローコストを排除
- ブラウザのフォントエンジンを基準にした独自計測ロジックにより、正確かつ高速な性能を提供
- **あらゆる言語、絵文字、双方向テキスト(bidi)**をサポートし、ブラウザごとの差異にも対応
インストールとデモ
主な機能
- Pretext は 2 つの主要な利用方法を提供
-
1. DOM アクセスなしで段落の高さを計測
prepare() はテキストを前処理し、空白の正規化・セグメント分離・glue ルール適用・canvas ベースの計測を行って、**不透明ハンドル(opaque handle)**を返す
layout() はキャッシュ済みの幅データを使い、純粋な算術演算で高さと行数を計算
- 同じテキストと設定では
prepare() を繰り返し呼び出す必要はなく、リサイズ時には layout() のみ再実行
{ whiteSpace: 'pre-wrap' } オプションで空白、タブ(\t)、改行(\n)をそのまま保持
- ベンチマーク結果:
prepare() は約 19ms(500 テキスト基準)、layout() は約 0.09ms
- 返された高さの値は、次のような UI 機能に活用可能
- 仮想化およびオクルージョン処理での正確な高さ計算
- JS ベースのレイアウトシステム(例: masonry、flexbox 類似構造)
- AI ベースのテキストオーバーフロー検証
- テキスト読み込み時のスクロール位置維持
-
2. 手動で段落レイアウトを構成
prepareWithSegments() でセグメント単位のデータを生成
layoutWithLines() は固定幅で各行のテキストと幅情報を返す
walkLineRanges() はテキスト文字列を作らずに、各行の幅とカーソル範囲を走査
- 例: 複数の幅を試して適切な行数と高さを見つける二分探索型レイアウト調整が可能
layoutNextLine() は行ごとに幅が異なる場合に、1 行ずつ順次レイアウト
- 例: 画像の周囲にテキストを回り込ませるフローティングテキスト配置
- この方式はCanvas、SVG、WebGL、サーバーサイドレンダリングにも同様に適用可能
API 要約
-
基本計測用 API
prepare(text, font, options?): テキストを解析・計測し、layout() に渡すハンドルを返す
layout(prepared, maxWidth, lineHeight): 与えられた幅と行高に応じたテキストの高さと行数を計算
-
手動レイアウト用 API
prepareWithSegments(text, font, options?): セグメント単位のデータを返す
layoutWithLines(prepared, maxWidth, lineHeight): 各行のテキスト、幅、カーソル情報を含む
walkLineRanges(prepared, maxWidth, onLine): 各行の幅とカーソル範囲をコールバックで渡す
layoutNextLine(prepared, start, maxWidth): 行単位イテレーター形式でレイアウトを実行
LayoutLine, LayoutLineRange, LayoutCursor の型定義を含む
-
その他のユーティリティ
clearCache(): 内部キャッシュを初期化
setLocale(locale?): ロケールを設定してキャッシュを初期化(既存状態には影響なし)
制約と注意事項
- Pretext は完全なフォントレンダリングエンジンではない
- 基本対象の CSS プロパティ
white-space: normal
word-break: normal
overflow-wrap: break-word
line-break: auto
{ whiteSpace: 'pre-wrap' } 使用時は空白、タブ、改行を保持し、tab-size: 8 を適用
- macOS で
system-ui フォントは layout() の精度に不向きなため、明示的なフォント名の使用を推奨
overflow-wrap: break-word により、非常に狭い幅では単語内部でも改行される可能性があるが、**文字単位(grapheme)**でのみ分割
開発関連
- 開発環境およびコマンドは
DEVELOPMENT.md を参照
貢献と背景
- Sebastian Markbage の text-layout プロジェクトのアイデアを継承
- canvas
measureText ベースのシェーピング、pdf.js の bidi 処理、ストリーミング改行処理の設計を受け継いで発展させた構造
1件のコメント
Hacker News の意見
このプロジェクトは本当に 印象的 Web ページで実際にテキストをレンダリングしなくても、改行されたテキストの高さを効率的に計算する問題を解決している 単語単位に分割されたセグメントの 幅と高さをキャッシュ し、ブラウザの改行アルゴリズムを自前で実装している ハイフン、絵文字、中国語など多様な文字処理や、ブラウザごとのレンダリング差異(Safari を含む)のため非常に難しい作業だ 実際のブラウザとの比較テストには corpora データセット と accuracy テストページ を使っている
これは本当に 長い間待ち望まれていた機能 以前からレスポンシブなアコーディオンのようなものをきちんと実装するのは難しかった Web の発展パターンはいつも ①複雑な要求の登場 → ②JS/CSS ハック → ③標準化 だった 今回はハックではなく、きちんとした第 2 段階だと思う RESEARCH.md を見ると、ブラウザごとの絵文字測定の違いまで細かく研究している 保守は大変だろうが、Web の発展における大きな 転換点 になりそう
ライブラリ作者によれば、Claude Code と Codex にブラウザの ground truth データを学習させ、数週間にわたって反復測定したとのこと 関連ツイート 参照 Autoresearch も一部使われたようだ
shape ベースのリフローの例が特に気に入った Ensō(enso.sonnet.io) に適用してみたかったが、シンプルさを保つために踏みとどまった アコーディオンの例 は CSS の
interpolate-sizeでも実装できる Josh Comeau の記事 参照 テキストバブルの例 はtext-wrap: balance | prettyで似たように実装できるbalanceやprettyでは完全な解決にはならない 行長を均等にしたくない場合も多い 関連する CSSWG Issue: #191text-wrapは 1 行あたりの単語数をそろえる助けにはなるが、右側の余白の問題は依然として残るpretext は canvas.measureText を直接使うのではなく、テキストと属性を JS API に渡すと自動でレイアウトを計算してくれる 以前は measureText を直接使うか、harfbuzz をブラウザへ持ち込む必要があった 技術的ブレークスルーというより、既存要素をうまく組み合わせた結果のように見える ただ Skia-wasm / Canvaskit との違いは気になる
去年、HTML で印刷用パンフレットの組版システムを作ったが、改行とウィドウ・オーファン防止のために Selection API でボックス境界を繰り返し計算していた 今でもうまく動いているが、理由の分からない off-by-one ハックがある pretext の 反復的な行生成機能 は本当にうれしい機能だ
Fedora + Firefox 環境ではデモがすべて壊れて見える 例: スクリーンショット
こうした機能は本来 ブラウザ標準 API として提供 されるべきだ W3C に機能要望を出すにはどうすればいいのか、コミュニティ投票のようなものが可能なのか気になる
Sciter エンジンにはすでに Graphics.Text 機能がある CSS スタイルをそのまま適用できる canvas ベースのテキストレンダリング要素 だ
ブラウザの テキスト検索(Ctrl+F) は仮想スクロールリストでは 正常に 動作しない この問題を解決するには、JS ではなく新しい "Search" API が必要かもしれない