@expensify/react-native-live-markdown 0.1.186 → 0.1.188
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 +55 -8
- package/RNLiveMarkdown.podspec +2 -4
- package/android/build.gradle +4 -9
- package/android/src/main/cpp/CMakeLists.txt +14 -6
- package/android/src/main/cpp/MarkdownUtils.cpp +13 -24
- package/android/src/main/cpp/MarkdownUtils.h +2 -7
- package/android/src/main/cpp/OnLoad.cpp +6 -0
- package/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java +2 -2
- package/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java +4 -6
- package/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java +12 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +17 -3
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java +6 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +21 -34
- package/android/src/main/new_arch/CMakeLists.txt +0 -2
- package/android/src/main/new_arch/MarkdownCommitHook.cpp +7 -4
- package/android/src/main/new_arch/MarkdownCommitHook.h +2 -0
- package/android/src/newarch/LiveMarkdownModuleSpec.java +9 -0
- package/android/src/oldarch/LiveMarkdownModuleSpec.java +12 -0
- package/android/src/oldarch/MarkdownTextInputDecoratorViewManagerSpec.java +1 -0
- package/apple/LiveMarkdownModule.h +16 -0
- package/apple/{RCTLiveMarkdownModule.mm → LiveMarkdownModule.mm} +31 -17
- package/apple/MarkdownCommitHook.mm +2 -0
- package/apple/MarkdownTextInputDecoratorComponentView.mm +4 -0
- package/apple/MarkdownTextInputDecoratorView.h +2 -0
- package/apple/MarkdownTextInputDecoratorView.mm +13 -0
- package/apple/MarkdownTextInputDecoratorViewManager.mm +9 -0
- package/apple/RCTMarkdownUtils.h +1 -0
- package/apple/RCTMarkdownUtils.mm +43 -45
- package/cpp/MarkdownGlobal.cpp +45 -0
- package/cpp/MarkdownGlobal.h +24 -0
- package/cpp/RuntimeDecorator.cpp +43 -0
- package/cpp/RuntimeDecorator.h +13 -0
- package/lib/commonjs/MarkdownTextInput.js +46 -5
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +25 -17
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js +1 -1
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/commonjs/NativeLiveMarkdownModule.js.map +1 -1
- package/lib/commonjs/commonTypes.js.map +1 -1
- package/lib/commonjs/index.js +8 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/parseExpensiMark.js +263 -0
- package/lib/commonjs/parseExpensiMark.js.map +1 -0
- package/lib/commonjs/react-native-web.d.js.map +1 -1
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/InputHistory.js.map +1 -1
- package/lib/commonjs/web/inputElements/inlineImage.js.map +1 -1
- package/lib/commonjs/web/inputElements/loadingIndicator.js.map +1 -1
- package/lib/commonjs/web/utils/animationUtils.js.map +1 -1
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
- package/lib/commonjs/web/utils/browserUtils.js.map +1 -1
- package/lib/commonjs/web/utils/cursorUtils.js.map +1 -1
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
- package/lib/commonjs/web/utils/parserUtils.js +3 -3
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
- package/lib/module/MarkdownTextInput.js +45 -4
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +23 -15
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/module/NativeLiveMarkdownModule.js.map +1 -1
- package/lib/module/commonTypes.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/parseExpensiMark.js +258 -0
- package/lib/module/parseExpensiMark.js.map +1 -0
- package/lib/module/react-native-web.d.js.map +1 -1
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/InputHistory.js.map +1 -1
- package/lib/module/web/inputElements/inlineImage.js.map +1 -1
- package/lib/module/web/inputElements/loadingIndicator.js.map +1 -1
- package/lib/module/web/utils/animationUtils.js.map +1 -1
- package/lib/module/web/utils/blockUtils.js.map +1 -1
- package/lib/module/web/utils/browserUtils.js.map +1 -1
- package/lib/module/web/utils/cursorUtils.js.map +1 -1
- package/lib/module/web/utils/inputUtils.js.map +1 -1
- package/lib/module/web/utils/parserUtils.js +3 -3
- package/lib/module/web/utils/parserUtils.js.map +1 -1
- package/lib/module/web/utils/treeUtils.js.map +1 -1
- package/lib/module/web/utils/webStyleUtils.js.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.d.ts +9 -1
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +2 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +2 -2
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/parseExpensiMark.d.ts +4 -0
- package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts +1 -1
- package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
- package/package.json +20 -23
- package/src/MarkdownTextInput.tsx +62 -2
- package/src/MarkdownTextInput.web.tsx +28 -19
- package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +2 -1
- package/src/index.tsx +2 -0
- package/src/parseExpensiMark.ts +277 -0
- package/src/web/utils/parserUtils.ts +3 -2
- package/apple/RCTLiveMarkdownModule.h +0 -16
- package/lib/parser/react-native-live-markdown-parser.js +0 -29
- package/parser/react-native-live-markdown-parser.js +0 -29
|
@@ -23,14 +23,13 @@ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponen
|
|
|
23
23
|
import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils';
|
|
24
24
|
import {parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils';
|
|
25
25
|
import {forceRefreshAllImages} from './web/inputElements/inlineImage';
|
|
26
|
-
import type {InlineImagesInputProps} from './commonTypes';
|
|
27
|
-
|
|
28
|
-
require('../parser/react-native-live-markdown-parser.js');
|
|
26
|
+
import type {MarkdownRange, InlineImagesInputProps} from './commonTypes';
|
|
29
27
|
|
|
30
28
|
const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
31
29
|
|
|
32
30
|
interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps {
|
|
33
31
|
markdownStyle?: MarkdownStyle;
|
|
32
|
+
parser: (text: string) => MarkdownRange[];
|
|
34
33
|
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
|
35
34
|
dir?: string;
|
|
36
35
|
disabled?: boolean;
|
|
@@ -85,6 +84,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
85
84
|
numberOfLines,
|
|
86
85
|
multiline = false,
|
|
87
86
|
markdownStyle,
|
|
87
|
+
parser,
|
|
88
88
|
onBlur,
|
|
89
89
|
onChange,
|
|
90
90
|
onChangeText,
|
|
@@ -111,6 +111,13 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
111
111
|
},
|
|
112
112
|
ref,
|
|
113
113
|
) => {
|
|
114
|
+
if (parser === undefined) {
|
|
115
|
+
throw new Error('[react-native-live-markdown] `parser` is undefined');
|
|
116
|
+
}
|
|
117
|
+
if (typeof parser !== 'function') {
|
|
118
|
+
throw new Error('[react-native-live-markdown] `parser` is not a function');
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
const compositionRef = useRef<boolean>(false);
|
|
115
122
|
const divRef = useRef<MarkdownTextInputElement | null>(null);
|
|
116
123
|
const currentlyFocusedField = useRef<HTMLDivElement | null>(null);
|
|
@@ -146,6 +153,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
146
153
|
|
|
147
154
|
const parseText = useCallback(
|
|
148
155
|
(
|
|
156
|
+
parserFunction: (input: string) => MarkdownRange[],
|
|
149
157
|
target: MarkdownTextInputElement,
|
|
150
158
|
text: string | null,
|
|
151
159
|
customMarkdownStyles: MarkdownStyle,
|
|
@@ -161,7 +169,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
161
169
|
if (text === null) {
|
|
162
170
|
return {text: divRef.current.value, cursorPosition: null};
|
|
163
171
|
}
|
|
164
|
-
const parsedText = updateInputStructure(target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate, shouldScrollIntoView, {
|
|
172
|
+
const parsedText = updateInputStructure(parserFunction, target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate, shouldScrollIntoView, {
|
|
165
173
|
addAuthTokenToImageURLCallback,
|
|
166
174
|
imagePreviewAuthRequiredURLs,
|
|
167
175
|
});
|
|
@@ -179,10 +187,10 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
179
187
|
const processedMarkdownStyle = useMemo(() => {
|
|
180
188
|
const newMarkdownStyle = processMarkdownStyle(markdownStyle);
|
|
181
189
|
if (divRef.current) {
|
|
182
|
-
parseText(divRef.current, divRef.current.value, newMarkdownStyle, null, false, false);
|
|
190
|
+
parseText(parser, divRef.current, divRef.current.value, newMarkdownStyle, null, false, false);
|
|
183
191
|
}
|
|
184
192
|
return newMarkdownStyle;
|
|
185
|
-
}, [markdownStyle, parseText]);
|
|
193
|
+
}, [parser, markdownStyle, parseText]);
|
|
186
194
|
|
|
187
195
|
const inputStyles = useMemo(
|
|
188
196
|
() =>
|
|
@@ -208,9 +216,9 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
208
216
|
}
|
|
209
217
|
const item = history.current.undo();
|
|
210
218
|
const undoValue = item ? item.text : null;
|
|
211
|
-
return parseText(target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
|
|
219
|
+
return parseText(parser, target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
|
|
212
220
|
},
|
|
213
|
-
[parseText, processedMarkdownStyle],
|
|
221
|
+
[parser, parseText, processedMarkdownStyle],
|
|
214
222
|
);
|
|
215
223
|
|
|
216
224
|
const redo = useCallback(
|
|
@@ -223,9 +231,9 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
223
231
|
}
|
|
224
232
|
const item = history.current.redo();
|
|
225
233
|
const redoValue = item ? item.text : null;
|
|
226
|
-
return parseText(target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
|
|
234
|
+
return parseText(parser, target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false);
|
|
227
235
|
},
|
|
228
|
-
[parseText, processedMarkdownStyle],
|
|
236
|
+
[parser, parseText, processedMarkdownStyle],
|
|
229
237
|
);
|
|
230
238
|
|
|
231
239
|
// Placeholder text color logic
|
|
@@ -275,7 +283,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
275
283
|
);
|
|
276
284
|
|
|
277
285
|
const handleOnSelect = useCallback(
|
|
278
|
-
(e) => {
|
|
286
|
+
(e: React.SyntheticEvent<HTMLDivElement>) => {
|
|
279
287
|
updateSelection(e);
|
|
280
288
|
|
|
281
289
|
// If the input has just been focused, we need to scroll the cursor into view
|
|
@@ -354,7 +362,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
354
362
|
newInputUpdate = redo(divRef.current);
|
|
355
363
|
break;
|
|
356
364
|
default:
|
|
357
|
-
newInputUpdate = parseText(divRef.current, parsedText, processedMarkdownStyle, newCursorPosition, true, !inputType, inputType === 'pasteText');
|
|
365
|
+
newInputUpdate = parseText(parser, divRef.current, parsedText, processedMarkdownStyle, newCursorPosition, true, !inputType, inputType === 'pasteText');
|
|
358
366
|
}
|
|
359
367
|
const {text, cursorPosition} = newInputUpdate;
|
|
360
368
|
updateTextColor(divRef.current, text);
|
|
@@ -406,7 +414,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
406
414
|
|
|
407
415
|
handleContentSizeChange();
|
|
408
416
|
},
|
|
409
|
-
[updateTextColor, updateSelection, onChange, onChangeText, handleContentSizeChange, undo, redo, parseText, processedMarkdownStyle, setEventProps, maxLength],
|
|
417
|
+
[parser, updateTextColor, updateSelection, onChange, onChangeText, handleContentSizeChange, undo, redo, parseText, processedMarkdownStyle, setEventProps, maxLength],
|
|
410
418
|
);
|
|
411
419
|
|
|
412
420
|
const insertText = useCallback(
|
|
@@ -576,7 +584,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
576
584
|
}, []);
|
|
577
585
|
|
|
578
586
|
const handleCut = useCallback(
|
|
579
|
-
(e) => {
|
|
587
|
+
(e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
580
588
|
if (!divRef.current || !contentSelection.current) {
|
|
581
589
|
return;
|
|
582
590
|
}
|
|
@@ -589,7 +597,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
589
597
|
);
|
|
590
598
|
|
|
591
599
|
const handlePaste = useCallback(
|
|
592
|
-
(e) => {
|
|
600
|
+
(e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
593
601
|
if (e.isDefaultPrevented() || !divRef.current || !contentSelection.current) {
|
|
594
602
|
return;
|
|
595
603
|
}
|
|
@@ -606,7 +614,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
606
614
|
}, []);
|
|
607
615
|
|
|
608
616
|
const endComposition = useCallback(
|
|
609
|
-
(e) => {
|
|
617
|
+
(e: React.CompositionEvent<HTMLDivElement>) => {
|
|
610
618
|
compositionRef.current = false;
|
|
611
619
|
handleOnChangeText(e);
|
|
612
620
|
},
|
|
@@ -654,17 +662,17 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
654
662
|
}
|
|
655
663
|
|
|
656
664
|
if (value === undefined) {
|
|
657
|
-
parseText(divRef.current, divRef.current.value, processedMarkdownStyle);
|
|
665
|
+
parseText(parser, divRef.current, divRef.current.value, processedMarkdownStyle);
|
|
658
666
|
return;
|
|
659
667
|
}
|
|
660
668
|
const normalizedValue = normalizeValue(value);
|
|
661
669
|
|
|
662
670
|
divRef.current.value = normalizedValue;
|
|
663
|
-
parseText(divRef.current, normalizedValue, processedMarkdownStyle, null, true, false, true);
|
|
671
|
+
parseText(parser, divRef.current, normalizedValue, processedMarkdownStyle, null, true, false, true);
|
|
664
672
|
|
|
665
673
|
updateTextColor(divRef.current, value);
|
|
666
674
|
},
|
|
667
|
-
[multiline, processedMarkdownStyle, value, maxLength],
|
|
675
|
+
[parser, multiline, processedMarkdownStyle, value, maxLength],
|
|
668
676
|
);
|
|
669
677
|
|
|
670
678
|
useClientEffect(
|
|
@@ -741,6 +749,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
|
|
|
741
749
|
onCopy={handleCopy}
|
|
742
750
|
onCut={handleCut}
|
|
743
751
|
onPaste={handlePaste}
|
|
752
|
+
// @ts-expect-error: we use placeholder prop to style it in CSS even though its not handled internally
|
|
744
753
|
placeholder={heightSafePlaceholder}
|
|
745
754
|
spellCheck={spellCheck}
|
|
746
755
|
dir={dir}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {ColorValue, ViewProps} from 'react-native';
|
|
2
2
|
|
|
3
|
-
import type {Float} from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
import type {Float, Int32} from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
4
|
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
5
5
|
|
|
6
6
|
interface MarkdownStyle {
|
|
@@ -74,6 +74,7 @@ interface MarkdownStyle {
|
|
|
74
74
|
|
|
75
75
|
interface NativeProps extends ViewProps {
|
|
76
76
|
markdownStyle: MarkdownStyle;
|
|
77
|
+
parserId: Int32;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export default codegenNativeComponent<NativeProps>('MarkdownTextInputDecoratorView', {
|
package/src/index.tsx
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export {default as MarkdownTextInput} from './MarkdownTextInput';
|
|
2
2
|
export type {MarkdownTextInputProps, MarkdownStyle} from './MarkdownTextInput';
|
|
3
|
+
export type {MarkdownType, MarkdownRange} from './commonTypes';
|
|
4
|
+
export {default as parseExpensiMark} from './parseExpensiMark';
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'worklet';
|
|
2
|
+
|
|
3
|
+
import {ExpensiMark} from 'expensify-common';
|
|
4
|
+
import {unescapeText} from 'expensify-common/dist/utils';
|
|
5
|
+
import type {MarkdownType, MarkdownRange} from './commonTypes';
|
|
6
|
+
|
|
7
|
+
const MAX_PARSABLE_LENGTH = 4000;
|
|
8
|
+
|
|
9
|
+
type Token = ['TEXT' | 'HTML', string];
|
|
10
|
+
type StackItem = {tag: string; children: Array<StackItem | string>};
|
|
11
|
+
|
|
12
|
+
function parseMarkdownToHTML(markdown: string): string {
|
|
13
|
+
const parser = new ExpensiMark();
|
|
14
|
+
const html = parser.replace(markdown, {
|
|
15
|
+
shouldKeepRawInput: true,
|
|
16
|
+
});
|
|
17
|
+
return html as string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseHTMLToTokens(html: string): Token[] {
|
|
21
|
+
const tokens: Token[] = [];
|
|
22
|
+
let left = 0;
|
|
23
|
+
// eslint-disable-next-line no-constant-condition
|
|
24
|
+
while (true) {
|
|
25
|
+
const open = html.indexOf('<', left);
|
|
26
|
+
if (open === -1) {
|
|
27
|
+
if (left < html.length) {
|
|
28
|
+
tokens.push(['TEXT', html.substring(left)]);
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (open !== left) {
|
|
33
|
+
tokens.push(['TEXT', html.substring(left, open)]);
|
|
34
|
+
}
|
|
35
|
+
const close = html.indexOf('>', open);
|
|
36
|
+
if (close === -1) {
|
|
37
|
+
throw new Error('[react-native-live-markdown] Error in function parseHTMLToTokens: Invalid HTML: no matching ">"');
|
|
38
|
+
}
|
|
39
|
+
tokens.push(['HTML', html.substring(open, close + 1)]);
|
|
40
|
+
left = close + 1;
|
|
41
|
+
}
|
|
42
|
+
return tokens;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTokensToTree(tokens: Token[]): StackItem {
|
|
46
|
+
const stack: StackItem[] = [{tag: '<>', children: []}];
|
|
47
|
+
tokens.forEach(([type, payload]) => {
|
|
48
|
+
if (type === 'TEXT') {
|
|
49
|
+
const text = unescapeText(payload);
|
|
50
|
+
const top = stack[stack.length - 1];
|
|
51
|
+
top!.children.push(text);
|
|
52
|
+
} else if (type === 'HTML') {
|
|
53
|
+
if (payload.startsWith('</')) {
|
|
54
|
+
// closing tag
|
|
55
|
+
const child = stack.pop();
|
|
56
|
+
const top = stack[stack.length - 1];
|
|
57
|
+
top!.children.push(child!);
|
|
58
|
+
} else if (payload.endsWith('/>')) {
|
|
59
|
+
// self-closing tag
|
|
60
|
+
const top = stack[stack.length - 1];
|
|
61
|
+
top!.children.push({tag: payload, children: []});
|
|
62
|
+
} else {
|
|
63
|
+
// opening tag
|
|
64
|
+
stack.push({tag: payload, children: []});
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[react-native-live-markdown] Error in function parseTokensToTree: Unknown token type: ${type as string}. Expected 'TEXT' or 'HTML'. Please ensure tokens only contain these types.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (stack.length !== 1) {
|
|
73
|
+
const unclosedTags =
|
|
74
|
+
stack.length > 0
|
|
75
|
+
? stack
|
|
76
|
+
.slice(1)
|
|
77
|
+
.map((item) => item.tag)
|
|
78
|
+
.join(', ')
|
|
79
|
+
: '';
|
|
80
|
+
throw new Error(
|
|
81
|
+
`[react-native-live-markdown] Invalid HTML structure: the following tags are not properly closed: ${unclosedTags}. Ensure each opening tag has a corresponding closing tag.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return stack[0]!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
|
|
88
|
+
let text = '';
|
|
89
|
+
|
|
90
|
+
function processChildren(node: StackItem | string) {
|
|
91
|
+
if (typeof node === 'string') {
|
|
92
|
+
text += node;
|
|
93
|
+
} else {
|
|
94
|
+
node.children.forEach(dfs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function appendSyntax(syntax: string) {
|
|
99
|
+
addChildrenWithStyle(syntax, 'syntax');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addChildrenWithStyle(node: StackItem | string, type: MarkdownType) {
|
|
103
|
+
const start = text.length;
|
|
104
|
+
processChildren(node);
|
|
105
|
+
const end = text.length;
|
|
106
|
+
ranges.push({type, start, length: end - start});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ranges: MarkdownRange[] = [];
|
|
110
|
+
function dfs(node: StackItem | string) {
|
|
111
|
+
if (typeof node === 'string') {
|
|
112
|
+
text += node;
|
|
113
|
+
} else {
|
|
114
|
+
// eslint-disable-next-line no-lonely-if
|
|
115
|
+
if (node.tag === '<>') {
|
|
116
|
+
processChildren(node);
|
|
117
|
+
} else if (node.tag === '<strong>') {
|
|
118
|
+
appendSyntax('*');
|
|
119
|
+
addChildrenWithStyle(node, 'bold');
|
|
120
|
+
appendSyntax('*');
|
|
121
|
+
} else if (node.tag === '<em>') {
|
|
122
|
+
appendSyntax('_');
|
|
123
|
+
addChildrenWithStyle(node, 'italic');
|
|
124
|
+
appendSyntax('_');
|
|
125
|
+
} else if (node.tag === '<del>') {
|
|
126
|
+
appendSyntax('~');
|
|
127
|
+
addChildrenWithStyle(node, 'strikethrough');
|
|
128
|
+
appendSyntax('~');
|
|
129
|
+
} else if (node.tag === '<emoji>') {
|
|
130
|
+
addChildrenWithStyle(node, 'emoji');
|
|
131
|
+
} else if (node.tag === '<code>') {
|
|
132
|
+
appendSyntax('`');
|
|
133
|
+
addChildrenWithStyle(node, 'code');
|
|
134
|
+
appendSyntax('`');
|
|
135
|
+
} else if (node.tag === '<mention-here>') {
|
|
136
|
+
addChildrenWithStyle(node, 'mention-here');
|
|
137
|
+
} else if (node.tag === '<mention-user>') {
|
|
138
|
+
addChildrenWithStyle(node, 'mention-user');
|
|
139
|
+
} else if (node.tag === '<mention-report>') {
|
|
140
|
+
addChildrenWithStyle(node, 'mention-report');
|
|
141
|
+
} else if (node.tag === '<blockquote>') {
|
|
142
|
+
appendSyntax('>');
|
|
143
|
+
addChildrenWithStyle(node, 'blockquote');
|
|
144
|
+
// compensate for "> " at the beginning
|
|
145
|
+
if (ranges.length > 0) {
|
|
146
|
+
const curr = ranges[ranges.length - 1];
|
|
147
|
+
curr!.start -= 1;
|
|
148
|
+
curr!.length += 1;
|
|
149
|
+
}
|
|
150
|
+
} else if (node.tag === '<h1>') {
|
|
151
|
+
appendSyntax('# ');
|
|
152
|
+
addChildrenWithStyle(node, 'h1');
|
|
153
|
+
} else if (node.tag === '<br />') {
|
|
154
|
+
text += '\n';
|
|
155
|
+
} else if (node.tag.startsWith('<pre')) {
|
|
156
|
+
appendSyntax('```');
|
|
157
|
+
const content = node.children.join('').replaceAll(' ', ' ');
|
|
158
|
+
addChildrenWithStyle(content, 'pre');
|
|
159
|
+
appendSyntax('```');
|
|
160
|
+
} else if (node.tag.startsWith('<a href="')) {
|
|
161
|
+
const rawHref = node.tag.match(/href="([^"]*)"/)![1]!; // always present
|
|
162
|
+
const href = unescapeText(rawHref);
|
|
163
|
+
const isLabeledLink = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
|
|
164
|
+
const dataRawHref = node.tag.match(/data-raw-href="([^"]*)"/);
|
|
165
|
+
const matchString = dataRawHref ? unescapeText(dataRawHref[1]!) : href;
|
|
166
|
+
if (!isLabeledLink && node.children.length === 1 && typeof node.children[0] === 'string' && (node.children[0] === matchString || `mailto:${node.children[0]}` === href)) {
|
|
167
|
+
addChildrenWithStyle(node.children[0], 'link');
|
|
168
|
+
} else {
|
|
169
|
+
appendSyntax('[');
|
|
170
|
+
processChildren(node);
|
|
171
|
+
appendSyntax('](');
|
|
172
|
+
addChildrenWithStyle(matchString, 'link');
|
|
173
|
+
appendSyntax(')');
|
|
174
|
+
}
|
|
175
|
+
} else if (node.tag.startsWith('<img src="')) {
|
|
176
|
+
const src = node.tag.match(/src="([^"]*)"/)![1]!; // always present
|
|
177
|
+
const alt = node.tag.match(/alt="([^"]*)"/);
|
|
178
|
+
const hasAlt = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
|
|
179
|
+
const rawLink = node.tag.match(/data-raw-href="([^"]*)"/);
|
|
180
|
+
const linkString = rawLink ? unescapeText(rawLink[1]!) : src;
|
|
181
|
+
|
|
182
|
+
const start = text.length;
|
|
183
|
+
const length = 3 + (hasAlt ? 2 + unescapeText(alt?.[1] || '').length : 0) + linkString.length;
|
|
184
|
+
ranges.push({type: 'inline-image', start, length});
|
|
185
|
+
|
|
186
|
+
appendSyntax('!');
|
|
187
|
+
if (hasAlt) {
|
|
188
|
+
appendSyntax('[');
|
|
189
|
+
processChildren(unescapeText(alt?.[1] || ''));
|
|
190
|
+
appendSyntax(']');
|
|
191
|
+
}
|
|
192
|
+
appendSyntax('(');
|
|
193
|
+
addChildrenWithStyle(linkString, 'link');
|
|
194
|
+
appendSyntax(')');
|
|
195
|
+
} else if (node.tag.startsWith('<video data-expensify-source="')) {
|
|
196
|
+
const src = node.tag.match(/data-expensify-source="([^"]*)"/)![1]!; // always present
|
|
197
|
+
const rawLink = node.tag.match(/data-raw-href="([^"]*)"/);
|
|
198
|
+
const hasAlt = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
|
|
199
|
+
const linkString = rawLink ? unescapeText(rawLink[1]!) : src;
|
|
200
|
+
appendSyntax('!');
|
|
201
|
+
if (hasAlt) {
|
|
202
|
+
appendSyntax('[');
|
|
203
|
+
node.children.forEach((child) => processChildren(child));
|
|
204
|
+
appendSyntax(']');
|
|
205
|
+
}
|
|
206
|
+
appendSyntax('(');
|
|
207
|
+
addChildrenWithStyle(linkString, 'link');
|
|
208
|
+
appendSyntax(')');
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error(`[react-native-live-markdown] Error in function parseTreeToTextAndRanges: Unknown tag '${node.tag}'. This tag is not supported in this function's logic.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
dfs(tree);
|
|
215
|
+
return [text, ranges];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first
|
|
219
|
+
function getTagPriority(tag: string) {
|
|
220
|
+
switch (tag) {
|
|
221
|
+
case 'blockquote':
|
|
222
|
+
return 2;
|
|
223
|
+
case 'h1':
|
|
224
|
+
return 1;
|
|
225
|
+
default:
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sortRanges(ranges: MarkdownRange[]) {
|
|
231
|
+
// sort ranges by start position, then by length, then by tag hierarchy
|
|
232
|
+
return ranges.sort((a, b) => a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function groupRanges(ranges: MarkdownRange[]) {
|
|
236
|
+
const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {};
|
|
237
|
+
|
|
238
|
+
return ranges.reduce((acc, range) => {
|
|
239
|
+
const start = range.start;
|
|
240
|
+
const end = range.start + range.length;
|
|
241
|
+
|
|
242
|
+
const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type];
|
|
243
|
+
const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined;
|
|
244
|
+
|
|
245
|
+
if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) {
|
|
246
|
+
// increment depth of overlapping range
|
|
247
|
+
sameStyleRange.depth = (sameStyleRange.depth || 1) + 1;
|
|
248
|
+
} else {
|
|
249
|
+
lastVisibleRangeIndex[range.type] = acc.length;
|
|
250
|
+
acc.push(range);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return acc;
|
|
254
|
+
}, [] as MarkdownRange[]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseExpensiMark(markdown: string): MarkdownRange[] {
|
|
258
|
+
if (markdown.length > MAX_PARSABLE_LENGTH) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const html = parseMarkdownToHTML(markdown);
|
|
262
|
+
const tokens = parseHTMLToTokens(html);
|
|
263
|
+
const tree = parseTokensToTree(tokens);
|
|
264
|
+
const [text, ranges] = parseTreeToTextAndRanges(tree);
|
|
265
|
+
if (text !== markdown) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`[react-native-live-markdown] Parsing error: the processed text does not match the original Markdown input. This may be caused by incorrect parsing functions or invalid input Markdown.\nProcessed input: '${JSON.stringify(
|
|
268
|
+
text,
|
|
269
|
+
)}'\nOriginal input: '${JSON.stringify(markdown)}'`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const sortedRanges = sortRanges(ranges);
|
|
273
|
+
const groupedRanges = groupRanges(sortedRanges);
|
|
274
|
+
return groupedRanges;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default parseExpensiMark;
|
|
@@ -117,7 +117,7 @@ function addTextToElement(node: TreeNode, text: string) {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function addParagraph(node: TreeNode, text: string | null
|
|
120
|
+
function addParagraph(node: TreeNode, text: string | null, length: number, disableInlineStyles = false) {
|
|
121
121
|
const p = document.createElement('p');
|
|
122
122
|
p.setAttribute('data-type', 'line');
|
|
123
123
|
if (!disableInlineStyles) {
|
|
@@ -272,6 +272,7 @@ function moveCursor(isFocused: boolean, alwaysMoveCursorToTheEnd: boolean, curso
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
function updateInputStructure(
|
|
275
|
+
parserFunction: (input: string) => MarkdownRange[],
|
|
275
276
|
target: MarkdownTextInputElement,
|
|
276
277
|
text: string,
|
|
277
278
|
cursorPositionIndex: number | null,
|
|
@@ -291,7 +292,7 @@ function updateInputStructure(
|
|
|
291
292
|
const selection = getCurrentCursorPosition(target);
|
|
292
293
|
cursorPosition = selection ? selection.start : null;
|
|
293
294
|
}
|
|
294
|
-
const markdownRanges =
|
|
295
|
+
const markdownRanges = parserFunction(text);
|
|
295
296
|
if (!text || targetElement.innerHTML === '<br>' || (targetElement && targetElement.innerHTML === '\n')) {
|
|
296
297
|
targetElement.innerHTML = '';
|
|
297
298
|
targetElement.innerText = '';
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
|
-
|
|
3
|
-
#import <RNLiveMarkdownSpec/RNLiveMarkdownSpec.h>
|
|
4
|
-
#import <React/RCTEventEmitter.h>
|
|
5
|
-
#import <React/RCTUIManager.h>
|
|
6
|
-
|
|
7
|
-
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
-
|
|
9
|
-
// Without inheriting after RCTEventEmitter we don't get access to bridge
|
|
10
|
-
@interface RCTLiveMarkdownModule
|
|
11
|
-
: RCTEventEmitter <NativeLiveMarkdownModuleSpec>
|
|
12
|
-
@end
|
|
13
|
-
|
|
14
|
-
NS_ASSUME_NONNULL_END
|
|
15
|
-
|
|
16
|
-
#endif // RCT_NEW_ARCH_ENABLED
|