@skbkontur/markdown 2.7.0-alpha.0 → 2.7.0-alpha.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skbkontur/markdown",
3
- "version": "2.7.0-alpha.0",
3
+ "version": "2.7.0-alpha.3",
4
4
  "packageManager": "yarn@4.13.0",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -67,7 +67,7 @@
67
67
  "@storybook/react": "8.2.8",
68
68
  "@storybook/react-vite": "8.2.8",
69
69
  "@storybook/test-runner": "0.19.1",
70
- "@swc/core": "1.3.50",
70
+ "@swc/core": "1.15.11",
71
71
  "@swc/jest": "0.2.26",
72
72
  "@swc/plugin-styled-components": "5.0.0",
73
73
  "@types/jest": "29.5.0",
@@ -121,4 +121,4 @@
121
121
  "resolutions": {
122
122
  "@types/react": "18.0.28"
123
123
  }
124
- }
124
+ }
@@ -82,5 +82,41 @@ kind('Markdown', () => {
82
82
  openedEmojiPicker,
83
83
  });
84
84
  });
85
+ test('autoContinueNumberedList', async (context) => {
86
+ const textarea = context.webdriver.locator('textarea').nth(0);
87
+ await textarea.click();
88
+ await textarea.type('1. Первый');
89
+ await textarea.press('Enter');
90
+ const autoContinueNumberedList = await context.takeScreenshot();
91
+ await context.matchImages({ autoContinueNumberedList });
92
+ });
93
+ test('autoContinueNumberedListWithTail', async (context) => {
94
+ const textarea = context.webdriver.locator('textarea').nth(0);
95
+ await textarea.click();
96
+ await textarea.type('1. Первый');
97
+ await textarea.press('Enter');
98
+ await textarea.type('Второй');
99
+ await textarea.press('Control+Home');
100
+ await textarea.press('End');
101
+ await textarea.press('Enter');
102
+ const autoContinueNumberedListWithTail = await context.takeScreenshot();
103
+ await context.matchImages({ autoContinueNumberedListWithTail });
104
+ });
105
+ test('autoContinueUnorderedList', async (context) => {
106
+ const textarea = context.webdriver.locator('textarea').nth(0);
107
+ await textarea.click();
108
+ await textarea.type('- Первый');
109
+ await textarea.press('Enter');
110
+ const autoContinueUnorderedList = await context.takeScreenshot();
111
+ await context.matchImages({ autoContinueUnorderedList });
112
+ });
113
+ test('autoContinueCheckboxList', async (context) => {
114
+ const textarea = context.webdriver.locator('textarea').nth(0);
115
+ await textarea.click();
116
+ await textarea.type('- [ ] Первый');
117
+ await textarea.press('Enter');
118
+ const autoContinueCheckboxList = await context.takeScreenshot();
119
+ await context.matchImages({ autoContinueCheckboxList });
120
+ });
85
121
  });
86
122
  });
@@ -84,27 +84,24 @@ export const usePasteFromClipboard = (textarea, uploadFileApi, downloadFileApi,
84
84
  const { uploadFile } = useFileLogic(uploadFileApi, downloadFileApi, fileApiUrl, textarea);
85
85
  const textareaNode = textarea?.node;
86
86
  useEffect(() => {
87
- if (downloadFileApi && uploadFileApi) {
88
- const handlePaste = (event) => {
89
- const files = event?.clipboardData?.files;
90
- const html = event?.clipboardData
91
- ?.getData('text/html')
92
- ?.replace(`<meta charset='utf-8'>`, '')
93
- ?.replace(`<meta charset="utf-8">`, '');
94
- if (files?.length) {
95
- event.preventDefault();
96
- void uploadFile(files[0]);
97
- return;
98
- }
99
- if (html) {
100
- getPastedHtml(html, event, textareaNode);
101
- return;
102
- }
103
- };
104
- if (textareaNode) {
105
- textareaNode.addEventListener('paste', handlePaste);
87
+ const handlePaste = (event) => {
88
+ const files = event?.clipboardData?.files;
89
+ const html = event?.clipboardData
90
+ ?.getData('text/html')
91
+ ?.replace(`<meta charset='utf-8'>`, '')
92
+ ?.replace(`<meta charset="utf-8">`, '');
93
+ if (downloadFileApi && uploadFileApi && files?.length) {
94
+ event.preventDefault();
95
+ void uploadFile(files[0]);
96
+ return;
106
97
  }
107
- return () => textareaNode?.removeEventListener('paste', handlePaste);
108
- }
98
+ if (html) {
99
+ getPastedHtml(html, event, textareaNode);
100
+ return;
101
+ }
102
+ };
103
+ if (textareaNode)
104
+ textareaNode.addEventListener('paste', handlePaste);
105
+ return () => textareaNode?.removeEventListener('paste', handlePaste);
109
106
  }, [downloadFileApi, textareaNode, uploadFile, uploadFileApi]);
110
107
  };
@@ -1,4 +1,20 @@
1
1
  import { onInsertText } from '../utils/onInsertText';
2
+ /**
3
+ * RegExp для парсинга markdown-like list item.
4
+ *
5
+ * Поддерживает:
6
+ * - unordered lists: "- item", "* item", "+ item";
7
+ * - ordered lists: "1. item", "1) item";
8
+ * - checkbox items: "- [ ] item", "- [x] item done", "- [X] item done".
9
+ *
10
+ * Группы:
11
+ * - spacesBeforeMarker: отступы перед маркером списка;
12
+ * - orderedListNumber: номер ordered-list элемента;
13
+ * - orderedListDelimiter: разделитель ordered-list: "." или ")";
14
+ * - unorderedListMarker: маркер unordered-list: "*", "-" или "+";
15
+ * - checkboxListMarker: checkbox-маркер: "[ ]", "[x]" или "[X]";
16
+ * - text: текст элемента списка, может быть пустым.
17
+ */
2
18
  const listItemRegExp = /^(?<spacesBeforeMarker> *)(?:(?<orderedListNumber>\d+)(?<orderedListDelimiter>[.)])|(?<unorderedListMarker>[*+-])(?: +(?<checkboxListMarker>\[[ xX]]))?)(?: +(?<text>.*)|$)$/;
