@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,971 @@
1
+ import CoreText
2
+ import UIKit
3
+
4
+ /// Draws list markers visually in the gutter without inserting them into the
5
+ /// editable text storage. This keeps UIKit paragraph-start behaviors, such as
6
+ /// sentence auto-capitalization, working naturally inside list items.
7
+ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
8
+ override init() {
9
+ super.init()
10
+ delegate = self
11
+ }
12
+
13
+ required init?(coder: NSCoder) {
14
+ super.init(coder: coder)
15
+ delegate = self
16
+ }
17
+
18
+ func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes: UnsafePointer<Int>, font: UIFont, forGlyphRange glyphRange: NSRange) -> Int {
19
+ guard let storage = textStorage else { return 0 }
20
+ // Reserve one chip glyph while retaining every canonical UTF-16 offset.
21
+ var properties = Array(UnsafeBufferPointer(start: props, count: glyphRange.length))
22
+ var changed = false
23
+ for index in 0..<glyphRange.length {
24
+ var range = NSRange()
25
+ guard storage.attribute(editorMentionBoxAttribute, at: characterIndexes[index], effectiveRange: &range) is EditorMentionRenderedBox else { continue }
26
+ properties[index] = characterIndexes[index] == range.location && (index == 0 || characterIndexes[index - 1] != range.location) ? .controlCharacter : .null
27
+ changed = true
28
+ }
29
+ guard changed else { return 0 }
30
+ setGlyphs(glyphs, properties: properties, characterIndexes: characterIndexes, font: font, forGlyphRange: glyphRange)
31
+ return glyphRange.length
32
+ }
33
+
34
+ func layoutManager(_ layoutManager: NSLayoutManager, shouldUse action: NSLayoutManager.ControlCharacterAction, forControlCharacterAt charIndex: Int) -> NSLayoutManager.ControlCharacterAction {
35
+ textStorage?.attribute(editorMentionBoxAttribute, at: charIndex, effectiveRange: nil) is EditorMentionRenderedBox ? .whitespace : action
36
+ }
37
+
38
+ func layoutManager(_ layoutManager: NSLayoutManager, boundingBoxForControlGlyphAt glyphIndex: Int, for textContainer: NSTextContainer, proposedLineFragment proposedRect: CGRect, glyphPosition: CGPoint, characterIndex charIndex: Int) -> CGRect {
39
+ guard let chip = textStorage?.attribute(editorMentionBoxAttribute, at: charIndex, effectiveRange: nil) as? EditorMentionRenderedBox else { return .zero }
40
+ return CGRect(origin: .zero, size: chip.size)
41
+ }
42
+
43
+ func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool {
44
+ guard let storage = textStorage, charIndex < storage.length else { return true }
45
+ var range = NSRange()
46
+ guard storage.attribute(editorMentionBoxAttribute, at: charIndex, effectiveRange: &range) is EditorMentionRenderedBox else { return true }
47
+ return charIndex == range.location
48
+ }
49
+
50
+ func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<CGRect>, lineFragmentUsedRect: UnsafeMutablePointer<CGRect>, baselineOffset: UnsafeMutablePointer<CGFloat>, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool {
51
+ guard let storage = textStorage, storage.length > 0 else { return false }
52
+ let characters = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
53
+ var height = lineFragmentUsedRect.pointee.height
54
+ storage.enumerateAttribute(editorInlineLineHeightAttribute, in: characters) { value, _, _ in
55
+ height = max(height, EditorTheme.cgFloat(value) ?? 0)
56
+ }
57
+ let extra = height - lineFragmentUsedRect.pointee.height
58
+ var leading: CGFloat = 0
59
+ var overlap: CGFloat = 0
60
+ if characters.location < storage.length,
61
+ storage.attribute(editorStyledContentAttribute, at: characters.location, effectiveRange: nil) != nil,
62
+ let style = storage.attribute(.paragraphStyle, at: characters.location, effectiveRange: nil) as? NSParagraphStyle {
63
+ let paragraph = (storage.string as NSString).paragraphRange(for: NSRange(location: characters.location, length: 0))
64
+ if characters.location == paragraph.location {
65
+ if glyphRange.location == 0 {
66
+ leading = style.paragraphSpacingBefore
67
+ } else {
68
+ let previous = storage.attribute(.paragraphStyle, at: characters.location - 1, effectiveRange: nil) as? NSParagraphStyle
69
+ overlap = min(0, style.paragraphSpacingBefore) + min(0, previous?.paragraphSpacing ?? 0)
70
+ }
71
+ }
72
+ }
73
+ guard extra != 0 || leading != 0 || overlap != 0 else { return false }
74
+ // TextKit clamps negative paragraph spacing; move the following fragment.
75
+ lineFragmentRect.pointee.origin.y += overlap
76
+ lineFragmentRect.pointee.size.height += extra + leading
77
+ lineFragmentUsedRect.pointee.size.height = height
78
+ lineFragmentUsedRect.pointee.origin.y += leading + overlap
79
+ baselineOffset.pointee += extra / 2 + leading
80
+ return true
81
+ }
82
+
83
+ override func usedRect(for textContainer: NSTextContainer) -> CGRect {
84
+ var rect = super.usedRect(for: textContainer)
85
+ if let storage = textStorage, storage.length > 0,
86
+ storage.attribute(editorStyledContentAttribute, at: storage.length - 1, effectiveRange: nil) != nil,
87
+ let paragraph = storage.attribute(.paragraphStyle, at: storage.length - 1, effectiveRange: nil) as? NSParagraphStyle {
88
+ rect.size.height += paragraph.paragraphSpacing
89
+ }
90
+ return rect
91
+ }
92
+ private(set) var blockquoteStripeDrawPassesForTesting: [[CGRect]] = []
93
+ private(set) var codeBlockDrawPassesForTesting: [[CGRect]] = []
94
+
95
+ func blockquoteStripeRectsForTesting(
96
+ in textStorage: NSTextStorage,
97
+ visibleGlyphRange: NSRange? = nil,
98
+ origin: CGPoint = .zero
99
+ ) -> [CGRect] {
100
+ let glyphsToShow = visibleGlyphRange ?? NSRange(location: 0, length: numberOfGlyphs)
101
+ guard glyphsToShow.length > 0 else { return [] }
102
+
103
+ let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
104
+ let nsString = textStorage.string as NSString
105
+ var drawnBlockquoteStarts = Set<Int>()
106
+ var rects: [CGRect] = []
107
+
108
+ textStorage.enumerateAttribute(
109
+ RenderBridgeAttributes.blockquoteBorderColor,
110
+ in: characterRange,
111
+ options: [.longestEffectiveRangeNotRequired]
112
+ ) { value, range, _ in
113
+ guard range.length > 0, let color = value as? UIColor else { return }
114
+
115
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
116
+ let paragraphStart = paragraphRange.location
117
+ let groupRange = Self.blockquoteGroupCharacterRange(
118
+ containing: paragraphStart,
119
+ in: textStorage,
120
+ nsString: nsString
121
+ )
122
+ let groupStart = groupRange.location
123
+ guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
124
+ guard let rect = blockquoteStripeRect(
125
+ characterRange: groupRange,
126
+ color: color,
127
+ textStorage: textStorage,
128
+ origin: origin
129
+ ) else {
130
+ return
131
+ }
132
+ rects.append(rect)
133
+ }
134
+
135
+ return rects
136
+ }
137
+
138
+ func resetBlockquoteStripeDrawPassesForTesting() {
139
+ blockquoteStripeDrawPassesForTesting.removeAll()
140
+ }
141
+
142
+ func resetCodeBlockDrawPassesForTesting() {
143
+ codeBlockDrawPassesForTesting.removeAll()
144
+ }
145
+
146
+ override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
147
+ guard let textStorage, glyphsToShow.length > 0 else { return }
148
+
149
+ drawStyleBoxes(in: textStorage, glyphsToShow: glyphsToShow, origin: origin)
150
+ drawMentionBoxes(in: textStorage, glyphsToShow: glyphsToShow, origin: origin)
151
+ drawCodeBlockBackgrounds(
152
+ in: textStorage,
153
+ glyphsToShow: glyphsToShow,
154
+ origin: origin
155
+ )
156
+ super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
157
+
158
+ let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
159
+ let nsString = textStorage.string as NSString
160
+ var drawnParagraphStarts = Set<Int>()
161
+ var drawnBlockquoteStarts = Set<Int>()
162
+ var drawnStripeRects: [CGRect] = []
163
+
164
+ textStorage.enumerateAttribute(
165
+ RenderBridgeAttributes.listMarkerContext,
166
+ in: characterRange,
167
+ options: [.longestEffectiveRangeNotRequired]
168
+ ) { value, range, _ in
169
+ guard range.length > 0, let listContext = value as? [String: Any] else { return }
170
+
171
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
172
+ let paragraphStart = paragraphRange.location
173
+ guard !Self.isParagraphStartCreatedByHardBreak(paragraphStart, in: textStorage) else {
174
+ return
175
+ }
176
+ guard drawnParagraphStarts.insert(paragraphStart).inserted else { return }
177
+
178
+ self.drawListMarker(
179
+ listContext: listContext,
180
+ paragraphStart: paragraphStart,
181
+ origin: origin,
182
+ textStorage: textStorage
183
+ )
184
+ }
185
+
186
+ textStorage.enumerateAttribute(
187
+ RenderBridgeAttributes.blockquoteBorderColor,
188
+ in: characterRange,
189
+ options: [.longestEffectiveRangeNotRequired]
190
+ ) { value, range, _ in
191
+ guard range.length > 0, let color = value as? UIColor else { return }
192
+
193
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
194
+ let paragraphStart = paragraphRange.location
195
+ let groupRange = Self.blockquoteGroupCharacterRange(
196
+ containing: paragraphStart,
197
+ in: textStorage,
198
+ nsString: nsString
199
+ )
200
+ let groupStart = groupRange.location
201
+ guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
202
+
203
+ guard let stripeRect = self.blockquoteStripeRect(
204
+ characterRange: groupRange,
205
+ color: color,
206
+ textStorage: textStorage,
207
+ origin: origin
208
+ ) else {
209
+ return
210
+ }
211
+ self.drawBlockquoteBorder(
212
+ stripeRect: stripeRect,
213
+ color: color
214
+ )
215
+ drawnStripeRects.append(stripeRect)
216
+ }
217
+
218
+ if !drawnStripeRects.isEmpty {
219
+ blockquoteStripeDrawPassesForTesting.append(drawnStripeRects)
220
+ }
221
+ }
222
+
223
+ private func drawMentionBoxes(in storage: NSTextStorage, glyphsToShow: NSRange, origin: CGPoint) {
224
+ guard let context = UIGraphicsGetCurrentContext() else { return }
225
+ let visible = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
226
+ storage.enumerateAttribute(editorMentionBoxAttribute, in: visible) { value, range, _ in
227
+ guard let box = value as? EditorMentionRenderedBox else { return }
228
+ let glyph = self.glyphIndexForCharacter(at: range.location)
229
+ let line = self.lineFragmentUsedRect(forGlyphAt: glyph, effectiveRange: nil)
230
+ let location = self.location(forGlyphAt: glyph)
231
+ let fragment = self.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
232
+ let rect = CGRect(x: fragment.minX + location.x + origin.x, y: line.midY - box.size.height / 2 + origin.y, width: box.size.width, height: box.size.height)
233
+ box.box.draw(in: rect, context: context)
234
+ let labelHeight = box.label?.size().height ?? 0
235
+ box.label?.draw(at: CGPoint(x: rect.minX + box.padding.left,
236
+ y: rect.minY + box.padding.top + (rect.height - box.padding.top - box.padding.bottom - labelHeight) / 2))
237
+ }
238
+ }
239
+
240
+ private func drawStyleBoxes(in storage: NSTextStorage, glyphsToShow: NSRange, origin: CGPoint) {
241
+ guard let context = UIGraphicsGetCurrentContext(), let container = textContainers.first else { return }
242
+ let visible = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
243
+ var groups: [ObjectIdentifier: (EditorRenderedBox, NSRange)] = [:]
244
+ storage.enumerateAttribute(editorStyleBoxesAttribute, in: NSRange(location: 0, length: storage.length)) { value, range, _ in
245
+ for box in value as? [EditorRenderedBox] ?? [] {
246
+ let id = ObjectIdentifier(box)
247
+ groups[id] = (box, groups[id].map { NSUnionRange($0.1, range) } ?? range)
248
+ }
249
+ }
250
+ for (descriptor, range) in groups.values.sorted(by: { $0.0.depth < $1.0.depth }) where NSIntersectionRange(range, visible).length > 0 {
251
+ let glyphs = glyphRange(forCharacterRange: range, actualCharacterRange: nil)
252
+ var union = CGRect.null
253
+ enumerateLineFragments(forGlyphRange: glyphs) { line, used, _, _, _ in
254
+ union = union.union(used.height > 0 ? used : line)
255
+ }
256
+ guard !union.isNull else { continue }
257
+ let rect = CGRect(x: origin.x + descriptor.leading + container.lineFragmentPadding,
258
+ y: origin.y + union.minY - descriptor.topInset,
259
+ width: max(0, container.size.width - descriptor.leading - descriptor.trailing - container.lineFragmentPadding * 2),
260
+ height: union.height + descriptor.topInset + descriptor.bottomInset)
261
+ descriptor.box.draw(in: rect, context: context)
262
+ }
263
+ }
264
+
265
+ func taskListMarkerParagraphStart(
266
+ at point: CGPoint,
267
+ in textStorage: NSTextStorage,
268
+ textContainerOrigin: CGPoint
269
+ ) -> Int? {
270
+ guard numberOfGlyphs > 0, let container = textContainers.first else { return nil }
271
+
272
+ // Resolve the touched line first — O(1) instead of walking every
273
+ // task item in the document on every touch.
274
+ let containerPoint = CGPoint(
275
+ x: point.x - textContainerOrigin.x,
276
+ y: point.y - textContainerOrigin.y
277
+ )
278
+ let glyphIndex = self.glyphIndex(for: containerPoint, in: container)
279
+ if let hit = taskListMarkerParagraphStart(
280
+ forParagraphContainingGlyphAt: glyphIndex,
281
+ tapPoint: point,
282
+ in: textStorage,
283
+ textContainerOrigin: textContainerOrigin
284
+ ) {
285
+ return hit
286
+ }
287
+
288
+ // The tap-slop inset (dx: -10, dy: -8) below, plus the checkbox
289
+ // itself already being taller than one line's pitch, means the
290
+ // marker rect for the PREVIOUS or NEXT line can still contain
291
+ // `point` even though `point` glyph-resolves to a different line.
292
+ // Probe both neighboring lines by nudging just past the resolved
293
+ // line's own fragment bounds — this reaches the adjacent paragraph
294
+ // regardless of how deep `point` sits within its own line, and it
295
+ // stays O(1) (two extra glyph lookups).
296
+ let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
297
+
298
+ if lineFragmentRect.minY > 0 {
299
+ let aboveGlyphIndex = self.glyphIndex(
300
+ for: CGPoint(x: containerPoint.x, y: lineFragmentRect.minY - 1),
301
+ in: container
302
+ )
303
+ if let hit = taskListMarkerParagraphStart(
304
+ forParagraphContainingGlyphAt: aboveGlyphIndex,
305
+ tapPoint: point,
306
+ in: textStorage,
307
+ textContainerOrigin: textContainerOrigin
308
+ ) {
309
+ return hit
310
+ }
311
+ }
312
+
313
+ let belowGlyphIndex = self.glyphIndex(
314
+ for: CGPoint(x: containerPoint.x, y: lineFragmentRect.maxY + 1),
315
+ in: container
316
+ )
317
+ if let hit = taskListMarkerParagraphStart(
318
+ forParagraphContainingGlyphAt: belowGlyphIndex,
319
+ tapPoint: point,
320
+ in: textStorage,
321
+ textContainerOrigin: textContainerOrigin
322
+ ) {
323
+ return hit
324
+ }
325
+
326
+ return nil
327
+ }
328
+
329
+ /// Resolves the paragraph containing `glyphIndex` and, if it is a task
330
+ /// item's paragraph start, checks whether that item's marker rect
331
+ /// contains `tapPoint`.
332
+ private func taskListMarkerParagraphStart(
333
+ forParagraphContainingGlyphAt glyphIndex: Int,
334
+ tapPoint: CGPoint,
335
+ in textStorage: NSTextStorage,
336
+ textContainerOrigin: CGPoint
337
+ ) -> Int? {
338
+ let charIndex = characterIndexForGlyph(at: glyphIndex)
339
+ guard charIndex < textStorage.length else { return nil }
340
+
341
+ let nsString = textStorage.string as NSString
342
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: charIndex, length: 0))
343
+ let paragraphStart = paragraphRange.location
344
+ guard paragraphStart < textStorage.length else { return nil }
345
+ guard !Self.isParagraphStartCreatedByHardBreak(paragraphStart, in: textStorage) else { return nil }
346
+
347
+ guard let listContext = textStorage.attribute(
348
+ RenderBridgeAttributes.listMarkerContext,
349
+ at: paragraphStart,
350
+ effectiveRange: nil
351
+ ) as? [String: Any],
352
+ (listContext["kind"] as? String) == "task"
353
+ else { return nil }
354
+
355
+ let startGlyphIndex = glyphIndexForCharacter(at: paragraphStart)
356
+ guard startGlyphIndex < numberOfGlyphs else { return nil }
357
+
358
+ let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
359
+ let baseFont = Self.markerBaseFont(from: attrs)
360
+ let markerWidth = (attrs[RenderBridgeAttributes.listMarkerWidth] as? NSNumber)
361
+ .map { CGFloat(truncating: $0) }
362
+ ?? LayoutConstants.listMarkerWidth
363
+ let markerGap = (attrs[RenderBridgeAttributes.listMarkerGap] as? NSNumber)
364
+ .map { CGFloat(truncating: $0) }
365
+ ?? LayoutConstants.listMarkerTextGap
366
+
367
+ var lineGlyphRange = NSRange()
368
+ let usedRect = lineFragmentUsedRect(forGlyphAt: startGlyphIndex, effectiveRange: &lineGlyphRange)
369
+ let lineFragmentRect = self.lineFragmentRect(forGlyphAt: startGlyphIndex, effectiveRange: nil)
370
+ let glyphLocation = location(forGlyphAt: startGlyphIndex)
371
+ let baselineY = lineFragmentRect.minY + glyphLocation.y
372
+ let markerRect = Self.taskMarkerDrawingRect(
373
+ usedRect: usedRect,
374
+ lineFragmentRect: lineFragmentRect,
375
+ markerWidth: markerWidth,
376
+ baselineY: baselineY,
377
+ baseFont: baseFont,
378
+ origin: textContainerOrigin,
379
+ markerGap: markerGap,
380
+ explicitSize: (attrs[editorTaskCheckboxAttribute] as? EditorMentionRenderedBox)?.box.number("size", fallback: 24)
381
+ ).insetBy(dx: -10, dy: -8)
382
+
383
+ return markerRect.contains(tapPoint) ? paragraphStart : nil
384
+ }
385
+
386
+ private func drawCodeBlockBackgrounds(
387
+ in textStorage: NSTextStorage,
388
+ glyphsToShow: NSRange,
389
+ origin: CGPoint
390
+ ) {
391
+ let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
392
+ let nsString = textStorage.string as NSString
393
+ var drawnBlockStarts = Set<Int>()
394
+ var drawnCodeBlockRects: [CGRect] = []
395
+
396
+ textStorage.enumerateAttribute(
397
+ RenderBridgeAttributes.codeBlockBackgroundColor,
398
+ in: characterRange,
399
+ options: [.longestEffectiveRangeNotRequired]
400
+ ) { value, range, _ in
401
+ guard range.length > 0, let color = value as? UIColor else { return }
402
+
403
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
404
+ let paragraphStart = paragraphRange.location
405
+ let codeBlockRange = Self.codeBlockCharacterRange(
406
+ containing: paragraphStart,
407
+ in: textStorage,
408
+ nsString: nsString
409
+ )
410
+ guard drawnBlockStarts.insert(codeBlockRange.location).inserted else { return }
411
+
412
+ guard let rect = self.codeBlockRect(
413
+ characterRange: codeBlockRange,
414
+ textStorage: textStorage,
415
+ origin: origin
416
+ ) else {
417
+ return
418
+ }
419
+
420
+ let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
421
+ let radius = (attrs[RenderBridgeAttributes.codeBlockBorderRadius] as? NSNumber)
422
+ .map { CGFloat(truncating: $0) }
423
+ ?? 8
424
+
425
+ color.setFill()
426
+ UIBezierPath(roundedRect: rect, cornerRadius: radius).fill()
427
+ drawnCodeBlockRects.append(rect)
428
+ }
429
+
430
+ if !drawnCodeBlockRects.isEmpty {
431
+ codeBlockDrawPassesForTesting.append(drawnCodeBlockRects)
432
+ }
433
+ }
434
+
435
+ private func drawListMarker(
436
+ listContext: [String: Any],
437
+ paragraphStart: Int,
438
+ origin: CGPoint,
439
+ textStorage: NSTextStorage
440
+ ) {
441
+ guard paragraphStart < textStorage.length else { return }
442
+
443
+ let glyphIndex = glyphIndexForCharacter(at: paragraphStart)
444
+ guard glyphIndex < numberOfGlyphs else { return }
445
+
446
+ var lineGlyphRange = NSRange()
447
+ let usedRect = lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &lineGlyphRange)
448
+ let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
449
+ let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
450
+
451
+ let baseFont = Self.markerBaseFont(from: attrs)
452
+ let textColor = attrs[RenderBridgeAttributes.listMarkerColor] as? UIColor
453
+ ?? attrs[.foregroundColor] as? UIColor
454
+ ?? .label
455
+ let markerScale = (attrs[RenderBridgeAttributes.listMarkerScale] as? NSNumber)
456
+ .map { CGFloat(truncating: $0) }
457
+ ?? LayoutConstants.unorderedListMarkerFontScale
458
+ let markerWidth = (attrs[RenderBridgeAttributes.listMarkerWidth] as? NSNumber)
459
+ .map { CGFloat(truncating: $0) }
460
+ ?? LayoutConstants.listMarkerWidth
461
+ let markerGap = (attrs[RenderBridgeAttributes.listMarkerGap] as? NSNumber)
462
+ .map { CGFloat(truncating: $0) }
463
+ ?? LayoutConstants.listMarkerTextGap
464
+ let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
465
+ let isTask = (listContext["kind"] as? String) == "task"
466
+
467
+ let glyphLocation = location(forGlyphAt: glyphIndex)
468
+ let baselineY = lineFragmentRect.minY + glyphLocation.y
469
+
470
+ if isTask {
471
+ let checkboxRect = Self.taskMarkerDrawingRect(
472
+ usedRect: usedRect,
473
+ lineFragmentRect: lineFragmentRect,
474
+ markerWidth: markerWidth,
475
+ baselineY: baselineY,
476
+ baseFont: baseFont,
477
+ origin: origin,
478
+ markerGap: markerGap,
479
+ explicitSize: (attrs[editorTaskCheckboxAttribute] as? EditorMentionRenderedBox)?.box.number("size", fallback: 24)
480
+ )
481
+ if let box = attrs[editorTaskCheckboxAttribute] as? EditorMentionRenderedBox,
482
+ let context = UIGraphicsGetCurrentContext() {
483
+ EditorStyleSheet.drawCheckbox(box.box, in: checkboxRect, checked: listContext["checked"] as? Bool == true, context: context)
484
+ return
485
+ }
486
+ drawTaskCheckbox(
487
+ in: checkboxRect,
488
+ checked: (listContext["checked"] as? NSNumber)?.boolValue ?? false,
489
+ color: textColor
490
+ )
491
+ return
492
+ }
493
+
494
+ if ordered {
495
+ let markerFont = attrs[editorStyledContentAttribute] != nil
496
+ ? baseFont.withSize(baseFont.pointSize * markerScale)
497
+ : markerFont(for: listContext, baseFont: baseFont, markerScale: markerScale)
498
+ let markerText = attrs[RenderBridgeAttributes.orderedListMarkerLabel] as? String
499
+ ?? RenderBridge.listMarkerString(listContext: listContext)
500
+ .trimmingCharacters(in: .whitespaces)
501
+ let markerOrigin = Self.orderedMarkerDrawingOrigin(
502
+ usedRect: usedRect,
503
+ lineFragmentRect: lineFragmentRect,
504
+ markerWidth: markerWidth,
505
+ baselineY: baselineY,
506
+ markerFont: markerFont,
507
+ markerText: markerText,
508
+ origin: origin,
509
+ markerGap: markerGap
510
+ )
511
+ let markerAttrs: [NSAttributedString.Key: Any] = [
512
+ .font: markerFont,
513
+ .foregroundColor: textColor
514
+ ]
515
+ NSAttributedString(string: markerText, attributes: markerAttrs).draw(at: markerOrigin)
516
+ return
517
+ }
518
+
519
+ let bulletRect = Self.unorderedBulletDrawingRect(
520
+ usedRect: usedRect,
521
+ lineFragmentRect: lineFragmentRect,
522
+ markerWidth: markerWidth,
523
+ baselineY: baselineY,
524
+ baseFont: baseFont,
525
+ markerScale: markerScale,
526
+ origin: origin,
527
+ markerGap: markerGap
528
+ )
529
+ let path = UIBezierPath(ovalIn: bulletRect)
530
+ textColor.setFill()
531
+ path.fill()
532
+ }
533
+
534
+ private func blockquoteStripeRect(
535
+ characterRange: NSRange,
536
+ color: UIColor,
537
+ textStorage: NSTextStorage,
538
+ origin: CGPoint
539
+ ) -> CGRect? {
540
+ guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
541
+ return nil
542
+ }
543
+
544
+ ensureLayout(forCharacterRange: characterRange)
545
+ let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
546
+ guard glyphRange.length > 0 else { return nil }
547
+
548
+ var topEdge: CGFloat?
549
+ var bottomEdge: CGFloat?
550
+ var textLeadingEdge: CGFloat?
551
+ enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
552
+ let verticalReferenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
553
+ if let currentTop = topEdge {
554
+ topEdge = min(currentTop, lineFragmentRect.minY)
555
+ } else {
556
+ topEdge = lineFragmentRect.minY
557
+ }
558
+ if let currentBottom = bottomEdge {
559
+ bottomEdge = max(currentBottom, verticalReferenceRect.maxY)
560
+ } else {
561
+ bottomEdge = verticalReferenceRect.maxY
562
+ }
563
+ let referenceMinX = usedRect.width > 0 ? usedRect.minX : lineFragmentRect.minX
564
+ if let current = textLeadingEdge {
565
+ textLeadingEdge = min(current, referenceMinX)
566
+ } else {
567
+ textLeadingEdge = referenceMinX
568
+ }
569
+ }
570
+ guard let topEdge, let bottomEdge, bottomEdge > topEdge, let textLeadingEdge else { return nil }
571
+
572
+ let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
573
+ let borderWidth = (attrs[RenderBridgeAttributes.blockquoteBorderWidth] as? NSNumber)
574
+ .map { CGFloat(truncating: $0) }
575
+ ?? LayoutConstants.blockquoteBorderWidth
576
+ let gap = (attrs[RenderBridgeAttributes.blockquoteMarkerGap] as? NSNumber)
577
+ .map { CGFloat(truncating: $0) }
578
+ ?? LayoutConstants.blockquoteMarkerGap
579
+
580
+ let stripeX = origin.x + textLeadingEdge - gap - borderWidth
581
+ let stripeRect = CGRect(
582
+ x: stripeX,
583
+ y: origin.y + topEdge,
584
+ width: borderWidth,
585
+ height: bottomEdge - topEdge
586
+ )
587
+ return stripeRect
588
+ }
589
+
590
+ private func drawBlockquoteBorder(
591
+ stripeRect: CGRect,
592
+ color: UIColor
593
+ ) {
594
+ color.setFill()
595
+ UIBezierPath(rect: stripeRect).fill()
596
+ }
597
+
598
+ /// Walk contiguous paragraphs around `paragraphStart` while
599
+ /// `paragraphPredicate` holds. When `requireAttributedJoin` is non-nil,
600
+ /// a neighboring paragraph only joins the group if the newline character
601
+ /// separating the two paragraphs ALSO carries that attribute — this is
602
+ /// what distinguishes intra-block newlines (attributed) from the
603
+ /// separator between two distinct blocks (bare).
604
+ private static func attributeGroupCharacterRange(
605
+ containing paragraphStart: Int,
606
+ in textStorage: NSTextStorage,
607
+ nsString: NSString,
608
+ paragraphPredicate: (NSRange, NSTextStorage) -> Bool,
609
+ requireAttributedJoin: NSAttributedString.Key?
610
+ ) -> NSRange {
611
+ let initialParagraphRange = nsString.paragraphRange(for: NSRange(location: paragraphStart, length: 0))
612
+ var groupStart = initialParagraphRange.location
613
+ var groupEnd = NSMaxRange(initialParagraphRange)
614
+
615
+ func newlineJoins(at separatorIndex: Int) -> Bool {
616
+ guard let key = requireAttributedJoin else { return true }
617
+ guard separatorIndex >= 0, separatorIndex < textStorage.length else { return false }
618
+ return textStorage.attribute(key, at: separatorIndex, effectiveRange: nil) != nil
619
+ }
620
+
621
+ var probeStart = groupStart
622
+ while probeStart > 0 {
623
+ let previousParagraphRange = nsString.paragraphRange(for: NSRange(location: probeStart - 1, length: 0))
624
+ guard paragraphPredicate(previousParagraphRange, textStorage),
625
+ newlineJoins(at: probeStart - 1)
626
+ else { break }
627
+ groupStart = previousParagraphRange.location
628
+ probeStart = previousParagraphRange.location
629
+ }
630
+
631
+ var nextParagraphLocation = groupEnd
632
+ while nextParagraphLocation < textStorage.length {
633
+ let nextParagraphRange = nsString.paragraphRange(for: NSRange(location: nextParagraphLocation, length: 0))
634
+ guard paragraphPredicate(nextParagraphRange, textStorage),
635
+ newlineJoins(at: nextParagraphLocation - 1)
636
+ else { break }
637
+ groupEnd = NSMaxRange(nextParagraphRange)
638
+ nextParagraphLocation = groupEnd
639
+ }
640
+
641
+ return NSRange(location: groupStart, length: groupEnd - groupStart)
642
+ }
643
+
644
+ private static func blockquoteGroupCharacterRange(
645
+ containing paragraphStart: Int,
646
+ in textStorage: NSTextStorage,
647
+ nsString: NSString
648
+ ) -> NSRange {
649
+ attributeGroupCharacterRange(
650
+ containing: paragraphStart,
651
+ in: textStorage,
652
+ nsString: nsString,
653
+ paragraphPredicate: paragraphHasBlockquoteBorder,
654
+ requireAttributedJoin: nil // blockquote merging keeps its existing semantics
655
+ )
656
+ }
657
+
658
+ private static func paragraphHasBlockquoteBorder(
659
+ _ paragraphRange: NSRange,
660
+ in textStorage: NSTextStorage
661
+ ) -> Bool {
662
+ guard paragraphRange.length > 0 else { return false }
663
+ let nsString = textStorage.string as NSString
664
+ var sawQuotedContent = false
665
+ var sawAnyQuotedCharacter = false
666
+
667
+ for offset in 0..<paragraphRange.length {
668
+ let index = paragraphRange.location + offset
669
+ guard index < textStorage.length else { break }
670
+
671
+ let hasBorder = textStorage.attribute(
672
+ RenderBridgeAttributes.blockquoteBorderColor,
673
+ at: index,
674
+ effectiveRange: nil
675
+ ) != nil
676
+ guard hasBorder else { continue }
677
+ sawAnyQuotedCharacter = true
678
+
679
+ let scalar = nsString.character(at: index)
680
+ if scalar != 0x000A, scalar != 0x000D {
681
+ sawQuotedContent = true
682
+ break
683
+ }
684
+ }
685
+
686
+ if sawQuotedContent {
687
+ return true
688
+ }
689
+
690
+ let trimmed = nsString.substring(with: paragraphRange)
691
+ .trimmingCharacters(in: .newlines)
692
+ return trimmed.isEmpty && sawAnyQuotedCharacter
693
+ }
694
+
695
+ static func codeBlockCharacterRange(
696
+ containing paragraphStart: Int,
697
+ in textStorage: NSTextStorage,
698
+ nsString: NSString
699
+ ) -> NSRange {
700
+ attributeGroupCharacterRange(
701
+ containing: paragraphStart,
702
+ in: textStorage,
703
+ nsString: nsString,
704
+ paragraphPredicate: paragraphHasCodeBlockBackground,
705
+ requireAttributedJoin: RenderBridgeAttributes.codeBlockBackgroundColor
706
+ )
707
+ }
708
+
709
+ private static func paragraphHasCodeBlockBackground(
710
+ _ paragraphRange: NSRange,
711
+ in textStorage: NSTextStorage
712
+ ) -> Bool {
713
+ guard paragraphRange.length > 0 else { return false }
714
+ return textStorage.attribute(
715
+ RenderBridgeAttributes.codeBlockBackgroundColor,
716
+ at: paragraphRange.location,
717
+ effectiveRange: nil
718
+ ) != nil
719
+ }
720
+
721
+ private func codeBlockRect(
722
+ characterRange: NSRange,
723
+ textStorage: NSTextStorage,
724
+ origin: CGPoint
725
+ ) -> CGRect? {
726
+ guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
727
+ return nil
728
+ }
729
+
730
+ ensureLayout(forCharacterRange: characterRange)
731
+ let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
732
+ guard glyphRange.length > 0 else { return nil }
733
+
734
+ let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
735
+ let horizontalPadding = (attrs[RenderBridgeAttributes.codeBlockPaddingHorizontal] as? NSNumber)
736
+ .map { CGFloat(truncating: $0) }
737
+ ?? 12
738
+ let verticalPadding = (attrs[RenderBridgeAttributes.codeBlockPaddingVertical] as? NSNumber)
739
+ .map { CGFloat(truncating: $0) }
740
+ ?? 8
741
+
742
+ var minX: CGFloat?
743
+ var maxX: CGFloat?
744
+ var minY: CGFloat?
745
+ var maxY: CGFloat?
746
+
747
+ enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
748
+ let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
749
+ let lineMinX = referenceRect.minX - horizontalPadding
750
+ let lineMaxX = lineFragmentRect.maxX + horizontalPadding
751
+ let lineMinY = lineFragmentRect.minY
752
+ let lineMaxY = referenceRect.maxY
753
+
754
+ minX = min(minX ?? lineMinX, lineMinX)
755
+ maxX = max(maxX ?? lineMaxX, lineMaxX)
756
+ minY = min(minY ?? lineMinY, lineMinY)
757
+ maxY = max(maxY ?? lineMaxY, lineMaxY)
758
+ }
759
+
760
+ guard let minX, let maxX, let minY, let maxY, maxY > minY else { return nil }
761
+
762
+ return CGRect(
763
+ x: origin.x + minX,
764
+ y: origin.y + minY - verticalPadding,
765
+ width: maxX - minX,
766
+ height: (maxY - minY) + (verticalPadding * 2)
767
+ )
768
+ }
769
+
770
+ static func markerParagraphStyle(from attrs: [NSAttributedString.Key: Any]) -> NSMutableParagraphStyle {
771
+ let markerStyle = NSMutableParagraphStyle()
772
+ let sourceStyle = attrs[.paragraphStyle] as? NSParagraphStyle
773
+
774
+ markerStyle.minimumLineHeight = sourceStyle?.minimumLineHeight ?? 0
775
+ markerStyle.maximumLineHeight = sourceStyle?.maximumLineHeight ?? 0
776
+ markerStyle.lineHeightMultiple = sourceStyle?.lineHeightMultiple ?? 0
777
+ markerStyle.baseWritingDirection = sourceStyle?.baseWritingDirection ?? .natural
778
+ markerStyle.alignment = .right
779
+ markerStyle.lineBreakMode = .byClipping
780
+ markerStyle.firstLineHeadIndent = 0
781
+ markerStyle.headIndent = 0
782
+ markerStyle.tailIndent = 0
783
+
784
+ return markerStyle
785
+ }
786
+
787
+ static func markerDrawingRect(
788
+ usedRect: CGRect,
789
+ lineFragmentRect: CGRect,
790
+ markerWidth: CGFloat,
791
+ baselineY: CGFloat,
792
+ markerFont: UIFont,
793
+ origin: CGPoint
794
+ ) -> CGRect {
795
+ let typographicHeight = markerFont.ascender - markerFont.descender
796
+ let leading = max(markerFont.lineHeight - typographicHeight, 0)
797
+ let topY = baselineY - markerFont.ascender - (leading / 2.0)
798
+ let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
799
+ return CGRect(
800
+ x: origin.x + referenceRect.minX - markerWidth,
801
+ y: origin.y + topY,
802
+ width: markerWidth - 4.0,
803
+ height: markerFont.lineHeight
804
+ )
805
+ }
806
+
807
+ static func taskMarkerDrawingRect(
808
+ usedRect: CGRect,
809
+ lineFragmentRect: CGRect,
810
+ markerWidth: CGFloat,
811
+ baselineY: CGFloat,
812
+ baseFont: UIFont,
813
+ origin: CGPoint,
814
+ markerGap: CGFloat = LayoutConstants.listMarkerTextGap,
815
+ explicitSize: CGFloat? = nil
816
+ ) -> CGRect {
817
+ let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
818
+ let checkboxSize = explicitSize ?? min(
819
+ max(baseFont.lineHeight * 1.05, 24),
820
+ max(markerWidth - 4, 24)
821
+ )
822
+ let centerY = baselineY - ((baseFont.ascender + baseFont.descender) / 2.0)
823
+ let x = origin.x + referenceRect.minX - markerGap - checkboxSize
824
+ let y = origin.y + centerY - (checkboxSize / 2.0)
825
+ return CGRect(x: x, y: y, width: checkboxSize, height: checkboxSize)
826
+ }
827
+
828
+ static func orderedMarkerDrawingOrigin(
829
+ usedRect: CGRect,
830
+ lineFragmentRect: CGRect,
831
+ markerWidth: CGFloat,
832
+ baselineY: CGFloat,
833
+ markerFont: UIFont,
834
+ markerText: String,
835
+ origin: CGPoint,
836
+ markerGap: CGFloat = LayoutConstants.listMarkerTextGap
837
+ ) -> CGPoint {
838
+ let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
839
+ let visibleMarkerText = markerText.trimmingCharacters(in: .whitespaces)
840
+ let markerSize = (visibleMarkerText as NSString).size(withAttributes: [
841
+ .font: markerFont
842
+ ])
843
+ let x = origin.x + referenceRect.minX - markerGap - ceil(markerSize.width)
844
+ let y = origin.y + baselineY - markerFont.ascender
845
+ return CGPoint(x: x, y: y)
846
+ }
847
+
848
+ static func markerBaselineOffset(
849
+ for listContext: [String: Any],
850
+ baseFont: UIFont,
851
+ markerFont: UIFont
852
+ ) -> CGFloat {
853
+ let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
854
+ guard !ordered else { return 0 }
855
+
856
+ let targetMidline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
857
+ let glyphMidline = unorderedBulletGlyphMidline(for: markerFont)
858
+ return targetMidline - glyphMidline
859
+ }
860
+
861
+ static func unorderedBulletDrawingRect(
862
+ usedRect: CGRect,
863
+ lineFragmentRect: CGRect,
864
+ markerWidth: CGFloat,
865
+ baselineY: CGFloat,
866
+ baseFont: UIFont,
867
+ markerScale: CGFloat,
868
+ origin: CGPoint,
869
+ markerGap: CGFloat = LayoutConstants.listMarkerTextGap
870
+ ) -> CGRect {
871
+ let markerFont = baseFont.withSize(baseFont.pointSize * markerScale)
872
+ let bulletBounds = unorderedBulletGlyphBounds(for: markerFont)
873
+ let bulletDiameter = max(max(bulletBounds.width, bulletBounds.height), 1)
874
+ let targetCenterAboveBaseline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
875
+ let centerY = baselineY - targetCenterAboveBaseline
876
+ let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
877
+ let x = origin.x + referenceRect.minX - markerGap - bulletDiameter
878
+ let y = origin.y + centerY - (bulletDiameter / 2.0)
879
+
880
+ return CGRect(
881
+ x: x,
882
+ y: y,
883
+ width: bulletDiameter,
884
+ height: bulletDiameter
885
+ )
886
+ }
887
+
888
+ static func isParagraphStartCreatedByHardBreak(
889
+ _ paragraphStart: Int,
890
+ in textStorage: NSTextStorage
891
+ ) -> Bool {
892
+ guard paragraphStart > 0, paragraphStart <= textStorage.length else { return false }
893
+ let previousVoidType = textStorage.attribute(
894
+ RenderBridgeAttributes.voidNodeType,
895
+ at: paragraphStart - 1,
896
+ effectiveRange: nil
897
+ ) as? String
898
+ return EditorNodeTypes.isHardBreak(previousVoidType)
899
+ }
900
+
901
+ private func markerFont(
902
+ for listContext: [String: Any],
903
+ baseFont: UIFont,
904
+ markerScale: CGFloat
905
+ ) -> UIFont {
906
+ let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
907
+ let isTask = (listContext["kind"] as? String) == "task"
908
+ if ordered {
909
+ return baseFont
910
+ }
911
+ if isTask {
912
+ return baseFont.withSize(baseFont.pointSize * 1.35)
913
+ }
914
+ return baseFont.withSize(baseFont.pointSize * markerScale)
915
+ }
916
+
917
+ private func drawTaskCheckbox(
918
+ in rect: CGRect,
919
+ checked: Bool,
920
+ color: UIColor
921
+ ) {
922
+ let path = UIBezierPath(roundedRect: rect, cornerRadius: min(rect.width, rect.height) * 0.22)
923
+ color.setStroke()
924
+ path.lineWidth = max(1.8, rect.width * 0.09)
925
+ path.stroke()
926
+
927
+ guard checked else { return }
928
+
929
+ let checkPath = UIBezierPath()
930
+ checkPath.move(to: CGPoint(x: rect.minX + rect.width * 0.22, y: rect.midY + rect.height * 0.04))
931
+ checkPath.addLine(to: CGPoint(x: rect.minX + rect.width * 0.42, y: rect.maxY - rect.height * 0.24))
932
+ checkPath.addLine(to: CGPoint(x: rect.maxX - rect.width * 0.18, y: rect.minY + rect.height * 0.24))
933
+ checkPath.lineCapStyle = .round
934
+ checkPath.lineJoinStyle = .round
935
+ checkPath.lineWidth = max(2.1, rect.width * 0.12)
936
+ color.setStroke()
937
+ checkPath.stroke()
938
+ }
939
+
940
+ static func markerBaseFont(
941
+ from attrs: [NSAttributedString.Key: Any],
942
+ fallback fallbackFont: UIFont = .systemFont(ofSize: 16)
943
+ ) -> UIFont {
944
+ (attrs[RenderBridgeAttributes.listMarkerBaseFont] as? UIFont)
945
+ ?? (attrs[.font] as? UIFont)
946
+ ?? fallbackFont
947
+ }
948
+
949
+ private static func unorderedBulletGlyphBounds(for font: UIFont) -> CGRect {
950
+ let ctFont = font as CTFont
951
+ let bullet = UniChar(0x2022)
952
+ var glyph = CGGlyph()
953
+ guard CTFontGetGlyphsForCharacters(ctFont, [bullet], &glyph, 1) else {
954
+ let fallbackDiameter = max(font.pointSize * 0.28, 1)
955
+ return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
956
+ }
957
+
958
+ var boundingRect = CGRect.zero
959
+ CTFontGetBoundingRectsForGlyphs(ctFont, .default, [glyph], &boundingRect, 1)
960
+ if boundingRect.isNull || boundingRect.isEmpty {
961
+ let fallbackDiameter = max(font.pointSize * 0.28, 1)
962
+ return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
963
+ }
964
+
965
+ return boundingRect
966
+ }
967
+
968
+ private static func unorderedBulletGlyphMidline(for font: UIFont) -> CGFloat {
969
+ unorderedBulletGlyphBounds(for: font).midY
970
+ }
971
+ }