世界を動かす技術を、日本語で。

Pretext: マルチラインテキストの測定とレイアウトのためのTypeScriptライブラリ

概要

Pretext は、マルチラインテキストの測定とレイアウトを行う 純粋なJavaScript/TypeScriptライブラリ高速かつ正確 で、多言語対応および絵文字・双方向テキストもサポート。 DOMへのアクセス不要 で、レイアウト再計算によるパフォーマンス低下を回避。 Canvas, SVG, WebGL, サーバーサイド描画 にも対応予定。 npm install @chenglou/pretext で簡単インストール。

Pretextの特徴

  • DOM測定不要

    • getBoundingClientRectやoffsetHeightなどの 高コストなDOM再計算を回避
    • 独自のテキスト測定ロジックを実装し、 ブラウザのフォントエンジンを基準 に利用
    • AIフレンドリーなイテレーション方式 採用
  • 多様な描画先対応

    • DOM, Canvas, SVG, WebGL, サーバーサイド 描画サポート
    • 多言語・絵文字・混在双方向テキスト も正確に測定
  • 高速なパフォーマンス

    • 500テキストのバッチ処理で prepare()は約19ms、layout()は約0.09ms
    • キャッシュ利用による効率化
  • 柔軟なレイアウト制御

    • Masonryや仮想スクロールなど 高度なUIレイアウト にも最適
    • ユーザー独自のレイアウト実装 も容易
  • 開発・検証用途

    • AI生成UIの検証ラベルのオーバーフロー防止 に活用
    • レイアウトシフトの防止スクロール位置の再アンカー にも有効

インストール・デモ

主要APIと使い方

  • 段落の高さ測定(DOM非依存)

    • import { prepare, layout } from '@chenglou/pretext'
      const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
      const { height, lineCount } = layout(prepared, textWidth, 20)
      
    • prepare(): テキストの正規化・分割・測定を一度だけ実行、結果をハンドルとして返却
    • layout(): キャッシュ済み幅を使った純粋な算術処理で高速計算
  • textarea風の表示

    • const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
      const { height } = layout(prepared, textareaWidth, 20)
      
    • whiteSpace: 'pre-wrap' でスペース・タブ・改行をそのまま表示
  • 段落の手動レイアウト

    • import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
      const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
      const { lines } = layoutWithLines(prepared, 320, 26)
      for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i].text, 0, i * 26)
      
    • walkLineRanges で各行の幅やカーソル情報のみ取得可能
    • layoutNextLine で幅が変化する場合に一行ずつレイアウト可能

APIリファレンス

  • prepare(text, font, options?)

    • テキスト解析・測定、layout()用ハンドルを返却
    • fontは16px InterなどCanvas互換指定
  • layout(prepared, maxWidth, lineHeight)

    • 指定幅・行高に対する 高さ・行数 を返却
  • prepareWithSegments(text, font, options?)

    • 手動レイアウト用 の詳細なセグメント情報を返却
  • layoutWithLines(prepared, maxWidth, lineHeight)

    • 各行情報(text, width, start/end cursor) を含む結果を返却
  • walkLineRanges(prepared, maxWidth, onLine)

    • 各行の幅・カーソル範囲のみコールバックで取得
  • layoutNextLine(prepared, start, maxWidth)

    • カーソルから次の行を一行ずつ取得、段組や画像回り込みに最適
  • clearCache()

    • 内部キャッシュの消去
  • setLocale(locale?)

    • ロケール指定&キャッシュクリア

注意点・制限

  • 完全なフォントレンダリングエンジンではない
    • 現状はwhite-space: normal, word-break: normal, overflow-wrap: break-word, line-break: autoが対象
    • whiteSpace: 'pre-wrap'指定時はスペース・タブ・改行を保持
  • system-uiはmacOSで精度保証外
    • 必ず明示的なフォント名を指定

開発・クレジット

  • 開発手順DEVELOPMENT.md参照
  • 着想元: Sebastian Markbage氏のtext-layout
    • Canvas measureText + pdf.jsのbidi + ストリーミング改行 方式を踏襲

参考リンク・デモ

Hackerたちの意見

誰か、ブラウザのネイティブテキスト検索を壊さずに長いリストや無限リスト、グリッドの仮想化の良い解決策を見つけた人いる?これには新しいウェブの「検索」APIが必要かもしれないね。ブラウザの助けなしでは無理なんじゃないかな。

