@cyberyozh/text-editor 0.1.1

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 (209) hide show
  1. package/README.md +102 -0
  2. package/dist/editor.css +2069 -0
  3. package/dist/editor.css.map +1 -0
  4. package/dist/editor.d.mts +45 -0
  5. package/dist/editor.d.ts +45 -0
  6. package/dist/editor.js +6155 -0
  7. package/dist/editor.js.map +1 -0
  8. package/dist/editor.mjs +6118 -0
  9. package/dist/editor.mjs.map +1 -0
  10. package/dist/render/client.d.mts +24 -0
  11. package/dist/render/client.d.ts +24 -0
  12. package/dist/render/client.js +98 -0
  13. package/dist/render/client.js.map +1 -0
  14. package/dist/render/client.mjs +74 -0
  15. package/dist/render/client.mjs.map +1 -0
  16. package/dist/render.d.mts +58 -0
  17. package/dist/render.d.ts +58 -0
  18. package/dist/render.js +2521 -0
  19. package/dist/render.js.map +1 -0
  20. package/dist/render.mjs +2484 -0
  21. package/dist/render.mjs.map +1 -0
  22. package/dist/styles/index.css +2068 -0
  23. package/package.json +92 -0
  24. package/src/editor/commands/items.ts +374 -0
  25. package/src/editor/commands/suggestion.tsx +146 -0
  26. package/src/editor/components/button/button.css +56 -0
  27. package/src/editor/components/button/button.tsx +35 -0
  28. package/src/editor/components/button/index.ts +1 -0
  29. package/src/editor/components/common-modal/common-modal.css +85 -0
  30. package/src/editor/components/common-modal/common-modal.tsx +58 -0
  31. package/src/editor/components/common-modal/index.ts +1 -0
  32. package/src/editor/components/index.ts +3 -0
  33. package/src/editor/components/input/index.ts +1 -0
  34. package/src/editor/components/input/input.css +43 -0
  35. package/src/editor/components/input/input.tsx +22 -0
  36. package/src/editor/context/dialog-command.tsx +47 -0
  37. package/src/editor/context/index.ts +2 -0
  38. package/src/editor/context/translations-context.tsx +28 -0
  39. package/src/editor/controls/bubble-menu-content.tsx +394 -0
  40. package/src/editor/controls/callout-menu.tsx +169 -0
  41. package/src/editor/controls/character-count.tsx +36 -0
  42. package/src/editor/controls/commands-list.tsx +117 -0
  43. package/src/editor/controls/drag-handle.tsx +98 -0
  44. package/src/editor/controls/emoji-picker.tsx +62 -0
  45. package/src/editor/controls/index.ts +5 -0
  46. package/src/editor/controls/table-menu.tsx +322 -0
  47. package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.css +115 -0
  48. package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.tsx +116 -0
  49. package/src/editor/dialogs/add-katex-dialog/index.ts +1 -0
  50. package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +105 -0
  51. package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +177 -0
  52. package/src/editor/dialogs/add-link-dialog/index.ts +1 -0
  53. package/src/editor/dialogs/add-video-dialog/add-video-dialog.css +146 -0
  54. package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +157 -0
  55. package/src/editor/dialogs/add-video-dialog/index.ts +1 -0
  56. package/src/editor/dialogs/image-dialog/image-dialog.css +101 -0
  57. package/src/editor/dialogs/image-dialog/image-dialog.tsx +276 -0
  58. package/src/editor/dialogs/image-dialog/index.ts +1 -0
  59. package/src/editor/dialogs/index.ts +4 -0
  60. package/src/editor/editor.tsx +247 -0
  61. package/src/editor/handlers/create-image-upload-handler.ts +24 -0
  62. package/src/editor/handlers/dialog-handlers.ts +143 -0
  63. package/src/editor/handlers/index.ts +8 -0
  64. package/src/editor/index.ts +3 -0
  65. package/src/extensions/base/callout.ts +138 -0
  66. package/src/extensions/base/code-block.ts +24 -0
  67. package/src/extensions/base/compatibility.ts +493 -0
  68. package/src/extensions/base/copyable-text.ts +56 -0
  69. package/src/extensions/base/custom-quote.ts +174 -0
  70. package/src/extensions/base/figure.ts +141 -0
  71. package/src/extensions/base/index.ts +19 -0
  72. package/src/extensions/base/link.ts +18 -0
  73. package/src/extensions/base/myth-reality.ts +45 -0
  74. package/src/extensions/base/prompt.ts +109 -0
  75. package/src/extensions/base/video.ts +29 -0
  76. package/src/extensions/editor/callout.ts +43 -0
  77. package/src/extensions/editor/code-block.ts +9 -0
  78. package/src/extensions/editor/commands.ts +43 -0
  79. package/src/extensions/editor/copyable-text.ts +16 -0
  80. package/src/extensions/editor/custom-quote.ts +54 -0
  81. package/src/extensions/editor/figure.ts +20 -0
  82. package/src/extensions/editor/get-editor-extensions.ts +113 -0
  83. package/src/extensions/editor/index.ts +25 -0
  84. package/src/extensions/editor/prompt.ts +43 -0
  85. package/src/extensions/editor/video.ts +23 -0
  86. package/src/extensions/render/get-render-extensions.ts +74 -0
  87. package/src/extensions/render/index.ts +6 -0
  88. package/src/extensions/views/components/callout.tsx +70 -0
  89. package/src/extensions/views/components/code-block.tsx +73 -0
  90. package/src/extensions/views/components/copy-button.tsx +52 -0
  91. package/src/extensions/views/components/copyable-text.tsx +11 -0
  92. package/src/extensions/views/components/custom-quote.tsx +64 -0
  93. package/src/extensions/views/components/figure.tsx +63 -0
  94. package/src/extensions/views/components/index.ts +7 -0
  95. package/src/extensions/views/components/prompt.tsx +43 -0
  96. package/src/extensions/views/components/video.tsx +40 -0
  97. package/src/extensions/views/node-views/callout.tsx +51 -0
  98. package/src/extensions/views/node-views/code-block.tsx +30 -0
  99. package/src/extensions/views/node-views/copyable-text.tsx +10 -0
  100. package/src/extensions/views/node-views/custom-quote.tsx +66 -0
  101. package/src/extensions/views/node-views/figure.tsx +38 -0
  102. package/src/extensions/views/node-views/index.ts +7 -0
  103. package/src/extensions/views/node-views/prompt.tsx +35 -0
  104. package/src/extensions/views/node-views/video.tsx +19 -0
  105. package/src/icons/Check.tsx +12 -0
  106. package/src/icons/Copy.tsx +15 -0
  107. package/src/icons/DragIcon.tsx +10 -0
  108. package/src/icons/Edit.tsx +12 -0
  109. package/src/icons/Quote.tsx +10 -0
  110. package/src/icons/X.tsx +13 -0
  111. package/src/icons/color/TiptapCalloutError.tsx +25 -0
  112. package/src/icons/color/TiptapCalloutInfo.tsx +18 -0
  113. package/src/icons/color/TiptapCalloutSuccess.tsx +49 -0
  114. package/src/icons/color/TiptapCalloutWarning.tsx +28 -0
  115. package/src/icons/color/index.ts +4 -0
  116. package/src/icons/index.ts +60 -0
  117. package/src/icons/menu/AddImage.tsx +10 -0
  118. package/src/icons/menu/AlignCenter.tsx +10 -0
  119. package/src/icons/menu/AlignLeft.tsx +10 -0
  120. package/src/icons/menu/AlignRight.tsx +10 -0
  121. package/src/icons/menu/ArrowDown.tsx +13 -0
  122. package/src/icons/menu/ArrowLeft.tsx +13 -0
  123. package/src/icons/menu/ArrowRight.tsx +13 -0
  124. package/src/icons/menu/ArrowUp.tsx +13 -0
  125. package/src/icons/menu/Blockquote.tsx +7 -0
  126. package/src/icons/menu/Bold.tsx +10 -0
  127. package/src/icons/menu/BulletList.tsx +10 -0
  128. package/src/icons/menu/CalloutError.tsx +10 -0
  129. package/src/icons/menu/CalloutInfo.tsx +12 -0
  130. package/src/icons/menu/CalloutSuccess.tsx +10 -0
  131. package/src/icons/menu/CalloutWarning.tsx +12 -0
  132. package/src/icons/menu/ClearFormatting.tsx +10 -0
  133. package/src/icons/menu/Code.tsx +10 -0
  134. package/src/icons/menu/CodeBlock.tsx +10 -0
  135. package/src/icons/menu/Copyable.tsx +10 -0
  136. package/src/icons/menu/CustomQuote.tsx +17 -0
  137. package/src/icons/menu/DeleteColumn.tsx +17 -0
  138. package/src/icons/menu/DeleteRow.tsx +17 -0
  139. package/src/icons/menu/Details.tsx +10 -0
  140. package/src/icons/menu/H2.tsx +10 -0
  141. package/src/icons/menu/H3.tsx +10 -0
  142. package/src/icons/menu/H4.tsx +10 -0
  143. package/src/icons/menu/H5.tsx +10 -0
  144. package/src/icons/menu/H6.tsx +10 -0
  145. package/src/icons/menu/Highlight.tsx +13 -0
  146. package/src/icons/menu/HorizontalRule.tsx +7 -0
  147. package/src/icons/menu/Italic.tsx +10 -0
  148. package/src/icons/menu/Link.tsx +10 -0
  149. package/src/icons/menu/LinkOff.tsx +10 -0
  150. package/src/icons/menu/Math.tsx +10 -0
  151. package/src/icons/menu/MergeCells.tsx +10 -0
  152. package/src/icons/menu/MythReality.tsx +10 -0
  153. package/src/icons/menu/OrderedList.tsx +10 -0
  154. package/src/icons/menu/Prompt.tsx +18 -0
  155. package/src/icons/menu/Redo.tsx +14 -0
  156. package/src/icons/menu/SplitCells.tsx +7 -0
  157. package/src/icons/menu/Strikethrough.tsx +10 -0
  158. package/src/icons/menu/Subscript.tsx +10 -0
  159. package/src/icons/menu/Superscript.tsx +10 -0
  160. package/src/icons/menu/Table.tsx +14 -0
  161. package/src/icons/menu/TableColumnHeading.tsx +7 -0
  162. package/src/icons/menu/TableRowHeading.tsx +10 -0
  163. package/src/icons/menu/TextColor.tsx +13 -0
  164. package/src/icons/menu/Underline.tsx +10 -0
  165. package/src/icons/menu/Undo.tsx +14 -0
  166. package/src/icons/menu/Video.tsx +14 -0
  167. package/src/icons/menu/index.ts +49 -0
  168. package/src/render/index.ts +22 -0
  169. package/src/render/static-renderer-config.tsx +206 -0
  170. package/src/render/tiptap-interactivity-provider.tsx +78 -0
  171. package/src/render/tiptap-render.tsx +102 -0
  172. package/src/styles/components.css +10 -0
  173. package/src/styles/editor/base.css +39 -0
  174. package/src/styles/editor/callout-menu.css +81 -0
  175. package/src/styles/editor/controls.css +57 -0
  176. package/src/styles/editor/emoji-picker.css +113 -0
  177. package/src/styles/editor/index.css +9 -0
  178. package/src/styles/editor/menus.css +16 -0
  179. package/src/styles/editor/states.css +43 -0
  180. package/src/styles/editor/typography.css +165 -0
  181. package/src/styles/editor/utilities.css +71 -0
  182. package/src/styles/editor/variables.css +70 -0
  183. package/src/styles/extensions/callout.css +155 -0
  184. package/src/styles/extensions/code-block.css +109 -0
  185. package/src/styles/extensions/commands-list.css +83 -0
  186. package/src/styles/extensions/custom-quote.css +103 -0
  187. package/src/styles/extensions/details.css +87 -0
  188. package/src/styles/extensions/figure.css +66 -0
  189. package/src/styles/extensions/index.css +11 -0
  190. package/src/styles/extensions/math.css +32 -0
  191. package/src/styles/extensions/myth-reality.css +59 -0
  192. package/src/styles/extensions/prompt.css +75 -0
  193. package/src/styles/extensions/table.css +253 -0
  194. package/src/styles/extensions/video.css +53 -0
  195. package/src/styles/styles.css +3 -0
  196. package/src/translations/en.json +158 -0
  197. package/src/translations/ru.json +158 -0
  198. package/src/types/image-handler.ts +33 -0
  199. package/src/types/index.ts +1 -0
  200. package/src/utils/copy-to-clipboard.ts +23 -0
  201. package/src/utils/custom-emoji.ts +174 -0
  202. package/src/utils/detect-video-hosting.ts +106 -0
  203. package/src/utils/get-shared-extensions.ts +45 -0
  204. package/src/utils/get-translations.ts +25 -0
  205. package/src/utils/handle-exit-node.ts +61 -0
  206. package/src/utils/image-src-resolver.ts +74 -0
  207. package/src/utils/index.ts +2 -0
  208. package/src/utils/migrate-legacy-callouts.ts +59 -0
  209. package/src/utils/pluralize.ts +38 -0
