@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.
Files changed (105) hide show
  1. package/README.md +55 -8
  2. package/RNLiveMarkdown.podspec +2 -4
  3. package/android/build.gradle +4 -9
  4. package/android/src/main/cpp/CMakeLists.txt +14 -6
  5. package/android/src/main/cpp/MarkdownUtils.cpp +13 -24
  6. package/android/src/main/cpp/MarkdownUtils.h +2 -7
  7. package/android/src/main/cpp/OnLoad.cpp +6 -0
  8. package/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java +2 -2
  9. package/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java +4 -6
  10. package/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java +12 -0
  11. package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +17 -3
  12. package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java +6 -0
  13. package/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +21 -34
  14. package/android/src/main/new_arch/CMakeLists.txt +0 -2
  15. package/android/src/main/new_arch/MarkdownCommitHook.cpp +7 -4
  16. package/android/src/main/new_arch/MarkdownCommitHook.h +2 -0
  17. package/android/src/newarch/LiveMarkdownModuleSpec.java +9 -0
  18. package/android/src/oldarch/LiveMarkdownModuleSpec.java +12 -0
  19. package/android/src/oldarch/MarkdownTextInputDecoratorViewManagerSpec.java +1 -0
  20. package/apple/LiveMarkdownModule.h +16 -0
  21. package/apple/{RCTLiveMarkdownModule.mm → LiveMarkdownModule.mm} +31 -17
  22. package/apple/MarkdownCommitHook.mm +2 -0
  23. package/apple/MarkdownTextInputDecoratorComponentView.mm +4 -0
  24. package/apple/MarkdownTextInputDecoratorView.h +2 -0
  25. package/apple/MarkdownTextInputDecoratorView.mm +13 -0
  26. package/apple/MarkdownTextInputDecoratorViewManager.mm +9 -0
  27. package/apple/RCTMarkdownUtils.h +1 -0
  28. package/apple/RCTMarkdownUtils.mm +43 -45
  29. package/cpp/MarkdownGlobal.cpp +45 -0
  30. package/cpp/MarkdownGlobal.h +24 -0
  31. package/cpp/RuntimeDecorator.cpp +43 -0
  32. package/cpp/RuntimeDecorator.h +13 -0
  33. package/lib/commonjs/MarkdownTextInput.js +46 -5
  34. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  35. package/lib/commonjs/MarkdownTextInput.web.js +25 -17
  36. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  37. package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js +1 -1
  38. package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  39. package/lib/commonjs/NativeLiveMarkdownModule.js.map +1 -1
  40. package/lib/commonjs/commonTypes.js.map +1 -1
  41. package/lib/commonjs/index.js +8 -1
  42. package/lib/commonjs/index.js.map +1 -1
  43. package/lib/commonjs/parseExpensiMark.js +263 -0
  44. package/lib/commonjs/parseExpensiMark.js.map +1 -0
  45. package/lib/commonjs/react-native-web.d.js.map +1 -1
  46. package/lib/commonjs/styleUtils.js.map +1 -1
  47. package/lib/commonjs/web/InputHistory.js.map +1 -1
  48. package/lib/commonjs/web/inputElements/inlineImage.js.map +1 -1
  49. package/lib/commonjs/web/inputElements/loadingIndicator.js.map +1 -1
  50. package/lib/commonjs/web/utils/animationUtils.js.map +1 -1
  51. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  52. package/lib/commonjs/web/utils/browserUtils.js.map +1 -1
  53. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -1
  54. package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
  55. package/lib/commonjs/web/utils/parserUtils.js +3 -3
  56. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  57. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  58. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
  59. package/lib/module/MarkdownTextInput.js +45 -4
  60. package/lib/module/MarkdownTextInput.js.map +1 -1
  61. package/lib/module/MarkdownTextInput.web.js +23 -15
  62. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  63. package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  64. package/lib/module/NativeLiveMarkdownModule.js.map +1 -1
  65. package/lib/module/commonTypes.js.map +1 -1
  66. package/lib/module/index.js +1 -0
  67. package/lib/module/index.js.map +1 -1
  68. package/lib/module/parseExpensiMark.js +258 -0
  69. package/lib/module/parseExpensiMark.js.map +1 -0
  70. package/lib/module/react-native-web.d.js.map +1 -1
  71. package/lib/module/styleUtils.js.map +1 -1
  72. package/lib/module/web/InputHistory.js.map +1 -1
  73. package/lib/module/web/inputElements/inlineImage.js.map +1 -1
  74. package/lib/module/web/inputElements/loadingIndicator.js.map +1 -1
  75. package/lib/module/web/utils/animationUtils.js.map +1 -1
  76. package/lib/module/web/utils/blockUtils.js.map +1 -1
  77. package/lib/module/web/utils/browserUtils.js.map +1 -1
  78. package/lib/module/web/utils/cursorUtils.js.map +1 -1
  79. package/lib/module/web/utils/inputUtils.js.map +1 -1
  80. package/lib/module/web/utils/parserUtils.js +3 -3
  81. package/lib/module/web/utils/parserUtils.js.map +1 -1
  82. package/lib/module/web/utils/treeUtils.js.map +1 -1
  83. package/lib/module/web/utils/webStyleUtils.js.map +1 -1
  84. package/lib/typescript/src/MarkdownTextInput.d.ts +9 -1
  85. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  86. package/lib/typescript/src/MarkdownTextInput.web.d.ts +2 -1
  87. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  88. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +2 -2
  89. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  90. package/lib/typescript/src/index.d.ts +2 -0
  91. package/lib/typescript/src/index.d.ts.map +1 -1
  92. package/lib/typescript/src/parseExpensiMark.d.ts +4 -0
  93. package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -0
  94. package/lib/typescript/src/web/utils/parserUtils.d.ts +1 -1
  95. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
  96. package/package.json +20 -23
  97. package/src/MarkdownTextInput.tsx +62 -2
  98. package/src/MarkdownTextInput.web.tsx +28 -19
  99. package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +2 -1
  100. package/src/index.tsx +2 -0
  101. package/src/parseExpensiMark.ts +277 -0
  102. package/src/web/utils/parserUtils.ts +3 -2
  103. package/apple/RCTLiveMarkdownModule.h +0 -16
  104. package/lib/parser/react-native-live-markdown-parser.js +0 -29
  105. 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('&#32;', ' ');
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 = null, length: number, disableInlineStyles = false) {
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 = global.parseExpensiMarkToRanges(text);
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