react-native-marcus 0.1.0 → 0.2.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/README.md +220 -63
- package/RNMarcus.podspec +6 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
- package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
- package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
- package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
- package/android/src/main/new_arch/CMakeLists.txt +17 -0
- package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
- package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
- package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
- package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
- package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
- package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
- package/android/src/test/resources/render-model.txt +221 -16
- package/apple/MarcusSwizzle.h +25 -0
- package/apple/MarcusTextDecoratorComponentView.h +22 -0
- package/apple/MarcusTextDecoratorComponentView.mm +72 -0
- package/apple/MarcusTextDecoratorShadowNode.h +62 -0
- package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
- package/apple/MarcusTextDecoratorViewManager.h +4 -0
- package/apple/MarcusTextDecoratorViewManager.mm +11 -0
- package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
- package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
- package/apple/MarcusTextLayoutManager.h +40 -0
- package/apple/MarcusTextLayoutManager.mm +173 -0
- package/apple/MarkdownAttributes.h +23 -2
- package/apple/MarkdownAttributes.m +6 -0
- package/apple/MarkdownCopy.swift +63 -0
- package/apple/MarkdownDecorator.swift +2 -2
- package/apple/MarkdownFormatter+React.swift +23 -0
- package/apple/MarkdownFormatter.swift +869 -148
- package/apple/MarkdownParagraphLayoutManager.swift +455 -0
- package/apple/MarkdownSwiftInterop.h +0 -1
- package/apple/MarkdownTextBackground.swift +59 -1
- package/apple/MarkdownTextFieldObserver.swift +2 -2
- package/apple/MarkdownTextLayoutFragment.swift +159 -33
- package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
- package/apple/MarkdownTextStorageDelegate.swift +2 -2
- package/apple/MarkdownTextViewObserver.swift +2 -2
- package/apple/MarkdownUtils.swift +38 -0
- package/apple/MarkdownWritingDirection.swift +132 -0
- package/apple/RCTMarcusStyle+Codegen.h +115 -9
- package/apple/RCTMarcusStyle.h +128 -31
- package/apple/RCTMarcusStyle.m +48 -0
- package/apple/RCTParagraphComponentView+Marcus.h +17 -0
- package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
- package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
- package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/lib/commonjs/MarkdownText.js +330 -0
- package/lib/commonjs/MarkdownText.js.map +1 -0
- package/lib/commonjs/MarkdownText.web.js +529 -0
- package/lib/commonjs/MarkdownText.web.js.map +1 -0
- package/lib/commonjs/MarkdownTextInput.js +22 -27
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +12 -7
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/atomUtils.js +112 -0
- package/lib/commonjs/atomUtils.js.map +1 -0
- package/lib/commonjs/childrenUtils.js +36 -0
- package/lib/commonjs/childrenUtils.js.map +1 -0
- package/lib/commonjs/index.js +8 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/math/environment.js +69 -0
- package/lib/commonjs/math/environment.js.map +1 -0
- package/lib/commonjs/math/index.js +61 -0
- package/lib/commonjs/math/index.js.map +1 -0
- package/lib/commonjs/math/mathjax.js +548 -0
- package/lib/commonjs/math/mathjax.js.map +1 -0
- package/lib/commonjs/metro.js +172 -0
- package/lib/commonjs/metro.js.map +1 -0
- package/lib/commonjs/parser/index.d.ts +3 -0
- package/lib/commonjs/parser/index.js +450 -42
- package/lib/commonjs/parser/index.js.map +4 -4
- package/lib/commonjs/stripUtils.js +743 -0
- package/lib/commonjs/stripUtils.js.map +1 -0
- package/lib/commonjs/styleUtils.js +194 -23
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/MarkdownElements.css +67 -0
- package/lib/commonjs/web/MarkdownTextInput.css +9 -7
- package/lib/commonjs/web/utils/blockLayout.js +95 -7
- package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
- package/lib/commonjs/web/utils/blockUtils.js +327 -54
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
- package/lib/commonjs/web/utils/elementUtils.js +80 -0
- package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +84 -28
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
- package/lib/commonjs/web/utils/treeUtils.js +6 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
- package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
- package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/lib/module/MarkdownText.js +325 -0
- package/lib/module/MarkdownText.js.map +1 -0
- package/lib/module/MarkdownText.web.js +524 -0
- package/lib/module/MarkdownText.web.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +24 -29
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +12 -7
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/atomUtils.js +106 -0
- package/lib/module/atomUtils.js.map +1 -0
- package/lib/module/childrenUtils.js +33 -0
- package/lib/module/childrenUtils.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/math/environment.js +67 -0
- package/lib/module/math/environment.js.map +1 -0
- package/lib/module/math/index.js +56 -0
- package/lib/module/math/index.js.map +1 -0
- package/lib/module/math/mathjax.js +541 -0
- package/lib/module/math/mathjax.js.map +1 -0
- package/lib/module/metro.js +169 -0
- package/lib/module/metro.js.map +1 -0
- package/lib/module/parser/index.d.ts +3 -0
- package/lib/module/parser/index.js +450 -42
- package/lib/module/parser/index.js.map +4 -4
- package/lib/module/stripUtils.js +739 -0
- package/lib/module/stripUtils.js.map +1 -0
- package/lib/module/styleUtils.js +196 -25
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/MarkdownElements.css +67 -0
- package/lib/module/web/MarkdownTextInput.css +9 -7
- package/lib/module/web/utils/blockLayout.js +94 -8
- package/lib/module/web/utils/blockLayout.js.map +1 -1
- package/lib/module/web/utils/blockUtils.js +328 -56
- package/lib/module/web/utils/blockUtils.js.map +1 -1
- package/lib/module/web/utils/elementUtils.js +75 -0
- package/lib/module/web/utils/elementUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +86 -30
- package/lib/module/web/utils/parserUtils.js.map +1 -1
- package/lib/module/web/utils/treeUtils.js +6 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -1
- package/lib/module/web/utils/webStyleUtils.js +25 -5
- package/lib/module/web/utils/webStyleUtils.js.map +1 -1
- package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
- package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
- package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
- package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
- package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
- package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +2 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
- package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
- package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
- package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/metro.d.ts +59 -0
- package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
- package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
- package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
- package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
- package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
- package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
- package/lib/typescript/module/e2e/links.test.d.ts +2 -0
- package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
- package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
- package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
- package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
- package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
- package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/module/src/atomUtils.d.ts +58 -0
- package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
- package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/commonTypes.d.ts +39 -3
- package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +2 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/math/environment.d.ts +7 -0
- package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
- package/lib/typescript/module/src/math/index.d.ts +64 -0
- package/lib/typescript/module/src/math/index.d.ts.map +1 -0
- package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
- package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
- package/lib/typescript/module/src/metro.d.ts +59 -0
- package/lib/typescript/module/src/metro.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/index.d.ts +3 -3
- package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
- package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
- package/lib/typescript/module/src/stripUtils.d.ts +108 -0
- package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/styleUtils.d.ts +62 -4
- package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
- package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
- package/package.json +29 -2
- package/react-native.config.js +8 -1
- package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/src/MarkdownText.tsx +472 -0
- package/src/MarkdownText.web.tsx +668 -0
- package/src/MarkdownTextInput.tsx +42 -38
- package/src/MarkdownTextInput.web.tsx +36 -13
- package/src/atomUtils.ts +112 -0
- package/src/childrenUtils.ts +39 -0
- package/src/commonTypes.ts +69 -9
- package/src/index.tsx +2 -1
- package/src/math/environment.ts +68 -0
- package/src/math/index.ts +78 -0
- package/src/math/mathjax.ts +604 -0
- package/src/metro.ts +238 -0
- package/src/parser/index.ts +175 -10
- package/src/parser/micromark-extension-mentions.ts +104 -11
- package/src/stripUtils.ts +919 -0
- package/src/styleUtils.ts +290 -26
- package/src/web/MarkdownElements.css +67 -0
- package/src/web/MarkdownTextInput.css +9 -7
- package/src/web/utils/blockLayout.ts +124 -14
- package/src/web/utils/blockUtils.ts +365 -59
- package/src/web/utils/elementUtils.ts +87 -0
- package/src/web/utils/parserUtils.ts +119 -31
- package/src/web/utils/treeUtils.ts +6 -1
- package/src/web/utils/webStyleUtils.ts +32 -8
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
- package/apple/RCTMarcusStyle+Codegen.mm +0 -64
- package/apple/RCTMarcusUtils.h +0 -26
- package/apple/RCTMarcusUtils.mm +0 -26
package/README.md
CHANGED
|
@@ -11,9 +11,10 @@ Marcus takes a different approach to editing markdown: formatting is rendered li
|
|
|
11
11
|
- ⚛️ Drop-in replacement for `<TextInput>` component
|
|
12
12
|
- ⌨️ Live synchronous formatting on every keystroke
|
|
13
13
|
- ⚡ Fully native experience (selection, spellcheck, autocomplete)
|
|
14
|
-
- 🔧 Customizable
|
|
14
|
+
- 🔧 Customizable markdown flavor
|
|
15
15
|
- 🎨 Customizable styles
|
|
16
16
|
- 🌐 Universal support (Android, iOS, web)
|
|
17
|
+
- ➗ Optional LaTeX math, typeset by MathJax and drawn identically on all three
|
|
17
18
|
- 🏗️ Supports only the New Architecture
|
|
18
19
|
|
|
19
20
|
## Differences from Expensify/react-native-live-markdown
|
|
@@ -55,12 +56,12 @@ The library includes native code so you will need to re-build the native app.
|
|
|
55
56
|
|
|
56
57
|
```tsx
|
|
57
58
|
import { useState } from "react"
|
|
58
|
-
import { MarkdownTextInput
|
|
59
|
+
import { MarkdownTextInput } from "react-native-marcus"
|
|
59
60
|
|
|
60
61
|
export default function App() {
|
|
61
62
|
const [text, setText] = useState("Hello, **world**!")
|
|
62
63
|
|
|
63
|
-
return <MarkdownTextInput value={text} onChangeText={setText}
|
|
64
|
+
return <MarkdownTextInput value={text} onChangeText={setText} />
|
|
64
65
|
}
|
|
65
66
|
```
|
|
66
67
|
|
|
@@ -68,7 +69,11 @@ export default function App() {
|
|
|
68
69
|
|
|
69
70
|
`MarkdownTextInput` can be styled using `style` prop just like regular `TextInput` component.
|
|
70
71
|
|
|
71
|
-
It is also possible to customize the styling of the formatted contents of `MarkdownTextInput` component. The style object supports all color representations from React Native including `PlatformColor` and `DynamicColorIOS` according to the [color reference](https://reactnative.dev/docs/colors).
|
|
72
|
+
It is also possible to customize the styling of the formatted contents of `MarkdownTextInput` component. The style object supports all color representations from React Native including `PlatformColor` and `DynamicColorIOS` according to the [color reference](https://reactnative.dev/docs/colors).
|
|
73
|
+
|
|
74
|
+
Every styled entry takes the same set of text properties, picked from React Native's own `TextStyle`: `color`, `fontFamily`, `fontSize`, `fontStyle`, `fontWeight`, `letterSpacing`, `lineHeight`, `textDecorationLine`, `textDecorationColor`, `textDecorationStyle`, `textShadowColor`, `textShadowOffset` and `textShadowRadius`. Anything left out is inherited, so an entry naming only a colour keeps the face, the size and the weight of the prose around it.
|
|
75
|
+
|
|
76
|
+
Two of those are not honoured everywhere. `textDecorationColor` and `textDecorationStyle` are applied on iOS and the web; Android draws an underline and a strikethrough in the text's own colour as a solid line and offers no way to say otherwise. `lineHeight` is a property of a line rather than of a run on every platform, so a range that asks for one raises the whole line it sits on.
|
|
72
77
|
|
|
73
78
|
```tsx
|
|
74
79
|
import type { MarkdownStyle } from "react-native-marcus"
|
|
@@ -90,39 +95,105 @@ const markdownStyle: MarkdownStyle = {
|
|
|
90
95
|
},
|
|
91
96
|
link: {
|
|
92
97
|
color: "blue",
|
|
98
|
+
textDecorationLine: "underline",
|
|
99
|
+
},
|
|
100
|
+
// The marks that used to be hard-coded. Naming one replaces it rather than
|
|
101
|
+
// fighting it: `strong: { fontWeight: "600" }` is a lighter bold, not a bold
|
|
102
|
+
// with a 600 on top.
|
|
103
|
+
strong: {
|
|
104
|
+
fontWeight: "bold",
|
|
105
|
+
},
|
|
106
|
+
emphasis: {
|
|
107
|
+
fontStyle: "italic",
|
|
108
|
+
},
|
|
109
|
+
strikethrough: {
|
|
110
|
+
textDecorationLine: "line-through",
|
|
93
111
|
},
|
|
94
|
-
|
|
112
|
+
heading: {
|
|
113
|
+
// Level N is this size scaled N-1 times, so one pair of values covers all six.
|
|
95
114
|
fontSize: 25,
|
|
115
|
+
scale: 0.85,
|
|
116
|
+
fontWeight: "bold",
|
|
96
117
|
},
|
|
97
118
|
emoji: {
|
|
98
119
|
fontSize: 20,
|
|
99
120
|
fontFamily: FONT_FAMILY_EMOJI,
|
|
100
121
|
},
|
|
122
|
+
// The bar beside a quoted block, the indent it opens, and the text inside it.
|
|
123
|
+
// `marginStart` and `paddingStart` rather than left and right: the gutter
|
|
124
|
+
// opens on the line's leading edge, which is the right-hand side of a
|
|
125
|
+
// right-to-left paragraph.
|
|
101
126
|
blockquote: {
|
|
102
127
|
borderColor: "gray",
|
|
103
128
|
borderWidth: 6,
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
// Rounds the bar's ends. A radius of half the width gives a capsule.
|
|
130
|
+
borderRadius: 0,
|
|
131
|
+
marginStart: 6,
|
|
132
|
+
paddingStart: 6,
|
|
133
|
+
fontStyle: "italic",
|
|
134
|
+
},
|
|
135
|
+
// A list's indent, and the marker `MarkdownText` draws in the gutter it opens.
|
|
136
|
+
// Both markers are sized from the base font -- whatever the wrapped `Text`
|
|
137
|
+
// renders at. `marker` styles the marker itself, and what it leaves unset
|
|
138
|
+
// comes from `syntax`. A `MarkdownTextInput` shows the marker you typed
|
|
139
|
+
// instead, in the base font: there it is text being edited.
|
|
140
|
+
orderedList: {
|
|
141
|
+
marginStart: 6,
|
|
142
|
+
paddingStart: 18,
|
|
143
|
+
// The item's number, at this fraction of the base font size.
|
|
144
|
+
markerScale: 0.8,
|
|
145
|
+
// Room held open either side of the marker.
|
|
146
|
+
markerPadding: 2,
|
|
147
|
+
marker: { color: "gray" },
|
|
148
|
+
},
|
|
149
|
+
unorderedList: {
|
|
150
|
+
marginStart: 6,
|
|
151
|
+
paddingStart: 18,
|
|
152
|
+
// The bullet's diameter, as a fraction of the base font size. A circle,
|
|
153
|
+
// centred on the line rather than sat on the baseline.
|
|
154
|
+
markerScale: 0.3,
|
|
155
|
+
markerPadding: 2,
|
|
156
|
+
marker: { color: "gray" },
|
|
106
157
|
},
|
|
107
158
|
code: {
|
|
108
159
|
fontFamily: FONT_FAMILY_MONOSPACE,
|
|
109
160
|
fontSize: 20,
|
|
110
161
|
color: "black",
|
|
111
162
|
backgroundColor: "lightgray",
|
|
163
|
+
// An inline run is drawn as a box too, so it takes the same lengths. Its
|
|
164
|
+
// padding and margin hold space open on either side of the run and grow into
|
|
165
|
+
// the line's spacing above and below it.
|
|
166
|
+
borderRadius: 4,
|
|
167
|
+
borderWidth: 0,
|
|
168
|
+
borderColor: "gray",
|
|
169
|
+
padding: 2,
|
|
170
|
+
margin: 2,
|
|
112
171
|
},
|
|
113
172
|
pre: {
|
|
114
173
|
fontFamily: FONT_FAMILY_MONOSPACE,
|
|
115
174
|
fontSize: 20,
|
|
116
175
|
color: "black",
|
|
117
176
|
backgroundColor: "lightgray",
|
|
177
|
+
// A block is drawn as a box behind the whole of it: the padding is the space
|
|
178
|
+
// inside the box, the margin the space around it. `padding` and `margin`
|
|
179
|
+
// cover both axes; `paddingVertical` and the rest win over them where they
|
|
180
|
+
// are set, which is what a block usually wants -- more room above and below
|
|
181
|
+
// than beside it.
|
|
182
|
+
borderRadius: 4,
|
|
183
|
+
borderWidth: 1,
|
|
184
|
+
borderColor: "gray",
|
|
185
|
+
paddingHorizontal: 8,
|
|
186
|
+
paddingVertical: 12,
|
|
187
|
+
margin: 4,
|
|
118
188
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
mentionUser: {
|
|
189
|
+
// A name with an `@` in front of it, drawn as a pill: the same inline box a
|
|
190
|
+
// run of code sits in, and so the same lengths.
|
|
191
|
+
mention: {
|
|
124
192
|
color: "blue",
|
|
125
193
|
backgroundColor: "cyan",
|
|
194
|
+
borderRadius: 5,
|
|
195
|
+
padding: 2,
|
|
196
|
+
margin: 0,
|
|
126
197
|
},
|
|
127
198
|
}
|
|
128
199
|
```
|
|
@@ -141,70 +212,156 @@ The style object can be passed to multiple `MarkdownTextInput` components using
|
|
|
141
212
|
> [!TIP]
|
|
142
213
|
> We recommend to store the style object outside of a component body or memoize the style object with `React.useMemo`.
|
|
143
214
|
|
|
144
|
-
##
|
|
215
|
+
## Markdown flavors support
|
|
216
|
+
|
|
217
|
+
`react-native-marcus` implements the [CommonMark](https://spec.commonmark.org/0.31.2/) spec, and on top of it three extensions you can ask for: two from [GFM (Github Flavored Markdown)](https://github.github.com/gfm/) — strikethrough (`~~text~~`) and bare autolinks (`www.example.com`, which CommonMark leaves as text) — and mentions, which no flavour of markdown has a notion of. [Math](./README.md#math) is a fourth, and has a section to itself.
|
|
145
218
|
|
|
146
|
-
|
|
219
|
+
Emoji are recognised too, but they are not an extension: they are a property of the characters rather than a way of writing something, so nothing turns them on or off.
|
|
147
220
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
221
|
+
A mention is an `@` followed by a name. The name starts with a letter and is made of letters, digits and marks, with `-`, `_` and another `@` joining two parts of it -- each needs a part on either side, so `@user@example.com` is a single mention while the `@` in `@user@ ` is left out of it. A `.` may appear anywhere, the last character included, so `@bullet.` is one mention; only the first of a run of dots belongs to the name, so `@bullet..` is `@bullet.` followed by a full stop.
|
|
222
|
+
|
|
223
|
+
A mention cannot begin inside a word, or straight after another `@`: that is what keeps `user@example.com` an email address rather than an address with a mention buried in it. Anything else may sit against one -- `**@someone**`, `_@someone_`, `[@user](/u)` and `(@user)` all hold a mention, since the markup is gone by the time a display draws it and punctuation was never part of a name.
|
|
224
|
+
|
|
225
|
+
Every extension is off unless it is asked for, on `MarkdownTextInput` and `MarkdownText` alike:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
<MarkdownTextInput value={text} onChangeText={setText} gfm mention />
|
|
154
229
|
```
|
|
155
230
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| "code"
|
|
167
|
-
| "pre"
|
|
168
|
-
| "blockquote"
|
|
169
|
-
| "h1"
|
|
170
|
-
| "syntax"
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Parser needs to be marked as a [worklet](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#worklet) because it's executed on the UI thread as the user types.
|
|
174
|
-
|
|
175
|
-
Here's a sample function that parses all substrings located between two asterisks as italic text:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
function parser(input: string) {
|
|
179
|
-
"worklet"
|
|
180
|
-
|
|
181
|
-
const ranges = []
|
|
182
|
-
const regexp = /\*(.*?)\*/g
|
|
183
|
-
let match
|
|
184
|
-
while ((match = regexp.exec(input)) !== null) {
|
|
185
|
-
ranges.push({ start: match.index, length: 1, type: "syntax" })
|
|
186
|
-
ranges.push({ start: match.index + 1, length: match[1]!.length, type: "emphasis" })
|
|
187
|
-
ranges.push({ start: match.index + 1 + match[1]!.length, length: 1, type: "syntax" })
|
|
188
|
-
}
|
|
189
|
-
return ranges
|
|
190
|
-
}
|
|
231
|
+
Without them what is left is CommonMark: an `@name` is a word like any other and `~~text~~` keeps its tildes. That is the default because each extension changes what an ordinary character means, and a document written somewhere else — by someone who never heard of this library — is the worst possible place to guess at that. Emoji are a property of the characters rather than an extension, so they are always recognised.
|
|
232
|
+
|
|
233
|
+
Each combination is parsed by one parser that is built once and shared by every component asking for the same one, so a flavour costs nothing to choose.
|
|
234
|
+
|
|
235
|
+
## Math
|
|
236
|
+
|
|
237
|
+
LaTeX between double dollar signs — `$$x^2$$` inline, and the same on lines of its own for a block — is recognised with the `math` prop.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
<MarkdownTextInput value={text} onChangeText={setText} math />
|
|
191
241
|
```
|
|
192
242
|
|
|
193
|
-
|
|
194
|
-
> We recommend to store the parser function outside of a component body or memoize the parser function with `React.useMemo`.
|
|
243
|
+
**Two dollars, never one.** A single `$` is a currency sign far more often than it is a delimiter: with the single form recognised, `$5 and $6` is one maths span whose TeX is `5 and `, and a price list quietly becomes a picture. Two dollars cost a keystroke and can never be an accident.
|
|
195
244
|
|
|
196
|
-
|
|
245
|
+
In an input that is all `math` does. The TeX stays exactly where you typed it and the dollars are greyed as the markup they are — the caret moves through those characters and backspace deletes them, so drawing a picture over them would make both unexplainable.
|
|
246
|
+
|
|
247
|
+
`MarkdownText` draws it instead, given two things: the maths backend, which is [MathJax and is switched on in `metro.config.js`](#enabling-mathjax), and `renderSVG`, which puts the finished drawing on the screen.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import { SvgXml } from "react-native-svg"
|
|
251
|
+
|
|
252
|
+
;<MarkdownText
|
|
253
|
+
math
|
|
254
|
+
style={{ fontSize: 16, color: "#111" }}
|
|
255
|
+
renderSVG={(xml, width, height) => <SvgXml xml={xml} width={width} height={height} />}
|
|
256
|
+
>
|
|
257
|
+
{message}
|
|
258
|
+
</MarkdownText>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
That is the whole of it. `renderSVG` is handed a complete SVG document and the box it belongs in, and there is deliberately nothing else in the signature: the glyph outlines are inline, the colour is already painted in, and the baseline has been dealt with by the time it is called. It exists only because an SVG renderer is a dependency and which one you use is not this library's business.
|
|
262
|
+
|
|
263
|
+
Three things follow from that:
|
|
264
|
+
|
|
265
|
+
- **The colour comes from the `style` you gave the `MarkdownText`.** A view mounted in a `Text` inherits none of its text attributes on either platform, so the library reads `color` off the style and paints it into the SVG. A display with no `color` of its own draws maths in whatever your SVG renderer makes of `currentColor`, which is usually black.
|
|
266
|
+
- **The size comes from `fontSize` on that same style**, because maths is the one kind of attachment that has to be sized in step with the prose around it. An inherited size is resolved by the platform long after this and cannot be read here.
|
|
267
|
+
- **Returning nothing leaves the drawing out.** The span still costs its one character, so nothing shifts.
|
|
268
|
+
|
|
269
|
+
Without the backend, or without `renderSVG`, no maths is taken out of the text: `$$x^2$$` is left on the screen exactly as it was written. That beats an empty box where an equation should be, and it is the one hint you will get that a line is missing from your Metro config.
|
|
270
|
+
|
|
271
|
+
### Enabling MathJax
|
|
272
|
+
|
|
273
|
+
[MathJax](https://www.mathjax.org/) is the only JavaScript typesetter that renders TeX to a standalone SVG, which is what lets Android, iOS and web draw identical glyphs from one implementation. It is also 11 MB, and most applications will never show an equation — so nothing in this library imports it, and turning it on is two lines:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
npm install @mathjax/src speech-rule-engine react-native-svg
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
// metro.config.js
|
|
281
|
+
const { getDefaultConfig } = require("expo/metro-config")
|
|
282
|
+
const { withMath } = require("react-native-marcus/metro")
|
|
283
|
+
|
|
284
|
+
module.exports = withMath(getDefaultConfig(__dirname))
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`withMath` does two things. It points the library's maths backend — a stub that draws nothing — at the real one, which is what keeps MathJax out of every bundle that has not asked for it. And it teaches Metro the [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) MathJax addresses its own modules with: the `imports` field in its `package.json`, which Metro does not implement, so without this every internal import fails to resolve. The mapping is read out of MathJax's own manifest rather than written down here. Any `resolveRequest` already on your config is kept and chained to.
|
|
288
|
+
|
|
289
|
+
The whole font is bundled — all forty glyph ranges, about 3.5 MB gzipped — because the alternative is an expression that silently fails to draw. Everything else about running MathJax under Hermes and Metro is handled in [`src/math/mathjax.ts`](./src/math/mathjax.ts), which is worth reading if you are doing something similar: the `navigator.appVersion` it assumes exists, the synchronous font loader it needs instead of `fetch`, the inline line-breaking that has to be turned off, and the `ex` units that must not reach a browser.
|
|
290
|
+
|
|
291
|
+
`speech-rule-engine` is MathJax's own accessibility engine and is what says an equation out loud — see [Accessibility and copying](#accessibility-and-copying). It comes with MathJax as a dependency of its own, and is named on the install line so that it resolves from this package too under a strict node_modules layout. Leave it out and everything still draws; equations are just read as their TeX.
|
|
292
|
+
|
|
293
|
+
`withMath` takes the languages you want equations read out in, and bundles only those:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
module.exports = withMath(getDefaultConfig(__dirname), { locales: ["en", "fr"] })
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The engine ships `af`, `ca`, `da`, `de`, `en`, `es`, `fr`, `hi`, `it`, `ko`, `nb`, `nn` and `sv`, at around 300 KB of rules each. `["en"]` is the default, and everything not named is resolved away to nothing — an application that reads its maths in English has no business carrying the four megabytes for the twelve languages it does not. Name the ones you already translate the rest of your interface into.
|
|
300
|
+
|
|
301
|
+
## Accessibility and copying
|
|
302
|
+
|
|
303
|
+
A drawing is one character in the text the platform lays out — U+FFFC, the object replacement character — which is what a view nested in a `Text` costs on both platforms. That one character is what makes the layout work, and it is also all that a screen reader and the clipboard would otherwise get.
|
|
304
|
+
|
|
305
|
+
**Reading.** `MarkdownText` gives the paragraph an `accessibilityLabel` in which every placeholder is written back out as what it stands for: an image's `alt`, and, for maths, the equation said out loud.
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
$$\frac{a}{b}$$ → "StartFraction a Over b EndFraction"
|
|
309
|
+
$$\alpha\beta \in \mathbb{R}$$ → "alpha beta element of double struck upper R"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
That is [`speech-rule-engine`](https://github.com/zorkow/speech-rule-engine), the engine MathJax itself uses for the same job on the web, so an equation in your app is announced in the phrasing a screen reader user of maths already knows. An expression it cannot say falls back to its TeX, and so does every expression when the maths backend is not installed.
|
|
313
|
+
|
|
314
|
+
**In whichever language your reader speaks.** `mathAccessibilityLocale` says which; it is usually the same value the rest of your interface is translated into, since what a document is written in and what its reader speaks are two different questions.
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
<MarkdownText math renderSVG={renderSVG} mathAccessibilityLocale="fr">
|
|
318
|
+
{message}
|
|
319
|
+
</MarkdownText>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
$$\frac{a}{b}$$ en → "StartFraction a Over b EndFraction"
|
|
324
|
+
fr → "début fraction a sur b fin fraction"
|
|
325
|
+
de → "Anfang Bruch a durch b Ende Bruch"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Only the languages named in [`withMath(config, { locales })`](#enabling-mathjax) are in the bundle; asking for one that is not reads it in English and warns in the console saying which line would fix it. Nothing else about the drawing changes — the picture is the same in every language, and the prose around it is read in the language it was written in.
|
|
329
|
+
|
|
330
|
+
An empty `alt` is left empty, since that is how a decorative image is declared, and your own `accessibilityLabel` always wins. On the web each drawing is a `role="math"` (or `role="img"`) element with an `aria-label`, so it is read in place rather than as part of one long string.
|
|
331
|
+
|
|
332
|
+
`MarkdownTextInput` keeps the TeX, and should: there the TeX is what is on the screen, what the caret moves through and what backspace deletes, so reading anything else out would describe a document that is not there.
|
|
333
|
+
|
|
334
|
+
**Copying.** Copying a selection puts the maths back on the clipboard as the markdown it was written as — `$$x^2$$`, not a hole where an equation was — so a message copied out of a chat pastes back into one, on all three platforms.
|
|
335
|
+
|
|
336
|
+
| | how |
|
|
337
|
+
| ------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
338
|
+
| Web | the `copy` event is intercepted and the fragment the browser was about to serialise is walked |
|
|
339
|
+
| Android | the selection toolbar's copy is answered by the decorator, which rebuilds the selected range from the source |
|
|
340
|
+
| iOS | `copy:` on the paragraph is replaced with one that substitutes the placeholders before writing to the pasteboard |
|
|
341
|
+
|
|
342
|
+
The rest of a copy is what a copy of styled text has always been: plain text. `**bold**` comes off the screen as "bold". List markers are laid out rather than removed, so they come with it; a heading's `#` and a quote's `>` are drawn rather than written, so they do not. An image copies as nothing, the way an `<img>` does on the web.
|
|
197
343
|
|
|
198
|
-
|
|
344
|
+
On Android this only ever applies to a `Text` you have marked `selectable`, and only to paragraphs that have a drawing in them; everything else copies exactly as it did before.
|
|
199
345
|
|
|
200
346
|
## API reference
|
|
201
347
|
|
|
202
348
|
`MarkdownTextInput` inherits all props of React Native's `TextInput` component as well as introduces the following properties:
|
|
203
349
|
|
|
204
|
-
| Prop | Type
|
|
205
|
-
| --------------- |
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
350
|
+
| Prop | Type | Default | Note |
|
|
351
|
+
| --------------- | --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
352
|
+
| `markdownStyle` | `MarkdownStyle` | `undefined` | Adds custom styling to Markdown text. The provided value is merged with default style object. See [Styling](./README.md#styling) for more information. |
|
|
353
|
+
| `gfm` | `boolean` | `false` | Recognises GFM's strikethrough and bare autolinks. See [Markdown flavors support](./README.md#markdown-flavors-support). |
|
|
354
|
+
| `mention` | `boolean` | `false` | Recognises `@name` mentions. See [Markdown flavors support](./README.md#markdown-flavors-support). |
|
|
355
|
+
| `math` | `boolean` | `false` | Recognises `$$x^2$$` LaTeX. See [Math](./README.md#math). |
|
|
356
|
+
|
|
357
|
+
`MarkdownText` inherits all props of React Native's `Text` component, takes `markdownStyle` and the three flavour props above, and adds:
|
|
358
|
+
|
|
359
|
+
| Prop | Type | Default | Note |
|
|
360
|
+
| ------------- | --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
|
361
|
+
| `renderEmbed` | `(uri, alt, title, inline) => element?` | `undefined` | What to put where an image was. Without it, images are left as markup. |
|
|
362
|
+
| `renderSVG` | `(xml, width, height) => element?` | `undefined` | What to draw an SVG with. Without it, maths is left as it was written. See [Math](./README.md#math). |
|
|
363
|
+
| `mathAccessibilityLocale` | `string` | `"en"` | The language a drawn equation is read out in. See [Accessibility and copying](./README.md#accessibility-and-copying). |
|
|
364
|
+
| `onLinkPress` | `(uri, label, title) => void` | `undefined` | Called when a link is pressed. The URL is not opened for you. |
|
|
208
365
|
|
|
209
366
|
## Compatibility
|
|
210
367
|
|
package/RNMarcus.podspec
CHANGED
|
@@ -37,10 +37,15 @@ Pod::Spec.new do |s|
|
|
|
37
37
|
# the .mm/.cpp sources that actually need them.
|
|
38
38
|
s.private_header_files = [
|
|
39
39
|
"apple/MarcusModule.h",
|
|
40
|
+
"apple/MarcusSwizzle.h",
|
|
40
41
|
"apple/MarkdownSwiftInterop.h",
|
|
41
42
|
"apple/MarcusTextInputDecoratorComponentView.h",
|
|
42
43
|
"apple/MarcusTextInputDecoratorShadowNode.h",
|
|
44
|
+
"apple/MarcusTextDecoratorComponentView.h",
|
|
45
|
+
"apple/MarcusTextDecoratorShadowNode.h",
|
|
46
|
+
"apple/MarcusTextLayoutManager.h",
|
|
43
47
|
"apple/RCTMarcusStyle+Codegen.h",
|
|
48
|
+
"apple/RCTParagraphComponentView+Marcus.h",
|
|
44
49
|
"apple/RCTTextInputComponentView+Marcus.h",
|
|
45
50
|
"cpp/MarkdownGlobal.h",
|
|
46
51
|
"cpp/MarkdownWorkletParser.h",
|
|
@@ -72,6 +77,7 @@ Pod::Spec.new do |s|
|
|
|
72
77
|
add_dependency(s, "React-FabricComponents", :additional_framework_paths => [
|
|
73
78
|
"react/renderer/textlayoutmanager/platform/ios",
|
|
74
79
|
"react/renderer/components/textinput/platform/ios",
|
|
80
|
+
"react/renderer/components/text",
|
|
75
81
|
])
|
|
76
82
|
add_dependency(s, "React-rendererconsistency")
|
|
77
83
|
end
|
|
@@ -11,7 +11,8 @@ class MarcusPackage : BaseReactPackage() {
|
|
|
11
11
|
|
|
12
12
|
override fun createViewManagers(
|
|
13
13
|
reactContext: ReactApplicationContext
|
|
14
|
-
): List<ViewManager<in Nothing, in Nothing>> =
|
|
14
|
+
): List<ViewManager<in Nothing, in Nothing>> =
|
|
15
|
+
listOf(MarcusTextInputDecoratorViewManager(), MarcusTextDecoratorViewManager())
|
|
15
16
|
|
|
16
17
|
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
17
18
|
mapOf(
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.text.SpannableStringBuilder
|
|
7
|
+
import android.view.ActionMode
|
|
8
|
+
import android.view.Menu
|
|
9
|
+
import android.view.MenuItem
|
|
10
|
+
import android.view.ViewTreeObserver
|
|
11
|
+
import android.widget.TextView
|
|
12
|
+
import com.facebook.react.bridge.ReactContext
|
|
13
|
+
import com.facebook.react.uimanager.ReactCompoundViewGroup
|
|
14
|
+
import com.facebook.react.views.text.ReactTextView
|
|
15
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Formats the spannable of the `Text` it wraps.
|
|
19
|
+
*
|
|
20
|
+
* Android splits text in two: the measure pass runs on the background thread through the
|
|
21
|
+
* `TextLayoutManager` the shadow node was given, and the mount pass builds a second spannable on
|
|
22
|
+
* the UI thread from the same fragments. `MarcusTextDecoratorShadowNode` covers the first; this
|
|
23
|
+
* covers the second, because the callback React Native offers there
|
|
24
|
+
* (`ReactTextViewManagerCallback`) belongs to the view manager and is therefore shared by every
|
|
25
|
+
* `Text` in the app, with no way to tell one instance's markdown from another's.
|
|
26
|
+
*
|
|
27
|
+
* Formatting is driven from a pre-draw listener rather than a text watcher: attaching a
|
|
28
|
+
* `TextWatcher` to a `TextView` makes it copy its text into an `Editable`, which would throw away
|
|
29
|
+
* the spannable React Native is holding. The listener is cheap -- `MarkdownUtils` stamps what it
|
|
30
|
+
* formatted onto the spannable, so all a quiet frame costs is reading one span -- and returning
|
|
31
|
+
* false on the frame that did format keeps a partly styled frame from ever being drawn.
|
|
32
|
+
*/
|
|
33
|
+
class MarcusTextDecoratorView(context: Context) : ReactViewGroup(context), ReactCompoundViewGroup {
|
|
34
|
+
|
|
35
|
+
private var markdownUtils: MarkdownUtils? = null
|
|
36
|
+
|
|
37
|
+
private val preDrawListener = ViewTreeObserver.OnPreDrawListener { applyMarkdown() }
|
|
38
|
+
|
|
39
|
+
var markdownStyle: MarkdownStyle? = null
|
|
40
|
+
set(value) {
|
|
41
|
+
field = value
|
|
42
|
+
markdownUtils?.markdownStyle = value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var ranges: List<MarkdownRange> = emptyList()
|
|
46
|
+
set(value) {
|
|
47
|
+
field = value
|
|
48
|
+
markdownUtils?.ranges = value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What each drawing in the text copies as; empty for a paragraph with none. */
|
|
52
|
+
var substitutions: List<MarkdownSubstitution> = emptyList()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Answers the selection toolbar's copy, and only that.
|
|
56
|
+
*
|
|
57
|
+
* Everything else in the toolbar -- select all, share, the app's own items -- is left to the
|
|
58
|
+
* `TextView`, and so is copy itself when there is nothing in the text that would copy wrong.
|
|
59
|
+
* `false` from `onActionItemClicked` means "not handled", which is exactly the fall-through
|
|
60
|
+
* wanted.
|
|
61
|
+
*/
|
|
62
|
+
private val selectionActionMode =
|
|
63
|
+
object : ActionMode.Callback {
|
|
64
|
+
// Both true, and both have to be: the floating toolbar asks the callback
|
|
65
|
+
// to prepare its menu every time it is about to be shown, and takes false
|
|
66
|
+
// for "there is nothing to show" and finishes the mode. The menu itself is
|
|
67
|
+
// left exactly as the `TextView` built it.
|
|
68
|
+
override fun onCreateActionMode(mode: ActionMode, menu: Menu) = true
|
|
69
|
+
|
|
70
|
+
override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = true
|
|
71
|
+
|
|
72
|
+
override fun onDestroyActionMode(mode: ActionMode) = Unit
|
|
73
|
+
|
|
74
|
+
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
75
|
+
if (item.itemId != android.R.id.copy || substitutions.isEmpty()) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
val child = getChildAt(0) as? TextView ?: return false
|
|
80
|
+
val start = child.selectionStart
|
|
81
|
+
val end = child.selectionEnd
|
|
82
|
+
|
|
83
|
+
if (start < 0 || end <= start) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val clipboard =
|
|
88
|
+
context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return false
|
|
89
|
+
|
|
90
|
+
clipboard.setPrimaryClip(
|
|
91
|
+
ClipData.newPlainText(null, MarkdownCopy.substitute(child.text, start, end, substitutions))
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
mode.finish()
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun onAttachedToWindow() {
|
|
100
|
+
super.onAttachedToWindow()
|
|
101
|
+
|
|
102
|
+
val child = getChildAt(0)
|
|
103
|
+
if (child is ReactTextView) {
|
|
104
|
+
markdownUtils =
|
|
105
|
+
MarkdownUtils(context as ReactContext, child.paint, display = true).also {
|
|
106
|
+
it.markdownStyle = markdownStyle
|
|
107
|
+
it.ranges = ranges
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Only reached when the `Text` is selectable, which is the only way a copy can be asked for
|
|
111
|
+
// in the first place.
|
|
112
|
+
child.customSelectionActionModeCallback = selectionActionMode
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override fun onDetachedFromWindow() {
|
|
119
|
+
viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
120
|
+
markdownUtils = null
|
|
121
|
+
|
|
122
|
+
super.onDetachedFromWindow()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Answers for the `Text` below, so that a press is measured from where the text is drawn.
|
|
127
|
+
*
|
|
128
|
+
* `ReactTextView.reactTagForTouch` reads the touch straight off the layout without taking the
|
|
129
|
+
* view's padding off it first, so a `Text` with any padding at all looks up the character that
|
|
130
|
+
* many pixels down and to the right of the one under the finger. With 8dp of padding and a 19dp
|
|
131
|
+
* line that is the bottom of every line pressing the line below it, which is most of what makes
|
|
132
|
+
* a link feel like it has to be aimed at. Claiming the touch here is the only way to correct
|
|
133
|
+
* the coordinates: the child is asked first otherwise, and by then they are already wrong.
|
|
134
|
+
*/
|
|
135
|
+
override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean =
|
|
136
|
+
getChildAt(0) is ReactTextView
|
|
137
|
+
|
|
138
|
+
override fun reactTagForTouch(touchX: Float, touchY: Float): Int {
|
|
139
|
+
val child = getChildAt(0) as? ReactTextView ?: return id
|
|
140
|
+
|
|
141
|
+
// Nothing lands on the tag of a view that has no press to report, so a miss falling back to
|
|
142
|
+
// the text's own tag is the same as no target at all.
|
|
143
|
+
return child.reactTagForTouch(
|
|
144
|
+
touchX - child.left - child.totalPaddingLeft,
|
|
145
|
+
touchY - child.top - child.totalPaddingTop,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** False when the frame was formatted, so it is drawn again rather than half styled. */
|
|
150
|
+
private fun applyMarkdown(): Boolean {
|
|
151
|
+
val child = getChildAt(0) as? ReactTextView ?: return true
|
|
152
|
+
val utils = markdownUtils ?: return true
|
|
153
|
+
if (utils.markdownStyle == null) {
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The spannable React Native built for this commit. The view's own text is a copy of it --
|
|
158
|
+
// `TextView` freezes what it is given -- so the formatted result has to be set back below.
|
|
159
|
+
val spannable = child.spanned as? SpannableStringBuilder ?: return true
|
|
160
|
+
|
|
161
|
+
if (!utils.applyMarkdownFormatting(spannable)) {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
child.setText(spannable, TextView.BufferType.SPANNABLE)
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap
|
|
5
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
|
|
9
|
+
@ReactModule(name = MarcusTextDecoratorViewManager.NAME)
|
|
10
|
+
class MarcusTextDecoratorViewManager : MarcusTextDecoratorViewManagerSpec() {
|
|
11
|
+
|
|
12
|
+
override fun getName(): String = NAME
|
|
13
|
+
|
|
14
|
+
public override fun createViewInstance(context: ThemedReactContext): MarcusTextDecoratorView =
|
|
15
|
+
MarcusTextDecoratorView(context)
|
|
16
|
+
|
|
17
|
+
// The generated interface declares these parameters @Nullable, so the overrides have to accept
|
|
18
|
+
// null even though both props always arrive set.
|
|
19
|
+
@ReactProp(name = "markdownStyle")
|
|
20
|
+
override fun setMarkdownStyle(view: MarcusTextDecoratorView, value: ReadableMap?) {
|
|
21
|
+
val map = requireNotNull(value) { "[react-native-marcus] markdownStyle prop is required" }
|
|
22
|
+
view.markdownStyle = MarkdownStyle(map, view.context)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@ReactProp(name = "ranges")
|
|
26
|
+
override fun setRanges(view: MarcusTextDecoratorView, value: ReadableArray?) {
|
|
27
|
+
view.ranges = MarkdownRanges.read(value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "substitutions")
|
|
31
|
+
override fun setSubstitutions(view: MarcusTextDecoratorView, value: ReadableArray?) {
|
|
32
|
+
view.substitutions = MarkdownCopy.read(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
companion object {
|
|
36
|
+
const val NAME = "MarcusTextDecoratorView"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package app.getbullet.marcus
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
4
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
5
|
+
import com.facebook.react.viewmanagers.MarcusTextDecoratorViewManagerDelegate
|
|
6
|
+
import com.facebook.react.viewmanagers.MarcusTextDecoratorViewManagerInterface
|
|
7
|
+
|
|
8
|
+
// Not generic over the view type, for the same reason as
|
|
9
|
+
// `MarcusTextInputDecoratorViewManagerSpec`: the generated delegate's second type parameter is a
|
|
10
|
+
// Java intersection type, which Kotlin has no syntax for.
|
|
11
|
+
abstract class MarcusTextDecoratorViewManagerSpec :
|
|
12
|
+
ViewGroupManager<MarcusTextDecoratorView>(),
|
|
13
|
+
MarcusTextDecoratorViewManagerInterface<MarcusTextDecoratorView> {
|
|
14
|
+
|
|
15
|
+
private val delegate: ViewManagerDelegate<MarcusTextDecoratorView> =
|
|
16
|
+
MarcusTextDecoratorViewManagerDelegate(this)
|
|
17
|
+
|
|
18
|
+
override fun getDelegate(): ViewManagerDelegate<MarcusTextDecoratorView> = delegate
|
|
19
|
+
}
|