これ、めっちゃいいね!特に形に基づくリフローの例が気に入った。ずっと考えてたことだし、Ensō(enso.sonnet.io)に加えたいな。テキストの行間でより良いキャレットの遷移を適用できるから。ただ、シンプルさを保ちたいからやらないけど、すごく誘惑される。で、CSSの話に移るけど、サイトのアコーディオンの例(https://chenglou.me/pretext/accordion)は、interpolate-sizeプロパティを使えば純粋なCSSで解決できるよ(その後にJSのフォールバックもありかも)。https://www.joshwcomeau.com/snippets/html/interpolate-size/ テキストバブルの問題(https://chenglou.me/pretext/bubbles)については、text-wrap: balance | prettyを使えば同じ結果が得られるよ。(balanceは確か行数を均等にするやつ)

Pretextの簡単な概要だけど、ウェブでテキストをレイアウトしたいなら、canvas.measureText APIを使って行の折り返しやセグメンテーション、RTLを自分で実装しなきゃいけない。Pretextはこれを簡単にしてくれる。テキストとテキストのプロパティ(フォント、色、サイズなど)を純粋なJS APIに渡すだけで、指定したビューポートのサイズにレイアウトしてくれる。前はmeasureTextを使うか、harbuzzをブラウザに何とかして送らなきゃいけなかった。Pretextは技術的なブレークスルーではなく、純粋なJS APIとしてレイアウトを実現するために必要なものを組み合わせただけだと思う。ただ一つ質問があるんだけど、これはSkia-wasm / Canvaskitとどう違うの?

著者が正しいなら、これはGUIウェブフレームワークや未来のリッチテキストエディタにとって大きな影響があるね。

これはSkia-wasmとどう違うの? wasmじゃないの?

Skiaが世界を持ってくる。君の言ってることは間違ってないし、君が聞いた質問の微妙なところも理解してるよ(つまり、Flutterの話をしてる;「Skia-wasmはwasmだ」というコメントは、こっちにはちょっとしたこだわりに聞こえる)。Flutterを使うときは、デバイスに依存しない世界を描画するAPI、つまりSkia / Impellerが必要だってことだ。ここで、誰かがAIを使ってグリフレンダリングの純粋なTypescriptバージョンをコーディングする時間をかけたんだ。私たちにとって、その違いはDartにffmpegがあるのと、抽象的にffmpegのCライブラリがあるのとの違いに似てる。WASM/FFIバージョンをDart APIで持つことは技術的には何年も可能だったけど、自分一人でやるには大変だから実現してないんだ。「本当の会社」はサーバーを使うだろうし、一度お金を取ると、バックアップやダウンロードリンク、トランスコードを完了するのにコンピュータが数分間起きている必要がないことを期待されるからね。比喩をきれいにまとめると、今、君や私がこの作業をAIを使って2週間で仕上げてテストして、GitHubに載せるってことだ。言語の選択やツール自体が魅力的なわけじゃなくて、クライアントサイドのメディアFOSSで新しい地平を切り開くことができるっていうのが本当にすごいことなんだ。

この技術、すごく印象的だね。解決する問題は、ウェブページ上のラップされたテキストの高さを効率的に計算することなんだけど、実際にそのテキストをページにレンダリングする前に計算しちゃう(これってすごくコストがかかる)。個々のセグメント、つまり単語の幅と高さを事前に計算してキャッシュすることで実現してるんだ。それから、ブラウザがテキスト文字列を行でラップする方法をカスタムコードで実装してる。これは、ハイフネーションや絵文字、中国語など、考慮しなきゃいけないラッピングや文字の種類が多いから、めちゃくちゃ難しい。さらに、異なるブラウザ(特にSafari)ではレンダリングアルゴリズムに微妙な違いがあるからね。実際のブラウザで様々な長文ドキュメントを使って結果のライブラリをテストしてるよ。https://github.com/chenglou/pretext/tree/main/corpora と https://github.com/chenglou/pretext/blob/main/pages/accuracy...

Remotionの動画用にダイナミックな字幕を作るとき、テキストや行数を測るのにめちゃくちゃ苦労したんだ。自分の無能さなのか、DOM自体の複雑さなのかは分からないけど、これがあればもっと簡単になるといいな :-)

この技術、すごく印象的だね。 ほんとそうだよね!テキストレイアウトエンジンはめちゃくちゃ難しい。最初は「難しいけど、できる!」って思って始めるのに、3ヶ月後には「なんで中国語は列でレンダリングするときに句読点を違う風に回転させるの?!」って叫んでる自分がいる。これってDOMにフィードバックを与えるから、マルチラインテキストをキャンバスにレンダリングするだけの自分の努力よりもずっと役立つよ。例えば、https://scrawl-v8.rikweb.org.uk/demo/canvas-206.html

