@expensify/react-native-live-markdown 0.1.298 → 0.1.300

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 (57) hide show
  1. package/lib/commonjs/MarkdownTextInput.web.js +2 -2
  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 +28 -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 +51 -13
  14. package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
  15. package/lib/commonjs/web/utils/parserUtils.js +6 -4
  16. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  17. package/lib/module/MarkdownTextInput.web.js +2 -2
  18. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  19. package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  20. package/lib/module/parseExpensiMark.js +9 -3
  21. package/lib/module/parseExpensiMark.js.map +1 -1
  22. package/lib/module/styleUtils.js +15 -2
  23. package/lib/module/styleUtils.js.map +1 -1
  24. package/lib/module/web/MarkdownTextInput.css +28 -5
  25. package/lib/module/web/utils/blockUtils.js +58 -6
  26. package/lib/module/web/utils/blockUtils.js.map +1 -1
  27. package/lib/module/web/utils/cursorUtils.js +28 -3
  28. package/lib/module/web/utils/cursorUtils.js.map +1 -1
  29. package/lib/module/web/utils/inputUtils.js +50 -13
  30. package/lib/module/web/utils/inputUtils.js.map +1 -1
  31. package/lib/module/web/utils/parserUtils.js +6 -4
  32. package/lib/module/web/utils/parserUtils.js.map +1 -1
  33. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  34. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +16 -11
  35. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  36. package/lib/typescript/src/commonTypes.d.ts +1 -1
  37. package/lib/typescript/src/commonTypes.d.ts.map +1 -1
  38. package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -1
  39. package/lib/typescript/src/styleUtils.d.ts +1 -1
  40. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  41. package/lib/typescript/src/web/utils/blockUtils.d.ts +5 -2
  42. package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -1
  43. package/lib/typescript/src/web/utils/cursorUtils.d.ts.map +1 -1
  44. package/lib/typescript/src/web/utils/inputUtils.d.ts +4 -1
  45. package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -1
  46. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/MarkdownTextInput.web.tsx +4 -3
  49. package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +17 -11
  50. package/src/commonTypes.ts +2 -1
  51. package/src/parseExpensiMark.ts +5 -3
  52. package/src/styleUtils.ts +14 -1
  53. package/src/web/MarkdownTextInput.css +28 -5
  54. package/src/web/utils/blockUtils.ts +74 -8
  55. package/src/web/utils/cursorUtils.ts +29 -3
  56. package/src/web/utils/inputUtils.ts +63 -14
  57. package/src/web/utils/parserUtils.ts +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"inputUtils.d.ts","sourceRoot":"","sources":["../../../../../src/web/utils/inputUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AACzC,OAAO,KAAK,EAAC,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,6BAA6B,CAAC;AAM/F,iBAAS,gBAAgB,CAAC,WAAW,EAAE,UAAU,CAAC,aAAa,GAAG,mBAAmB,WAEpF;AAED,iBAAS,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,UAK3D;AAED,iBAAS,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,UAcvG;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,MAAM,UAEpC;AAGD,iBAAS,oBAAoB,CAAC,MAAM,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAwElH;AAED,OAAO,EAAC,gBAAgB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,cAAc,EAAC,CAAC"}
