@expensify/react-native-live-markdown 0.1.299 → 0.1.301

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 (51) hide show
  1. package/lib/commonjs/MarkdownTextInput.web.js +1 -1
  2. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  3. package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  4. package/lib/commonjs/parseExpensiMark.js +9 -3
  5. package/lib/commonjs/parseExpensiMark.js.map +1 -1
  6. package/lib/commonjs/styleUtils.js +15 -2
  7. package/lib/commonjs/styleUtils.js.map +1 -1
  8. package/lib/commonjs/web/MarkdownTextInput.css +32 -5
  9. package/lib/commonjs/web/utils/blockUtils.js +60 -5
  10. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  11. package/lib/commonjs/web/utils/cursorUtils.js +28 -3
  12. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -1
  13. package/lib/commonjs/web/utils/inputUtils.js +49 -11
  14. package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
  15. package/lib/module/MarkdownTextInput.web.js +1 -1
  16. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  17. package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  18. package/lib/module/parseExpensiMark.js +9 -3
  19. package/lib/module/parseExpensiMark.js.map +1 -1
  20. package/lib/module/styleUtils.js +15 -2
  21. package/lib/module/styleUtils.js.map +1 -1
  22. package/lib/module/web/MarkdownTextInput.css +32 -5
  23. package/lib/module/web/utils/blockUtils.js +58 -6
  24. package/lib/module/web/utils/blockUtils.js.map +1 -1
  25. package/lib/module/web/utils/cursorUtils.js +28 -3
  26. package/lib/module/web/utils/cursorUtils.js.map +1 -1
  27. package/lib/module/web/utils/inputUtils.js +48 -11
  28. package/lib/module/web/utils/inputUtils.js.map +1 -1
  29. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  30. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +16 -11
  31. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  32. package/lib/typescript/src/commonTypes.d.ts +1 -1
  33. package/lib/typescript/src/commonTypes.d.ts.map +1 -1
  34. package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -1
  35. package/lib/typescript/src/styleUtils.d.ts +1 -1
  36. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  37. package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -2
  38. package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -1
  39. package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -1
  40. package/lib/typescript/src/web/utils/inputUtils.d.ts +3 -0
  41. package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -1
  42. package/package.json +3 -3
  43. package/src/MarkdownTextInput.web.tsx +3 -2
  44. package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +17 -11
  45. package/src/commonTypes.ts +2 -1
  46. package/src/parseExpensiMark.ts +5 -3
  47. package/src/styleUtils.ts +14 -1
  48. package/src/web/MarkdownTextInput.css +32 -5
  49. package/src/web/utils/blockUtils.ts +74 -8
  50. package/src/web/utils/cursorUtils.ts +29 -3
  51. package/src/web/utils/inputUtils.ts +61 -12
@@ -1,5 +1,6 @@
1
- import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
1
  import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes';
2
+ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
3
+ import {parseStringWithUnitToNumber} from '../../styleUtils';
3
4
  import type {PartialMarkdownStyle} from '../../styleUtils';
4
5
  import {addInlineImagePreview} from '../inputElements/inlineImage';
5
6
  import type {NodeType, TreeNode} from './treeUtils';
@@ -51,12 +52,9 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
51
52
  });
52
53
  break;
53
54
  case 'code':
54
- Object.assign(node.style, markdownStyle.code);
55
- break;
56
55
  case 'pre':
57
- Object.assign(node.style, markdownStyle.pre);
56
+ addCodeBlockStyles(targetElement, type, markdownStyle, isMultiline);
58
57
  break;
59
-
60
58
  case 'blockquote':
61
59
  Object.assign(node.style, {
62
60
  ...markdownStyle.blockquote,
@@ -94,9 +92,49 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
94
92
  }
95
93
  }
96
94
 
