@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.
- package/LICENSE +160 -0
- package/README.md +259 -0
- package/RUST-STANDARD-LIBRARY-NOTICES.html +8266 -0
- package/ReactNativeProseEditor.podspec +59 -0
- package/THIRD_PARTY_NOTICES.md +31155 -0
- package/android/build.gradle +188 -0
- package/android/consumer-rules.pro +18 -0
- package/android/expo/build.gradle +17 -0
- package/android/src/debug/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +7 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/AndroidApiCompat.kt +19 -0
- package/android/src/main/java/com/apollohg/editor/AndroidCollaborationTransport.kt +860 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +91 -0
- package/android/src/main/java/com/apollohg/editor/CodeHighlighting.kt +141 -0
- package/android/src/main/java/com/apollohg/editor/DecodedBitmapBudget.kt +250 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/EditorBlockSpacing.kt +51 -0
- package/android/src/main/java/com/apollohg/editor/EditorBoxDrawable.kt +16 -0
- package/android/src/main/java/com/apollohg/editor/EditorBoxDrawing.kt +234 -0
- package/android/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +87 -0
- package/android/src/main/java/com/apollohg/editor/EditorCodeHighlighting.kt +120 -0
- package/android/src/main/java/com/apollohg/editor/EditorDecorationLayer.java +18 -0
- package/android/src/main/java/com/apollohg/editor/EditorDocumentLayout.kt +626 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1042 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextAnnotations.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextCaret.kt +100 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +101 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextComposition.kt +220 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +316 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextCorrection.kt +303 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextDeferredUpdates.kt +327 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextDeletion.kt +258 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextDiagnostics.kt +83 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextDragDrop.kt +67 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextExternalComposition.kt +357 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextHardwareInput.kt +277 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +280 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextInputLifecycle.kt +183 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextInputPreparation.kt +79 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextInputTraits.kt +384 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextLayout.kt +100 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextNativeMutation.kt +243 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextPlaceholder.kt +154 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextRenderPatch.kt +347 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextRendering.kt +313 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextRustCommands.kt +281 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +352 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextState.kt +113 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextTaskMarkers.kt +92 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextTextCommit.kt +327 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +84 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextUpdates.kt +245 -0
- package/android/src/main/java/com/apollohg/editor/EditorGlyphHeightCursorDrawable.kt +42 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +13 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +923 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputCoordinates.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputCorrection.kt +107 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputCursorGeometry.kt +128 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputSnapshots.kt +134 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputSurroundingDeletion.kt +321 -0
- package/android/src/main/java/com/apollohg/editor/EditorKeyboardToolbarView.kt +851 -0
- package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +192 -0
- package/android/src/main/java/com/apollohg/editor/EditorReconciliationWatcher.kt +46 -0
- package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +303 -0
- package/android/src/main/java/com/apollohg/editor/EditorStyleSheetSpans.kt +253 -0
- package/android/src/main/java/com/apollohg/editor/EditorSurfaceInputConnection.kt +67 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextDecorationDrawing.kt +143 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurface.kt +712 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceDragDrop.kt +94 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +708 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +650 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +912 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2AdapterMutations.kt +402 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2AdapterRendering.kt +123 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2AdapterSelection.kt +203 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +120 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +87 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Registry.kt +97 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +409 -0
- package/android/src/main/java/com/apollohg/editor/ExpoAutoGrowStyleSizePublisher.kt +34 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +266 -0
- package/android/src/main/java/com/apollohg/editor/ImeTextCoordinateMapper.kt +102 -0
- package/android/src/main/java/com/apollohg/editor/KeyboardToolbarImeAnimationController.kt +101 -0
- package/android/src/main/java/com/apollohg/editor/MentionSuggestionChipView.kt +234 -0
- package/android/src/main/java/com/apollohg/editor/NativeCollaborationTransportRegistry.kt +493 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +813 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewAtoms.kt +68 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewBinding.kt +126 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewDiagnostics.kt +208 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewErrors.kt +146 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewEvents.kt +150 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewExternalComposition.kt +39 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewFocus.kt +176 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewLifecycle.kt +302 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewMentions.kt +259 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewNativeActions.kt +179 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewOutsideTap.kt +214 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPendingUpdates.kt +376 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +234 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPropertyRetries.kt +197 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewToolbar.kt +285 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewTypes.kt +28 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewUpdates.kt +271 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +687 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorOutsideTapDispatcher.kt +380 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorV2Numbers.kt +45 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorViewRegistry.kt +372 -0
- package/android/src/main/java/com/apollohg/editor/NativeImagePipeline.kt +22 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +193 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbarIcons.kt +177 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbarItem.kt +274 -0
- package/android/src/main/java/com/apollohg/editor/OkHttpCollaborationSocket.kt +102 -0
- package/android/src/main/java/com/apollohg/editor/OrderedListMarkerFormatter.kt +130 -0
- package/android/src/main/java/com/apollohg/editor/PointerTransparentView.java +18 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +128 -0
- package/android/src/main/java/com/apollohg/editor/ProseViewerView.kt +854 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +314 -0
- package/android/src/main/java/com/apollohg/editor/RenderBlockSpans.kt +314 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +224 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +407 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +544 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +631 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridgeParagraphs.kt +215 -0
- package/android/src/main/java/com/apollohg/editor/RenderImageDecoder.kt +748 -0
- package/android/src/main/java/com/apollohg/editor/RenderImageLoader.kt +900 -0
- package/android/src/main/java/com/apollohg/editor/RenderLayoutConstants.kt +117 -0
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +337 -0
- package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +182 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +951 -0
- package/android/src/main/java/com/apollohg/editor/SharedNativeImagePipeline.kt +78 -0
- package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +261 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1656 -0
- package/android/src/main/java/com/apollohg/editor/viewer/FabricAttachmentSidecars.kt +80 -0
- package/android/src/main/java/com/apollohg/editor/viewer/FabricLeaseHandleBridge.kt +59 -0
- package/android/src/main/java/com/apollohg/editor/viewer/FallbackSelectionGeometry.kt +450 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseDrawingView.kt +678 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseInstrumentation.kt +608 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayout.kt +186 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutCache.kt +393 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutRegistry.kt +828 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseTheme.kt +316 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerManager.kt +900 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerPackage.kt +16 -0
- package/android/src/main/java/com/apollohg/editor/viewer/PreparedViewerAtoms.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/viewer/ViewerCodeHighlighting.kt +91 -0
- package/android/src/main/java/com/apollohg/editor/viewer/ViewerDocument.kt +453 -0
- package/android/src/main/java/com/apollohg/editor/viewer/ViewerFontEnvironment.kt +282 -0
- package/android/src/main/java/com/apollohg/editor/viewer/ViewerImagePipeline.kt +460 -0
- package/android/src/main/jni/CMakeLists.txt +85 -0
- package/android/src/main/jni/PreparedProseViewerMeasurementsManager.cpp +334 -0
- package/android/src/main/jni/ReactNativeProseEditorSpec.h +17 -0
- package/android/src/main/jni/reactnativeproseeditor.h +6 -0
- package/android/src/main/res/values/attrs.xml +6 -0
- package/android/src/release/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +9 -0
- package/app.plugin.js +62 -0
- package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseMeasurementsManager.h +62 -0
- package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerComponentDescriptor.h +30 -0
- package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.cpp +192 -0
- package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.h +46 -0
- package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerState.h +145 -0
- package/dist/AtomHost.d.ts +17 -0
- package/dist/AtomHost.js +135 -0
- package/dist/DefaultAtomChip.d.ts +2 -0
- package/dist/DefaultAtomChip.js +22 -0
- package/dist/EditorAddon.d.ts +32 -0
- package/dist/EditorAddon.js +29 -0
- package/dist/EditorAddonNormalization.d.ts +6 -0
- package/dist/EditorAddonNormalization.js +48 -0
- package/dist/EditorMentionThemeNormalization.d.ts +8 -0
- package/dist/EditorMentionThemeNormalization.js +22 -0
- package/dist/EditorMentionThemeValidation.d.ts +12 -0
- package/dist/EditorMentionThemeValidation.js +95 -0
- package/dist/EditorStyleSheet.d.ts +4 -0
- package/dist/EditorStyleSheet.js +10 -0
- package/dist/EditorStyleSheetNormalization.d.ts +3 -0
- package/dist/EditorStyleSheetNormalization.js +226 -0
- package/dist/EditorStyleSheetTypes.d.ts +151 -0
- package/dist/EditorStyleSheetTypes.js +2 -0
- package/dist/EditorTheme.d.ts +117 -0
- package/dist/EditorTheme.js +41 -0
- package/dist/EditorToolbar.d.ts +4 -0
- package/dist/EditorToolbar.js +17 -0
- package/dist/EditorToolbarComponent.d.ts +13 -0
- package/dist/EditorToolbarComponent.js +24 -0
- package/dist/EditorToolbarItems.d.ts +9 -0
- package/dist/EditorToolbarItems.js +58 -0
- package/dist/EditorToolbarRegistry.d.ts +59 -0
- package/dist/EditorToolbarRegistry.js +175 -0
- package/dist/EditorToolbarTypes.d.ts +273 -0
- package/dist/EditorToolbarTypes.js +2 -0
- package/dist/EditorToolbarVisuals.d.ts +154 -0
- package/dist/EditorToolbarVisuals.js +228 -0
- package/dist/EditorUpdateRevision.d.ts +8 -0
- package/dist/EditorUpdateRevision.js +22 -0
- package/dist/ExternalTextComposition.d.ts +45 -0
- package/dist/ExternalTextComposition.js +537 -0
- package/dist/ImageLoadingPolicy.d.ts +54 -0
- package/dist/ImageLoadingPolicy.js +60 -0
- package/dist/NativeEditorBoundaryError.d.ts +111 -0
- package/dist/NativeEditorBoundaryError.js +268 -0
- package/dist/NativeEditorBridge.d.ts +9 -0
- package/dist/NativeEditorBridge.js +37 -0
- package/dist/NativeEditorCollaborationTransport.d.ts +154 -0
- package/dist/NativeEditorCollaborationTransport.js +275 -0
- package/dist/NativeEditorCreateConfig.d.ts +16 -0
- package/dist/NativeEditorCreateConfig.js +249 -0
- package/dist/NativeEditorCreateJson.d.ts +76 -0
- package/dist/NativeEditorCreateJson.js +474 -0
- package/dist/NativeEditorCreateValidation.d.ts +32 -0
- package/dist/NativeEditorCreateValidation.js +150 -0
- package/dist/NativeEditorDocumentBridge.d.ts +72 -0
- package/dist/NativeEditorDocumentBridge.js +305 -0
- package/dist/NativeEditorDocumentHandle.d.ts +70 -0
- package/dist/NativeEditorDocumentHandle.js +89 -0
- package/dist/NativeEditorLocalAwareness.d.ts +34 -0
- package/dist/NativeEditorLocalAwareness.js +153 -0
- package/dist/NativeEditorNativeModule.d.ts +48 -0
- package/dist/NativeEditorNativeModule.js +34 -0
- package/dist/NativeEditorRenderNormalization.d.ts +22 -0
- package/dist/NativeEditorRenderNormalization.js +274 -0
- package/dist/NativeEditorResultNormalization.d.ts +108 -0
- package/dist/NativeEditorResultNormalization.js +400 -0
- package/dist/NativeEditorTypes.d.ts +318 -0
- package/dist/NativeEditorTypes.js +15 -0
- package/dist/NativeEditorV2Decimal.d.ts +5 -0
- package/dist/NativeEditorV2Decimal.js +18 -0
- package/dist/NativeProseViewer.d.ts +122 -0
- package/dist/NativeProseViewer.js +130 -0
- package/dist/NativeRichTextEditor.d.ts +3 -0
- package/dist/NativeRichTextEditor.js +6 -0
- package/dist/ResourceLimits.d.ts +100 -0
- package/dist/ResourceLimits.js +80 -0
- package/dist/RichTextEditor.d.ts +4 -0
- package/dist/RichTextEditor.js +25 -0
- package/dist/RichTextEditorNativeTypes.d.ts +122 -0
- package/dist/RichTextEditorNativeTypes.js +2 -0
- package/dist/RichTextEditorNativeView.d.ts +12 -0
- package/dist/RichTextEditorNativeView.js +15 -0
- package/dist/RichTextEditorSerialization.d.ts +53 -0
- package/dist/RichTextEditorSerialization.js +283 -0
- package/dist/RichTextEditorTypes.d.ts +281 -0
- package/dist/RichTextEditorTypes.js +2 -0
- package/dist/ViewerConfiguration.d.ts +22 -0
- package/dist/ViewerConfiguration.js +6 -0
- package/dist/YjsCollaboration.d.ts +146 -0
- package/dist/YjsCollaboration.js +496 -0
- package/dist/addons.d.ts +193 -0
- package/dist/addons.js +122 -0
- package/dist/atomConstants.d.ts +3 -0
- package/dist/atomConstants.js +6 -0
- package/dist/atomInstances.d.ts +35 -0
- package/dist/atomInstances.js +79 -0
- package/dist/atomPolicy.d.ts +4 -0
- package/dist/atomPolicy.js +47 -0
- package/dist/atomUpdates.d.ts +2 -0
- package/dist/atomUpdates.js +38 -0
- package/dist/atoms.d.ts +92 -0
- package/dist/atoms.js +222 -0
- package/dist/attributeValidation.d.ts +4 -0
- package/dist/attributeValidation.js +97 -0
- package/dist/contentExpression.d.ts +6 -0
- package/dist/contentExpression.js +335 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +78 -0
- package/dist/listMarkers.d.ts +15 -0
- package/dist/listMarkers.js +21 -0
- package/dist/schemaDefinition.d.ts +143 -0
- package/dist/schemaDefinition.js +206 -0
- package/dist/schemaDocument.d.ts +17 -0
- package/dist/schemaDocument.js +69 -0
- package/dist/schemaDocumentConstruction.d.ts +5 -0
- package/dist/schemaDocumentConstruction.js +125 -0
- package/dist/schemaNormalization.d.ts +29 -0
- package/dist/schemaNormalization.js +313 -0
- package/dist/schemaPresets.d.ts +68 -0
- package/dist/schemaPresets.js +198 -0
- package/dist/schemaResolution.d.ts +9 -0
- package/dist/schemaResolution.js +211 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/schemas.js +28 -0
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +37 -0
- package/dist/specs/PreparedProseViewerNativeComponent.js +6 -0
- package/dist/useEditorToolbarInteractions.d.ts +12 -0
- package/dist/useEditorToolbarInteractions.js +218 -0
- package/dist/useEditorToolbarItems.d.ts +8 -0
- package/dist/useEditorToolbarItems.js +280 -0
- package/dist/useEditorToolbarPresentation.d.ts +4 -0
- package/dist/useEditorToolbarPresentation.js +203 -0
- package/dist/useEditorToolbarState.d.ts +63 -0
- package/dist/useEditorToolbarState.js +108 -0
- package/dist/useFocusPreservingFrames.d.ts +17 -0
- package/dist/useFocusPreservingFrames.js +108 -0
- package/dist/useNativeEditor.d.ts +66 -0
- package/dist/useNativeEditor.js +370 -0
- package/dist/useRichTextEditorCommands.d.ts +21 -0
- package/dist/useRichTextEditorCommands.js +299 -0
- package/dist/useRichTextEditorEvents.d.ts +18 -0
- package/dist/useRichTextEditorEvents.js +216 -0
- package/dist/useRichTextEditorMentions.d.ts +11 -0
- package/dist/useRichTextEditorMentions.js +316 -0
- package/dist/useRichTextEditorPresentation.d.ts +5 -0
- package/dist/useRichTextEditorPresentation.js +178 -0
- package/dist/useRichTextEditorState.d.ts +180 -0
- package/dist/useRichTextEditorState.js +405 -0
- package/dist/useRichTextEditorUpdates.d.ts +10 -0
- package/dist/useRichTextEditorUpdates.js +308 -0
- package/dist/useViewerAtoms.d.ts +38 -0
- package/dist/useViewerAtoms.js +288 -0
- package/expo-module.config.json +13 -0
- package/ios/EditorAddons.swift +354 -0
- package/ios/EditorCodeHighlighting.swift +184 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorInteractionOverlays.swift +697 -0
- package/ios/EditorLayoutManager.swift +971 -0
- package/ios/EditorNodeTypes.swift +37 -0
- package/ios/EditorStyleSheet.swift +461 -0
- package/ios/EditorTextView+AttributedPatch.swift +305 -0
- package/ios/EditorTextView+Binding.swift +61 -0
- package/ios/EditorTextView+Caret.swift +333 -0
- package/ios/EditorTextView+Commands.swift +203 -0
- package/ios/EditorTextView+Delegate.swift +65 -0
- package/ios/EditorTextView+Diagnostics.swift +225 -0
- package/ios/EditorTextView+DragDrop.swift +290 -0
- package/ios/EditorTextView+ExternalComposition.swift +364 -0
- package/ios/EditorTextView+Images.swift +354 -0
- package/ios/EditorTextView+InputTraits.swift +200 -0
- package/ios/EditorTextView+KeyboardInput.swift +318 -0
- package/ios/EditorTextView+Layout.swift +245 -0
- package/ios/EditorTextView+MarkedText.swift +267 -0
- package/ios/EditorTextView+NativeInputLifecycle.swift +372 -0
- package/ios/EditorTextView+NativeMutation.swift +471 -0
- package/ios/EditorTextView+Reconciliation.swift +97 -0
- package/ios/EditorTextView+RenderPatch.swift +573 -0
- package/ios/EditorTextView+Rendering.swift +539 -0
- package/ios/EditorTextView+Selection.swift +542 -0
- package/ios/EditorTextView.swift +953 -0
- package/ios/EditorTheme.swift +626 -0
- package/ios/EditorV2Adapter+Commands.swift +204 -0
- package/ios/EditorV2Adapter+Envelope.swift +238 -0
- package/ios/EditorV2Adapter+Mutation.swift +392 -0
- package/ios/EditorV2Adapter+Registry.swift +185 -0
- package/ios/EditorV2Adapter+RenderState.swift +360 -0
- package/ios/EditorV2Adapter+RenderValidation.swift +420 -0
- package/ios/EditorV2Adapter+Selection.swift +329 -0
- package/ios/EditorV2Adapter+TextCommands.swift +430 -0
- package/ios/EditorV2Adapter.swift +376 -0
- package/ios/EditorV2Shadow.swift +353 -0
- package/ios/Generated_editor_core.swift +2378 -0
- package/ios/NativeCodeHighlighting.swift +159 -0
- package/ios/NativeCollaborationSocket.swift +144 -0
- package/ios/NativeCollaborationTransport+Protocol.swift +258 -0
- package/ios/NativeCollaborationTransport+Socket.swift +379 -0
- package/ios/NativeCollaborationTransport+Validation.swift +103 -0
- package/ios/NativeCollaborationTransport.swift +322 -0
- package/ios/NativeCollaborationTransportRegistry.swift +521 -0
- package/ios/NativeEditorExpoView+AccessoryRetry.swift +219 -0
- package/ios/NativeEditorExpoView+Binding.swift +268 -0
- package/ios/NativeEditorExpoView+Diagnostics.swift +66 -0
- package/ios/NativeEditorExpoView+Events.swift +131 -0
- package/ios/NativeEditorExpoView+Focus.swift +169 -0
- package/ios/NativeEditorExpoView+MentionRetry.swift +288 -0
- package/ios/NativeEditorExpoView+Mentions.swift +288 -0
- package/ios/NativeEditorExpoView+Properties.swift +300 -0
- package/ios/NativeEditorExpoView+Toolbar.swift +86 -0
- package/ios/NativeEditorExpoView+Updates.swift +312 -0
- package/ios/NativeEditorExpoView.swift +349 -0
- package/ios/NativeEditorModule.swift +856 -0
- package/ios/NativeEditorViewRegistry.swift +226 -0
- package/ios/NativeToolbar+ButtonStyle.swift +214 -0
- package/ios/NativeToolbar+Buttons.swift +228 -0
- package/ios/NativeToolbar+Diagnostics.swift +114 -0
- package/ios/NativeToolbar+Menu.swift +101 -0
- package/ios/NativeToolbar+Models.swift +519 -0
- package/ios/NativeToolbar+Placeholder.swift +39 -0
- package/ios/NativeToolbar.swift +612 -0
- package/ios/OrderedListMarkerFormatter.swift +103 -0
- package/ios/PositionBridge.swift +596 -0
- package/ios/ProseViewerView.swift +462 -0
- package/ios/RenderBridge+Atoms.swift +288 -0
- package/ios/RenderBridge+Attachments.swift +246 -0
- package/ios/RenderBridge+BlockBoundaries.swift +313 -0
- package/ios/RenderBridge+Style.swift +343 -0
- package/ios/RenderBridge.swift +704 -0
- package/ios/RichTextEditorView.swift +930 -0
- package/ios/SharedNativeImagePipeline+Cache.swift +189 -0
- package/ios/SharedNativeImagePipeline+Transport.swift +328 -0
- package/ios/SharedNativeImagePipeline.swift +533 -0
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +436 -0
- package/ios/Viewer/CoreTextProseLayoutEngine+Models.swift +322 -0
- package/ios/Viewer/CoreTextProseLayoutEngine.swift +635 -0
- package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.h +4 -0
- package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.mm +767 -0
- package/ios/Viewer/Fabric/PreparedProseMeasurementsManager.mm +118 -0
- package/ios/Viewer/NativeSVGImageDecoder.h +9 -0
- package/ios/Viewer/NativeSVGImageDecoder.mm +106 -0
- package/ios/Viewer/NativeSVGPreflight.h +11 -0
- package/ios/Viewer/NativeSVGPreflight.mm +219 -0
- package/ios/Viewer/PreparedProseDrawingView.swift +639 -0
- package/ios/Viewer/PreparedProseInstrumentation.swift +387 -0
- package/ios/Viewer/PreparedProseLayout.swift +326 -0
- package/ios/Viewer/PreparedProseLayoutCache.swift +766 -0
- package/ios/Viewer/PreparedProseLayoutRegistry+Artifacts.swift +377 -0
- package/ios/Viewer/PreparedProseLayoutRegistry+Fabric.swift +582 -0
- package/ios/Viewer/PreparedProseLayoutRegistry+FabricOwnership.swift +128 -0
- package/ios/Viewer/PreparedProseLayoutRegistry.swift +381 -0
- package/ios/Viewer/ViewerDocument.swift +575 -0
- package/ios/Viewer/ViewerFontEnvironment.swift +283 -0
- package/ios/Viewer/ViewerImagePipeline.swift +478 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1023 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/package.json +151 -0
- package/react-native.config.js +17 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +3146 -0
- package/src/AtomHost.tsx +180 -0
- package/src/DefaultAtomChip.tsx +27 -0
- package/src/EditorAddon.ts +65 -0
- package/src/EditorAddonNormalization.ts +49 -0
- package/src/EditorMentionThemeNormalization.ts +25 -0
- package/src/EditorMentionThemeValidation.ts +133 -0
- package/src/EditorStyleSheet.ts +9 -0
- package/src/EditorStyleSheetNormalization.ts +219 -0
- package/src/EditorStyleSheetTypes.ts +189 -0
- package/src/EditorTheme.ts +196 -0
- package/src/EditorToolbar.tsx +31 -0
- package/src/EditorToolbarComponent.tsx +23 -0
- package/src/EditorToolbarItems.tsx +64 -0
- package/src/EditorToolbarRegistry.tsx +254 -0
- package/src/EditorToolbarTypes.tsx +322 -0
- package/src/EditorToolbarVisuals.tsx +270 -0
- package/src/EditorUpdateRevision.ts +26 -0
- package/src/ExternalTextComposition.ts +658 -0
- package/src/ImageLoadingPolicy.ts +125 -0
- package/src/NativeEditorBoundaryError.ts +345 -0
- package/src/NativeEditorBridge.ts +93 -0
- package/src/NativeEditorCollaborationTransport.ts +493 -0
- package/src/NativeEditorCreateConfig.ts +326 -0
- package/src/NativeEditorCreateJson.ts +579 -0
- package/src/NativeEditorCreateValidation.ts +178 -0
- package/src/NativeEditorDocumentBridge.ts +485 -0
- package/src/NativeEditorDocumentHandle.ts +168 -0
- package/src/NativeEditorLocalAwareness.ts +168 -0
- package/src/NativeEditorNativeModule.ts +97 -0
- package/src/NativeEditorRenderNormalization.ts +320 -0
- package/src/NativeEditorResultNormalization.ts +469 -0
- package/src/NativeEditorTypes.ts +366 -0
- package/src/NativeEditorV2Decimal.ts +13 -0
- package/src/NativeProseViewer.tsx +356 -0
- package/src/NativeRichTextEditor.tsx +33 -0
- package/src/ResourceLimits.ts +187 -0
- package/src/RichTextEditor.tsx +27 -0
- package/src/RichTextEditorNativeTypes.tsx +144 -0
- package/src/RichTextEditorNativeView.tsx +21 -0
- package/src/RichTextEditorSerialization.tsx +381 -0
- package/src/RichTextEditorTypes.tsx +327 -0
- package/src/ViewerConfiguration.ts +23 -0
- package/src/YjsCollaboration.ts +692 -0
- package/src/addons.ts +311 -0
- package/src/atomConstants.ts +3 -0
- package/src/atomInstances.ts +123 -0
- package/src/atomPolicy.ts +47 -0
- package/src/atomUpdates.ts +52 -0
- package/src/atoms.ts +379 -0
- package/src/attributeValidation.ts +116 -0
- package/src/contentExpression.ts +338 -0
- package/src/index.ts +295 -0
- package/src/listMarkers.ts +18 -0
- package/src/schemaDefinition.ts +395 -0
- package/src/schemaDocument.ts +92 -0
- package/src/schemaDocumentConstruction.ts +148 -0
- package/src/schemaNormalization.ts +367 -0
- package/src/schemaPresets.ts +250 -0
- package/src/schemaResolution.ts +242 -0
- package/src/schemas.ts +43 -0
- package/src/specs/PreparedProseViewerNativeComponent.ts +40 -0
- package/src/useEditorToolbarInteractions.tsx +329 -0
- package/src/useEditorToolbarItems.tsx +390 -0
- package/src/useEditorToolbarPresentation.tsx +483 -0
- package/src/useEditorToolbarState.tsx +172 -0
- package/src/useFocusPreservingFrames.ts +171 -0
- package/src/useNativeEditor.ts +534 -0
- package/src/useRichTextEditorCommands.tsx +478 -0
- package/src/useRichTextEditorEvents.tsx +342 -0
- package/src/useRichTextEditorMentions.tsx +441 -0
- package/src/useRichTextEditorPresentation.tsx +563 -0
- package/src/useRichTextEditorState.tsx +620 -0
- package/src/useRichTextEditorUpdates.tsx +403 -0
- 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
|
+
}
|