1
+ {"version":3,"file":"inputUtils.d.ts","sourceRoot":"","sources":["../../../../../src/web/utils/inputUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AACzC,OAAO,KAAK,EAAC,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,6BAA6B,CAAC;AAQ/F,iBAAS,gBAAgB,CAAC,WAAW,EAAE,UAAU,CAAC,aAAa,GAAG,mBAAmB,WAEpF;AAED,iBAAS,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,UAK3D;AAED,iBAAS,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,UAcvG;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,MAAM,UAEpC;AAyBD;;GAEG;AACH,iBAAS,oBAAoB,CAAC,MAAM,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,MAAM,CA8FtI;AAED,OAAO,EAAC,gBAAgB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,cAAc,EAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"parserUtils.d.ts","sourceRoot":"","sources":["../../../../../src/web/utils/parserUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,6BAA6B,CAAC;AAE/F,OAAO,KAAK,EAAW,QAAQ,EAAC,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,kBAAkB,CAAC;AAG3D,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAI7E,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,aAAa,EAAE,CAAC;CACjC,CAAC;AAoEF;;;GAGG;AACH,iBAAS,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,eAyBlE;AA0ED,yEAAyE;AACzE,iBAAS,sBAAsB,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EAAE,EACvB,WAAW,UAAO,EAClB,aAAa,GAAE,oBAAyB,EACxC,mBAAmB,UAAQ,EAC3B,YAAY,GAAE,wBAAwB,GAAG,IAAW,EACpD,iBAAiB,GAAE,sBAA2B;;;EA4G/C;AAcD,iBAAS,oBAAoB,CAC3B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,aAAa,EAAE,EAClD,MAAM,EAAE,wBAAwB,EAChC,IAAI,EAAE,MAAM,EACZ,mBAAmB,EAAE,MAAM,GAAG,IAAI,EAClC,WAAW,UAAO,EAClB,aAAa,GAAE,oBAAyB,EACxC,wBAAwB,UAAQ,EAChC,oBAAoB,UAAQ,EAC5B,oBAAoB,UAAQ,EAC5B,iBAAiB,GAAE,sBAA2B;;;EAsC/C;AAED,OAAO,EAAC,oBAAoB,EAAE,sBAAsB,EAAE,cAAc,EAAC,CAAC;AACtE,YAAY,EAAC,SAAS,EAAC,CAAC"}
1
+ {"version":3,"file":"parserUtils.d.ts","sourceRoot":"","sources":["../../../../../src/web/utils/parserUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,6BAA6B,CAAC;AAE/F,OAAO,KAAK,EAAW,QAAQ,EAAC,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,kBAAkB,CAAC;AAG3D,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAI7E,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,aAAa,EAAE,CAAC;CACjC,CAAC;AAoEF;;;GAGG;AACH,iBAAS,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,eAyBlE;AA2ED,yEAAyE;AACzE,iBAAS,sBAAsB,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EAAE,EACvB,WAAW,UAAO,EAClB,aAAa,GAAE,oBAAyB,EACxC,mBAAmB,UAAQ,EAC3B,YAAY,GAAE,wBAAwB,GAAG,IAAW,EACpD,iBAAiB,GAAE,sBAA2B;;;EA4G/C;AAcD,iBAAS,oBAAoB,CAC3B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,aAAa,EAAE,EAClD,MAAM,EAAE,wBAAwB,EAChC,IAAI,EAAE,MAAM,EACZ,mBAAmB,EAAE,MAAM,GAAG,IAAI,EAClC,WAAW,UAAO,EAClB,aAAa,GAAE,oBAAyB,EACxC,wBAAwB,UAAQ,EAChC,oBAAoB,UAAQ,EAC5B,oBAAoB,UAAQ,EAC5B,iBAAiB,GAAE,sBAA2B;;;EAsC/C;AAED,OAAO,EAAC,oBAAoB,EAAE,sBAAsB,EAAE,cAAc,EAAC,CAAC;AACtE,YAAY,EAAC,SAAS,EAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expensify/react-native-live-markdown",
3
- "version": "0.1.298",
3
+ "version": "0.1.300",
4
4
  "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -87,7 +87,7 @@
87
87
  "eslint-plugin-prettier": "^4.0.0",
88
88
  "eslint-plugin-promise": "^6.1.1",
89
89
  "eslint-plugin-tsdoc": "^0.2.17",
90
- "expensify-common": "2.0.134",
90
+ "expensify-common": "2.0.148",
91
91
  "jest": "^29.6.3",
92
92
  "jest-environment-jsdom": "^29.7.0",
93
93
  "nodemon": "^3.1.3",
@@ -104,7 +104,7 @@
104
104
  "typescript": "~5.3.3"
105
105
  },
106
106
  "peerDependencies": {
107
- "expensify-common": ">=2.0.115",
107
+ "expensify-common": ">=2.0.148",
108
108
  "react": "*",
109
109
  "react-native": "*",
110
110
  "react-native-reanimated": ">=3.17.0"
@@ -355,7 +355,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
355
355
  updateTextColor(divRef.current, e.target.textContent ?? '');
356
356
  const previousText = divRef.current.value;
357
357
  let parsedText = normalizeValue(
358
- inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType),
358
+ inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType, multiline),
359
359
  );
360
360
 
361
361
  if (pasteContent.current) {
@@ -451,8 +451,9 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
451
451
  handleContentSizeChange();
452
452
  },
453
453
  [
454
- parser,
455
454
  updateTextColor,
455
+ multiline,
456
+ maxLength,
456
457
  updateSelection,
457
458
  onChange,
458
459
  onChangeText,
@@ -461,9 +462,9 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
461
462
  redo,
462
463
  handleFormatSelection,
463
464
  parseText,
465
+ parser,
464
466
  processedMarkdownStyle,
465
467
  setEventProps,
466
- maxLength,
467
468
  ],
468
469
  );
469
470
 
@@ -3,6 +3,20 @@ import type {ColorValue, ViewProps} from 'react-native';
3
3
  import type {Float, Int32} from 'react-native/Libraries/Types/CodegenTypes';
4
4
  import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
5
5
 
6
+ interface CodeBlockStyle {
7
+ fontFamily: string;
8
+ fontSize: Float;
9
+ color: ColorValue;
10
+ backgroundColor: ColorValue;
11
+ borderColor?: ColorValue;
12
+ borderWidth?: Float;
13
+ borderRadius?: Float;
14
+ borderStyle?: string;
15
+ padding?: Float;
16
+ paddingVertical?: Float;
17
+ paddingHorizontal?: Float;
18
+ }
19
+
6
20
  interface MarkdownStyle {
7
21
  syntax: {
8
22
  color: ColorValue;
@@ -23,18 +37,10 @@ interface MarkdownStyle {
23
37
  marginLeft: Float;
24
38
  paddingLeft: Float;
25
39
  };
26
- code: {
27
- fontFamily: string;
28
- fontSize: Float;
29
- color: ColorValue;
30
- backgroundColor: ColorValue;
31
- };
32
- pre: {
33
- fontFamily: string;
34
- fontSize: Float;
35
- color: ColorValue;
36
- backgroundColor: ColorValue;
40
+ code: CodeBlockStyle & {
41
+ h1NestedFontSize?: Float;
37
42
  };
43
+ pre: CodeBlockStyle;
38
44
  mentionHere: {
39
45
  color: ColorValue;
40
46
  backgroundColor: ColorValue;
@@ -13,7 +13,8 @@ type MarkdownType =
13
13
  | 'blockquote'
14
14
  | 'h1'
15
15
  | 'syntax'
16
- | 'inline-image';
16
+ | 'inline-image'
17
+ | 'codeblock';
17
18
 
18
19
  interface MarkdownRange {
19
20
  type: MarkdownType;
@@ -174,9 +174,11 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
174
174
  } else if (node.tag === '<br />') {
175
175
  text += '\n';
176
176
  } else if (node.tag.startsWith('<pre')) {
177
- appendSyntax('```');
178
- const content = node.children.join('');
179
- addChildrenWithStyle(content, 'pre');
177
+ const [_, lb, content] = node.children.join('').match(/^(\r?\n)([\s\S]*)$/) ?? [];
178
+ // Adding opening ("```${lb}") and closing ("```") codeblock syntax length, equal to 7, to the content length
179
+ ranges.push({type: 'codeblock', start: text.length, length: (content?.length ?? 0) + 7});
180
+ appendSyntax(`\`\`\`${lb}`);
181
+ addChildrenWithStyle(`${content}`, 'pre');
180
182
  appendSyntax('```');
181
183
  } else if (node.tag.startsWith('<a href="')) {
182
184
  const rawHref = node.tag.match(/href="([^"]*)"/)![1]!; // always present
package/src/styleUtils.ts CHANGED
@@ -42,12 +42,22 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
42
42
  fontSize: 20,
43
43
  color: 'black',
44
44
  backgroundColor: 'lightgray',
45
+ borderColor: 'gray',
46
+ borderWidth: 1,
47
+ borderRadius: 4,
48
+ borderStyle: 'solid',
49
+ padding: 0,
45
50
  },
46
51
  pre: {
47
52
  fontFamily: FONT_FAMILY_MONOSPACE,
48
53
  fontSize: 20,
49
54
  color: 'black',
50
55
  backgroundColor: 'lightgray',
56
+ borderColor: 'gray',
57
+ borderWidth: 1,
58
+ borderRadius: 4,
59
+ borderStyle: 'solid',
60
+ padding: 2,
51
61
  },
52
62
  mentionHere: {
53
63
  color: 'green',
@@ -96,7 +106,10 @@ function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined):
96
106
  return output;
97
107
  }
98
108
 
99
- function parseStringWithUnitToNumber(value: string | null): number {
109
+ function parseStringWithUnitToNumber(value: string | number | null): number {
110
+ if (typeof value === 'number') {
111
+ return value;
112
+ }
100
113
  return value ? parseInt(value.replace('px', ''), 10) : 0;
101
114
  }
102
115
 
@@ -30,11 +30,34 @@
30
30
  padding-right: 1px !important;
31
31
  }
32
32
 
33
- .react-native-live-markdown-input-singleline:empty::before,
34
- .react-native-live-markdown-input-multiline:empty::before {
35
- pointer-events: none;
36
- display: block; /* For Firefox */
37
- content: attr(placeholder);
33
+ .react-native-live-markdown-input-multiline *[data-type='pre'] {
34
+ line-height: 1.3;
35
+ display: block;
36
+ width: fit-content;
37
+ max-width: 100%;
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ .react-native-live-markdown-input-multiline *[data-type='codeblock'] {
42
+ position: relative;
43
+ width: fit-content;
44
+ max-width: 100%;
45
+ box-sizing: border-box;
46
+ z-index: 2;
47
+ display: block;
48
+ }
49
+
50
+ .react-native-live-markdown-input-singleline *[data-type='code'] > *[data-type='emoji'],
51
+ .react-native-live-markdown-input-multiline *[data-type='code'] > *[data-type='emoji'] {
52
+ font-size: inherit !important;
53
+ line-height: inherit !important;
54
+ vertical-align: unset !important;
55
+ }
56
+
57
+ .react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='syntax']:has(+ *[data-type='pre']),
58
+ .react-native-live-markdown-input-multiline *[data-type='line'] *[data-type='pre'] + *[data-type='syntax'] {
59
+ display: block;
60
+ line-height: 1.3;
38
61
  }
39
62
 
40
63
  @keyframes react-native-live-markdown-spin {
@@ -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): 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
48
  }
49
+ return currentParentNode;
50
+ }
49
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;
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
+ }
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
  }