95
+ function addCodeBlockStyles(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle, isMultiline = true) {
96
+ const node = targetElement;
97
+
98
+ const defaultPrePadding = markdownStyle.pre?.padding ?? 2;
99
+ const preHorizontalPadding = parseStringWithUnitToNumber(markdownStyle.pre?.paddingHorizontal ?? defaultPrePadding).toString();
100
+ const preVerticalPadding = parseStringWithUnitToNumber(markdownStyle.pre?.paddingVertical ?? defaultPrePadding).toString();
101
+
102
+ const defaultCodePadding = markdownStyle.code?.padding ?? 0;
103
+ const codeHorizontalPadding = parseStringWithUnitToNumber(markdownStyle.code?.paddingHorizontal ?? defaultCodePadding).toString();
104
+ const codeVerticalPadding = parseStringWithUnitToNumber(markdownStyle.code?.paddingVertical ?? defaultCodePadding).toString();
105
+
106
+ switch (type) {
107
+ case 'code':
108
+ Object.assign(node.style, {
109
+ ...markdownStyle.code,
110
+ fontSize: markdownStyle.code?.h1NestedFontSize && isChildOfMarkdownElement(node, 'h1') ? markdownStyle.code.h1NestedFontSize : markdownStyle.code?.fontSize,
111
+ padding: `${codeVerticalPadding}px ${codeHorizontalPadding}px`,
112
+ lineHeight: 1.5,
113
+ });
114
+ break;
115
+ case 'pre':
116
+ // In multiline style the pre block using pseudoelements, otherwise default to inline
117
+ if (isMultiline) {
118
+ Object.assign(node.style, {
119
+ ...markdownStyle.pre,
120
+ padding: `${preVerticalPadding}px ${preHorizontalPadding}px`,
121
+ });
122
+ } else {
123
+ Object.assign(node.style, {
124
+ ...markdownStyle.code,
125
+ padding: `${codeVerticalPadding}px ${codeHorizontalPadding}px`,
126
+ lineHeight: 1.5,
127
+ });
128
+ }
129
+ break;
130
+ default:
131
+ break;
132
+ }
133
+ }
134
+
97
135
  const BLOCK_MARKDOWN_TYPES = ['inline-image'];
98
136
  const FULL_LINE_MARKDOWN_TYPES = ['blockquote'];
99
- const MULTILINE_MARKDOWN_TYPES = ['pre'];
137
+ const MULTILINE_MARKDOWN_TYPES = ['codeblock'];
100
138
 
