react-native-enriched-markdown-scaffold 0.7.4-scaffold.9 → 1.0.2-scaffold.2
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/README.md +54 -8
- package/ReactNativeEnrichedMarkdown.podspec +82 -73
- package/android/build.gradle +186 -1
- package/android/consumer-rules.pro +1 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +9 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +24 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +8 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +9 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +75 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +63 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +35 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +886 -23
- package/android/src/main/baseline-prof.txt +1 -1
- package/android/src/main/cpp/jni-adapter.cpp +74 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +189 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +50 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +10 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +82 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyPressEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +54 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +901 -462
- package/android/src/main/java/com/swmansion/enriched/markdown/input/MarkdownClipboard.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/BlockEditCoordinator.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/ClipboardCoordinator.kt +73 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditContext.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditPipeline.kt +248 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditSession.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +101 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/LinkCoordinator.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MarkdownTextWatcher.kt +3 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MentionCoordinator.kt +118 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnChangeStateEvent.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnContextMenuItemPressEvent.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnInputKeyPressEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockStore.kt +254 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +80 -87
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputFormatter.kt +109 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +116 -22
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/MarkdownSerializer.kt +167 -38
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/RangeEditAdjustment.kt +149 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +66 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +4 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockRange.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockType.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/FormattingRange.kt +9 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/InputFormatterStyle.kt +39 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathAtom.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MutableRangeBounds.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputBulletSpan.kt +82 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputHeadingSpan.kt +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputListItemSpacingSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputOrderedListMarkerSpan.kt +63 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/BlockHandler.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/HeadingBlockHandler.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/OrderedListBlockHandler.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/UnorderedListBlockHandler.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +8 -26
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +9 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +100 -12
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +16 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +74 -18
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +43 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +9 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +30 -27
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +174 -24
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +27 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +13 -16
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +182 -46
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListItemSpacingSpan.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListMarkerAnchorSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +7 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +4 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +13 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TableStyle.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockHighlighter.kt +90 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockNode.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/FeatureFlags.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +9 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/ReadableMapExtensions.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter.kt +83 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +74 -44
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageCache.kt +26 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt +22 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt +161 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/TextPaintExtensions.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/CheckboxTouchHelper.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/TaskListTapUtils.kt +41 -21
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/span/BlockSpacing.kt +15 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt +58 -27
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +49 -15
- package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +45 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +396 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/ContextMenuPopup.kt +9 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CopyGlyph.kt +101 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +36 -9
- package/android/src/main/jni/CMakeLists.txt +53 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp +10 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h +11 -2
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +10 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +12 -2
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +24 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +16 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/input/math/MathChipControllerImpl.kt +21 -4
- package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +7 -2
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +6 -4
- package/cpp/highlight/CodeBlockHighlighter.cpp +284 -0
- package/cpp/highlight/CodeBlockHighlighter.hpp +66 -0
- package/cpp/highlight/CodeBlockLanguages.cpp +138 -0
- package/cpp/highlight/CodeBlockLanguages.hpp +25 -0
- package/cpp/highlight/HighlightGrammars.hpp +28 -0
- package/cpp/highlight/code_highlight_podspec.rb +211 -0
- package/cpp/highlight/gen-registry.mjs +155 -0
- package/cpp/highlight/grammar-versions.json +159 -0
- package/cpp/highlight/vendor/generated/generated_queries.h +1665 -0
- package/cpp/highlight/vendor/generated/generated_registry.cpp +59 -0
- package/cpp/parser/MD4CParser.cpp +173 -61
- package/cpp/parser/MD4CParser.hpp +1 -0
- package/cpp/parser/MarkdownASTNode.hpp +2 -1
- package/cpp/wasm/ASTSerializer.cpp +2 -0
- package/cpp/wasm/build.sh +1 -0
- package/cpp/wasm/md4c_wasm.cpp +3 -1
- package/docs/ACCESSIBILITY.md +14 -2
- package/docs/API_REFERENCE.md +234 -12
- package/docs/BREAKING_CHANGES.md +129 -0
- package/docs/CODE_HIGHLIGHT.md +133 -0
- package/docs/COPY_OPTIONS.md +47 -3
- package/docs/ELEMENTS_STRUCTURE.md +52 -4
- package/docs/IMAGE_CACHING.md +30 -0
- package/docs/INPUT.md +108 -11
- package/docs/LATEX_MATH.md +87 -45
- package/docs/MACOS.md +1 -1
- package/docs/MARKDOWN_STREAMING.md +27 -1
- package/docs/NATIVE_ASSETS.md +127 -0
- package/docs/RTL.md +1 -1
- package/docs/STYLES.md +46 -6
- package/docs/TESTING.md +112 -0
- package/docs/TEXT.md +16 -6
- package/docs/WEB.md +3 -1
- package/ios/EnrichedMarkdown.h +7 -0
- package/ios/EnrichedMarkdown.mm +284 -75
- package/ios/EnrichedMarkdownText.h +7 -0
- package/ios/EnrichedMarkdownText.mm +101 -73
- package/ios/attachments/ENRMImageAttachment.h +9 -0
- package/ios/attachments/ENRMImageAttachment.m +143 -40
- package/ios/attachments/ENRMMathInlineAttachment.h +2 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +48 -81
- package/ios/attachments/ENRMMathInlineAttachmentShared.h +2 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +75 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +63 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +35 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +886 -23
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +131 -0
- package/ios/input/ENRMAutoLinkDetector.h +2 -1
- package/ios/input/ENRMAutoLinkDetector.mm +79 -41
- package/ios/input/ENRMBlockEditCoordinator.h +71 -0
- package/ios/input/ENRMBlockEditCoordinator.mm +195 -0
- package/ios/input/ENRMBlockRange.h +28 -0
- package/ios/input/ENRMBlockRange.mm +30 -0
- package/ios/input/ENRMBlockStore.h +56 -0
- package/ios/input/ENRMBlockStore.mm +275 -0
- package/ios/input/ENRMClipboardCoordinator.h +45 -0
- package/ios/input/ENRMClipboardCoordinator.mm +120 -0
- package/ios/input/ENRMDetectorPipeline.h +2 -1
- package/ios/input/ENRMEditPipeline.h +76 -0
- package/ios/input/ENRMEditPipeline.mm +228 -0
- package/ios/input/ENRMEditSession.h +36 -0
- package/ios/input/ENRMEditSession.mm +63 -0
- package/ios/input/ENRMFormattingStore.h +17 -0
- package/ios/input/ENRMFormattingStore.mm +95 -105
- package/ios/input/ENRMInputBlockType.h +91 -0
- package/ios/input/ENRMInputBlockType.mm +5 -0
- package/ios/input/ENRMInputDecorationDrawer.h +18 -0
- package/ios/input/ENRMInputEventEmitter.h +56 -0
- package/ios/input/ENRMInputEventEmitter.mm +308 -0
- package/ios/input/ENRMInputFormatter.h +29 -69
- package/ios/input/ENRMInputFormatter.mm +216 -124
- package/ios/input/ENRMInputFormatterStyle.h +99 -0
- package/ios/input/ENRMInputFormatterStyle.mm +188 -0
- package/ios/input/ENRMInputLayoutManager.h +9 -0
- package/ios/input/ENRMInputLayoutManager.mm +30 -0
- package/ios/input/ENRMInputListMarkerDrawer.h +32 -0
- package/ios/input/ENRMInputListMarkerDrawer.mm +426 -0
- package/ios/input/ENRMInputParser.h +5 -6
- package/ios/input/ENRMInputParser.mm +461 -178
- package/ios/input/ENRMInputRemend.mm +2 -2
- package/ios/input/ENRMInputStyleStateBuilder.h +40 -0
- package/ios/input/ENRMInputStyleStateBuilder.mm +60 -0
- package/ios/input/ENRMInputTextView.mm +103 -8
- package/ios/input/ENRMInputTypingAttributesController.h +42 -0
- package/ios/input/ENRMInputTypingAttributesController.mm +203 -0
- package/ios/input/ENRMLinkCoordinator.h +41 -0
- package/ios/input/ENRMLinkCoordinator.mm +89 -0
- package/ios/input/ENRMMarkdownSerializer.h +29 -5
- package/ios/input/ENRMMarkdownSerializer.mm +170 -48
- package/ios/input/ENRMMathAtom.h +26 -0
- package/ios/input/ENRMMathAtom.mm +12 -0
- package/ios/input/ENRMMentionCoordinator.h +45 -0
- package/ios/input/ENRMMentionCoordinator.mm +167 -0
- package/ios/input/ENRMRangeEditAdjustment.h +38 -0
- package/ios/input/ENRMRangeEditAdjustment.mm +91 -0
- package/ios/input/ENRMRangeStoreUtils.h +39 -0
- package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +6 -6
- package/ios/input/EnrichedMarkdownTextInput+Internal.h +11 -3
- package/ios/input/EnrichedMarkdownTextInput.h +7 -1
- package/ios/input/EnrichedMarkdownTextInput.mm +1089 -1023
- package/ios/input/InputStylePropsUtils.h +109 -68
- package/ios/input/internals/EnrichedMarkdownTextInputShadowNode.mm +2 -1
- package/ios/input/styles/ENRMBlockHandler.h +46 -0
- package/ios/input/styles/ENRMHeadingBlockHandler.h +15 -0
- package/ios/input/styles/ENRMHeadingBlockHandler.mm +38 -0
- package/ios/input/styles/ENRMOrderedListBlockHandler.h +14 -0
- package/ios/input/styles/ENRMOrderedListBlockHandler.mm +48 -0
- package/ios/input/styles/ENRMUnorderedListBlockHandler.h +15 -0
- package/ios/input/styles/ENRMUnorderedListBlockHandler.mm +51 -0
- package/ios/internals/ENRMViewFreeMeasurement.h +331 -0
- package/ios/internals/EnrichedMarkdownShadowNode.h +1 -3
- package/ios/internals/EnrichedMarkdownShadowNode.mm +16 -20
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +1 -3
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +15 -20
- package/ios/internals/MeasurementCache.h +33 -7
- package/ios/internals/ShadowMeasurementUtils.h +63 -58
- package/ios/math/ENRMMathChipController.h +74 -0
- package/ios/math/ENRMMathChipController.mm +476 -0
- package/ios/math/ENRMRaTeXBridge.swift +4 -1
- package/ios/parser/ENRMMarkdownParser.h +1 -8
- package/ios/parser/ENRMMarkdownParser.mm +2 -11
- package/ios/parser/MarkdownASTNode.h +2 -1
- package/ios/parser/MarkdownParserBridge.mm +4 -0
- package/ios/renderer/AttributedRenderer.m +20 -2
- package/ios/renderer/BaseRenderer.h +17 -2
- package/ios/renderer/BaseRenderer.m +30 -0
- package/ios/renderer/BlockquoteRenderer.h +1 -1
- package/ios/renderer/BlockquoteRenderer.m +144 -35
- package/ios/renderer/CodeBlockRenderer.h +1 -1
- package/ios/renderer/CodeBlockRenderer.m +27 -23
- package/ios/renderer/CodeRenderer.h +1 -1
- package/ios/renderer/CodeRenderer.m +3 -1
- package/ios/renderer/ENRMImageRenderer.h +1 -1
- package/ios/renderer/ENRMImageRenderer.m +5 -1
- package/ios/renderer/ENRMMathInlineRenderer.h +1 -1
- package/ios/renderer/ENRMMathInlineRenderer.m +5 -1
- package/ios/renderer/ENRMSpoilerRenderer.h +1 -1
- package/ios/renderer/ENRMSpoilerRenderer.m +3 -1
- package/ios/renderer/EmphasisRenderer.h +1 -1
- package/ios/renderer/EmphasisRenderer.m +3 -1
- package/ios/renderer/HeadingRenderer.h +1 -1
- package/ios/renderer/HeadingRenderer.m +13 -6
- package/ios/renderer/HighlightRenderer.h +1 -1
- package/ios/renderer/HighlightRenderer.m +3 -1
- package/ios/renderer/LinkRenderer.h +1 -1
- package/ios/renderer/LinkRenderer.m +3 -1
- package/ios/renderer/ListItemRenderer.h +13 -1
- package/ios/renderer/ListItemRenderer.m +102 -44
- package/ios/renderer/ListRenderer.h +2 -2
- package/ios/renderer/ListRenderer.m +63 -25
- package/ios/renderer/ParagraphRenderer.h +1 -1
- package/ios/renderer/ParagraphRenderer.m +16 -8
- package/ios/renderer/RenderContext.h +10 -0
- package/ios/renderer/RenderContext.m +44 -0
- package/ios/renderer/RendererFactory.m +14 -0
- package/ios/renderer/StrikethroughRenderer.h +1 -1
- package/ios/renderer/StrikethroughRenderer.m +3 -1
- package/ios/renderer/StrongRenderer.h +1 -1
- package/ios/renderer/StrongRenderer.m +3 -1
- package/ios/renderer/SubscriptRenderer.h +1 -1
- package/ios/renderer/SubscriptRenderer.m +3 -1
- package/ios/renderer/SuperscriptRenderer.h +1 -1
- package/ios/renderer/SuperscriptRenderer.m +3 -1
- package/ios/renderer/TextRenderer.h +2 -2
- package/ios/renderer/TextRenderer.m +3 -1
- package/ios/renderer/ThematicBreakRenderer.h +1 -1
- package/ios/renderer/ThematicBreakRenderer.m +3 -1
- package/ios/renderer/UnderlineRenderer.h +1 -1
- package/ios/renderer/UnderlineRenderer.m +3 -1
- package/ios/styles/StyleConfig.h +22 -0
- package/ios/styles/StyleConfig.mm +132 -0
- package/ios/utils/BlockquoteBorder.h +6 -0
- package/ios/utils/BlockquoteBorder.m +249 -52
- package/ios/utils/CodeBlockBackground.m +16 -2
- package/ios/utils/ENRMAtomicSize.h +65 -0
- package/ios/utils/ENRMBlockMenu.h +57 -0
- package/ios/utils/ENRMCodeBlockContent.h +37 -0
- package/ios/utils/ENRMCodeBlockContent.mm +79 -0
- package/ios/utils/ENRMCodeBlockHighlighter.h +37 -0
- package/ios/utils/ENRMCodeBlockHighlighter.mm +51 -0
- package/ios/utils/ENRMFeatureFlags.h +7 -0
- package/ios/utils/ENRMImageDownloader.h +11 -1
- package/ios/utils/ENRMImageDownloader.m +60 -16
- package/ios/utils/ENRMLocalImageLoader.h +31 -0
- package/ios/utils/ENRMLocalImageLoader.m +68 -0
- package/ios/utils/ENRMMathFallback.h +26 -0
- package/ios/utils/ENRMMathFallback.m +18 -0
- package/ios/utils/ENRMTextViewSetup.h +35 -17
- package/ios/utils/EditMenuUtils.h +22 -5
- package/ios/utils/EditMenuUtils.m +53 -9
- package/ios/utils/ImageRequestHeaderUtils.h +35 -0
- package/ios/utils/LastElementUtils.h +1 -0
- package/ios/utils/ListMarkerDrawer.m +29 -42
- package/ios/utils/MarkdownASTSerializer.m +4 -0
- package/ios/utils/ParagraphStyleUtils.h +5 -0
- package/ios/utils/ParagraphStyleUtils.m +77 -17
- package/ios/utils/RenderedMarkdownSegment.h +13 -1
- package/ios/utils/RenderedMarkdownSegment.m +20 -0
- package/ios/utils/SegmentRenderer.m +17 -4
- package/ios/utils/StreamingMarkdownFilter.h +11 -1
- package/ios/utils/StreamingMarkdownFilter.m +98 -2
- package/ios/utils/StylePropsUtils.h +63 -0
- package/ios/utils/TaskListTapUtils.m +24 -0
- package/ios/views/ENRMCodeBlockContainerView.h +53 -0
- package/ios/views/ENRMCodeBlockContainerView.m +559 -0
- package/ios/views/ENRMMathContainerView.h +16 -0
- package/ios/views/ENRMMathContainerView.m +74 -19
- package/ios/views/TableContainerView.h +22 -0
- package/ios/views/TableContainerView.m +261 -175
- package/jest.d.ts +1 -0
- package/jest.js +5 -0
- package/lib/module/EnrichedMarkdownNativeComponent.ts +73 -0
- package/lib/module/EnrichedMarkdownTextInput.js +84 -5
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +74 -0
- package/lib/module/headingDefaults.js +34 -0
- package/lib/module/headingDefaults.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +191 -0
- package/lib/module/jest/index.js.map +1 -0
- package/lib/module/native/EnrichedMarkdownText.js +37 -5
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +81 -19
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +56 -18
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/normalizeMarkdownTextInputStyle.js +62 -15
- package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
- package/lib/module/normalizeMenuItem.js +1 -1
- package/lib/module/utils/textInputState.js +26 -0
- package/lib/module/utils/textInputState.js.map +1 -0
- package/lib/module/web/EnrichedMarkdownText.js +9 -5
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/parseMarkdown.js +4 -3
- package/lib/module/web/parseMarkdown.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +6 -0
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/renderers/ListRenderers.js +7 -2
- package/lib/module/web/renderers/ListRenderers.js.map +1 -1
- package/lib/module/web/renderers/index.js +7 -4
- package/lib/module/web/renderers/index.js.map +1 -1
- package/lib/module/web/styles.js +83 -6
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +1 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/module/web/wasm/md4c.js +0 -0
- package/lib/module/web/wasm/md4c.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +66 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +94 -9
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +69 -5
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +66 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/headingDefaults.d.ts +9 -0
- package/lib/typescript/src/headingDefaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +1 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/jest/index.d.ts +6 -0
- package/lib/typescript/src/jest/index.d.ts.map +1 -0
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +19 -5
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +72 -0
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +27 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +104 -9
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +19 -0
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +19 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/utils/textInputState.d.ts +24 -0
- package/lib/typescript/src/utils/textInputState.d.ts.map +1 -0
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/web/parseMarkdown.d.ts +1 -1
- package/lib/typescript/src/web/parseMarkdown.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/ListRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/index.d.ts +2 -1
- package/lib/typescript/src/web/renderers/index.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts +2 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +6 -2
- package/lib/typescript/src/web/types.d.ts.map +1 -1
- package/lib/typescript/src/web/utils.d.ts.map +1 -1
- package/package.json +36 -9
- package/postinstall.mjs +131 -0
- package/ratex-version.json +21 -0
- package/src/EnrichedMarkdownNativeComponent.ts +73 -0
- package/src/EnrichedMarkdownTextInput.tsx +159 -8
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
- package/src/EnrichedMarkdownTextNativeComponent.ts +74 -0
- package/src/headingDefaults.ts +21 -0
- package/src/index.tsx +9 -0
- package/src/index.web.tsx +1 -0
- package/src/jest/index.tsx +215 -0
- package/src/native/EnrichedMarkdownText.tsx +53 -2
- package/src/normalizeMarkdownStyle.ts +102 -16
- package/src/normalizeMarkdownStyle.web.ts +63 -15
- package/src/normalizeMarkdownTextInputStyle.ts +110 -35
- package/src/normalizeMenuItem.ts +1 -1
- package/src/types/MarkdownStyle.ts +74 -0
- package/src/types/MarkdownStyleInternal.ts +39 -0
- package/src/types/MarkdownTextProps.ts +101 -9
- package/src/types/MarkdownTextProps.web.ts +19 -0
- package/src/types/events.ts +21 -0
- package/src/utils/textInputState.ts +35 -0
- package/src/web/EnrichedMarkdownText.tsx +16 -2
- package/src/web/parseMarkdown.ts +6 -2
- package/src/web/renderers/InlineRenderers.tsx +5 -0
- package/src/web/renderers/ListRenderers.tsx +14 -2
- package/src/web/renderers/index.tsx +6 -2
- package/src/web/styles.ts +89 -5
- package/src/web/types.ts +6 -1
- package/src/web/utils.ts +1 -0
- package/src/web/wasm/md4c.js +0 -0
- package/vendor-grammars.mjs +511 -0
- package/vendor-ratex.mjs +173 -0
- package/app.plugin.js +0 -2
- package/plugin/build/withAndroidMath.js +0 -19
- package/plugin/build/withIosMath.js +0 -28
- package/plugin/build/withReactNativeEnrichedMarkdown.js +0 -11
- package/plugin/src/withAndroidMath.ts +0 -24
- package/plugin/src/withIosMath.ts +0 -35
- package/plugin/src/withReactNativeEnrichedMarkdown.ts +0 -17
- package/plugin/tsconfig.build.json +0 -16
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
#import "EnrichedMarkdownTextInput.h"
|
|
2
2
|
#import "ContextMenuUtils.h"
|
|
3
3
|
#import "ENRMAutoLinkDetector.h"
|
|
4
|
+
#import "ENRMBlockEditCoordinator.h"
|
|
5
|
+
#import "ENRMBlockHandler.h"
|
|
6
|
+
#import "ENRMBlockStore.h"
|
|
7
|
+
#import "ENRMClipboardCoordinator.h"
|
|
4
8
|
#import "ENRMDetectorPipeline.h"
|
|
5
|
-
#import "
|
|
9
|
+
#import "ENRMEditPipeline.h"
|
|
10
|
+
#import "ENRMEditSession.h"
|
|
6
11
|
#import "ENRMFormattingRange.h"
|
|
7
12
|
#import "ENRMFormattingStore.h"
|
|
13
|
+
#import "ENRMInputEventEmitter.h"
|
|
8
14
|
#import "ENRMInputFormatter.h"
|
|
9
15
|
#import "ENRMInputLayoutManager.h"
|
|
10
16
|
#import "ENRMInputLinkPrompt.h"
|
|
11
|
-
#import "
|
|
17
|
+
#import "ENRMInputListMarkerDrawer.h"
|
|
12
18
|
#import "ENRMInputParser.h"
|
|
13
19
|
#import "ENRMInputTextView.h"
|
|
20
|
+
#import "ENRMInputTypingAttributesController.h"
|
|
21
|
+
#import "ENRMLinkCoordinator.h"
|
|
14
22
|
#import "ENRMLinkRegexConfig.h"
|
|
15
|
-
#import "
|
|
16
|
-
#import "
|
|
17
|
-
#if ENRICHED_MARKDOWN_MATH
|
|
18
|
-
#import "ENRMMathInlineAttachment.h"
|
|
19
|
-
#endif
|
|
23
|
+
#import "ENRMMathChipController.h"
|
|
24
|
+
#import "ENRMMentionCoordinator.h"
|
|
20
25
|
#import "ENRMStyleHandler.h"
|
|
21
26
|
#import "ENRMStyleMergingConfig.h"
|
|
22
27
|
#import "ENRMUIKit.h"
|
|
23
|
-
#import "ENRMWordsUtils.h"
|
|
24
28
|
#import "EnrichedMarkdownTextInput+Internal.h"
|
|
25
29
|
#import "InputStylePropsUtils.h"
|
|
26
30
|
#import "ParagraphStyleUtils.h"
|
|
@@ -45,17 +49,25 @@
|
|
|
45
49
|
using namespace facebook::react;
|
|
46
50
|
|
|
47
51
|
#if !TARGET_OS_OSX
|
|
52
|
+
// UIGestureRecognizerDelegate is for the math chip tap: it has to run alongside
|
|
53
|
+
// UITextView's own selection gestures, never instead of them.
|
|
48
54
|
@interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, UITextViewDelegate,
|
|
49
|
-
|
|
55
|
+
ENRMEditPipelineHost, ENRMInputEventEmitterDataSource,
|
|
56
|
+
ENRMInputTypingAttributesDataSource, UIGestureRecognizerDelegate>
|
|
50
57
|
#else
|
|
51
|
-
@interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, RCTBackedTextInputDelegate
|
|
58
|
+
@interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, RCTBackedTextInputDelegate,
|
|
59
|
+
ENRMEditPipelineHost, ENRMInputEventEmitterDataSource,
|
|
60
|
+
ENRMInputTypingAttributesDataSource>
|
|
52
61
|
#endif
|
|
53
62
|
- (void)setupTextView;
|
|
54
63
|
- (void)applyFormatting;
|
|
64
|
+
- (void)applyFormattingScopedToEditAtLocation:(NSUInteger)editLocation insertedLength:(NSUInteger)insertedLength;
|
|
55
65
|
- (void)toggleInlineStyle:(ENRMInputStyleType)styleType;
|
|
56
66
|
- (void)resetBaseTypingAttributes;
|
|
57
67
|
@end
|
|
58
68
|
|
|
69
|
+
static const NSTimeInterval kENRMAtomicSnapPollInterval = 0.1;
|
|
70
|
+
|
|
59
71
|
@implementation EnrichedMarkdownTextInput {
|
|
60
72
|
ENRMPlatformTextView *_textView;
|
|
61
73
|
ENRMInputLayoutManager *_layoutManager;
|
|
@@ -64,26 +76,30 @@ using namespace facebook::react;
|
|
|
64
76
|
ENRMInputFormatter *_formatter;
|
|
65
77
|
ENRMInputFormatterStyle *_formatterStyle;
|
|
66
78
|
ENRMFormattingStore *_formattingStore;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
BOOL _emitMarkdown;
|
|
79
|
+
ENRMBlockStore *_blockStore;
|
|
80
|
+
ENRMInputTypingAttributesController *_typingController;
|
|
81
|
+
ENRMInputEventEmitter *_inputEventEmitter;
|
|
82
|
+
ENRMEditSession *_editSession;
|
|
72
83
|
BOOL _blockSyntax;
|
|
73
84
|
NSString *_lastImportedMarkdown;
|
|
74
|
-
CFTimeInterval _lastTextChangeTime;
|
|
75
85
|
|
|
76
86
|
ENRMPlaceholderLabel *_placeholderLabel;
|
|
77
87
|
|
|
78
88
|
NSUInteger _lastTextLength;
|
|
79
89
|
NSRange _lastSelectedRange;
|
|
80
90
|
NSRange _preEditSelectedRange;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
BOOL _atomicSnapScheduled;
|
|
92
|
+
|
|
93
|
+
// Block type/level of the line being edited, captured before a text change so
|
|
94
|
+
// a Return that continues a list (or an autocorrect/paste that replaces the
|
|
95
|
+
// line) can restore the right block on the resulting line(s).
|
|
96
|
+
ENRMInputBlockType _preEditBlockType;
|
|
97
|
+
NSInteger _preEditBlockLevel;
|
|
98
|
+
BOOL _preEditParagraphWasEmpty;
|
|
99
|
+
// Whether the replaced range contained a line break, captured before the edit
|
|
100
|
+
// applies (the deleted characters are gone afterwards). Drives the full-vs-
|
|
101
|
+
// scoped reformat decision in handleTextChanged.
|
|
102
|
+
BOOL _preEditReplacedNewline;
|
|
87
103
|
|
|
88
104
|
#if TARGET_OS_OSX
|
|
89
105
|
NSScrollView *_scrollView;
|
|
@@ -91,25 +107,24 @@ using namespace facebook::react;
|
|
|
91
107
|
|
|
92
108
|
NSArray<NSString *> *_contextMenuItemTexts;
|
|
93
109
|
NSArray<NSString *> *_contextMenuItemIcons;
|
|
94
|
-
NSArray<NSString *> *_mentionIndicators;
|
|
95
|
-
NSString *_activeMentionIndicator;
|
|
96
|
-
NSRange _activeMentionRange;
|
|
97
|
-
NSString *_activeMentionText;
|
|
98
|
-
|
|
99
110
|
ENRMAutoLinkDetector *_autoLinkDetector;
|
|
100
111
|
ENRMDetectorPipeline *_detectorPipeline;
|
|
112
|
+
ENRMEditPipeline *_editPipeline;
|
|
113
|
+
ENRMBlockEditCoordinator *_blockCoordinator;
|
|
114
|
+
ENRMMentionCoordinator *_mentionCoordinator;
|
|
115
|
+
ENRMLinkCoordinator *_linkCoordinator;
|
|
116
|
+
ENRMClipboardCoordinator *_clipboardCoordinator;
|
|
117
|
+
ENRMMathChipController *_mathChipController;
|
|
118
|
+
|
|
119
|
+
// Buffer position of the math chip currently drawn selected, or NSNotFound.
|
|
120
|
+
NSUInteger _activeMathChipPosition;
|
|
121
|
+
// A caret move made by the chip tap itself must not clear the highlight the tap
|
|
122
|
+
// just set; consumed by the next selection change.
|
|
123
|
+
BOOL _suppressMathChipClearOnce;
|
|
101
124
|
|
|
102
125
|
ENRMWritingDirectionMode _writingDirectionMode;
|
|
103
126
|
NSWritingDirection _resolvedLayoutDirection;
|
|
104
127
|
|
|
105
|
-
/// One-shot guard: when YES, the next textViewDidChangeSelection skips
|
|
106
|
-
/// clearing math chip selections. Set by handleTapForMathChip right before
|
|
107
|
-
/// it moves the caret to the chip, so the caret update doesn't immediately
|
|
108
|
-
/// wipe the chip we just selected.
|
|
109
|
-
BOOL _suppressMathChipClearOnce;
|
|
110
|
-
/// Character index of the currently selected math chip, or NSNotFound.
|
|
111
|
-
NSUInteger _activeMathChipIndex;
|
|
112
|
-
|
|
113
128
|
ENRMInputSelectionMenuConfig _inputSelectionMenuConfig;
|
|
114
129
|
ENRMFormatMenuConfig _formatMenuConfig;
|
|
115
130
|
|
|
@@ -125,6 +140,8 @@ using namespace facebook::react;
|
|
|
125
140
|
NSString *_formatMenuLinkLabel;
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
@synthesize editSession = _editSession;
|
|
144
|
+
|
|
128
145
|
#pragma mark - Fabric lifecycle
|
|
129
146
|
|
|
130
147
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -144,20 +161,16 @@ using namespace facebook::react;
|
|
|
144
161
|
_props = defaultProps;
|
|
145
162
|
|
|
146
163
|
self.backgroundColor = [RCTUIColor clearColor];
|
|
147
|
-
_blockEmitting = NO;
|
|
148
164
|
_blockSyntax = YES;
|
|
149
165
|
_lastImportedMarkdown = nil;
|
|
150
166
|
_heightUpdateCounter = 0;
|
|
151
167
|
_formatter = [[ENRMInputFormatter alloc] init];
|
|
152
168
|
_formatterStyle = [[ENRMInputFormatterStyle alloc] init];
|
|
153
169
|
_formattingStore = [[ENRMFormattingStore alloc] init];
|
|
154
|
-
|
|
155
|
-
_pendingStyleRemovals = [NSMutableSet set];
|
|
170
|
+
_blockStore = [[ENRMBlockStore alloc] init];
|
|
156
171
|
_lastTextLength = 0;
|
|
157
172
|
_lastSelectedRange = NSMakeRange(0, 0);
|
|
158
|
-
|
|
159
|
-
_activeMentionRange = NSMakeRange(NSNotFound, 0);
|
|
160
|
-
_activeMentionText = @"";
|
|
173
|
+
_mentionCoordinator = [[ENRMMentionCoordinator alloc] initWithFormattingStore:_formattingStore];
|
|
161
174
|
|
|
162
175
|
_writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
|
|
163
176
|
_resolvedLayoutDirection =
|
|
@@ -166,12 +179,32 @@ using namespace facebook::react;
|
|
|
166
179
|
_formatMenuConfig = (ENRMFormatMenuConfig){
|
|
167
180
|
.bold = YES, .italic = YES, .underline = YES, .strikethrough = YES, .spoiler = YES, .link = YES};
|
|
168
181
|
|
|
182
|
+
_activeMathChipPosition = NSNotFound;
|
|
169
183
|
_suppressMathChipClearOnce = NO;
|
|
170
|
-
_activeMathChipIndex = NSNotFound;
|
|
171
184
|
|
|
172
185
|
[self setupTextView];
|
|
186
|
+
_mathChipController = [[ENRMMathChipController alloc] initWithTextView:_textView style:_formatterStyle];
|
|
187
|
+
_editSession = [[ENRMEditSession alloc] initWithTextView:_textView];
|
|
188
|
+
_typingController = [[ENRMInputTypingAttributesController alloc] initWithTextView:_textView
|
|
189
|
+
formatterStyle:_formatterStyle
|
|
190
|
+
dataSource:self
|
|
191
|
+
editSession:_editSession];
|
|
192
|
+
_inputEventEmitter = [[ENRMInputEventEmitter alloc] initWithDataSource:self];
|
|
173
193
|
|
|
174
194
|
[self setupDetectorPipeline];
|
|
195
|
+
|
|
196
|
+
_editPipeline = [[ENRMEditPipeline alloc] initWithFormattingStore:_formattingStore
|
|
197
|
+
blockStore:_blockStore
|
|
198
|
+
formatter:_formatter
|
|
199
|
+
detectorPipeline:_detectorPipeline
|
|
200
|
+
host:self];
|
|
201
|
+
_blockCoordinator = [[ENRMBlockEditCoordinator alloc] initWithBlockStore:_blockStore];
|
|
202
|
+
_linkCoordinator = [[ENRMLinkCoordinator alloc] initWithFormattingStore:_formattingStore
|
|
203
|
+
autoLinkDetector:_autoLinkDetector];
|
|
204
|
+
_clipboardCoordinator = [[ENRMClipboardCoordinator alloc] initWithFormattingStore:_formattingStore
|
|
205
|
+
blockStore:_blockStore
|
|
206
|
+
transientRangeProvider:_detectorPipeline
|
|
207
|
+
formatter:_formatter];
|
|
175
208
|
}
|
|
176
209
|
return self;
|
|
177
210
|
}
|
|
@@ -184,7 +217,11 @@ using namespace facebook::react;
|
|
|
184
217
|
|
|
185
218
|
__weak EnrichedMarkdownTextInput *weakSelf = self;
|
|
186
219
|
_autoLinkDetector.onLinkDetected = ^(NSString *text, NSString *url, NSRange range) {
|
|
187
|
-
|
|
220
|
+
EnrichedMarkdownTextInput *strongSelf = weakSelf;
|
|
221
|
+
if (strongSelf == nil) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
[strongSelf->_inputEventEmitter emitOnLinkDetectedWithText:text url:url range:range];
|
|
188
225
|
};
|
|
189
226
|
|
|
190
227
|
_detectorPipeline = [[ENRMDetectorPipeline alloc] init];
|
|
@@ -205,6 +242,8 @@ using namespace facebook::react;
|
|
|
205
242
|
ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero textContainer:textContainer];
|
|
206
243
|
#else
|
|
207
244
|
ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero];
|
|
245
|
+
_layoutManager = [[ENRMInputLayoutManager alloc] init];
|
|
246
|
+
[inputTextView.textContainer replaceLayoutManager:_layoutManager];
|
|
208
247
|
#endif
|
|
209
248
|
inputTextView.markdownTextInput = self;
|
|
210
249
|
_textView = inputTextView;
|
|
@@ -241,124 +280,89 @@ using namespace facebook::react;
|
|
|
241
280
|
_placeholderLabel = ENRMCreatePlaceholderLabel(_textView, _formatterStyle.baseFont);
|
|
242
281
|
#if !TARGET_OS_OSX
|
|
243
282
|
_placeholderLabel.adjustsFontForContentSizeCategory = YES;
|
|
283
|
+
_placeholderLabel.accessibilityElementsHidden = YES;
|
|
284
|
+
_placeholderLabel.isAccessibilityElement = NO;
|
|
244
285
|
#endif
|
|
245
286
|
|
|
246
287
|
[self resetBaseTypingAttributes];
|
|
247
288
|
|
|
248
|
-
#if
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
289
|
+
#if !TARGET_OS_OSX
|
|
290
|
+
if (ENRMMathChipController.isAvailable) {
|
|
291
|
+
UITapGestureRecognizer *mathTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
|
292
|
+
action:@selector(handleMathChipTap:)];
|
|
293
|
+
// The text view's own tap gestures must still run: swallowing the touch is
|
|
294
|
+
// what made the first tap on a chip fail to focus the field and raise the
|
|
295
|
+
// keyboard. This recognizer only adds behaviour, it never replaces any.
|
|
296
|
+
mathTap.cancelsTouchesInView = NO;
|
|
297
|
+
mathTap.delaysTouchesBegan = NO;
|
|
298
|
+
mathTap.delaysTouchesEnded = NO;
|
|
299
|
+
mathTap.delegate = self;
|
|
300
|
+
[_textView addGestureRecognizer:mathTap];
|
|
301
|
+
}
|
|
256
302
|
#endif
|
|
257
303
|
}
|
|
258
304
|
|
|
259
|
-
#if
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// `locationInView:_textView` reports). Single source of truth for both the tap
|
|
263
|
-
// hit-test and the rect reported to JS.
|
|
264
|
-
- (CGRect)mathChipRectInTextViewAtIndex:(NSUInteger)index layoutManager:(NSLayoutManager *)lm
|
|
305
|
+
#if !TARGET_OS_OSX
|
|
306
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
307
|
+
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
265
308
|
{
|
|
266
|
-
|
|
267
|
-
return CGRectNull;
|
|
268
|
-
NSRange glyphRange = [lm glyphRangeForCharacterRange:NSMakeRange(index, 1) actualCharacterRange:NULL];
|
|
269
|
-
CGRect rect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
|
|
270
|
-
rect.origin.x += _textView.textContainerInset.left;
|
|
271
|
-
rect.origin.y += _textView.textContainerInset.top;
|
|
272
|
-
return rect;
|
|
309
|
+
return YES;
|
|
273
310
|
}
|
|
311
|
+
#endif
|
|
312
|
+
|
|
313
|
+
#if !TARGET_OS_OSX
|
|
314
|
+
#pragma mark - Accessibility
|
|
274
315
|
|
|
275
|
-
|
|
316
|
+
// The custom TextKit 1 stack leaves the inner UITextView invisible to VoiceOver, so
|
|
317
|
+
// the host vends the content itself as one readable element.
|
|
318
|
+
- (BOOL)isAccessibilityElement
|
|
276
319
|
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
CGPoint loc = [gesture locationInView:_textView];
|
|
280
|
-
UITextPosition *pos = [_textView closestPositionToPoint:loc];
|
|
281
|
-
if (!pos)
|
|
282
|
-
return;
|
|
283
|
-
NSInteger idx = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:pos];
|
|
284
|
-
if (idx < 0)
|
|
285
|
-
return;
|
|
286
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
287
|
-
NSLayoutManager *lm = storage.layoutManagers.firstObject;
|
|
288
|
-
// The closest position can land just past the chip, so probe both `idx` and `idx-1`.
|
|
289
|
-
// `closestPositionToPoint:` has no distance bound — it clamps a tap anywhere in the
|
|
290
|
-
// text view (blank space past the text, textContainerInset padding) onto the nearest
|
|
291
|
-
// character. Requiring the touch to fall inside the chip's own glyph rect is what
|
|
292
|
-
// keeps a tap beside a chip from reading as a tap on it.
|
|
293
|
-
ENRMMathInlineAttachment *att = nil;
|
|
294
|
-
NSUInteger chipIdx = NSNotFound;
|
|
295
|
-
for (NSInteger probe = idx; probe >= idx - 1; probe--) {
|
|
296
|
-
if (probe < 0 || (NSUInteger)probe >= storage.length)
|
|
297
|
-
continue;
|
|
298
|
-
id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)probe effectiveRange:NULL];
|
|
299
|
-
if (![v isKindOfClass:[ENRMMathInlineAttachment class]])
|
|
300
|
-
continue;
|
|
301
|
-
// An empty rect means the layout could not give us the chip's extent; accept the
|
|
302
|
-
// candidate then, rather than making the chip untappable.
|
|
303
|
-
CGRect rect = [self mathChipRectInTextViewAtIndex:(NSUInteger)probe layoutManager:lm];
|
|
304
|
-
if (!CGRectIsEmpty(rect) && !CGRectContainsPoint(rect, loc))
|
|
305
|
-
continue;
|
|
306
|
-
att = v;
|
|
307
|
-
chipIdx = (NSUInteger)probe;
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
if (!att)
|
|
311
|
-
return;
|
|
320
|
+
return YES;
|
|
321
|
+
}
|
|
312
322
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
[lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
|
|
319
|
-
[lm ensureLayoutForCharacterRange:chipRange];
|
|
323
|
+
- (NSString *)accessibilityValue
|
|
324
|
+
{
|
|
325
|
+
NSString *text = ENRMGetPlainText(_textView);
|
|
326
|
+
if (text.length > 0) {
|
|
327
|
+
return text;
|
|
320
328
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
if (_placeholderLabel.text.length > 0) {
|
|
330
|
+
return _placeholderLabel.text;
|
|
331
|
+
}
|
|
332
|
+
return [super accessibilityValue];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
- (BOOL)accessibilityActivate
|
|
336
|
+
{
|
|
337
|
+
ENRMFocusTextView(_textView);
|
|
338
|
+
[super accessibilityActivate];
|
|
339
|
+
return YES;
|
|
340
|
+
}
|
|
341
|
+
#endif
|
|
331
342
|
|
|
332
|
-
|
|
333
|
-
CGRect glyphRect = [self mathChipRectInTextViewAtIndex:chipIdx layoutManager:lm];
|
|
334
|
-
CGRect rectInSelf = [self.contentView convertRect:glyphRect fromView:_textView];
|
|
343
|
+
#pragma mark - Paste
|
|
335
344
|
|
|
336
|
-
|
|
345
|
+
- (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown
|
|
346
|
+
{
|
|
347
|
+
[_inputEventEmitter emitOnPasteWithText:text markdown:markdown];
|
|
337
348
|
}
|
|
338
349
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
rect:(CGRect)rect
|
|
350
|
+
/// The single exit for an intercepted paste. One command rather than two makes
|
|
351
|
+
/// "never swallow a paste" structural: the branch that turns the clipboard into
|
|
352
|
+
/// something else still has to come through here, with empty strings.
|
|
353
|
+
- (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown
|
|
344
354
|
{
|
|
345
|
-
|
|
346
|
-
|
|
355
|
+
// Round-trips the formatting when the copy came from an input of ours;
|
|
356
|
+
// without this a copy-then-paste inside the app would land as flat text.
|
|
357
|
+
if (markdown.length > 0) {
|
|
358
|
+
[self pasteMarkdown:markdown];
|
|
347
359
|
return;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
.displayMode = displayMode ? true : false,
|
|
353
|
-
.x = rect.origin.x,
|
|
354
|
-
.y = rect.origin.y,
|
|
355
|
-
.width = rect.size.width,
|
|
356
|
-
.height = rect.size.height,
|
|
357
|
-
});
|
|
360
|
+
}
|
|
361
|
+
// Empty text with an empty selection is a zero-length edit: it still emits
|
|
362
|
+
// onChangeText with unchanged content, which the caller reads as a no-op.
|
|
363
|
+
[self replaceSelectedTextWith:text formattingRanges:@[]];
|
|
358
364
|
}
|
|
359
365
|
|
|
360
|
-
#endif
|
|
361
|
-
|
|
362
366
|
#pragma mark - State
|
|
363
367
|
|
|
364
368
|
- (void)updateState:(const facebook::react::State::Shared &)state
|
|
@@ -456,6 +460,10 @@ using namespace facebook::react;
|
|
|
456
460
|
}
|
|
457
461
|
}
|
|
458
462
|
|
|
463
|
+
if (newViewProps.interceptPaste != oldViewProps.interceptPaste) {
|
|
464
|
+
_interceptPaste = newViewProps.interceptPaste;
|
|
465
|
+
}
|
|
466
|
+
|
|
459
467
|
if (newViewProps.multiline != oldViewProps.multiline) {
|
|
460
468
|
_textView.textContainer.maximumNumberOfLines = newViewProps.multiline ? 0 : 1;
|
|
461
469
|
_textView.textContainer.lineBreakMode =
|
|
@@ -483,7 +491,7 @@ using namespace facebook::react;
|
|
|
483
491
|
ENRMApplySelectionColor(_textView, newViewProps.selectionColor);
|
|
484
492
|
}
|
|
485
493
|
|
|
486
|
-
|
|
494
|
+
_inputEventEmitter.emitMarkdown = newViewProps.isOnChangeMarkdownSet;
|
|
487
495
|
|
|
488
496
|
{
|
|
489
497
|
auto configFromProp = [](const auto &prop) {
|
|
@@ -551,11 +559,8 @@ using namespace facebook::react;
|
|
|
551
559
|
[indicators addObject:value];
|
|
552
560
|
}
|
|
553
561
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
[self clearActiveMention:_activeMentionIndicator];
|
|
557
|
-
}
|
|
558
|
-
[self updateActiveMention];
|
|
562
|
+
[self dispatchMentionEvents:[_mentionCoordinator setIndicators:indicators]];
|
|
563
|
+
[self dispatchMentionUpdate];
|
|
559
564
|
}
|
|
560
565
|
|
|
561
566
|
BOOL styleChanged = applyInputStyleProps(_formatterStyle, newViewProps, oldViewProps);
|
|
@@ -592,9 +597,11 @@ using namespace facebook::react;
|
|
|
592
597
|
[self applyFormatting];
|
|
593
598
|
}
|
|
594
599
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
600
|
+
// Chips cache the style they were built with, so a style pass has to re-stamp
|
|
601
|
+
// them. Not gated on a style diff: every field is a flat scalar on
|
|
602
|
+
// `_formatterStyle`, so there is no cheap "did the math style change" test,
|
|
603
|
+
// and `refreshAll` already no-ops when the buffer holds no chip.
|
|
604
|
+
[_mathChipController refreshAll];
|
|
598
605
|
|
|
599
606
|
[self requestHeightUpdate];
|
|
600
607
|
} else if (writingDirectionChanged && _textView.textStorage.length > 0) {
|
|
@@ -618,20 +625,23 @@ using namespace facebook::react;
|
|
|
618
625
|
return;
|
|
619
626
|
}
|
|
620
627
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
628
|
+
NSUInteger textLength = _textView.textStorage.length;
|
|
629
|
+
if (textLength == 0) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
NSRange wholeRange = NSMakeRange(0, textLength);
|
|
634
|
+
[_textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:NULL];
|
|
635
|
+
[_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
|
|
636
|
+
[_textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
|
|
637
|
+
|
|
638
|
+
CGSize measuredSize = [self measureSize:_textView.frame.size.width];
|
|
639
|
+
CGSize currentSize = _textView.contentSize;
|
|
640
|
+
BOOL sizeChanged =
|
|
641
|
+
fabs(currentSize.width - measuredSize.width) > 0.5 || fabs(currentSize.height - measuredSize.height) > 0.5;
|
|
642
|
+
if (sizeChanged) {
|
|
643
|
+
ENRMSetContentSize(_textView, measuredSize);
|
|
644
|
+
}
|
|
635
645
|
}
|
|
636
646
|
|
|
637
647
|
#pragma mark - Window attachment
|
|
@@ -685,14 +695,6 @@ using namespace facebook::react;
|
|
|
685
695
|
[_textView mouseDown:event];
|
|
686
696
|
}
|
|
687
697
|
|
|
688
|
-
#else // iOS
|
|
689
|
-
|
|
690
|
-
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
691
|
-
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
692
|
-
{
|
|
693
|
-
return YES;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
698
|
#endif
|
|
697
699
|
|
|
698
700
|
#if !TARGET_OS_OSX
|
|
@@ -713,11 +715,34 @@ using namespace facebook::react;
|
|
|
713
715
|
}
|
|
714
716
|
#endif
|
|
715
717
|
|
|
718
|
+
#pragma mark - ENRMEditPipelineHost
|
|
719
|
+
|
|
720
|
+
/// Backs `onChangeText`. A math chip stays as its raw `kENRMObjectReplacement`
|
|
721
|
+
/// character here — do not strip or expand it. `onChangeSelection`,
|
|
722
|
+
/// `onCaretRectChange` and the three math commands all speak in buffer offsets
|
|
723
|
+
/// where one formula is exactly one character, so removing it would put every
|
|
724
|
+
/// offset JS holds out by one per chip, and expanding it to `$latex$` would be
|
|
725
|
+
/// worse still. Use `onChangeMarkdown` when you want `$...$`.
|
|
726
|
+
- (NSString *)plainText
|
|
727
|
+
{
|
|
728
|
+
return ENRMGetPlainText(_textView);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
- (NSTextStorage *)textStorage
|
|
732
|
+
{
|
|
733
|
+
return _textView.textStorage;
|
|
734
|
+
}
|
|
735
|
+
|
|
716
736
|
#pragma mark - Placeholder
|
|
717
737
|
|
|
718
738
|
- (void)updatePlaceholderVisibility
|
|
719
739
|
{
|
|
720
|
-
|
|
740
|
+
// Hide the placeholder once there's any content OR any block has been started
|
|
741
|
+
// (e.g. an empty bullet/heading line with only a zero-length anchor): the block
|
|
742
|
+
// marker or indent would otherwise overlap the placeholder text.
|
|
743
|
+
BOOL hasText = ENRMGetPlainText(_textView).length > 0;
|
|
744
|
+
BOOL hasBlock = _blockStore.allRanges.count > 0;
|
|
745
|
+
_placeholderLabel.hidden = hasText || hasBlock;
|
|
721
746
|
}
|
|
722
747
|
|
|
723
748
|
#pragma mark - Markdown import
|
|
@@ -728,167 +753,140 @@ using namespace facebook::react;
|
|
|
728
753
|
ENRMInputParser *parser = [[ENRMInputParser alloc] init];
|
|
729
754
|
ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
|
|
730
755
|
|
|
731
|
-
|
|
756
|
+
[_editSession enterPhase:ENRMEditPhaseImporting];
|
|
732
757
|
|
|
733
|
-
|
|
758
|
+
[_editSession enterPhase:ENRMEditPhaseFormatting];
|
|
734
759
|
ENRMSetPlainText(_textView, parsed.plainText);
|
|
760
|
+
[_editSession enterPhase:ENRMEditPhaseImporting];
|
|
735
761
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
unichar c = [storage.string characterAtIndex:atom.position];
|
|
744
|
-
if (c != (unichar)0xFFFC)
|
|
745
|
-
continue;
|
|
746
|
-
ENRMMathInlineAttachment *att = [self createMathAttachmentWithLatex:atom.latex displayMode:atom.displayMode];
|
|
747
|
-
NSAttributedString *replacement = [NSAttributedString attributedStringWithAttachment:att];
|
|
748
|
-
[storage replaceCharactersInRange:NSMakeRange(atom.position, 1) withAttributedString:replacement];
|
|
749
|
-
if (atom.displayMode && _formatterStyle.mathBlockAlignment != NSTextAlignmentNatural) {
|
|
750
|
-
NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
|
|
751
|
-
ps.alignment = _formatterStyle.mathBlockAlignment;
|
|
752
|
-
[storage addAttribute:NSParagraphStyleAttributeName value:ps range:NSMakeRange(atom.position, 1)];
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
[storage endEditing];
|
|
756
|
-
}
|
|
757
|
-
#endif
|
|
758
|
-
|
|
759
|
-
_isApplyingFormatting = NO;
|
|
762
|
+
// Stamped inside the Importing phase, before the stores are read back: the
|
|
763
|
+
// parser already reserved one placeholder character per formula, so this only
|
|
764
|
+
// hangs an attachment on characters that are part of `parsed.plainText`. No
|
|
765
|
+
// deferral is needed the way Android needs one — `applyInputStyleProps` runs at
|
|
766
|
+
// the top of `updateProps`, so `_formatterStyle` is current before a
|
|
767
|
+
// `defaultValue` import ever reaches here.
|
|
768
|
+
[_mathChipController applyAtoms:parsed.mathAtoms offset:0];
|
|
760
769
|
|
|
761
770
|
[_formattingStore setRanges:parsed.formattingRanges];
|
|
771
|
+
[_blockStore setRanges:parsed.blockRanges];
|
|
762
772
|
_lastTextLength = parsed.plainText.length;
|
|
763
773
|
_lastSelectedRange = _textView.selectedRange;
|
|
774
|
+
|
|
775
|
+
[_editSession exitPhase];
|
|
764
776
|
[self applyFormatting];
|
|
765
777
|
[self updatePlaceholderVisibility];
|
|
766
778
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
#if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
|
|
771
|
-
|
|
772
|
-
- (void)applyMathStyleToAttachment:(ENRMMathInlineAttachment *)att
|
|
773
|
-
{
|
|
774
|
-
if (att.displayMode) {
|
|
775
|
-
att.fontSize = _formatterStyle.mathBlockFontSize > 0 ? _formatterStyle.mathBlockFontSize : 20.0;
|
|
776
|
-
att.mathTextColor = _formatterStyle.mathBlockColor ?: _formatterStyle.baseTextColor;
|
|
777
|
-
att.containerBackgroundColor = _formatterStyle.mathBlockBackgroundColor;
|
|
778
|
-
att.containerBorderColor = _formatterStyle.mathBlockBorderColor;
|
|
779
|
-
att.containerBorderWidth = _formatterStyle.mathBlockBorderWidth;
|
|
780
|
-
att.containerBorderRadius = _formatterStyle.mathBlockBorderRadius;
|
|
781
|
-
att.containerPaddingHorizontal = _formatterStyle.mathBlockPaddingHorizontal;
|
|
782
|
-
att.containerPaddingVertical = _formatterStyle.mathBlockPaddingVertical;
|
|
783
|
-
att.blockMarginTop = _formatterStyle.mathBlockMarginTop;
|
|
784
|
-
att.blockMarginBottom = _formatterStyle.mathBlockMarginBottom;
|
|
785
|
-
att.blockAlignment = _formatterStyle.mathBlockAlignment;
|
|
786
|
-
att.selectedBackgroundColor = _formatterStyle.mathBlockSelectedBackgroundColor;
|
|
787
|
-
att.selectedBorderColor = _formatterStyle.mathBlockSelectedBorderColor;
|
|
788
|
-
att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathBlockFontWeight);
|
|
789
|
-
} else {
|
|
790
|
-
att.fontSize = _formatterStyle.mathFontSize > 0 ? _formatterStyle.mathFontSize : 16.0;
|
|
791
|
-
att.mathTextColor = _formatterStyle.mathColor ?: _formatterStyle.baseTextColor;
|
|
792
|
-
att.containerBackgroundColor = _formatterStyle.mathBackgroundColor;
|
|
793
|
-
att.containerBorderColor = _formatterStyle.mathBorderColor;
|
|
794
|
-
att.containerBorderWidth = _formatterStyle.mathBorderWidth;
|
|
795
|
-
att.containerBorderRadius = _formatterStyle.mathBorderRadius;
|
|
796
|
-
att.containerPaddingHorizontal = _formatterStyle.mathPaddingHorizontal;
|
|
797
|
-
att.containerPaddingVertical = _formatterStyle.mathPaddingVertical;
|
|
798
|
-
att.blockMarginTop = 0;
|
|
799
|
-
att.blockMarginBottom = 0;
|
|
800
|
-
att.blockAlignment = NSTextAlignmentNatural;
|
|
801
|
-
att.selectedBackgroundColor = _formatterStyle.mathSelectedBackgroundColor;
|
|
802
|
-
att.selectedBorderColor = _formatterStyle.mathSelectedBorderColor;
|
|
803
|
-
att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathFontWeight);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
- (ENRMMathInlineAttachment *)createMathAttachmentWithLatex:(NSString *)latex displayMode:(BOOL)displayMode
|
|
808
|
-
{
|
|
809
|
-
ENRMMathInlineAttachment *att = [[ENRMMathInlineAttachment alloc] init];
|
|
810
|
-
att.latex = latex ?: @"";
|
|
811
|
-
att.displayMode = displayMode;
|
|
812
|
-
[self applyMathStyleToAttachment:att];
|
|
813
|
-
return att;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
- (void)refreshAllMathChips
|
|
817
|
-
{
|
|
818
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
819
|
-
if (storage.length == 0)
|
|
820
|
-
return;
|
|
821
|
-
__weak EnrichedMarkdownTextInput *weakSelf = self;
|
|
822
|
-
[storage beginEditing];
|
|
823
|
-
[storage enumerateAttribute:NSAttachmentAttributeName
|
|
824
|
-
inRange:NSMakeRange(0, storage.length)
|
|
825
|
-
options:0
|
|
826
|
-
usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
827
|
-
EnrichedMarkdownTextInput *strongSelf = weakSelf;
|
|
828
|
-
if (!strongSelf)
|
|
829
|
-
return;
|
|
830
|
-
if ([value isKindOfClass:[ENRMMathInlineAttachment class]]) {
|
|
831
|
-
ENRMMathInlineAttachment *att = value;
|
|
832
|
-
[strongSelf applyMathStyleToAttachment:att];
|
|
833
|
-
[att invalidateRender];
|
|
834
|
-
}
|
|
835
|
-
}];
|
|
836
|
-
[storage endEditing];
|
|
837
|
-
|
|
838
|
-
NSLayoutManager *lm = storage.layoutManagers.firstObject;
|
|
839
|
-
if (lm) {
|
|
840
|
-
NSRange fullRange = NSMakeRange(0, storage.length);
|
|
841
|
-
[lm invalidateLayoutForCharacterRange:fullRange actualCharacterRange:NULL];
|
|
842
|
-
[lm ensureLayoutForCharacterRange:fullRange];
|
|
779
|
+
if (parsed.plainText.length == 0) {
|
|
780
|
+
[self resetBaseTypingAttributes];
|
|
843
781
|
}
|
|
844
|
-
ENRMSetNeedsDisplay(_textView);
|
|
845
782
|
}
|
|
846
783
|
|
|
847
|
-
#endif
|
|
848
|
-
|
|
849
784
|
- (void)replaceTextInRange:(NSRange)selection
|
|
850
785
|
withText:(NSString *)text
|
|
851
786
|
formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
|
|
787
|
+
blockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
|
|
852
788
|
{
|
|
853
789
|
NSUInteger editLocation = selection.location;
|
|
854
790
|
|
|
855
|
-
|
|
791
|
+
[_editSession enterPhase:ENRMEditPhaseFormatting];
|
|
856
792
|
ENRMReplaceTextInRange(_textView, text, selection);
|
|
857
|
-
|
|
793
|
+
[_editSession exitPhase];
|
|
858
794
|
|
|
859
|
-
[
|
|
795
|
+
NSString *plainText = [self adjustStoresForEditAtLocation:editLocation
|
|
796
|
+
deletedLength:selection.length
|
|
797
|
+
insertedLength:text.length];
|
|
860
798
|
|
|
861
799
|
for (ENRMFormattingRange *range in ranges) {
|
|
862
800
|
NSRange shifted = NSMakeRange(range.range.location + editLocation, range.range.length);
|
|
863
801
|
[_formattingStore addRange:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
|
|
864
802
|
}
|
|
865
803
|
|
|
866
|
-
|
|
804
|
+
for (ENRMBlockRange *block in blockRanges) {
|
|
805
|
+
NSRange shifted = NSMakeRange(block.range.location + editLocation, block.range.length);
|
|
806
|
+
[_blockStore setBlockType:block.type level:block.level forParagraphRange:shifted inText:plainText];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
_lastTextLength = plainText.length;
|
|
867
810
|
_lastSelectedRange = _textView.selectedRange;
|
|
868
811
|
|
|
869
812
|
[self applyFormatting];
|
|
870
813
|
|
|
871
|
-
[_detectorPipeline processTextChange:
|
|
872
|
-
modificationRange:NSMakeRange(editLocation, text.length)];
|
|
814
|
+
[_detectorPipeline processTextChange:plainText modificationRange:NSMakeRange(editLocation, text.length)];
|
|
873
815
|
|
|
874
816
|
[self updatePlaceholderVisibility];
|
|
875
|
-
[
|
|
876
|
-
[
|
|
877
|
-
[
|
|
817
|
+
[_inputEventEmitter emitOnChangeText];
|
|
818
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
819
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
878
820
|
[self requestHeightUpdate];
|
|
879
821
|
[self scheduleRelayoutIfNeeded];
|
|
880
822
|
}
|
|
881
823
|
|
|
824
|
+
- (void)replaceTextInRange:(NSRange)selection
|
|
825
|
+
withText:(NSString *)text
|
|
826
|
+
formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
|
|
827
|
+
{
|
|
828
|
+
[self replaceTextInRange:selection withText:text formattingRanges:ranges blockRanges:@[]];
|
|
829
|
+
}
|
|
830
|
+
|
|
882
831
|
- (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
|
|
883
832
|
{
|
|
884
833
|
[self replaceTextInRange:_textView.selectedRange withText:text formattingRanges:ranges];
|
|
885
834
|
}
|
|
886
835
|
|
|
836
|
+
/// Insertion is literal: the string's characters land at the cursor exactly as
|
|
837
|
+
/// given. Markdown parsing consumes leading and trailing newlines as block
|
|
838
|
+
/// structure, so they are split off before the parse and re-attached verbatim;
|
|
839
|
+
/// without this, inserting "\n- item\n" mid-paragraph would merge the list line
|
|
840
|
+
/// with the surrounding text. Callers decide separation by including (or
|
|
841
|
+
/// omitting) newlines in the string.
|
|
887
842
|
- (void)pasteMarkdown:(NSString *)markdown
|
|
888
843
|
{
|
|
844
|
+
NSUInteger lead = 0;
|
|
845
|
+
while (lead < markdown.length && [markdown characterAtIndex:lead] == '\n') {
|
|
846
|
+
lead++;
|
|
847
|
+
}
|
|
848
|
+
NSUInteger trail = 0;
|
|
849
|
+
while (lead + trail < markdown.length && [markdown characterAtIndex:markdown.length - 1 - trail] == '\n') {
|
|
850
|
+
trail++;
|
|
851
|
+
}
|
|
852
|
+
NSString *core = [markdown substringWithRange:NSMakeRange(lead, markdown.length - lead - trail)];
|
|
853
|
+
|
|
889
854
|
ENRMInputParser *parser = [[ENRMInputParser alloc] init];
|
|
890
|
-
ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:
|
|
891
|
-
|
|
855
|
+
ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:core blockSyntax:_blockSyntax];
|
|
856
|
+
|
|
857
|
+
if (lead == 0 && trail == 0) {
|
|
858
|
+
NSUInteger insertLocation = _textView.selectedRange.location;
|
|
859
|
+
[self replaceTextInRange:_textView.selectedRange
|
|
860
|
+
withText:parsed.plainText
|
|
861
|
+
formattingRanges:parsed.formattingRanges
|
|
862
|
+
blockRanges:parsed.blockRanges];
|
|
863
|
+
// Positions are relative to the parsed fragment, so shift them onto the spot
|
|
864
|
+
// it landed. Pasted markdown carries formulas like any other source.
|
|
865
|
+
[_mathChipController applyAtoms:parsed.mathAtoms offset:insertLocation];
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
NSString *prefix = [@"" stringByPaddingToLength:lead withString:@"\n" startingAtIndex:0];
|
|
870
|
+
NSString *suffix = [@"" stringByPaddingToLength:trail withString:@"\n" startingAtIndex:0];
|
|
871
|
+
NSString *plainText = [NSString stringWithFormat:@"%@%@%@", prefix, parsed.plainText, suffix];
|
|
872
|
+
|
|
873
|
+
NSMutableArray<ENRMFormattingRange *> *formattingRanges =
|
|
874
|
+
[NSMutableArray arrayWithCapacity:parsed.formattingRanges.count];
|
|
875
|
+
for (ENRMFormattingRange *range in parsed.formattingRanges) {
|
|
876
|
+
NSRange shifted = NSMakeRange(range.range.location + lead, range.range.length);
|
|
877
|
+
[formattingRanges addObject:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
|
|
878
|
+
}
|
|
879
|
+
NSMutableArray<ENRMBlockRange *> *blockRanges = [NSMutableArray arrayWithCapacity:parsed.blockRanges.count];
|
|
880
|
+
for (ENRMBlockRange *block in parsed.blockRanges) {
|
|
881
|
+
NSRange shifted = NSMakeRange(block.range.location + lead, block.range.length);
|
|
882
|
+
[blockRanges addObject:[ENRMBlockRange rangeWithType:block.type range:shifted level:block.level]];
|
|
883
|
+
}
|
|
884
|
+
NSUInteger insertLocation = _textView.selectedRange.location;
|
|
885
|
+
[self replaceTextInRange:_textView.selectedRange
|
|
886
|
+
withText:plainText
|
|
887
|
+
formattingRanges:formattingRanges
|
|
888
|
+
blockRanges:blockRanges];
|
|
889
|
+
[_mathChipController applyAtoms:parsed.mathAtoms offset:insertLocation + lead];
|
|
892
890
|
}
|
|
893
891
|
|
|
894
892
|
#pragma mark - Formatting
|
|
@@ -903,17 +901,44 @@ using namespace facebook::react;
|
|
|
903
901
|
|
|
904
902
|
- (void)applyFormatting
|
|
905
903
|
{
|
|
906
|
-
|
|
904
|
+
[self applyFormattingScopedToRange:NSMakeRange(0, ENRMGetPlainText(_textView).length)];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/// Per-keystroke variant: re-applies inline and block attributes only on the
|
|
908
|
+
/// line(s) touched by the edit, mirroring Android's applyFormattingScopedToEdit.
|
|
909
|
+
- (void)applyFormattingScopedToEditAtLocation:(NSUInteger)editLocation insertedLength:(NSUInteger)insertedLength
|
|
910
|
+
{
|
|
911
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
912
|
+
NSUInteger textLength = plainText.length;
|
|
913
|
+
if (textLength == 0) {
|
|
914
|
+
[self applyFormatting];
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
NSUInteger rawStart = MIN(editLocation, textLength);
|
|
919
|
+
NSUInteger rawEnd = MIN(editLocation + insertedLength, textLength);
|
|
920
|
+
rawEnd = MAX(rawEnd, rawStart);
|
|
921
|
+
NSRange scope = [plainText lineRangeForRange:NSMakeRange(rawStart, rawEnd - rawStart)];
|
|
922
|
+
[self applyFormattingScopedToRange:scope];
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
- (void)applyFormattingScopedToRange:(NSRange)scope
|
|
926
|
+
{
|
|
927
|
+
if (_editSession.shouldSuppressFormatting) {
|
|
907
928
|
return;
|
|
908
929
|
}
|
|
909
|
-
if (
|
|
930
|
+
if (_editSession.isComposing) {
|
|
910
931
|
return;
|
|
911
932
|
}
|
|
912
|
-
|
|
933
|
+
[_editSession enterPhase:ENRMEditPhaseFormatting];
|
|
913
934
|
|
|
914
935
|
NSRange savedSelection = _textView.selectedRange;
|
|
915
936
|
|
|
916
|
-
[_formatter applyFormattingRanges:_formattingStore.allRanges
|
|
937
|
+
[_formatter applyFormattingRanges:_formattingStore.allRanges
|
|
938
|
+
toTextView:_textView
|
|
939
|
+
style:_formatterStyle
|
|
940
|
+
scopedToRange:scope];
|
|
941
|
+
[_formatter applyBlockRanges:_blockStore.allRanges toTextView:_textView style:_formatterStyle scopedToRange:scope];
|
|
917
942
|
[_detectorPipeline refreshAllStyling];
|
|
918
943
|
[self applyWritingDirection];
|
|
919
944
|
|
|
@@ -922,7 +947,7 @@ using namespace facebook::react;
|
|
|
922
947
|
_textView.selectedRange = savedSelection;
|
|
923
948
|
}
|
|
924
949
|
|
|
925
|
-
|
|
950
|
+
[_editSession exitPhase];
|
|
926
951
|
}
|
|
927
952
|
|
|
928
953
|
- (void)applyWritingDirection
|
|
@@ -952,9 +977,9 @@ using namespace facebook::react;
|
|
|
952
977
|
{
|
|
953
978
|
[self importMarkdown:markdown];
|
|
954
979
|
_lastSelectedRange = _textView.selectedRange;
|
|
955
|
-
[
|
|
956
|
-
[
|
|
957
|
-
[
|
|
980
|
+
[_inputEventEmitter emitOnChangeText];
|
|
981
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
982
|
+
[_inputEventEmitter emitOnChangeState];
|
|
958
983
|
[self requestHeightUpdate];
|
|
959
984
|
}
|
|
960
985
|
|
|
@@ -964,8 +989,8 @@ using namespace facebook::react;
|
|
|
964
989
|
NSInteger clampedStart = MIN(MAX(start, 0), textLen);
|
|
965
990
|
NSInteger clampedEnd = MIN(MAX(end, clampedStart), textLen);
|
|
966
991
|
_textView.selectedRange = NSMakeRange((NSUInteger)clampedStart, (NSUInteger)(clampedEnd - clampedStart));
|
|
967
|
-
[
|
|
968
|
-
[
|
|
992
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
993
|
+
[_inputEventEmitter emitOnChangeState];
|
|
969
994
|
}
|
|
970
995
|
|
|
971
996
|
- (void)toggleBold
|
|
@@ -1002,813 +1027,817 @@ using namespace facebook::react;
|
|
|
1002
1027
|
ENRMStyleMergingConfig *mergingConfig = handler.mergingConfig;
|
|
1003
1028
|
|
|
1004
1029
|
NSRange selection = _textView.selectedRange;
|
|
1005
|
-
NSUInteger cursor = selection.location;
|
|
1006
|
-
NSNumber *key = @(styleType);
|
|
1007
|
-
|
|
1008
|
-
// Check blocking rules: if any blocking style is active, refuse to toggle on.
|
|
1009
|
-
if (mergingConfig.blockingStyles.count > 0) {
|
|
1010
|
-
BOOL isCurrentlyActive = [_formattingStore isStyleActive:styleType atPosition:cursor];
|
|
1011
|
-
if (!isCurrentlyActive) {
|
|
1012
|
-
for (NSNumber *blockerNum in mergingConfig.blockingStyles) {
|
|
1013
|
-
if ([_formattingStore isStyleActive:(ENRMInputStyleType)blockerNum.integerValue atPosition:cursor]) {
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
1030
|
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
while (pos < selEnd) {
|
|
1025
|
-
ENRMFormattingRange *match = [_formattingStore rangeOfType:styleType containingPosition:pos];
|
|
1026
|
-
if (match == nil) {
|
|
1027
|
-
fullyStyled = NO;
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
pos = NSMaxRange(match.range);
|
|
1031
|
-
}
|
|
1032
|
-
if (fullyStyled) {
|
|
1033
|
-
[_formattingStore removeType:styleType inRange:selection];
|
|
1034
|
-
} else {
|
|
1035
|
-
// Remove conflicting styles from the range before applying.
|
|
1036
|
-
for (NSNumber *conflictNum in mergingConfig.conflictingStyles) {
|
|
1037
|
-
[_formattingStore removeType:(ENRMInputStyleType)conflictNum.integerValue inRange:selection];
|
|
1038
|
-
}
|
|
1039
|
-
ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:styleType range:selection];
|
|
1040
|
-
[_formattingStore addRange:newRange];
|
|
1041
|
-
}
|
|
1042
|
-
[_pendingStyles removeObject:key];
|
|
1043
|
-
[_pendingStyleRemovals removeObject:key];
|
|
1044
|
-
} else {
|
|
1045
|
-
BOOL isInsideRange = [_formattingStore isStyleActive:styleType atPosition:cursor];
|
|
1046
|
-
|
|
1047
|
-
if ([_pendingStyleRemovals containsObject:key]) {
|
|
1048
|
-
[_pendingStyleRemovals removeObject:key];
|
|
1049
|
-
} else if ([_pendingStyles containsObject:key]) {
|
|
1050
|
-
[_pendingStyles removeObject:key];
|
|
1051
|
-
} else if (isInsideRange) {
|
|
1052
|
-
[_pendingStyleRemovals addObject:key];
|
|
1053
|
-
} else {
|
|
1054
|
-
[_pendingStyles addObject:key];
|
|
1055
|
-
}
|
|
1031
|
+
if ([_formattingStore isToggleBlocked:styleType
|
|
1032
|
+
atPosition:selection.location
|
|
1033
|
+
blockingStyles:mergingConfig.blockingStyles]) {
|
|
1034
|
+
return;
|
|
1056
1035
|
}
|
|
1057
1036
|
|
|
1037
|
+
BOOL wasActive = [_formattingStore toggleStyle:styleType
|
|
1038
|
+
inRange:selection
|
|
1039
|
+
conflictingStyles:mergingConfig.conflictingStyles];
|
|
1040
|
+
|
|
1041
|
+
[_typingController togglePendingStyle:styleType wasActive:wasActive hasSelection:(selection.length > 0)];
|
|
1042
|
+
|
|
1058
1043
|
[self applyFormatting];
|
|
1059
|
-
[
|
|
1060
|
-
[
|
|
1044
|
+
[_typingController syncWithPendingStyles];
|
|
1045
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1061
1046
|
}
|
|
1062
1047
|
|
|
1063
|
-
- (void)
|
|
1048
|
+
- (void)toggleHeading:(NSInteger)level
|
|
1064
1049
|
{
|
|
1065
|
-
|
|
1066
|
-
|
|
1050
|
+
if (level < 1 || level > 6) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
[self toggleBlockType:ENRMBlockTypeForHeadingLevel(level) level:level];
|
|
1054
|
+
}
|
|
1067
1055
|
|
|
1068
|
-
|
|
1056
|
+
- (void)toggleBlockType:(ENRMInputBlockType)type level:(NSInteger)level
|
|
1057
|
+
{
|
|
1058
|
+
NSString *text = ENRMGetPlainText(_textView);
|
|
1059
|
+
BOOL alreadyActive = [_blockCoordinator toggleBlockType:type
|
|
1060
|
+
level:level
|
|
1061
|
+
selectionRange:_textView.selectedRange
|
|
1062
|
+
inText:text];
|
|
1069
1063
|
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1064
|
+
if (alreadyActive) {
|
|
1065
|
+
// Strip the stored block paragraph style directly: an empty item's line has no
|
|
1066
|
+
// stamped block-marker attribute (zero-length ranges are never stamped), so the
|
|
1067
|
+
// applyFormatting reset below — which is keyed off that marker — cannot reach it.
|
|
1068
|
+
NSRange paragraphRange = [text paragraphRangeForRange:_textView.selectedRange];
|
|
1069
|
+
NSTextStorage *storage = _textView.textStorage;
|
|
1070
|
+
NSRange clamped = NSIntersectionRange(paragraphRange, NSMakeRange(0, storage.length));
|
|
1071
|
+
if (clamped.length > 0) {
|
|
1072
|
+
[storage beginEditing];
|
|
1073
|
+
[storage removeAttribute:NSParagraphStyleAttributeName range:clamped];
|
|
1074
|
+
[storage endEditing];
|
|
1075
|
+
}
|
|
1079
1076
|
}
|
|
1080
1077
|
|
|
1081
1078
|
[self applyFormatting];
|
|
1082
|
-
[
|
|
1079
|
+
[_typingController syncWithCursorBlock];
|
|
1080
|
+
[self updatePlaceholderVisibility];
|
|
1081
|
+
if (alreadyActive) {
|
|
1082
|
+
[_typingController clearListParagraphStyle];
|
|
1083
|
+
}
|
|
1084
|
+
[self updateEmptyBulletMarker];
|
|
1085
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1083
1086
|
}
|
|
1084
1087
|
|
|
1085
|
-
- (void)
|
|
1088
|
+
- (void)toggleUnorderedList
|
|
1086
1089
|
{
|
|
1087
|
-
|
|
1088
|
-
NSRange linkRange = NSMakeRange(0, displayText.length);
|
|
1089
|
-
ENRMFormattingRange *range = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
|
|
1090
|
-
range:linkRange
|
|
1091
|
-
url:[self sanitizeLinkURL:url]];
|
|
1092
|
-
[self replaceSelectedTextWith:displayText formattingRanges:@[ range ]];
|
|
1090
|
+
[self toggleBlockType:ENRMInputBlockTypeUnorderedListItem level:0];
|
|
1093
1091
|
}
|
|
1094
1092
|
|
|
1095
|
-
- (
|
|
1093
|
+
- (void)toggleOrderedList
|
|
1096
1094
|
{
|
|
1097
|
-
|
|
1098
|
-
return [result stringByReplacingOccurrencesOfString:@")" withString:@"%29"];
|
|
1095
|
+
[self toggleBlockType:ENRMInputBlockTypeOrderedListItem level:0];
|
|
1099
1096
|
}
|
|
1100
1097
|
|
|
1101
|
-
- (void)
|
|
1098
|
+
- (void)indentList
|
|
1102
1099
|
{
|
|
1103
|
-
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
[self replaceSelectedTextWith:indicator formattingRanges:@[]];
|
|
1108
|
-
[self updateActiveMention];
|
|
1100
|
+
[self changeListDepthBy:1];
|
|
1109
1101
|
}
|
|
1110
1102
|
|
|
1111
|
-
- (void)
|
|
1103
|
+
- (void)outdentList
|
|
1112
1104
|
{
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1105
|
+
[self changeListDepthBy:-1];
|
|
1106
|
+
}
|
|
1116
1107
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1108
|
+
- (void)changeListDepthBy:(NSInteger)delta
|
|
1109
|
+
{
|
|
1110
|
+
NSString *text = ENRMGetPlainText(_textView);
|
|
1111
|
+
ENRMDepthChangeResult result = [_blockCoordinator changeDepthBy:delta
|
|
1112
|
+
cursorPosition:_textView.selectedRange.location
|
|
1113
|
+
selectionRange:_textView.selectedRange
|
|
1114
|
+
inText:text];
|
|
1115
|
+
if (result == ENRMDepthChangeResultNoOp) {
|
|
1120
1116
|
return;
|
|
1121
1117
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
range:NSMakeRange(0, displayText.length)
|
|
1128
|
-
url:[self sanitizeLinkURL:url]];
|
|
1129
|
-
NSString *indicator = _activeMentionIndicator;
|
|
1130
|
-
NSRange mentionRange = _activeMentionRange;
|
|
1118
|
+
[self applyFormatting];
|
|
1119
|
+
[_typingController syncWithCursorBlock];
|
|
1120
|
+
[self updateEmptyBulletMarker];
|
|
1121
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1122
|
+
}
|
|
1131
1123
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
_textView.selectedRange
|
|
1135
|
-
[self emitOnChangeSelection];
|
|
1124
|
+
- (nullable ENRMBlockRange *)listBlockForCursorParagraph
|
|
1125
|
+
{
|
|
1126
|
+
return [self listBlockForParagraphAtPosition:_textView.selectedRange.location];
|
|
1136
1127
|
}
|
|
1137
1128
|
|
|
1138
|
-
- (
|
|
1129
|
+
- (nullable ENRMBlockRange *)listBlockForParagraphAtPosition:(NSUInteger)position
|
|
1139
1130
|
{
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
if (activeLink == nil) {
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1131
|
+
return [_blockCoordinator listBlockAtPosition:position inText:_textView.textStorage.string];
|
|
1132
|
+
}
|
|
1145
1133
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
[
|
|
1134
|
+
- (nullable ENRMBlockRange *)blockForParagraphAtPosition:(NSUInteger)position
|
|
1135
|
+
{
|
|
1136
|
+
return [_blockCoordinator blockAtPosition:position inText:_textView.textStorage.string];
|
|
1149
1137
|
}
|
|
1150
1138
|
|
|
1151
|
-
|
|
1139
|
+
- (BOOL)listStateOfType:(ENRMInputBlockType)type forCursorParagraphDepth:(nullable NSInteger *)outDepth
|
|
1140
|
+
{
|
|
1141
|
+
return [_blockCoordinator listStateOfType:type
|
|
1142
|
+
atPosition:_textView.selectedRange.location
|
|
1143
|
+
inText:_textView.textStorage.string
|
|
1144
|
+
depth:outDepth];
|
|
1145
|
+
}
|
|
1152
1146
|
|
|
1153
|
-
|
|
1147
|
+
/// Records the block kind/level of the line being edited before the change, so a
|
|
1148
|
+
/// Return that continues a list can recover the previous item's depth and an
|
|
1149
|
+
/// in-line replacement (autocorrect/paste) that wipes the line can heal it.
|
|
1150
|
+
- (void)capturePreEditBlockForRange:(NSRange)range
|
|
1154
1151
|
{
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
NSRange selection = _textView.selectedRange;
|
|
1160
|
-
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1152
|
+
ENRMBlockRange *block = [self blockForParagraphAtPosition:range.location];
|
|
1153
|
+
_preEditBlockType = block != nil ? block.type : ENRMInputBlockTypeParagraph;
|
|
1154
|
+
_preEditBlockLevel = block != nil ? block.level : 0;
|
|
1155
|
+
}
|
|
1161
1156
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
[prefix appendString:@"\n"];
|
|
1170
|
-
if (!atLineEnd)
|
|
1171
|
-
[suffix appendString:@"\n"];
|
|
1157
|
+
/// Whether `range` of the current (pre-edit) text contains a line break. Scans
|
|
1158
|
+
/// only the replaced run, mirroring Android's editTouchedNewline.
|
|
1159
|
+
- (BOOL)replacedRangeTouchesNewline:(NSRange)range
|
|
1160
|
+
{
|
|
1161
|
+
NSString *text = _textView.textStorage.string;
|
|
1162
|
+
if (range.length == 0 || range.location >= text.length) {
|
|
1163
|
+
return NO;
|
|
1172
1164
|
}
|
|
1165
|
+
NSRange clamped = NSMakeRange(range.location, MIN(range.length, text.length - range.location));
|
|
1166
|
+
return [text rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet] options:0 range:clamped].location !=
|
|
1167
|
+
NSNotFound;
|
|
1168
|
+
}
|
|
1173
1169
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1170
|
+
/// Whether the caret's paragraph had no glyph content at the start of the edit
|
|
1171
|
+
/// (an empty line). Used to decide whether Return continues or exits the list.
|
|
1172
|
+
- (BOOL)preEditParagraphWasEmpty:(NSRange)range
|
|
1173
|
+
{
|
|
1174
|
+
NSString *text = _textView.textStorage.string;
|
|
1175
|
+
if (text.length == 0) {
|
|
1176
|
+
return YES;
|
|
1178
1177
|
}
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1178
|
+
NSRange paragraph = [text paragraphRangeForRange:range];
|
|
1179
|
+
if (paragraph.length == 0) {
|
|
1180
|
+
return YES;
|
|
1182
1181
|
}
|
|
1182
|
+
return [[text substringWithRange:paragraph] isEqualToString:@"\n"];
|
|
1183
|
+
}
|
|
1183
1184
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1185
|
+
/// Intercepts Tab (indent) and Backspace at a bullet item's start (outdent, then
|
|
1186
|
+
/// un-list at depth 0). Returns YES when handled, suppressing the default edit.
|
|
1187
|
+
/// Keyed off the caret's own paragraph (NSMaxRange(range)), not range.location —
|
|
1188
|
+
/// a backspace at a line start targets the previous line's newline, and using the
|
|
1189
|
+
/// caret's line lets the next Backspace after un-listing fall through to a merge.
|
|
1190
|
+
- (BOOL)handleListKeyForReplacementRange:(NSRange)range replacementText:(NSString *)text
|
|
1191
|
+
{
|
|
1192
|
+
NSUInteger caret = NSMaxRange(range);
|
|
1193
|
+
NSInteger depth;
|
|
1194
|
+
if (![self listStateForParagraphAtPosition:caret depth:&depth]) {
|
|
1195
|
+
return NO;
|
|
1189
1196
|
}
|
|
1190
1197
|
|
|
1191
|
-
|
|
1192
|
-
[
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
[_formattingStore adjustForEditAtLocation:selection.location
|
|
1197
|
-
deletedLength:selection.length
|
|
1198
|
-
insertedLength:insertedLength];
|
|
1198
|
+
// Tab indents the current item.
|
|
1199
|
+
if ([text isEqualToString:@"\t"]) {
|
|
1200
|
+
[self indentList];
|
|
1201
|
+
return YES;
|
|
1202
|
+
}
|
|
1199
1203
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
+
// Backspace at the very start of the caret's own item: outdent, or remove the
|
|
1205
|
+
// marker entirely once at depth 0 (de-indenting the line to a plain paragraph).
|
|
1206
|
+
if (text.length == 0 && range.length == 1) {
|
|
1207
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1208
|
+
NSRange paragraphRange = [plainText paragraphRangeForRange:NSMakeRange(caret, 0)];
|
|
1209
|
+
BOOL atItemStart = caret == paragraphRange.location;
|
|
1210
|
+
if (atItemStart) {
|
|
1211
|
+
if (depth > 0) {
|
|
1212
|
+
[self outdentList];
|
|
1213
|
+
} else {
|
|
1214
|
+
ENRMBlockRange *listBlock = [self listBlockForParagraphAtPosition:caret];
|
|
1215
|
+
[self toggleBlockType:listBlock != nil ? listBlock.type : ENRMInputBlockTypeUnorderedListItem level:0];
|
|
1216
|
+
}
|
|
1217
|
+
return YES;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1204
1220
|
|
|
1205
|
-
|
|
1206
|
-
[self updatePlaceholderVisibility];
|
|
1207
|
-
[self emitOnChangeText];
|
|
1208
|
-
[self emitOnChangeSelection];
|
|
1209
|
-
[self emitFormattingChanged];
|
|
1210
|
-
[self requestHeightUpdate];
|
|
1211
|
-
[self scheduleRelayoutIfNeeded];
|
|
1212
|
-
#endif
|
|
1221
|
+
return NO;
|
|
1213
1222
|
}
|
|
1214
1223
|
|
|
1215
|
-
|
|
1224
|
+
/// Backspace at the document start (caret at 0) never fires the text-change
|
|
1225
|
+
/// delegate — nothing precedes the caret — so the first line's bullet has to be
|
|
1226
|
+
/// outdented/removed here. Returns YES when handled.
|
|
1227
|
+
- (BOOL)handleBackspaceAtDocumentStart
|
|
1216
1228
|
{
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
return;
|
|
1221
|
-
id value = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)start effectiveRange:NULL];
|
|
1222
|
-
if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
|
|
1223
|
-
return;
|
|
1224
|
-
|
|
1225
|
-
ENRMMathInlineAttachment *att = value;
|
|
1226
|
-
BOOL modeChanged = (att.displayMode != displayMode);
|
|
1227
|
-
att.latex = latex ?: @"";
|
|
1228
|
-
att.displayMode = displayMode;
|
|
1229
|
-
[self applyMathStyleToAttachment:att];
|
|
1230
|
-
[att invalidateRender];
|
|
1231
|
-
|
|
1232
|
-
NSRange chipRange = NSMakeRange((NSUInteger)start, 1);
|
|
1233
|
-
if (modeChanged) {
|
|
1234
|
-
[storage beginEditing];
|
|
1235
|
-
if (displayMode && att.blockAlignment != NSTextAlignmentNatural) {
|
|
1236
|
-
NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
|
|
1237
|
-
ps.alignment = att.blockAlignment;
|
|
1238
|
-
[storage addAttribute:NSParagraphStyleAttributeName value:ps range:chipRange];
|
|
1239
|
-
} else {
|
|
1240
|
-
[storage removeAttribute:NSParagraphStyleAttributeName range:chipRange];
|
|
1241
|
-
}
|
|
1242
|
-
[storage endEditing];
|
|
1229
|
+
NSRange selection = _textView.selectedRange;
|
|
1230
|
+
if (selection.location != 0 || selection.length != 0) {
|
|
1231
|
+
return NO;
|
|
1243
1232
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
[lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
|
|
1248
|
-
[lm ensureLayoutForCharacterRange:chipRange];
|
|
1233
|
+
ENRMBlockRange *listBlock = [self listBlockForCursorParagraph];
|
|
1234
|
+
if (listBlock == nil) {
|
|
1235
|
+
return NO;
|
|
1249
1236
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
[self
|
|
1237
|
+
if (listBlock.level > 0) {
|
|
1238
|
+
[self outdentList];
|
|
1239
|
+
} else {
|
|
1240
|
+
[self toggleBlockType:listBlock.type level:0];
|
|
1254
1241
|
}
|
|
1255
|
-
|
|
1256
|
-
[self scheduleRelayoutIfNeeded];
|
|
1257
|
-
#endif
|
|
1242
|
+
return YES;
|
|
1258
1243
|
}
|
|
1259
1244
|
|
|
1260
|
-
- (
|
|
1245
|
+
- (BOOL)listStateForParagraphAtPosition:(NSUInteger)position depth:(NSInteger *)outDepth
|
|
1261
1246
|
{
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
[self replaceTextInRange:NSMakeRange((NSUInteger)start, 1) withText:@"" formattingRanges:@[]];
|
|
1271
|
-
#endif
|
|
1247
|
+
ENRMBlockRange *block = [_blockCoordinator listBlockAtPosition:position inText:_textView.textStorage.string];
|
|
1248
|
+
if (block != nil) {
|
|
1249
|
+
if (outDepth) {
|
|
1250
|
+
*outDepth = block.level;
|
|
1251
|
+
}
|
|
1252
|
+
return YES;
|
|
1253
|
+
}
|
|
1254
|
+
return NO;
|
|
1272
1255
|
}
|
|
1273
1256
|
|
|
1274
|
-
-
|
|
1257
|
+
/// Drives the layout manager's empty-line bullet: an empty bullet line has no
|
|
1258
|
+
/// character to anchor the marker to, so the manager is told its location/depth
|
|
1259
|
+
/// explicitly; cleared when the caret isn't on an empty bullet line. Runs on
|
|
1260
|
+
/// every selection-change fire, so it early-returns when there is nothing to
|
|
1261
|
+
/// draw or clear and only touches storage when the paragraph style differs,
|
|
1262
|
+
/// avoiding layout churn that jitters the edit menu.
|
|
1263
|
+
- (void)updateEmptyBulletMarker
|
|
1275
1264
|
{
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1265
|
+
NSString *text = ENRMGetPlainText(_textView);
|
|
1266
|
+
NSRange selection = _textView.selectedRange;
|
|
1267
|
+
BOOL show = NO;
|
|
1268
|
+
NSUInteger location = 0;
|
|
1269
|
+
NSInteger depth = 0;
|
|
1279
1270
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1271
|
+
BOOL ordered = NO;
|
|
1272
|
+
NSInteger ordinal = 1;
|
|
1273
|
+
ENRMBlockRange *cursorListBlock = selection.length == 0 ? [self listBlockForCursorParagraph] : nil;
|
|
1283
1274
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
return nil;
|
|
1275
|
+
ENRMInputListMarkerDrawer *listDrawer = _layoutManager.listMarkerDrawer;
|
|
1276
|
+
BOOL wasShown = listDrawer.emptyBulletDepth >= 0;
|
|
1277
|
+
if (cursorListBlock == nil && !wasShown) {
|
|
1278
|
+
return;
|
|
1289
1279
|
}
|
|
1290
1280
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1281
|
+
if (cursorListBlock != nil) {
|
|
1282
|
+
NSInteger cursorDepth = cursorListBlock.level;
|
|
1283
|
+
ordered = cursorListBlock.type == ENRMInputBlockTypeOrderedListItem;
|
|
1284
|
+
ordinal = cursorListBlock.ordinal;
|
|
1285
|
+
NSRange paragraphRange = text.length == 0 ? NSMakeRange(0, 0) : [text paragraphRangeForRange:selection];
|
|
1286
|
+
NSString *paragraphText = text.length == 0 ? @"" : [text substringWithRange:paragraphRange];
|
|
1287
|
+
BOOL empty = paragraphText.length == 0 || [paragraphText isEqualToString:@"\n"];
|
|
1288
|
+
if (empty) {
|
|
1289
|
+
show = YES;
|
|
1290
|
+
location = paragraphRange.location;
|
|
1291
|
+
depth = cursorDepth;
|
|
1292
|
+
|
|
1293
|
+
// A mid-document empty bullet line has no paragraph style and lays out
|
|
1294
|
+
// flush left; stamp the list style so caret and marker indent immediately.
|
|
1295
|
+
// (A trailing empty line uses the extra line fragment instead.)
|
|
1296
|
+
if (paragraphRange.length > 0) {
|
|
1297
|
+
CGFloat indent = (depth + 1) * kENRMListIndentPerDepth;
|
|
1298
|
+
NSTextStorage *storage = _textView.textStorage;
|
|
1299
|
+
NSParagraphStyle *existing = [storage attribute:NSParagraphStyleAttributeName
|
|
1300
|
+
atIndex:paragraphRange.location
|
|
1301
|
+
effectiveRange:NULL];
|
|
1302
|
+
if (existing == nil || existing.firstLineHeadIndent != indent || existing.headIndent != indent ||
|
|
1303
|
+
existing.paragraphSpacingBefore != _formatterStyle.listItemSpacing) {
|
|
1304
|
+
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
|
|
1305
|
+
paragraph.firstLineHeadIndent = indent;
|
|
1306
|
+
paragraph.headIndent = indent;
|
|
1307
|
+
paragraph.paragraphSpacingBefore = _formatterStyle.listItemSpacing;
|
|
1308
|
+
[storage beginEditing];
|
|
1309
|
+
[storage addAttribute:NSParagraphStyleAttributeName value:paragraph range:paragraphRange];
|
|
1310
|
+
[storage endEditing];
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1294
1314
|
}
|
|
1295
|
-
NSAttributedString *selectedAttr = [full attributedSubstringFromRange:selection];
|
|
1296
|
-
NSUInteger selEnd = NSMaxRange(selection);
|
|
1297
1315
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
NSUInteger rangeEnd = NSMaxRange(range.range);
|
|
1316
|
+
if (!show && !wasShown) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1302
1319
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1320
|
+
listDrawer.emptyBulletDepth = show ? depth : -1;
|
|
1321
|
+
listDrawer.emptyBulletOrdered = ordered;
|
|
1322
|
+
listDrawer.emptyBulletOrdinal = ordinal;
|
|
1323
|
+
listDrawer.emptyBulletLocation = location;
|
|
1324
|
+
listDrawer.emptyBulletFont = _formatterStyle.baseFont;
|
|
1325
|
+
listDrawer.emptyBulletColor = _formatterStyle.baseTextColor;
|
|
1326
|
+
listDrawer.emptyBulletRTL = [self emptyListLineIsRTL];
|
|
1327
|
+
listDrawer.listItemSpacing = _formatterStyle.listItemSpacing;
|
|
1306
1328
|
|
|
1307
|
-
|
|
1308
|
-
NSUInteger clippedEnd = MIN(rangeEnd, selEnd);
|
|
1309
|
-
NSRange shifted = NSMakeRange(clippedStart - selection.location, clippedEnd - clippedStart);
|
|
1329
|
+
BOOL isTrailing = show && text.length > 0 && location >= text.length;
|
|
1310
1330
|
|
|
1311
|
-
|
|
1331
|
+
// Ensure the extra line fragment is laid out so we can read
|
|
1332
|
+
// extraLineFragmentTextContainer / extraLineFragmentUsedRect. The
|
|
1333
|
+
// empty-editor case (length 0) has no glyphs at all; the trailing-line case
|
|
1334
|
+
// (location >= length) has glyphs on preceding lines but the extra line
|
|
1335
|
+
// fragment lives beyond them and isn't covered by ensureLayoutForCharRange:.
|
|
1336
|
+
if (show && (text.length == 0 || location >= text.length)) {
|
|
1337
|
+
[_layoutManager ensureLayoutForTextContainer:_textView.textContainer];
|
|
1312
1338
|
}
|
|
1313
1339
|
|
|
1314
|
-
|
|
1340
|
+
#if !TARGET_OS_OSX
|
|
1341
|
+
if (isTrailing && _layoutManager.extraLineFragmentTextContainer != nil) {
|
|
1342
|
+
[listDrawer showTrailingBulletInTextView:_textView
|
|
1343
|
+
textContainer:_layoutManager.extraLineFragmentTextContainer
|
|
1344
|
+
usedRect:_layoutManager.extraLineFragmentUsedRect];
|
|
1345
|
+
} else {
|
|
1346
|
+
[listDrawer hideTrailingBullet];
|
|
1347
|
+
}
|
|
1348
|
+
#endif
|
|
1349
|
+
|
|
1350
|
+
ENRMSetNeedsDisplay(_textView);
|
|
1351
|
+
|
|
1352
|
+
// The empty-editor bullet would otherwise overlap the placeholder.
|
|
1353
|
+
[self updatePlaceholderVisibility];
|
|
1315
1354
|
}
|
|
1316
1355
|
|
|
1317
|
-
|
|
1356
|
+
/// Writing direction the empty list line resolves to, for mirroring its marker.
|
|
1357
|
+
/// An empty line has no strong character, so Auto and FirstStrong fall back the
|
|
1358
|
+
/// same way the formatter's direction pass would.
|
|
1359
|
+
- (BOOL)emptyListLineIsRTL
|
|
1318
1360
|
{
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1361
|
+
switch (_writingDirectionMode) {
|
|
1362
|
+
case ENRMWritingDirectionModeLTR:
|
|
1363
|
+
return NO;
|
|
1364
|
+
case ENRMWritingDirectionModeRTL:
|
|
1365
|
+
return YES;
|
|
1366
|
+
case ENRMWritingDirectionModeFirstStrong:
|
|
1367
|
+
return _resolvedLayoutDirection == NSWritingDirectionRightToLeft;
|
|
1368
|
+
case ENRMWritingDirectionModeAuto:
|
|
1369
|
+
default:
|
|
1370
|
+
return ENRMParagraphIsRTL(nil);
|
|
1328
1371
|
}
|
|
1329
|
-
copyItemsToPasteboard(items);
|
|
1330
1372
|
}
|
|
1331
1373
|
|
|
1332
|
-
- (
|
|
1374
|
+
- (NSInteger)headingLevelForCursorParagraph
|
|
1333
1375
|
{
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
|
|
1339
|
-
ranges:[self allRangesIncludingTransient]];
|
|
1340
|
-
emitter->onRequestMarkdownResult({
|
|
1341
|
-
.requestId = static_cast<int>(requestId),
|
|
1342
|
-
.markdown = std::string([markdown UTF8String] ?: ""),
|
|
1343
|
-
});
|
|
1376
|
+
return [_blockCoordinator headingLevelAtPosition:_textView.selectedRange.location
|
|
1377
|
+
inText:_textView.textStorage.string];
|
|
1344
1378
|
}
|
|
1345
1379
|
|
|
1346
|
-
- (
|
|
1380
|
+
- (NSString *)adjustStoresForEditAtLocation:(NSUInteger)editLocation
|
|
1381
|
+
deletedLength:(NSUInteger)deletedLength
|
|
1382
|
+
insertedLength:(NSUInteger)insertedLength
|
|
1347
1383
|
{
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (selectedRange != nil) {
|
|
1352
|
-
caretRect = [_textView caretRectForPosition:selectedRange.start];
|
|
1353
|
-
}
|
|
1354
|
-
#else
|
|
1355
|
-
NSRange selection = _textView.selectedRange;
|
|
1356
|
-
if (selection.location != NSNotFound) {
|
|
1357
|
-
NSRange glyphRange = [_textView.layoutManager glyphRangeForCharacterRange:NSMakeRange(selection.location, 0)
|
|
1358
|
-
actualCharacterRange:NULL];
|
|
1359
|
-
caretRect = [_textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
|
|
1360
|
-
caretRect.origin.x += _textView.textContainerInset.left;
|
|
1361
|
-
caretRect.origin.y += _textView.textContainerInset.top;
|
|
1362
|
-
}
|
|
1363
|
-
#endif
|
|
1364
|
-
return caretRect;
|
|
1384
|
+
return [_editPipeline adjustStoresForEditAtLocation:editLocation
|
|
1385
|
+
deletedLength:deletedLength
|
|
1386
|
+
insertedLength:insertedLength];
|
|
1365
1387
|
}
|
|
1366
1388
|
|
|
1367
|
-
- (void)
|
|
1389
|
+
- (void)setLink:(NSString *)url
|
|
1368
1390
|
{
|
|
1369
|
-
|
|
1370
|
-
if (
|
|
1391
|
+
NSRange selection = _textView.selectedRange;
|
|
1392
|
+
if (![_linkCoordinator setLinkURL:url atCursor:selection.location selection:selection]) {
|
|
1371
1393
|
return;
|
|
1372
1394
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
emitter->onRequestCaretRectResult({
|
|
1376
|
-
.requestId = static_cast<int>(requestId),
|
|
1377
|
-
.x = caretRect.origin.x,
|
|
1378
|
-
.y = caretRect.origin.y,
|
|
1379
|
-
.width = caretRect.size.width,
|
|
1380
|
-
.height = caretRect.size.height,
|
|
1381
|
-
});
|
|
1395
|
+
[self applyFormatting];
|
|
1396
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1382
1397
|
}
|
|
1383
1398
|
|
|
1384
|
-
- (void)
|
|
1399
|
+
- (void)insertLink:(NSString *)text url:(NSString *)url
|
|
1385
1400
|
{
|
|
1386
|
-
|
|
1401
|
+
NSString *displayText = text.length > 0 ? text : url;
|
|
1402
|
+
NSRange linkRange = NSMakeRange(0, displayText.length);
|
|
1403
|
+
ENRMFormattingRange *range = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
|
|
1404
|
+
range:linkRange
|
|
1405
|
+
url:[_linkCoordinator sanitizeURL:url]];
|
|
1406
|
+
[self replaceSelectedTextWith:displayText formattingRanges:@[ range ]];
|
|
1387
1407
|
}
|
|
1388
1408
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
- (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
|
|
1409
|
+
- (void)insertText:(NSString *)text
|
|
1392
1410
|
{
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if ([_pendingStyleRemovals containsObject:key]) {
|
|
1396
|
-
return NO;
|
|
1397
|
-
}
|
|
1398
|
-
if ([_pendingStyles containsObject:key]) {
|
|
1399
|
-
return YES;
|
|
1411
|
+
if (text.length == 0) {
|
|
1412
|
+
return;
|
|
1400
1413
|
}
|
|
1401
|
-
|
|
1414
|
+
[self pasteMarkdown:text];
|
|
1402
1415
|
}
|
|
1403
1416
|
|
|
1404
|
-
- (void)
|
|
1417
|
+
- (void)startMention:(NSString *)indicator
|
|
1405
1418
|
{
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
traits |= UIFontDescriptorTraitBold;
|
|
1409
|
-
}
|
|
1410
|
-
if ([_pendingStyles containsObject:@(ENRMInputStyleTypeEmphasis)]) {
|
|
1411
|
-
traits |= UIFontDescriptorTraitItalic;
|
|
1419
|
+
if (indicator.length == 0 || ![_mentionCoordinator containsIndicator:indicator]) {
|
|
1420
|
+
return;
|
|
1412
1421
|
}
|
|
1413
1422
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
_textView.typingAttributes = attrs;
|
|
1423
|
+
[self replaceSelectedTextWith:indicator formattingRanges:@[]];
|
|
1424
|
+
[self dispatchMentionUpdate];
|
|
1417
1425
|
}
|
|
1418
1426
|
|
|
1419
|
-
- (void)
|
|
1427
|
+
- (void)insertMention:(NSString *)displayText url:(NSString *)url
|
|
1420
1428
|
{
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
static const CFTimeInterval kPostEditGracePeriod = 0.1;
|
|
1424
|
-
BOOL isPostEditAdjustment =
|
|
1425
|
-
(_lastTextChangeTime > 0 && (CACurrentMediaTime() - _lastTextChangeTime) < kPostEditGracePeriod);
|
|
1426
|
-
if (isPostEditAdjustment) {
|
|
1429
|
+
if (displayText.length == 0 || !_mentionCoordinator.isActive ||
|
|
1430
|
+
_mentionCoordinator.activeRange.location == NSNotFound) {
|
|
1427
1431
|
return;
|
|
1428
1432
|
}
|
|
1429
|
-
[_pendingStyles removeAllObjects];
|
|
1430
|
-
[_pendingStyleRemovals removeAllObjects];
|
|
1431
|
-
[self rebuildPendingStylesFromContext];
|
|
1432
|
-
[self syncTypingAttributesWithPendingStyles];
|
|
1433
|
-
}
|
|
1434
1433
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
NSUInteger
|
|
1438
|
-
if (
|
|
1434
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1435
|
+
NSRange activeRange = _mentionCoordinator.activeRange;
|
|
1436
|
+
NSUInteger rangeEnd = NSMaxRange(activeRange);
|
|
1437
|
+
if (rangeEnd > plainText.length) {
|
|
1439
1438
|
return;
|
|
1440
1439
|
}
|
|
1440
|
+
BOOL nextCharIsWhitespace = rangeEnd < plainText.length && [[NSCharacterSet whitespaceAndNewlineCharacterSet]
|
|
1441
|
+
characterIsMember:[plainText
|
|
1442
|
+
characterAtIndex:rangeEnd]];
|
|
1443
|
+
NSString *replacement = nextCharIsWhitespace ? displayText : [displayText stringByAppendingString:@" "];
|
|
1444
|
+
ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
|
|
1445
|
+
range:NSMakeRange(0, displayText.length)
|
|
1446
|
+
url:[_linkCoordinator sanitizeURL:url]];
|
|
1447
|
+
NSString *indicator = _mentionCoordinator.activeIndicator;
|
|
1441
1448
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
for (NSUInteger i = 0; i < sizeof(inlineStyles) / sizeof(inlineStyles[0]); i++) {
|
|
1448
|
-
ENRMInputStyleType type = inlineStyles[i];
|
|
1449
|
-
if ([_formattingStore isStyleAdjacentBefore:type position:cursor]) {
|
|
1450
|
-
[_pendingStyles addObject:@(type)];
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1449
|
+
[self dispatchMentionEvents:[_mentionCoordinator clearWithIndicatorOverride:indicator]];
|
|
1450
|
+
[self replaceTextInRange:activeRange withText:replacement formattingRanges:@[ linkRange ]];
|
|
1451
|
+
_textView.selectedRange = NSMakeRange(activeRange.location + replacement.length, 0);
|
|
1452
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
1453
1453
|
}
|
|
1454
1454
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
- (void)emitFormattingChanged
|
|
1455
|
+
- (void)removeLink
|
|
1458
1456
|
{
|
|
1459
|
-
[
|
|
1460
|
-
|
|
1461
|
-
[self emitOnChangeMarkdown];
|
|
1457
|
+
if (![_linkCoordinator removeLinkAtPosition:_textView.selectedRange.location]) {
|
|
1458
|
+
return;
|
|
1462
1459
|
}
|
|
1460
|
+
[self applyFormatting];
|
|
1461
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1463
1462
|
}
|
|
1464
1463
|
|
|
1465
|
-
-
|
|
1464
|
+
#pragma mark - Math chip commands
|
|
1465
|
+
|
|
1466
|
+
// A chip is one U+FFFC character in the buffer plus an ENRMMathInlineAttachment
|
|
1467
|
+
// carrying the LaTeX (see ENRMMathChipController). Every text mutation below goes
|
|
1468
|
+
// through the same replaceTextInRange/EditPhase machinery as links and mentions —
|
|
1469
|
+
// a chip is an atomic inline character, not a block, so it needs no ENRMBlockHandler
|
|
1470
|
+
// and no phase of its own.
|
|
1471
|
+
//
|
|
1472
|
+
// No ENRICHED_MARKDOWN_MATH guard here on purpose: codegen declares these three on
|
|
1473
|
+
// RCTEnrichedMarkdownTextInputViewProtocol regardless of the math flag, and
|
|
1474
|
+
// RCTEnrichedMarkdownTextInputHandleCommand sends the selector unconditionally, so
|
|
1475
|
+
// conformance must hold in math-disabled builds. The controller is inert there.
|
|
1476
|
+
|
|
1477
|
+
- (void)insertMath:(NSString *)latex displayMode:(BOOL)displayMode
|
|
1466
1478
|
{
|
|
1467
|
-
if (
|
|
1468
|
-
return
|
|
1479
|
+
if (latex.length == 0 || !ENRMMathChipController.isAvailable) {
|
|
1480
|
+
return;
|
|
1469
1481
|
}
|
|
1470
|
-
return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
|
|
1471
|
-
}
|
|
1472
1482
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
return _formattingStore.allRanges;
|
|
1483
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1484
|
+
NSRange selection = _textView.selectedRange;
|
|
1485
|
+
if (NSMaxRange(selection) > plainText.length) {
|
|
1486
|
+
return;
|
|
1478
1487
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1488
|
+
|
|
1489
|
+
// A block chip claims the full row, so it needs a line of its own: add the
|
|
1490
|
+
// separators the caret position is missing rather than pulling the surrounding
|
|
1491
|
+
// text onto the chip's line.
|
|
1492
|
+
BOOL needsLeadingBreak =
|
|
1493
|
+
displayMode && selection.location > 0 && [plainText characterAtIndex:selection.location - 1] != '\n';
|
|
1494
|
+
NSUInteger afterSelection = NSMaxRange(selection);
|
|
1495
|
+
BOOL needsTrailingBreak =
|
|
1496
|
+
displayMode && afterSelection < plainText.length && [plainText characterAtIndex:afterSelection] != '\n';
|
|
1497
|
+
|
|
1498
|
+
NSString *prefix = needsLeadingBreak ? @"\n" : @"";
|
|
1499
|
+
NSString *suffix = needsTrailingBreak ? @"\n" : @"";
|
|
1500
|
+
NSString *replacement = [NSString stringWithFormat:@"%@%@%@", prefix, ENRMObjectReplacementString(), suffix];
|
|
1501
|
+
NSUInteger chipPosition = selection.location + prefix.length;
|
|
1502
|
+
|
|
1503
|
+
// Plain text first, chip second. Going through replaceTextInRange: is what gets
|
|
1504
|
+
// the formatting store, the block store, orphaned-anchor pruning and the
|
|
1505
|
+
// detectors adjusted for the edit; stamping the attachment afterwards only adds
|
|
1506
|
+
// an attribute, so none of that bookkeeping sees a second edit.
|
|
1507
|
+
[self replaceTextInRange:selection withText:replacement formattingRanges:@[]];
|
|
1508
|
+
|
|
1509
|
+
ENRMMathAtom *atom = [[ENRMMathAtom alloc] init];
|
|
1510
|
+
atom.position = chipPosition;
|
|
1511
|
+
atom.latex = latex;
|
|
1512
|
+
atom.displayMode = displayMode;
|
|
1513
|
+
[_mathChipController applyAtoms:@[ atom ] offset:0];
|
|
1514
|
+
|
|
1515
|
+
NSUInteger caret = selection.location + replacement.length;
|
|
1516
|
+
_textView.selectedRange = NSMakeRange(MIN(caret, ENRMGetPlainText(_textView).length), 0);
|
|
1517
|
+
|
|
1518
|
+
// replaceTextInRange: already emitted text/selection/formatting for the edit, but
|
|
1519
|
+
// the caret moved after it and the chip attribute changed the measured height.
|
|
1520
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
1521
|
+
[self requestHeightUpdate];
|
|
1522
|
+
[self scheduleRelayoutIfNeeded];
|
|
1482
1523
|
}
|
|
1483
1524
|
|
|
1484
|
-
- (void)
|
|
1525
|
+
- (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode
|
|
1485
1526
|
{
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1527
|
+
if (start < 0) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
// Formula and mode live on the attachment, so this changes no text: no store
|
|
1531
|
+
// adjustment and no edit phase are needed, only a re-render and a re-measure.
|
|
1532
|
+
if (![_mathChipController updateChipAtPosition:(NSUInteger)start latex:latex displayMode:displayMode]) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1490
1535
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1536
|
+
// Gated like every other producer: `emitOnChangeMarkdown` is not self-guarding
|
|
1537
|
+
// (the check lives at each call site, e.g. `emitFormattingChanged`), and it
|
|
1538
|
+
// serializes the whole document — firing it for an app that never set
|
|
1539
|
+
// `onChangeMarkdown` would be pure cost. Android gates the same way.
|
|
1540
|
+
if (_inputEventEmitter.emitMarkdown) {
|
|
1541
|
+
[_inputEventEmitter emitOnChangeMarkdown];
|
|
1493
1542
|
}
|
|
1543
|
+
[self requestHeightUpdate];
|
|
1544
|
+
[self scheduleRelayoutIfNeeded];
|
|
1494
1545
|
}
|
|
1495
1546
|
|
|
1496
|
-
- (
|
|
1547
|
+
- (void)removeMath:(NSInteger)start
|
|
1497
1548
|
{
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
return nil;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1504
|
-
NSUInteger cursor = selectedRange.location;
|
|
1505
|
-
if (cursor > plainText.length) {
|
|
1506
|
-
return nil;
|
|
1549
|
+
if (start < 0 || [_mathChipController atomAtPosition:(NSUInteger)start] == nil) {
|
|
1550
|
+
return;
|
|
1507
1551
|
}
|
|
1508
1552
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
matchedIndicator = indicator;
|
|
1515
|
-
break;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
if (matchedIndicator == nil) {
|
|
1519
|
-
return nil;
|
|
1520
|
-
}
|
|
1553
|
+
// Deleting the character takes the attachment and its paragraph style with it,
|
|
1554
|
+
// so the ordinary text path is enough — no controller-side cleanup like Android
|
|
1555
|
+
// needs for its separate AlignmentSpan, and replaceTextInRange: already emits
|
|
1556
|
+
// the events and re-requests the height.
|
|
1557
|
+
[self replaceTextInRange:NSMakeRange((NSUInteger)start, 1) withText:@"" formattingRanges:@[]];
|
|
1521
1558
|
|
|
1522
|
-
if (
|
|
1523
|
-
|
|
1559
|
+
if (_activeMathChipPosition == (NSUInteger)start) {
|
|
1560
|
+
_activeMathChipPosition = NSNotFound;
|
|
1524
1561
|
}
|
|
1562
|
+
}
|
|
1525
1563
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1564
|
+
/// Chips in document order, as plain data for the serializer. Every path that
|
|
1565
|
+
/// serializes the live buffer must pass this, or each formula leaves the editor as
|
|
1566
|
+
/// a bare placeholder character instead of `$latex$`.
|
|
1567
|
+
- (NSArray<ENRMMathAtom *> *)mathAtomsForSerialization
|
|
1568
|
+
{
|
|
1569
|
+
return [_mathChipController allAtoms];
|
|
1530
1570
|
}
|
|
1531
1571
|
|
|
1532
|
-
|
|
1572
|
+
#if !TARGET_OS_OSX
|
|
1573
|
+
|
|
1574
|
+
- (void)handleMathChipTap:(UITapGestureRecognizer *)gesture
|
|
1533
1575
|
{
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1576
|
+
if (gesture.state != UIGestureRecognizerStateRecognized) {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
CGPoint location = [gesture locationInView:_textView];
|
|
1580
|
+
NSUInteger chipPosition = [_mathChipController chipPositionAtPoint:location];
|
|
1581
|
+
if (chipPosition == NSNotFound) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
ENRMMathAtom *atom = [_mathChipController atomAtPosition:chipPosition];
|
|
1585
|
+
if (atom == nil) {
|
|
1537
1586
|
return;
|
|
1538
1587
|
}
|
|
1539
1588
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
NSUInteger end = candidate.end;
|
|
1543
|
-
NSString *query = candidate.text;
|
|
1589
|
+
[_mathChipController clearSelectionsExceptPosition:chipPosition];
|
|
1590
|
+
[_mathChipController setSelected:YES atPosition:chipPosition];
|
|
1544
1591
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
_activeMentionRange = NSMakeRange(start, end - start);
|
|
1592
|
+
// Set before moving the caret: textViewDidChangeSelection: reads both to decide
|
|
1593
|
+
// whether the highlight it is about to clear was set by this very tap.
|
|
1594
|
+
_activeMathChipPosition = chipPosition;
|
|
1595
|
+
_suppressMathChipClearOnce = YES;
|
|
1596
|
+
// Caret after the chip, not a length-1 selection: a selection would draw the
|
|
1597
|
+
// system's blue range handles on top of the chip's own selected styling.
|
|
1598
|
+
NSUInteger caret = MIN(chipPosition + 1, ENRMGetPlainText(_textView).length);
|
|
1599
|
+
_textView.selectedRange = NSMakeRange(caret, 0);
|
|
1554
1600
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1601
|
+
// Recomputed rather than reused from the hit-test: selecting the chip
|
|
1602
|
+
// invalidated its layout above.
|
|
1603
|
+
CGRect glyphRect = [_mathChipController chipRectAtPosition:chipPosition];
|
|
1604
|
+
CGRect rectInSelf =
|
|
1605
|
+
CGRectIsNull(glyphRect) ? CGRectZero : [self.contentView convertRect:glyphRect fromView:_textView];
|
|
1606
|
+
|
|
1607
|
+
[_inputEventEmitter emitOnMathPressWithLatex:atom.latex
|
|
1608
|
+
range:NSMakeRange(chipPosition, 1)
|
|
1609
|
+
displayMode:atom.displayMode
|
|
1610
|
+
rect:rectInSelf];
|
|
1559
1611
|
}
|
|
1560
1612
|
|
|
1561
|
-
|
|
1613
|
+
#endif
|
|
1614
|
+
|
|
1615
|
+
#pragma mark -
|
|
1616
|
+
|
|
1617
|
+
- (void)showLinkPrompt
|
|
1562
1618
|
{
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1619
|
+
ENRMFormattingRange *activeLink = [_linkCoordinator linkAtPosition:_textView.selectedRange.location];
|
|
1620
|
+
NSString *existingURL = activeLink != nil ? activeLink.url : nil;
|
|
1566
1621
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
} else if (range.location > 0) {
|
|
1571
|
-
lookupPosition = range.location - 1;
|
|
1572
|
-
} else {
|
|
1573
|
-
return NO;
|
|
1574
|
-
}
|
|
1622
|
+
__weak EnrichedMarkdownTextInput *weakSelf = self;
|
|
1623
|
+
ENRMShowLinkPrompt(self, existingURL, ^(NSString *url) { [weakSelf setLink:url]; });
|
|
1624
|
+
}
|
|
1575
1625
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1626
|
+
/// Serializes plain text with inline + block formatting, resolving each block's
|
|
1627
|
+
/// markdown line prefix through its registered handler. With no block handlers
|
|
1628
|
+
/// registered the prefix provider is never consulted productively and output
|
|
1629
|
+
/// equals the inline-only serialization.
|
|
1630
|
+
- (NSString *)serializeText:(NSString *)text
|
|
1631
|
+
ranges:(NSArray<ENRMFormattingRange *> *)ranges
|
|
1632
|
+
blockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
|
|
1633
|
+
{
|
|
1634
|
+
return [_clipboardCoordinator serializeText:text
|
|
1635
|
+
mathAtoms:[self mathAtomsForSerialization]
|
|
1636
|
+
ranges:ranges
|
|
1637
|
+
blockRanges:blockRanges];
|
|
1638
|
+
}
|
|
1581
1639
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
return
|
|
1640
|
+
- (nullable NSString *)markdownForSelectedRange
|
|
1641
|
+
{
|
|
1642
|
+
return [_clipboardCoordinator serializeSelectedRange:_textView.selectedRange
|
|
1643
|
+
inText:ENRMGetPlainText(_textView)
|
|
1644
|
+
mathAtoms:[self mathAtomsForSerialization]];
|
|
1585
1645
|
}
|
|
1586
1646
|
|
|
1587
|
-
- (void)
|
|
1647
|
+
- (void)copyToClipboard
|
|
1588
1648
|
{
|
|
1589
|
-
|
|
1590
|
-
if (
|
|
1649
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1650
|
+
if (plainText.length == 0) {
|
|
1591
1651
|
return;
|
|
1592
1652
|
}
|
|
1593
|
-
NSString *
|
|
1594
|
-
|
|
1653
|
+
NSString *markdown = [_clipboardCoordinator serializeFullDocument:plainText
|
|
1654
|
+
mathAtoms:[self mathAtomsForSerialization]];
|
|
1655
|
+
NSMutableDictionary *items = [NSMutableDictionary dictionary];
|
|
1656
|
+
items[kUTIPlainText] = plainText;
|
|
1657
|
+
if (markdown.length > 0) {
|
|
1658
|
+
items[kENRMMarkdownPasteboardType] = markdown;
|
|
1659
|
+
}
|
|
1660
|
+
copyItemsToPasteboard(items);
|
|
1595
1661
|
}
|
|
1596
1662
|
|
|
1597
|
-
- (void)
|
|
1663
|
+
- (void)requestMarkdown:(NSInteger)requestId
|
|
1598
1664
|
{
|
|
1599
|
-
|
|
1600
|
-
if (emitter == nullptr) {
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
|
|
1604
|
-
ranges:[self allRangesIncludingTransient]];
|
|
1605
|
-
emitter->onChangeMarkdown({.value = std::string([markdown UTF8String] ?: "")});
|
|
1665
|
+
[_inputEventEmitter requestMarkdown:requestId];
|
|
1606
1666
|
}
|
|
1607
1667
|
|
|
1608
|
-
- (
|
|
1668
|
+
- (CGRect)computeCaretRect
|
|
1609
1669
|
{
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1670
|
+
CGRect caretRect = CGRectZero;
|
|
1671
|
+
#if !TARGET_OS_OSX
|
|
1672
|
+
UITextRange *selectedRange = _textView.selectedTextRange;
|
|
1673
|
+
if (selectedRange != nil) {
|
|
1674
|
+
caretRect = [_textView caretRectForPosition:selectedRange.start];
|
|
1613
1675
|
}
|
|
1676
|
+
#else
|
|
1614
1677
|
NSRange selection = _textView.selectedRange;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1678
|
+
if (selection.location != NSNotFound) {
|
|
1679
|
+
NSRange glyphRange = [_textView.layoutManager glyphRangeForCharacterRange:NSMakeRange(selection.location, 0)
|
|
1680
|
+
actualCharacterRange:NULL];
|
|
1681
|
+
caretRect = [_textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
|
|
1682
|
+
caretRect.origin.x += _textView.textContainerInset.left;
|
|
1683
|
+
caretRect.origin.y += _textView.textContainerInset.top;
|
|
1684
|
+
}
|
|
1685
|
+
#endif
|
|
1686
|
+
return caretRect;
|
|
1619
1687
|
}
|
|
1620
1688
|
|
|
1621
|
-
- (void)
|
|
1689
|
+
- (void)requestCaretRect:(NSInteger)requestId
|
|
1622
1690
|
{
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1691
|
+
[_inputEventEmitter requestCaretRect:requestId];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
1695
|
+
{
|
|
1696
|
+
RCTEnrichedMarkdownTextInputHandleCommand(self, commandName, args);
|
|
1697
|
+
}
|
|
1627
1698
|
|
|
1628
|
-
|
|
1629
|
-
BOOL boldActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrong atPosition:cursor];
|
|
1630
|
-
BOOL italicActive = [self isEffectiveStyleActive:ENRMInputStyleTypeEmphasis atPosition:cursor];
|
|
1631
|
-
BOOL underlineActive = [self isEffectiveStyleActive:ENRMInputStyleTypeUnderline atPosition:cursor];
|
|
1632
|
-
BOOL strikethroughActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrikethrough atPosition:cursor];
|
|
1633
|
-
BOOL spoilerActive = [self isEffectiveStyleActive:ENRMInputStyleTypeSpoiler atPosition:cursor];
|
|
1634
|
-
BOOL linkActive = [self isEffectiveStyleActive:ENRMInputStyleTypeLink atPosition:cursor];
|
|
1699
|
+
#pragma mark - ENRMInputEventEmitterDataSource
|
|
1635
1700
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
return;
|
|
1701
|
+
- (std::shared_ptr<EnrichedMarkdownTextInputEventEmitter const>)fabricEventEmitter
|
|
1702
|
+
{
|
|
1703
|
+
if (_eventEmitter == nullptr || _editSession.shouldSuppressEvents) {
|
|
1704
|
+
return nullptr;
|
|
1640
1705
|
}
|
|
1706
|
+
return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
|
|
1707
|
+
}
|
|
1641
1708
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
_prevState.strikethrough = strikethroughActive;
|
|
1646
|
-
_prevState.spoiler = spoilerActive;
|
|
1647
|
-
_prevState.link = linkActive;
|
|
1648
|
-
_prevState.initialized = YES;
|
|
1649
|
-
|
|
1650
|
-
emitter->onChangeState({
|
|
1651
|
-
.bold = {.isActive = boldActive},
|
|
1652
|
-
.italic = {.isActive = italicActive},
|
|
1653
|
-
.underline = {.isActive = underlineActive},
|
|
1654
|
-
.strikethrough = {.isActive = strikethroughActive},
|
|
1655
|
-
.spoiler = {.isActive = spoilerActive},
|
|
1656
|
-
.link = {.isActive = linkActive},
|
|
1657
|
-
});
|
|
1709
|
+
- (NSRange)selectedRange
|
|
1710
|
+
{
|
|
1711
|
+
return _textView.selectedRange;
|
|
1658
1712
|
}
|
|
1659
1713
|
|
|
1660
|
-
- (
|
|
1714
|
+
- (BOOL)isStyleActive:(ENRMInputStyleType)type inRange:(NSRange)range
|
|
1661
1715
|
{
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1716
|
+
return [_formattingStore isStyleActive:type inRange:range];
|
|
1717
|
+
}
|
|
1666
1718
|
|
|
1667
|
-
|
|
1719
|
+
- (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
|
|
1720
|
+
{
|
|
1721
|
+
return [_typingController isEffectiveStyleActive:type atPosition:position];
|
|
1722
|
+
}
|
|
1668
1723
|
|
|
1669
|
-
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1724
|
+
#pragma mark - ENRMInputTypingAttributesDataSource
|
|
1672
1725
|
|
|
1673
|
-
|
|
1726
|
+
- (BOOL)isStyleAdjacentBefore:(ENRMInputStyleType)type position:(NSUInteger)position
|
|
1727
|
+
{
|
|
1728
|
+
return [_formattingStore isStyleAdjacentBefore:type position:position];
|
|
1729
|
+
}
|
|
1674
1730
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
.width = caretRect.size.width,
|
|
1679
|
-
.height = caretRect.size.height,
|
|
1680
|
-
});
|
|
1731
|
+
- (BOOL)isStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
|
|
1732
|
+
{
|
|
1733
|
+
return [_formattingStore isStyleActive:type atPosition:position];
|
|
1681
1734
|
}
|
|
1682
1735
|
|
|
1683
|
-
- (
|
|
1736
|
+
- (NSString *)currentMarkdown
|
|
1684
1737
|
{
|
|
1685
|
-
return
|
|
1738
|
+
return [self serializeText:ENRMGetPlainText(_textView)
|
|
1739
|
+
ranges:[self allRangesIncludingTransient]
|
|
1740
|
+
blockRanges:_blockStore.allRanges];
|
|
1686
1741
|
}
|
|
1687
1742
|
|
|
1688
|
-
- (NSArray<
|
|
1743
|
+
- (NSArray<ENRMFormattingRange *> *)allRangesIncludingTransient
|
|
1689
1744
|
{
|
|
1690
|
-
return
|
|
1745
|
+
return [_clipboardCoordinator allRangesIncludingTransient];
|
|
1691
1746
|
}
|
|
1692
1747
|
|
|
1693
|
-
- (
|
|
1748
|
+
- (void)updateActiveMention
|
|
1694
1749
|
{
|
|
1695
|
-
|
|
1750
|
+
[self dispatchMentionUpdate];
|
|
1696
1751
|
}
|
|
1697
1752
|
|
|
1698
|
-
- (
|
|
1753
|
+
- (void)clearActiveMention:(nullable NSString *)indicatorOverride
|
|
1699
1754
|
{
|
|
1700
|
-
|
|
1755
|
+
[self dispatchMentionEvents:[_mentionCoordinator clearWithIndicatorOverride:indicatorOverride]];
|
|
1701
1756
|
}
|
|
1702
1757
|
|
|
1703
|
-
- (void)
|
|
1758
|
+
- (void)dispatchMentionUpdate
|
|
1704
1759
|
{
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1760
|
+
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1761
|
+
[self dispatchMentionEvents:[_mentionCoordinator updateWithText:plainText selectedRange:_textView.selectedRange]];
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
- (void)dispatchMentionEvents:(NSArray<ENRMMentionEvent *> *)events
|
|
1765
|
+
{
|
|
1766
|
+
for (ENRMMentionEvent *event in events) {
|
|
1767
|
+
switch (event.type) {
|
|
1768
|
+
case ENRMMentionEventStart:
|
|
1769
|
+
[_inputEventEmitter emitOnStartMention:event.indicator];
|
|
1770
|
+
break;
|
|
1771
|
+
case ENRMMentionEventChange:
|
|
1772
|
+
[_inputEventEmitter emitOnChangeMentionWithIndicator:event.indicator text:event.text];
|
|
1773
|
+
break;
|
|
1774
|
+
case ENRMMentionEventEnd:
|
|
1775
|
+
[_inputEventEmitter emitOnEndMention:event.indicator];
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1708
1778
|
}
|
|
1779
|
+
}
|
|
1709
1780
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1781
|
+
- (BOOL)deleteLinkForReplacementRange:(NSRange)range replacementText:(NSString *)text
|
|
1782
|
+
{
|
|
1783
|
+
if (text.length > 0) {
|
|
1784
|
+
return NO;
|
|
1785
|
+
}
|
|
1713
1786
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
}
|
|
1787
|
+
NSUInteger lookupPosition;
|
|
1788
|
+
if (range.length > 0) {
|
|
1789
|
+
lookupPosition = range.location;
|
|
1790
|
+
} else if (range.location > 0) {
|
|
1791
|
+
lookupPosition = range.location - 1;
|
|
1792
|
+
} else {
|
|
1793
|
+
return NO;
|
|
1794
|
+
}
|
|
1720
1795
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
.itemText = std::string(itemText.UTF8String),
|
|
1730
|
-
.selectedText = std::string(selectedText.UTF8String),
|
|
1731
|
-
.selectionStart = static_cast<int>(selectedRange.location),
|
|
1732
|
-
.selectionEnd = static_cast<int>(NSMaxRange(selectedRange)),
|
|
1733
|
-
.styleState =
|
|
1734
|
-
{
|
|
1735
|
-
.bold = {.isActive = boldActive},
|
|
1736
|
-
.italic = {.isActive = italicActive},
|
|
1737
|
-
.underline = {.isActive = underlineActive},
|
|
1738
|
-
.strikethrough = {.isActive = strikethroughActive},
|
|
1739
|
-
.spoiler = {.isActive = spoilerActive},
|
|
1740
|
-
.link = {.isActive = linkActive},
|
|
1741
|
-
},
|
|
1742
|
-
});
|
|
1796
|
+
ENRMFormattingRange *linkRange = [_linkCoordinator linkAtPosition:lookupPosition];
|
|
1797
|
+
if (linkRange == nil) {
|
|
1798
|
+
return NO;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
[self replaceTextInRange:linkRange.range withText:@"" formattingRanges:@[]];
|
|
1802
|
+
[self clearActiveMention:nil];
|
|
1803
|
+
return YES;
|
|
1743
1804
|
}
|
|
1744
1805
|
|
|
1745
|
-
- (
|
|
1806
|
+
- (NSArray<NSString *> *)contextMenuItemTexts
|
|
1746
1807
|
{
|
|
1747
|
-
|
|
1748
|
-
if (emitter == nullptr) {
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
emitter->onInputFocus({});
|
|
1808
|
+
return _contextMenuItemTexts ?: @[];
|
|
1752
1809
|
}
|
|
1753
1810
|
|
|
1754
|
-
- (
|
|
1811
|
+
- (NSArray<NSString *> *)contextMenuItemIcons
|
|
1755
1812
|
{
|
|
1756
|
-
|
|
1757
|
-
if (emitter == nullptr) {
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
emitter->onInputBlur({});
|
|
1813
|
+
return _contextMenuItemIcons ?: @[];
|
|
1761
1814
|
}
|
|
1762
1815
|
|
|
1763
|
-
- (
|
|
1816
|
+
- (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
|
|
1764
1817
|
{
|
|
1765
|
-
|
|
1766
|
-
if (emitter == nullptr) {
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
emitter->onLinkDetected({
|
|
1770
|
-
.text = std::string([text UTF8String] ?: ""),
|
|
1771
|
-
.url = std::string([url UTF8String] ?: ""),
|
|
1772
|
-
.start = static_cast<int>(range.location),
|
|
1773
|
-
.end = static_cast<int>(range.location + range.length),
|
|
1774
|
-
});
|
|
1818
|
+
return _inputSelectionMenuConfig;
|
|
1775
1819
|
}
|
|
1776
1820
|
|
|
1777
|
-
- (
|
|
1821
|
+
- (ENRMFormatMenuConfig)formatMenuConfig
|
|
1778
1822
|
{
|
|
1779
|
-
|
|
1780
|
-
if (emitter == nullptr) {
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
emitter->onStartMention({.indicator = std::string([indicator UTF8String] ?: "")});
|
|
1823
|
+
return _formatMenuConfig;
|
|
1784
1824
|
}
|
|
1785
1825
|
|
|
1786
|
-
- (
|
|
1826
|
+
- (ENRMInputEventEmitter *)inputEventEmitter
|
|
1787
1827
|
{
|
|
1788
|
-
|
|
1789
|
-
if (emitter == nullptr) {
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
emitter->onChangeMention({
|
|
1793
|
-
.indicator = std::string([indicator UTF8String] ?: ""),
|
|
1794
|
-
.text = std::string([text UTF8String] ?: ""),
|
|
1795
|
-
});
|
|
1828
|
+
return _inputEventEmitter;
|
|
1796
1829
|
}
|
|
1797
1830
|
|
|
1798
|
-
- (
|
|
1831
|
+
- (ENRMInputTypingAttributesController *)typingController
|
|
1799
1832
|
{
|
|
1800
|
-
|
|
1801
|
-
if (emitter == nullptr) {
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
emitter->onEndMention({.indicator = std::string([indicator UTF8String] ?: "")});
|
|
1833
|
+
return _typingController;
|
|
1805
1834
|
}
|
|
1806
1835
|
|
|
1807
1836
|
#pragma mark - Text edit tracking
|
|
1808
1837
|
|
|
1809
1838
|
- (void)handleTextChanged
|
|
1810
1839
|
{
|
|
1811
|
-
if (
|
|
1840
|
+
if (_editSession.isComposing) {
|
|
1812
1841
|
return;
|
|
1813
1842
|
}
|
|
1814
1843
|
|
|
@@ -1838,64 +1867,63 @@ using namespace facebook::react;
|
|
|
1838
1867
|
}
|
|
1839
1868
|
}
|
|
1840
1869
|
|
|
1841
|
-
[
|
|
1870
|
+
ENRMEditContext *context = [[ENRMEditContext alloc] initWithEditLocation:editLocation
|
|
1871
|
+
deletedLength:deletedLength
|
|
1872
|
+
insertedLength:insertedLength
|
|
1873
|
+
preEditReplacedNewline:_preEditReplacedNewline
|
|
1874
|
+
preEditBlockType:_preEditBlockType
|
|
1875
|
+
preEditBlockLevel:_preEditBlockLevel
|
|
1876
|
+
preEditParagraphWasEmpty:_preEditParagraphWasEmpty
|
|
1877
|
+
pendingStyles:_typingController.pendingStyles
|
|
1878
|
+
pendingStyleRemovals:_typingController.pendingStyleRemovals];
|
|
1842
1879
|
|
|
1843
|
-
|
|
1844
|
-
NSRange insertedRange = NSMakeRange(editLocation, insertedLength);
|
|
1845
|
-
|
|
1846
|
-
// Skip applying pending styles when the insertion is only line breaks —
|
|
1847
|
-
// a phantom range over a bare newline corrupts isStyleActive() at the boundary.
|
|
1848
|
-
NSString *plainText = ENRMGetPlainText(_textView);
|
|
1849
|
-
NSUInteger insertedEnd = NSMaxRange(insertedRange);
|
|
1850
|
-
BOOL insertedHasGlyphContent = NO;
|
|
1851
|
-
if (insertedEnd <= plainText.length) {
|
|
1852
|
-
NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet];
|
|
1853
|
-
for (NSUInteger i = insertedRange.location; i < insertedEnd; i++) {
|
|
1854
|
-
if (![newlines characterIsMember:[plainText characterAtIndex:i]]) {
|
|
1855
|
-
insertedHasGlyphContent = YES;
|
|
1856
|
-
break;
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
if (insertedHasGlyphContent) {
|
|
1862
|
-
for (NSNumber *styleNum in _pendingStyles) {
|
|
1863
|
-
ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:(ENRMInputStyleType)styleNum.integerValue
|
|
1864
|
-
range:insertedRange];
|
|
1865
|
-
[_formattingStore addRange:newRange];
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1880
|
+
BOOL touchedNewline = [_editPipeline processTextChangeWithContext:context];
|
|
1868
1881
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
}
|
|
1882
|
+
// Consumed: clear pre-edit state so stale values can't leak to the next edit.
|
|
1883
|
+
_preEditBlockType = ENRMInputBlockTypeParagraph;
|
|
1884
|
+
_preEditBlockLevel = 0;
|
|
1885
|
+
_preEditParagraphWasEmpty = NO;
|
|
1886
|
+
_preEditReplacedNewline = NO;
|
|
1875
1887
|
|
|
1876
1888
|
_lastTextLength = newLength;
|
|
1877
1889
|
|
|
1878
1890
|
#if !TARGET_OS_OSX
|
|
1879
1891
|
if (newLength == 0) {
|
|
1880
|
-
[self
|
|
1892
|
+
if ([self headingLevelForCursorParagraph] > 0 || [self listBlockForCursorParagraph] != nil) {
|
|
1893
|
+
[_typingController syncWithCursorBlock];
|
|
1894
|
+
} else {
|
|
1895
|
+
[self resetBaseTypingAttributes];
|
|
1896
|
+
}
|
|
1881
1897
|
}
|
|
1882
1898
|
#endif
|
|
1883
1899
|
|
|
1884
|
-
|
|
1900
|
+
if (touchedNewline) {
|
|
1901
|
+
[self applyFormatting];
|
|
1902
|
+
} else {
|
|
1903
|
+
[self applyFormattingScopedToEditAtLocation:editLocation insertedLength:insertedLength];
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (_textView.selectedRange.length == 0) {
|
|
1907
|
+
[_typingController syncWithCursorBlock];
|
|
1908
|
+
}
|
|
1885
1909
|
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1910
|
+
if ([self listBlockForCursorParagraph] != nil) {
|
|
1911
|
+
[_typingController syncWithCursorBlock];
|
|
1912
|
+
} else if (_textView.typingAttributes[NSParagraphStyleAttributeName] != nil) {
|
|
1913
|
+
[_typingController clearListParagraphStyle];
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
[_editPipeline detectLinksAtLocation:editLocation insertedLength:insertedLength];
|
|
1890
1917
|
|
|
1891
1918
|
[self updatePlaceholderVisibility];
|
|
1892
|
-
[
|
|
1893
|
-
[
|
|
1894
|
-
[
|
|
1919
|
+
[_inputEventEmitter emitOnChangeText];
|
|
1920
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
1921
|
+
[_inputEventEmitter emitFormattingChanged];
|
|
1895
1922
|
[self updateActiveMention];
|
|
1896
|
-
[
|
|
1923
|
+
[_inputEventEmitter emitCaretRectChangeIfNeeded];
|
|
1897
1924
|
[self requestHeightUpdate];
|
|
1898
1925
|
[self scheduleRelayoutIfNeeded];
|
|
1926
|
+
[self updateEmptyBulletMarker];
|
|
1899
1927
|
}
|
|
1900
1928
|
|
|
1901
1929
|
#pragma mark - Text view delegate
|
|
@@ -1933,17 +1961,21 @@ using namespace facebook::react;
|
|
|
1933
1961
|
{
|
|
1934
1962
|
[self stripLinkTypingAttributes];
|
|
1935
1963
|
|
|
1936
|
-
if (_textView.selectedRange.length == 0 &&
|
|
1964
|
+
if (_textView.selectedRange.length == 0 && _editSession.phase == ENRMEditPhaseIdle) {
|
|
1937
1965
|
NSString *text = ENRMGetPlainText(_textView);
|
|
1938
1966
|
if (text.length > 0) {
|
|
1939
1967
|
NSRange paragraphRange = [text paragraphRangeForRange:_textView.selectedRange];
|
|
1940
1968
|
NSString *paragraphText = [text substringWithRange:paragraphRange];
|
|
1941
1969
|
BOOL isEmpty = paragraphText.length == 0 || [paragraphText isEqualToString:@"\n"];
|
|
1942
1970
|
if (isEmpty) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1971
|
+
if ([self headingLevelForCursorParagraph] > 0 || [self listBlockForCursorParagraph] != nil) {
|
|
1972
|
+
[_typingController syncWithCursorBlock];
|
|
1973
|
+
} else {
|
|
1974
|
+
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
|
|
1975
|
+
attrs[NSFontAttributeName] = _formatterStyle.baseFont;
|
|
1976
|
+
attrs[NSForegroundColorAttributeName] = _formatterStyle.baseTextColor;
|
|
1977
|
+
_textView.typingAttributes = attrs;
|
|
1978
|
+
}
|
|
1947
1979
|
}
|
|
1948
1980
|
}
|
|
1949
1981
|
}
|
|
@@ -1951,125 +1983,154 @@ using namespace facebook::react;
|
|
|
1951
1983
|
|
|
1952
1984
|
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
|
1953
1985
|
{
|
|
1986
|
+
[_inputEventEmitter emitOnKeyPress:text];
|
|
1954
1987
|
if ([self deleteLinkForReplacementRange:range replacementText:text]) {
|
|
1955
1988
|
return NO;
|
|
1956
1989
|
}
|
|
1990
|
+
if ([self handleListKeyForReplacementRange:range replacementText:text]) {
|
|
1991
|
+
return NO;
|
|
1992
|
+
}
|
|
1957
1993
|
_preEditSelectedRange = _lastSelectedRange;
|
|
1958
|
-
|
|
1994
|
+
[self capturePreEditBlockForRange:range];
|
|
1995
|
+
_preEditParagraphWasEmpty = [self preEditParagraphWasEmpty:range];
|
|
1996
|
+
_preEditReplacedNewline = [self replacedRangeTouchesNewline:range];
|
|
1997
|
+
[_editSession enterPhase:ENRMEditPhaseProcessing];
|
|
1959
1998
|
[self stripLinkTypingAttributes];
|
|
1960
1999
|
return YES;
|
|
1961
2000
|
}
|
|
1962
2001
|
|
|
1963
2002
|
- (void)textViewDidChange:(UITextView *)textView
|
|
1964
2003
|
{
|
|
1965
|
-
if (
|
|
2004
|
+
if (_editSession.shouldSuppressFormatting) {
|
|
1966
2005
|
return;
|
|
1967
2006
|
}
|
|
1968
2007
|
[self handleTextChanged];
|
|
1969
|
-
|
|
1970
|
-
|
|
2008
|
+
[_editSession exitPhase];
|
|
2009
|
+
[_editSession recordTextChange];
|
|
1971
2010
|
_lastSelectedRange = textView.selectedRange;
|
|
1972
2011
|
}
|
|
1973
2012
|
|
|
1974
2013
|
- (void)textViewDidBeginEditing:(UITextView *)textView
|
|
1975
2014
|
{
|
|
1976
|
-
[
|
|
2015
|
+
[_inputEventEmitter emitOnFocus];
|
|
1977
2016
|
}
|
|
1978
2017
|
|
|
1979
2018
|
- (void)textViewDidEndEditing:(UITextView *)textView
|
|
1980
2019
|
{
|
|
1981
2020
|
[self clearActiveMention:nil];
|
|
1982
|
-
[
|
|
2021
|
+
[_inputEventEmitter emitOnBlur];
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/// Atomic-link snap clamped to the current text length, so a stale link range
|
|
2025
|
+
/// past the text end can't drive an unbounded snap/clamp loop.
|
|
2026
|
+
- (NSRange)clampedAtomicSelectionForSelection:(NSRange)selection
|
|
2027
|
+
{
|
|
2028
|
+
NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:selection];
|
|
2029
|
+
NSUInteger textLength = _textView.textStorage.length;
|
|
2030
|
+
if (adjusted.location > textLength) {
|
|
2031
|
+
return NSMakeRange(textLength, 0);
|
|
2032
|
+
}
|
|
2033
|
+
if (NSMaxRange(adjusted) > textLength) {
|
|
2034
|
+
adjusted.length = textLength - adjusted.location;
|
|
2035
|
+
}
|
|
2036
|
+
return adjusted;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/// YES while a UIKit selection gesture (handle drag, long-press loupe) is
|
|
2040
|
+
/// mid-flight; snapping selectedRange then fights the gesture and flickers the
|
|
2041
|
+
/// edit menu, so callers defer the snap until it settles.
|
|
2042
|
+
- (BOOL)selectionGestureIsActive
|
|
2043
|
+
{
|
|
2044
|
+
for (UIGestureRecognizer *recognizer in _textView.gestureRecognizers) {
|
|
2045
|
+
if (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged) {
|
|
2046
|
+
return YES;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return NO;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/// Re-applies the atomic-link snap once the selection gesture ends, polling the
|
|
2053
|
+
/// main queue because UIKit may not re-fire the selection delegate at touch-up.
|
|
2054
|
+
- (void)schedulePostGestureAtomicSnap
|
|
2055
|
+
{
|
|
2056
|
+
if (_atomicSnapScheduled) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
_atomicSnapScheduled = YES;
|
|
2060
|
+
__weak __typeof(self) weakSelf = self;
|
|
2061
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kENRMAtomicSnapPollInterval * NSEC_PER_SEC)),
|
|
2062
|
+
dispatch_get_main_queue(), ^{
|
|
2063
|
+
__typeof(self) strongSelf = weakSelf;
|
|
2064
|
+
if (strongSelf == nil) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
strongSelf->_atomicSnapScheduled = NO;
|
|
2068
|
+
if (strongSelf->_editSession.shouldSuppressSelectionSideEffects ||
|
|
2069
|
+
strongSelf->_editSession.isComposing) {
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if ([strongSelf selectionGestureIsActive]) {
|
|
2073
|
+
[strongSelf schedulePostGestureAtomicSnap];
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
NSRange selection = strongSelf->_textView.selectedRange;
|
|
2077
|
+
NSRange adjusted = [strongSelf clampedAtomicSelectionForSelection:selection];
|
|
2078
|
+
if (!NSEqualRanges(adjusted, selection)) {
|
|
2079
|
+
strongSelf->_textView.selectedRange = adjusted;
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
1983
2082
|
}
|
|
1984
2083
|
|
|
1985
2084
|
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
1986
2085
|
{
|
|
1987
2086
|
NSRange newSelection = textView.selectedRange;
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2087
|
+
if (!_editSession.shouldSuppressSelectionSideEffects && !_editSession.isComposing) {
|
|
2088
|
+
if ([self selectionGestureIsActive]) {
|
|
2089
|
+
[self schedulePostGestureAtomicSnap];
|
|
2090
|
+
} else {
|
|
2091
|
+
NSRange adjusted = [self clampedAtomicSelectionForSelection:newSelection];
|
|
2092
|
+
if (!NSEqualRanges(adjusted, newSelection)) {
|
|
2093
|
+
_textView.selectedRange = adjusted;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
1995
2096
|
}
|
|
1996
2097
|
}
|
|
1997
2098
|
NSRange previousSelection = _lastSelectedRange;
|
|
1998
2099
|
_lastSelectedRange = newSelection;
|
|
1999
2100
|
|
|
2000
|
-
if (
|
|
2101
|
+
if (_editSession.shouldSuppressSelectionSideEffects) {
|
|
2001
2102
|
return;
|
|
2002
2103
|
}
|
|
2003
2104
|
|
|
2004
|
-
if (
|
|
2105
|
+
if (_editSession.isComposing) {
|
|
2005
2106
|
return;
|
|
2006
2107
|
}
|
|
2007
2108
|
|
|
2008
|
-
|
|
2109
|
+
BOOL selectionMoved =
|
|
2110
|
+
newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
|
|
2111
|
+
|
|
2009
2112
|
if (_suppressMathChipClearOnce) {
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2113
|
+
// This caret move is the one handleMathChipTap: just made — keep its chip
|
|
2114
|
+
// highlighted and consume the suppression.
|
|
2012
2115
|
_suppressMathChipClearOnce = NO;
|
|
2013
|
-
} else {
|
|
2014
|
-
[
|
|
2015
|
-
|
|
2116
|
+
} else if (_activeMathChipPosition != NSNotFound) {
|
|
2117
|
+
[_mathChipController clearSelectionsExceptPosition:NSNotFound];
|
|
2118
|
+
_activeMathChipPosition = NSNotFound;
|
|
2016
2119
|
}
|
|
2017
|
-
#endif
|
|
2018
|
-
|
|
2019
|
-
BOOL selectionMoved =
|
|
2020
|
-
newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
|
|
2021
2120
|
|
|
2022
2121
|
if (selectionMoved) {
|
|
2023
|
-
[
|
|
2122
|
+
[_typingController resetForSelectionChange];
|
|
2024
2123
|
}
|
|
2025
2124
|
|
|
2026
2125
|
[self manageSelectionBasedChanges];
|
|
2027
2126
|
|
|
2028
|
-
[
|
|
2127
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
2029
2128
|
[self updateActiveMention];
|
|
2030
|
-
[
|
|
2031
|
-
[
|
|
2129
|
+
[_inputEventEmitter emitOnChangeState];
|
|
2130
|
+
[_inputEventEmitter emitCaretRectChangeIfNeeded];
|
|
2131
|
+
[self updateEmptyBulletMarker];
|
|
2032
2132
|
}
|
|
2033
2133
|
|
|
2034
|
-
#if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
|
|
2035
|
-
- (void)clearMathChipSelectionsExcept:(NSRange)keepRange
|
|
2036
|
-
{
|
|
2037
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
2038
|
-
if (storage.length == 0)
|
|
2039
|
-
return;
|
|
2040
|
-
__block BOOL anyChanged = NO;
|
|
2041
|
-
__block NSMutableArray<NSValue *> *invalidationRanges = [NSMutableArray array];
|
|
2042
|
-
[storage enumerateAttribute:NSAttachmentAttributeName
|
|
2043
|
-
inRange:NSMakeRange(0, storage.length)
|
|
2044
|
-
options:0
|
|
2045
|
-
usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
2046
|
-
if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
|
|
2047
|
-
return;
|
|
2048
|
-
ENRMMathInlineAttachment *att = value;
|
|
2049
|
-
BOOL keep = NSEqualRanges(range, keepRange);
|
|
2050
|
-
if (att.isSelected && !keep) {
|
|
2051
|
-
att.isSelected = NO;
|
|
2052
|
-
[att invalidateRender];
|
|
2053
|
-
[invalidationRanges addObject:[NSValue valueWithRange:range]];
|
|
2054
|
-
anyChanged = YES;
|
|
2055
|
-
}
|
|
2056
|
-
}];
|
|
2057
|
-
if (anyChanged) {
|
|
2058
|
-
NSLayoutManager *lm = storage.layoutManagers.firstObject;
|
|
2059
|
-
for (NSValue *v in invalidationRanges) {
|
|
2060
|
-
NSRange r = v.rangeValue;
|
|
2061
|
-
[lm invalidateLayoutForCharacterRange:r actualCharacterRange:NULL];
|
|
2062
|
-
}
|
|
2063
|
-
ENRMSetNeedsDisplay(_textView);
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
#else
|
|
2067
|
-
- (void)clearMathChipSelectionsExcept:(NSRange)keepRange
|
|
2068
|
-
{
|
|
2069
|
-
(void)keepRange;
|
|
2070
|
-
}
|
|
2071
|
-
#endif
|
|
2072
|
-
|
|
2073
2134
|
#else
|
|
2074
2135
|
|
|
2075
2136
|
#pragma mark - RCTBackedTextInputDelegate (macOS)
|
|
@@ -2081,7 +2142,7 @@ using namespace facebook::react;
|
|
|
2081
2142
|
|
|
2082
2143
|
- (void)textInputDidBeginEditing
|
|
2083
2144
|
{
|
|
2084
|
-
[
|
|
2145
|
+
[_inputEventEmitter emitOnFocus];
|
|
2085
2146
|
}
|
|
2086
2147
|
|
|
2087
2148
|
- (BOOL)textInputShouldEndEditing
|
|
@@ -2092,7 +2153,7 @@ using namespace facebook::react;
|
|
|
2092
2153
|
- (void)textInputDidEndEditing
|
|
2093
2154
|
{
|
|
2094
2155
|
[self clearActiveMention:nil];
|
|
2095
|
-
[
|
|
2156
|
+
[_inputEventEmitter emitOnBlur];
|
|
2096
2157
|
}
|
|
2097
2158
|
|
|
2098
2159
|
- (BOOL)textInputShouldReturn
|
|
@@ -2111,31 +2172,36 @@ using namespace facebook::react;
|
|
|
2111
2172
|
|
|
2112
2173
|
- (nullable NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
|
|
2113
2174
|
{
|
|
2175
|
+
[_inputEventEmitter emitOnKeyPress:text];
|
|
2114
2176
|
if ([self deleteLinkForReplacementRange:range replacementText:text]) {
|
|
2115
2177
|
return nil;
|
|
2116
2178
|
}
|
|
2117
2179
|
_preEditSelectedRange = _lastSelectedRange;
|
|
2118
|
-
|
|
2180
|
+
// Capture the same pre-edit block state the iOS path does — otherwise these
|
|
2181
|
+
// ivars carry stale values from a previous edit into reconcileBlockContinuation.
|
|
2182
|
+
[self capturePreEditBlockForRange:range];
|
|
2183
|
+
_preEditParagraphWasEmpty = [self preEditParagraphWasEmpty:range];
|
|
2184
|
+
_preEditReplacedNewline = [self replacedRangeTouchesNewline:range];
|
|
2185
|
+
[_editSession enterPhase:ENRMEditPhaseProcessing];
|
|
2119
2186
|
return text;
|
|
2120
2187
|
}
|
|
2121
2188
|
|
|
2122
2189
|
- (void)textInputDidChange
|
|
2123
2190
|
{
|
|
2124
|
-
if (
|
|
2125
|
-
|
|
2191
|
+
if (_editSession.shouldSuppressFormatting) {
|
|
2192
|
+
[_editSession exitPhase];
|
|
2126
2193
|
return;
|
|
2127
2194
|
}
|
|
2128
2195
|
[self handleTextChanged];
|
|
2129
|
-
|
|
2130
|
-
|
|
2196
|
+
[_editSession exitPhase];
|
|
2197
|
+
[_editSession recordTextChange];
|
|
2131
2198
|
_lastSelectedRange = _textView.selectedRange;
|
|
2132
2199
|
}
|
|
2133
2200
|
|
|
2134
2201
|
- (void)textInputDidChangeSelection
|
|
2135
2202
|
{
|
|
2136
2203
|
NSRange newSelection = _textView.selectedRange;
|
|
2137
|
-
|
|
2138
|
-
if (!_isApplyingFormatting && !_isTextChanging && !ENRMHasMarkedText(_textView)) {
|
|
2204
|
+
if (!_editSession.shouldSuppressSelectionSideEffects && !_editSession.isComposing) {
|
|
2139
2205
|
NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:newSelection];
|
|
2140
2206
|
if (!NSEqualRanges(adjusted, newSelection)) {
|
|
2141
2207
|
_textView.selectedRange = adjusted;
|
|
@@ -2145,11 +2211,11 @@ using namespace facebook::react;
|
|
|
2145
2211
|
NSRange previousSelection = _lastSelectedRange;
|
|
2146
2212
|
_lastSelectedRange = newSelection;
|
|
2147
2213
|
|
|
2148
|
-
if (
|
|
2214
|
+
if (_editSession.shouldSuppressSelectionSideEffects) {
|
|
2149
2215
|
return;
|
|
2150
2216
|
}
|
|
2151
2217
|
|
|
2152
|
-
if (
|
|
2218
|
+
if (_editSession.isComposing) {
|
|
2153
2219
|
return;
|
|
2154
2220
|
}
|
|
2155
2221
|
|
|
@@ -2157,13 +2223,13 @@ using namespace facebook::react;
|
|
|
2157
2223
|
newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
|
|
2158
2224
|
|
|
2159
2225
|
if (selectionMoved) {
|
|
2160
|
-
[
|
|
2226
|
+
[_typingController resetForSelectionChange];
|
|
2161
2227
|
}
|
|
2162
2228
|
|
|
2163
|
-
[
|
|
2229
|
+
[_inputEventEmitter emitOnChangeSelection];
|
|
2164
2230
|
[self updateActiveMention];
|
|
2165
|
-
[
|
|
2166
|
-
[
|
|
2231
|
+
[_inputEventEmitter emitOnChangeState];
|
|
2232
|
+
[_inputEventEmitter emitCaretRectChangeIfNeeded];
|
|
2167
2233
|
}
|
|
2168
2234
|
|
|
2169
2235
|
// @required stubs for RCTBackedTextInputDelegate — RCTUITextView's internal adapter
|