3
19
  export function handleMarkdownListEnter(event, textareaNode) {
4
20
  const { selectionStart, selectionEnd, value } = textareaNode;
@@ -20,12 +36,24 @@ export function handleMarkdownListEnter(event, textareaNode) {
20
36
  const { spacesBeforeMarker, orderedListNumber, orderedListDelimiter, unorderedListMarker, checkboxListMarker, text, } = listLineMatch.groups;
21
37
  event.stopPropagation();
22
38
  event.preventDefault();
39
+ /* Если пункт списка пустой, удаляем маркер и завершаем список */
23
40
  if (!text?.trim()) {
24
41
  textareaNode.setSelectionRange(currentLineStartIndex, currentLineEndIndex);
25
42
  return document.execCommand('delete');
26
43
  }
27
- if (orderedListNumber && orderedListDelimiter)
28
- return onInsertText(`\n${spacesBeforeMarker}${Number(orderedListNumber) + 1}${orderedListDelimiter} `);
44
+ if (orderedListNumber && orderedListDelimiter) {
45
+ const nextOrderedListNumber = Number(orderedListNumber) + 1;
46
+ const newLine = `\n${spacesBeforeMarker}${nextOrderedListNumber}${orderedListDelimiter} `;
47
+ const { isChanged, listEndIndex, renumberedList } = getRenumberedListLines(value, currentLineEndIndex, spacesBeforeMarker, orderedListDelimiter, nextOrderedListNumber + 1);
48
+ if (isChanged) {
49
+ const currentLineTextAfterCursor = value.slice(selectionStart, currentLineEndIndex);
50
+ textareaNode.setSelectionRange(selectionStart, listEndIndex);
51
+ onInsertText(`${newLine}${currentLineTextAfterCursor}${renumberedList}`);
52
+ const cursorPosition = selectionStart + newLine.length;
53
+ return textareaNode.setSelectionRange(cursorPosition, cursorPosition);
54
+ }
55
+ return onInsertText(newLine);
56
+ }
29
57
  if (checkboxListMarker)
30
58
  return onInsertText(`\n${spacesBeforeMarker}${unorderedListMarker} ${checkboxListMarker} `);
31
59
  if (unorderedListMarker)
@@ -43,3 +71,32 @@ function getCurrentLineEndIndex(text, cursorPosition) {
43
71
  return text.length;
44
72
  return currentLineEndIndex;
45
73
  }
74
+ function getRenumberedListLines(value, currentLineEndIndex, spacesBeforeMarker, orderedListDelimiter, nextOrderedListNumber) {
75
+ let nextLineStartIndex = currentLineEndIndex + 1;
76
+ let currentOrderedListNumber = nextOrderedListNumber;
77
+ let isChanged = false;
78
+ let renumberedList = '';
79
+ let listEndIndex = currentLineEndIndex;
80
+ while (nextLineStartIndex < value.length) {
81
+ const nextLineEndIndex = getCurrentLineEndIndex(value, nextLineStartIndex);
82
+ const line = value.slice(nextLineStartIndex, nextLineEndIndex);
83
+ const listLineMatch = line.match(listItemRegExp);
84
+ const groups = listLineMatch?.groups;
85
+ if (!groups?.orderedListNumber ||
86
+ groups.spacesBeforeMarker !== spacesBeforeMarker ||
87
+ groups.orderedListDelimiter !== orderedListDelimiter)
88
+ break;
89
+ const renumberedLine = line.replace(`${spacesBeforeMarker}${groups.orderedListNumber}${orderedListDelimiter}`, `${spacesBeforeMarker}${currentOrderedListNumber}${orderedListDelimiter}`);
90
+ if (renumberedLine !== line)
91
+ isChanged = true;
92
+ renumberedList += `\n${renumberedLine}`;
93
+ listEndIndex = nextLineEndIndex;
94
+ nextLineStartIndex = nextLineEndIndex + 1;
95
+ currentOrderedListNumber += 1;
96
+ }
97
+ return {
98
+ isChanged,
99
+ listEndIndex,
100
+ renumberedList,
101
+ };
102
+ }