@apollohg/react-native-rich-text-editor 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (494) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +259 -0
  3. package/RUST-STANDARD-LIBRARY-NOTICES.html +8266 -0
  4. package/ReactNativeProseEditor.podspec +59 -0
  5. package/THIRD_PARTY_NOTICES.md +31155 -0
  6. package/android/build.gradle +188 -0
  7. package/android/consumer-rules.pro +18 -0
  8. package/android/expo/build.gradle +17 -0
  9. package/android/src/debug/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +7 -0
  10. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  11. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  12. package/android/src/main/java/com/apollohg/editor/AndroidApiCompat.kt +19 -0
  13. package/android/src/main/java/com/apollohg/editor/AndroidCollaborationTransport.kt +860 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +91 -0
  15. package/android/src/main/java/com/apollohg/editor/CodeHighlighting.kt +141 -0
  16. package/android/src/main/java/com/apollohg/editor/DecodedBitmapBudget.kt +250 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +133 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorBlockSpacing.kt +51 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorBoxDrawable.kt +16 -0
  20. package/android/src/main/java/com/apollohg/editor/EditorBoxDrawing.kt +234 -0
  21. package/android/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +87 -0
  22. package/android/src/main/java/com/apollohg/editor/EditorCodeHighlighting.kt +120 -0
  23. package/android/src/main/java/com/apollohg/editor/EditorDecorationLayer.java +18 -0
  24. package/android/src/main/java/com/apollohg/editor/EditorDocumentLayout.kt +626 -0
  25. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1042 -0
  26. package/android/src/main/java/com/apollohg/editor/EditorEditTextAnnotations.kt +191 -0
  27. package/android/src/main/java/com/apollohg/editor/EditorEditTextCaret.kt +100 -0
  28. package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +101 -0
  29. package/android/src/main/java/com/apollohg/editor/EditorEditTextComposition.kt +220 -0
  30. package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +316 -0
  31. package/android/src/main/java/com/apollohg/editor/EditorEditTextCorrection.kt +303 -0
  32. package/android/src/main/java/com/apollohg/editor/EditorEditTextDeferredUpdates.kt +327 -0
  33. package/android/src/main/java/com/apollohg/editor/EditorEditTextDeletion.kt +258 -0
  34. package/android/src/main/java/com/apollohg/editor/EditorEditTextDiagnostics.kt +83 -0
  35. package/android/src/main/java/com/apollohg/editor/EditorEditTextDragDrop.kt +67 -0
  36. package/android/src/main/java/com/apollohg/editor/EditorEditTextExternalComposition.kt +357 -0
  37. package/android/src/main/java/com/apollohg/editor/EditorEditTextHardwareInput.kt +277 -0
  38. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +280 -0
  39. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputLifecycle.kt +183 -0
  40. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputPreparation.kt +79 -0
  41. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputTraits.kt +384 -0
  42. package/android/src/main/java/com/apollohg/editor/EditorEditTextLayout.kt +100 -0
  43. package/android/src/main/java/com/apollohg/editor/EditorEditTextNativeMutation.kt +243 -0
  44. package/android/src/main/java/com/apollohg/editor/EditorEditTextPlaceholder.kt +154 -0
  45. package/android/src/main/java/com/apollohg/editor/EditorEditTextRenderPatch.kt +347 -0
  46. package/android/src/main/java/com/apollohg/editor/EditorEditTextRendering.kt +313 -0
  47. package/android/src/main/java/com/apollohg/editor/EditorEditTextRustCommands.kt +281 -0
  48. package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +352 -0
  49. package/android/src/main/java/com/apollohg/editor/EditorEditTextState.kt +113 -0
  50. package/android/src/main/java/com/apollohg/editor/EditorEditTextTaskMarkers.kt +92 -0
  51. package/android/src/main/java/com/apollohg/editor/EditorEditTextTextCommit.kt +327 -0
  52. package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +84 -0
  53. package/android/src/main/java/com/apollohg/editor/EditorEditTextUpdates.kt +245 -0
  54. package/android/src/main/java/com/apollohg/editor/EditorGlyphHeightCursorDrawable.kt +42 -0
  55. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +13 -0
  56. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +923 -0
  57. package/android/src/main/java/com/apollohg/editor/EditorInputCoordinates.kt +191 -0
  58. package/android/src/main/java/com/apollohg/editor/EditorInputCorrection.kt +107 -0
  59. package/android/src/main/java/com/apollohg/editor/EditorInputCursorGeometry.kt +128 -0
  60. package/android/src/main/java/com/apollohg/editor/EditorInputSnapshots.kt +134 -0
  61. package/android/src/main/java/com/apollohg/editor/EditorInputSurroundingDeletion.kt +321 -0
  62. package/android/src/main/java/com/apollohg/editor/EditorKeyboardToolbarView.kt +851 -0
  63. package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +192 -0
  64. package/android/src/main/java/com/apollohg/editor/EditorReconciliationWatcher.kt +46 -0
  65. package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +303 -0
  66. package/android/src/main/java/com/apollohg/editor/EditorStyleSheetSpans.kt +253 -0
  67. package/android/src/main/java/com/apollohg/editor/EditorSurfaceInputConnection.kt +67 -0
  68. package/android/src/main/java/com/apollohg/editor/EditorTextDecorationDrawing.kt +143 -0
  69. package/android/src/main/java/com/apollohg/editor/EditorTextSurface.kt +712 -0
  70. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceDragDrop.kt +94 -0
  71. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +708 -0
  72. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +650 -0
  73. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +912 -0
  74. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterMutations.kt +402 -0
  75. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterRendering.kt +123 -0
  76. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterSelection.kt +203 -0
  77. package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +120 -0
  78. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +87 -0
  79. package/android/src/main/java/com/apollohg/editor/EditorV2Registry.kt +97 -0
  80. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +409 -0
  81. package/android/src/main/java/com/apollohg/editor/ExpoAutoGrowStyleSizePublisher.kt +34 -0
  82. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +266 -0
  83. package/android/src/main/java/com/apollohg/editor/ImeTextCoordinateMapper.kt +102 -0
  84. package/android/src/main/java/com/apollohg/editor/KeyboardToolbarImeAnimationController.kt +101 -0
  85. package/android/src/main/java/com/apollohg/editor/MentionSuggestionChipView.kt +234 -0
  86. package/android/src/main/java/com/apollohg/editor/NativeCollaborationTransportRegistry.kt +493 -0
  87. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +813 -0
  88. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewAtoms.kt +68 -0
  89. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewBinding.kt +126 -0
  90. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewDiagnostics.kt +208 -0
  91. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewErrors.kt +146 -0
  92. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewEvents.kt +150 -0
  93. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewExternalComposition.kt +39 -0
  94. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewFocus.kt +176 -0
  95. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewLifecycle.kt +302 -0
  96. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewMentions.kt +259 -0
  97. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewNativeActions.kt +179 -0
  98. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewOutsideTap.kt +214 -0
  99. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPendingUpdates.kt +376 -0
  100. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +234 -0
  101. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPropertyRetries.kt +197 -0
  102. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewToolbar.kt +285 -0
  103. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewTypes.kt +28 -0
  104. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewUpdates.kt +271 -0
  105. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +687 -0
  106. package/android/src/main/java/com/apollohg/editor/NativeEditorOutsideTapDispatcher.kt +380 -0
  107. package/android/src/main/java/com/apollohg/editor/NativeEditorV2Numbers.kt +45 -0
  108. package/android/src/main/java/com/apollohg/editor/NativeEditorViewRegistry.kt +372 -0
  109. package/android/src/main/java/com/apollohg/editor/NativeImagePipeline.kt +22 -0
  110. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +193 -0
  111. package/android/src/main/java/com/apollohg/editor/NativeToolbarIcons.kt +177 -0
  112. package/android/src/main/java/com/apollohg/editor/NativeToolbarItem.kt +274 -0
  113. package/android/src/main/java/com/apollohg/editor/OkHttpCollaborationSocket.kt +102 -0
  114. package/android/src/main/java/com/apollohg/editor/OrderedListMarkerFormatter.kt +130 -0
  115. package/android/src/main/java/com/apollohg/editor/PointerTransparentView.java +18 -0
  116. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +128 -0
  117. package/android/src/main/java/com/apollohg/editor/ProseViewerView.kt +854 -0
  118. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +314 -0
  119. package/android/src/main/java/com/apollohg/editor/RenderBlockSpans.kt +314 -0
  120. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +224 -0
  121. package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +407 -0
  122. package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +544 -0
  123. package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +631 -0
  124. package/android/src/main/java/com/apollohg/editor/RenderBridgeParagraphs.kt +215 -0
  125. package/android/src/main/java/com/apollohg/editor/RenderImageDecoder.kt +748 -0
  126. package/android/src/main/java/com/apollohg/editor/RenderImageLoader.kt +900 -0
  127. package/android/src/main/java/com/apollohg/editor/RenderLayoutConstants.kt +117 -0
  128. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +337 -0
  129. package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +182 -0
  130. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +951 -0
  131. package/android/src/main/java/com/apollohg/editor/SharedNativeImagePipeline.kt +78 -0
  132. package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +261 -0
  133. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1656 -0
  134. package/android/src/main/java/com/apollohg/editor/viewer/FabricAttachmentSidecars.kt +80 -0
  135. package/android/src/main/java/com/apollohg/editor/viewer/FabricLeaseHandleBridge.kt +59 -0
  136. package/android/src/main/java/com/apollohg/editor/viewer/FallbackSelectionGeometry.kt +450 -0
  137. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseDrawingView.kt +678 -0
  138. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseInstrumentation.kt +608 -0
  139. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayout.kt +186 -0
  140. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutCache.kt +393 -0
  141. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutRegistry.kt +828 -0
  142. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseTheme.kt +316 -0
  143. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerManager.kt +900 -0
  144. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerPackage.kt +16 -0
  145. package/android/src/main/java/com/apollohg/editor/viewer/PreparedViewerAtoms.kt +89 -0
  146. package/android/src/main/java/com/apollohg/editor/viewer/ViewerCodeHighlighting.kt +91 -0
  147. package/android/src/main/java/com/apollohg/editor/viewer/ViewerDocument.kt +453 -0
  148. package/android/src/main/java/com/apollohg/editor/viewer/ViewerFontEnvironment.kt +282 -0
  149. package/android/src/main/java/com/apollohg/editor/viewer/ViewerImagePipeline.kt +460 -0
  150. package/android/src/main/jni/CMakeLists.txt +85 -0
  151. package/android/src/main/jni/PreparedProseViewerMeasurementsManager.cpp +334 -0
  152. package/android/src/main/jni/ReactNativeProseEditorSpec.h +17 -0
  153. package/android/src/main/jni/reactnativeproseeditor.h +6 -0
  154. package/android/src/main/res/values/attrs.xml +6 -0
  155. package/android/src/release/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +9 -0
  156. package/app.plugin.js +62 -0
  157. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseMeasurementsManager.h +62 -0
  158. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerComponentDescriptor.h +30 -0
  159. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.cpp +192 -0
  160. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.h +46 -0
  161. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerState.h +145 -0
  162. package/dist/AtomHost.d.ts +17 -0
  163. package/dist/AtomHost.js +135 -0
  164. package/dist/DefaultAtomChip.d.ts +2 -0
  165. package/dist/DefaultAtomChip.js +22 -0
  166. package/dist/EditorAddon.d.ts +32 -0
  167. package/dist/EditorAddon.js +29 -0
  168. package/dist/EditorAddonNormalization.d.ts +6 -0
  169. package/dist/EditorAddonNormalization.js +48 -0
  170. package/dist/EditorMentionThemeNormalization.d.ts +8 -0
  171. package/dist/EditorMentionThemeNormalization.js +22 -0
  172. package/dist/EditorMentionThemeValidation.d.ts +12 -0
  173. package/dist/EditorMentionThemeValidation.js +95 -0
  174. package/dist/EditorStyleSheet.d.ts +4 -0
  175. package/dist/EditorStyleSheet.js +10 -0
  176. package/dist/EditorStyleSheetNormalization.d.ts +3 -0
  177. package/dist/EditorStyleSheetNormalization.js +226 -0
  178. package/dist/EditorStyleSheetTypes.d.ts +151 -0
  179. package/dist/EditorStyleSheetTypes.js +2 -0
  180. package/dist/EditorTheme.d.ts +117 -0
  181. package/dist/EditorTheme.js +41 -0
  182. package/dist/EditorToolbar.d.ts +4 -0
  183. package/dist/EditorToolbar.js +17 -0
  184. package/dist/EditorToolbarComponent.d.ts +13 -0
  185. package/dist/EditorToolbarComponent.js +24 -0
  186. package/dist/EditorToolbarItems.d.ts +9 -0
  187. package/dist/EditorToolbarItems.js +58 -0
  188. package/dist/EditorToolbarRegistry.d.ts +59 -0
  189. package/dist/EditorToolbarRegistry.js +175 -0
  190. package/dist/EditorToolbarTypes.d.ts +273 -0
  191. package/dist/EditorToolbarTypes.js +2 -0
  192. package/dist/EditorToolbarVisuals.d.ts +154 -0
  193. package/dist/EditorToolbarVisuals.js +228 -0
  194. package/dist/EditorUpdateRevision.d.ts +8 -0
  195. package/dist/EditorUpdateRevision.js +22 -0
  196. package/dist/ExternalTextComposition.d.ts +45 -0
  197. package/dist/ExternalTextComposition.js +537 -0
  198. package/dist/ImageLoadingPolicy.d.ts +54 -0
  199. package/dist/ImageLoadingPolicy.js +60 -0
  200. package/dist/NativeEditorBoundaryError.d.ts +111 -0
  201. package/dist/NativeEditorBoundaryError.js +268 -0
  202. package/dist/NativeEditorBridge.d.ts +9 -0
  203. package/dist/NativeEditorBridge.js +37 -0
  204. package/dist/NativeEditorCollaborationTransport.d.ts +154 -0
  205. package/dist/NativeEditorCollaborationTransport.js +275 -0
  206. package/dist/NativeEditorCreateConfig.d.ts +16 -0
  207. package/dist/NativeEditorCreateConfig.js +249 -0
  208. package/dist/NativeEditorCreateJson.d.ts +76 -0
  209. package/dist/NativeEditorCreateJson.js +474 -0
  210. package/dist/NativeEditorCreateValidation.d.ts +32 -0
  211. package/dist/NativeEditorCreateValidation.js +150 -0
  212. package/dist/NativeEditorDocumentBridge.d.ts +72 -0
  213. package/dist/NativeEditorDocumentBridge.js +305 -0
  214. package/dist/NativeEditorDocumentHandle.d.ts +70 -0
  215. package/dist/NativeEditorDocumentHandle.js +89 -0
  216. package/dist/NativeEditorLocalAwareness.d.ts +34 -0
  217. package/dist/NativeEditorLocalAwareness.js +153 -0
  218. package/dist/NativeEditorNativeModule.d.ts +48 -0
  219. package/dist/NativeEditorNativeModule.js +34 -0
  220. package/dist/NativeEditorRenderNormalization.d.ts +22 -0
  221. package/dist/NativeEditorRenderNormalization.js +274 -0
  222. package/dist/NativeEditorResultNormalization.d.ts +108 -0
  223. package/dist/NativeEditorResultNormalization.js +400 -0
  224. package/dist/NativeEditorTypes.d.ts +318 -0
  225. package/dist/NativeEditorTypes.js +15 -0
  226. package/dist/NativeEditorV2Decimal.d.ts +5 -0
  227. package/dist/NativeEditorV2Decimal.js +18 -0
  228. package/dist/NativeProseViewer.d.ts +122 -0
  229. package/dist/NativeProseViewer.js +130 -0
  230. package/dist/NativeRichTextEditor.d.ts +3 -0
  231. package/dist/NativeRichTextEditor.js +6 -0
  232. package/dist/ResourceLimits.d.ts +100 -0
  233. package/dist/ResourceLimits.js +80 -0
  234. package/dist/RichTextEditor.d.ts +4 -0
  235. package/dist/RichTextEditor.js +25 -0
  236. package/dist/RichTextEditorNativeTypes.d.ts +122 -0
  237. package/dist/RichTextEditorNativeTypes.js +2 -0
  238. package/dist/RichTextEditorNativeView.d.ts +12 -0
  239. package/dist/RichTextEditorNativeView.js +15 -0
  240. package/dist/RichTextEditorSerialization.d.ts +53 -0
  241. package/dist/RichTextEditorSerialization.js +283 -0
  242. package/dist/RichTextEditorTypes.d.ts +281 -0
  243. package/dist/RichTextEditorTypes.js +2 -0
  244. package/dist/ViewerConfiguration.d.ts +22 -0
  245. package/dist/ViewerConfiguration.js +6 -0
  246. package/dist/YjsCollaboration.d.ts +146 -0
  247. package/dist/YjsCollaboration.js +496 -0
  248. package/dist/addons.d.ts +193 -0
  249. package/dist/addons.js +122 -0
  250. package/dist/atomConstants.d.ts +3 -0
  251. package/dist/atomConstants.js +6 -0
  252. package/dist/atomInstances.d.ts +35 -0
  253. package/dist/atomInstances.js +79 -0
  254. package/dist/atomPolicy.d.ts +4 -0
  255. package/dist/atomPolicy.js +47 -0
  256. package/dist/atomUpdates.d.ts +2 -0
  257. package/dist/atomUpdates.js +38 -0
  258. package/dist/atoms.d.ts +92 -0
  259. package/dist/atoms.js +222 -0
  260. package/dist/attributeValidation.d.ts +4 -0
  261. package/dist/attributeValidation.js +97 -0
  262. package/dist/contentExpression.d.ts +6 -0
  263. package/dist/contentExpression.js +335 -0
  264. package/dist/index.d.ts +21 -0
  265. package/dist/index.js +78 -0
  266. package/dist/listMarkers.d.ts +15 -0
  267. package/dist/listMarkers.js +21 -0
  268. package/dist/schemaDefinition.d.ts +143 -0
  269. package/dist/schemaDefinition.js +206 -0
  270. package/dist/schemaDocument.d.ts +17 -0
  271. package/dist/schemaDocument.js +69 -0
  272. package/dist/schemaDocumentConstruction.d.ts +5 -0
  273. package/dist/schemaDocumentConstruction.js +125 -0
  274. package/dist/schemaNormalization.d.ts +29 -0
  275. package/dist/schemaNormalization.js +313 -0
  276. package/dist/schemaPresets.d.ts +68 -0
  277. package/dist/schemaPresets.js +198 -0
  278. package/dist/schemaResolution.d.ts +9 -0
  279. package/dist/schemaResolution.js +211 -0
  280. package/dist/schemas.d.ts +5 -0
  281. package/dist/schemas.js +28 -0
  282. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +37 -0
  283. package/dist/specs/PreparedProseViewerNativeComponent.js +6 -0
  284. package/dist/useEditorToolbarInteractions.d.ts +12 -0
  285. package/dist/useEditorToolbarInteractions.js +218 -0
  286. package/dist/useEditorToolbarItems.d.ts +8 -0
  287. package/dist/useEditorToolbarItems.js +280 -0
  288. package/dist/useEditorToolbarPresentation.d.ts +4 -0
  289. package/dist/useEditorToolbarPresentation.js +203 -0
  290. package/dist/useEditorToolbarState.d.ts +63 -0
  291. package/dist/useEditorToolbarState.js +108 -0
  292. package/dist/useFocusPreservingFrames.d.ts +17 -0
  293. package/dist/useFocusPreservingFrames.js +108 -0
  294. package/dist/useNativeEditor.d.ts +66 -0
  295. package/dist/useNativeEditor.js +370 -0
  296. package/dist/useRichTextEditorCommands.d.ts +21 -0
  297. package/dist/useRichTextEditorCommands.js +299 -0
  298. package/dist/useRichTextEditorEvents.d.ts +18 -0
  299. package/dist/useRichTextEditorEvents.js +216 -0
  300. package/dist/useRichTextEditorMentions.d.ts +11 -0
  301. package/dist/useRichTextEditorMentions.js +316 -0
  302. package/dist/useRichTextEditorPresentation.d.ts +5 -0
  303. package/dist/useRichTextEditorPresentation.js +178 -0
  304. package/dist/useRichTextEditorState.d.ts +180 -0
  305. package/dist/useRichTextEditorState.js +405 -0
  306. package/dist/useRichTextEditorUpdates.d.ts +10 -0
  307. package/dist/useRichTextEditorUpdates.js +308 -0
  308. package/dist/useViewerAtoms.d.ts +38 -0
  309. package/dist/useViewerAtoms.js +288 -0
  310. package/expo-module.config.json +13 -0
  311. package/ios/EditorAddons.swift +354 -0
  312. package/ios/EditorCodeHighlighting.swift +184 -0
  313. package/ios/EditorCore.xcframework/Info.plist +44 -0
  314. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  315. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  316. package/ios/EditorInteractionOverlays.swift +697 -0
  317. package/ios/EditorLayoutManager.swift +971 -0
  318. package/ios/EditorNodeTypes.swift +37 -0
  319. package/ios/EditorStyleSheet.swift +461 -0
  320. package/ios/EditorTextView+AttributedPatch.swift +305 -0
  321. package/ios/EditorTextView+Binding.swift +61 -0
  322. package/ios/EditorTextView+Caret.swift +333 -0
  323. package/ios/EditorTextView+Commands.swift +203 -0
  324. package/ios/EditorTextView+Delegate.swift +65 -0
  325. package/ios/EditorTextView+Diagnostics.swift +225 -0
  326. package/ios/EditorTextView+DragDrop.swift +290 -0
  327. package/ios/EditorTextView+ExternalComposition.swift +364 -0
  328. package/ios/EditorTextView+Images.swift +354 -0
  329. package/ios/EditorTextView+InputTraits.swift +200 -0
  330. package/ios/EditorTextView+KeyboardInput.swift +318 -0
  331. package/ios/EditorTextView+Layout.swift +245 -0
  332. package/ios/EditorTextView+MarkedText.swift +267 -0
  333. package/ios/EditorTextView+NativeInputLifecycle.swift +372 -0
  334. package/ios/EditorTextView+NativeMutation.swift +471 -0
  335. package/ios/EditorTextView+Reconciliation.swift +97 -0
  336. package/ios/EditorTextView+RenderPatch.swift +573 -0
  337. package/ios/EditorTextView+Rendering.swift +539 -0
  338. package/ios/EditorTextView+Selection.swift +542 -0
  339. package/ios/EditorTextView.swift +953 -0
  340. package/ios/EditorTheme.swift +626 -0
  341. package/ios/EditorV2Adapter+Commands.swift +204 -0
  342. package/ios/EditorV2Adapter+Envelope.swift +238 -0
  343. package/ios/EditorV2Adapter+Mutation.swift +392 -0
  344. package/ios/EditorV2Adapter+Registry.swift +185 -0
  345. package/ios/EditorV2Adapter+RenderState.swift +360 -0
  346. package/ios/EditorV2Adapter+RenderValidation.swift +420 -0
  347. package/ios/EditorV2Adapter+Selection.swift +329 -0
  348. package/ios/EditorV2Adapter+TextCommands.swift +430 -0
  349. package/ios/EditorV2Adapter.swift +376 -0
  350. package/ios/EditorV2Shadow.swift +353 -0
  351. package/ios/Generated_editor_core.swift +2378 -0
  352. package/ios/NativeCodeHighlighting.swift +159 -0
  353. package/ios/NativeCollaborationSocket.swift +144 -0
  354. package/ios/NativeCollaborationTransport+Protocol.swift +258 -0
  355. package/ios/NativeCollaborationTransport+Socket.swift +379 -0
  356. package/ios/NativeCollaborationTransport+Validation.swift +103 -0
  357. package/ios/NativeCollaborationTransport.swift +322 -0
  358. package/ios/NativeCollaborationTransportRegistry.swift +521 -0
  359. package/ios/NativeEditorExpoView+AccessoryRetry.swift +219 -0
  360. package/ios/NativeEditorExpoView+Binding.swift +268 -0
  361. package/ios/NativeEditorExpoView+Diagnostics.swift +66 -0
  362. package/ios/NativeEditorExpoView+Events.swift +131 -0
  363. package/ios/NativeEditorExpoView+Focus.swift +169 -0
  364. package/ios/NativeEditorExpoView+MentionRetry.swift +288 -0
  365. package/ios/NativeEditorExpoView+Mentions.swift +288 -0
  366. package/ios/NativeEditorExpoView+Properties.swift +300 -0
  367. package/ios/NativeEditorExpoView+Toolbar.swift +86 -0
  368. package/ios/NativeEditorExpoView+Updates.swift +312 -0
  369. package/ios/NativeEditorExpoView.swift +349 -0
  370. package/ios/NativeEditorModule.swift +856 -0
  371. package/ios/NativeEditorViewRegistry.swift +226 -0
  372. package/ios/NativeToolbar+ButtonStyle.swift +214 -0
  373. package/ios/NativeToolbar+Buttons.swift +228 -0
  374. package/ios/NativeToolbar+Diagnostics.swift +114 -0
  375. package/ios/NativeToolbar+Menu.swift +101 -0
  376. package/ios/NativeToolbar+Models.swift +519 -0
  377. package/ios/NativeToolbar+Placeholder.swift +39 -0
  378. package/ios/NativeToolbar.swift +612 -0
  379. package/ios/OrderedListMarkerFormatter.swift +103 -0
  380. package/ios/PositionBridge.swift +596 -0
  381. package/ios/ProseViewerView.swift +462 -0
  382. package/ios/RenderBridge+Atoms.swift +288 -0
  383. package/ios/RenderBridge+Attachments.swift +246 -0
  384. package/ios/RenderBridge+BlockBoundaries.swift +313 -0
  385. package/ios/RenderBridge+Style.swift +343 -0
  386. package/ios/RenderBridge.swift +704 -0
  387. package/ios/RichTextEditorView.swift +930 -0
  388. package/ios/SharedNativeImagePipeline+Cache.swift +189 -0
  389. package/ios/SharedNativeImagePipeline+Transport.swift +328 -0
  390. package/ios/SharedNativeImagePipeline.swift +533 -0
  391. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +436 -0
  392. package/ios/Viewer/CoreTextProseLayoutEngine+Models.swift +322 -0
  393. package/ios/Viewer/CoreTextProseLayoutEngine.swift +635 -0
  394. package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.h +4 -0
  395. package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.mm +767 -0
  396. package/ios/Viewer/Fabric/PreparedProseMeasurementsManager.mm +118 -0
  397. package/ios/Viewer/NativeSVGImageDecoder.h +9 -0
  398. package/ios/Viewer/NativeSVGImageDecoder.mm +106 -0
  399. package/ios/Viewer/NativeSVGPreflight.h +11 -0
  400. package/ios/Viewer/NativeSVGPreflight.mm +219 -0
  401. package/ios/Viewer/PreparedProseDrawingView.swift +639 -0
  402. package/ios/Viewer/PreparedProseInstrumentation.swift +387 -0
  403. package/ios/Viewer/PreparedProseLayout.swift +326 -0
  404. package/ios/Viewer/PreparedProseLayoutCache.swift +766 -0
  405. package/ios/Viewer/PreparedProseLayoutRegistry+Artifacts.swift +377 -0
  406. package/ios/Viewer/PreparedProseLayoutRegistry+Fabric.swift +582 -0
  407. package/ios/Viewer/PreparedProseLayoutRegistry+FabricOwnership.swift +128 -0
  408. package/ios/Viewer/PreparedProseLayoutRegistry.swift +381 -0
  409. package/ios/Viewer/ViewerDocument.swift +575 -0
  410. package/ios/Viewer/ViewerFontEnvironment.swift +283 -0
  411. package/ios/Viewer/ViewerImagePipeline.swift +478 -0
  412. package/ios/editor_coreFFI/editor_coreFFI.h +1023 -0
  413. package/ios/editor_coreFFI/module.modulemap +7 -0
  414. package/package.json +151 -0
  415. package/react-native.config.js +17 -0
  416. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  417. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  418. package/rust/android/x86/libeditor_core.so +0 -0
  419. package/rust/android/x86_64/libeditor_core.so +0 -0
  420. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +3146 -0
  421. package/src/AtomHost.tsx +180 -0
  422. package/src/DefaultAtomChip.tsx +27 -0
  423. package/src/EditorAddon.ts +65 -0
  424. package/src/EditorAddonNormalization.ts +49 -0
  425. package/src/EditorMentionThemeNormalization.ts +25 -0
  426. package/src/EditorMentionThemeValidation.ts +133 -0
  427. package/src/EditorStyleSheet.ts +9 -0
  428. package/src/EditorStyleSheetNormalization.ts +219 -0
  429. package/src/EditorStyleSheetTypes.ts +189 -0
  430. package/src/EditorTheme.ts +196 -0
  431. package/src/EditorToolbar.tsx +31 -0
  432. package/src/EditorToolbarComponent.tsx +23 -0
  433. package/src/EditorToolbarItems.tsx +64 -0
  434. package/src/EditorToolbarRegistry.tsx +254 -0
  435. package/src/EditorToolbarTypes.tsx +322 -0
  436. package/src/EditorToolbarVisuals.tsx +270 -0
  437. package/src/EditorUpdateRevision.ts +26 -0
  438. package/src/ExternalTextComposition.ts +658 -0
  439. package/src/ImageLoadingPolicy.ts +125 -0
  440. package/src/NativeEditorBoundaryError.ts +345 -0
  441. package/src/NativeEditorBridge.ts +93 -0
  442. package/src/NativeEditorCollaborationTransport.ts +493 -0
  443. package/src/NativeEditorCreateConfig.ts +326 -0
  444. package/src/NativeEditorCreateJson.ts +579 -0
  445. package/src/NativeEditorCreateValidation.ts +178 -0
  446. package/src/NativeEditorDocumentBridge.ts +485 -0
  447. package/src/NativeEditorDocumentHandle.ts +168 -0
  448. package/src/NativeEditorLocalAwareness.ts +168 -0
  449. package/src/NativeEditorNativeModule.ts +97 -0
  450. package/src/NativeEditorRenderNormalization.ts +320 -0
  451. package/src/NativeEditorResultNormalization.ts +469 -0
  452. package/src/NativeEditorTypes.ts +366 -0
  453. package/src/NativeEditorV2Decimal.ts +13 -0
  454. package/src/NativeProseViewer.tsx +356 -0
  455. package/src/NativeRichTextEditor.tsx +33 -0
  456. package/src/ResourceLimits.ts +187 -0
  457. package/src/RichTextEditor.tsx +27 -0
  458. package/src/RichTextEditorNativeTypes.tsx +144 -0
  459. package/src/RichTextEditorNativeView.tsx +21 -0
  460. package/src/RichTextEditorSerialization.tsx +381 -0
  461. package/src/RichTextEditorTypes.tsx +327 -0
  462. package/src/ViewerConfiguration.ts +23 -0
  463. package/src/YjsCollaboration.ts +692 -0
  464. package/src/addons.ts +311 -0
  465. package/src/atomConstants.ts +3 -0
  466. package/src/atomInstances.ts +123 -0
  467. package/src/atomPolicy.ts +47 -0
  468. package/src/atomUpdates.ts +52 -0
  469. package/src/atoms.ts +379 -0
  470. package/src/attributeValidation.ts +116 -0
  471. package/src/contentExpression.ts +338 -0
  472. package/src/index.ts +295 -0
  473. package/src/listMarkers.ts +18 -0
  474. package/src/schemaDefinition.ts +395 -0
  475. package/src/schemaDocument.ts +92 -0
  476. package/src/schemaDocumentConstruction.ts +148 -0
  477. package/src/schemaNormalization.ts +367 -0
  478. package/src/schemaPresets.ts +250 -0
  479. package/src/schemaResolution.ts +242 -0
  480. package/src/schemas.ts +43 -0
  481. package/src/specs/PreparedProseViewerNativeComponent.ts +40 -0
  482. package/src/useEditorToolbarInteractions.tsx +329 -0
  483. package/src/useEditorToolbarItems.tsx +390 -0
  484. package/src/useEditorToolbarPresentation.tsx +483 -0
  485. package/src/useEditorToolbarState.tsx +172 -0
  486. package/src/useFocusPreservingFrames.ts +171 -0
  487. package/src/useNativeEditor.ts +534 -0
  488. package/src/useRichTextEditorCommands.tsx +478 -0
  489. package/src/useRichTextEditorEvents.tsx +342 -0
  490. package/src/useRichTextEditorMentions.tsx +441 -0
  491. package/src/useRichTextEditorPresentation.tsx +563 -0
  492. package/src/useRichTextEditorState.tsx +620 -0
  493. package/src/useRichTextEditorUpdates.tsx +403 -0
  494. package/src/useViewerAtoms.tsx +427 -0
