[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-1384":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":15,"stars7d":15,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":18,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":21,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":45,"readmeContent":46,"aiSummary":47,"trendingCount":16,"starSnapshotCount":16,"syncStatus":15,"lastSyncTime":48,"discoverSource":49},1384,"expo-pretext","JubaKitiashvili\u002Fexpo-pretext","JubaKitiashvili","Predict React Native text heights before rendering. Native TextKit\u002FTextPaint measurement + ~0.0002ms JS layout. FlashList, streaming AI chat, typewriter, pinch-to-zoom, obstacle reflow, Dynamic Type, Expo Web. 386 tests. MIT.","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fexpo-pretext",null,"TypeScript",249,11,230,2,0,18,6,3.24,"MIT License",false,"main",true,[25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44],"ai-chat","android","cross-platform","expo","expo-modules","flashlist","ios","line-breaking","mobile","pretext","react-native","reanimated","streaming","text-layout","text-measurement","textkit","typewriter","typography","unicode","virtualization","2026-06-12 02:00:27","# expo-pretext\n\n**The text layout primitive React Native was missing.** Native measurement, ~0.0002ms pure-JS layout arithmetic, and full control over how text flows — around shapes, inside gestures, through animations. All with regular `\u003CView>` and `\u003CText>`. No Skia canvas, no SVG tricks.\n\n**v1.0.0 is here.** Production-ready. Closes [18+ open React Native text bugs](#v100--fixes-for-18-open-rnexpo-text-bugs) — Android cut-off clusters, italic clipping, `numberOfLines` edge cases, font-load races, silent font fallback, and CJK typography.\n\n[![npm](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Fexpo-pretext.svg)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fexpo-pretext)\n[![license](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fl\u002Fexpo-pretext.svg)](.\u002FLICENSE)\n[![tests](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Ftests-637%20passing-brightgreen.svg)](.\u002Fsrc\u002F__tests__)\n[![benchmarks](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fbenchmarks-docs-blue.svg)](.\u002Fdocs\u002FBENCHMARKS.md)\n[![live demo](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flive%20demo-expo--pretext.vercel.app-ffd369.svg)](https:\u002F\u002Fexpo-pretext.vercel.app)\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fgithub.com\u002FJubaKitiashvili\u002Fexpo-pretext\u002Fraw\u002Fmain\u002Fassets\u002Fdemos\u002Fhero.gif\" width=\"720\" alt=\"expo-pretext demo reel\" \u002F>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fgithub.com\u002FJubaKitiashvili\u002Fexpo-pretext\u002Fraw\u002Fmain\u002Fassets\u002Fdemos\u002Fhero-reel.gif\" width=\"720\" alt=\"expo-pretext creative demos reel\" \u002F>\n\u003C\u002Fp>\n\n---\n\n## What React Native couldn't do before\n\nFlexbox is rectangular boxes all the way down. iOS TextKit has `NSTextContainer.exclusionPaths`. CSS has `shape-outside`. The native capability exists on every platform — React Native just never exposed it. Even `react-native-skia`'s `Paragraph` API is rectangle-only ([open issue since 2022](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F968)).\n\nexpo-pretext closes that gap:\n\n- **Text reflow around arbitrary shapes** — circles, rectangles, or any polygon. Magazine-style layouts, circular avatars with wrapping captions, text that reacts to moving obstacles.\n- **Exact heights before render** — FlashList virtualization with zero `onLayout` jumps, even for 10k messages with rich markdown.\n- **Per-frame layout recomputation** — `layout()` runs in ~0.0002ms, fast enough to run 120+ times per frame during gestures, physics, or animations.\n- **Streaming AI chat** — cache-aware incremental measurement for token-by-token reveals.\n- **Regular RN rendering tree** — A11y, Dynamic Type, selection, copy\u002Fpaste, VoiceOver, RTL, emoji ZWJ — all work, because you're still using `\u003CText>`.\n\n## Why it's fast\n\n```\nprepare(text, style)         One native measurement call per text\n  → Native:  iOS TextKit \u002F Android TextPaint \u002F Web Canvas\n  → Returns: cached segment widths + break opportunities\n  → Time:    ~15ms for 500 texts in a batch\n\nlayout(prepared, maxWidth)   Pure JS arithmetic on cached data\n  → No native bridge, no DOM, no reflow\n  → Time:    ~0.0002ms per text\n  → Safe to call 120+ times per frame\n```\n\nThe flagship demo is a [Breakout arcade game](.\u002Fexample\u002Fcomponents\u002Fdemos\u002FBreakoutText.tsx) where the live prose background reflows around the ball, the paddle, and every falling brick at 60fps. It exists to prove the performance claim visually, not just in a benchmark.\n\n## Install\n\n```sh\nnpx expo install expo-pretext\n```\n\nRequires Expo SDK 52+, React Native 0.76+, New Architecture \u002F Fabric. Reanimated is an optional peer dependency used only by the animation hooks. Expo Go falls back to JS estimates — use a development build for native measurement.\n\n## Quick start\n\n### Text height before render\n\n```tsx\nimport { useTextHeight } from 'expo-pretext'\n\nfunction ChatBubble({ text, maxWidth }) {\n  const height = useTextHeight(text, {\n    fontFamily: 'Inter',\n    fontSize: 16,\n    lineHeight: 24,\n  }, maxWidth)\n\n  return \u003CView style={{ height }}>\u003CText>{text}\u003C\u002FText>\u003C\u002FView>\n}\n```\n\n### FlashList with exact heights (v2)\n\n`useFlashListHeights` pre-warms a height cache in the background and returns\n`getHeight(item)` — set it as an explicit height on the wrapping View inside\n`renderItem`. FlashList v2 skips the measurement pass when the height is known,\neliminating first-paint jitter for plain-text lists.\n\n```tsx\nimport { useFlashListHeights } from 'expo-pretext'\nimport { FlashList } from '@shopify\u002Fflash-list'\nimport { View, Text } from 'react-native'\n\nconst STYLE = { fontFamily: 'Inter', fontSize: 16, lineHeight: 24 }\nconst PAD_Y = 10\n\nfunction ChatScreen({ messages, width }) {\n  const { getHeight } = useFlashListHeights(\n    messages,\n    msg => msg.text,\n    STYLE,\n    width,\n  )\n\n  return (\n    \u003CFlashList\n      data={messages}\n      keyExtractor={m => m.id}\n      renderItem={({ item }) => (\n        \u003CView style={{ height: getHeight(item) + PAD_Y * 2 }}>\n          \u003CText style={STYLE}>{item.text}\u003C\u002FText>\n        \u003C\u002FView>\n      )}\n    \u002F>\n  )\n}\n```\n\n> For plain `\u003CText>` content, `getHeight(item)` returns the exact rendered\n> height (FlashList v2 no longer accepts `estimatedItemSize` or a size-bearing\n> `overrideItemLayout`). For rich content like Markdown or mixed components,\n> let FlashList v2 auto-measure instead.\n\n### Text reflow around a shape\n\n```tsx\nimport { useObstacleLayout } from 'expo-pretext'\n\nfunction MagazineLayout({ text, width }) {\n  const layout = useObstacleLayout(\n    text,\n    { fontFamily: 'Georgia', fontSize: 18, lineHeight: 28 },\n    { x: 0, y: 0, width, height: 600 },\n    \u002F\u002F circular avatar to flow around\n    [{ cx: 80, cy: 80, r: 64 }],\n  )\n\n  return (\n    \u003CView style={{ height: layout.height }}>\n      {layout.lines.map((line, i) => (\n        \u003CText key={i} style={{ position: 'absolute', left: line.x, top: line.y }}>\n          {line.text}\n        \u003C\u002FText>\n      ))}\n      \u003CImage source={avatar} style={{ position: 'absolute', width: 128, height: 128, borderRadius: 64 }} \u002F>\n    \u003C\u002FView>\n  )\n}\n```\n\n### Streaming AI chat with incremental measurement\n\n```tsx\nimport { useStreamingLayout } from 'expo-pretext'\n\nfunction StreamingBubble({ text, maxWidth }) {\n  \u002F\u002F Auto-detects append pattern. Cache-aware. ~2ms per token.\n  const { height, lineCount, doesNextTokenWrap } = useStreamingLayout(text, style, maxWidth)\n  return (\n    \u003CView style={{ minHeight: height }}>\n      \u003CText>{text}\u003C\u002FText>\n    \u003C\u002FView>\n  )\n}\n```\n\n### Pinch-to-zoom text at 60fps\n\n```tsx\nimport { usePinchToZoomText } from 'expo-pretext\u002Fanimated'\nimport Animated from 'react-native-reanimated'\n\nfunction ZoomableText({ text, maxWidth }) {\n  const zoom = usePinchToZoomText(text, baseStyle, maxWidth, {\n    minFontSize: 8,\n    maxFontSize: 48,\n  })\n  \u002F\u002F layout() runs per frame. 120+ recalculations\u002Fframe possible.\n  return \u003CAnimated.Text style={[baseStyle, zoom.animatedStyle]}>{text}\u003C\u002FAnimated.Text>\n}\n```\n\n### Balanced headlines + widow-free paragraphs (CSS `text-wrap: balance` \u002F `pretty`)\n\nChrome 114+ shipped `text-wrap: balance`; Safari 17.5 followed. Firefox took a year. On React Native, neither iOS nor Android has any equivalent. `\u003CBalancedText>` \u002F `\u003CPrettyText>` ship both on every platform — pixel-identical across iOS, Android, and Expo Web. Under 10 µs per invocation.\n\n```tsx\nimport { BalancedText, PrettyText } from 'expo-pretext'\n\n\u002F\u002F Balanced — no lonely last word\n\u003CBalancedText\n  style={{ fontFamily: 'Inter', fontSize: 32, fontWeight: '700', lineHeight: 40 }}\n  maxWidth={cardWidth}\n>\n  {headline}\n\u003C\u002FBalancedText>\n\n\u002F\u002F Pretty — widow-free paragraph tails\n\u003CPrettyText\n  style={{ fontFamily: 'Georgia', fontSize: 16, lineHeight: 24 }}\n  maxWidth={columnWidth}\n>\n  {articleText}\n\u003C\u002FPrettyText>\n```\n\nSee the [live web playground](https:\u002F\u002Fexpo-pretext.vercel.app) — same demo on iOS, Android, and a browser at the URL above.\n\n### Line-by-line rendering (bypasses Android wrap\u002Fcut-off bugs)\n\nSince RN 0.78, Android's text renderer has regressed in several ways: descender clipping, text disappearing under certain font weights, extra wraps from `letterSpacing`. `\u003CSafeText>` computes line breaks ourselves and emits one `\u003CText>` per line — RN has no wrap decision left to make, and cut-off goes away.\n\n```tsx\nimport { SafeText } from 'expo-pretext'\n\n\u003CSafeText\n  style={{ fontFamily: 'Inter', fontSize: 16, lineHeight: 24 }}\n  maxWidth={containerWidth}\n>\n  {paragraphText}\n\u003C\u002FSafeText>\n```\n\nCloses [RN #15114](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F15114), [#49886](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F49886), [#53286](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F53286), [#53666](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F53666), [#56402](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F56402), [#48921](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F48921).\n\n### Truncation without `numberOfLines` edge cases\n\n`numberOfLines` + `ellipsizeMode` has long-standing issues: Android's `middle` \u002F `head` modes break for multi-line; iOS drops the ellipsis when the source has `\\n`; the ellipsis inherits the trimmed text's background color. `\u003CTruncatedText>` computes the visible substring in JS and renders it as plain text.\n\n```tsx\nimport { TruncatedText } from 'expo-pretext'\n\n\u003CTruncatedText\n  style={{ fontFamily: 'Inter', fontSize: 14 }}\n  maxWidth={containerWidth}\n  maxLines={3}\n  mode=\"tail\"   \u002F\u002F or 'head' \u002F 'middle'\n>\n  {longArticleText}\n\u003C\u002FTruncatedText>\n```\n\nCloses [RN #19117](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F19117), [#41405](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F41405), [#37926](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F37926).\n\n### Auto cache invalidation on font load \u002F Dynamic Type\n\nCall `enableAutoInvalidation()` once at app root; caches clear automatically on system font-scale changes and on `expo-font` load events.\n\n```tsx\nimport { enableAutoInvalidation, notifyFontsLoaded } from 'expo-pretext'\nimport { useFonts } from 'expo-font'\n\nexport default function App() {\n  const [loaded] = useFonts({ Inter: require('.\u002FInter.ttf') })\n\n  useEffect(() => {\n    const stop = enableAutoInvalidation()\n    return stop\n  }, [])\n\n  useEffect(() => {\n    if (loaded) notifyFontsLoaded()\n  }, [loaded])\n\n  \u002F\u002F …\n}\n```\n\nAddresses [Expo #21885](https:\u002F\u002Fgithub.com\u002Fexpo\u002Fexpo\u002Fissues\u002F21885) (82 comments on `useFonts` reliability).\n\n### Detect silent font fallback (RN 0.83 New Arch)\n\nCustom fonts sometimes report as loaded but fall back to System. `verifyFontsLoaded()` measures a reference string with the requested font vs System and reports whether the custom font is actually applied.\n\n```ts\nimport { verifyFontsLoaded } from 'expo-pretext'\n\nconst v = verifyFontsLoaded({ fontFamily: 'Inter', fontSize: 16 })\nif (v && !v.applied) {\n  console.warn('Inter is not being applied — falling back to System')\n}\n```\n\nCloses [RN #54934](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F54934), [#56309](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F56309), [#54642](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F54642).\n\n### Skia adapter — `measureRuns()`\n\nFor `react-native-skia` users who need precise glyph bounds with font fallback resolved in JS ([Skia #3493](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F3493), [#3488](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F3488), [#1736](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F1736)).\n\n```ts\nimport { measureRuns } from 'expo-pretext'\n\nconst { naturalWidth, naturalHeight, runs } = measureRuns('Hello', style)\n\u002F\u002F runs: Array\u003C{ text, bounds, advance, font: { family, size, weight, style } }>\n```\n\nPure measurement — doesn't require `react-native-skia` installed.\n\n### Fix italic text clipping (RN #56349)\n\nReact Native sizes text containers to advance width, but italic glyphs extend beyond that — causing visual clipping. expo-pretext fixes this with ink-bounds measurement.\n\n**Drop-in fix — one component:**\n\n```tsx\nimport { InkSafeText } from 'expo-pretext'\n\n\u002F\u002F Before (clips):  \u003CText style={style}>fly\u003C\u002FText>\n\u002F\u002F After (fixed):\n\u003CInkSafeText style={{\n  fontFamily: 'Georgia',\n  fontSize: 80,\n  fontWeight: 'bold',\n  fontStyle: 'italic',\n}}>\n  fly\n\u003C\u002FInkSafeText>\n```\n\n`\u003CInkSafeText>` is a drop-in `\u003CText>` replacement. Non-italic text renders with zero overhead.\n\n**Custom container sizing — hook:**\n\n```tsx\nimport { useInkSafeStyle } from 'expo-pretext'\n\nconst { style: safeStyle, inkWidth } = useInkSafeStyle(text, baseStyle)\n\n\u003CView style={{ width: inkWidth, overflow: 'hidden' }}>\n  \u003CText style={safeStyle} numberOfLines={1}>{text}\u003C\u002FText>\n\u003C\u002FView>\n```\n\n**FlashList \u002F imperative — pure function:**\n\n```tsx\nimport { getInkSafePadding } from 'expo-pretext'\n\nconst { padding, inkWidth } = getInkSafePadding(text, style)\n```\n\n## Feature tour\n\n| Category | What you get |\n|---|---|\n| **Drop-in components** | `\u003CSafeText>`, `\u003CTruncatedText>`, `\u003CInkSafeText>` |\n| **Layout primitives** | `layoutColumn` (obstacles), `useObstacleLayout`, `fitFontSize`, `truncateText`, `customBreakRules`, `measureNaturalWidth`, `measureInkWidth` (italic-safe) |\n| **Virtualization** | `useTextHeight`, `useFlashListHeights`, `measureHeights` (batch) |\n| **Streaming AI chat** | `useStreamingLayout`, `useMultiStreamLayout`, `prepareStreaming`, `measureCodeBlockHeight` |\n| **Animation (Reanimated)** | `useAnimatedTextHeight`, `useCollapsibleHeight`, `usePinchToZoomText`, `useTypewriterLayout`, `useTextMorphing` |\n| **Rich inline flow** | `prepareInlineFlow`, `walkInlineFlowLines`, `measureInlineFlow` — mixed fonts, @mentions, pills |\n| **Accessibility** | `getFontScale`, `onFontScaleChange`, `clearAllCaches`, `enableAutoInvalidation`, `notifyFontsLoaded` |\n| **Cross-platform consistency** | `ENGINE_PROFILES`, `setEngineProfile`, `getEngineProfile` — iOS ≡ Android ≡ Web |\n| **Font metrics + fallback** | `getFontMetrics`, `resolveFontFamily`, `validateFont`, `verifyFontsLoaded` |\n| **Hyphenation** | `compileHyphenationPatterns`, `hyphenate`, `hyphenateAndJoin` (Liang-Knuth; bring your own TeX patterns) |\n| **Skia adapter** | `measureRuns` — per-run bounds + advance + font for Skia Paragraph |\n| **Developer tools** | `\u003CPretextDebugOverlay>`, `compareDebugMeasurement`, `buildHeightSnapshot`, `compareHeightSnapshots`, `prepareWithBudget`, `PrepareBudgetTracker` |\n| **Power API** | `prepare`, `layout`, `layoutWithLines`, `layoutNextLine`, `walkLineRanges`, `prepareWithSegments` |\n\nSee [`src\u002Findex.ts`](.\u002Fsrc\u002Findex.ts) for the full public surface.\n\n## v1.0.0 — fixes for 18+ open RN\u002FExpo text bugs\n\n| # | Feature | Closes |\n|---|---------|--------|\n| 1 | `letterSpacing` support | [RN #54823](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F54823), [#46436](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F46436) |\n| 2 | Auto cache invalidation | [Expo #21885](https:\u002F\u002Fgithub.com\u002Fexpo\u002Fexpo\u002Fissues\u002F21885) (82 comments) |\n| 3 | `\u003CInkSafeText strict>` | [RN #49886](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F49886), [#53286](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F53286), [#56402](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F56402), [#15114](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F15114) |\n| 4 | `\u003CTruncatedText>` | [RN #19117](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F19117), [#41405](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F41405), [#37926](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F37926) |\n| 5 | `\u003CSafeText>` (flagship) | [RN #15114](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F15114), [#49886](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F49886), [#53286](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F53286), [#53666](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F53666), [#56402](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F56402), [#48921](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F48921) |\n| 6 | Kinsoku Shori (CJK) | Japanese \u002F Chinese line-break correctness |\n| 7 | `verifyFontsLoaded()` | [RN #54934](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F54934), [#56309](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F56309), [#54642](https:\u002F\u002Fgithub.com\u002Ffacebook\u002Freact-native\u002Fissues\u002F54642) |\n| 8 | `measureRuns()` Skia adapter | [Skia #3493](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F3493), [#3488](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F3488), [#1736](https:\u002F\u002Fgithub.com\u002FShopify\u002Freact-native-skia\u002Fissues\u002F1736) |\n\n## Internationalization\n\nFull Unicode via native OS segmenters. No locale hacks, no userland `Intl` polyfills, no manual script detection:\n\n- **CJK** (Chinese, Japanese, Korean) — per-character breaking with Kinsoku Shori (`、。）」` never start a line; `（「『` never end one) — locked in with 13 targeted tests\n- **Arabic, Hebrew** — RTL with bidi metadata; full UBA rules covered by 30 tests\n- **Thai, Lao, Khmer, Myanmar** — dictionary-based word boundaries\n- **Georgian, Devanagari, Armenian, Ethiopic** — all native scripts\n- **Emoji** — compound graphemes, flag sequences, ZWJ family joiners\n- **Mixed scripts** in a single string, measured correctly\n- **Hyphenation** (Liang-Knuth) — user-supplied TeX patterns, any language\n\n## Performance\n\n| Operation | Cost |\n|---|---|\n| `prepare()` batch | ~15ms for 500 texts |\n| `layout()` per call | ~0.0002ms (pure arithmetic) |\n| Streaming token | ~2ms (mostly cache hits) |\n| Native cache | LRU 5000 segments\u002Ffont, frequency-based eviction |\n| JS cache | Skip native calls entirely when all segments are cached |\n\n## Accuracy\n\nexpo-pretext uses native platform text measurement — the same engines that render your text. Two modes:\n\n- **`fast`** (default): sum individual segment widths. Sub-pixel kerning differences absorbed by tolerance.\n- **`exact`**: re-measure merged segments. Pixel-perfect at the cost of one extra native call.\n\nCross-platform drift between iOS, Android, and Web is bounded by `ENGINE_PROFILES` — use `consistent` mode when you need identical layouts across all three.\n\n## Platform support\n\n| Platform | Backend | Status |\n|---|---|---|\n| iOS | TextKit (`NSLayoutManager`, `NSAttributedString`, `CFStringTokenizer`) | New Architecture \u002F Fabric verified |\n| Android | `TextPaint`, `BreakIterator`, `Paint.FontMetrics` | Kotlin native module |\n| Expo Web | `CanvasRenderingContext2D.measureText` + `Intl.Segmenter` | Zero API changes |\n| Expo Go | JS estimates (no native measurement) | Use a dev build for production |\n\nVerified against FlashList 2.3.1, React Native 0.83, Expo SDK 55.\n\n## Credits\n\nexpo-pretext is a React Native \u002F Expo \u002F Web port of [Pretext](https:\u002F\u002Fgithub.com\u002Fchenglou\u002Fpretext) by Cheng Lou. The core line-breaking algorithm is ported; the measurement backends are new (iOS TextKit, Android TextPaint, Web Canvas instead of DOM APIs). Pretext itself builds on Sebastian Markbage's [text-layout](https:\u002F\u002Fgithub.com\u002Fnicolo-ribaudo\u002Ftext-layout) research.\n\n## License\n\nMIT\n","expo-pretext 是一个用于预测 React Native 文本高度的库，在渲染前就能准确计算文本的高度。它结合了原生 TextKit\u002FTextPaint 测量技术和约 0.0002 毫秒的纯 JavaScript 布局算法，提供了对文本流的完全控制，包括围绕形状、手势和动画中的文本布局。适用于需要精确文本布局的场景，如 FlashList 虚拟化、流式 AI 对话、打字机效果、捏合缩放等。该库支持跨平台开发，兼容 Expo 和 Web，并且已经解决了多个 React Native 中存在的文本显示问题，如 Android 上的截断、斜体剪裁以及字体加载竞争等问题。","2026-06-11 02:43:26","CREATED_QUERY"]