@@ -67,12 +85,12 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
67
85
  if (isTopComponent) {
68
86
  // When inputType is undefined, the first part of the replaced text is added as a text node.
69
87
  // Because of it, we need to prevent adding new lines in this case
70
- if (!inputType && node.nodeType === Node.TEXT_NODE) {
88
+ if (!isMultiline || (!inputType && node.nodeType === Node.TEXT_NODE)) {
71
89
  shouldAddNewline = false;
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,13 +99,44 @@ 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 (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
- // It prevents adding extra newlines when entering text
139
+ // It prevents adding extra newlines when entering text - and now only for multiline inputs
91
140
  text += '\n';
92
141
  }
93
142
  } else {
@@ -152,7 +152,8 @@ function addTextToElement(node: TreeNode, text: string, isMultiline = true) {
152
152
  }
153
153
  }
154
154
 
155
- if (index < lines.length - 1 || (index === 0 && line === '')) {
155
+ // Only add BR elements for multiline inputs or when there are actual line breaks
156
+ if (isMultiline && (index < lines.length - 1 || (index === 0 && line === ''))) {
156
157
  addBrElement(node);
157
158
  }
158
159
  });
@@ -228,7 +229,7 @@ function parseRangesToHTMLNodes(
228
229
  }
229
230
 
230
231
  if (line.markdownRanges.length === 0) {
231
- addTextToElement(currentParentNode, line.text);
232
+ addTextToElement(currentParentNode, line.text, isMultiline);
232
233
  }
233
234
 
234
235
  let wasBlockGenerated = false;
@@ -254,7 +255,7 @@ function parseRangesToHTMLNodes(
254
255
  // add text before the markdown range
255
256
  const textBeforeRange = line.text.substring(lastRangeEndIndex - line.start, range.start - line.start);
256
257
  if (textBeforeRange) {
257
- addTextToElement(currentParentNode, textBeforeRange);
258
+ addTextToElement(currentParentNode, textBeforeRange, isMultiline);
258
259
  }
259
260
 
260
261
  // create markdown span element
@@ -284,7 +285,7 @@ function parseRangesToHTMLNodes(
284
285
  while (currentParentNode.parentNode !== null && nextRangeStartIndex >= currentParentNode.start + currentParentNode.length) {
285
286
  const textAfterRange = line.text.substring(lastRangeEndIndex - line.start, currentParentNode.start - line.start + currentParentNode.length);
286
287
  if (textAfterRange) {
287
- addTextToElement(currentParentNode, textAfterRange);
288
+ addTextToElement(currentParentNode, textAfterRange, isMultiline);
288
289
  }
289
290
  lastRangeEndIndex = currentParentNode.start + currentParentNode.length;
290
291
  if (currentParentNode.parentNode.type !== 'root') {