@expensify/react-native-live-markdown 0.1.115 → 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.
- package/lib/commonjs/MarkdownTextInput.js +2 -4
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +105 -149
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/utils/blockUtils.js +77 -0
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
- package/lib/commonjs/web/utils/browserUtils.js +18 -0
- package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/cursorUtils.js +111 -0
- package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
- package/lib/commonjs/web/utils/inputUtils.js +108 -0
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +266 -0
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/treeUtils.js +85 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
- package/lib/commonjs/web/utils/webStyleUtils.js +51 -0
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +2 -2
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +101 -145
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/utils/blockUtils.js +72 -0
- package/lib/module/web/utils/blockUtils.js.map +1 -0
- package/lib/module/web/utils/browserUtils.js +12 -0
- package/lib/module/web/utils/browserUtils.js.map +1 -0
- package/lib/module/web/utils/cursorUtils.js +102 -0
- package/lib/module/web/utils/cursorUtils.js.map +1 -0
- package/lib/module/web/utils/inputUtils.js +99 -0
- package/lib/module/web/utils/inputUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +260 -0
- package/lib/module/web/utils/parserUtils.js.map +1 -0
- package/lib/module/web/utils/treeUtils.js +78 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -0
- package/lib/module/web/utils/webStyleUtils.js +45 -0
- package/lib/module/web/utils/webStyleUtils.js.map +1 -0
- package/lib/typescript/src/MarkdownTextInput.d.ts +1 -2
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -3
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/styleUtils.d.ts +1 -2
- package/lib/typescript/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -0
- package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/browserUtils.d.ts +12 -0
- package/lib/typescript/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/inputUtils.d.ts +9 -0
- package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts +22 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/treeUtils.d.ts +17 -0
- package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/webStyleUtils.d.ts +6 -0
- package/lib/typescript/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/MarkdownTextInput.tsx +4 -7
- package/src/MarkdownTextInput.web.tsx +142 -171
- package/src/styleUtils.ts +1 -3
- package/src/web/utils/blockUtils.ts +72 -0
- package/src/web/utils/browserUtils.ts +13 -0
- package/src/web/utils/cursorUtils.ts +116 -0
- package/src/web/utils/inputUtils.ts +106 -0
- package/src/web/utils/parserUtils.ts +302 -0
- package/src/web/utils/treeUtils.ts +103 -0
- package/src/web/utils/webStyleUtils.ts +54 -0
- package/lib/commonjs/web/browserUtils.js +0 -16
- package/lib/commonjs/web/browserUtils.js.map +0 -1
- package/lib/commonjs/web/cursorUtils.js +0 -156
- package/lib/commonjs/web/cursorUtils.js.map +0 -1
- package/lib/commonjs/web/parserUtils.js +0 -214
- package/lib/commonjs/web/parserUtils.js.map +0 -1
- package/lib/module/web/browserUtils.js +0 -11
- package/lib/module/web/browserUtils.js.map +0 -1
- package/lib/module/web/cursorUtils.js +0 -143
- package/lib/module/web/cursorUtils.js.map +0 -1
- package/lib/module/web/parserUtils.js +0 -206
- package/lib/module/web/parserUtils.js.map +0 -1
- package/lib/typescript/src/web/browserUtils.d.ts +0 -10
- package/lib/typescript/src/web/browserUtils.d.ts.map +0 -1
- package/lib/typescript/src/web/cursorUtils.d.ts +0 -12
- package/lib/typescript/src/web/cursorUtils.d.ts.map +0 -1
- package/lib/typescript/src/web/parserUtils.d.ts +0 -17
- package/lib/typescript/src/web/parserUtils.d.ts.map +0 -1
- package/src/web/browserUtils.ts +0 -11
- package/src/web/cursorUtils.ts +0 -165
- 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};
|