Pretext
DOM 레이아웃 리플로우 없이 멀티라인 텍스트 측정 및 레이아웃
요약
chenglou/pretext는 순수 JavaScript/TypeScript로 작성된 멀티라인 텍스트 측정 및 레이아웃 라이브러리입니다. 빠르고 정확하며 모든 언어를 지원합니다.
핵심 특징
DOM 측정 회피
getBoundingClientRect,offsetHeight등 DOM 측정 필요 없음- 레이아웃 리플로우(Layout Reflow) — 브라우저에서 가장 비싼 작업 중 하나 — 회피
- 브라우저 자체 폰트 엔진을 기반 진리(truth)로 사용 (AI 친화적 반복 방식)
렌더링 지원
- DOM, Canvas, SVG 렌더링 지원
- 곧 서버 사이드 렌더링 지원 예정
언어 지원
- 모든 언어 지원 (이모지, 혼합 bidi 포함)
- 특정 브라우저 퀴크 대응
설치
npm install @chenglou/pretext데모 실행
# 레포 복제, 실행, 브라우저에서 /demos 열기
git clone https://github.com/chenglou/pretext.git
cd pretext
bun install
bun start
# 라이브 데모
# https://chenglou.me/pretext
# https://somnai-dreams.github.io/pretext-demos/기본 사용법
import { prepare, layout } from '@chenglou/pretext'
// 텍스트 준비 (일회성 작업)
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
// 레이아웃 (고속 경로)
const { height, lineCount } = layout(prepared, textWidth, 20)
// 순수 산술 연산. DOM 레이아웃 & 리플로우 없음!Textarea 모드
import { prepare, layout } from '@chenglou/pretext'
// 일반 공백, 탭, \n 하드 브레이크 유지
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 20)성능 (현재 체크인 벤치마크 스냅샷 기준)
prepare(): 공유 500텍스트 배치에 대해 약 19mslayout(): 동일 배치에 대해 약 0.09ms
고급 기능
1. Lines 반환 (layoutWithLines)
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26) // 320px 최대 폭, 26px 라인 높이
for(let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26)
}2. Line Ranges 반환 (walkLineRanges)
텍스트 문자열 없이 라인 폭과 커서 반환:
import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'
const prepared = prepareWithSegments('...', '16px Inter')
let maxW = 0
walkLineRanges(prepared, 320, line => {
if (line.width > maxW) maxW = line.width
})
// maxW는 이제 가장 넓은 라인 — 텍스트가 맞는 가장 타이트한 컨테이너 폭!3. 한 줄씩 레이아웃 (layoutNextLine)
폭이 변할 때마다 텍스트를 한 행씩 라우팅:
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'
const prepared = prepareWithSegments('...', '16px Inter')
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
// 플로팅 이미지 주변 텍스트: 이미지 옆 라인이 좁아짐
while (true) {
const width = y < image.bottom ? columnWidth - image.width : columnWidth
const line = layoutNextLine(prepared, cursor, width)
if (line === null) break
ctx.fillText(line.text, 0, y)
cursor = line.end
y += 26
}Use-Case 1 APIs (기본)
| API | 설명 |
|---|---|
prepare(text, font, options?) | 일회성 텍스트 분석 + 측정 패스, layout()에 전달할 불투명 값 반환 |
layout(prepared, maxWidth, lineHeight) | 최대 폭과 라인 높이가 주어진 텍스트 높이 계산 |
참고: font는 CSS font 선언과 동기화해야 함 (예: 16px Inter)
Use-Case 2 APIs (고급)
| API | 설명 |
|---|---|
prepareWithSegments(text, font, options?) | prepare()와 동일하지만 수동 레이아웃 요구를 위한 더 풍부한 구조 반환 |
layoutWithLines(prepared, maxWidth, lineHeight) | 고급 API. 모든 라인에 고정 최대 폭. layout() 반환과 유사하지만 라인 정보 추가로 반환 |
walkLineRanges(prepared, maxWidth, onLine) | 저수준 API. 라인별로 onLine 한 번 호출. 실제 계산된 라인 폭과 시작/끝 커서 제공. 텍스트 문자열 없음. 너비/높이 경계 사전 테스트에 유용 |
layoutNextLine(prepared, start, maxWidth) | 이터레이터 같은 API. 다른 폭으로 각 라인 레이아웃! start에서 시작하는 LayoutLine 반환 |
타입 정의
type LayoutLine = {
text: string // 이 라인의 전체 텍스트 내용
width: number // 이 라인의 측정된 폭
start: LayoutCursor // 준비된 세그먼트/그라펨에서 포괄적 시작 커서
end: LayoutCursor // 준비된 세그먼트/그라펨에서 배타적 끝 커서
}
type LayoutLineRange = {
width: number // 이 라인의 측정된 폭
start: LayoutCursor // 포괄적 시작 커서
end: LayoutCursor // 배타적 끝 커서
}
type LayoutCursor = {
segmentIndex: number // `prepareWithSegments`의 준비된 리치 세그먼트 스트림에서 세그먼트 인덱스
graphemeIndex: number // 해당 세그먼트 내 그라펨 인덱스
}기타 헬퍼
| 헬퍼 | 설명 |
|---|---|
clearCache() | prepare()와 prepareWithSegments()가 사용하는 Pretext의 공유 내부 캐시 비움. 다양한 폰트/텍스트 변형 순환 앱에 유용 |
setLocale(locale?) | 선택사항 (기본: 현재 로케일 사용). 향후 prepare() 및 prepareWithSegments()에 대한 로케일 설정. 내부적으로 clearCache()도 호출 |
대상 CSS 설정
Pretext는 전체 폰트 렌더링 엔진이 되려고 시도하지 않습니다. 현재 일반 텍스트 설정 타겟팅:
white-space: normalword-break: normaloverflow-wrap: break-wordline-break: auto
{ whiteSpace: 'pre-wrap' } 전달 시:
- 일반 공백,
\t탭,\n하드 브레이크 유지 (축소 대신) - 탭은 기본 브라우저 스타일
tab-size: 8따름 - 다른 래핑 기본값 동일 유지:
word-break: normal,overflow-wrap: break-word,line-break: auto
제한사항
system-ui폰트는 macOS에서layout()정확도에 안전하지 않음. 명명된 폰트 사용 권장- 기본 대상에는
overflow-wrap: break-word포함되어 있어서 매우 좁은 폭에서도 단어 내부 분리 가능 (그라펨 경계에서만)
Web UI 잠금 해제
반환된 높이는 다음을 잠금 해제하는 중요한 조각:
- 가상화/폐색 없이: 추정 및 캐싱 없음
- 팬시 유저랜드 레이아웃: 메이슨리, JS 구동 flexbox 유사 구현, CSS 핵 없는 몇 레이아웃 값 넛징 등
- 개발 시간 검증: 특히 현재 AI로 — 버튼 라벨 등이 다음 라인으로 오버플로우하지 않음 브라우저 없음
- 새 텍스트 로드 시 스크롤 위치 리앵커링: 레이아웃 시프트 방지
영감
Sebastian Markbage가 지난 10년에 text-layout로 시드를 심었습니다. 그의 디자인 — canvas measureText로 쉐이핑, pdf.js의 bidi, 스트리밍 라인 브레이킹 — 이 아키텍처를 계속 발전시키는 데 영감을 주었습니다.
관련
- Text Layout
- Canvas
- SVG