101
139
  function isBlockMarkdownType(type: NodeType) {
102
140
  return BLOCK_MARKDOWN_TYPES.includes(type);
@@ -108,7 +146,7 @@ function isMultilineMarkdownType(type: NodeType) {
108
146
 
109
147
  function getFirstBlockMarkdownRange(ranges: MarkdownRange[]) {
110
148
  const blockMarkdownRange = ranges.find((r) => isBlockMarkdownType(r.type) || FULL_LINE_MARKDOWN_TYPES.includes(r.type));
111
- return FULL_LINE_MARKDOWN_TYPES.includes(blockMarkdownRange?.type || '') ? undefined : blockMarkdownRange;
149
+ return blockMarkdownRange && FULL_LINE_MARKDOWN_TYPES.includes(blockMarkdownRange.type) ? undefined : blockMarkdownRange;
112
150
  }
113
151
 
114
152
  function extendBlockStructure(
@@ -130,4 +168,32 @@ function extendBlockStructure(
130
168
  return targetNode;
131
169
  }
132
170
 
133
- export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, isMultilineMarkdownType, getFirstBlockMarkdownRange};
171
+ function isDescendantOfMarkdownElement(node: HTMLElement, predicate: (type: string | null) => boolean): boolean {
172
+ let currentNode = node.parentNode;
173
+ while (currentNode && (currentNode as HTMLElement)?.contentEditable !== 'true') {
174
+ const elementType = (currentNode as HTMLElement).getAttribute?.('data-type');
175
+ if (predicate(elementType)) {
176
+ return true;
177
+ }
178
+ currentNode = currentNode.parentNode;
179
+ }
180
+ return false;
181
+ }
182
+
183
+ function isChildOfMarkdownElement(node: HTMLElement, elementType: NodeType): boolean {
184
+ return isDescendantOfMarkdownElement(node, (type) => type === elementType);
185
+ }
186
+ function isChildOfMultilineMarkdownElement(node: HTMLElement): boolean {
187
+ return isDescendantOfMarkdownElement(node, (type) => MULTILINE_MARKDOWN_TYPES.includes(type as NodeType));
188
+ }
189
+
190
+ export {
191
+ addStyleToBlock,
192
+ extendBlockStructure,
193
+ isBlockMarkdownType,
194
+ isMultilineMarkdownType,
195
+ getFirstBlockMarkdownRange,
196
+ isChildOfMarkdownElement,
197
+ isChildOfMultilineMarkdownElement,
198
+ MULTILINE_MARKDOWN_TYPES,
199
+ };
@@ -1,4 +1,5 @@
1
1
  import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+ import {isChildOfMultilineMarkdownElement} from './blockUtils';
2
3
  import {findHTMLElementInTree, getTreeNodeByIndex} from './treeUtils';
3
4
  import type {TreeNode} from './treeUtils';
4
5
 
@@ -53,10 +54,35 @@ function setCursorPosition(target: MarkdownTextInputElement, startIndex: number,
53
54
  }
54
55
 
55
56
  function scrollIntoView(target: MarkdownTextInputElement, node: TreeNode) {
57
+ let scrollTargetElement = node.element;
56
58
  const targetElement = target;
57
- const orderIndex = Number(node.orderIndex.split(',')[0]);
58
- const currentLine = target.tree.childNodes[orderIndex]?.element;
59
- const scrollTargetElement = currentLine || node.element;
59
+
60
+ if (!isChildOfMultilineMarkdownElement(node.element)) {
61
+ const orderIndex = Number(node.orderIndex.split(',')[0]);
62
+ const currentLine = target.tree.childNodes[orderIndex]?.element;
63
+ if (currentLine) {
64
+ scrollTargetElement = currentLine;
65
+ }
66
+ } else if (node.element.nodeName === 'BR') {
67
+ // Force scrolling BR into view
68
+ const selection = window.getSelection();
69
+ if (selection) {
70
+ const range = document.createRange();
71
+
72
+ range.setStartBefore(node.element);
73
+ range.collapse(true);
74
+
75
+ selection.addRange(range);
76
+
77
+ // Scroll to caret
78
+ const span = document.createElement('span');
79
+ span.textContent = '\u200B'; // zero-width space
80
+ range.insertNode(span);
81
+ span.scrollIntoView({block: 'center'});
82
+ span.remove(); // cleanup
83
+ return;
84
+ }
85
+ }
60
86
 
61
87
  const caretRect = scrollTargetElement.getBoundingClientRect();
62
88
  const targetRect = target.getBoundingClientRect();
@@ -1,5 +1,7 @@
1
1
  import type {CSSProperties} from 'react';
2
2
  import type {MarkdownNativeEvent, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
3
+ import {isChildOfMarkdownElement, isChildOfMultilineMarkdownElement} from './blockUtils';
4
+ import BrowserUtils from './browserUtils';
3
5
 
4
6
  const ZERO_WIDTH_SPACE = '\u200B';
5
7
 
@@ -36,17 +38,33 @@ function normalizeValue(value: string) {
36
38
  return value.replaceAll('\r\n', '\n');
37
39
  }
38
40
 
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, cursorPosition: number, inputType?: string, isMultiline = true): 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;
41
+ /**
42
+ * Returns the parent of a given node that is higher in the hierarchy and is of a different type than 'text', 'br' or 'line'
43
+ */
44
+ function getTopParentNode(node: ChildNode) {
45
+ let currentParentNode = node.parentNode;
46
+ while (currentParentNode && ['text', 'br', 'line'].includes(currentParentNode.parentElement?.getAttribute('data-type') || '')) {
47
+ currentParentNode = currentParentNode?.parentNode || null;
48
+ }
49
+ return currentParentNode;
50
+ }
51
+
52
+ /**
53
+ * On Firefox, when breaking one codeblock, its syntax and the <br> after it can be merged into the closing syntax of the previous codeblock.
54
+ */
55
+ function didTwoCodeblocksMerge(node: ChildNode | null) {
56
+ if (!node || !BrowserUtils.isFirefox) {
57
+ return;
48
58
  }
59
+ // To identify that two codeblock has merged, we check if current line ends with <br> tag, that previously was second codeblock's opening syntax line break
60
+ const hasPartOfBrokenCodeblock = node.lastChild?.lastChild?.lastChild?.lastChild?.nodeName === 'BR';
61
+ return BrowserUtils.isFirefox && (node.lastChild as HTMLElement)?.getAttribute('data-type') === 'codeblock' && hasPartOfBrokenCodeblock;
62
+ }
49
63
 
64
+ /**
65
+ * Parses the HTML structure of a MarkdownTextInputElement to a plain text string. Used for getting the correct value of the input element.
66
+ */
67
+ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string, isMultiline = true): string {
50
68
  const stack: ChildNode[] = [target];
51
69
  let text = '';
52
70
  let shouldAddNewline = false;
@@ -57,7 +75,7 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
57
75
  }
58
76
 
59
77
  while (stack.length > 0) {
60
- const node = stack.pop();
78
+ const node = stack.pop() as HTMLElement;
61
79
  if (!node) {
62
80
  break;
63
81
  }
@@ -72,7 +90,7 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
72
90
  } else {
73
91
  const firstChild = node.firstChild as HTMLElement;
74
92
  const containsEmptyBlockElement = firstChild?.getAttribute?.('data-type') === 'block' && firstChild.textContent === '';
75
- if (shouldAddNewline && !containsEmptyBlockElement) {
93
+ if (firstChild && shouldAddNewline && !containsEmptyBlockElement && !didTwoCodeblocksMerge(node.previousSibling)) {
76
94
  text += '\n';
77
95
  shouldAddNewline = false;
78
96
  }
@@ -81,11 +99,42 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
81
99
  }
82
100
 
83
101
  if (node.nodeType === Node.TEXT_NODE) {
102
+ let hasAddedNewline = false;
103
+ // Fix for codeblocks: Removing last codeblock newline, moves codeblock syntax too far into the codeblock content
104
+ // skipping one <br> after the codeblock syntax. We need to force parsing it before the text node is added.
105
+ if (node.parentElement && !node.parentElement.getAttribute?.('data-type') && isChildOfMarkdownElement(node, 'pre')) {
106
+ text += '\n';
107
+ const nextBR = node.parentElement?.nextElementSibling?.firstElementChild ?? node.parentElement?.nextElementSibling;
108
+ if (nextBR && nextBR.tagName === 'BR') {
109
+ nextBR.remove();
110
+ }
111
+ hasAddedNewline = true;
112
+ }
113
+
84
114
  // Parse text nodes into text
85
115
  text += node.textContent;
116
+
117
+ // Fix for codeblocks: If we are adding text at the end of a multiline markdown type element, we need to add a newline
118
+ // because the new text can replace the last <br> element and it will not be added to the text.
119
+ if (
120
+ node.parentElement &&
121
+ node.parentNode?.parentElement?.nextSibling &&
122
+ !node.parentNode?.nextSibling &&
123
+ isChildOfMultilineMarkdownElement(node) &&
124
+ ((!hasAddedNewline && isChildOfMarkdownElement(node, 'br')) || (!node.parentElement.getAttribute?.('data-type') && isChildOfMarkdownElement(node, 'syntax')))
125
+ ) {
126
+ text += '\n';
127
+ }
86
128
  } else if (node.nodeName === 'BR') {
87
129
  const parentNode = getTopParentNode(node);
88
- if (isMultiline && parentNode && parentNode.parentElement?.contentEditable !== 'true' && !!(node as HTMLElement).getAttribute('data-id')) {
130
+
131
+ if (
132
+ (isMultiline &&
133
+ parentNode &&
134
+ parentNode.parentElement?.contentEditable !== 'true' &&
135
+ !!((node as HTMLElement).getAttribute('data-id') || (node.parentElement as HTMLElement).getAttribute('data-type') === 'br')) ||
136
+ (node.parentElement?.getAttribute('data-type') === 'text' && isChildOfMultilineMarkdownElement(node))
137
+ ) {
89
138
  // 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).
90
139
  // It prevents adding extra newlines when entering text - and now only for multiline inputs
91
140
  text += '\n';