@expensify/react-native-live-markdown 0.1.116 → 0.1.117

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 (90) hide show
  1. package/lib/commonjs/MarkdownTextInput.js +2 -4
  2. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  3. package/lib/commonjs/MarkdownTextInput.web.js +105 -149
  4. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  5. package/lib/commonjs/styleUtils.js.map +1 -1
  6. package/lib/commonjs/web/utils/blockUtils.js +77 -0
  7. package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
  8. package/lib/commonjs/web/utils/browserUtils.js +18 -0
  9. package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
  10. package/lib/commonjs/web/utils/cursorUtils.js +111 -0
  11. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
  12. package/lib/commonjs/web/utils/inputUtils.js +108 -0
  13. package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
  14. package/lib/commonjs/web/utils/parserUtils.js +266 -0
  15. package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
  16. package/lib/commonjs/web/utils/treeUtils.js +85 -0
  17. package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
  18. package/lib/commonjs/web/utils/webStyleUtils.js +51 -0
  19. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
  20. package/lib/module/MarkdownTextInput.js +2 -2
  21. package/lib/module/MarkdownTextInput.js.map +1 -1
  22. package/lib/module/MarkdownTextInput.web.js +101 -145
  23. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  24. package/lib/module/styleUtils.js.map +1 -1
  25. package/lib/module/web/utils/blockUtils.js +72 -0
  26. package/lib/module/web/utils/blockUtils.js.map +1 -0
  27. package/lib/module/web/utils/browserUtils.js +12 -0
  28. package/lib/module/web/utils/browserUtils.js.map +1 -0
  29. package/lib/module/web/utils/cursorUtils.js +102 -0
  30. package/lib/module/web/utils/cursorUtils.js.map +1 -0
  31. package/lib/module/web/utils/inputUtils.js +99 -0
  32. package/lib/module/web/utils/inputUtils.js.map +1 -0
  33. package/lib/module/web/utils/parserUtils.js +260 -0
  34. package/lib/module/web/utils/parserUtils.js.map +1 -0
  35. package/lib/module/web/utils/treeUtils.js +78 -0
  36. package/lib/module/web/utils/treeUtils.js.map +1 -0
  37. package/lib/module/web/utils/webStyleUtils.js +45 -0
  38. package/lib/module/web/utils/webStyleUtils.js.map +1 -0
  39. package/lib/typescript/src/MarkdownTextInput.d.ts +1 -2
  40. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  41. package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -3
  42. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  43. package/lib/typescript/src/styleUtils.d.ts +1 -2
  44. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  45. package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -0
  46. package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -0
  47. package/lib/typescript/src/web/utils/browserUtils.d.ts +12 -0
  48. package/lib/typescript/src/web/utils/browserUtils.d.ts.map +1 -0
  49. package/lib/typescript/src/web/utils/cursorUtils.d.ts +12 -0
  50. package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -0
  51. package/lib/typescript/src/web/utils/inputUtils.d.ts +9 -0
  52. package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -0
  53. package/lib/typescript/src/web/utils/parserUtils.d.ts +22 -0
  54. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -0
  55. package/lib/typescript/src/web/utils/treeUtils.d.ts +17 -0
  56. package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -0
  57. package/lib/typescript/src/web/utils/webStyleUtils.d.ts +6 -0
  58. package/lib/typescript/src/web/utils/webStyleUtils.d.ts.map +1 -0
  59. package/package.json +1 -1
  60. package/src/MarkdownTextInput.tsx +4 -7
  61. package/src/MarkdownTextInput.web.tsx +142 -171
  62. package/src/styleUtils.ts +1 -3
  63. package/src/web/utils/blockUtils.ts +72 -0
  64. package/src/web/utils/browserUtils.ts +13 -0
  65. package/src/web/utils/cursorUtils.ts +116 -0
  66. package/src/web/utils/inputUtils.ts +106 -0
  67. package/src/web/utils/parserUtils.ts +302 -0
  68. package/src/web/utils/treeUtils.ts +103 -0
  69. package/src/web/utils/webStyleUtils.ts +54 -0
  70. package/lib/commonjs/web/browserUtils.js +0 -16
  71. package/lib/commonjs/web/browserUtils.js.map +0 -1
  72. package/lib/commonjs/web/cursorUtils.js +0 -167
  73. package/lib/commonjs/web/cursorUtils.js.map +0 -1
  74. package/lib/commonjs/web/parserUtils.js +0 -214
  75. package/lib/commonjs/web/parserUtils.js.map +0 -1
  76. package/lib/module/web/browserUtils.js +0 -11
  77. package/lib/module/web/browserUtils.js.map +0 -1
  78. package/lib/module/web/cursorUtils.js +0 -154
  79. package/lib/module/web/cursorUtils.js.map +0 -1
  80. package/lib/module/web/parserUtils.js +0 -206
  81. package/lib/module/web/parserUtils.js.map +0 -1
  82. package/lib/typescript/src/web/browserUtils.d.ts +0 -10
  83. package/lib/typescript/src/web/browserUtils.d.ts.map +0 -1
  84. package/lib/typescript/src/web/cursorUtils.d.ts +0 -12
  85. package/lib/typescript/src/web/cursorUtils.d.ts.map +0 -1
  86. package/lib/typescript/src/web/parserUtils.d.ts +0 -17
  87. package/lib/typescript/src/web/parserUtils.d.ts.map +0 -1
  88. package/src/web/browserUtils.ts +0 -11
  89. package/src/web/cursorUtils.ts +0 -177
  90. package/src/web/parserUtils.ts +0 -228