@@ -0,0 +1,174 @@
1
+ import TiptapCalloutInfo from "../icons/color/TiptapCalloutInfo";
2
+ import TiptapCalloutSuccess from "../icons/color/TiptapCalloutSuccess";
3
+ import TiptapCalloutWarning from "../icons/color/TiptapCalloutWarning";
4
+ import TiptapCalloutError from "../icons/color/TiptapCalloutError";
5
+
6
+ export const calloutIconComponents = {
7
+ "callout-info": TiptapCalloutInfo,
8
+ "callout-success": TiptapCalloutSuccess,
9
+ "callout-warning": TiptapCalloutWarning,
10
+ "callout-error": TiptapCalloutError,
11
+ };
12
+
13
+ // Favorite emojis list - these are regular emojis, not custom
14
+ // Just the Unicode strings that should be suggested/shown first
15
+ export const favoriteEmojis = [
16
+ "👋", // wave
17
+ "💡", // bulb
18
+ "🔥", // fire
19
+ "🔒", // lock
20
+ "🏆", // trophy
21
+ "🔑", // key
22
+ "👮", // police
23
+ "⭐", // star
24
+ "⛔", // no entry sign
25
+ "🧠", // brain
26
+ "⚠️", // warning
27
+ "📕", // book
28
+ "💎", // gem
29
+ "🚫", // no entry
30
+ "❗", // exclamation
31
+ "⚡", // lightning
32
+ "📝", // memo
33
+ "❤️", // heart
34
+ ];
35
+
36
+ // Mapping from favorite custom emoji IDs to actual emoji characters
37
+ export const favoriteEmojiIdMap: Record<string, string> = {
38
+ "fav-wave": "👋",
39
+ "fav-bulb": "💡",
40
+ "fav-fire": "🔥",
41
+ "fav-lock": "🔒",
42
+ "fav-trophy": "🏆",
43
+ "fav-key": "🔑",
44
+ "fav-police": "👮",
45
+ "fav-star": "⭐",
46
+ "fav-no-entry-sign": "⛔",
47
+ "fav-brain": "🧠",
48
+ "fav-warning": "⚠️",
49
+ "fav-book": "📕",
50
+ "fav-gem": "💎",
51
+ "fav-no-entry": "🚫",
52
+ "fav-exclamation": "❗",
53
+ "fav-lightning": "⚡",
54
+ "fav-memo": "📝",
55
+ "fav-heart": "❤️",
56
+ };
57
+
58
+ // Favorites - custom emojis category with specific emojis
59
+ // These will appear in a "Custom" category group in the emoji picker
60
+ export const favoriteCustomEmojis = [
61
+ {
62
+ id: "fav-wave",
63
+ names: ["wave", "hand", "hello", "hi"],
64
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f44b.png",
65
+ },
66
+ {
67
+ id: "fav-bulb",
68
+ names: ["bulb", "light", "idea"],
69
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
70
+ },
71
+ {
72
+ id: "fav-fire",
73
+ names: ["fire", "flame", "hot"],
74
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f525.png",
75
+ },
76
+ {
77
+ id: "fav-lock",
78
+ names: ["lock", "locked", "secure"],
79
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f512.png",
80
+ },
81
+ {
82
+ id: "fav-trophy",
83
+ names: ["trophy", "award", "win"],
84
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f3c6.png",
85
+ },
86
+ {
87
+ id: "fav-key",
88
+ names: ["key", "password", "unlock"],
89
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f511.png",
90
+ },
91
+ {
92
+ id: "fav-police",
93
+ names: ["police", "officer", "cop"],
94
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f46e.png",
95
+ },
96
+ {
97
+ id: "fav-star",
98
+ names: ["star", "favorite"],
99
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/2b50.png",
100
+ },
101
+ {
102
+ id: "fav-no-entry-sign",
103
+ names: ["no entry sign", "prohibited", "banned"],
104
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/26d4.png",
105
+ },
106
+ {
107
+ id: "fav-brain",
108
+ names: ["brain", "think", "smart"],
109
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f9e0.png",
110
+ },
111
+ {
112
+ id: "fav-warning",
113
+ names: ["warning", "alert", "caution"],
114
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/26a0-fe0f.png",
115
+ },
116
+ {
117
+ id: "fav-book",
118
+ names: ["book", "read", "closed book"],
119
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4d5.png",
120
+ },
121
+ {
122
+ id: "fav-gem",
123
+ names: ["gem", "diamond", "jewel"],
124
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f48e.png",
125
+ },
126
+ {
127
+ id: "fav-no-entry",
128
+ names: ["no entry", "stop", "do not enter"],
129
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f6ab.png",
130
+ },
131
+ {
132
+ id: "fav-exclamation",
133
+ names: ["exclamation", "important", "alert"],
134
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/2757.png",
135
+ },
136
+ {
137
+ id: "fav-lightning",
138
+ names: ["lightning", "bolt", "zap"],
139
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/26a1.png",
140
+ },
141
+ {
142
+ id: "fav-memo",
143
+ names: ["memo", "note", "pencil"],
144
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4dd.png",
145
+ },
146
+ {
147
+ id: "fav-heart",
148
+ names: ["heart", "love", "red heart"],
149
+ imgUrl: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/2764-fe0f.png",
150
+ },
151
+ ];
152
+
153
+ export const calloutCustomEmojis = [
154
+ {
155
+ id: "callout-info",
156
+ names: ["info", "information", "callout info"],
157
+ imgUrl: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath fill='%23FE9738' d='m11.654 11.196 3.51-2.75.549-4.789-4.06-2.234-1.647 2.264a2.79 2.79 0 0 1 1.531 2.28 2.8 2.8 0 0 1-1.85 2.84 3.96 3.96 0 0 0-2.624 3.728v.72c0 .937.76 1.697 1.696 1.697h2.482zm-4.895-3.32H4.277c-.937 0-1.697-.76-1.697-1.697q0-.432.06-.86C3.084 2.135 5.954-.196 9.172.015h2.482L9.888 1.797m1.353 14.796H8.759c-.937 0-1.696.76-1.696 1.696v.014c0 .938.76 1.697 1.696 1.697h2.18'/%3E%3Cpath fill='%23FFD15B' d='M11.241 14.951c-.937 0-1.697-.76-1.697-1.697v-.72a3.955 3.955 0 0 1 2.625-3.727 2.8 2.8 0 0 0 1.85-2.84A2.79 2.79 0 0 0 11.432 3.4a2.8 2.8 0 0 0-2.976 2.78 1.697 1.697 0 0 1-3.394 0q0-.434.06-.86C5.566 2.133 8.435-.198 11.654.014c3.055.2 5.527 2.653 5.75 5.706a6.2 6.2 0 0 1-4.104 6.286.56.56 0 0 0-.362.529v.72c0 .936-.76 1.696-1.697 1.696m0 5.05c-.937 0-1.697-.76-1.697-1.698v-.014a1.697 1.697 0 0 1 3.394 0v.014c0 .937-.76 1.697-1.697 1.697'/%3E%3Cpath fill='%23FFC344' d='M6.622 5.32q-.06.427-.06.86c0 .667.386 1.245.947 1.521A1.697 1.697 0 0 1 5.062 6.18q0-.432.06-.86C5.566 2.134 8.436-.197 11.654.014q.173.012.343.033C9.273.382 7.012 2.521 6.622 5.32m7.047 3.486a3.96 3.96 0 0 0-2.625 3.729v.72c0 .667.386 1.245.947 1.521a1.696 1.696 0 0 1-2.447-1.522v-.72a3.955 3.955 0 0 1 2.625-3.728 2.8 2.8 0 0 0 1.85-2.839 2.79 2.79 0 0 0-2.022-2.47 2.786 2.786 0 0 1 3.522 2.47 2.8 2.8 0 0 1-1.85 2.84m-1.678 11.02a1.696 1.696 0 0 1-2.447-1.522v-.015a1.697 1.697 0 0 1 2.448-1.523 1.7 1.7 0 0 0-.948 1.523v.014c0 .668.386 1.246.947 1.522'/%3E%3C/svg%3E`,
158
+ },
159
+ {
160
+ id: "callout-success",
161
+ names: ["success", "check", "done", "callout success"],
162
+ imgUrl: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath fill='%2335495E' d='M1.972 18.004.06 7.138a2.4 2.4 0 0 1-.035-.317 2.47 2.47 0 0 1 2.073-2.493l3.693-.652-.1 14.328z'/%3E%3Cpath fill='%232C3E50' d='M16.186 17.528a.69.69 0 0 1-.548.517l-10.87 1.917a2.42 2.42 0 0 1-2.83-2.276 2.46 2.46 0 0 1 1.934-2.462l3.045.855h-.003l-2.742.487a1.037 1.037 0 0 0-.786 1.396 1.066 1.066 0 0 0 1.186.635l8.307-1.466z'/%3E%3Cpath fill='%23F9EAB0' d='m6.914 16.08-2.742.486a1.035 1.035 0 0 0-.786 1.396 1.066 1.066 0 0 0 1.186.635l8.307-1.466z'/%3E%3Cpath fill='%23E64C3C' d='M4.423 11.438 6.339.571a.695.695 0 0 1 .799-.559l10.785 1.902a2.47 2.47 0 0 1 2.074 2.495 3 3 0 0 1-.034.316l-1.917 10.867z'/%3E%3Cpath fill='%233F5C6C' d='M15.668 3.612a.4.4 0 0 1-.06-.005l-7.81-1.378a.345.345 0 0 1 .12-.677l7.81 1.375a.345.345 0 0 1-.06.685M14.01 5.069a.3.3 0 0 1-.06-.005l-5.094-.897a.345.345 0 0 1 .12-.68l5.094.897a.345.345 0 0 1-.06.685'/%3E%3Cpath fill='%23ECF0F1' d='m14.727 6.608-7.131-1.26a.345.345 0 0 0-.4.28l-.6 3.395a.345.345 0 0 0 .28.4l7.13 1.26a.345.345 0 0 0 .4-.28l.6-3.396a.345.345 0 0 0-.28-.4'/%3E%3Cpath fill='%23C03A2B' d='M19.997 4.407a2 2 0 0 1-.035.317L18.045 15.59l-2.659-.81 1.542-8.738.365-2.063.373-2.11.258.045a2.47 2.47 0 0 1 2.073 2.493'/%3E%3Cpath fill='%23802F34' d='M16.007 12.78 5.22 10.88a.69.69 0 0 0-.238 1.358l10.865 1.914a1.038 1.038 0 0 1 .783 1.4 1.06 1.06 0 0 1-1.183.63L4.624 14.277a.69.69 0 0 0-.241 1.359l10.869 1.913a2.416 2.416 0 0 0 2.83-2.272 2.474 2.474 0 0 0-2.075-2.496'/%3E%3Cpath fill='%23F9EAB0' d='M16.63 15.552a1.06 1.06 0 0 1-1.182.63L4.624 14.277l.359-2.038 10.865 1.914a1.04 1.04 0 0 1 .783 1.4'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h20v20H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E`,
163
+ },
164
+ {
165
+ id: "callout-warning",
166
+ names: ["warning", "alert", "caution", "callout warning"],
167
+ imgUrl: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath fill='%23FFF59D' fill-opacity='0.6' d='M1.667 9.167a8.333 8.333 0 1 0 16.666 0 8.333 8.333 0 0 0-16.666 0'/%3E%3Cpath fill='%23FBC02D' d='M15.417 9.167c0-3.209-2.75-5.75-6.042-5.375-2.5.291-4.5 2.291-4.75 4.791a5.27 5.27 0 0 0 1.917 4.709c.583.5.958 1.208.958 2v.125h5v-.042c0-.75.333-1.5.917-2 1.208-1 2-2.5 2-4.208'/%3E%3Cpath fill='%23FFF59D' d='m12.75 8.417-1.25-.834a.45.45 0 0 0-.458 0L10 8.25l-1-.667a.45.45 0 0 0-.458 0l-1.25.834c-.084.083-.167.166-.167.291s0 .25.083.334L8.792 11v4.417h.833v-4.584c0-.083-.042-.166-.083-.25L8.167 8.875l.625-.417 1 .667a.45.45 0 0 0 .458 0l1-.667.625.417-1.375 1.708a.6.6 0 0 0-.083.25v4.584h.833V11l1.583-1.958a.32.32 0 0 0 .084-.334c-.042-.125-.084-.25-.167-.291'/%3E%3Cpath fill='%235C6BC0' d='M8.75 18.334a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0'/%3E%3Cpath fill='%239FA8DA' d='M10.833 18.75H9.167C8.25 18.75 7.5 18 7.5 17.083V15h5v2.083c0 .917-.75 1.667-1.667 1.667'/%3E%3Cpath fill='%235C6BC0' d='m12.5 17.083-4.833.667c.125.291.375.583.666.75l3.917-.542q.25-.375.25-.875m-5-.958v.833l5-.708v-.834z'/%3E%3C/svg%3E`,
168
+ },
169
+ {
170
+ id: "callout-error",
171
+ names: ["error", "danger", "fail", "callout error"],
172
+ imgUrl: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath fill='%23E03757' d='M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0'/%3E%3Cpath fill='%23F04760' d='M4.692 15.603c6.011 0 10.885-4.874 10.885-10.885a10.9 10.9 0 0 0-.525-3.35A9.95 9.95 0 0 0 10 0C4.477 0 0 4.477 0 10a9.95 9.95 0 0 0 1.394 5.094c1.066.338 2.179.51 3.298.509'/%3E%3Cpath fill='%23fff' d='M9.996 16.783a1.626 1.626 0 1 1 0-3.251 1.626 1.626 0 0 1 0 3.251m1.575-5.598c-.027.252-.14.486-.321.663a1.13 1.13 0 0 1-.674.315h-.008q-.239.027-.48.027a6 6 0 0 1-.575-.027h-.007a1.13 1.13 0 0 1-.674-.315 1.1 1.1 0 0 1-.321-.663c-.04-.366-.059-2.914-.059-3.481 0-.568.02-3.122.059-3.488.027-.252.14-.487.32-.665.183-.177.42-.289.675-.315h.007q.287-.029.576-.017.24-.012.479.017h.008c.254.027.492.14.674.319.18.177.294.412.321.664.04.366.059 2.92.059 3.488 0 .567-.02 3.113-.059 3.478'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h20v20H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E`,
173
+ },
174
+ ];
@@ -0,0 +1,106 @@
1
+ export enum VideoHosting {
2
+ Youtube = 'youtube',
3
+ Vimeo = 'vimeo',
4
+ Rutube = 'rutube',
5
+ VK = 'vk',
6
+ }
7
+
8
+ export interface VideoInfo {
9
+ hosting: VideoHosting;
10
+ videoId: string;
11
+ embedUrl: string;
12
+ }
13
+
14
+ const VIDEO_PATTERNS = {
15
+ youtube: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|shorts\/)?([a-zA-Z0-9_-]{11})/,
16
+ youtubeEmbed: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtube-nocookie\.com)\/embed\/([a-zA-Z0-9_-]{11})/,
17
+ vimeo: /(?:http|https)?:\/\/(?:www\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)?\/videos\/)?(\d+)(?:|\/\?)/,
18
+ rutube: /^https:\/\/rutube\.ru\/video\/([\w-]+)/,
19
+ vk: /(?:https?:\/\/)?(?:vk\.com|vkvideo\.ru)\/video(-?\d+)_(\d+)/,
20
+ } as const;
21
+
22
+ /**
23
+ * Detects video hosting platform and extracts video information from URL
24
+ * Supports YouTube, Vimeo, Rutube, and VK video platforms
25
+ *
26
+ * Note: This function is synchronous as it only performs regex pattern matching.
27
+ * It can be safely used in both client and server-side rendering contexts.
28
+ */
29
+ export const detectVideoHosting = (src: string): VideoInfo | null => {
30
+ if (!src || typeof src !== 'string') {
31
+ return null;
32
+ }
33
+
34
+ const cleanSrc = src.trim();
35
+ if (!cleanSrc) {
36
+ return null;
37
+ }
38
+
39
+ try {
40
+ // Check YouTube embed URLs (already in embed format from old editor)
41
+ const youTubeEmbedMatch = VIDEO_PATTERNS.youtubeEmbed.exec(cleanSrc);
42
+ if (youTubeEmbedMatch?.[1]) {
43
+ const videoId = youTubeEmbedMatch[1];
44
+ return {
45
+ hosting: VideoHosting.Youtube,
46
+ videoId,
47
+ embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}`,
48
+ };
49
+ }
50
+
51
+ // Check YouTube regular URLs
52
+ const youTubeMatch = VIDEO_PATTERNS.youtube.exec(cleanSrc);
53
+ if (youTubeMatch?.[1]) {
54
+ const videoId = youTubeMatch[1];
55
+ return {
56
+ hosting: VideoHosting.Youtube,
57
+ videoId,
58
+ embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}`,
59
+ };
60
+ }
61
+
62
+ // Check Vimeo
63
+ const vimeoMatch = VIDEO_PATTERNS.vimeo.exec(cleanSrc);
64
+ if (vimeoMatch?.[1]) {
65
+ const videoId = vimeoMatch[1];
66
+ // Extract hash parameter if present
67
+ const hashRegex = /\/(\d+)\/([a-fA-F0-9]+)/;
68
+ const hashMatch = hashRegex.exec(cleanSrc);
69
+ const hash = hashMatch?.[2] || null;
70
+ const hashParam = hash ? `?h=${hash}` : '';
71
+
72
+ return {
73
+ hosting: VideoHosting.Vimeo,
74
+ videoId,
75
+ embedUrl: `https://player.vimeo.com/video/${videoId}${hashParam}`,
76
+ };
77
+ }
78
+
79
+ // Check Rutube
80
+ const ruTubeMatch = VIDEO_PATTERNS.rutube.exec(cleanSrc);
81
+ if (ruTubeMatch?.[1]) {
82
+ const videoId = ruTubeMatch[1];
83
+ return {
84
+ hosting: VideoHosting.Rutube,
85
+ videoId,
86
+ embedUrl: `https://rutube.ru/play/embed/${videoId}`,
87
+ };
88
+ }
89
+
90
+ // Check VK
91
+ const vkMatch = VIDEO_PATTERNS.vk.exec(cleanSrc);
92
+ if (vkMatch?.[1] && vkMatch?.[2]) {
93
+ const videoId = `${vkMatch[1]}_${vkMatch[2]}`;
94
+ return {
95
+ hosting: VideoHosting.VK,
96
+ videoId,
97
+ embedUrl: `https://vk.com/video_ext.php?oid=${vkMatch[1]}&id=${vkMatch[2]}`,
98
+ };
99
+ }
100
+ } catch {
101
+ // Silent failure for unsupported video URLs
102
+ // Error details available in development via devtools
103
+ }
104
+
105
+ return null;
106
+ };
@@ -0,0 +1,45 @@
1
+ import StarterKit from "@tiptap/starter-kit";
2
+ import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
3
+ import Highlight from "@tiptap/extension-highlight";
4
+ import { LinkExtension, BaseMythReality, BaseMythRealityBlock } from "@/extensions/base";
5
+ import Subscript from "@tiptap/extension-subscript";
6
+ import Superscript from "@tiptap/extension-superscript";
7
+ import TextAlign from "@tiptap/extension-text-align";
8
+ import { TextStyleKit } from "@tiptap/extension-text-style";
9
+ import { LegacyInlineLatexSE } from "@/extensions/base/compatibility";
10
+
11
+ /**
12
+ * Get all Tiptap extensions for editing and rendering without ReactNodeViewRenderer for Editor
13
+ * Can be used for both static render and editor
14
+ */
15
+ export function getSharedExtensions() {
16
+ return [
17
+ StarterKit.configure({
18
+ codeBlock: false,
19
+ link: false,
20
+ }),
21
+ Highlight.configure({ multicolor: true }),
22
+ LinkExtension,
23
+ Superscript,
24
+ Subscript,
25
+ TextAlign.configure({
26
+ types: ["heading", "paragraph"],
27
+ }),
28
+ TextStyleKit.configure({
29
+ color: {
30
+ types: ["textStyle"],
31
+ },
32
+ }),
33
+ Details.configure({
34
+ persist: true,
35
+ HTMLAttributes: {
36
+ class: "details",
37
+ },
38
+ }),
39
+ DetailsSummary,
40
+ DetailsContent,
41
+ BaseMythReality,
42
+ BaseMythRealityBlock,
43
+ LegacyInlineLatexSE,
44
+ ];
45
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Tiptap Editor Translations
3
+ */
4
+
5
+ import ruTranslations from "../translations/ru.json";
6
+ import enTranslations from "../translations/en.json";
7
+
8
+ export type TiptapTranslations = typeof ruTranslations;
9
+
10
+ export type SupportedLanguage = "ru" | "en";
11
+
12
+ const translations: Record<SupportedLanguage, TiptapTranslations> = {
13
+ ru: ruTranslations,
14
+ en: enTranslations,
15
+ };
16
+
17
+ /**
18
+ * Get translations for specified language
19
+ * @param lang - ISO 2-letter language code ('ru' or 'en')
20
+ * @returns Translation object for the specified language
21
+ * @default 'ru' - Falls back to Russian if language not found
22
+ */
23
+ export function getTranslations(lang: SupportedLanguage): TiptapTranslations {
24
+ return translations[lang];
25
+ }
@@ -0,0 +1,61 @@
1
+ import { Editor } from "@tiptap/core";
2
+ import { TextSelection } from "@tiptap/pm/state";
3
+
4
+ /**
5
+ * Handles exiting a node (like callout) when pressing Enter on an empty paragraph
6
+ * @param editor - The Tiptap editor instance
7
+ * @param nodeName - The name of the node type to exit from (e.g., 'callout')
8
+ * @returns true if the exit was handled, false otherwise
9
+ */
10
+ export function handleExitNode(editor: Editor, nodeName: string): boolean {
11
+ const { view, schema } = editor;
12
+ const { state } = view;
13
+ const { selection } = state;
14
+
15
+ const $pos = selection.$from;
16
+ const currentNode = $pos.node();
17
+
18
+ let depth = $pos.depth;
19
+ let isInsideTargetNode = false;
20
+ let targetNodeDepth = 0;
21
+ while (depth > 0) {
22
+ const parentNode = $pos.node(depth);
23
+ if (parentNode.type.name === nodeName) {
24
+ isInsideTargetNode = true;
25
+ targetNodeDepth = depth;
26
+ break;
27
+ }
28
+ depth--;
29
+ }
30
+
31
+ const isCurrentNodeEmptyParagraph =
32
+ currentNode.type.name === "paragraph" && currentNode.content.size === 0;
33
+
34
+ if (!isInsideTargetNode || !isCurrentNodeEmptyParagraph) {
35
+ return false;
36
+ }
37
+
38
+ const { tr } = state;
39
+ const posAfterTargetNode = $pos.after(targetNodeDepth);
40
+
41
+ const targetNode = $pos.node(targetNodeDepth);
42
+ let newPosAfterTargetNode = posAfterTargetNode;
43
+
44
+ if (targetNode.childCount > 1) {
45
+ const emptyParagraphStart = $pos.before();
46
+ const emptyParagraphEnd = $pos.after();
47
+ const deletionLength = emptyParagraphEnd - emptyParagraphStart;
48
+
49
+ tr.delete(emptyParagraphStart, emptyParagraphEnd);
50
+ newPosAfterTargetNode = posAfterTargetNode - deletionLength;
51
+ }
52
+
53
+ const paragraphNode = schema.nodes.paragraph.createAndFill();
54
+ if (!paragraphNode) return false;
55
+
56
+ tr.insert(newPosAfterTargetNode, paragraphNode);
57
+ tr.setSelection(TextSelection.create(tr.doc, newPosAfterTargetNode + 1));
58
+
59
+ view.dispatch(tr);
60
+ return true;
61
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Utility for resolving image sources from fileId or direct src
3
+ */
4
+
5
+ export interface ImageSrcResolverOptions {
6
+ /**
7
+ * Optional function to resolve fileId to URL
8
+ * If not provided, fileId will be used as-is or src will be used
9
+ */
10
+ getImageById?: (fileId: string) => string | null;
11
+ }
12
+
13
+ export interface ImageAttributes {
14
+ fileId?: string | null;
15
+ src?: string | null;
16
+ }
17
+
18
+ /**
19
+ * Resolves image src with clear priority:
20
+ * 1. If fileId exists AND getImageById provided → resolve via getImageById(fileId)
21
+ * 2. If no fileId OR no getImageById → use src/fileUrl directly
22
+ * 3. If both missing → return empty string (error state)
23
+ *
24
+ * Clear separation:
25
+ * - fileId workflow: fileId + getImageById() → resolved URL
26
+ * - Direct URL workflow: fileUrl/src → used as-is
27
+ */
28
+ export function resolveImageSrc(attrs: ImageAttributes, options?: ImageSrcResolverOptions): string {
29
+ const { fileId, src } = attrs;
30
+ const { getImageById } = options || {};
31
+
32
+ // Path 1: FileId workflow with resolution function
33
+ if (fileId && getImageById) {
34
+ const resolvedSrc = getImageById(fileId);
35
+ if (resolvedSrc) {
36
+ return resolvedSrc;
37
+ }
38
+ // If resolution fails, this is an error - return empty to show broken image
39
+ console.warn(`Failed to resolve fileId: ${fileId}`);
40
+ return "";
41
+ }
42
+
43
+ // Path 2: Direct URL workflow (fileUrl/src)
44
+ if (src) {
45
+ return src;
46
+ }
47
+
48
+ // Path 3: FileId without getImageById (legacy or misconfigured)
49
+ if (fileId) {
50
+ console.warn("fileId provided but no getImageById function - cannot resolve image");
51
+ return "";
52
+ }
53
+
54
+ // Error state: no way to display image
55
+ return "";
56
+ }
57
+
58
+ /**
59
+ * Normalizes attributes for storage
60
+ * Prefers fileId over src if both are provided
61
+ */
62
+ export function normalizeImageAttributes(attrs: ImageAttributes): ImageAttributes {
63
+ if (attrs.fileId) {
64
+ // If fileId exists, it's the source of truth
65
+ return {
66
+ fileId: attrs.fileId,
67
+ };
68
+ }
69
+
70
+ // Otherwise use src
71
+ return {
72
+ src: attrs.src,
73
+ };
74
+ }
@@ -0,0 +1,2 @@
1
+ export { copyToClipboard } from './copy-to-clipboard';
2
+ export { pluralize } from './pluralize';
@@ -0,0 +1,59 @@
1
+ import { Editor } from "@tiptap/core";
2
+
3
+ export type CalloutType = "info" | "warning" | "success" | "error";
4
+
5
+ const legacyTypeMap: Record<string, CalloutType> = {
6
+ question: "info",
7
+ idea: "warning",
8
+ info: "success",
9
+ error: "error",
10
+ };
11
+
12
+ export function migrateLegacyCallouts(editor: Editor) {
13
+ const { doc, schema } = editor.state;
14
+ const calloutsToMigrate: Array<{ node: any; pos: number }> = [];
15
+
16
+ doc.descendants((node, pos) => {
17
+ if (node.type.name === "callout" && node.attrs.calloutType) {
18
+ calloutsToMigrate.push({ node, pos });
19
+ }
20
+ });
21
+
22
+ if (calloutsToMigrate.length === 0) return;
23
+
24
+ const tr = editor.state.tr;
25
+ calloutsToMigrate.sort((a, b) => b.pos - a.pos);
26
+
27
+ for (const { node, pos } of calloutsToMigrate) {
28
+ const oldType = node.attrs.calloutType;
29
+ const newType = legacyTypeMap[oldType] || oldType;
30
+ const hasInlineContent = node.content.size > 0 && !node.firstChild?.isBlock;
31
+
32
+ if (hasInlineContent) {
33
+ const paragraph = schema.nodes.paragraph?.create(null, node.content);
34
+ const newContent = paragraph ? [paragraph] : [];
35
+
36
+ tr.replaceRangeWith(
37
+ pos,
38
+ pos + node.nodeSize,
39
+ schema.nodes.callout.create(
40
+ {
41
+ type: newType,
42
+ calloutType: null,
43
+ callout: null,
44
+ },
45
+ newContent
46
+ )
47
+ );
48
+ } else {
49
+ tr.setNodeMarkup(pos, undefined, {
50
+ ...node.attrs,
51
+ type: newType,
52
+ calloutType: null,
53
+ callout: null,
54
+ });
55
+ }
56
+ }
57
+
58
+ editor.view.dispatch(tr);
59
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Returns the correct plural form for Russian language based on the number
3
+ *
4
+ * @param count - The number to determine plural form
5
+ * @param forms - Array of 3 forms: [one, few, many]
6
+ * - one: for 1, 21, 31, etc. (символ)
7
+ * - few: for 2-4, 22-24, etc. (символа)
8
+ * - many: for 0, 5-20, 25-30, etc. (символов)
9
+ *
10
+ * @example
11
+ * pluralize(1, ['символ', 'символа', 'символов']) // символ
12
+ * pluralize(2, ['символ', 'символа', 'символов']) // символа
13
+ * pluralize(5, ['символ', 'символа', 'символов']) // символов
14
+ * pluralize(21, ['символ', 'символа', 'символов']) // символ
15
+ */
16
+ export function pluralize(count: number, forms: readonly [string, string, string]): string {
17
+ const absCount = Math.abs(count);
18
+ const lastDigit = absCount % 10;
19
+ const lastTwoDigits = absCount % 100;
20
+
21
+ // Special case for 11-14 (always use 'many' form)
22
+ if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
23
+ return forms[2];
24
+ }
25
+
26
+ // For numbers ending in 1 (except 11)
27
+ if (lastDigit === 1) {
28
+ return forms[0];
29
+ }
30
+
31
+ // For numbers ending in 2-4 (except 12-14)
32
+ if (lastDigit >= 2 && lastDigit <= 4) {
33
+ return forms[1];
34
+ }
35
+
36
+ // For numbers ending in 0, 5-9, and 11-14
37
+ return forms[2];
38
+ }