콘솔의 오른쪽에서 왼쪽으로 쓰는 텍스트
Text.js는 유니코드 양방향 알고리즘을 구현하고 BidiTest.txt의 770,241개 테스트 케이스를 모두 통과해요. 히브리어와 아랍어를 시각적 순서로 렌더링하며, 아랍어 자형 처리를 적용하고 셀 격자에 맞춰 정렬해요.
이제 Text.js는 히브리어, 아랍어, 페르시아어를 비롯한 오른쪽에서 왼쪽으로 쓰는 문자를 다른 문자와 같은 셀 격자에 올바른 순서로 렌더링하며, 아랍어 글자는 연결해서 표시해요.
import { Box, Text } from "@textjs/react";export default function Page() { return ( <Box flexDirection="column" padding={1}> <Text>שלום עולם! זהו טקסט בעברית.</Text> <Text>مرحبا بالعالم! هذا نص عربي.</Text> <Text>Order #1234 (<Text color="accent">שלום</Text>) total: 25.50 ₪</Text> <Text>user <Text dir="auto" color="success">שלום!</Text> replied 3 times</Text> </Box> );}각 문단은 HTML의 dir="auto"처럼 첫 번째 강한 방향성 문자를 기준으로 기본 방향을 정하고, 오른쪽에서 왼쪽으로 쓰는 문단은 오른쪽에 정렬해요. 중첩된 <Text>에서 dir은 <bdi>처럼 스팬을 격리하므로, 방향을 알 수 없는 사용자 이름이 주변 텍스트의 순서를 바꾸지 못해요.
알고리즘
오른쪽에서 왼쪽으로 쓰는 텍스트 지원은 @textjs/core에서 처음부터 작성한 유니코드 양방향 알고리즘(UAX #9, Unicode 18.0) 구현이에요. 명시적 임베딩과 격리, 약한 유형과 중립 유형, 괄호 쌍을 포함해 각 문단의 임베딩 수준을 결정해요. 그런 다음 줄바꿈된 각 줄의 순서를 개별적으로 재배치하고, 오른쪽에서 왼쪽으로 쓰는 수준에서는 대칭 문자를 교체하므로 히브리어 안에서도 (abc)는 (abc)로 유지돼요.
Unicode는 이 알고리즘의 적합성 검사용 파일 2개를 공개해요. 두 파일 모두 gzip으로 압축해 저장소에 포함했으며, 테스트 모음에서 모든 사례를 실행해요:
BidiTest.txt는 입력을 양방향 클래스의 시퀀스로 나열하고, 각 입력에 적용할 문단 방향을 명시해요. Text.js는 770,241개 사례를 모두 통과해요.BidiCharacterTest.txt는 실제 문자열과 예상 수준 및 순서를 나열해요. Text.js는 91,707개를 모두 통과해요.
for (const [bit, dir] of [[1, -1], [2, 0], [4, 1]] as const) { if ((bitset & bit) === 0) continue; cases++; const r = run(types, null, dir); const gotLevels = fmtLevels(r.levels); const gotOrder = r.order.join(" "); if (gotLevels !== levels || gotOrder !== reorder) { if (failures.length < 20) failures.push(`${input.trim()} [dir ${dir}]: levels ${gotLevels} (want ${levels}), order ${gotOrder} (want ${reorder})`); else failures.push(""); }}논리적 순서에서 셀까지
오른쪽에서 왼쪽으로 쓰는 텍스트 한 줄은 콘솔에 도달하기 전에 4단계를 거쳐요:
- 논리적 순서로 줄바꿈. 왼쪽에서 오른쪽으로 쓰는 텍스트와 같은 위치에서 셀 너비를 기준으로 줄을 나눠요.
- 각 줄의 순서 재배치. 결정된 수준에 따라 줄에 있는 자소의 표시 순서가 정해져요.
- 아랍어 자형 구성. 글자는 Arabic Presentation Forms 블록에서 문맥에 맞는 형태(독립형, 어두형, 어중형, 어말형)를 가져와요. 글자 하나가 셀 하나를 차지하고, 단어 중간에 커서가 있거나 색상이 바뀌어도 연결된 형태를 유지해요.
- 순서 고정. 이제 셀에는 표시 순서대로 글리프가 들어 있어요.
마지막 단계가 필요한 이유는 DevTools가 텍스트에 양방향 알고리즘을 한 번 더 실행해 순서를 다시 뒤집기 때문이에요. DevTools의 새니타이저가 direction과 unicode-bidi를 제거하므로 %c 스타일로는 이 동작을 끌 수 없어요. 그래서 드라이버는 오른쪽에서 왼쪽으로 쓰는 문자가 포함된 모든 스팬을 LRO와 PDF(U+202D와 U+202C)로 감싸 셀에 담긴 내용 그대로 왼쪽에서 오른쪽으로 표시하도록 강제해요.
반 셀의 어긋남
SF Mono와 Menlo에는 히브리어와 아랍어 글리프가 없어서 브라우저가 문자마다 대체 글꼴을 사용해요. macOS에서는 같은 고정폭 글꼴인 Courier New를 사용하며, 12px에서 글리프당 너비는 Menlo의 7.2246px에 비해 7.2012px예요. 0.3% 더 좁아서 히브리어 한 줄 전체가 약 반 셀만큼 어긋날 수 있어요.
CssConsoleDriver는 DevTools와 동일한 Blink 대체 글꼴 처리를 사용하는 콘솔 글꼴로 캔버스에서 각 오른쪽에서 왼쪽으로 쓰는 스팬을 측정하고, LayoutUnits(1/64px) 단위의 정확한 차이만큼 스팬에 여백을 추가해요. 모든 스팬의 최종 너비는 해당 스팬이 차지하는 격자 셀의 너비와 같아져요. 스팬을 안쪽으로 당기는 범위는 최대 반 셀이므로, 훨씬 넓은 대체 글꼴은 다음 스팬과 겹치는 대신 영역 밖으로 넘쳐요.
bun bench/devtools/bidi-shot.ts는 히브리어, 아랍어, 혼합 텍스트, 히브리어 안에 이모지가 들어간 줄로 구성된 64×21 프레임에서 실제 DevTools 프런트엔드의 결과를 측정해요:
| 프레임 | 행 간 오른쪽 가장자리 어긋남 |
|---|---|
| 순서 고정 없이 출력한 셀 | 14.5px |
| 순서 고정, 너비 맞춤 없음 | 2.22px |
| 배포 드라이버 | 0.156px |
대조군: 모든 RTL 글리프를 x로 교체 | 1.67px |
대조군은 같은 프레임을 라틴 문자로 표시한 결과예요. 배포 드라이버는 오른쪽에서 왼쪽으로 쓰는 스팬 안의 이모지도 함께 너비를 맞추므로 대조군보다 더 잘 정렬돼요.
왼쪽에서 오른쪽으로 쓰는 텍스트는 변경 없음
오른쪽에서 왼쪽으로 쓰는 문자가 없는 텍스트는 이 과정을 모두 건너뛰어요. 자소를 분할할 때 자소마다 한 번씩 비교해 확인하며, 왼쪽에서 오른쪽으로만 쓰는 텍스트는 이전과 같은 경로로 그려지므로 콘솔 프레임이 바이트 단위까지 동일해요.
오른쪽에서 왼쪽으로 쓰는 텍스트 지원은 Text.js 1.0에 포함돼요. 국제화에서는 입력, 편집, 터미널과 API를 다뤄요.