@@ -0,0 +1,116 @@
1
+ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+ import {findHTMLElementInTree, getTreeNodeByIndex} from './treeUtils';
3
+ import type {TreeNode} from './treeUtils';
4
+
5
+ function setCursorPosition(target: MarkdownTextInputElement, startIndex: number, endIndex: number | null = null) {
6
+ // We don't want to move the cursor if the target is not focused
7
+ if (!target.tree || target !== document.activeElement) {
8
+ return;
9
+ }
10
+
11
+ const start = Math.max(0, Math.min(startIndex, target.tree.length));
12
+ const end = endIndex ? Math.max(0, Math.min(endIndex, target.tree.length)) : null;
13
+ if (end && end < start) {
14
+ return;
15
+ }
16
+
17
+ const range = document.createRange();
18
+ range.selectNodeContents(target);
19
+
20
+ const startTreeNode = getTreeNodeByIndex(target.tree, start);
21
+ const endTreeNode = end && startTreeNode && (end < startTreeNode.start || end >= startTreeNode.start + startTreeNode.length) ? getTreeNodeByIndex(target.tree, end) : startTreeNode;
22
+ if (!startTreeNode || !endTreeNode) {
23
+ console.error('Invalid start or end tree node');
24
+ return;
25
+ }
26
+
27
+ if (startTreeNode.type === 'br') {
28
+ range.setStartBefore(startTreeNode.element);
29
+ } else {
30
+ range.setStart(startTreeNode.element.childNodes[0] as ChildNode, start - startTreeNode.start);
31
+ }
32
+
33
+ if (endTreeNode.type === 'br') {
34
+ range.setEndBefore(endTreeNode.element);
35
+ } else {
36
+ range.setEnd(endTreeNode.element.childNodes[0] as ChildNode, (end || start) - endTreeNode.start);
37
+ }
38
+
39
+ if (!end) {
40
+ range.collapse(true);
41
+ }
42
+
43
+ const selection = window.getSelection();
44
+ if (selection) {
45
+ selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
46
+ }
47
+
48
+ scrollIntoView(startTreeNode);
49
+ }
50
+
51
+ function scrollIntoView(node: TreeNode) {
52
+ if (node.type === 'br' && node.parentNode?.parentNode?.type === 'line') {
53
+ // If the node is a line break, scroll to the parent paragraph, because Safari doesn't support scrollIntoView on br elements
54
+ node.parentNode.parentNode.element.scrollIntoView({
55
+ block: 'nearest',
56
+ });
57
+ } else {
58
+ node.element.scrollIntoView({
59
+ block: 'nearest',
60
+ });
61
+ }
62
+ }
63
+
64
+ function moveCursorToEnd(target: HTMLElement) {
65
+ const range = document.createRange();
66
+ const selection = window.getSelection();
67
+ if (selection) {
68
+ range.setStart(target, target.childNodes.length);
69
+ range.collapse(true);
70
+ selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
71
+ }
72
+ }
73
+
74
+ function getCurrentCursorPosition(target: MarkdownTextInputElement) {
75
+ function getHTMLElement(node: Node) {
76
+ let element = node as HTMLElement | Text;
77
+ if (element instanceof Text) {
78
+ element = node.parentElement as HTMLElement;
79
+ }
80
+ return element;
81
+ }
82
+
83
+ const selection = window.getSelection();
84
+ if (!selection || (selection && selection.rangeCount === 0)) {
85
+ return null;
86
+ }
87
+ const range = selection.getRangeAt(0);
88
+ const startElement = getHTMLElement(range.startContainer);
89
+ const endElement = range.startContainer === range.endContainer ? startElement : getHTMLElement(range.endContainer);
90
+
91
+ const startTreeNode = findHTMLElementInTree(target.tree, startElement);
92
+ const endTreeNode = findHTMLElementInTree(target.tree, endElement);
93
+
94
+ let start = -1;
95
+ let end = -1;
96
+ if (startTreeNode && endTreeNode) {
97
+ start = startTreeNode.start + range.startOffset;
98
+
99
+ // If the end node is a root node, we need to set the end to the end of the text (FireFox fix)
100
+ if (endTreeNode?.parentNode === null) {
101
+ end = target.value.length;
102
+ } else {
103
+ end = endTreeNode.start + range.endOffset;
104
+ }
105
+ }
106
+ return {start, end};
107
+ }
108
+
109
+ function removeSelection() {
110
+ const selection = window.getSelection();
111
+ if (selection) {
112
+ selection.removeAllRanges();
113
+ }
114
+ }
115
+
116
+ export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, removeSelection, scrollIntoView};
@@ -0,0 +1,106 @@
1
+ import type {CSSProperties} from 'react';
2
+ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
3
+
4
+ const ZERO_WIDTH_SPACE = '\u200B';
5
+
6
+ // If an Input Method Editor is processing key input, the 'keyCode' is 229.
7
+ // https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
8
+ function isEventComposing(nativeEvent: globalThis.KeyboardEvent) {
9
+ return nativeEvent.isComposing || nativeEvent.keyCode === 229;
10
+ }
11
+
12
+ function getPlaceholderValue(placeholder: string | undefined) {
13
+ if (!placeholder) {
14
+ return ZERO_WIDTH_SPACE;
15
+ }
16
+ return placeholder.length ? placeholder : ZERO_WIDTH_SPACE;
17
+ }
18
+
19
+ function getElementHeight(node: HTMLDivElement, styles: CSSProperties, numberOfLines: number | undefined) {
20
+ if (numberOfLines) {
21
+ const tempElement = document.createElement('div');
22
+ tempElement.setAttribute('contenteditable', 'true');
23
+ Object.assign(tempElement.style, styles);
24
+ tempElement.textContent = Array(numberOfLines).fill('A').join('\n');
25
+ if (node.parentElement) {
26
+ node.parentElement.appendChild(tempElement);
27
+ const height = tempElement.clientHeight;
28
+ node.parentElement.removeChild(tempElement);
29
+ return height ? `${height}px` : 'auto';
30
+ }
31
+ }
32
+ return styles.height ? `${styles.height}px` : 'auto';
33
+ }
34
+
35
+ function normalizeValue(value: string) {
36
+ return value.replaceAll('\r\n', '\n');
37
+ }
38
+
39
+ // Parses the HTML structure of a MarkdownTextInputElement to a plain text string. Used for getting the correct value of the input element.
40
+ function parseInnerHTMLToText(target: MarkdownTextInputElement): string {
41
+ // Returns the parent of a given node that is higher in the hierarchy and is of a different type than 'text', 'br' or 'line'
42
+ function getTopParentNode(node: ChildNode) {
43
+ let currentParentNode = node.parentNode;
44
+ while (currentParentNode && ['text', 'br', 'line'].includes(currentParentNode.parentElement?.getAttribute('data-type') || '')) {
45
+ currentParentNode = currentParentNode?.parentNode || null;
46
+ }
47
+ return currentParentNode;
48
+ }
49
+
50
+ const stack: ChildNode[] = [target];
51
+ let text = '';
52
+ let shouldAddNewline = false;
53
+ const lastNode = target.childNodes[target.childNodes.length - 1];
54
+ // Remove the last <br> element if it's the last child of the target element. Fixes the issue with adding extra newline when pasting into the empty input.
55
+ if (lastNode?.nodeName === 'DIV' && (lastNode as HTMLElement)?.innerHTML === '<br>') {
56
+ target.removeChild(lastNode);
57
+ }
58
+
59
+ while (stack.length > 0) {
60
+ const node = stack.pop();
61
+ if (!node) {
62
+ break;
63
+ }
64
+
65
+ // If we are operating on the nodes that are children of the MarkdownTextInputElement, we need to add a newline after each
66
+ const isTopComponent = node.parentElement?.contentEditable === 'true';
67
+ if (isTopComponent) {
68
+ // Replaced text is beeing added as text node, so we need to not add the newline before and after it
69
+ if (node.nodeType === Node.TEXT_NODE) {
70
+ shouldAddNewline = false;
71
+ } else {
72
+ if (shouldAddNewline) {
73
+ text += '\n';
74
+ shouldAddNewline = false;
75
+ }
76
+ shouldAddNewline = true;
77
+ }
78
+ }
79
+
80
+ if (node.nodeType === Node.TEXT_NODE) {
81
+ // Parse text nodes into text
82
+ text += node.textContent;
83
+ } else if (node.nodeName === 'BR') {
84
+ const parentNode = getTopParentNode(node);
85
+ if (parentNode && parentNode.parentElement?.contentEditable !== 'true') {
86
+ // Parse br elements into newlines only if their parent is not a child of the MarkdownTextInputElement (a paragraph when writing or a div when pasting).
87
+ // It prevents adding extra newlines when entering text
88
+ text += '\n';
89
+ }
90
+ } else {
91
+ let i = node.childNodes.length - 1;
92
+ while (i > -1) {
93
+ const child = node.childNodes[i];
94
+ if (!child) {
95
+ break;
96
+ }
97
+ stack.push(child);
98
+ i--;
99
+ }
100
+ }
101
+ }
102
+
103
+ return text.replaceAll('\r\n', '\n');
104
+ }
105
+
106
+ export {isEventComposing, getPlaceholderValue, getElementHeight, parseInnerHTMLToText, normalizeValue};
@@ -0,0 +1,302 @@
1
+ import type {HTMLMarkdownElement, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+ import {addNodeToTree} from './treeUtils';
3
+ import type {NodeType, TreeNode} from './treeUtils';
4
+ import type {PartialMarkdownStyle} from '../../styleUtils';
5
+ import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cursorUtils';
6
+ import {addStyleToBlock} from './blockUtils';
7
+
8
+ type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax' | 'mention-here' | 'mention-user' | 'mention-report';
9
+
10
+ type MarkdownRange = {
11
+ type: MarkdownType;
12
+ start: number;
13
+ length: number;
14
+ depth?: number;
15
+ };
16
+
17
+ type Paragraph = {
18
+ text: string;
19
+ start: number;
20
+ length: number;
21
+ markdownRanges: MarkdownRange[];
22
+ };
23
+
24
+ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] {
25
+ const ungroupedRanges: MarkdownRange[] = [];
26
+ ranges.forEach((range) => {
27
+ if (!range.depth) {
28
+ ungroupedRanges.push(range);
29
+ }
30
+ const {depth, ...rangeWithoutDepth} = range;
31
+ Array.from({length: depth!}).forEach(() => {
32
+ ungroupedRanges.push(rangeWithoutDepth);
33
+ });
34
+ });
35
+ return ungroupedRanges;
36
+ }
37
+
38
+ function splitTextIntoLines(text: string): Paragraph[] {
39
+ let lineStartIndex = 0;
40
+ const lines: Paragraph[] = text.split('\n').map((line) => {
41
+ const lineObject: Paragraph = {
42
+ text: line,
43
+ start: lineStartIndex,
44
+ length: line.length,
45
+ markdownRanges: [],
46
+ };
47
+ lineStartIndex += line.length + 1; // Adding 1 for the newline character
48
+ return lineObject;
49
+ });
50
+
51
+ return lines;
52
+ }
53
+
54
+ /** Merges lines that contain multiline markdown tags into one line */
55
+ function mergeLinesWithMultilineTags(lines: Paragraph[], ranges: MarkdownRange[]) {
56
+ let mergedLines = [...lines];
57
+ const lineIndexes = mergedLines.map((_line, index) => index);
58
+
59
+ ranges.forEach((range) => {
60
+ const beginLineIndex = mergedLines.findLastIndex((line) => line.start <= range.start);
61
+ const endLineIndex = mergedLines.findIndex((line) => line.start + line.length >= range.start + range.length);
62
+ const correspondingLineIndexes = lineIndexes.slice(beginLineIndex, endLineIndex + 1);
63
+
64
+ if (correspondingLineIndexes.length > 0) {
65
+ const mainLineIndex = correspondingLineIndexes[0] as number;
66
+ const mainLine = mergedLines[mainLineIndex] as Paragraph;
67
+
68
+ mainLine.markdownRanges.push(range);
69
+
70
+ const otherLineIndexes = correspondingLineIndexes.slice(1);
71
+ otherLineIndexes.forEach((lineIndex) => {
72
+ const otherLine = mergedLines[lineIndex] as Paragraph;
73
+
74
+ mainLine.text += `\n${otherLine.text}`;
75
+ mainLine.length += otherLine.length + 1;
76
+ mainLine.markdownRanges.push(...otherLine.markdownRanges);
77
+ });
78
+ if (otherLineIndexes.length > 0) {
79
+ mergedLines = mergedLines.filter((_line, index) => !otherLineIndexes.includes(index));
80
+ }
81
+ }
82
+ });
83
+
84
+ return mergedLines;
85
+ }
86
+
87
+ /** Adds a value prop to the element and appends the value to the parent node element */
88
+ function appendValueToElement(element: HTMLMarkdownElement, parentTreeNode: TreeNode, value: string) {
89
+ const targetElement = element;
90
+ const parentNode = parentTreeNode;
91
+ targetElement.value = value;
92
+ parentNode.element.value = (parentNode.element.value || '') + value;
93
+ }
94
+
95
+ function appendNode(element: HTMLMarkdownElement, parentTreeNode: TreeNode, type: NodeType, length: number) {
96
+ const node = addNodeToTree(element, parentTreeNode, type, length);
97
+ parentTreeNode.element.appendChild(element);
98
+ return node;
99
+ }
100
+
101
+ function addBrElement(node: TreeNode) {
102
+ const span = document.createElement('span') as HTMLMarkdownElement;
103
+ span.setAttribute('data-type', 'br');
104
+ appendValueToElement(span, node, '\n');
105
+ const spanNode = appendNode(span, node, 'br', 1);
106
+ appendNode(document.createElement('br') as unknown as HTMLMarkdownElement, spanNode, 'br', 1);
107
+ return spanNode;
108
+ }
109
+
110
+ function addTextToElement(node: TreeNode, text: string) {
111
+ const lines = text.split('\n');
112
+ lines.forEach((line, index) => {
113
+ if (line !== '') {
114
+ const span = document.createElement('span') as HTMLMarkdownElement;
115
+ appendValueToElement(span, node, line);
116
+ span.setAttribute('data-type', 'text');
117
+ span.appendChild(document.createTextNode(line));
118
+ appendNode(span, node, 'text', line.length);
119
+ }
120
+
121
+ if (index < lines.length - 1 || (index === 0 && line === '')) {
122
+ addBrElement(node);
123
+ }
124
+ });
125
+ }
126
+
127
+ function addParagraph(node: TreeNode, text: string | null = null, length: number, disableInlineStyles = false) {
128
+ const p = document.createElement('p');
129
+ p.setAttribute('data-type', 'line');
130
+ if (!disableInlineStyles) {
131
+ addStyleToBlock(p, 'line', {});
132
+ }
133
+
134
+ const pNode = appendNode(p as unknown as HTMLMarkdownElement, node, 'line', length);
135
+
136
+ if (text === '') {
137
+ // If the line is empty, we still need to add a br element to keep the line height
138
+ addBrElement(pNode);
139
+ } else if (text) {
140
+ addTextToElement(pNode, text);
141
+ }
142
+
143
+ return pNode;
144
+ }
145
+
146
+ /** Builds HTML DOM structure based on passed text and markdown ranges */
147
+ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownStyle: PartialMarkdownStyle = {}, disableInlineStyles = false) {
148
+ const rootElement: HTMLMarkdownElement = document.createElement('span') as HTMLMarkdownElement;
149
+ const textLength = text.replace(/\n/g, '\\n').length;
150
+ const rootNode: TreeNode = {
151
+ element: rootElement,
152
+ start: 0,
153
+ length: textLength,
154
+ parentNode: null,
155
+ childNodes: [],
156
+ type: 'root',
157
+ orderIndex: '',
158
+ isGeneratingNewline: false,
159
+ };
160
+ let currentParentNode: TreeNode = rootNode;
161
+ let lines = splitTextIntoLines(text);
162
+
163
+ if (ranges.length === 0) {
164
+ lines.forEach((line) => {
165
+ addParagraph(rootNode, line.text, line.length, disableInlineStyles);
166
+ });
167
+ return {dom: rootElement, tree: rootNode};
168
+ }
169
+
170
+ const markdownRanges = ungroupRanges(ranges);
171
+ lines = mergeLinesWithMultilineTags(lines, markdownRanges);
172
+
173
+ let lastRangeEndIndex = 0;
174
+ while (lines.length > 0) {
175
+ const line = lines.shift();
176
+ if (!line) {
177
+ break;
178
+ }
179
+
180
+ // preparing line paragraph element for markdown text
181
+ currentParentNode = addParagraph(rootNode, null, line.length, disableInlineStyles);
182
+ rootElement.value = (rootElement.value || '') + line.text;
183
+ if (lines.length > 0) {
184
+ rootElement.value = `${rootElement.value || ''}\n`;
185
+ }
186
+
187
+ if (line.markdownRanges.length === 0) {
188
+ addTextToElement(currentParentNode, line.text);
189
+ }
190
+
191
+ lastRangeEndIndex = line.start;
192
+ const lineMarkdownRanges = line.markdownRanges;
193
+ // go through all markdown ranges in the line
194
+ while (lineMarkdownRanges.length > 0) {
195
+ const range = lineMarkdownRanges.shift();
196
+ if (!range) {
197
+ break;
198
+ }
199
+
200
+ const endOfCurrentRange = range.start + range.length;
201
+ const nextRangeStartIndex = lineMarkdownRanges.length > 0 && !!lineMarkdownRanges[0] ? lineMarkdownRanges[0].start || 0 : textLength;
202
+
203
+ // add text before the markdown range
204
+ const textBeforeRange = line.text.substring(lastRangeEndIndex - line.start, range.start - line.start);
205
+ if (textBeforeRange) {
206
+ addTextToElement(currentParentNode, textBeforeRange);
207
+ }
208
+
209
+ // create markdown span element
210
+ const span = document.createElement('span') as HTMLMarkdownElement;
211
+ span.setAttribute('data-type', range.type);
212
+ if (!disableInlineStyles) {
213
+ addStyleToBlock(span, range.type, markdownStyle);
214
+ }
215
+
216
+ const spanNode = appendNode(span, currentParentNode, range.type, range.length);
217
+
218
+ if (lineMarkdownRanges.length > 0 && nextRangeStartIndex < endOfCurrentRange && range.type !== 'syntax') {
219
+ // tag nesting
220
+ currentParentNode = spanNode;
221
+ lastRangeEndIndex = range.start;
222
+ } else {
223
+ // adding markdown tag
224
+ addTextToElement(spanNode, text.substring(range.start, endOfCurrentRange));
225
+ currentParentNode.element.value = (currentParentNode.element.value || '') + (spanNode.element.value || '');
226
+ lastRangeEndIndex = endOfCurrentRange;
227
+ // tag unnesting and adding text after the tag
228
+ while (currentParentNode.parentNode !== null && nextRangeStartIndex >= currentParentNode.start + currentParentNode.length) {
229
+ const textAfterRange = line.text.substring(lastRangeEndIndex - line.start, currentParentNode.start - line.start + currentParentNode.length);
230
+ if (textAfterRange) {
231
+ addTextToElement(currentParentNode, textAfterRange);
232
+ }
233
+ lastRangeEndIndex = currentParentNode.start + currentParentNode.length;
234
+ if (currentParentNode.parentNode.type !== 'root') {
235
+ currentParentNode.parentNode.element.value = currentParentNode.element.value || '';
236
+ }
237
+ currentParentNode = currentParentNode.parentNode || rootNode;
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ return {dom: rootElement, tree: rootNode};
244
+ }
245
+
246
+ function moveCursor(isFocused: boolean, alwaysMoveCursorToTheEnd: boolean, cursorPosition: number | null, target: MarkdownTextInputElement) {
247
+ if (!isFocused) {
248
+ return;
249
+ }
250
+
251
+ if (alwaysMoveCursorToTheEnd || cursorPosition === null) {
252
+ moveCursorToEnd(target);
253
+ } else if (cursorPosition !== null) {
254
+ setCursorPosition(target, cursorPosition);
255
+ }
256
+ }
257
+ function updateInputStructure(
258
+ target: MarkdownTextInputElement,
259
+ text: string,
260
+ cursorPositionIndex: number | null,
261
+ markdownStyle: PartialMarkdownStyle = {},
262
+ alwaysMoveCursorToTheEnd = false,
263
+ shouldForceDOMUpdate = false,
264
+ ) {
265
+ const targetElement = target;
266
+
267
+ // in case the cursorPositionIndex is larger than text length, cursorPosition will be null, i.e: move the caret to the end
268
+ let cursorPosition: number | null = cursorPositionIndex !== null && cursorPositionIndex <= text.length ? cursorPositionIndex : null;
269
+ const isFocused = document.activeElement === target;
270
+ if (isFocused && cursorPositionIndex === null) {
271
+ const selection = getCurrentCursorPosition(target);
272
+ cursorPosition = selection ? selection.start : null;
273
+ }
274
+ const ranges = global.parseExpensiMarkToRanges(text);
275
+ const markdownRanges: MarkdownRange[] = ranges as MarkdownRange[];
276
+ if (!text || targetElement.innerHTML === '<br>' || (targetElement && targetElement.innerHTML === '\n')) {
277
+ targetElement.innerHTML = '';
278
+ targetElement.innerText = '';
279
+ }
280
+
281
+ // We don't want to parse text with single '\n', because contentEditable represents it as invisible <br />
282
+ if (text) {
283
+ const {dom, tree} = parseRangesToHTMLNodes(text, markdownRanges, markdownStyle);
284
+
285
+ if (shouldForceDOMUpdate || targetElement.innerHTML !== dom.innerHTML) {
286
+ targetElement.innerHTML = '';
287
+ targetElement.innerText = '';
288
+ Array.from(dom.children).forEach((child) => {
289
+ targetElement.appendChild(child);
290
+ });
291
+ }
292
+
293
+ targetElement.tree = tree;
294
+ moveCursor(isFocused, alwaysMoveCursorToTheEnd, cursorPosition, targetElement);
295
+ }
296
+
297
+ return {text, cursorPosition: cursorPosition || 0};
298
+ }
299
+
300
+ export {updateInputStructure, parseRangesToHTMLNodes};
301
+
302
+ export type {MarkdownRange, MarkdownType};
@@ -0,0 +1,103 @@
1
+ import type {HTMLMarkdownElement} from '../../MarkdownTextInput.web';
2
+ import type {MarkdownRange, MarkdownType} from './parserUtils';
3
+
4
+ type NodeType = MarkdownType | 'line' | 'text' | 'br' | 'root';
5
+
6
+ type TreeNode = Omit<MarkdownRange, 'type'> & {
7
+ element: HTMLMarkdownElement;
8
+ parentNode: TreeNode | null;
9
+ childNodes: TreeNode[];
10
+ type: NodeType;
11
+ orderIndex: string;
12
+ isGeneratingNewline: boolean;
13
+ };
14
+
15
+ function addNodeToTree(element: HTMLMarkdownElement, parentTreeNode: TreeNode, type: NodeType, length: number | null = null) {
16
+ const contentLength = length || (element.nodeName === 'BR' || type === 'br' ? 1 : element.value?.length) || 0;
17
+ const isGeneratingNewline = type === 'line' && !(element.childNodes.length === 1 && (element.childNodes[0] as HTMLElement)?.getAttribute?.('data-type') === 'br');
18
+ const parentChildrenCount = parentTreeNode?.childNodes.length || 0;
19
+ let startIndex = parentTreeNode.start;
20
+ if (parentChildrenCount > 0) {
21
+ const lastParentChild = parentTreeNode.childNodes[parentChildrenCount - 1];
22
+ if (lastParentChild) {
23
+ startIndex = lastParentChild.start + lastParentChild.length;
24
+ startIndex += lastParentChild.isGeneratingNewline || element.style.display === 'block' ? 1 : 0;
25
+ }
26
+ }
27
+
28
+ const item: TreeNode = {
29
+ element,
30
+ parentNode: parentTreeNode,
31
+ childNodes: [],
32
+ start: startIndex,
33
+ length: contentLength,
34
+ type,
35
+ orderIndex: parentTreeNode.parentNode === null ? `${parentChildrenCount}` : `${parentTreeNode.orderIndex},${parentChildrenCount}`,
36
+ isGeneratingNewline,
37
+ };
38
+
39
+ element.setAttribute('data-id', item.orderIndex);
40
+ parentTreeNode.childNodes.push(item);
41
+ return item;
42
+ }
43
+
44
+ function findHTMLElementInTree(treeRoot: TreeNode, element: HTMLElement): TreeNode | null {
45
+ if (element.hasAttribute?.('contenteditable')) {
46
+ return treeRoot;
47
+ }
48
+
49
+ if (!element || !element.hasAttribute?.('data-id')) {
50
+ return null;
51
+ }
52
+ const indexes = element.getAttribute('data-id')?.split(',');
53
+ let el: TreeNode | null = treeRoot;
54
+
55
+ while (el && indexes && indexes.length > 0) {
56
+ const index = Number(indexes.shift() || -1);
57
+ if (index < 0) {
58
+ break;
59
+ }
60
+
61
+ if (el) {
62
+ el = el.childNodes[index] || null;
63
+ }
64
+ }
65
+
66
+ return el;
67
+ }
68
+
69
+ function getTreeNodeByIndex(treeRoot: TreeNode, index: number): TreeNode | null {
70
+ let el: TreeNode | null = treeRoot;
71
+
72
+ let i = 0;
73
+ let newLineGenerated = false;
74
+ while (el && el.childNodes.length > 0 && i < el.childNodes.length) {
75
+ const child = el.childNodes[i] as TreeNode;
76
+
77
+ if (!child) {
78
+ break;
79
+ }
80
+
81
+ if (index >= child.start && index < child.start + child.length) {
82
+ if (child.childNodes.length === 0) {
83
+ return child;
84
+ }
85
+ el = child;
86
+ i = 0;
87
+ } else if ((child.isGeneratingNewline || newLineGenerated || i === el.childNodes.length - 1) && index === child.start + child.length) {
88
+ newLineGenerated = true;
89
+ if (child.childNodes.length === 0) {
90
+ return child;
91
+ }
92
+ el = child;
93
+ i = 0;
94
+ } else {
95
+ i++;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ export {addNodeToTree, findHTMLElementInTree, getTreeNodeByIndex};
102
+
103
+ export type {TreeNode, NodeType};
@@ -0,0 +1,54 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type {TextStyle} from 'react-native';
3
+ import type {MarkdownStyle} from '../../MarkdownTextInputDecoratorViewNativeComponent';
4
+ import {mergeMarkdownStyleWithDefault} from '../../styleUtils';
5
+
6
+ let createReactDOMStyle: (style: any) => any;
7
+ try {
8
+ createReactDOMStyle =
9
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
10
+ require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
11
+ } catch (e) {
12
+ throw new Error('[react-native-live-markdown] Function `createReactDOMStyle` from react-native-web not found. Please make sure that you are using React Native Web 0.18 or newer.');
13
+ }
14
+
15
+ let preprocessStyle: (style: any) => any;
16
+ try {
17
+ preprocessStyle =
18
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
19
+ require('react-native-web/dist/exports/StyleSheet/preprocess').default;
20
+ } catch (e) {
21
+ throw new Error('[react-native-live-markdown] Function `preprocessStyle` from react-native-web not found.');
22
+ }
23
+
24
+ let dangerousStyleValue: (name: string, value: any, isCustomProperty: boolean) => any;
25
+ try {
26
+ dangerousStyleValue =
27
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
28
+ require('react-native-web/dist/modules/setValueForStyles/dangerousStyleValue').default;
29
+ } catch (e) {
30
+ throw new Error('[react-native-live-markdown] Function `dangerousStyleValue` from react-native-web not found.');
31
+ }
32
+
33
+ function processUnitsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
34
+ const output = JSON.parse(JSON.stringify(input));
35
+
36
+ Object.keys(output).forEach((key) => {
37
+ const obj = output[key];
38
+ Object.keys(obj).forEach((prop) => {
39
+ obj[prop] = dangerousStyleValue(prop, obj[prop], false);
40
+ });
41
+ });
42
+
43
+ return output as MarkdownStyle;
44
+ }
45
+
46
+ function processMarkdownStyle(input: MarkdownStyle | undefined): MarkdownStyle {
47
+ return processUnitsInMarkdownStyle(mergeMarkdownStyleWithDefault(input));
48
+ }
49
+
50
+ function parseToReactDOMStyle(style: TextStyle): any {
51
+ return createReactDOMStyle(preprocessStyle(style));
52
+ }
53
+
54
+ export {parseToReactDOMStyle, processMarkdownStyle};