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텍스트 배치에 대해 약 19ms
  • layout(): 동일 배치에 대해 약 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: normal
  • word-break: normal
  • overflow-wrap: break-word
  • line-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

Source: https://github.com/chenglou/pretext