react-native-marcus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +227 -0
- package/RNMarcus.podspec +78 -0
- package/android/build.gradle +220 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/MarkdownParser.cpp +49 -0
- package/android/src/main/cpp/MarkdownParser.h +56 -0
- package/android/src/main/cpp/OnLoad.cpp +17 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusModule.kt +28 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +31 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +63 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManager.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManagerSpec.kt +21 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +333 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownParseResult.kt +12 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownParser.kt +89 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownRange.kt +11 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +81 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownTextWatcher.kt +17 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +84 -0
- package/android/src/main/java/app/getbullet/marcus/ReactLineHeight.java +29 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +105 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +67 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontFamilySpan.kt +28 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontSizeSpan.kt +7 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownForegroundColorSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +57 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLineHeightSpan.kt +21 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownSpan.kt +7 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownStrikethroughSpan.kt +5 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownUnderlineSpan.kt +5 -0
- package/android/src/main/new_arch/CMakeLists.txt +70 -0
- package/android/src/main/new_arch/RNMarcusSpec.cpp +43 -0
- package/android/src/main/new_arch/RNMarcusSpec.h +36 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.cpp +218 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.h +54 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +156 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +130 -0
- package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +205 -0
- package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +78 -0
- package/android/src/test/resources/render-model.txt +583 -0
- package/apple/MarcusModule.h +8 -0
- package/apple/MarcusModule.mm +26 -0
- package/apple/MarcusRange.h +15 -0
- package/apple/MarcusRange.mm +15 -0
- package/apple/MarcusTextInputDecoratorComponentView.h +12 -0
- package/apple/MarcusTextInputDecoratorComponentView.mm +121 -0
- package/apple/MarcusTextInputDecoratorShadowNode.cpp +251 -0
- package/apple/MarcusTextInputDecoratorShadowNode.h +65 -0
- package/apple/MarcusTextInputDecoratorShadowNode.mm +204 -0
- package/apple/MarcusTextInputDecoratorViewManager.h +8 -0
- package/apple/MarcusTextInputDecoratorViewManager.mm +11 -0
- package/apple/MarkdownAdaptiveImageGlyph.swift +44 -0
- package/apple/MarkdownAttributes.h +31 -0
- package/apple/MarkdownAttributes.m +9 -0
- package/apple/MarkdownBackedTextInputDelegate.swift +117 -0
- package/apple/MarkdownDecorator.swift +316 -0
- package/apple/MarkdownFonts.swift +21 -0
- package/apple/MarkdownFormatter+React.swift +51 -0
- package/apple/MarkdownFormatter.swift +509 -0
- package/apple/MarkdownLog.h +10 -0
- package/apple/MarkdownLog.mm +6 -0
- package/apple/MarkdownParser.swift +200 -0
- package/apple/MarkdownSwiftInterop.h +23 -0
- package/apple/MarkdownTextBackground.swift +17 -0
- package/apple/MarkdownTextFieldObserver.swift +90 -0
- package/apple/MarkdownTextLayoutFragment.swift +205 -0
- package/apple/MarkdownTextLayoutManagerDelegate.swift +84 -0
- package/apple/MarkdownTextStorageDelegate.swift +32 -0
- package/apple/MarkdownTextViewObserver.swift +30 -0
- package/apple/RCTMarcusStyle+Codegen.h +18 -0
- package/apple/RCTMarcusStyle+Codegen.mm +64 -0
- package/apple/RCTMarcusStyle.h +41 -0
- package/apple/RCTMarcusStyle.m +5 -0
- package/apple/RCTMarcusUtils.h +26 -0
- package/apple/RCTMarcusUtils.mm +26 -0
- package/apple/RCTTextInput+AdaptiveImageGlyph.h +16 -0
- package/apple/RCTTextInput+AdaptiveImageGlyph.mm +79 -0
- package/apple/RCTTextInputComponentView+Marcus.h +11 -0
- package/apple/RCTTextInputComponentView+Marcus.mm +79 -0
- package/cpp/MarkdownCxx.modulemap +8 -0
- package/cpp/MarkdownGlobal.cpp +48 -0
- package/cpp/MarkdownGlobal.h +33 -0
- package/cpp/MarkdownWorkletParser.cpp +93 -0
- package/cpp/MarkdownWorkletParser.h +45 -0
- package/cpp/RuntimeDecorator.cpp +39 -0
- package/cpp/RuntimeDecorator.h +14 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorState.h +33 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorViewComponentDescriptor.h +17 -0
- package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/lib/commonjs/MarkdownTextInput.js +99 -0
- package/lib/commonjs/MarkdownTextInput.js.map +1 -0
- package/lib/commonjs/MarkdownTextInput.web.js +618 -0
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
- package/lib/commonjs/NativeMarcusModule.js +9 -0
- package/lib/commonjs/NativeMarcusModule.js.map +1 -0
- package/lib/commonjs/commonTypes.js +2 -0
- package/lib/commonjs/commonTypes.js.map +1 -0
- package/lib/commonjs/global.d.js +2 -0
- package/lib/commonjs/global.d.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/parser/index.js +7030 -0
- package/lib/commonjs/parser/index.js.map +7 -0
- package/lib/commonjs/rangeUtils.js +71 -0
- package/lib/commonjs/rangeUtils.js.map +1 -0
- package/lib/commonjs/react-native-web.d.js +2 -0
- package/lib/commonjs/react-native-web.d.js.map +1 -0
- package/lib/commonjs/styleUtils.js +91 -0
- package/lib/commonjs/styleUtils.js.map +1 -0
- package/lib/commonjs/web/InputHistory.js +111 -0
- package/lib/commonjs/web/InputHistory.js.map +1 -0
- package/lib/commonjs/web/MarkdownTextInput.css +72 -0
- package/lib/commonjs/web/utils/blockLayout.js +132 -0
- package/lib/commonjs/web/utils/blockLayout.js.map +1 -0
- package/lib/commonjs/web/utils/blockUtils.js +219 -0
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
- package/lib/commonjs/web/utils/browserUtils.js +40 -0
- package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/cursorUtils.js +157 -0
- package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
- package/lib/commonjs/web/utils/inputUtils.js +152 -0
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
- package/lib/commonjs/web/utils/measureUtils.js +0 -0
- package/lib/commonjs/web/utils/measureUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +552 -0
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/treeUtils.js +109 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
- package/lib/commonjs/web/utils/webStyleUtils.js +59 -0
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
- package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/lib/module/MarkdownTextInput.js +94 -0
- package/lib/module/MarkdownTextInput.js.map +1 -0
- package/lib/module/MarkdownTextInput.web.js +612 -0
- package/lib/module/MarkdownTextInput.web.js.map +1 -0
- package/lib/module/NativeMarcusModule.js +5 -0
- package/lib/module/NativeMarcusModule.js.map +1 -0
- package/lib/module/commonTypes.js +2 -0
- package/lib/module/commonTypes.js.map +1 -0
- package/lib/module/global.d.js +2 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/parser/index.js +7029 -0
- package/lib/module/parser/index.js.map +7 -0
- package/lib/module/rangeUtils.js +66 -0
- package/lib/module/rangeUtils.js.map +1 -0
- package/lib/module/react-native-web.d.js +2 -0
- package/lib/module/react-native-web.d.js.map +1 -0
- package/lib/module/styleUtils.js +88 -0
- package/lib/module/styleUtils.js.map +1 -0
- package/lib/module/web/InputHistory.js +106 -0
- package/lib/module/web/InputHistory.js.map +1 -0
- package/lib/module/web/MarkdownTextInput.css +72 -0
- package/lib/module/web/utils/blockLayout.js +128 -0
- package/lib/module/web/utils/blockLayout.js.map +1 -0
- package/lib/module/web/utils/blockUtils.js +210 -0
- package/lib/module/web/utils/blockUtils.js.map +1 -0
- package/lib/module/web/utils/browserUtils.js +36 -0
- package/lib/module/web/utils/browserUtils.js.map +1 -0
- package/lib/module/web/utils/cursorUtils.js +150 -0
- package/lib/module/web/utils/cursorUtils.js.map +1 -0
- package/lib/module/web/utils/inputUtils.js +144 -0
- package/lib/module/web/utils/inputUtils.js.map +1 -0
- package/lib/module/web/utils/measureUtils.js +0 -0
- package/lib/module/web/utils/measureUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +547 -0
- package/lib/module/web/utils/parserUtils.js.map +1 -0
- package/lib/module/web/utils/treeUtils.js +103 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -0
- package/lib/module/web/utils/webStyleUtils.js +54 -0
- package/lib/module/web/utils/webStyleUtils.js.map +1 -0
- package/lib/typescript/commonjs/e2e/checks.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/checks.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/editing.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/editing.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/helpers.d.ts +23 -0
- package/lib/typescript/commonjs/e2e/helpers.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/smoke.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/smoke.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +22 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +41 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts +7 -0
- package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts +10 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +5 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts +8 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts +8 -0
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/rangeUtils.d.ts +6 -0
- package/lib/typescript/commonjs/src/rangeUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts +8 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/InputHistory.d.ts +24 -0
- package/lib/typescript/commonjs/src/web/InputHistory.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +41 -0
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +33 -0
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts +7 -0
- package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts +22 -0
- package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +44 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +25 -0
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +7 -0
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/lib/typescript/module/e2e/checks.test.d.ts +2 -0
- package/lib/typescript/module/e2e/checks.test.d.ts.map +1 -0
- package/lib/typescript/module/e2e/editing.test.d.ts +2 -0
- package/lib/typescript/module/e2e/editing.test.d.ts.map +1 -0
- package/lib/typescript/module/e2e/helpers.d.ts +23 -0
- package/lib/typescript/module/e2e/helpers.d.ts.map +1 -0
- package/lib/typescript/module/e2e/smoke.test.d.ts +2 -0
- package/lib/typescript/module/e2e/smoke.test.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts +22 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +41 -0
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeMarcusModule.d.ts +7 -0
- package/lib/typescript/module/src/NativeMarcusModule.d.ts.map +1 -0
- package/lib/typescript/module/src/commonTypes.d.ts +10 -0
- package/lib/typescript/module/src/commonTypes.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +5 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/index.d.ts +8 -0
- package/lib/typescript/module/src/parser/index.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts +8 -0
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -0
- package/lib/typescript/module/src/rangeUtils.d.ts +6 -0
- package/lib/typescript/module/src/rangeUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/styleUtils.d.ts +8 -0
- package/lib/typescript/module/src/styleUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/InputHistory.d.ts +24 -0
- package/lib/typescript/module/src/web/InputHistory.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts +41 -0
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts +33 -0
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/browserUtils.d.ts +7 -0
- package/lib/typescript/module/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/module/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/inputUtils.d.ts +12 -0
- package/lib/typescript/module/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/measureUtils.d.ts +22 -0
- package/lib/typescript/module/src/web/utils/measureUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts +44 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts +25 -0
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +7 -0
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/package.json +167 -0
- package/react-native.config.js +10 -0
- package/src/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/src/MarkdownTextInput.tsx +139 -0
- package/src/MarkdownTextInput.web.tsx +958 -0
- package/src/NativeMarcusModule.ts +8 -0
- package/src/commonTypes.ts +30 -0
- package/src/global.d.ts +1 -0
- package/src/index.tsx +4 -0
- package/src/parser/index.ts +556 -0
- package/src/parser/micromark-extension-mentions.ts +82 -0
- package/src/rangeUtils.ts +68 -0
- package/src/react-native-web.d.ts +1 -0
- package/src/styleUtils.ts +100 -0
- package/src/web/InputHistory.ts +137 -0
- package/src/web/MarkdownTextInput.css +72 -0
- package/src/web/utils/blockLayout.ts +163 -0
- package/src/web/utils/blockUtils.ts +245 -0
- package/src/web/utils/browserUtils.ts +41 -0
- package/src/web/utils/cursorUtils.ts +204 -0
- package/src/web/utils/inputUtils.ts +206 -0
- package/src/web/utils/measureUtils.ts +0 -0
- package/src/web/utils/parserUtils.ts +813 -0
- package/src/web/utils/treeUtils.ts +149 -0
- package/src/web/utils/webStyleUtils.ts +69 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import CxxStdlib
|
|
2
|
+
import Foundation
|
|
3
|
+
import MarkdownCxx
|
|
4
|
+
|
|
5
|
+
/// Caches markdown ranges and keeps the parser off the main thread.
|
|
6
|
+
///
|
|
7
|
+
/// Running the worklet waits on the markdown runtime. Waiting for it on the main
|
|
8
|
+
/// thread during a Yoga measure could outlast the watchdog, so callers on the
|
|
9
|
+
/// main thread use `cachedRanges(forText:withParserId:)` and fall back to
|
|
10
|
+
/// `warmCacheAsync(forText:withParserId:completion:)`.
|
|
11
|
+
@objc public final class MarkdownParser: NSObject {
|
|
12
|
+
|
|
13
|
+
/// One shared instance, so the view layer and the shadow node's measure pass
|
|
14
|
+
/// hit the same cache. With an instance each, every keystroke was parsed
|
|
15
|
+
/// twice: once when the text view reformatted, and again when the shadow node
|
|
16
|
+
/// measured and found its own cache empty.
|
|
17
|
+
@objc public static let sharedParser = MarkdownParser()
|
|
18
|
+
|
|
19
|
+
private struct CacheEntry {
|
|
20
|
+
let text: String
|
|
21
|
+
let parserId: Int
|
|
22
|
+
let ranges: [MarcusRange]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// One entry is not enough once the main thread is restricted to cached
|
|
26
|
+
/// ranges: two texts alternating (typing then undoing, or an input echoing
|
|
27
|
+
/// older text back) evict each other and the main thread never finds what it
|
|
28
|
+
/// needs. The cache is also shared by every markdown input on screen, so it
|
|
29
|
+
/// needs room for a few distinct texts -- still short enough to scan linearly,
|
|
30
|
+
/// and entries only hold range objects.
|
|
31
|
+
private static let cacheCapacity = 8
|
|
32
|
+
|
|
33
|
+
/// Serial on purpose: the markdown runtime runs one parse at a time anyway,
|
|
34
|
+
/// and serialising here avoids doing the same work twice.
|
|
35
|
+
private static let warmupQueue = DispatchQueue(
|
|
36
|
+
label: "app.getbullet.marcus.parser-cache-warmup",
|
|
37
|
+
qos: .userInitiated
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
/// Guards everything below, and is never held across a parse. Holding a lock
|
|
41
|
+
/// while waiting on the markdown runtime is what let a background parse block
|
|
42
|
+
/// the main thread until iOS killed the app.
|
|
43
|
+
private let lock = NSLock()
|
|
44
|
+
|
|
45
|
+
/// Newest entry first.
|
|
46
|
+
private var cache: [CacheEntry] = []
|
|
47
|
+
|
|
48
|
+
private var pendingText: String?
|
|
49
|
+
private var pendingParserId: NSNumber?
|
|
50
|
+
private var pendingCompletion: (() -> Void)?
|
|
51
|
+
private var warmupScheduled = false
|
|
52
|
+
|
|
53
|
+
// MARK: - Cache
|
|
54
|
+
|
|
55
|
+
/// Returns the ranges for (text, parserId) only if they are already cached.
|
|
56
|
+
/// Never runs the parser, so it is safe on the main thread.
|
|
57
|
+
@objc public func cachedRanges(
|
|
58
|
+
forText text: String,
|
|
59
|
+
withParserId parserId: NSNumber
|
|
60
|
+
) -> [MarcusRange]? {
|
|
61
|
+
lock.lock()
|
|
62
|
+
defer { lock.unlock() }
|
|
63
|
+
|
|
64
|
+
guard
|
|
65
|
+
let index = cache.firstIndex(where: {
|
|
66
|
+
$0.parserId == parserId.intValue && $0.text == text
|
|
67
|
+
})
|
|
68
|
+
else { return nil }
|
|
69
|
+
|
|
70
|
+
guard index != 0 else { return cache[0].ranges }
|
|
71
|
+
|
|
72
|
+
// Promote, so text asked for repeatedly is not the next evicted.
|
|
73
|
+
let entry = cache.remove(at: index)
|
|
74
|
+
cache.insert(entry, at: 0)
|
|
75
|
+
return entry.ranges
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func store(
|
|
79
|
+
_ ranges: [MarcusRange],
|
|
80
|
+
forText text: String,
|
|
81
|
+
parserId: NSNumber
|
|
82
|
+
) {
|
|
83
|
+
lock.lock()
|
|
84
|
+
defer { lock.unlock() }
|
|
85
|
+
|
|
86
|
+
cache.removeAll { $0.parserId == parserId.intValue && $0.text == text }
|
|
87
|
+
cache.insert(
|
|
88
|
+
CacheEntry(text: text, parserId: parserId.intValue, ranges: ranges),
|
|
89
|
+
at: 0
|
|
90
|
+
)
|
|
91
|
+
if cache.count > Self.cacheCapacity {
|
|
92
|
+
cache.removeLast(cache.count - Self.cacheCapacity)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Parsing
|
|
97
|
+
|
|
98
|
+
/// Returns cached ranges when there are any, otherwise parses and caches.
|
|
99
|
+
/// Never call this from the main thread on a layout path.
|
|
100
|
+
@objc public func parse(_ text: String, withParserId parserId: NSNumber)
|
|
101
|
+
-> [MarcusRange]
|
|
102
|
+
{
|
|
103
|
+
if let cached = cachedRanges(forText: text, withParserId: parserId) {
|
|
104
|
+
return cached
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Deliberately parsed with no lock held. Two threads may parse the same text
|
|
108
|
+
// concurrently, which is harmless -- they produce the same result.
|
|
109
|
+
let ranges = parseUncached(text, parserId: parserId)
|
|
110
|
+
store(ranges, forText: text, parserId: parserId)
|
|
111
|
+
return ranges
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func parseUncached(_ text: String, parserId: NSNumber)
|
|
115
|
+
-> [MarcusRange]
|
|
116
|
+
{
|
|
117
|
+
// `utf16.count` is the unit the worklet reports range offsets in.
|
|
118
|
+
let result = bulletpoint.marcus.parseMarkdown(
|
|
119
|
+
std.string(text),
|
|
120
|
+
text.utf16.count,
|
|
121
|
+
Int32(parserId.intValue)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
let schemaError = String(result.schemaError)
|
|
125
|
+
if !schemaError.isEmpty {
|
|
126
|
+
markdownLogWarn(
|
|
127
|
+
"[react-native-marcus] Incorrect schema of worklet parser output: \(schemaError)"
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result.ranges.map { range in
|
|
132
|
+
MarcusRange(
|
|
133
|
+
type: String(range.type),
|
|
134
|
+
range: NSRange(location: Int(range.start), length: Int(range.length)),
|
|
135
|
+
depth: UInt(range.depth)
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - Background warm-up
|
|
141
|
+
|
|
142
|
+
/// Parses on a background queue and caches the result, so the caller never
|
|
143
|
+
/// waits on the markdown runtime.
|
|
144
|
+
///
|
|
145
|
+
/// Only the newest request survives: a later call replaces one that is still
|
|
146
|
+
/// queued. A parse already in flight cannot be cancelled, but the newest text
|
|
147
|
+
/// is picked up as soon as it finishes.
|
|
148
|
+
///
|
|
149
|
+
/// `completion` runs on the background queue once the ranges are cached, and
|
|
150
|
+
/// is skipped when a newer request replaced this one -- that request reports
|
|
151
|
+
/// instead.
|
|
152
|
+
@objc public func warmCacheAsync(
|
|
153
|
+
forText text: String,
|
|
154
|
+
withParserId parserId: NSNumber,
|
|
155
|
+
completion: (() -> Void)?
|
|
156
|
+
) {
|
|
157
|
+
lock.lock()
|
|
158
|
+
// Keep only the newest request, so stale text can never win over newer text.
|
|
159
|
+
// The replaced completion goes with it; the newer request reports instead.
|
|
160
|
+
pendingText = text
|
|
161
|
+
pendingParserId = parserId
|
|
162
|
+
pendingCompletion = completion
|
|
163
|
+
let alreadyScheduled = warmupScheduled
|
|
164
|
+
warmupScheduled = true
|
|
165
|
+
lock.unlock()
|
|
166
|
+
|
|
167
|
+
// A drain loop is already running and will pick this up.
|
|
168
|
+
guard !alreadyScheduled else { return }
|
|
169
|
+
|
|
170
|
+
Self.warmupQueue.async { [weak self] in
|
|
171
|
+
self?.drainPendingWarmups()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func drainPendingWarmups() {
|
|
176
|
+
while true {
|
|
177
|
+
lock.lock()
|
|
178
|
+
guard let text = pendingText, let parserId = pendingParserId else {
|
|
179
|
+
warmupScheduled = false
|
|
180
|
+
lock.unlock()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
let completion = pendingCompletion
|
|
184
|
+
pendingText = nil
|
|
185
|
+
pendingParserId = nil
|
|
186
|
+
pendingCompletion = nil
|
|
187
|
+
lock.unlock()
|
|
188
|
+
|
|
189
|
+
_ = parse(text, withParserId: parserId)
|
|
190
|
+
|
|
191
|
+
lock.lock()
|
|
192
|
+
let superseded = pendingText != nil
|
|
193
|
+
lock.unlock()
|
|
194
|
+
|
|
195
|
+
if !superseded {
|
|
196
|
+
completion?()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Single entry point for Objective-C++ code that needs the Swift half of the
|
|
4
|
+
// module.
|
|
5
|
+
//
|
|
6
|
+
// The generated Swift interface header references Objective-C types from other
|
|
7
|
+
// modules without importing them, so those have to be in scope first. It also
|
|
8
|
+
// lands in a different place depending on how the pod is linked: inside the
|
|
9
|
+
// framework with use_frameworks!, and in the target's DerivedSources otherwise.
|
|
10
|
+
|
|
11
|
+
#import <React/RCTBackedTextInputDelegate.h>
|
|
12
|
+
#import <React/RCTUITextField.h>
|
|
13
|
+
#import <React/RCTUITextView.h>
|
|
14
|
+
|
|
15
|
+
#import <RNMarcus/MarcusRange.h>
|
|
16
|
+
#import <RNMarcus/RCTMarcusStyle.h>
|
|
17
|
+
#import <RNMarcus/RCTMarcusUtils.h>
|
|
18
|
+
|
|
19
|
+
#if __has_include(<RNMarcus/RNMarcus-Swift.h>)
|
|
20
|
+
#import <RNMarcus/RNMarcus-Swift.h>
|
|
21
|
+
#else
|
|
22
|
+
#import "RNMarcus-Swift.h"
|
|
23
|
+
#endif
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// The rounded background drawn behind a mention.
|
|
4
|
+
///
|
|
5
|
+
/// Stored as an attributed-string attribute value under
|
|
6
|
+
/// `.marcusTextBackground`, so it stays an object rather than a struct.
|
|
7
|
+
final class MarkdownTextBackground: NSObject {
|
|
8
|
+
var color: UIColor = .clear
|
|
9
|
+
var borderRadius: CGFloat = 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// A `MarkdownTextBackground` paired with the range it applies to. Only ever a
|
|
13
|
+
/// local pairing while drawing, never stored in an attributed string.
|
|
14
|
+
struct MarkdownTextBackgroundWithRange {
|
|
15
|
+
let textBackground: MarkdownTextBackground
|
|
16
|
+
let range: NSRange
|
|
17
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Keeps a single-line text field formatted. Unlike the multiline case there is
|
|
5
|
+
/// no text storage to hook, so this reacts to control events and KVO instead and
|
|
6
|
+
/// rewrites `attributedText` in place.
|
|
7
|
+
@objc public final class MarkdownTextFieldObserver: NSObject {
|
|
8
|
+
|
|
9
|
+
// Strong on purpose -- see MarkdownTextStorageDelegate.
|
|
10
|
+
private let textField: RCTUITextField
|
|
11
|
+
private let markdownUtils: RCTMarcusUtils
|
|
12
|
+
|
|
13
|
+
/// Guards against the KVO observer re-entering while we assign `attributedText`.
|
|
14
|
+
private var active = true
|
|
15
|
+
|
|
16
|
+
@objc public init(textField: RCTUITextField, markdownUtils: RCTMarcusUtils)
|
|
17
|
+
{
|
|
18
|
+
self.textField = textField
|
|
19
|
+
self.markdownUtils = markdownUtils
|
|
20
|
+
super.init()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public override func observeValue(
|
|
24
|
+
forKeyPath keyPath: String?,
|
|
25
|
+
of object: Any?,
|
|
26
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
27
|
+
context: UnsafeMutableRawPointer?
|
|
28
|
+
) {
|
|
29
|
+
guard active, keyPath == "text" || keyPath == "attributedText" else {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
applyMarkdownFormatting()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@objc public func textFieldDidChange(_ textField: UITextField) {
|
|
36
|
+
applyMarkdownFormatting()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
|
40
|
+
// Stops iOS underlining the whole field on blur when the text ends with a
|
|
41
|
+
// link: `defaultTextAttributes` does not carry NSUnderline yet at this
|
|
42
|
+
// point, so pushing a fresh value forces it to be picked up. The setter
|
|
43
|
+
// compares deeply, so the value has to actually differ each time -- hence
|
|
44
|
+
// the counter.
|
|
45
|
+
var defaultTextAttributes = self.textField.defaultTextAttributes ?? [:]
|
|
46
|
+
defaultTextAttributes[Self.forceUpdateAttributeName] =
|
|
47
|
+
Self.nextForceUpdateToken()
|
|
48
|
+
self.textField.defaultTextAttributes = defaultTextAttributes
|
|
49
|
+
|
|
50
|
+
applyMarkdownFormatting()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static let forceUpdateAttributeName = NSAttributedString.Key(
|
|
54
|
+
"RCTMarcusForceUpdate"
|
|
55
|
+
)
|
|
56
|
+
private static var forceUpdateCounter: UInt = 0
|
|
57
|
+
|
|
58
|
+
private static func nextForceUpdateToken() -> NSNumber {
|
|
59
|
+
defer { forceUpdateCounter &+= 1 }
|
|
60
|
+
return NSNumber(value: forceUpdateCounter)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func applyMarkdownFormatting() {
|
|
64
|
+
// Skip during multi-stage input (e.g. Japanese kana conversion), otherwise
|
|
65
|
+
// rewriting the text breaks the input method's internal state.
|
|
66
|
+
guard textField.markedTextRange == nil else { return }
|
|
67
|
+
|
|
68
|
+
guard
|
|
69
|
+
let attributedText = textField.attributedText?.mutableCopy()
|
|
70
|
+
as? NSMutableAttributedString
|
|
71
|
+
else { return }
|
|
72
|
+
|
|
73
|
+
markdownUtils.applyMarkdownFormatting(
|
|
74
|
+
attributedText,
|
|
75
|
+
withDefaultTextAttributes: textField.defaultTextAttributes ?? [:]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
let selectedTextRange = textField.selectedTextRange
|
|
79
|
+
|
|
80
|
+
active = false
|
|
81
|
+
textField.attributedText = attributedText
|
|
82
|
+
active = true
|
|
83
|
+
|
|
84
|
+
textField.setSelectedTextRange(selectedTextRange, notifyDelegate: false)
|
|
85
|
+
|
|
86
|
+
// Prevents underline flicker while typing when the previous text ends with
|
|
87
|
+
// a link.
|
|
88
|
+
textField.typingAttributes = textField.defaultTextAttributes
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Draws the parts of markdown rendering that plain attributed-string attributes
|
|
4
|
+
/// can't express: blockquote ribbons down the left edge, and rounded background
|
|
5
|
+
/// pills behind mentions.
|
|
6
|
+
///
|
|
7
|
+
/// Only constructed by `MarkdownTextLayoutManagerDelegate`, so it stays internal
|
|
8
|
+
/// to the Swift half of the module.
|
|
9
|
+
final class MarkdownTextLayoutFragment: NSTextLayoutFragment {
|
|
10
|
+
|
|
11
|
+
var markdownStyle: RCTMarcusStyle?
|
|
12
|
+
var depth: Int = 0
|
|
13
|
+
/// Distance from the paragraph's left edge to the blockquote's own box:
|
|
14
|
+
/// whatever containers nesting outside it indent by, plus the room left for a
|
|
15
|
+
/// list marker drawn in front of the ribbons.
|
|
16
|
+
var outerIndent: CGFloat = 0
|
|
17
|
+
var mentions: [MarkdownTextBackgroundWithRange] = []
|
|
18
|
+
|
|
19
|
+
// MARK: - NSTextLayoutFragment
|
|
20
|
+
|
|
21
|
+
override var renderingSurfaceBounds: CGRect {
|
|
22
|
+
guard depth > 0 else { return super.renderingSurfaceBounds }
|
|
23
|
+
return boundingRect.union(super.renderingSurfaceBounds)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override func draw(at point: CGPoint, in context: CGContext) {
|
|
27
|
+
guard !textLineFragments.isEmpty else {
|
|
28
|
+
super.draw(at: point, in: context)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
drawBlockquoteRibbons()
|
|
33
|
+
drawMentions()
|
|
34
|
+
|
|
35
|
+
super.draw(at: point, in: context)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// MARK: - Custom elements
|
|
39
|
+
|
|
40
|
+
private func drawBlockquoteRibbons() {
|
|
41
|
+
guard depth > 0, let style = markdownStyle else { return }
|
|
42
|
+
|
|
43
|
+
let borderWidth = style.blockquoteBorderWidth
|
|
44
|
+
let shift =
|
|
45
|
+
style.blockquoteMarginLeft + borderWidth + style.blockquotePaddingLeft
|
|
46
|
+
|
|
47
|
+
style.blockquoteBorderColor.setFill()
|
|
48
|
+
|
|
49
|
+
let bounds = boundingRect
|
|
50
|
+
for level in 0..<depth {
|
|
51
|
+
let x = bounds.origin.x + CGFloat(level) * shift
|
|
52
|
+
UIRectFill(
|
|
53
|
+
CGRect(
|
|
54
|
+
x: x,
|
|
55
|
+
y: bounds.origin.y,
|
|
56
|
+
width: borderWidth,
|
|
57
|
+
height: bounds.size.height
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func drawMentions() {
|
|
64
|
+
guard !mentions.isEmpty else { return }
|
|
65
|
+
|
|
66
|
+
let isSingleline = textLineFragments.count == 1
|
|
67
|
+
|
|
68
|
+
// Carried across line fragments: mentions are ordered, so each line resumes
|
|
69
|
+
// scanning where the previous one left off rather than restarting.
|
|
70
|
+
var mentionIndex = 0
|
|
71
|
+
|
|
72
|
+
for lineFragment in textLineFragments {
|
|
73
|
+
let lineRange = lineFragment.characterRange
|
|
74
|
+
if lineRange.length == 0 { continue }
|
|
75
|
+
|
|
76
|
+
let lineBounds = lineFragment.typographicBounds
|
|
77
|
+
// Absolute index into the source string, not a fragment-relative offset.
|
|
78
|
+
let lineEndLocation = lineFragment.locationForCharacter(
|
|
79
|
+
at: NSMaxRange(lineRange)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
while mentionIndex < mentions.count,
|
|
83
|
+
NSMaxRange(mentions[mentionIndex].range) <= lineRange.location
|
|
84
|
+
{
|
|
85
|
+
mentionIndex += 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for i in mentionIndex..<mentions.count {
|
|
89
|
+
let mention = mentions[i]
|
|
90
|
+
if mention.range.location >= NSMaxRange(lineRange) { break }
|
|
91
|
+
|
|
92
|
+
// Soft wrapping can split a mention across line fragments, so only the
|
|
93
|
+
// part falling on this line is drawn here; the corners are rounded on
|
|
94
|
+
// whichever side actually terminates the mention.
|
|
95
|
+
let intersection = NSIntersectionRange(lineRange, mention.range)
|
|
96
|
+
let startLocation = lineFragment.locationForCharacter(
|
|
97
|
+
at: intersection.location
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Singleline: the mention starts off screen, nothing to draw. Only
|
|
101
|
+
// happens while the text input is unfocused.
|
|
102
|
+
if isSingleline, startLocation.x == 0, intersection.location > 0 {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var endLocation = lineFragment.locationForCharacter(
|
|
107
|
+
at: intersection.location + intersection.length
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// Singleline: the mention is only partially visible -- it either starts
|
|
111
|
+
// mid-line or at the very beginning of the line.
|
|
112
|
+
if isSingleline,
|
|
113
|
+
startLocation.x > endLocation.x
|
|
114
|
+
|| (startLocation.x == endLocation.x && intersection.location == 0)
|
|
115
|
+
{
|
|
116
|
+
endLocation = lineEndLocation
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
guard
|
|
120
|
+
let font = lineFragment.attributedString.attribute(
|
|
121
|
+
.font,
|
|
122
|
+
at: intersection.location,
|
|
123
|
+
effectiveRange: nil
|
|
124
|
+
) as? UIFont
|
|
125
|
+
else { continue }
|
|
126
|
+
|
|
127
|
+
let backgroundRect = CGRect(
|
|
128
|
+
x: lineBounds.origin.x + startLocation.x,
|
|
129
|
+
y: lineBounds.origin.y + startLocation.y - font.ascender,
|
|
130
|
+
width: endLocation.x - startLocation.x,
|
|
131
|
+
height: font.lineHeight
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
var corners: UIRectCorner = []
|
|
135
|
+
if intersection.location == mention.range.location {
|
|
136
|
+
corners.insert([.topLeft, .bottomLeft])
|
|
137
|
+
}
|
|
138
|
+
if NSMaxRange(intersection) == NSMaxRange(mention.range) {
|
|
139
|
+
corners.insert([.topRight, .bottomRight])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let radius = mention.textBackground.borderRadius
|
|
143
|
+
let path =
|
|
144
|
+
corners.isEmpty
|
|
145
|
+
? UIBezierPath(rect: backgroundRect)
|
|
146
|
+
: UIBezierPath(
|
|
147
|
+
roundedRect: backgroundRect,
|
|
148
|
+
byRoundingCorners: corners,
|
|
149
|
+
cornerRadii: CGSize(width: radius, height: radius)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
mention.textBackground.color.setFill()
|
|
153
|
+
path.fill()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - Helpers
|
|
159
|
+
|
|
160
|
+
/// The strip to the left of the text that the blockquote ribbons occupy.
|
|
161
|
+
private var boundingRect: CGRect {
|
|
162
|
+
var bounds = CGRect.null
|
|
163
|
+
for lineFragment in textLineFragments
|
|
164
|
+
where lineFragment.characterRange.length != 0 {
|
|
165
|
+
bounds =
|
|
166
|
+
bounds.isNull
|
|
167
|
+
? lineFragment.typographicBounds
|
|
168
|
+
: bounds.union(lineFragment.typographicBounds)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A null rect has an infinite origin; offsetting it would yield an
|
|
172
|
+
// infinite rendering surface rather than an empty one.
|
|
173
|
+
guard !bounds.isNull, let style = markdownStyle else { return bounds }
|
|
174
|
+
|
|
175
|
+
let borderWidth = style.blockquoteBorderWidth
|
|
176
|
+
let shift =
|
|
177
|
+
style.blockquoteMarginLeft + borderWidth + style.blockquotePaddingLeft
|
|
178
|
+
|
|
179
|
+
// The ribbons sit at a fixed offset from the paragraph's own left edge,
|
|
180
|
+
// which is the text origin less however far this line is indented. Walking
|
|
181
|
+
// back from the text by the quote's own indent instead would move the bar
|
|
182
|
+
// whenever the line's indent changes -- a list nested inside the quote adds
|
|
183
|
+
// some, a marker drawn in front of the ribbon takes some away -- and split
|
|
184
|
+
// the bar between the lines of one quote.
|
|
185
|
+
let indent =
|
|
186
|
+
firstLineParagraphStyle?.firstLineHeadIndent ?? shift * CGFloat(depth)
|
|
187
|
+
|
|
188
|
+
bounds.origin.x -= indent - (outerIndent + style.blockquoteMarginLeft)
|
|
189
|
+
bounds.size.width = borderWidth + shift * CGFloat(depth - 1)
|
|
190
|
+
return bounds
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Paragraph style of the fragment's first non-empty line.
|
|
194
|
+
private var firstLineParagraphStyle: NSParagraphStyle? {
|
|
195
|
+
for lineFragment in textLineFragments
|
|
196
|
+
where lineFragment.characterRange.length != 0 {
|
|
197
|
+
return lineFragment.attributedString.attribute(
|
|
198
|
+
.paragraphStyle,
|
|
199
|
+
at: lineFragment.characterRange.location,
|
|
200
|
+
effectiveRange: nil
|
|
201
|
+
) as? NSParagraphStyle
|
|
202
|
+
}
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Vends a `MarkdownTextLayoutFragment` for paragraphs that carry markdown
|
|
4
|
+
/// attributes needing custom drawing, and a plain fragment for everything else.
|
|
5
|
+
@objc
|
|
6
|
+
public final class MarkdownTextLayoutManagerDelegate: NSObject,
|
|
7
|
+
NSTextLayoutManagerDelegate
|
|
8
|
+
{
|
|
9
|
+
|
|
10
|
+
private let textStorage: NSTextStorage
|
|
11
|
+
private let markdownUtils: RCTMarcusUtils
|
|
12
|
+
|
|
13
|
+
@objc public init(textStorage: NSTextStorage, markdownUtils: RCTMarcusUtils)
|
|
14
|
+
{
|
|
15
|
+
self.textStorage = textStorage
|
|
16
|
+
self.markdownUtils = markdownUtils
|
|
17
|
+
super.init()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public func textLayoutManager(
|
|
21
|
+
_ textLayoutManager: NSTextLayoutManager,
|
|
22
|
+
textLayoutFragmentFor location: NSTextLocation,
|
|
23
|
+
in textElement: NSTextElement
|
|
24
|
+
) -> NSTextLayoutFragment {
|
|
25
|
+
let plain = {
|
|
26
|
+
NSTextLayoutFragment(
|
|
27
|
+
textElement: textElement,
|
|
28
|
+
range: textElement.elementRange
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let index = textLayoutManager.offset(
|
|
33
|
+
from: textLayoutManager.documentRange.location,
|
|
34
|
+
to: location
|
|
35
|
+
)
|
|
36
|
+
guard index < textStorage.length else { return plain() }
|
|
37
|
+
|
|
38
|
+
let depth =
|
|
39
|
+
textStorage.attribute(
|
|
40
|
+
.marcusBlockquoteDepth,
|
|
41
|
+
at: index,
|
|
42
|
+
effectiveRange: nil
|
|
43
|
+
) as? NSNumber
|
|
44
|
+
let outerIndent =
|
|
45
|
+
textStorage.attribute(
|
|
46
|
+
.marcusBlockquoteIndent,
|
|
47
|
+
at: index,
|
|
48
|
+
effectiveRange: nil
|
|
49
|
+
) as? NSNumber
|
|
50
|
+
|
|
51
|
+
guard let paragraph = textElement as? NSTextParagraph else {
|
|
52
|
+
return plain()
|
|
53
|
+
}
|
|
54
|
+
let attributedString = paragraph.attributedString
|
|
55
|
+
|
|
56
|
+
var mentions: [MarkdownTextBackgroundWithRange] = []
|
|
57
|
+
attributedString.enumerateAttribute(
|
|
58
|
+
.marcusTextBackground,
|
|
59
|
+
in: NSRange(location: 0, length: attributedString.length)
|
|
60
|
+
) { value, range, _ in
|
|
61
|
+
guard let textBackground = value as? MarkdownTextBackground else {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
mentions.append(
|
|
65
|
+
MarkdownTextBackgroundWithRange(
|
|
66
|
+
textBackground: textBackground,
|
|
67
|
+
range: range
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
guard depth != nil || !mentions.isEmpty else { return plain() }
|
|
73
|
+
|
|
74
|
+
let fragment = MarkdownTextLayoutFragment(
|
|
75
|
+
textElement: textElement,
|
|
76
|
+
range: textElement.elementRange
|
|
77
|
+
)
|
|
78
|
+
fragment.markdownStyle = markdownUtils.markdownStyle
|
|
79
|
+
fragment.depth = depth?.intValue ?? 0
|
|
80
|
+
fragment.outerIndent = CGFloat(outerIndent?.doubleValue ?? 0)
|
|
81
|
+
fragment.mentions = mentions
|
|
82
|
+
return fragment
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Reformats the text view's storage after every edit.
|
|
5
|
+
@objc
|
|
6
|
+
public final class MarkdownTextStorageDelegate: NSObject, NSTextStorageDelegate
|
|
7
|
+
{
|
|
8
|
+
|
|
9
|
+
// Strong on purpose, matching the Objective-C original. Making it weak would
|
|
10
|
+
// let the text view deallocate while this object is still installed as its
|
|
11
|
+
// storage delegate and while KVO observers are registered on it.
|
|
12
|
+
private let textView: RCTUITextView
|
|
13
|
+
private let markdownUtils: RCTMarcusUtils
|
|
14
|
+
|
|
15
|
+
@objc public init(textView: RCTUITextView, markdownUtils: RCTMarcusUtils) {
|
|
16
|
+
self.textView = textView
|
|
17
|
+
self.markdownUtils = markdownUtils
|
|
18
|
+
super.init()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public func textStorage(
|
|
22
|
+
_ textStorage: NSTextStorage,
|
|
23
|
+
didProcessEditing editedMask: NSTextStorage.EditActions,
|
|
24
|
+
range editedRange: NSRange,
|
|
25
|
+
changeInLength delta: Int
|
|
26
|
+
) {
|
|
27
|
+
markdownUtils.applyMarkdownFormatting(
|
|
28
|
+
textStorage,
|
|
29
|
+
withDefaultTextAttributes: textView.defaultTextAttributes ?? [:]
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Re-applies formatting when the text view's default text attributes change,
|
|
5
|
+
/// which happens when style props such as colour or font size are updated.
|
|
6
|
+
@objc public final class MarkdownTextViewObserver: NSObject {
|
|
7
|
+
|
|
8
|
+
// Strong on purpose -- see MarkdownTextStorageDelegate.
|
|
9
|
+
private let textView: RCTUITextView
|
|
10
|
+
private let markdownUtils: RCTMarcusUtils
|
|
11
|
+
|
|
12
|
+
@objc public init(textView: RCTUITextView, markdownUtils: RCTMarcusUtils) {
|
|
13
|
+
self.textView = textView
|
|
14
|
+
self.markdownUtils = markdownUtils
|
|
15
|
+
super.init()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public override func observeValue(
|
|
19
|
+
forKeyPath keyPath: String?,
|
|
20
|
+
of object: Any?,
|
|
21
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
22
|
+
context: UnsafeMutableRawPointer?
|
|
23
|
+
) {
|
|
24
|
+
guard keyPath == "defaultTextAttributes" else { return }
|
|
25
|
+
// Round-tripping through textStorage re-triggers the storage delegate,
|
|
26
|
+
// which is what actually re-applies the formatting.
|
|
27
|
+
guard let attributedText = textView.attributedText else { return }
|
|
28
|
+
textView.textStorage.setAttributedString(attributedText)
|
|
29
|
+
}
|
|
30
|
+
}
|