@@ -0,0 +1,1656 @@
1
+ package com.apollohg.editor.viewer
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Paint
5
+ import android.graphics.Path
6
+ import android.graphics.PathMeasure
7
+ import android.graphics.Rect
8
+ import android.graphics.RectF
9
+ import android.graphics.Typeface
10
+ import android.text.Layout
11
+ import android.text.SpannableString
12
+ import android.text.Spanned
13
+ import android.text.StaticLayout
14
+ import android.text.TextPaint
15
+ import android.text.style.BackgroundColorSpan
16
+ import android.text.style.ForegroundColorSpan
17
+ import android.text.style.LineHeightSpan
18
+ import android.text.style.MetricAffectingSpan
19
+ import android.text.style.ReplacementSpan
20
+ import android.text.style.StrikethroughSpan
21
+ import android.text.style.UnderlineSpan
22
+ import com.apollohg.editor.EditorBoxStyle
23
+ import com.apollohg.editor.EditorEdges
24
+ import com.apollohg.editor.EditorLinkTheme
25
+ import com.apollohg.editor.EditorMentionTheme
26
+ import com.apollohg.editor.EditorOrderedListMarkerTheme
27
+ import com.apollohg.editor.EditorResolvedTextSpan
28
+ import com.apollohg.editor.EditorTextStyle
29
+ import com.apollohg.editor.EditorTheme
30
+ import com.apollohg.editor.OrderedListMarkerFormatter
31
+ import com.apollohg.editor.ProseViewerError
32
+ import com.apollohg.editor.applyPhysicalTextAlignment
33
+ import java.text.Bidi
34
+ import kotlin.math.abs
35
+ import kotlin.math.ceil
36
+ import kotlin.math.max
37
+ import kotlin.math.min
38
+
39
+ /** Creates immutable StaticLayout fragments without depending on a mounted View. */
40
+ internal interface AndroidProseLayoutEngine {
41
+ fun prepare(
42
+ document: ViewerDocument,
43
+ key: ProseLayoutKey,
44
+ theme: PreparedProseTheme,
45
+ widthPx: Int,
46
+ density: Float,
47
+ collapsesWhenEmpty: Boolean
48
+ ): PreparedProseLayout
49
+
50
+ /** Registry callers always provide the immutable semantic warning scope. */
51
+ fun prepare(
52
+ document: ViewerDocument,
53
+ key: ProseLayoutKey,
54
+ theme: PreparedProseTheme,
55
+ widthPx: Int,
56
+ density: Float,
57
+ collapsesWhenEmpty: Boolean,
58
+ semanticGenerationIdentity: String
59
+ ): PreparedProseLayout = prepare(document, key, theme, widthPx, density, collapsesWhenEmpty)
60
+ }
61
+
62
+ private data class PreparedAtomAppearance(
63
+ val paint: PreparedTextPaint,
64
+ val background: Int,
65
+ val borderColor: Int?,
66
+ val borderWidth: Float,
67
+ val radius: Float,
68
+ val paddingHorizontal: Int,
69
+ val paddingVertical: Int,
70
+ val box: EditorBoxStyle? = null,
71
+ val inset: EditorEdges? = null
72
+ )
73
+ private data class PreparedAtomSpec(
74
+ val start: Int,
75
+ val nodeType: String,
76
+ val docPos: Long,
77
+ val attrsJson: String,
78
+ val label: String,
79
+ val appearance: PreparedAtomAppearance,
80
+ val widthPx: Int,
81
+ val heightPx: Int,
82
+ val labelLayout: StaticLayout,
83
+ val labelBaselinePx: Int
84
+ )
85
+ private data class PreparedMarker(
86
+ val layout: StaticLayout?,
87
+ val label: String,
88
+ val widthPx: Int,
89
+ val heightPx: Int,
90
+ val ascentPx: Int,
91
+ val baselinePx: Int,
92
+ val checked: Boolean,
93
+ val checkbox: com.apollohg.editor.EditorElementStyle? = null
94
+ )
95
+
96
+ internal const val PREPARED_LIST_MARKER_GAP_DP = 6f
97
+
98
+ internal data class PreparedMarkerInk(val ascentPx: Int, val heightPx: Int)
99
+
100
+ internal fun preparedMarkerInk(
101
+ glyphBounds: Rect,
102
+ layoutAscentPx: Int,
103
+ layoutDescentPx: Int
104
+ ): PreparedMarkerInk {
105
+ if (glyphBounds.isEmpty) {
106
+ val ascent = max(0, -layoutAscentPx)
107
+ return PreparedMarkerInk(ascent, max(1, ascent + max(0, layoutDescentPx)))
108
+ }
109
+ return PreparedMarkerInk(-glyphBounds.top, max(1, glyphBounds.bottom - glyphBounds.top))
110
+ }
111
+
112
+ private class AtomMetricSpan(
113
+ private val widthPx: Int,
114
+ private val ascentPx: Int,
115
+ private val descentPx: Int
116
+ ) : ReplacementSpan() {
117
+ override fun getSize(
118
+ paint: Paint,
119
+ text: CharSequence?,
120
+ start: Int,
121
+ end: Int,
122
+ fm: Paint.FontMetricsInt?
123
+ ): Int {
124
+ fm?.let {
125
+ it.ascent = -ascentPx
126
+ it.top = it.ascent
127
+ it.descent = descentPx
128
+ it.bottom = descentPx
129
+ }
130
+ return widthPx
131
+ }
132
+ override fun draw(
133
+ canvas: android.graphics.Canvas,
134
+ text: CharSequence?,
135
+ start: Int,
136
+ end: Int,
137
+ x: Float,
138
+ top: Int,
139
+ y: Int,
140
+ bottom: Int,
141
+ paint: Paint
142
+ ) = Unit
143
+ }
144
+
145
+ /**
146
+ * Viewer-local fixed line metrics, deliberately independent of the legacy
147
+ * editable-render bridge. The metric follows Core Text parity: never shrink
148
+ * the shaped run/atom below its natural height, and split added leading around
149
+ * the baseline with the odd pixel assigned below it.
150
+ */
151
+ internal class FixedLineHeightMetricSpan(private val lineHeightPx: Int) : LineHeightSpan {
152
+ override fun chooseHeight(
153
+ text: CharSequence,
154
+ start: Int,
155
+ end: Int,
156
+ spanstartv: Int,
157
+ v: Int,
158
+ fm: Paint.FontMetricsInt
159
+ ) {
160
+ val naturalHeight = fm.descent - fm.ascent
161
+ val targetHeight = max(naturalHeight, lineHeightPx)
162
+ val extraLeading = targetHeight - naturalHeight
163
+ if (naturalHeight <= 0 || extraLeading <= 0) return
164
+
165
+ val leadingAboveBaseline = extraLeading / 2
166
+ fm.ascent -= leadingAboveBaseline
167
+ fm.top = fm.ascent
168
+ fm.descent += extraLeading - leadingAboveBaseline
169
+ fm.bottom = fm.descent
170
+ }
171
+ }
172
+
173
+ /** Immutable before StaticLayout construction, including full link typography. */
174
+ internal class ResolvedTextStyleSpan(internal val typeface: Typeface, internal val sizePx: Float) :
175
+ MetricAffectingSpan() {
176
+ override fun updateMeasureState(textPaint: TextPaint) {
177
+ textPaint.typeface = typeface
178
+ textPaint.textSize = sizePx
179
+ }
180
+
181
+ override fun updateDrawState(textPaint: TextPaint) = updateMeasureState(textPaint)
182
+ }
183
+
184
+ internal class StaticLayoutAndroidProseLayoutEngine : AndroidProseLayoutEngine {
185
+ /** Test seam: drawing must never increment this prepared-layout counter. */
186
+ internal var staticLayoutsBuilt: Int = 0
187
+ private set
188
+
189
+ override fun prepare(
190
+ document: ViewerDocument,
191
+ key: ProseLayoutKey,
192
+ theme: PreparedProseTheme,
193
+ widthPx: Int,
194
+ density: Float,
195
+ collapsesWhenEmpty: Boolean
196
+ ): PreparedProseLayout = prepare(
197
+ document,
198
+ key,
199
+ theme,
200
+ widthPx,
201
+ density,
202
+ collapsesWhenEmpty,
203
+ key.semanticGenerationIdentity
204
+ )
205
+
206
+ override fun prepare(
207
+ document: ViewerDocument,
208
+ key: ProseLayoutKey,
209
+ theme: PreparedProseTheme,
210
+ widthPx: Int,
211
+ density: Float,
212
+ collapsesWhenEmpty: Boolean,
213
+ semanticGenerationIdentity: String
214
+ ): PreparedProseLayout {
215
+ val warningSemanticGeneration = semanticGenerationIdentity
216
+ if (widthPx <= 0 || !density.isFinite() ||
217
+ density <= 0f
218
+ ) {
219
+ return PreparedProseLayout.error(key, 0, ProseViewerError.invalidWidth())
220
+ }
221
+ val visibleBlocks = if (collapsesWhenEmpty) {
222
+ document.blocks.dropLast(
223
+ document.trailingEmptyTextBlockCount.coerceAtMost(document.blocks.size)
224
+ )
225
+ } else {
226
+ document.blocks
227
+ }
228
+ if (visibleBlocks.isEmpty() &&
229
+ collapsesWhenEmpty
230
+ ) {
231
+ return PreparedProseLayout(
232
+ key,
233
+ widthPx,
234
+ 0,
235
+ emptyList(),
236
+ retainedBytes = document.retainedBytes
237
+ )
238
+ }
239
+ val contentWidth = max(1, widthPx - theme.insetLeftPx - theme.insetRightPx)
240
+ var cursorY = theme.insetTopPx
241
+ var retained = document.retainedBytes + theme.retainedBytes
242
+ val interactions = mutableListOf<PreparedProseInteraction>()
243
+ val imageAttachments = mutableListOf<ViewerImageAttachment>()
244
+ val viewerAtoms = mutableListOf<PreparedViewerAtom>()
245
+ val highlightedCodeKeys = mutableSetOf<String>()
246
+ val markers = mutableMapOf<Int, PreparedMarker>()
247
+ visibleBlocks.forEach { block ->
248
+ listItemAncestors(block).forEachIndexed { nestingDepth, ancestor ->
249
+ if (markers[ancestor.identity] == null) {
250
+ markers[ancestor.identity] = markerFor(
251
+ ancestor.context,
252
+ nestingDepth,
253
+ theme.paintFor(block),
254
+ theme
255
+ )
256
+ }
257
+ }
258
+ }
259
+ val containerBounds = mutableMapOf<Int, Rect>()
260
+ val sheet = theme.sourceTheme?.styleSheet
261
+ val leafBoxes = visibleBlocks.mapIndexed { index, block ->
262
+ val box = sheet?.box(block.nodeType)?.scaled(density) ?: EditorBoxStyle()
263
+ val parent = block.containers.lastOrNull()
264
+ if (block.nodeType == "paragraph" && parent?.nodeType == "blockquote" &&
265
+ parent.lastLeaf == index
266
+ ) {
267
+ box.copy(margin = box.margin.copy(bottom = 0f))
268
+ } else {
269
+ box
270
+ }
271
+ }
272
+ var blocks = visibleBlocks.mapIndexed { index, block ->
273
+ val nextAncestorIdentities = visibleBlocks.getOrNull(index + 1)
274
+ ?.let(::listItemAncestors)
275
+ ?.mapTo(mutableSetOf()) { it.identity }
276
+ .orEmpty()
277
+ val disappearingListItemIdentities = listItemAncestors(block)
278
+ .filter { it.identity !in nextAncestorIdentities }
279
+ .mapTo(mutableSetOf()) { it.identity }
280
+ val containers = if (sheet == null) emptyList() else block.containers
281
+ if (sheet != null && index > 0) {
282
+ val previous = visibleBlocks[index - 1].containers
283
+ var common = 0
284
+ while (common < previous.size && common < containers.size &&
285
+ previous[common].identity == containers[common].identity
286
+ ) {
287
+ common++
288
+ }
289
+ val bottom = (
290
+ previous.getOrNull(common)?.let {
291
+ sheet.box(it.nodeType).scaled(density).margin.bottom
292
+ }
293
+ ?: leafBoxes[index - 1].margin.bottom
294
+ ).toInt()
295
+ val top = (
296
+ containers.getOrNull(common)?.let {
297
+ sheet.box(it.nodeType).scaled(density).margin.top
298
+ }
299
+ ?: leafBoxes[index].margin.top
300
+ ).toInt()
301
+ cursorY += maxOf(0, bottom, top) + minOf(0, bottom, top) - bottom - top
302
+ }
303
+ var containerInset = EditorEdges()
304
+ containers.forEach { ancestor ->
305
+ val box = sheet!!.box(ancestor.nodeType).scaled(density)
306
+ if (ancestor.firstLeaf == index) {
307
+ cursorY += box.margin.top.toInt()
308
+ containerBounds[ancestor.identity] =
309
+ Rect(
310
+ theme.insetLeftPx + containerInset.left.toInt() +
311
+ box.margin.left.toInt(),
312
+ cursorY,
313
+ widthPx - theme.insetRightPx - containerInset.right.toInt() -
314
+ box.margin.right.toInt(),
315
+ cursorY
316
+ )
317
+ cursorY += box.inset.top.toInt()
318
+ }
319
+ containerInset += box.outerInset.copy(top = 0f, bottom = 0f)
320
+ }
321
+ val box = leafBoxes[index]
322
+ val outer = box.outerInset
323
+ val leafTop = cursorY
324
+ val leftInset = (containerInset.left + outer.left).toInt()
325
+ val rightInset = (containerInset.right + outer.right).toInt()
326
+ var prepared = prepareBlock(
327
+ block,
328
+ imageAttachments.size,
329
+ markers,
330
+ if (sheet ==
331
+ null
332
+ ) {
333
+ theme
334
+ } else {
335
+ theme.copy(
336
+ insetLeftPx = theme.insetLeftPx + leftInset,
337
+ listItemSpacingPx = 0,
338
+ listSpacingAfterPx = 0
339
+ )
340
+ },
341
+ max(1, contentWidth - leftInset - rightInset),
342
+ cursorY + outer.top.toInt(),
343
+ disappearingListItemIdentities,
344
+ warningSemanticGeneration
345
+ )
346
+ if (sheet != null) {
347
+ val end = prepared.nextY + outer.bottom.toInt()
348
+ val bounds = Rect(
349
+ theme.insetLeftPx + containerInset.left.toInt() + box.margin.left.toInt(),
350
+ leafTop + box.margin.top.toInt(),
351
+ widthPx - theme.insetRightPx - containerInset.right.toInt() -
352
+ box.margin.right.toInt(),
353
+ end - box.margin.bottom.toInt()
354
+ )
355
+ prepared.attachment?.let { attachment ->
356
+ bounds.left = attachment.bounds.left - box.inset.left.toInt()
357
+ bounds.right = attachment.bounds.right + box.inset.right.toInt()
358
+ }
359
+ val decoration =
360
+ PreparedProseFragment(PreparedProseFragmentKind.BACKGROUND, bounds, box = box)
361
+ val seed = Rect(
362
+ theme.insetLeftPx,
363
+ leafTop,
364
+ widthPx - theme.insetRightPx,
365
+ end
366
+ ).apply {
367
+ union(bounds)
368
+ prepared.block.fragments.forEach { union(it.bounds) }
369
+ }
370
+ prepared =
371
+ prepared.copy(
372
+ block = PreparedProseBlock(
373
+ listOf(decoration) + prepared.block.fragments,
374
+ seed
375
+ ),
376
+ nextY = end
377
+ )
378
+ }
379
+ cursorY = prepared.nextY
380
+ containers.asReversed().forEach { ancestor ->
381
+ if (ancestor.lastLeaf == index) {
382
+ val box = sheet!!.box(ancestor.nodeType).scaled(density)
383
+ cursorY += box.inset.bottom.toInt()
384
+ containerBounds[ancestor.identity]?.bottom = cursorY
385
+ cursorY += box.margin.bottom.toInt()
386
+ }
387
+ }
388
+ retained += prepared.block.retainedBytes + prepared.extraBytes
389
+ interactions += prepared.interactions
390
+ prepared.highlightedCodeKey?.let(highlightedCodeKeys::add)
391
+ prepared.attachment?.let(imageAttachments::add)
392
+ prepared.viewerAtom?.let {
393
+ viewerAtoms += it
394
+ retained += it.retainedBytes
395
+ }
396
+ prepared.block
397
+ }
398
+ if (sheet != null) {
399
+ blocks = blocks.mapIndexed { index, prepared ->
400
+ val decorations = visibleBlocks[index].containers.mapNotNull { ancestor ->
401
+ val bounds = containerBounds[ancestor.identity] ?: return@mapNotNull null
402
+ val clip = Rect(
403
+ bounds.left,
404
+ if (ancestor.firstLeaf ==
405
+ index
406
+ ) {
407
+ bounds.top
408
+ } else {
409
+ prepared.bounds.top
410
+ },
411
+ bounds.right,
412
+ if (ancestor.lastLeaf ==
413
+ index
414
+ ) {
415
+ bounds.bottom
416
+ } else {
417
+ prepared.bounds.bottom
418
+ }
419
+ )
420
+ PreparedProseFragment(
421
+ PreparedProseFragmentKind.BACKGROUND,
422
+ clip,
423
+ box = sheet.box(ancestor.nodeType).scaled(density),
424
+ decorationBounds = bounds
425
+ )
426
+ }
427
+ prepared.copy(
428
+ fragments = decorations + prepared.fragments,
429
+ bounds = Rect(prepared.bounds).apply {
430
+ decorations.forEach { union(it.bounds) }
431
+ }
432
+ )
433
+ }
434
+ }
435
+ val height = max(
436
+ 0,
437
+ (
438
+ if (sheet !=
439
+ null
440
+ ) {
441
+ max(cursorY, blocks.maxOfOrNull { it.bounds.bottom } ?: 0)
442
+ } else {
443
+ blocks.maxOfOrNull { it.bounds.bottom }
444
+ ?: cursorY
445
+ }
446
+ ) +
447
+ theme.insetBottomPx
448
+ )
449
+ interactions.sortWith(
450
+ compareBy<PreparedProseInteraction> {
451
+ it.rects.firstOrNull()?.top
452
+ ?: Int.MAX_VALUE
453
+ }.thenBy { it.rects.firstOrNull()?.left ?: Int.MAX_VALUE }
454
+ )
455
+ val nodes = interactions.mapIndexed { index, interaction ->
456
+ PreparedProseAccessibilityNode(
457
+ index,
458
+ if (interaction.kind ==
459
+ PreparedProseInteraction.Kind.LINK
460
+ ) {
461
+ PreparedProseAccessibilityNode.Role.LINK
462
+ } else {
463
+ PreparedProseAccessibilityNode.Role.MENTION
464
+ },
465
+ if (interaction.kind ==
466
+ PreparedProseInteraction.Kind.LINK
467
+ ) {
468
+ interaction.visibleText
469
+ } else {
470
+ interaction.label
471
+ },
472
+ interaction.rects.fold(Rect()) { bounds, rect ->
473
+ if (bounds.isEmpty) Rect(rect) else Rect(bounds).apply { union(rect) }
474
+ }
475
+ )
476
+ }
477
+ retained += interactions.sumOf { it.retainedBytes } + nodes.sumOf { it.retainedBytes }
478
+ val highlightBlocks = if (theme.codeHighlighting ==
479
+ null
480
+ ) {
481
+ emptyList()
482
+ } else {
483
+ visibleBlocks.mapIndexedNotNull { index, block ->
484
+ if (block.nodeType !=
485
+ "codeBlock"
486
+ ) {
487
+ null
488
+ } else {
489
+ com.apollohg.editor.CodeHighlightBlock(index, codeText(block), block.language)
490
+ }
491
+ }
492
+ }
493
+ retained += highlightBlocks.sumOf { 64L + it.text.length * 2L }
494
+ // Mounted image-publication sidecars are runtime surface ownership,
495
+ // not immutable artifact/cache ownership; account them at the host.
496
+ return PreparedProseLayout(
497
+ key,
498
+ widthPx,
499
+ height,
500
+ blocks,
501
+ interactions,
502
+ nodes,
503
+ imageAttachments,
504
+ retained,
505
+ viewerAtoms = viewerAtoms,
506
+ contentBox = sheet?.box(
507
+ "content"
508
+ )?.scaled(
509
+ density
510
+ ),
511
+ codeHighlighting = theme.codeHighlighting,
512
+ codeHighlightBlocks = highlightBlocks,
513
+ highlightedCodeKeys = highlightedCodeKeys.toSet()
514
+ )
515
+ }
516
+
517
+ private data class BlockResult(
518
+ val block: PreparedProseBlock,
519
+ val interactions: List<PreparedProseInteraction>,
520
+ val attachment: ViewerImageAttachment? = null,
521
+ val nextY: Int,
522
+ val extraBytes: Long,
523
+ val viewerAtom: PreparedViewerAtom? = null,
524
+ val highlightedCodeKey: String? = null
525
+ )
526
+
527
+ private fun prepareBlock(
528
+ block: ViewerBlock,
529
+ attachmentOrdinal: Int,
530
+ measuredMarkers: Map<Int, PreparedMarker>,
531
+ theme: PreparedProseTheme,
532
+ contentWidth: Int,
533
+ cursorY: Int,
534
+ disappearingListItemIdentities: Set<Int>,
535
+ warningSemanticGeneration: String
536
+ ): BlockResult {
537
+ val paint = theme.paintFor(block)
538
+ val ancestors = listItemAncestors(block)
539
+ val ancestorMarkers = ancestors.mapNotNull { ancestor ->
540
+ measuredMarkers[ancestor.identity]?.let {
541
+ ancestor to
542
+ it
543
+ }
544
+ }
545
+ val firstMarkers = ancestorMarkers.filter { (ancestor, _) ->
546
+ ancestor.isFirstRenderableLeaf
547
+ }
548
+ fun listStyle(ancestor: ViewerListItemAncestor) = theme.sourceTheme?.styleSheet?.get(
549
+ if (ancestor.context.kind ==
550
+ "task"
551
+ ) {
552
+ "taskList"
553
+ } else if (ancestor.context.ordered) {
554
+ "orderedList"
555
+ } else {
556
+ "bulletList"
557
+ }
558
+ )
559
+ fun listIndent(ancestor: ViewerListItemAncestor) =
560
+ listStyle(ancestor)?.indent?.times(theme.density)?.toInt() ?: theme.listIndentPx
561
+ val baseListInset = if (ancestors.isEmpty()) {
562
+ 0
563
+ } else {
564
+ max(
565
+ 0,
566
+ (
567
+ listIndent(ancestors.first()) *
568
+ (
569
+ listStyle(ancestors.first())?.baseIndentMultiplier
570
+ ?: theme.listBaseIndentMultiplier
571
+ )
572
+ ).toInt()
573
+ )
574
+ }
575
+ val ancestorGutters = ancestorMarkers.associate { (ancestor, marker) ->
576
+ ancestor.identity to
577
+ (
578
+ marker.widthPx +
579
+ (
580
+ marker.checkbox?.gap?.times(theme.density)?.toInt()
581
+ ?: theme.listMarkerGapPx
582
+ )
583
+ )
584
+ }
585
+ // A nested leaf owns every outer list column too: each ancestor adds
586
+ // its list indent and independently measured marker gutter.
587
+ val listInset =
588
+ baseListInset +
589
+ ancestorMarkers.sumOf { (ancestor, _) ->
590
+ listIndent(ancestor) +
591
+ (ancestorGutters[ancestor.identity] ?: 0)
592
+ }
593
+ val quoteInset = if (block.inBlockquote &&
594
+ theme.sourceTheme?.styleSheet == null
595
+ ) {
596
+ theme.quoteBorderWidthPx + theme.quoteMarkerGapPx +
597
+ theme.quoteIndentPx
598
+ } else {
599
+ 0
600
+ }
601
+ val codeInset = if (block.nodeType == "codeBlock") theme.codePaddingHorizontalPx else 0
602
+ val textX = theme.insetLeftPx + listInset + quoteInset + codeInset
603
+ val itemSpacing = if (ancestors.isEmpty()) {
604
+ paint.spacingAfterPx
605
+ } else {
606
+ ancestors.sumOf { ancestor ->
607
+ when {
608
+ ancestor.identity in disappearingListItemIdentities &&
609
+ ancestor.context.isLast ->
610
+ theme.listSpacingAfterPx
611
+
612
+ ancestor.identity in disappearingListItemIdentities -> theme.listItemSpacingPx
613
+
614
+ ancestor.isFinalRenderableLeaf -> theme.listItemSpacingPx
615
+
616
+ else -> 0
617
+ }
618
+ }
619
+ }
620
+ fun markerAnchor(ancestor: ViewerListItemAncestor): Int {
621
+ var inset = baseListInset
622
+ ancestorMarkers.forEach { (candidate, _) ->
623
+ inset += listIndent(candidate) + (ancestorGutters[candidate.identity] ?: 0)
624
+ if (candidate.identity ==
625
+ ancestor.identity
626
+ ) {
627
+ return theme.insetLeftPx + quoteInset + inset
628
+ }
629
+ }
630
+ return textX - codeInset
631
+ }
632
+ val customAtom = (block.inlines.singleOrNull() as? ViewerInline.Atom)?.takeIf {
633
+ block.isBlockAtom && theme.viewerAtoms?.nodeTypes?.contains(it.nodeType) == true
634
+ }
635
+ if (customAtom != null) {
636
+ val atomWidth = max(1, contentWidth - listInset - quoteInset - codeInset * 2)
637
+ val atomHeight = requireNotNull(
638
+ theme.viewerAtoms
639
+ ).heightPx(customAtom, atomWidth, theme.density)
640
+ val bounds = Rect(textX, cursorY, textX + atomWidth, cursorY + atomHeight)
641
+ val fragments = mutableListOf<PreparedProseFragment>()
642
+ if (block.inBlockquote &&
643
+ theme.sourceTheme?.styleSheet == null
644
+ ) {
645
+ fragments +=
646
+ PreparedProseFragment(
647
+ PreparedProseFragmentKind.BORDER,
648
+ Rect(
649
+ theme.insetLeftPx,
650
+ cursorY,
651
+ theme.insetLeftPx + theme.quoteBorderWidthPx,
652
+ bounds.bottom
653
+ ),
654
+ color = theme.quoteBorderColor
655
+ )
656
+ }
657
+ firstMarkers.forEach { (ancestor, marker) ->
658
+ fragments +=
659
+ markerFragment(
660
+ marker,
661
+ markerAnchor(ancestor),
662
+ cursorY,
663
+ bounds.bottom,
664
+ ancestorGutters.getValue(ancestor.identity),
665
+ theme.listMarkerColor
666
+ )
667
+ }
668
+ return finishBlock(
669
+ fragments,
670
+ emptyList(),
671
+ Rect(
672
+ theme.insetLeftPx,
673
+ cursorY,
674
+ theme.insetLeftPx + contentWidth,
675
+ bounds.bottom
676
+ ),
677
+ bounds.bottom,
678
+ itemSpacing
679
+ ).copy(
680
+ viewerAtom = PreparedViewerAtom(
681
+ customAtom.nodeType,
682
+ customAtom.docPos,
683
+ customAtom.attrsJson,
684
+ bounds
685
+ )
686
+ )
687
+ }
688
+ if (block.nodeType == "image") {
689
+ val source = ViewerImageAttachment.sourceAndDeclaredSize(block)
690
+ if (source != null) {
691
+ val availableImageWidth = max(1, contentWidth - listInset - quoteInset)
692
+ val imageWidth = if (theme.sourceTheme?.styleSheet != null &&
693
+ source.third != null
694
+ ) {
695
+ minOf(
696
+ availableImageWidth,
697
+ (
698
+ source.third!!.first *
699
+ theme.density
700
+ ).toInt().coerceAtLeast(1)
701
+ )
702
+ } else {
703
+ availableImageWidth
704
+ }
705
+ val resolved = source.third ?: ViewerImageIntrinsicStore.shared.size(source.first)
706
+ val imageHeight =
707
+ resolved?.let { imageWidth * it.second / max(1, it.first) }
708
+ ?: max(44, minOf(240, (imageWidth * .56f).toInt()))
709
+ val bounds = Rect(textX, cursorY, textX + imageWidth, cursorY + imageHeight)
710
+ val attachment =
711
+ ViewerImageAttachment(
712
+ source.first,
713
+ source.second,
714
+ bounds,
715
+ source.third,
716
+ attachmentOrdinal
717
+ )
718
+ return BlockResult(
719
+ PreparedProseBlock(
720
+ listOf(
721
+ PreparedProseFragment(
722
+ PreparedProseFragmentKind.IMAGE,
723
+ bounds,
724
+ color =
725
+ theme.sourceTheme?.styleSheet?.box("image")?.backgroundColor
726
+ ?: 0xFFF2F2F7.toInt(),
727
+ box = theme.sourceTheme?.styleSheet?.box(
728
+ "image"
729
+ )?.scaled(theme.density)?.let {
730
+ it.copy(
731
+ border = EditorEdges(),
732
+ padding = EditorEdges(),
733
+ corners = com.apollohg.editor.EditorBoxDrawing.innerCorners(
734
+ it
735
+ )
736
+ )
737
+ },
738
+ resizeMode =
739
+ theme.sourceTheme?.styleSheet?.get("image")?.resizeMode
740
+ ?: "contain"
741
+ )
742
+ ),
743
+ bounds
744
+ ),
745
+ emptyList(),
746
+ attachment,
747
+ bounds.bottom + itemSpacing,
748
+ 192
749
+ )
750
+ }
751
+ }
752
+ if (block.nodeType == "horizontalRule" || block.nodeType == "horizontal_rule") {
753
+ val ruleTop = cursorY + theme.ruleMarginPx
754
+ val ruleLeft = theme.insetLeftPx + listInset + quoteInset
755
+ val ruleRight = max(
756
+ ruleLeft + 1,
757
+ theme.insetLeftPx + contentWidth - listInset - quoteInset
758
+ )
759
+ val rule = Rect(ruleLeft, ruleTop, ruleRight, ruleTop + theme.ruleThicknessPx)
760
+ val fragments =
761
+ mutableListOf(
762
+ PreparedProseFragment(
763
+ PreparedProseFragmentKind.RULE,
764
+ rule,
765
+ color = theme.ruleColor,
766
+ strokeWidth = theme.ruleThicknessPx.toFloat()
767
+ )
768
+ )
769
+ val end = rule.bottom + theme.ruleMarginPx
770
+ if (block.inBlockquote &&
771
+ theme.sourceTheme?.styleSheet == null
772
+ ) {
773
+ fragments +=
774
+ PreparedProseFragment(
775
+ PreparedProseFragmentKind.BORDER,
776
+ Rect(
777
+ theme.insetLeftPx,
778
+ cursorY,
779
+ theme.insetLeftPx + theme.quoteBorderWidthPx,
780
+ end
781
+ ),
782
+ color = theme.quoteBorderColor
783
+ )
784
+ }
785
+ firstMarkers.forEach { (ancestor, marker) ->
786
+ fragments +=
787
+ markerFragment(
788
+ marker,
789
+ markerAnchor(ancestor),
790
+ cursorY,
791
+ end,
792
+ ancestorGutters.getValue(ancestor.identity),
793
+ theme.listMarkerColor
794
+ )
795
+ }
796
+ return finishBlock(
797
+ fragments,
798
+ emptyList(),
799
+ Rect(
800
+ theme.insetLeftPx,
801
+ cursorY,
802
+ theme.insetLeftPx + contentWidth,
803
+ end
804
+ ),
805
+ end,
806
+ itemSpacing
807
+ )
808
+ }
809
+
810
+ val availableWidth = max(1, contentWidth - listInset - quoteInset - codeInset * 2)
811
+ val attributed = attributed(block.inlines, paint, theme, warningSemanticGeneration)
812
+ var highlightedCodeKey: String? = null
813
+ if (block.nodeType == "codeBlock") {
814
+ theme.codeHighlighting?.let { config ->
815
+ val code = com.apollohg.editor.CodeHighlightBlock(
816
+ 0,
817
+ codeText(block),
818
+ block.language
819
+ )
820
+ ViewerCodeHighlightCache.get(config, code)?.let { ranges ->
821
+ highlightedCodeKey = ViewerCodeHighlightCache.key(config, code)
822
+ ranges.forEach { range ->
823
+ if (range.start >= 0 &&
824
+ range.start + range.length <= attributed.text.length
825
+ ) {
826
+ attributed.text.setSpan(
827
+ com.apollohg.editor.EditorCodeHighlightSpan(range),
828
+ range.start,
829
+ range.start + range.length,
830
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
831
+ )
832
+ }
833
+ }
834
+ }
835
+ }
836
+ }
837
+ val layout = staticLayout(attributed.text, paint, availableWidth)
838
+ val codeTopInset = if (block.nodeType == "codeBlock") theme.codePaddingVerticalPx else 0
839
+ val firstLineHeight = layout.getLineBottom(0) - layout.getLineTop(0)
840
+ val markerTopProtection = firstMarkers.maxOfOrNull { (_, marker) ->
841
+ max(0, ceil((marker.heightPx - firstLineHeight) / 2f).toInt() - codeTopInset)
842
+ } ?: 0
843
+ val textTop =
844
+ cursorY + codeTopInset + if (cursorY == theme.insetTopPx) markerTopProtection else 0
845
+ val textHeight = max(1, layout.height)
846
+ val totalEnd =
847
+ textTop + textHeight +
848
+ if (block.nodeType == "codeBlock") theme.codePaddingVerticalPx else 0
849
+ val fragments = mutableListOf<PreparedProseFragment>()
850
+ val interactionRects = MutableList(attributed.semanticRanges.size) { mutableListOf<Rect>() }
851
+ if (block.nodeType == "codeBlock" &&
852
+ theme.sourceTheme?.styleSheet == null
853
+ ) {
854
+ fragments +=
855
+ PreparedProseFragment(
856
+ PreparedProseFragmentKind.BACKGROUND,
857
+ Rect(
858
+ theme.insetLeftPx + listInset + quoteInset,
859
+ cursorY,
860
+ theme.insetLeftPx + contentWidth - listInset - quoteInset,
861
+ totalEnd
862
+ ),
863
+ color = theme.codeBackground,
864
+ cornerRadius = theme.codeRadiusPx
865
+ )
866
+ }
867
+ fragments +=
868
+ PreparedProseFragment(
869
+ PreparedProseFragmentKind.TEXT,
870
+ Rect(
871
+ textX,
872
+ textTop,
873
+ textX + availableWidth,
874
+ textTop + textHeight
875
+ ),
876
+ layout,
877
+ textX,
878
+ textTop
879
+ )
880
+ attributed.semanticRanges.forEachIndexed { index, semantic ->
881
+ val firstLine = layout.getLineForOffset(semantic.start)
882
+ val lastLine = layout.getLineForOffset((semantic.end - 1).coerceAtLeast(semantic.start))
883
+ for (line in firstLine..lastLine) {
884
+ val lineStart = layout.getLineStart(line)
885
+ val lineEnd = layout.getLineEnd(line)
886
+ val start = maxOf(semantic.start, lineStart)
887
+ val end = minOf(semantic.end, lineEnd)
888
+ if (start >= end) continue
889
+ // StaticLayout's selection path follows its shaped visual runs.
890
+ // Clipping it to this line preserves discontiguous bidi pieces
891
+ // rather than collapsing a logical range to endpoint geometry.
892
+ mergeAdjacentSameLineSelectionFragments(
893
+ selectionRectsForLine(layout, start, end, line, availableWidth)
894
+ ).forEach { rect ->
895
+ rect.offset(textX, textTop)
896
+ // This semantic target's touching contours share one visual
897
+ // run. Gapped bidi contours remain separate hit regions.
898
+ interactionRects[index] += rect
899
+ }
900
+ }
901
+ }
902
+ attributed.atoms.forEach { atom ->
903
+ val line = layout.getLineForOffset(atom.start)
904
+ // Replacement spans consume a visual slot that can run right-to-left.
905
+ // The two endpoints, rather than a logical-start-plus-width guess,
906
+ // are the only hit/draw geometry published for the atom.
907
+ val visualStart = layout.getPrimaryHorizontal(atom.start)
908
+ val visualEnd = layout.getPrimaryHorizontal(atom.start + 1)
909
+ val atomLeft = textX + min(visualStart, visualEnd).toInt()
910
+ val atomRight = textX + max(visualStart, visualEnd).toInt()
911
+ val baseline = textTop + layout.getLineBaseline(line)
912
+ val atomTop =
913
+ baseline -
914
+ (atom.appearance.inset?.top?.toInt() ?: atom.appearance.paddingVertical) -
915
+ atom.labelBaselinePx
916
+ val bounds = Rect(
917
+ atomLeft,
918
+ atomTop,
919
+ max(atomLeft + 1, atomRight),
920
+ atomTop + atom.heightPx
921
+ )
922
+ fragments += PreparedProseFragment(
923
+ kind = PreparedProseFragmentKind.ATOM,
924
+ bounds = bounds,
925
+ labelLayout = atom.labelLayout,
926
+ labelX =
927
+ bounds.left +
928
+ (atom.appearance.inset?.left?.toInt() ?: atom.appearance.paddingHorizontal),
929
+ labelY =
930
+ bounds.top +
931
+ (atom.appearance.inset?.top?.toInt() ?: atom.appearance.paddingVertical),
932
+ color = atom.appearance.background,
933
+ borderColor = atom.appearance.borderColor,
934
+ cornerRadius = atom.appearance.radius,
935
+ strokeWidth = atom.appearance.borderWidth,
936
+ label = atom.label,
937
+ atomNodeType = atom.nodeType,
938
+ atomDocPos = atom.docPos,
939
+ atomAttrsJson = atom.attrsJson,
940
+ box = atom.appearance.box
941
+ )
942
+ }
943
+ if (block.inBlockquote &&
944
+ theme.sourceTheme?.styleSheet == null
945
+ ) {
946
+ fragments +=
947
+ PreparedProseFragment(
948
+ PreparedProseFragmentKind.BORDER,
949
+ Rect(
950
+ theme.insetLeftPx,
951
+ cursorY,
952
+ theme.insetLeftPx + theme.quoteBorderWidthPx,
953
+ totalEnd
954
+ ),
955
+ color = theme.quoteBorderColor
956
+ )
957
+ }
958
+ val firstLineTop = textTop + layout.getLineTop(0)
959
+ val firstLineBottom = textTop + layout.getLineBottom(0)
960
+ firstMarkers.forEach { (ancestor, marker) ->
961
+ fragments +=
962
+ markerFragment(
963
+ marker,
964
+ markerAnchor(ancestor),
965
+ firstLineTop,
966
+ firstLineBottom,
967
+ ancestorGutters.getValue(ancestor.identity),
968
+ theme.listMarkerColor
969
+ )
970
+ }
971
+ val interactions = attributed.semanticRanges.zip(
972
+ interactionRects
973
+ ).mapNotNull { (semantic, rects) ->
974
+ if (rects.isEmpty()) {
975
+ null
976
+ } else {
977
+ when (semantic) {
978
+ is PreparedSemanticRange.Link -> PreparedProseInteraction(
979
+ PreparedProseInteraction.Kind.LINK,
980
+ rects,
981
+ semantic.href,
982
+ semantic.text,
983
+ null,
984
+ semantic.text,
985
+ null
986
+ )
987
+
988
+ is PreparedSemanticRange.Mention -> PreparedProseInteraction(
989
+ PreparedProseInteraction.Kind.MENTION,
990
+ rects,
991
+ null,
992
+ semantic.label,
993
+ semantic.docPos,
994
+ semantic.label,
995
+ semantic.attrsJson
996
+ )
997
+ }
998
+ }
999
+ }
1000
+ return finishBlock(
1001
+ fragments,
1002
+ interactions,
1003
+ Rect(
1004
+ theme.insetLeftPx,
1005
+ cursorY,
1006
+ theme.insetLeftPx + contentWidth,
1007
+ totalEnd
1008
+ ),
1009
+ totalEnd,
1010
+ itemSpacing,
1011
+ attributed.retainedBytes
1012
+ ).copy(highlightedCodeKey = highlightedCodeKey)
1013
+ }
1014
+
1015
+ private fun codeText(block: ViewerBlock): String = block.inlines.joinToString("") {
1016
+ when (it) {
1017
+ is ViewerInline.Text -> it.text
1018
+
1019
+ is ViewerInline.Atom -> if (it.nodeType ==
1020
+ "hardBreak" ||
1021
+ it.nodeType == "hard_break"
1022
+ ) {
1023
+ "\n"
1024
+ } else {
1025
+ "\uFFFC"
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ private fun listItemAncestors(block: ViewerBlock): List<ViewerListItemAncestor> =
1031
+ block.listItemAncestors.ifEmpty {
1032
+ val boundary = block.listItemBoundary
1033
+ val context = block.listContext
1034
+ if (boundary == null || context == null) {
1035
+ emptyList()
1036
+ } else {
1037
+ listOf(
1038
+ ViewerListItemAncestor(
1039
+ boundary.identity,
1040
+ context,
1041
+ boundary.nestingDepth,
1042
+ boundary.isFirstRenderableLeaf,
1043
+ boundary.isFinalRenderableLeaf
1044
+ )
1045
+ )
1046
+ }
1047
+ }
1048
+
1049
+ private fun finishBlock(
1050
+ fragments: List<PreparedProseFragment>,
1051
+ interactions: List<PreparedProseInteraction>,
1052
+ seed: Rect,
1053
+ end: Int,
1054
+ spacing: Int,
1055
+ extraBytes: Long = 0
1056
+ ): BlockResult {
1057
+ val bounds = fragments.fold(seed) { acc, fragment ->
1058
+ Rect(acc).apply { union(fragment.bounds) }
1059
+ }
1060
+ return BlockResult(
1061
+ PreparedProseBlock(fragments.toList(), bounds),
1062
+ interactions,
1063
+ null,
1064
+ end + spacing,
1065
+ extraBytes
1066
+ )
1067
+ }
1068
+
1069
+ private fun selectionRectsForLine(
1070
+ layout: StaticLayout,
1071
+ start: Int,
1072
+ end: Int,
1073
+ line: Int,
1074
+ width: Int
1075
+ ): List<Rect> {
1076
+ val path = Path()
1077
+ layout.getSelectionPath(start, end, path)
1078
+ val lineClip = Rect(0, layout.getLineTop(line), width, layout.getLineBottom(line))
1079
+ val measure = PathMeasure(path, false)
1080
+ val result = mutableListOf<Rect>()
1081
+ do {
1082
+ if (measure.length == 0f) continue
1083
+ val contour = Path()
1084
+ measure.getSegment(0f, measure.length, contour, true)
1085
+ val bounds = RectF()
1086
+ contour.computeBounds(bounds, true)
1087
+ val piece = Rect(
1088
+ bounds.left.toInt(),
1089
+ bounds.top.toInt(),
1090
+ ceil(bounds.right).toInt(),
1091
+ ceil(bounds.bottom).toInt()
1092
+ )
1093
+ if (piece.intersect(lineClip) && !piece.isEmpty) result += piece
1094
+ } while (measure.nextContour())
1095
+ if (result.isEmpty()) {
1096
+ result += fallbackSelectionRectsForLine(layout, start, end, line, width)
1097
+ }
1098
+ return result.sortedWith(compareBy<Rect> { it.top }.thenBy { it.left })
1099
+ }
1100
+
1101
+ private data class AttributedBlock(
1102
+ val text: SpannableString,
1103
+ val atoms: List<PreparedAtomSpec>,
1104
+ val semanticRanges: List<PreparedSemanticRange>,
1105
+ val retainedBytes: Long
1106
+ )
1107
+ private sealed interface PreparedSemanticRange {
1108
+ val start: Int
1109
+ val end: Int
1110
+ data class Link(
1111
+ override val start: Int,
1112
+ override val end: Int,
1113
+ val href: String,
1114
+ val text: String
1115
+ ) : PreparedSemanticRange
1116
+ data class Mention(
1117
+ override val start: Int,
1118
+ override val end: Int,
1119
+ val docPos: Long,
1120
+ val label: String,
1121
+ val attrsJson: String
1122
+ ) : PreparedSemanticRange
1123
+ }
1124
+
1125
+ private fun attributed(
1126
+ inlines: List<ViewerInline>,
1127
+ base: PreparedTextPaint,
1128
+ theme: PreparedProseTheme,
1129
+ warningSemanticGeneration: String
1130
+ ): AttributedBlock {
1131
+ val source = StringBuilder()
1132
+ val spans = mutableListOf<(SpannableString) -> Unit>()
1133
+ val atoms = mutableListOf<PreparedAtomSpec>()
1134
+ val semanticRanges = mutableListOf<PreparedSemanticRange>()
1135
+ inlines.forEach { inline ->
1136
+ when (inline) {
1137
+ is ViewerInline.Text -> {
1138
+ val start = source.length
1139
+ source.append(inline.text)
1140
+ val end = source.length
1141
+ val markSpans = markSpans(inline.marks, base, theme, warningSemanticGeneration)
1142
+ spans +=
1143
+ { value ->
1144
+ markSpans.forEach {
1145
+ value.setSpan(
1146
+ it,
1147
+ start,
1148
+ end,
1149
+ android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1150
+ )
1151
+ }
1152
+ }
1153
+ href(inline.marks)?.let { href ->
1154
+ val previous = semanticRanges.lastOrNull() as? PreparedSemanticRange.Link
1155
+ if (previous != null && previous.href == href && previous.end == start) {
1156
+ semanticRanges[semanticRanges.lastIndex] =
1157
+ previous.copy(end = end, text = previous.text + inline.text)
1158
+ } else {
1159
+ semanticRanges +=
1160
+ PreparedSemanticRange.Link(start, end, href, inline.text)
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+ is ViewerInline.Atom -> {
1166
+ if (inline.nodeType == "hardBreak" ||
1167
+ inline.nodeType == "hard_break"
1168
+ ) {
1169
+ source.append('\n')
1170
+ } else {
1171
+ val appearance =
1172
+ atomAppearance(inline.nodeType, inline.attrsJson, base, theme)
1173
+ val label = inline.label.ifEmpty { " " }
1174
+ val labelPaint = appearance.paint.newTextPaint()
1175
+ val atomInset =
1176
+ appearance.inset
1177
+ ?: EditorEdges(
1178
+ appearance.paddingVertical.toFloat(),
1179
+ appearance.paddingHorizontal.toFloat(),
1180
+ appearance.paddingVertical.toFloat(),
1181
+ appearance.paddingHorizontal.toFloat()
1182
+ )
1183
+ val width =
1184
+ max(
1185
+ base.sizePx.toInt(),
1186
+ ceil(
1187
+ labelPaint.measureText(label) + atomInset.left + atomInset.right
1188
+ ).toInt()
1189
+ )
1190
+ val labelText = SpannableString(label).apply {
1191
+ if (theme.sourceTheme?.styleSheet !=
1192
+ null
1193
+ ) {
1194
+ appearance.paint.resolvedStyle?.let {
1195
+ setSpan(
1196
+ EditorResolvedTextSpan(
1197
+ it.copy(backgroundColor = null),
1198
+ theme.fontDensity
1199
+ ),
1200
+ 0,
1201
+ length,
1202
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1203
+ )
1204
+ }
1205
+ }
1206
+ }
1207
+ val labelLayout = staticLayout(
1208
+ labelText,
1209
+ appearance.paint,
1210
+ max(
1211
+ 1,
1212
+ width - (atomInset.left + atomInset.right).toInt()
1213
+ )
1214
+ )
1215
+ val labelMetrics = labelPaint.fontMetricsInt
1216
+ val labelAscent = max(0, -labelMetrics.ascent)
1217
+ val labelDescent = max(0, labelMetrics.descent)
1218
+ val ascent = labelAscent + atomInset.top.toInt()
1219
+ // Keep the label's descenders and any resolved line-height
1220
+ // expansion in the outer replacement metrics.
1221
+ val descent = labelDescent + atomInset.bottom.toInt()
1222
+ val metricDescent =
1223
+ descent +
1224
+ max(
1225
+ 0,
1226
+ labelLayout.height +
1227
+ (atomInset.top + atomInset.bottom).toInt() -
1228
+ ascent -
1229
+ descent
1230
+ )
1231
+ val height = ascent + metricDescent
1232
+ val start = source.length
1233
+ source.append('\uFFFC')
1234
+ atoms += PreparedAtomSpec(
1235
+ start = start,
1236
+ nodeType = inline.nodeType,
1237
+ docPos = inline.docPos,
1238
+ attrsJson = inline.attrsJson,
1239
+ label = label,
1240
+ appearance = appearance,
1241
+ widthPx = width,
1242
+ heightPx = height,
1243
+ labelLayout = labelLayout,
1244
+ labelBaselinePx = labelLayout.getLineBaseline(0)
1245
+ )
1246
+ spans +=
1247
+ { value ->
1248
+ value.setSpan(
1249
+ AtomMetricSpan(width, ascent, metricDescent),
1250
+ start,
1251
+ start + 1,
1252
+ android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1253
+ )
1254
+ }
1255
+ if (inline.nodeType ==
1256
+ "mention"
1257
+ ) {
1258
+ semanticRanges +=
1259
+ PreparedSemanticRange.Mention(
1260
+ start,
1261
+ start + 1,
1262
+ inline.docPos,
1263
+ label,
1264
+ inline.attrsJson
1265
+ )
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ val text = SpannableString(if (source.isEmpty()) "\u200B" else source.toString())
1272
+ spans.forEach { it(text) }
1273
+ return AttributedBlock(
1274
+ text,
1275
+ atoms,
1276
+ semanticRanges,
1277
+ 256L + text.length * 52L + atoms.sumOf { 256L + it.label.length * 2L }
1278
+ )
1279
+ }
1280
+
1281
+ private fun href(marks: List<uniffi.editor_core.FfiViewerMark>): String? = marks.firstOrNull {
1282
+ it.markType ==
1283
+ "link"
1284
+ }
1285
+ ?.let {
1286
+ runCatching {
1287
+ org.json.JSONObject(it.attrsJson).optString("href")
1288
+ }.getOrNull()?.takeIf(String::isNotEmpty)
1289
+ }
1290
+
1291
+ private fun markSpans(
1292
+ marks: List<uniffi.editor_core.FfiViewerMark>,
1293
+ base: PreparedTextPaint,
1294
+ theme: PreparedProseTheme,
1295
+ warningSemanticGeneration: String
1296
+ ): List<Any> {
1297
+ theme.sourceTheme?.styleSheet?.let { sheet ->
1298
+ var resolved = base.resolvedStyle ?: EditorTextStyle()
1299
+ val active = marks.map { com.apollohg.editor.canonicalMark(it.markType) }.toSet()
1300
+ listOf("inlineCode", "bold", "italic", "link", "underline", "strike").filter {
1301
+ it in
1302
+ active
1303
+ }.forEach {
1304
+ resolved =
1305
+ resolved.mergedWith(
1306
+ com.apollohg.editor.semanticText(it)
1307
+ ).mergedWith(sheet[it]?.text)
1308
+ }
1309
+ marks.forEach { mark ->
1310
+ val attrs = runCatching { org.json.JSONObject(mark.attrsJson) }.getOrNull()
1311
+ val override = when (mark.markType) {
1312
+ "textColor", "color", "foregroundColor" -> EditorTextStyle(
1313
+ color = parseColor(
1314
+ attrs?.optionalString("color") ?: attrs?.optionalString("textColor")
1315
+ )
1316
+ )
1317
+
1318
+ "highlight", "backgroundColor" -> EditorTextStyle(
1319
+ backgroundColor = parseColor(
1320
+ attrs?.optionalString(
1321
+ "color"
1322
+ ) ?: attrs?.optionalString("backgroundColor")
1323
+ )
1324
+ )
1325
+
1326
+ "textStyle", "font" -> EditorTextStyle(
1327
+ fontFamily = attrs?.optionalString("fontFamily"),
1328
+ fontSize = attrs?.optDouble("fontSize", Double.NaN)?.takeIf {
1329
+ it.isFinite() &&
1330
+ it > 0
1331
+ }?.toFloat()
1332
+ )
1333
+
1334
+ else -> null
1335
+ }
1336
+ resolved = resolved.mergedWith(override)
1337
+ }
1338
+ return listOf(EditorResolvedTextSpan(resolved, theme.fontDensity))
1339
+ }
1340
+ var explicitColor: Int? = null
1341
+ var background: Int? = null
1342
+ var underline = false
1343
+ var bold = false
1344
+ var italic = false
1345
+ var monospace = false
1346
+ var strike = false
1347
+ var size: Float? = null
1348
+ var family: String? = null
1349
+ var hasLink = false
1350
+ var link: EditorLinkTheme? = null
1351
+ marks.forEach { mark ->
1352
+ val attrs = runCatching { org.json.JSONObject(mark.attrsJson) }.getOrNull()
1353
+ when (mark.markType) {
1354
+ "bold", "strong" -> bold = true
1355
+
1356
+ "italic", "em" -> italic = true
1357
+
1358
+ "underline" -> underline = true
1359
+
1360
+ "strike", "strikethrough" -> strike = true
1361
+
1362
+ "code" -> monospace = true
1363
+
1364
+ "link" -> {
1365
+ hasLink = true
1366
+ link = theme.link
1367
+ underline = underline || (theme.link?.underline ?: true)
1368
+ background = theme.link?.backgroundColor ?: background
1369
+ }
1370
+
1371
+ "textColor", "color", "foregroundColor" ->
1372
+ explicitColor =
1373
+ parseColor(
1374
+ attrs?.optionalString("color") ?: attrs?.optionalString("textColor")
1375
+ )
1376
+ ?: explicitColor
1377
+
1378
+ "highlight", "backgroundColor" ->
1379
+ background =
1380
+ parseColor(
1381
+ attrs?.optionalString("color")
1382
+ ?: attrs?.optionalString("backgroundColor")
1383
+ )
1384
+ ?: background
1385
+
1386
+ "textStyle", "font" -> {
1387
+ family = attrs?.optionalString("fontFamily") ?: family
1388
+ size =
1389
+ attrs?.optDouble("fontSize", Double.NaN)?.takeIf {
1390
+ it.isFinite() && it > 0
1391
+ }?.toFloat()
1392
+ ?: size
1393
+ }
1394
+ }
1395
+ }
1396
+ // Resolve link family/size/weight/style first, then combine explicit
1397
+ // mark traits into one immutable metric span before StaticLayout sees it.
1398
+ var resolved =
1399
+ link?.let {
1400
+ base.withStyle(it.asTextStyle(), theme.fontDensity, warningSemanticGeneration)
1401
+ }
1402
+ ?: base
1403
+ if (family != null || size != null) {
1404
+ resolved =
1405
+ resolved.withStyle(
1406
+ EditorTextStyle(fontFamily = family, fontSize = size),
1407
+ theme.fontDensity,
1408
+ warningSemanticGeneration
1409
+ )
1410
+ }
1411
+ if (monospace) {
1412
+ resolved =
1413
+ resolved.withStyle(
1414
+ EditorTextStyle(fontFamily = "monospace"),
1415
+ theme.fontDensity,
1416
+ warningSemanticGeneration
1417
+ )
1418
+ }
1419
+ if (bold || italic) {
1420
+ resolved = resolved.withStyle(
1421
+ EditorTextStyle(
1422
+ fontWeight = if (bold) "bold" else null,
1423
+ fontStyle = if (italic) "italic" else null
1424
+ ),
1425
+ theme.fontDensity,
1426
+ warningSemanticGeneration
1427
+ )
1428
+ }
1429
+ val result = mutableListOf<Any>(ResolvedTextStyleSpan(resolved.typeface, resolved.sizePx))
1430
+ result +=
1431
+ ForegroundColorSpan(
1432
+ explicitColor ?: if (hasLink) link?.color ?: 0xFF007AFF.toInt() else base.color
1433
+ )
1434
+ background?.let { result += BackgroundColorSpan(it) }
1435
+ if (underline) result += UnderlineSpan()
1436
+ // Android shapes this with the run's resolved foreground and visual bidi
1437
+ // order, unlike manually derived logical-horizontal strike rectangles.
1438
+ if (strike) result += StrikethroughSpan()
1439
+ return result
1440
+ }
1441
+
1442
+ private fun staticLayout(
1443
+ text: CharSequence,
1444
+ paint: PreparedTextPaint,
1445
+ width: Int
1446
+ ): StaticLayout {
1447
+ staticLayoutsBuilt += 1
1448
+ val resolved = paint.newTextPaint()
1449
+ val preparedText = SpannableString(text).apply {
1450
+ // The full prepared range includes a single line and the final line
1451
+ // after a hard break; builder line spacing does not provide that
1452
+ // guarantee and would double-compensate these metrics.
1453
+ if (getSpans(0, length, EditorResolvedTextSpan::class.java).isNotEmpty()) {
1454
+ setSpan(
1455
+ com.apollohg.editor.EditorStyledLineMetricsSpan(),
1456
+ 0,
1457
+ length,
1458
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1459
+ )
1460
+ } else {
1461
+ paint.lineHeightPx?.let {
1462
+ setSpan(
1463
+ FixedLineHeightMetricSpan(it),
1464
+ 0,
1465
+ length,
1466
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE
1467
+ )
1468
+ }
1469
+ }
1470
+ paint.textAlign?.let { applyPhysicalTextAlignment(it) }
1471
+ }
1472
+ return StaticLayout.Builder.obtain(preparedText, 0, preparedText.length, resolved, width)
1473
+ .setAlignment(Layout.Alignment.ALIGN_NORMAL)
1474
+ .apply {
1475
+ if (android.os.Build.VERSION.SDK_INT >=
1476
+ 26
1477
+ ) {
1478
+ setJustificationMode(
1479
+ if (paint.textAlign ==
1480
+ "justify"
1481
+ ) {
1482
+ Layout.JUSTIFICATION_MODE_INTER_WORD
1483
+ } else {
1484
+ Layout.JUSTIFICATION_MODE_NONE
1485
+ }
1486
+ )
1487
+ }
1488
+ }
1489
+ .setIncludePad(false)
1490
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
1491
+ .build()
1492
+ }
1493
+
1494
+ private fun markerFor(
1495
+ context: ViewerListContext,
1496
+ nestingDepth: Int,
1497
+ textPaint: PreparedTextPaint,
1498
+ theme: PreparedProseTheme
1499
+ ): PreparedMarker {
1500
+ if (context.kind == "task" && theme.sourceTheme?.styleSheet != null) {
1501
+ val checkbox = com.apollohg.editor.resolvedCheckboxStyle(
1502
+ theme.sourceTheme.styleSheet,
1503
+ context.checked
1504
+ )
1505
+ val side = ((checkbox.size ?: 18f) * theme.density).toInt()
1506
+ return PreparedMarker(
1507
+ null,
1508
+ "",
1509
+ side,
1510
+ side,
1511
+ side / 2,
1512
+ 0,
1513
+ context.checked,
1514
+ checkbox.copy(box = checkbox.box.scaled(theme.density))
1515
+ )
1516
+ }
1517
+ val label = when {
1518
+ context.kind == "task" -> ""
1519
+
1520
+ context.ordered -> OrderedListMarkerFormatter.label(
1521
+ context.index,
1522
+ nestingDepth,
1523
+ theme.orderedListMarker
1524
+ )
1525
+
1526
+ else -> "•"
1527
+ }
1528
+ val markerScale = if (!context.ordered &&
1529
+ context.kind != "task"
1530
+ ) {
1531
+ max(0.01f, theme.listMarkerScale)
1532
+ } else {
1533
+ 1f
1534
+ }
1535
+ val markerPaint = textPaint.copy(
1536
+ sizePx = max(1f, textPaint.sizePx * markerScale),
1537
+ color = theme.listMarkerColor
1538
+ )
1539
+ if (label.isEmpty()) {
1540
+ val side = max(
1541
+ markerPaint.sizePx.toInt(),
1542
+ markerPaint.newTextPaint().fontMetricsInt.descent -
1543
+ markerPaint.newTextPaint().fontMetricsInt.ascent
1544
+ )
1545
+ return PreparedMarker(null, label, side, side, side / 2, 0, context.checked)
1546
+ }
1547
+ val text = markerPaint.newTextPaint()
1548
+ val layout =
1549
+ staticLayout(
1550
+ SpannableString(label),
1551
+ markerPaint,
1552
+ max(1, ceil(text.measureText(label)).toInt())
1553
+ )
1554
+ val glyphBounds = Rect()
1555
+ text.getTextBounds(label, 0, label.length, glyphBounds)
1556
+ val ink = preparedMarkerInk(glyphBounds, layout.getLineAscent(0), layout.getLineDescent(0))
1557
+ return PreparedMarker(
1558
+ layout,
1559
+ label,
1560
+ layout.width,
1561
+ ink.heightPx,
1562
+ ink.ascentPx,
1563
+ layout.getLineBaseline(0),
1564
+ context.checked
1565
+ )
1566
+ }
1567
+
1568
+ private fun markerFragment(
1569
+ marker: PreparedMarker,
1570
+ textX: Int,
1571
+ verticalTop: Int,
1572
+ verticalBottom: Int,
1573
+ gutter: Int,
1574
+ color: Int
1575
+ ): PreparedProseFragment {
1576
+ val x = textX - gutter
1577
+ val markerTop = verticalTop + (verticalBottom - verticalTop - marker.heightPx) / 2
1578
+ val layoutY = markerTop + marker.ascentPx - marker.baselinePx
1579
+ return PreparedProseFragment(
1580
+ PreparedProseFragmentKind.MARKER,
1581
+ Rect(
1582
+ x,
1583
+ markerTop,
1584
+ x + marker.widthPx,
1585
+ markerTop + marker.heightPx
1586
+ ),
1587
+ marker.layout,
1588
+ x,
1589
+ layoutY,
1590
+ color = color,
1591
+ label = marker.label,
1592
+ checked = marker.checked,
1593
+ box = marker.checkbox?.box,
1594
+ borderColor = marker.checkbox?.checkColor
1595
+ )
1596
+ }
1597
+
1598
+ private fun atomAppearance(
1599
+ nodeType: String,
1600
+ attrsJson: String,
1601
+ base: PreparedTextPaint,
1602
+ theme: PreparedProseTheme
1603
+ ): PreparedAtomAppearance {
1604
+ if (nodeType == "mention") {
1605
+ val values = runCatching { org.json.JSONObject(attrsJson) }.getOrNull()
1606
+ val local = EditorMentionTheme.fromJson(values?.optJSONObject("mentionTheme"))
1607
+ if (theme.sourceTheme?.styleSheet != null) {
1608
+ val element = com.apollohg.editor.resolvedMentionStyle(
1609
+ base.resolvedStyle ?: EditorTextStyle(),
1610
+ theme.sourceTheme,
1611
+ local
1612
+ )
1613
+ val box = element.box.scaled(theme.density)
1614
+ return PreparedAtomAppearance(
1615
+ base.withStyle(element.text, theme.fontDensity, "mention"),
1616
+ box.backgroundColor ?: 0, null, 0f, 0f, 0, 0, box, box.inset
1617
+ )
1618
+ }
1619
+ val mention = (theme.mention?.mergedWith(local) ?: local)?.node
1620
+ val weighted =
1621
+ mention?.fontWeight?.let { EditorTextStyle(fontWeight = it).typefaceStyle() }
1622
+ ?: base.typeface.style
1623
+ return PreparedAtomAppearance(
1624
+ base.copy(
1625
+ typeface = Typeface.create(base.typeface, weighted),
1626
+ color =
1627
+ mention?.textColor ?: base.color
1628
+ ),
1629
+ mention?.backgroundColor ?: 0x1F007AFF,
1630
+ mention?.borderColor,
1631
+ max(0f, (mention?.borderWidth ?: 0f) * theme.density),
1632
+ max(0f, (mention?.borderRadius ?: 6f) * theme.density),
1633
+ theme.atomPaddingHorizontalPx,
1634
+ theme.atomPaddingVerticalPx
1635
+ )
1636
+ }
1637
+ return PreparedAtomAppearance(
1638
+ base,
1639
+ 0xFFF2F2F7.toInt(),
1640
+ null,
1641
+ 0f,
1642
+ 5f * theme.density,
1643
+ theme.atomPaddingHorizontalPx,
1644
+ theme.atomPaddingVerticalPx
1645
+ )
1646
+ }
1647
+ }
1648
+
1649
+ internal fun Typeface.familyName(): String? = when (this) {
1650
+ Typeface.MONOSPACE -> "monospace"
1651
+ Typeface.SERIF -> "serif"
1652
+ Typeface.SANS_SERIF -> "sans"
1653
+ else -> null
1654
+ }
1655
+ private fun parseColor(raw: String?): Int? =
1656
+ runCatching { raw?.takeIf { it.isNotBlank() }?.let(Color::parseColor) }.getOrNull()