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,333 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.content.res.AssetManager
|
|
4
|
+
import android.os.Trace
|
|
5
|
+
import android.text.SpannableStringBuilder
|
|
6
|
+
import android.text.Spanned
|
|
7
|
+
import android.text.TextPaint
|
|
8
|
+
import android.text.style.MetricAffectingSpan
|
|
9
|
+
import app.getbullet.marcus.spans.MarkdownBackgroundColorSpan
|
|
10
|
+
import app.getbullet.marcus.spans.MarkdownBackgroundSpan
|
|
11
|
+
import app.getbullet.marcus.spans.MarkdownBlockIndentSpan
|
|
12
|
+
import app.getbullet.marcus.spans.MarkdownBoldSpan
|
|
13
|
+
import app.getbullet.marcus.spans.MarkdownFontFamilySpan
|
|
14
|
+
import app.getbullet.marcus.spans.MarkdownFontSizeSpan
|
|
15
|
+
import app.getbullet.marcus.spans.MarkdownForegroundColorSpan
|
|
16
|
+
import app.getbullet.marcus.spans.MarkdownGapSpan
|
|
17
|
+
import app.getbullet.marcus.spans.MarkdownItalicSpan
|
|
18
|
+
import app.getbullet.marcus.spans.MarkdownLineHeightSpan
|
|
19
|
+
import app.getbullet.marcus.spans.MarkdownSpan
|
|
20
|
+
import app.getbullet.marcus.spans.MarkdownStrikethroughSpan
|
|
21
|
+
import app.getbullet.marcus.spans.MarkdownUnderlineSpan
|
|
22
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
23
|
+
|
|
24
|
+
class MarkdownFormatter(private val assetManager: AssetManager) {
|
|
25
|
+
|
|
26
|
+
fun format(
|
|
27
|
+
ssb: SpannableStringBuilder,
|
|
28
|
+
markdownRanges: List<MarkdownRange>,
|
|
29
|
+
markdownStyle: MarkdownStyle,
|
|
30
|
+
textPaint: TextPaint?
|
|
31
|
+
) {
|
|
32
|
+
try {
|
|
33
|
+
Trace.beginSection("format")
|
|
34
|
+
removeSpans(ssb)
|
|
35
|
+
applyRanges(ssb, markdownRanges, markdownStyle, textPaint)
|
|
36
|
+
} finally {
|
|
37
|
+
Trace.endSection()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun removeSpans(ssb: SpannableStringBuilder) {
|
|
42
|
+
try {
|
|
43
|
+
Trace.beginSection("removeSpans")
|
|
44
|
+
// We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
|
|
45
|
+
for (span in ssb.getSpans(0, ssb.length, MarkdownSpan::class.java)) {
|
|
46
|
+
ssb.removeSpan(span)
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
Trace.endSection()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun applyRanges(
|
|
54
|
+
ssb: SpannableStringBuilder,
|
|
55
|
+
markdownRanges: List<MarkdownRange>,
|
|
56
|
+
markdownStyle: MarkdownStyle,
|
|
57
|
+
textPaint: TextPaint?
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
Trace.beginSection("applyRanges")
|
|
61
|
+
// Containers arrive per line, outermost first, each preceded by the run of
|
|
62
|
+
// text its own marker takes up on that line.
|
|
63
|
+
val line = BlockLayout(markdownStyle, textPaint)
|
|
64
|
+
var prefix: MarkdownRange? = null
|
|
65
|
+
|
|
66
|
+
for (markdownRange in markdownRanges) {
|
|
67
|
+
if (markdownRange.type == "block-prefix") {
|
|
68
|
+
prefix = markdownRange
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val gutter = gutterOf(markdownRange, markdownStyle)
|
|
73
|
+
|
|
74
|
+
if (gutter != null) {
|
|
75
|
+
line.add(ssb, markdownRange, gutter, prefix)
|
|
76
|
+
prefix = null
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
applyRange(ssb, markdownRange, markdownStyle)
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
Trace.endSection()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The gutter a container reserves for itself, or null if it is not one. */
|
|
88
|
+
private fun gutterOf(markdownRange: MarkdownRange, markdownStyle: MarkdownStyle): Float? {
|
|
89
|
+
val step = when (markdownRange.type) {
|
|
90
|
+
"blockquote" ->
|
|
91
|
+
markdownStyle.blockquoteMarginLeft +
|
|
92
|
+
markdownStyle.blockquoteBorderWidth +
|
|
93
|
+
markdownStyle.blockquotePaddingLeft
|
|
94
|
+
|
|
95
|
+
"list-ordered" ->
|
|
96
|
+
markdownStyle.orderedListMarginLeft + markdownStyle.orderedListPaddingLeft
|
|
97
|
+
|
|
98
|
+
"list-unordered" ->
|
|
99
|
+
markdownStyle.unorderedListMarginLeft + markdownStyle.unorderedListPaddingLeft
|
|
100
|
+
|
|
101
|
+
else -> return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return PixelUtil.toPixelFromDIP(step) * markdownRange.depth
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Places the containers of one line, left to right.
|
|
109
|
+
*
|
|
110
|
+
* Each container reserves a gutter and is then followed by its own marker,
|
|
111
|
+
* which is text and so has to be stepped over rather than reserved: the
|
|
112
|
+
* container nested inside it starts after the marker, not in front of it. That
|
|
113
|
+
* is what puts a quote's ribbons after a list bullet, and a list's indent
|
|
114
|
+
* after a quote's `>`.
|
|
115
|
+
*
|
|
116
|
+
* A line continuing a block carries no marker of its own, and reuses the one
|
|
117
|
+
* that opened it -- otherwise its text, and any ribbon beside it, would sit at
|
|
118
|
+
* a different offset than the line above and break the block in two.
|
|
119
|
+
*/
|
|
120
|
+
private class BlockLayout(
|
|
121
|
+
private val markdownStyle: MarkdownStyle,
|
|
122
|
+
private val textPaint: TextPaint?
|
|
123
|
+
) {
|
|
124
|
+
/** Scratch paint, reset from [textPaint] before each measurement. */
|
|
125
|
+
private val measurePaint = TextPaint()
|
|
126
|
+
|
|
127
|
+
/** Last marker seen for each container type, for the lines that continue it. */
|
|
128
|
+
private val markers = HashMap<String, MarkdownRange>()
|
|
129
|
+
private var lineStart = -1
|
|
130
|
+
/** Offset reached so far, from the line's own left edge. */
|
|
131
|
+
private var offset = 0f
|
|
132
|
+
/**
|
|
133
|
+
* Where the line's first marker begins, if it has one. The text starts
|
|
134
|
+
* there; everything past it is held open with padding instead.
|
|
135
|
+
*/
|
|
136
|
+
private var textStart = -1f
|
|
137
|
+
/** Marker of the container placed most recently, if it is on this line. */
|
|
138
|
+
private var padded: MarkdownRange? = null
|
|
139
|
+
private var span: MarkdownBlockIndentSpan? = null
|
|
140
|
+
|
|
141
|
+
fun add(
|
|
142
|
+
ssb: SpannableStringBuilder,
|
|
143
|
+
markdownRange: MarkdownRange,
|
|
144
|
+
gutter: Float,
|
|
145
|
+
prefix: MarkdownRange?
|
|
146
|
+
) {
|
|
147
|
+
if (markdownRange.start != lineStart) {
|
|
148
|
+
lineStart = markdownRange.start
|
|
149
|
+
offset = 0f
|
|
150
|
+
textStart = -1f
|
|
151
|
+
padded = null
|
|
152
|
+
span = MarkdownBlockIndentSpan(
|
|
153
|
+
markdownStyle.blockquoteBorderColor,
|
|
154
|
+
PixelUtil.toPixelFromDIP(markdownStyle.blockquoteBorderWidth),
|
|
155
|
+
PixelUtil.toPixelFromDIP(
|
|
156
|
+
markdownStyle.blockquoteMarginLeft +
|
|
157
|
+
markdownStyle.blockquoteBorderWidth +
|
|
158
|
+
markdownStyle.blockquotePaddingLeft
|
|
159
|
+
)
|
|
160
|
+
).also {
|
|
161
|
+
ssb.setSpan(it, markdownRange.start, markdownRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
val indent = span ?: return
|
|
166
|
+
|
|
167
|
+
padded?.let {
|
|
168
|
+
ssb.setSpan(
|
|
169
|
+
MarkdownGapSpan(gutter, markdownStyle.syntaxColor),
|
|
170
|
+
it.end - 1,
|
|
171
|
+
it.end,
|
|
172
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (markdownRange.type == "blockquote") {
|
|
177
|
+
indent.ribbonOffset = offset + PixelUtil.toPixelFromDIP(markdownStyle.blockquoteMarginLeft)
|
|
178
|
+
indent.depth = markdownRange.depth
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
offset += gutter
|
|
182
|
+
|
|
183
|
+
if (prefix != null) {
|
|
184
|
+
markers[markdownRange.type] = prefix
|
|
185
|
+
if (textStart < 0f) {
|
|
186
|
+
textStart = offset
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
(prefix ?: markers[markdownRange.type])?.let {
|
|
191
|
+
offset += measure(ssb, it.start, it.end)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
padded = prefix
|
|
195
|
+
indent.firstLineIndent = if (textStart >= 0f) textStart else offset
|
|
196
|
+
indent.indent = offset
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Width a marker renders at.
|
|
201
|
+
*
|
|
202
|
+
* The view's paint carries the text size while the measure path has none and
|
|
203
|
+
* leaves it on the spannable, so the metric spans are folded in either way.
|
|
204
|
+
* Padding added after a marker is a replacement span, which contributes
|
|
205
|
+
* nothing here -- it belongs to the container that follows, not the marker.
|
|
206
|
+
*/
|
|
207
|
+
private fun measure(ssb: SpannableStringBuilder, start: Int, end: Int): Float {
|
|
208
|
+
if (textPaint != null) {
|
|
209
|
+
measurePaint.set(textPaint)
|
|
210
|
+
} else {
|
|
211
|
+
measurePaint.reset()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (span in ssb.getSpans(start, end, MetricAffectingSpan::class.java)) {
|
|
215
|
+
span.updateMeasureState(measurePaint)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return measurePaint.measureText(ssb, start, end)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private fun applyRange(
|
|
223
|
+
ssb: SpannableStringBuilder,
|
|
224
|
+
markdownRange: MarkdownRange,
|
|
225
|
+
markdownStyle: MarkdownStyle
|
|
226
|
+
) {
|
|
227
|
+
val start = markdownRange.start
|
|
228
|
+
val end = markdownRange.end
|
|
229
|
+
when (markdownRange.type) {
|
|
230
|
+
"bold" -> setSpan(ssb, MarkdownBoldSpan(), start, end)
|
|
231
|
+
|
|
232
|
+
"italic" -> setSpan(ssb, MarkdownItalicSpan(), start, end)
|
|
233
|
+
|
|
234
|
+
"strikethrough" -> setSpan(ssb, MarkdownStrikethroughSpan(), start, end)
|
|
235
|
+
|
|
236
|
+
"emoji" -> {
|
|
237
|
+
setSpan(
|
|
238
|
+
ssb,
|
|
239
|
+
MarkdownFontFamilySpan(markdownStyle.emojiFontFamily, assetManager),
|
|
240
|
+
start,
|
|
241
|
+
end
|
|
242
|
+
)
|
|
243
|
+
setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.emojiFontSize), start, end)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
"mention-here" -> {
|
|
247
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionHereColor), start, end)
|
|
248
|
+
setSpan(
|
|
249
|
+
ssb,
|
|
250
|
+
MarkdownBackgroundSpan(
|
|
251
|
+
markdownStyle.mentionHereBackgroundColor,
|
|
252
|
+
markdownStyle.mentionHereBorderRadius,
|
|
253
|
+
start,
|
|
254
|
+
end
|
|
255
|
+
),
|
|
256
|
+
start,
|
|
257
|
+
end
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
"mention-user" -> {
|
|
262
|
+
// TODO: change mention color when it mentions current user
|
|
263
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionUserColor), start, end)
|
|
264
|
+
setSpan(
|
|
265
|
+
ssb,
|
|
266
|
+
MarkdownBackgroundSpan(
|
|
267
|
+
markdownStyle.mentionUserBackgroundColor,
|
|
268
|
+
markdownStyle.mentionUserBorderRadius,
|
|
269
|
+
start,
|
|
270
|
+
end
|
|
271
|
+
),
|
|
272
|
+
start,
|
|
273
|
+
end
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
"mention-report" -> {
|
|
278
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionReportColor), start, end)
|
|
279
|
+
setSpan(
|
|
280
|
+
ssb,
|
|
281
|
+
MarkdownBackgroundSpan(
|
|
282
|
+
markdownStyle.mentionReportBackgroundColor,
|
|
283
|
+
markdownStyle.mentionReportBorderRadius,
|
|
284
|
+
start,
|
|
285
|
+
end
|
|
286
|
+
),
|
|
287
|
+
start,
|
|
288
|
+
end
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
"syntax" -> setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.syntaxColor), start, end)
|
|
293
|
+
|
|
294
|
+
"link" -> {
|
|
295
|
+
setSpan(ssb, MarkdownUnderlineSpan(), start, end)
|
|
296
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.linkColor), start, end)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
"code" -> {
|
|
300
|
+
setSpan(ssb, MarkdownFontFamilySpan(markdownStyle.codeFontFamily, assetManager), start, end)
|
|
301
|
+
setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.codeFontSize), start, end)
|
|
302
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.codeColor), start, end)
|
|
303
|
+
setSpan(ssb, MarkdownBackgroundColorSpan(markdownStyle.codeBackgroundColor), start, end)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
"pre" -> {
|
|
307
|
+
setSpan(ssb, MarkdownFontFamilySpan(markdownStyle.preFontFamily, assetManager), start, end)
|
|
308
|
+
setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.preFontSize), start, end)
|
|
309
|
+
setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.preColor), start, end)
|
|
310
|
+
setSpan(ssb, MarkdownBackgroundColorSpan(markdownStyle.preBackgroundColor), start, end)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
"heading" -> {
|
|
314
|
+
setSpan(ssb, MarkdownBoldSpan(), start, end)
|
|
315
|
+
// Level N is the base size scaled N-1 times, so a single pair of style
|
|
316
|
+
// values covers all six.
|
|
317
|
+
var fontSize = markdownStyle.headingFontSize
|
|
318
|
+
repeat(maxOf(markdownRange.depth, 1) - 1) { fontSize *= markdownStyle.headingScale }
|
|
319
|
+
val lineHeight = ReactLineHeight.find(ssb)
|
|
320
|
+
if (lineHeight >= 0) {
|
|
321
|
+
setSpan(ssb, MarkdownLineHeightSpan(lineHeight * (fontSize / markdownStyle.headingFontSize) * 1.5f), start, end)
|
|
322
|
+
}
|
|
323
|
+
// NOTE: size span must be set after line height span to avoid height jumps
|
|
324
|
+
setSpan(ssb, MarkdownFontSizeSpan(fontSize), start, end)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private fun setSpan(ssb: SpannableStringBuilder, span: MarkdownSpan, start: Int, end: Int) {
|
|
331
|
+
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What [MarkdownParser.nativeParse] hands back across JNI. Constructed from C++ via fbjni
|
|
5
|
+
* `newInstance`, so the constructor signature is load-bearing:
|
|
6
|
+
* `([Lapp/getbullet/marcus/MarkdownRange;Ljava/lang/String;)V`.
|
|
7
|
+
*/
|
|
8
|
+
class MarkdownParseResult(
|
|
9
|
+
@JvmField val ranges: Array<MarkdownRange>,
|
|
10
|
+
/** Non-null when the worklet returned something that did not match the expected schema. */
|
|
11
|
+
@JvmField val schemaError: String?
|
|
12
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.os.Trace
|
|
4
|
+
import app.getbullet.marcus.MarkdownParser.Companion.shared
|
|
5
|
+
import com.facebook.soloader.SoLoader
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs the registered markdown parser worklet and caches its output.
|
|
9
|
+
*
|
|
10
|
+
* Use [shared]. Each markdown input has two independent callers, the decorator view's
|
|
11
|
+
* TextWatcher and CustomFabricUIManager's measureText, and one cache shared between them
|
|
12
|
+
* beats one cache each. Note this is not a 2x saving: tracing shows a keystroke produces
|
|
13
|
+
* four parse calls but only one worklet run, because they all ask for the same text.
|
|
14
|
+
*
|
|
15
|
+
* Knows nothing about ReactContext or logging: it outlives every reload, so holding a context
|
|
16
|
+
* would leak it, and reporting is left to the caller via [onSchemaError].
|
|
17
|
+
*/
|
|
18
|
+
class MarkdownParser private constructor() {
|
|
19
|
+
|
|
20
|
+
private data class CacheKey(val text: String, val parserId: Int)
|
|
21
|
+
|
|
22
|
+
private val lock = Any()
|
|
23
|
+
|
|
24
|
+
private val cache =
|
|
25
|
+
object : LinkedHashMap<CacheKey, List<MarkdownRange>>(CACHE_CAPACITY, 0.75f, true) {
|
|
26
|
+
override fun removeEldestEntry(
|
|
27
|
+
eldest: MutableMap.MutableEntry<CacheKey, List<MarkdownRange>>
|
|
28
|
+
): Boolean = size > CACHE_CAPACITY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Registered from C++ by JMarkdownParser::registerNatives in MarkdownParser.cpp. Must stay
|
|
32
|
+
// an instance method to match the `alias_ref<JMarkdownParser>` first parameter there.
|
|
33
|
+
private external fun nativeParse(text: String, parserId: Int): MarkdownParseResult
|
|
34
|
+
|
|
35
|
+
fun parse(
|
|
36
|
+
text: String,
|
|
37
|
+
parserId: Int,
|
|
38
|
+
onSchemaError: (String) -> Unit
|
|
39
|
+
): List<MarkdownRange> {
|
|
40
|
+
try {
|
|
41
|
+
Trace.beginSection("parse")
|
|
42
|
+
|
|
43
|
+
val key = CacheKey(text, parserId)
|
|
44
|
+
synchronized(lock) { cache[key] }?.let { return it }
|
|
45
|
+
|
|
46
|
+
// Deliberately outside the lock. nativeParse waits on the markdown worklet runtime,
|
|
47
|
+
// and holding a lock across that wait is what let a background parse block the main
|
|
48
|
+
// thread until iOS killed the app (Expensify/react-native-live-markdown#772). Android
|
|
49
|
+
// has no watchdog to make it fatal, but it is the same defect.
|
|
50
|
+
val ranges = parseUncached(text, parserId, onSchemaError)
|
|
51
|
+
|
|
52
|
+
synchronized(lock) { cache[key] = ranges }
|
|
53
|
+
return ranges
|
|
54
|
+
} finally {
|
|
55
|
+
Trace.endSection()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun parseUncached(
|
|
60
|
+
text: String,
|
|
61
|
+
parserId: Int,
|
|
62
|
+
onSchemaError: (String) -> Unit
|
|
63
|
+
): List<MarkdownRange> {
|
|
64
|
+
val result = try {
|
|
65
|
+
Trace.beginSection("nativeParse")
|
|
66
|
+
nativeParse(text, parserId)
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
// Skip formatting, runGuarded will show the error in LogBox
|
|
69
|
+
return emptyList()
|
|
70
|
+
} finally {
|
|
71
|
+
Trace.endSection()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result.schemaError?.let(onSchemaError)
|
|
75
|
+
|
|
76
|
+
return result.ranges.asList()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
companion object {
|
|
80
|
+
private const val CACHE_CAPACITY = 8
|
|
81
|
+
|
|
82
|
+
init {
|
|
83
|
+
SoLoader.loadLibrary("marcus")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@JvmStatic
|
|
87
|
+
val shared: MarkdownParser = MarkdownParser()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.annotation.ColorInt
|
|
5
|
+
import com.facebook.react.bridge.ColorPropConverter
|
|
6
|
+
import com.facebook.react.bridge.JSApplicationCausedNativeException
|
|
7
|
+
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.ReadableType
|
|
9
|
+
|
|
10
|
+
class MarkdownStyle(map: ReadableMap, context: Context) {
|
|
11
|
+
private val screenDensity = context.resources.displayMetrics.density
|
|
12
|
+
|
|
13
|
+
@ColorInt val syntaxColor = parseColor(map, "syntax", "color", context)
|
|
14
|
+
@ColorInt val linkColor = parseColor(map, "link", "color", context)
|
|
15
|
+
|
|
16
|
+
val headingFontSize = parseFloat(map, "heading", "fontSize")
|
|
17
|
+
|
|
18
|
+
val headingScale = parseFloat(map, "heading", "scale")
|
|
19
|
+
|
|
20
|
+
val emojiFontSize = parseFloat(map, "emoji", "fontSize")
|
|
21
|
+
val emojiFontFamily = parseString(map, "emoji", "fontFamily")
|
|
22
|
+
|
|
23
|
+
@ColorInt val blockquoteBorderColor = parseColor(map, "blockquote", "borderColor", context)
|
|
24
|
+
val blockquoteBorderWidth = parseFloat(map, "blockquote", "borderWidth")
|
|
25
|
+
val blockquoteMarginLeft = parseFloat(map, "blockquote", "marginLeft")
|
|
26
|
+
val blockquotePaddingLeft = parseFloat(map, "blockquote", "paddingLeft")
|
|
27
|
+
|
|
28
|
+
val orderedListMarginLeft = parseFloat(map, "orderedList", "marginLeft")
|
|
29
|
+
val orderedListPaddingLeft = parseFloat(map, "orderedList", "paddingLeft")
|
|
30
|
+
|
|
31
|
+
val unorderedListMarginLeft = parseFloat(map, "unorderedList", "marginLeft")
|
|
32
|
+
val unorderedListPaddingLeft = parseFloat(map, "unorderedList", "paddingLeft")
|
|
33
|
+
|
|
34
|
+
val codeFontFamily = parseString(map, "code", "fontFamily")
|
|
35
|
+
val codeFontSize = parseFloat(map, "code", "fontSize")
|
|
36
|
+
@ColorInt val codeColor = parseColor(map, "code", "color", context)
|
|
37
|
+
@ColorInt val codeBackgroundColor = parseColor(map, "code", "backgroundColor", context)
|
|
38
|
+
|
|
39
|
+
val preFontFamily = parseString(map, "pre", "fontFamily")
|
|
40
|
+
val preFontSize = parseFloat(map, "pre", "fontSize")
|
|
41
|
+
@ColorInt val preColor = parseColor(map, "pre", "color", context)
|
|
42
|
+
@ColorInt val preBackgroundColor = parseColor(map, "pre", "backgroundColor", context)
|
|
43
|
+
|
|
44
|
+
@ColorInt val mentionHereColor = parseColor(map, "mentionHere", "color", context)
|
|
45
|
+
@ColorInt val mentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context)
|
|
46
|
+
val mentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity
|
|
47
|
+
|
|
48
|
+
@ColorInt val mentionUserColor = parseColor(map, "mentionUser", "color", context)
|
|
49
|
+
@ColorInt val mentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context)
|
|
50
|
+
val mentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity
|
|
51
|
+
|
|
52
|
+
@ColorInt val mentionReportColor = parseColor(map, "mentionReport", "color", context)
|
|
53
|
+
@ColorInt val mentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context)
|
|
54
|
+
val mentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity
|
|
55
|
+
|
|
56
|
+
private companion object {
|
|
57
|
+
fun style(map: ReadableMap, key: String): ReadableMap =
|
|
58
|
+
requireNotNull(map.getMap(key)) { "[react-native-marcus] markdownStyle.$key is missing" }
|
|
59
|
+
|
|
60
|
+
@ColorInt
|
|
61
|
+
fun parseColor(map: ReadableMap, key: String, prop: String, context: Context): Int {
|
|
62
|
+
val value = style(map, key).getDynamic(prop)
|
|
63
|
+
val color = when (value.type) {
|
|
64
|
+
ReadableType.Number -> ColorPropConverter.getColor(value.asDouble(), context)
|
|
65
|
+
ReadableType.Map -> ColorPropConverter.getColor(value.asMap(), context)
|
|
66
|
+
else -> throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.")
|
|
67
|
+
}
|
|
68
|
+
return requireNotNull(color) {
|
|
69
|
+
"[react-native-marcus] markdownStyle.$key.$prop could not be resolved to a color"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun parseFloat(map: ReadableMap, key: String, prop: String): Float =
|
|
74
|
+
style(map, key).getDouble(prop).toFloat()
|
|
75
|
+
|
|
76
|
+
fun parseString(map: ReadableMap, key: String, prop: String): String =
|
|
77
|
+
requireNotNull(style(map, key).getString(prop)) {
|
|
78
|
+
"[react-native-marcus] markdownStyle.$key.$prop is missing"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.text.Editable
|
|
4
|
+
import android.text.SpannableStringBuilder
|
|
5
|
+
import android.text.TextWatcher
|
|
6
|
+
|
|
7
|
+
class MarkdownTextWatcher(private val markdownUtils: MarkdownUtils) : TextWatcher {
|
|
8
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
9
|
+
|
|
10
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
11
|
+
|
|
12
|
+
override fun afterTextChanged(editable: Editable) {
|
|
13
|
+
if (editable is SpannableStringBuilder) {
|
|
14
|
+
markdownUtils.applyMarkdownFormatting(editable)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.os.Trace
|
|
4
|
+
import android.text.SpannableStringBuilder
|
|
5
|
+
import android.text.Spanned
|
|
6
|
+
import android.text.TextPaint
|
|
7
|
+
import android.text.TextUtils
|
|
8
|
+
import com.facebook.react.bridge.ReactContext
|
|
9
|
+
import com.facebook.react.util.RNLog
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param textPaint the view's own paint, held live rather than copied: the
|
|
13
|
+
* formatter measures block markers with it to work out where the containers
|
|
14
|
+
* nested inside them start, and has to see the size the text is drawn at.
|
|
15
|
+
* Null on the measure path, which has no view yet and carries the size on the
|
|
16
|
+
* spannable instead.
|
|
17
|
+
*/
|
|
18
|
+
class MarkdownUtils @JvmOverloads constructor(
|
|
19
|
+
private val reactContext: ReactContext,
|
|
20
|
+
private val textPaint: TextPaint? = null
|
|
21
|
+
) {
|
|
22
|
+
private val markdownFormatter = MarkdownFormatter(reactContext.assets)
|
|
23
|
+
|
|
24
|
+
// Hoisted rather than built per call: applyMarkdownFormatting runs several times per
|
|
25
|
+
// keystroke, and this is only ever needed for a warning that normally never fires.
|
|
26
|
+
private val onSchemaError: (String) -> Unit = { error ->
|
|
27
|
+
RNLog.w(reactContext, "[react-native-marcus] Incorrect schema of worklet parser output: $error")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var markdownStyle: MarkdownStyle? = null
|
|
31
|
+
var parserId: Int = 0
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Records what the markdown spans currently on a spannable were built from.
|
|
35
|
+
*
|
|
36
|
+
* Held as a span rather than a field so that it is invalidated by exactly the things that
|
|
37
|
+
* invalidate the formatting itself. Anything that replaces the spannable's content drops
|
|
38
|
+
* this along with the markdown spans, and the next pass rebuilds both. A field would
|
|
39
|
+
* survive that and we would skip a pass that was actually needed.
|
|
40
|
+
*/
|
|
41
|
+
private class Fingerprint(val text: String, val parserId: Int, val style: MarkdownStyle)
|
|
42
|
+
|
|
43
|
+
fun applyMarkdownFormatting(ssb: SpannableStringBuilder) {
|
|
44
|
+
try {
|
|
45
|
+
Trace.beginSection("applyMarkdownFormatting")
|
|
46
|
+
val style = checkNotNull(markdownStyle) { "[react-native-marcus] markdownStyle is null" }
|
|
47
|
+
|
|
48
|
+
// A single keystroke drives four passes: two from the edit itself and two from React
|
|
49
|
+
// Native's state round-trip. They all ask for the same text, so only the first has any
|
|
50
|
+
// work to do.
|
|
51
|
+
if (isUpToDate(ssb, style)) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
val text = ssb.toString()
|
|
56
|
+
val markdownRanges = MarkdownParser.shared.parse(text, parserId, onSchemaError)
|
|
57
|
+
markdownFormatter.format(ssb, markdownRanges, style, textPaint)
|
|
58
|
+
stamp(ssb, text, style)
|
|
59
|
+
} finally {
|
|
60
|
+
Trace.endSection()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun isUpToDate(ssb: SpannableStringBuilder, style: MarkdownStyle): Boolean {
|
|
65
|
+
val fingerprint =
|
|
66
|
+
ssb.getSpans(0, ssb.length, Fingerprint::class.java).singleOrNull() ?: return false
|
|
67
|
+
return fingerprint.parserId == parserId &&
|
|
68
|
+
// Identity, not equality: MarkdownStyle is rebuilt whenever the prop arrives, so a
|
|
69
|
+
// different instance means the style may have changed and we re-apply to be safe.
|
|
70
|
+
fingerprint.style === style &&
|
|
71
|
+
// Compares the CharSequence in place. ssb.toString() would copy the whole document on
|
|
72
|
+
// a path whose entire purpose is to do no work.
|
|
73
|
+
TextUtils.equals(ssb, fingerprint.text)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun stamp(ssb: SpannableStringBuilder, text: String, style: MarkdownStyle) {
|
|
77
|
+
// Fingerprint is not a MarkdownSpan, so MarkdownFormatter.removeSpans leaves it alone
|
|
78
|
+
// and stale ones have to be cleared here or they would accumulate.
|
|
79
|
+
for (stale in ssb.getSpans(0, ssb.length, Fingerprint::class.java)) {
|
|
80
|
+
ssb.removeSpan(stale)
|
|
81
|
+
}
|
|
82
|
+
ssb.setSpan(Fingerprint(text, parserId, style), 0, ssb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package app.getbullet.marcus;
|
|
2
|
+
|
|
3
|
+
import android.text.Spanned;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React Native's {@code CustomLineHeightSpan} is declared {@code internal} in Kotlin. It stays
|
|
9
|
+
* public in the bytecode, so Java can still reach it, but the Kotlin compiler refuses to. This
|
|
10
|
+
* shim keeps that one lookup in Java.
|
|
11
|
+
*
|
|
12
|
+
* <p>The alternatives were worse: reflection breaks silently under R8, and deriving the height by
|
|
13
|
+
* probing the public {@code LineHeightSpan.chooseHeight} couples us to React Native's internal
|
|
14
|
+
* metrics maths, which would mis-space h1 lines without failing. If React Native ever renames or
|
|
15
|
+
* removes the class, this file stops compiling, which is the failure mode we want.
|
|
16
|
+
*/
|
|
17
|
+
public final class ReactLineHeight {
|
|
18
|
+
private ReactLineHeight() {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The line height React Native applied to {@code spanned}, or -1 if it applied none.
|
|
23
|
+
*/
|
|
24
|
+
public static int find(Spanned spanned) {
|
|
25
|
+
CustomLineHeightSpan[] spans =
|
|
26
|
+
spanned.getSpans(0, spanned.length(), CustomLineHeightSpan.class);
|
|
27
|
+
return spans.length >= 1 ? spans[0].getLineHeight() : -1;
|
|
28
|
+
}
|
|
29
|
+
}
|