解決する問題は、ウェブページ上のラップされたテキストの高さを効率的に計算することで、実際にそのテキストをページにレンダリングすることなく(すごくコストがかかる)。でも結局、ブラウザでは実際のテキストレンダリングはブラウザがやるんだよね?これは、ブラウザが実際のテキストをレンダリングする前に「何かをする」ことを可能にするライブラリだけど、最終的にはブラウザが実際のテキストをレンダリングするの?それとも、このものが実際のテキストの最終レンダリングもやってるの?

似たようなことを約1年前に書いたけど、もっとシンプルで2KBのサイズで、AIなしで作ったよ。uWrap.js: https://news.ycombinator.com/item?id=43583478。11kスターには一晩では届かなかったけどね :D ASCIIテキストの場合、俺のは80msで終わるけど、pretextは2200msかかる。pretextの精度(ブラウザとどれだけ一致するか)はまだ確認してないけど、今夜テストする予定。うまくいくと思うよ。pretextが80ms(それ以下でも)にどれだけ近づけるか見てみよう。同じテクニックを使わずにね。 https://github.com/chenglou/pretext/issues/18 現在、すでにいくつかのパフォーマンス改善のPRがオープンしていて、その中にはAutoresearchを使ったものもある。

他のところでも言ったけど、ここでも繰り返すね:これは本当にすごい!こういうのがずっと欠けてたんだよね!最初にちゃんとしたレスポンシブアコーディオンを作れなかった時のこと、2011年にリリースされたBootstrap 1の時だったのを覚えてる!今でもちゃんと解決されてないし(今まで?)。多くのことはCSSでやるべきなのに、JSでハック的に実装されてるのが現状だよね。ウェブの進化には、 1) 複雑なニーズへの進化 2) ハック的なJS/CSSの実装や回避策 3) CSSの標準として実装される っていうパターンがある。この段階はあまりハック的じゃないね。本当にすごい。もしこれが実現可能なら、誰かがもうやってると思ったけど、そうじゃなかったみたい。いつかこのライブラリの本当の洞察を理解したいな。彼らの https://github.com/chenglou/pretext/blob/main/RESEARCH.md は面白いし、ブラウザの違いを細かく調べて、絵文字が各ブラウザでどう測定されるかをちゃんとやったみたい。これがメンテナンスの悪夢にならないことを願ってる。全体的に見て、これがウェブを前に進めることは間違いないね。

今はレスポンシブアコーディオンはCSSで解決できるけど、他の多くのことはまだ解決されてないし、ウェブにはこういうAPIやライブラリがずっと必要だったんだよね。だから、今それが手に入ったのは素晴らしいことだよ。こういうものを作るのは以前も可能だったけど、かなりの労力がかかった。変わったのはシンプルで、AIのおかげだね。このライブラリは主にCursorを使ってエージェントで作られたみたい。これは批判じゃなくて、以前はできなかったものを作るためのAIの完璧な使い方だと思う。

うん、これは絶対に必要だよね。だから、SciterにGraphics.Text (https://docs.sciter.com/docs/Graphics/Text) を追加したんだ。Graphics.Textは基本的に、すべてのCSSの機能を使ってキャンバスに描画できる独立した要素なんだ。

これがブラウザで提供される標準機能であるべきだよね。W3Cに機能リクエストをどうやって出すの?コミュニティが機能アイデアに投票できるようになってるの?

うーん、デモが私のシステム(Fedora、Firefox)では全部間違って表示される。例えば、トーラスが完全に歪んでる。編集:例:https://files.catbox.moe/4w3um0.png

これを作ろうとすると、エッジケースの長い尾を追いかけ続けることになるってことを示してると思う。

作者によるライブラリについて > これは、Claude CodeとCodexにブラウザの実際のデータを見せて、重要なコンテナ幅ごとに測定・反復させることで達成されました。数週間にわたって行われました。 https://x.com/_chenglou/status/2037715226838343871?s=20 これに関してAutoresearchを使うというコメントもあったけど、ちょっと記憶が曖昧かも。

うわぁ、これが1年前にあったらよかったのに。HTMLで印刷用のパンフレット組版システムを作るのに、めちゃくちゃ時間をかけちゃったんだ。新しい行が孤立しないように、適切な改行ポイントを見つけるために、Selection APIを使って、候補のレンダリングのバウンディングボックスを繰り返し探してた。ちゃんと動いてるし、今でもプロダクションでうまく稼働してるけど、なぜかうまくいくオフバイワンのハックがあって、その理由が全然わからない。ここでの反復的な行生成機能はすごいね。