@cloud-ru/ds-markdown 1.0.0

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 (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +273 -0
  4. package/dist/cjs/components/Markdown/CodeBlock.d.ts +9 -0
  5. package/dist/cjs/components/Markdown/CodeBlock.js +58 -0
  6. package/dist/cjs/components/Markdown/Markdown.d.ts +2 -0
  7. package/dist/cjs/components/Markdown/Markdown.js +25 -0
  8. package/dist/cjs/components/Markdown/index.d.ts +1 -0
  9. package/dist/cjs/components/Markdown/index.js +17 -0
  10. package/dist/cjs/components/Markdown/styles.module.css +205 -0
  11. package/dist/cjs/components/MarkdownEditor/MarkdownEditor.d.ts +2 -0
  12. package/dist/cjs/components/MarkdownEditor/MarkdownEditor.js +176 -0
  13. package/dist/cjs/components/MarkdownEditor/index.d.ts +1 -0
  14. package/dist/cjs/components/MarkdownEditor/index.js +17 -0
  15. package/dist/cjs/components/MarkdownEditor/styles.module.css +273 -0
  16. package/dist/cjs/components/Toolbar/Toolbar.d.ts +9 -0
  17. package/dist/cjs/components/Toolbar/Toolbar.js +173 -0
  18. package/dist/cjs/components/Toolbar/constants.d.ts +13 -0
  19. package/dist/cjs/components/Toolbar/constants.js +40 -0
  20. package/dist/cjs/components/Toolbar/hooks/index.d.ts +2 -0
  21. package/dist/cjs/components/Toolbar/hooks/index.js +18 -0
  22. package/dist/cjs/components/Toolbar/hooks/useToolbarHotkeys.d.ts +21 -0
  23. package/dist/cjs/components/Toolbar/hooks/useToolbarHotkeys.js +59 -0
  24. package/dist/cjs/components/Toolbar/hooks/useToolbarOverflow.d.ts +14 -0
  25. package/dist/cjs/components/Toolbar/hooks/useToolbarOverflow.js +87 -0
  26. package/dist/cjs/components/Toolbar/index.d.ts +2 -0
  27. package/dist/cjs/components/Toolbar/index.js +20 -0
  28. package/dist/cjs/components/Toolbar/styles.module.css +17 -0
  29. package/dist/cjs/components/index.d.ts +2 -0
  30. package/dist/cjs/components/index.js +20 -0
  31. package/dist/cjs/constants.d.ts +65 -0
  32. package/dist/cjs/constants.js +67 -0
  33. package/dist/cjs/helperComponents/ButtonHeading/ButtonHeading.d.ts +6 -0
  34. package/dist/cjs/helperComponents/ButtonHeading/ButtonHeading.js +61 -0
  35. package/dist/cjs/helperComponents/ButtonHeading/index.d.ts +1 -0
  36. package/dist/cjs/helperComponents/ButtonHeading/index.js +17 -0
  37. package/dist/cjs/helperComponents/ButtonHeading/styles.module.css +3 -0
  38. package/dist/cjs/helperComponents/Buttons/Buttons.d.ts +10 -0
  39. package/dist/cjs/helperComponents/Buttons/Buttons.js +21 -0
  40. package/dist/cjs/helperComponents/Buttons/index.d.ts +1 -0
  41. package/dist/cjs/helperComponents/Buttons/index.js +17 -0
  42. package/dist/cjs/helperComponents/Buttons/styles.module.css +17 -0
  43. package/dist/cjs/helperComponents/Modals/CustomizeTableModal.d.ts +8 -0
  44. package/dist/cjs/helperComponents/Modals/CustomizeTableModal.js +39 -0
  45. package/dist/cjs/helperComponents/Modals/ImageModal.d.ts +7 -0
  46. package/dist/cjs/helperComponents/Modals/ImageModal.js +44 -0
  47. package/dist/cjs/helperComponents/Modals/LinkModal.d.ts +13 -0
  48. package/dist/cjs/helperComponents/Modals/LinkModal.js +42 -0
  49. package/dist/cjs/helperComponents/Modals/index.d.ts +3 -0
  50. package/dist/cjs/helperComponents/Modals/index.js +19 -0
  51. package/dist/cjs/helperComponents/Modals/styles.module.css +17 -0
  52. package/dist/cjs/helperComponents/PrivateButton/PrivateButton.d.ts +17 -0
  53. package/dist/cjs/helperComponents/PrivateButton/PrivateButton.js +27 -0
  54. package/dist/cjs/helperComponents/PrivateButton/index.d.ts +1 -0
  55. package/dist/cjs/helperComponents/PrivateButton/index.js +17 -0
  56. package/dist/cjs/helperComponents/PrivateButton/styles.module.css +140 -0
  57. package/dist/cjs/helperComponents/TableSelectItem/TableSelectItem.d.ts +11 -0
  58. package/dist/cjs/helperComponents/TableSelectItem/TableSelectItem.js +12 -0
  59. package/dist/cjs/helperComponents/TableSelectItem/index.d.ts +1 -0
  60. package/dist/cjs/helperComponents/TableSelectItem/index.js +17 -0
  61. package/dist/cjs/helperComponents/TableSelectItem/styles.module.css +101 -0
  62. package/dist/cjs/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.d.ts +4 -0
  63. package/dist/cjs/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.js +27 -0
  64. package/dist/cjs/helperComponents/TableSelectSettingsGrid/index.d.ts +1 -0
  65. package/dist/cjs/helperComponents/TableSelectSettingsGrid/index.js +17 -0
  66. package/dist/cjs/helperComponents/TableSelectSettingsGrid/styles.module.css +54 -0
  67. package/dist/cjs/helperComponents/index.d.ts +6 -0
  68. package/dist/cjs/helperComponents/index.js +22 -0
  69. package/dist/cjs/index.d.ts +4 -0
  70. package/dist/cjs/index.js +20 -0
  71. package/dist/cjs/locale/index.d.ts +199 -0
  72. package/dist/cjs/locale/index.js +104 -0
  73. package/dist/cjs/lowlight.d.ts +14 -0
  74. package/dist/cjs/lowlight.js +35 -0
  75. package/dist/cjs/mixins.css +0 -0
  76. package/dist/cjs/package.json +3 -0
  77. package/dist/cjs/rawMarkdownCommands.d.ts +16 -0
  78. package/dist/cjs/rawMarkdownCommands.js +145 -0
  79. package/dist/cjs/style.css +875 -0
  80. package/dist/cjs/styles/syntax.module.css +47 -0
  81. package/dist/cjs/toolbarApi.d.ts +33 -0
  82. package/dist/cjs/toolbarApi.js +100 -0
  83. package/dist/cjs/types.d.ts +52 -0
  84. package/dist/cjs/types.js +2 -0
  85. package/dist/esm/components/Markdown/CodeBlock.d.ts +9 -0
  86. package/dist/esm/components/Markdown/CodeBlock.js +52 -0
  87. package/dist/esm/components/Markdown/Markdown.d.ts +2 -0
  88. package/dist/esm/components/Markdown/Markdown.js +19 -0
  89. package/dist/esm/components/Markdown/index.d.ts +1 -0
  90. package/dist/esm/components/Markdown/index.js +1 -0
  91. package/dist/esm/components/Markdown/styles.module.css +205 -0
  92. package/dist/esm/components/MarkdownEditor/MarkdownEditor.d.ts +2 -0
  93. package/dist/esm/components/MarkdownEditor/MarkdownEditor.js +170 -0
  94. package/dist/esm/components/MarkdownEditor/index.d.ts +1 -0
  95. package/dist/esm/components/MarkdownEditor/index.js +1 -0
  96. package/dist/esm/components/MarkdownEditor/styles.module.css +273 -0
  97. package/dist/esm/components/Toolbar/Toolbar.d.ts +9 -0
  98. package/dist/esm/components/Toolbar/Toolbar.js +167 -0
  99. package/dist/esm/components/Toolbar/constants.d.ts +13 -0
  100. package/dist/esm/components/Toolbar/constants.js +37 -0
  101. package/dist/esm/components/Toolbar/hooks/index.d.ts +2 -0
  102. package/dist/esm/components/Toolbar/hooks/index.js +2 -0
  103. package/dist/esm/components/Toolbar/hooks/useToolbarHotkeys.d.ts +21 -0
  104. package/dist/esm/components/Toolbar/hooks/useToolbarHotkeys.js +56 -0
  105. package/dist/esm/components/Toolbar/hooks/useToolbarOverflow.d.ts +14 -0
  106. package/dist/esm/components/Toolbar/hooks/useToolbarOverflow.js +84 -0
  107. package/dist/esm/components/Toolbar/index.d.ts +2 -0
  108. package/dist/esm/components/Toolbar/index.js +2 -0
  109. package/dist/esm/components/Toolbar/styles.module.css +17 -0
  110. package/dist/esm/components/index.d.ts +2 -0
  111. package/dist/esm/components/index.js +4 -0
  112. package/dist/esm/constants.d.ts +65 -0
  113. package/dist/esm/constants.js +61 -0
  114. package/dist/esm/helperComponents/ButtonHeading/ButtonHeading.d.ts +6 -0
  115. package/dist/esm/helperComponents/ButtonHeading/ButtonHeading.js +55 -0
  116. package/dist/esm/helperComponents/ButtonHeading/index.d.ts +1 -0
  117. package/dist/esm/helperComponents/ButtonHeading/index.js +1 -0
  118. package/dist/esm/helperComponents/ButtonHeading/styles.module.css +3 -0
  119. package/dist/esm/helperComponents/Buttons/Buttons.d.ts +10 -0
  120. package/dist/esm/helperComponents/Buttons/Buttons.js +13 -0
  121. package/dist/esm/helperComponents/Buttons/index.d.ts +1 -0
  122. package/dist/esm/helperComponents/Buttons/index.js +1 -0
  123. package/dist/esm/helperComponents/Buttons/styles.module.css +17 -0
  124. package/dist/esm/helperComponents/Modals/CustomizeTableModal.d.ts +8 -0
  125. package/dist/esm/helperComponents/Modals/CustomizeTableModal.js +33 -0
  126. package/dist/esm/helperComponents/Modals/ImageModal.d.ts +7 -0
  127. package/dist/esm/helperComponents/Modals/ImageModal.js +38 -0
  128. package/dist/esm/helperComponents/Modals/LinkModal.d.ts +13 -0
  129. package/dist/esm/helperComponents/Modals/LinkModal.js +36 -0
  130. package/dist/esm/helperComponents/Modals/index.d.ts +3 -0
  131. package/dist/esm/helperComponents/Modals/index.js +3 -0
  132. package/dist/esm/helperComponents/Modals/styles.module.css +17 -0
  133. package/dist/esm/helperComponents/PrivateButton/PrivateButton.d.ts +17 -0
  134. package/dist/esm/helperComponents/PrivateButton/PrivateButton.js +21 -0
  135. package/dist/esm/helperComponents/PrivateButton/index.d.ts +1 -0
  136. package/dist/esm/helperComponents/PrivateButton/index.js +1 -0
  137. package/dist/esm/helperComponents/PrivateButton/styles.module.css +140 -0
  138. package/dist/esm/helperComponents/TableSelectItem/TableSelectItem.d.ts +11 -0
  139. package/dist/esm/helperComponents/TableSelectItem/TableSelectItem.js +6 -0
  140. package/dist/esm/helperComponents/TableSelectItem/index.d.ts +1 -0
  141. package/dist/esm/helperComponents/TableSelectItem/index.js +1 -0
  142. package/dist/esm/helperComponents/TableSelectItem/styles.module.css +101 -0
  143. package/dist/esm/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.d.ts +4 -0
  144. package/dist/esm/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.js +21 -0
  145. package/dist/esm/helperComponents/TableSelectSettingsGrid/index.d.ts +1 -0
  146. package/dist/esm/helperComponents/TableSelectSettingsGrid/index.js +1 -0
  147. package/dist/esm/helperComponents/TableSelectSettingsGrid/styles.module.css +54 -0
  148. package/dist/esm/helperComponents/index.d.ts +6 -0
  149. package/dist/esm/helperComponents/index.js +6 -0
  150. package/dist/esm/index.d.ts +4 -0
  151. package/dist/esm/index.js +4 -0
  152. package/dist/esm/locale/index.d.ts +199 -0
  153. package/dist/esm/locale/index.js +101 -0
  154. package/dist/esm/lowlight.d.ts +14 -0
  155. package/dist/esm/lowlight.js +29 -0
  156. package/dist/esm/mixins.css +0 -0
  157. package/dist/esm/rawMarkdownCommands.d.ts +16 -0
  158. package/dist/esm/rawMarkdownCommands.js +135 -0
  159. package/dist/esm/style.css +875 -0
  160. package/dist/esm/styles/syntax.module.css +47 -0
  161. package/dist/esm/toolbarApi.d.ts +33 -0
  162. package/dist/esm/toolbarApi.js +96 -0
  163. package/dist/esm/types.d.ts +52 -0
  164. package/dist/esm/types.js +1 -0
  165. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  166. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  167. package/package.json +84 -0
  168. package/src/components/Markdown/CodeBlock.tsx +96 -0
  169. package/src/components/Markdown/Markdown.tsx +47 -0
  170. package/src/components/Markdown/index.ts +1 -0
  171. package/src/components/Markdown/styles.module.scss +105 -0
  172. package/src/components/MarkdownEditor/MarkdownEditor.tsx +322 -0
  173. package/src/components/MarkdownEditor/index.ts +1 -0
  174. package/src/components/MarkdownEditor/styles.module.scss +201 -0
  175. package/src/components/Toolbar/Toolbar.tsx +292 -0
  176. package/src/components/Toolbar/constants.tsx +63 -0
  177. package/src/components/Toolbar/hooks/index.ts +2 -0
  178. package/src/components/Toolbar/hooks/useToolbarHotkeys.ts +74 -0
  179. package/src/components/Toolbar/hooks/useToolbarOverflow.ts +102 -0
  180. package/src/components/Toolbar/index.ts +2 -0
  181. package/src/components/Toolbar/styles.module.scss +29 -0
  182. package/src/components/index.ts +4 -0
  183. package/src/constants.ts +67 -0
  184. package/src/helperComponents/ButtonHeading/ButtonHeading.tsx +100 -0
  185. package/src/helperComponents/ButtonHeading/index.ts +1 -0
  186. package/src/helperComponents/ButtonHeading/styles.module.scss +5 -0
  187. package/src/helperComponents/Buttons/Buttons.tsx +30 -0
  188. package/src/helperComponents/Buttons/index.ts +1 -0
  189. package/src/helperComponents/Buttons/styles.module.scss +26 -0
  190. package/src/helperComponents/Modals/CustomizeTableModal.tsx +82 -0
  191. package/src/helperComponents/Modals/ImageModal.tsx +84 -0
  192. package/src/helperComponents/Modals/LinkModal.tsx +92 -0
  193. package/src/helperComponents/Modals/index.ts +3 -0
  194. package/src/helperComponents/Modals/styles.module.scss +21 -0
  195. package/src/helperComponents/PrivateButton/PrivateButton.tsx +88 -0
  196. package/src/helperComponents/PrivateButton/index.ts +1 -0
  197. package/src/helperComponents/PrivateButton/styles.module.scss +77 -0
  198. package/src/helperComponents/TableSelectItem/TableSelectItem.tsx +43 -0
  199. package/src/helperComponents/TableSelectItem/index.ts +1 -0
  200. package/src/helperComponents/TableSelectItem/styles.module.scss +26 -0
  201. package/src/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.tsx +48 -0
  202. package/src/helperComponents/TableSelectSettingsGrid/index.ts +1 -0
  203. package/src/helperComponents/TableSelectSettingsGrid/styles.module.scss +65 -0
  204. package/src/helperComponents/index.ts +6 -0
  205. package/src/index.ts +4 -0
  206. package/src/locale/index.ts +105 -0
  207. package/src/lowlight.ts +30 -0
  208. package/src/mixins.scss +123 -0
  209. package/src/rawMarkdownCommands.ts +179 -0
  210. package/src/styles/syntax.module.scss +54 -0
  211. package/src/toolbarApi.ts +152 -0
  212. package/src/types.ts +56 -0
@@ -0,0 +1,179 @@
1
+ import { TOOLBAR_ITEM } from './constants';
2
+ import { HeadingLevel, ToolbarItemId } from './types';
3
+
4
+ // Чистые трансформации markdown-исходника для raw-режима редактора (textarea).
5
+ // Каждая возвращает новый текст и диапазон выделения, который надо восстановить.
6
+
7
+ export type RawEdit = { text: string; selStart: number; selEnd: number };
8
+
9
+ // Инлайн-обёртки (toggle): **bold**, *italic*, ~~strike~~, `code`.
10
+ const INLINE_MARKER: Partial<Record<ToolbarItemId, string>> = {
11
+ [TOOLBAR_ITEM.Bold]: '**',
12
+ [TOOLBAR_ITEM.Italic]: '*',
13
+ [TOOLBAR_ITEM.Strikethrough]: '~~',
14
+ [TOOLBAR_ITEM.InlineCode]: '`',
15
+ };
16
+
17
+ // Любой ведущий блок-маркер строки (heading/quote/bullet/ordered) — снимается при смене типа блока.
18
+ const BLOCK_MARKER_RE = /^(#{1,6}\s+|>\s+|[-*+]\s+|\d+\.\s+)/;
19
+
20
+ function getLineBounds(value: string, start: number, end: number): { lineStart: number; lineEnd: number } {
21
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
22
+ let lineEnd = value.indexOf('\n', end);
23
+ if (lineEnd === -1) lineEnd = value.length;
24
+ return { lineStart, lineEnd };
25
+ }
26
+
27
+ function wrapInline(value: string, start: number, end: number, marker: string): RawEdit {
28
+ const selected = value.slice(start, end);
29
+ const before = value.slice(Math.max(0, start - marker.length), start);
30
+ const after = value.slice(end, end + marker.length);
31
+
32
+ // Уже обёрнуто — снимаем обёртку (toggle off).
33
+ if (start >= marker.length && before === marker && after === marker) {
34
+ const text = value.slice(0, start - marker.length) + selected + value.slice(end + marker.length);
35
+ return { text, selStart: start - marker.length, selEnd: end - marker.length };
36
+ }
37
+
38
+ const text = value.slice(0, start) + marker + selected + marker + value.slice(end);
39
+ return { text, selStart: start + marker.length, selEnd: end + marker.length };
40
+ }
41
+
42
+ function transformLines(value: string, start: number, end: number, transform: (lines: string[]) => string[]): RawEdit {
43
+ const { lineStart, lineEnd } = getLineBounds(value, start, end);
44
+ const block = value.slice(lineStart, lineEnd);
45
+ const next = transform(block.split('\n')).join('\n');
46
+ const text = value.slice(0, lineStart) + next + value.slice(lineEnd);
47
+ return { text, selStart: lineStart, selEnd: lineStart + next.length };
48
+ }
49
+
50
+ function toggleLinePrefix(value: string, start: number, end: number, prefix: string): RawEdit {
51
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ const ownRe = new RegExp(`^${escaped}`);
53
+ return transformLines(value, start, end, lines => {
54
+ const allPrefixed = lines.every(line => ownRe.test(line));
55
+ if (allPrefixed) return lines.map(line => line.replace(ownRe, ''));
56
+ return lines.map(line => prefix + line.replace(BLOCK_MARKER_RE, ''));
57
+ });
58
+ }
59
+
60
+ function toggleOrderedList(value: string, start: number, end: number): RawEdit {
61
+ const orderedRe = /^\d+\.\s+/;
62
+ return transformLines(value, start, end, lines => {
63
+ const allPrefixed = lines.every(line => orderedRe.test(line));
64
+ if (allPrefixed) return lines.map(line => line.replace(orderedRe, ''));
65
+ return lines.map((line, index) => `${index + 1}. ${line.replace(BLOCK_MARKER_RE, '')}`);
66
+ });
67
+ }
68
+
69
+ function setHeading(value: string, start: number, end: number, level: HeadingLevel): RawEdit {
70
+ const marker = `${'#'.repeat(level)} `;
71
+ const sameLevelRe = new RegExp(`^#{${level}}\\s+`);
72
+ return transformLines(value, start, end, lines => {
73
+ const allSame = lines.every(line => sameLevelRe.test(line));
74
+ // Повторный выбор того же уровня снимает заголовок (toggle).
75
+ if (allSame) return lines.map(line => line.replace(sameLevelRe, ''));
76
+ return lines.map(line => marker + line.replace(BLOCK_MARKER_RE, ''));
77
+ });
78
+ }
79
+
80
+ function setParagraph(value: string, start: number, end: number): RawEdit {
81
+ return transformLines(value, start, end, lines => lines.map(line => line.replace(/^#{1,6}\s+/, '')));
82
+ }
83
+
84
+ function fenceBlock(value: string, start: number, end: number): RawEdit {
85
+ const { lineStart, lineEnd } = getLineBounds(value, start, end);
86
+ const block = value.slice(lineStart, lineEnd);
87
+ const fenced = `\`\`\`\n${block}\n\`\`\``;
88
+ const text = value.slice(0, lineStart) + fenced + value.slice(lineEnd);
89
+ // Выделяем тело между fence'ами.
90
+ const innerStart = lineStart + 4;
91
+ return { text, selStart: innerStart, selEnd: innerStart + block.length };
92
+ }
93
+
94
+ function replaceSelection(
95
+ value: string,
96
+ start: number,
97
+ end: number,
98
+ insert: string,
99
+ selStart: number,
100
+ selEnd: number,
101
+ ): RawEdit {
102
+ const text = value.slice(0, start) + insert + value.slice(end);
103
+ return { text, selStart, selEnd };
104
+ }
105
+
106
+ export function buildMarkdownTable(rows: number, cols: number, columnLabel: (index: number) => string): string {
107
+ const header = `| ${Array.from({ length: cols }, (_, i) => columnLabel(i + 1)).join(' | ')} |`;
108
+ const divider = `| ${Array.from({ length: cols }, () => '---').join(' | ')} |`;
109
+ const bodyRow = `| ${Array.from({ length: cols }, () => ' ').join(' | ')} |`;
110
+ const body = Array.from({ length: rows }, () => bodyRow);
111
+ return [header, divider, ...body].join('\n');
112
+ }
113
+
114
+ /** Инлайн-toggle (bold/italic/strike/inline-code) или `null`, если id не инлайновый. */
115
+ export function applyInlineToggle(id: ToolbarItemId, value: string, start: number, end: number): RawEdit | null {
116
+ const marker = INLINE_MARKER[id];
117
+ return marker ? wrapInline(value, start, end, marker) : null;
118
+ }
119
+
120
+ /** Блочный toggle (bullet/ordered/quote/block-code) или `null`, если id не блочный. */
121
+ export function applyBlockToggle(id: ToolbarItemId, value: string, start: number, end: number): RawEdit | null {
122
+ switch (id) {
123
+ case TOOLBAR_ITEM.BulletList:
124
+ return toggleLinePrefix(value, start, end, '- ');
125
+ case TOOLBAR_ITEM.OrderedList:
126
+ return toggleOrderedList(value, start, end);
127
+ case TOOLBAR_ITEM.BlockQuote:
128
+ return toggleLinePrefix(value, start, end, '> ');
129
+ case TOOLBAR_ITEM.BlockCode:
130
+ return fenceBlock(value, start, end);
131
+ default:
132
+ return null;
133
+ }
134
+ }
135
+
136
+ export function applyHeading(value: string, start: number, end: number, level: HeadingLevel): RawEdit {
137
+ return setHeading(value, start, end, level);
138
+ }
139
+
140
+ export function applyParagraph(value: string, start: number, end: number): RawEdit {
141
+ return setParagraph(value, start, end);
142
+ }
143
+
144
+ export function applyLink(
145
+ value: string,
146
+ start: number,
147
+ end: number,
148
+ href: string,
149
+ title: string,
150
+ defaultText: string,
151
+ ): RawEdit {
152
+ const selected = title || value.slice(start, end) || defaultText;
153
+ const insert = `[${selected}](${href})`;
154
+ // Выделяем подпись ссылки внутри скобок.
155
+ return replaceSelection(value, start, end, insert, start + 1, start + 1 + selected.length);
156
+ }
157
+
158
+ export function applyImage(value: string, start: number, end: number, src: string, alt: string): RawEdit {
159
+ const insert = `![${alt}](${src})`;
160
+ const cursor = start + insert.length;
161
+ return replaceSelection(value, start, end, insert, cursor, cursor);
162
+ }
163
+
164
+ export function applyTable(
165
+ value: string,
166
+ start: number,
167
+ end: number,
168
+ rows: number,
169
+ cols: number,
170
+ columnLabel: (index: number) => string,
171
+ ): RawEdit {
172
+ const table = buildMarkdownTable(rows, cols, columnLabel);
173
+ // Таблица — отдельный блок: добавляем переводы строк по краям, если рядом непустой текст.
174
+ const prefix = start > 0 && value[start - 1] !== '\n' ? '\n\n' : '';
175
+ const suffix = end < value.length && value[end] !== '\n' ? '\n\n' : '';
176
+ const insert = prefix + table + suffix;
177
+ const cursor = start + insert.length;
178
+ return replaceSelection(value, start, end, insert, cursor, cursor);
179
+ }
@@ -0,0 +1,54 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+
3
+ // Подсветка кода поверх highlight.js / lowlight.
4
+ // `@cloud-ru/ds-figma-variables` не содержит syntax-токенов, поэтому маппим на семантические theme-цвета.
5
+ // Светлая тема — по умолчанию. Тёмная — через :global([data-theme='dark']).
6
+
7
+ .syntax {
8
+ :global(.hljs-comment),
9
+ :global(.hljs-quote) {
10
+ font-style: italic;
11
+ color: base.$sn-theme-color-available-version-textTertiary;
12
+ }
13
+
14
+ :global(.hljs-keyword),
15
+ :global(.hljs-selector-tag),
16
+ :global(.hljs-built_in),
17
+ :global(.hljs-name),
18
+ :global(.hljs-tag) {
19
+ color: base.$sn-theme-color-blue-text;
20
+ }
21
+
22
+ :global(.hljs-string),
23
+ :global(.hljs-title),
24
+ :global(.hljs-section),
25
+ :global(.hljs-attribute),
26
+ :global(.hljs-literal),
27
+ :global(.hljs-template-tag),
28
+ :global(.hljs-template-variable),
29
+ :global(.hljs-type),
30
+ :global(.hljs-addition) {
31
+ color: base.$sn-theme-color-green-text;
32
+ }
33
+
34
+ :global(.hljs-number),
35
+ :global(.hljs-symbol),
36
+ :global(.hljs-bullet),
37
+ :global(.hljs-link),
38
+ :global(.hljs-meta),
39
+ :global(.hljs-selector-id),
40
+ :global(.hljs-selector-class) {
41
+ color: base.$sn-theme-color-blue-accent;
42
+ }
43
+
44
+ :global(.hljs-deletion),
45
+ :global(.hljs-regexp),
46
+ :global(.hljs-variable),
47
+ :global(.hljs-template-variable) {
48
+ color: base.$sn-theme-color-red-text;
49
+ }
50
+
51
+ :global(.hljs-emphasis) { font-style: italic; }
52
+ :global(.hljs-strong) { font-weight: 600; }
53
+ :global(.hljs-link) { text-decoration: underline; }
54
+ }
@@ -0,0 +1,152 @@
1
+ import { Editor } from '@tiptap/react';
2
+ import { RefObject } from 'react';
3
+
4
+ import { TOOLBAR_ITEM } from './constants';
5
+ import { LinkProps } from './helperComponents';
6
+ import {
7
+ applyBlockToggle,
8
+ applyHeading,
9
+ applyImage,
10
+ applyInlineToggle,
11
+ applyLink,
12
+ applyParagraph,
13
+ applyTable,
14
+ RawEdit,
15
+ } from './rawMarkdownCommands';
16
+ import { HeadingLevel, ToolbarItemId } from './types';
17
+
18
+ // Бэкенд-независимый интерфейс тулбара: в preview-режиме его реализует TipTap (WYSIWYG),
19
+ // в raw-режиме — трансформации markdown-исходника в textarea.
20
+ export type ToolbarApi = {
21
+ /** Режим бэкенда: `wysiwyg` (TipTap, preview) либо `raw` (markdown-исходник в textarea). */
22
+ mode: 'wysiwyg' | 'raw';
23
+ focus(): void;
24
+ /** Активна ли кнопка (checked-стейт). В raw-режиме — всегда false. */
25
+ isActive(id: ToolbarItemId): boolean;
26
+ /** Активен ли заголовок уровня `level` на текущей строке. В raw-режиме — false. */
27
+ isHeadingActive(level: HeadingLevel): boolean;
28
+ /** Toggle инлайн/блочной кнопки (bold…block-code). */
29
+ toggle(id: ToolbarItemId): void;
30
+ toggleHeading(level: HeadingLevel): void;
31
+ setParagraph(): void;
32
+ getLinkHref(): string | undefined;
33
+ getLinkTitle(): string | undefined;
34
+ setLink(props: LinkProps): void;
35
+ insertImage(src: string, alt: string): void;
36
+ insertTable(rows: number, cols: number): void;
37
+ /** Подписка на изменение selection/контента — для перерисовки checked-стейтов. */
38
+ subscribe(callback: () => void): () => void;
39
+ };
40
+
41
+ const WYSIWYG_MARK: Partial<Record<ToolbarItemId, string>> = {
42
+ [TOOLBAR_ITEM.Bold]: 'bold',
43
+ [TOOLBAR_ITEM.Italic]: 'italic',
44
+ [TOOLBAR_ITEM.Strikethrough]: 'strike',
45
+ [TOOLBAR_ITEM.InlineCode]: 'code',
46
+ [TOOLBAR_ITEM.BulletList]: 'bulletList',
47
+ [TOOLBAR_ITEM.OrderedList]: 'orderedList',
48
+ [TOOLBAR_ITEM.BlockQuote]: 'blockquote',
49
+ [TOOLBAR_ITEM.BlockCode]: 'codeBlock',
50
+ };
51
+
52
+ export function createWysiwygApi(editor: Editor): ToolbarApi {
53
+ const toggleCommand: Partial<Record<ToolbarItemId, () => void>> = {
54
+ [TOOLBAR_ITEM.Bold]: () => editor.chain().focus().toggleBold().run(),
55
+ [TOOLBAR_ITEM.Italic]: () => editor.chain().focus().toggleItalic().run(),
56
+ [TOOLBAR_ITEM.Strikethrough]: () => editor.chain().focus().toggleStrike().run(),
57
+ [TOOLBAR_ITEM.InlineCode]: () => editor.chain().focus().toggleCode().run(),
58
+ [TOOLBAR_ITEM.BulletList]: () => editor.chain().focus().toggleBulletList().run(),
59
+ [TOOLBAR_ITEM.OrderedList]: () => editor.chain().focus().toggleOrderedList().run(),
60
+ [TOOLBAR_ITEM.BlockQuote]: () => editor.chain().focus().toggleBlockquote().run(),
61
+ [TOOLBAR_ITEM.BlockCode]: () => editor.chain().focus().toggleCodeBlock().run(),
62
+ };
63
+
64
+ return {
65
+ mode: 'wysiwyg',
66
+ focus: () => editor.chain().focus().run(),
67
+ isActive: id => {
68
+ const mark = WYSIWYG_MARK[id];
69
+ return mark ? editor.isActive(mark) : false;
70
+ },
71
+ isHeadingActive: level => editor.isActive('heading', { level }),
72
+ toggle: id => toggleCommand[id]?.(),
73
+ toggleHeading: level => editor.chain().focus().toggleHeading({ level }).run(),
74
+ setParagraph: () => editor.chain().focus().setParagraph().run(),
75
+ getLinkHref: () => editor.getAttributes('link').href as string | undefined,
76
+ // Видимый текст ссылки = выделенный текст (для префилла поля «текст ссылки»).
77
+ getLinkTitle: () => {
78
+ const { from, to } = editor.state.selection;
79
+ return from === to ? undefined : editor.state.doc.textBetween(from, to);
80
+ },
81
+ setLink: ({ href, title }) => {
82
+ if (!href) {
83
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
84
+ return;
85
+ }
86
+
87
+ const chain = editor.chain().focus().extendMarkRange('link');
88
+
89
+ // Нет выделения, но задан текст — вставляем текст-ссылку. Иначе оборачиваем выделение.
90
+ if (editor.state.selection.empty && title) {
91
+ chain.insertContent({ type: 'text', text: title, marks: [{ type: 'link', attrs: { href } }] }).run();
92
+ } else {
93
+ chain.setLink({ href }).run();
94
+ }
95
+ },
96
+ insertImage: (src, alt) => editor.chain().focus().setImage({ src, alt }).run(),
97
+ insertTable: (rows, cols) => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(),
98
+ subscribe: callback => {
99
+ editor.on('selectionUpdate', callback);
100
+ editor.on('transaction', callback);
101
+
102
+ return () => {
103
+ editor.off('selectionUpdate', callback);
104
+ editor.off('transaction', callback);
105
+ };
106
+ },
107
+ };
108
+ }
109
+
110
+ // Локализованные строки, которые raw-команды вставляют в markdown-исходник.
111
+ export type RawApiLabels = {
112
+ /** Текст-заглушка ссылки, когда выделения нет. */
113
+ linkText: string;
114
+ /** Подпись колонки таблицы по 1-based индексу. */
115
+ tableColumn(index: number): string;
116
+ };
117
+
118
+ export function createRawApi(
119
+ textareaRef: RefObject<HTMLTextAreaElement | null>,
120
+ commit: (edit: RawEdit) => void,
121
+ labels: RawApiLabels,
122
+ ): ToolbarApi {
123
+ const run = (compute: (value: string, start: number, end: number) => RawEdit | null) => {
124
+ const el = textareaRef.current;
125
+ if (!el) return;
126
+ const edit = compute(el.value, el.selectionStart, el.selectionEnd);
127
+ if (edit) commit(edit);
128
+ };
129
+
130
+ return {
131
+ mode: 'raw',
132
+ focus: () => textareaRef.current?.focus(),
133
+ // В raw-режиме разбор разметки под курсором не делаем — кнопки без checked-стейта.
134
+ isActive: () => false,
135
+ isHeadingActive: () => false,
136
+ toggle: id =>
137
+ run((value, start, end) => applyInlineToggle(id, value, start, end) ?? applyBlockToggle(id, value, start, end)),
138
+ toggleHeading: level => run((value, start, end) => applyHeading(value, start, end, level)),
139
+ setParagraph: () => run((value, start, end) => applyParagraph(value, start, end)),
140
+ getLinkHref: () => undefined,
141
+ getLinkTitle: () => undefined,
142
+ // В raw-режиме ссылка вставляется как сырой markdown-шаблон `[текст](href)` прямо в textarea
143
+ // (без модалки). href может быть пустым — пользователь дописывает URL в исходнике.
144
+ setLink: ({ href, title }) => {
145
+ run((value, start, end) => applyLink(value, start, end, href ?? '', title ?? '', labels.linkText));
146
+ },
147
+ insertImage: (src, alt) => run((value, start, end) => applyImage(value, start, end, src, alt)),
148
+ insertTable: (rows, cols) =>
149
+ run((value, start, end) => applyTable(value, start, end, rows, cols, labels.tableColumn)),
150
+ subscribe: () => () => {},
151
+ };
152
+ }
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { ValueOf, WithSupportProps } from '@cloud-ru/ds-utils';
2
+ import { Components } from 'react-markdown';
3
+ import { PluggableList } from 'unified';
4
+
5
+ import { HEADING_LEVEL, TOOLBAR_ITEM } from './constants';
6
+
7
+ export type ToolbarItemId = ValueOf<typeof TOOLBAR_ITEM>;
8
+ export type HeadingLevel = ValueOf<typeof HEADING_LEVEL>;
9
+
10
+ export type MarkdownProps = WithSupportProps<{
11
+ /** Markdown-исходник */
12
+ value: string;
13
+ /** Доп. remark-плагины */
14
+ remarkPlugins?: PluggableList;
15
+ /** Доп. rehype-плагины */
16
+ rehypePlugins?: PluggableList;
17
+ /** Override рендера элементов */
18
+ components?: Components;
19
+ /** По умолчанию true — сырой HTML вырезается */
20
+ skipHtml?: boolean;
21
+ /** Колбэк кнопки Copy на code-блоке */
22
+ onCodeCopyClick?(code: string): void;
23
+ /** CSS-класс корневого элемента. */
24
+ className?: string;
25
+ }>;
26
+
27
+ export type MarkdownEditorProps = WithSupportProps<{
28
+ /** Controlled markdown */
29
+ value?: string;
30
+ /** Uncontrolled */
31
+ defaultValue?: string;
32
+ /** Колбэк изменения markdown. Вызывается с дебаунсом `ON_CHANGE_DEBOUNCE_MS` после остановки ввода. */
33
+ onChange?(markdown: string): void;
34
+ /** Controlled preview-режим: показывает форматированный WYSIWYG с активным тулбаром. Выключен — редактирование «сырого» markdown-текста в textarea */
35
+ preview?: boolean;
36
+ /** Uncontrolled */
37
+ defaultPreview?: boolean;
38
+ /** Колбэк переключения Preview-тогла в шапке */
39
+ onPreviewChange?(preview: boolean): void;
40
+ /** Подпись в шапке справа. По умолчанию — `Markdown field`. `false` — без подписи. */
41
+ label?: string | false;
42
+ /** Текст тогла Preview в шапке. По умолчанию `Preview`. */
43
+ previewLabel?: string;
44
+ /** Скрыть шапку с тоглом Preview и подписью */
45
+ hideHeader?: boolean;
46
+ /** Placeholder пустого редактора. */
47
+ placeholder?: string;
48
+ /** Нативная проверка орфографии (`spellcheck`). По умолчанию `true`. */
49
+ spellCheck?: boolean;
50
+ /** Может ли пользователь менять размеры поля перетаскиванием уголка. По умолчанию `true`. */
51
+ resizable?: boolean;
52
+ /** Какие кнопки тулбара показать. false — без тулбара */
53
+ toolbar?: false | ToolbarItemId[];
54
+ /** CSS-класс корневого элемента. */
55
+ className?: string;
56
+ }>;