pretext について
Pretextは、DOMに触れずに複数行テキストを計測・レイアウトするJavaScript/TypeScriptライブラリです。Canvasでフォントを計測し、準備結果の再利用や行レイアウト、複数フォントのインライン処理を提供します。
主な機能
- 01DOMを使わないテキスト計測
- 02Canvasによるフォント幅計測
- 03準備結果を再利用した高さ計算
- 04行レイアウトと範囲の生成
- 05複数フォントのインライン処理
活用シーン
- 01仮想リストの可変行高を計算
- 02サイズ変更に合わせてテキスト高を更新
- 03CanvasやSVGで折り返しテキストを描画
- 04複数フォントのチップやメンションを配置
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ asharibali/ pretext-skills --skill pretextインストール後、Claude Code で/pretextを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/AGENTS.mdCHANGELOG.mdDEVELOPMENT.mdREADME.mdRESEARCH.mdSECURITY.mdSTATUS.mdTODO.mdreferences/examples/balanced-text.tsbasic-height.tsbubble-chat.tscanvas-manual-layout.tsflow-around-image.tslabel-overflow-check.tsmultilingual-layout.tsnatural-width.tspre-wrap-textarea.tsresize-handler.tsrich-inline.tsscroll-anchor.tsshrink-wrap.tssvg-text-render.tstwo-column-flow.tsvirtualized-list.ts詳細
Pretext – DOM を使わないテキストの計測とレイアウト
Pretext は、DOM に触れることなく複数行のテキストを計測してレイアウトする、純粋な JavaScript/TypeScript ライブラリです。ブラウザー独自のフォントエンジン(canvas の measureText 経由)を正確な基準として使用し、その後の改行処理と高さの計算はすべて純粋な算術演算で行います。DOM、Canvas、SVG へのレンダリングに対応しており、将来的にはサーバーサイドにも対応する予定です。
重要な理由
DOM ベースのテキスト計測(getBoundingClientRect、offsetHeight)は、レイアウトの再計算を引き起こします。これはブラウザーで最もコストの高い処理のひとつです。呼び出すたびに、エンジンはドキュメント全体に及ぶ可能性のあるスタイルとジオメトリを再計算します。10,000 行の仮想化リストでは、行の高さを取得するだけで 10,000 回の再計算が発生します。
Pretext は、これを 2 段階のアプローチに置き換えます。1 回の prepare() 呼び出しで、テキストの解析、分割、グルー規則、Canvas による計測(コストの高い処理を 1 回だけ実行)を行います。その後、layout() はキャッシュ済みの幅を使って純粋な計算で高さを算出します(コストの低い処理をリサイズのたびに実行)。これは DOM による計測より 300~600 倍高速で、レイアウトのスラッシングを完全に解消します。
その結果、ブラウザー間でピクセル単位の精度を実現しています(Chrome、Safari、Firefox の精度テストでは 7680/7680)。絵文字、双方向テキストの混在、CJK、タイ語、アラビア語、クメール語、ミャンマー語、ウルドゥー語など、あらゆる言語に対応しています。
インストール
npm install @chenglou/pretext
2 つの方法:よりシンプルな方法を選択
高速な方法(90% のケース — 高さだけが必要な場合)
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, maxWidth, 20)
これは、仮想化リスト、動的コンテナー、スクロール位置のアンカー設定など、指定した幅でテキストがどれくらいの高さになるかを知る必要があるあらゆるケースに対応します。prepared ハンドルは再利用できます。リサイズ時には、新しい幅を指定して layout() を再度呼び出すだけです。
スペース、タブ、\n による強制改行をそのまま表示する、テキストエリアのようなテキストの場合:
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 20)
単語の途中での改行を避けたい CJK/ハングルのテキストの場合:
const prepared = prepare(text, '16px Inter', { wordBreak: 'keep-all' })
高機能な方法(実際の行が必要な場合)
テキストを自分でレンダリングする場合(Canvas、SVG、WebGL)、または可変幅のレイアウト(画像の周囲にテキストを流し込むなど)を行う場合は、高機能な API を使用してください。
import { prepareWithSegments, layoutWithLines, walkLineRanges, layoutNextLine, layoutNextLineRange, materializeLineRange, measureLineStats, measureNaturalWidth } from '@chenglou/pretext'
layoutWithLines()— 固定幅でのすべての行を取得します(Canvas/SVG のレンダリング向け)。walkLineRanges()— 文字列を生成せずに行の範囲を取得します(縮小ラップ、最適幅の二分探索、バランスの取れたテキストレイアウト向け)。measureLineStats()— メモリー割り当てなしで、行数と最も幅の広い行の幅を取得します。measureNaturalWidth()— 幅が折り返しの原因ではない場合に、強制改行された行のうち最も幅の広い行を取得します。layoutNextLine()— 幅を変えながら一度に 1 行ずつ取得します(障害物を避けてテキストを流し込む場合)。テキストを含む行全体を返します。layoutNextLineRange()— 上記と同じですが、テキスト文字列を生成しません。必要なときにmaterializeLineRange()を使ってテキストを取得してください。
リッチテキストのインラインフロー(複数フォント、チップ、メンション)
複数フォント、コードスパン、メンション、チップ、ブラウザーのような境界空白の折りたたみを含む、リッチテキストのインラインフローには、次を使用してください。
import { prepareRichInline, walkRichInlineLineRanges, materializeRichInlineLineRange, measureRichInlineStats } from '@chenglou/pretext/rich-inline'
const prepared = prepareRichInline([
{ text: 'Ship ', font: '500 17px Inter' },
{ text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },
{ text: "'s rich-note", font: '500 17px Inter' },
])
walkRichInlineLineRanges(prepared, 320, range => {
const line = materializeRichInlineLineRange(prepared, range)
// each fragment keeps its source item index, text slice, gapBefore, and cursors
})
リッチインラインヘルパーは、意図的に用途を限定しています。
- 境界のスペースを含む生のインラインテキストを入力します。
- ピルの外観(パディング+ボーダー幅)には、呼び出し元が所有する
extraWidthを使用します。 - チップやメンションなどの分割不可アイテムには
break: 'never'を使用します。 white-space: normalのみに対応します。- ネストされたマークアップツリーや、汎用的な CSS インライン整形エンジンではありません。
API リファレンス
高速な方法の API
prepare(text: string, font: string, options?: {
whiteSpace?: 'normal' | 'pre-wrap',
wordBreak?: 'normal' | 'keep-all'
}): PreparedText
layout(prepared: PreparedText, maxWidth: number, lineHeight: number): {
height: number,
lineCount: number
}
高機能な方法の API
prepareWithSegments(text: string, font: string, options?: {
whiteSpace?: 'normal' | 'pre-wrap',
wordBreak?: 'normal' | 'keep-all'
}): PreparedTextWithSegments
layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): {
height: number, lineCount: number, lines: LayoutLine[]
}
walkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number,
onLine: (line: LayoutLineRange) => void): number
measureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): {
lineCount: number, maxLineWidth: number
}
measureNaturalWidth(prepared: PreparedTextWithSegments): number
layoutNextLineRange(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLineRange | null
layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLine | null
materializeLineRange(prepared: PreparedTextWithSegments, line: LayoutLineRange): LayoutLine
リッチインライン API(@chenglou/pretext/rich-inline)
prepareRichInline(items: RichInlineItem[]): PreparedRichInline
layoutNextRichInlineLineRange(prepared: PreparedRichInline, maxWidth: number, start?: RichInlineCursor): RichInlineLineRange | null
walkRichInlineLineRanges(prepared: PreparedRichInline, maxWidth: number,
onLine: (line: RichInlineLineRange) => void): number
materializeRichInlineLineRange(prepared: PreparedRichInline, line: RichInlineLineRange): RichInlineLine
measureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): {
lineCount: number, maxLineWidth: number
}
ユーティリティ API
clearCache(): void // release internal caches used by prepare/prepareWithSegments
setLocale(locale?: string): void // retarget word segmenter for future prepare() calls; also clears cache
主な型
type LayoutLine = {
text: string // full text of the line
width: number // measured pixel width
start: LayoutCursor // inclusive start cursor
end: LayoutCursor // exclusive end cursor
}
type LayoutLineRange = {
width: number
start: LayoutCursor
end: LayoutCursor
}
type LayoutCursor = {
segmentIndex: number // position in prepared segments
graphemeIndex: number // grapheme within that segment
}
type RichInlineItem = {
text: string // raw text including boundary spaces
font: string // canvas font shorthand
break?: 'normal' | 'never' // 'never' = atomic (chips, mentions)
extraWidth?: number // horizontal chrome width (padding + border)
}
type RichInlineFragment = {
itemIndex: number // index into original items array
text: string
gapBefore: number // collapsed boundary gap
occupiedWidth: number // text width + extraWidth
start: LayoutCursor
end: LayoutCursor
}
type RichInlineLine = {
fragments: RichInlineFragment[]
width: number
end: RichInlineCursor
}
正しく使うための重要なポイント
preparedハンドルをキャッシュしてください。 同じテキストと同じフォントであれば、同じハンドルを再利用します。テキストまたはフォントが実際に変わった場合にのみ、再度prepare()を呼び出してください。サイズ変更時は、layout()を再度呼び出すだけで構いません。- CSS と正確に一致させてください。
font文字列は、CSS のfontショートハンド(サイズ、ウェイト、スタイル、ファミリー)と一致させる必要があります。lineHeightは、CSS のline-heightと一致させてください。一致しないと、高さが正しく計算されません。 - スペース、タブ、改行を保持するテキストエリアでは、
{ whiteSpace: 'pre-wrap' }を使用してください。タブは、ブラウザーのデフォルトスタイルに従い、tab-size: 8の位置に揃います。 - 単語内での改行を避けたい CJK/ハングルのテキストには、
{ wordBreak: 'keep-all' }を使用してください。 - macOS では
system-uiを避けてください。 Canvas と DOM で異なるフォント(SF Pro Text と SF Pro Display)が解決され、精度が損なわれる場合があります。名前を指定したフォントを使用してください。 clearCache()が必要になることはほとんどありません。 多数の異なるフォントを切り替えてメモリを解放したい場合にのみ使用してください。setLocale()の使用は任意です。 デフォルトでは、Pretext は現在のロケールを使用します。特定のIntl.Segmenterロケールが必要な場合は、prepare()の前に呼び出してください。
対応する CSS の指定
white-space: normal(デフォルト)およびpre-wrapword-break: normal(デフォルト)およびkeep-alloverflow-wrap: break-word— 幅が狭い場合は単語内で改行できますが、書記素の境界でのみ改行されますline-break: auto- タブは、ブラウザーのデフォルトスタイルに従い、
tab-size: 8の位置に揃います
Pretext が内部で処理する内容
- 分割:テキストの解析と正規化、
Intl.Segmenterによる Unicode 対応の単語/書記素の分割 - CJK:補助平面の表意文字、互換ブロック、拡張ブロック、禁則処理(行頭・行末に置けない句読点)
- アラビア語/RTL:スペースのない句読点のクラスタ結合、句読点と結合文字のクラスタ、リッチパスでの双方向テキストメタデータ
- 絵文字:Canvas と DOM の幅の差を補正するフォントごとの補正値を自動検出、ZWJ シーケンス
- ソフトハイフン:改行されない場合は非表示、改行位置として選択された場合は末尾に
-を表示 - NBSP/NNBSP/WJ:可視コンテンツとして保持し、単語境界での折り返しを防止
- ZWSP:ゼロ幅の改行候補として保持
- URL に似た文字列:不適切なパス途中での改行を避けるため、構造化された改行可能な単位(パス+クエリ)として処理
- 複数スクリプトの混在:双方向テキストの混在、CJK で始まるスペースのない文字列、文脈に応じた ASCII 引用符、数値/時間範囲表現に対応
すぐに使えるサンプル
完全な、コピーしてそのまま使えるコードは、references/examples/ ディレクトリを参照してください。
ファストパス(高さの測定):
basic-height.ts—wordBreak: 'keep-all'を使用して、コンテナー内のテキストの高さを測定pre-wrap-textarea.ts— 空白とタブを保持したままテキストエリアを自動サイズ調整resize-handler.ts— 標準的なサイズ変更パターン:prepare()は一度だけ呼び出し、サイズ変更のたびにlayout()を呼び出すvirtualized-list.ts— キャッシュされたハンドルを使用した、仮想化リストの可変行高scroll-anchor.ts— DOM への挿入前に高さを予測し、レイアウトシフトを防止label-overflow-check.ts— ラベルが折り返されずに収まるかを確認する CI/開発時チェックmultilingual-layout.ts— CJK、アラビア語、タイ語に対応した、setLocale()によるロケール対応の測定
リッチパス(手動の行レイアウト):
canvas-manual-layout.ts—layoutWithLinesを使って Canvas に折り返しテキストを描画svg-text-render.ts— 折り返しテキストを SVG の<tspan>要素に描画flow-around-image.ts—layoutNextLineRangeを使って、フロート画像の周囲にテキストを配置two-column-flow.ts—layoutNextLineRangeを使った、連続した2段組みのエディトリアルレイアウトshrink-wrap.ts—walkLineRangesとmeasureLineStatsを使って、コンテナーの最適な幅を特定balanced-text.ts— 二分探索で行幅を調整し、孤立行を回避natural-width.ts—measureNaturalWidthを使ったツールチップ/ポップオーバーのサイズ調整bubble-chat.ts— チャットバブルのシュリンクラップ:最適な幅を特定してから行を描画
リッチインライン(複数フォント):
rich-inline.ts— アトミックなチップとメンションに対応した、複数フォントのインラインフロー
詳細リファレンス
references/README.md— ライブラリリポジトリの完全な API:インストール、すべての型定義と関数シグネチャ、使用例、注意点。references/AGENTS.md— エージェント向けのガイダンス:重要なソースファイル、実装メモ、実践的なパターン、注意事項、未解決の質問、Pretext を使った開発の判断基準。references/STATUS.md— 現在の精度ダッシュボードとベンチマークのスナップショットへのリンク(ブラウザーの調査結果、コーパスのカナリア)。references/RESEARCH.md— 研究ログ:試行、測定、確認したすべての内容(system-ui の発見、単語数の精度、アラビア語の限界、ソフトハイフン、絵文字の修正、CJK/タイ語/クメール語/ミャンマー語/日本語/中国語のコーパスカナリア、採用されなかった手法)。references/DEVELOPMENT.md— 開発環境のセットアップ、日常的に使うコマンド、パッケージ化とリリースの確認、ブラウザーでの精度測定とベンチマークのコマンド、コーパスツール、詳細なプロファイリングのワークフロー。references/CHANGELOG.md— 0.0.0(2026-03-26)から未リリースの変更までのバージョン履歴。keep-allのサポートやリッチインライン API などが含まれます。references/TODO.md— 現在の優先事項:カナリアのメンテナンス、次のエンジン開発、デモの方向性、未解決の設計上の質問。references/SECURITY.md— GitHub アドバイザリを通じたセキュリティポリシーと非公開の脆弱性報告について。