@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.
- package/README.md +102 -0
- package/dist/editor.css +2069 -0
- package/dist/editor.css.map +1 -0
- package/dist/editor.d.mts +45 -0
- package/dist/editor.d.ts +45 -0
- package/dist/editor.js +6155 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.mjs +6118 -0
- package/dist/editor.mjs.map +1 -0
- package/dist/render/client.d.mts +24 -0
- package/dist/render/client.d.ts +24 -0
- package/dist/render/client.js +98 -0
- package/dist/render/client.js.map +1 -0
- package/dist/render/client.mjs +74 -0
- package/dist/render/client.mjs.map +1 -0
- package/dist/render.d.mts +58 -0
- package/dist/render.d.ts +58 -0
- package/dist/render.js +2521 -0
- package/dist/render.js.map +1 -0
- package/dist/render.mjs +2484 -0
- package/dist/render.mjs.map +1 -0
- package/dist/styles/index.css +2068 -0
- package/package.json +92 -0
- package/src/editor/commands/items.ts +374 -0
- package/src/editor/commands/suggestion.tsx +146 -0
- package/src/editor/components/button/button.css +56 -0
- package/src/editor/components/button/button.tsx +35 -0
- package/src/editor/components/button/index.ts +1 -0
- package/src/editor/components/common-modal/common-modal.css +85 -0
- package/src/editor/components/common-modal/common-modal.tsx +58 -0
- package/src/editor/components/common-modal/index.ts +1 -0
- package/src/editor/components/index.ts +3 -0
- package/src/editor/components/input/index.ts +1 -0
- package/src/editor/components/input/input.css +43 -0
- package/src/editor/components/input/input.tsx +22 -0
- package/src/editor/context/dialog-command.tsx +47 -0
- package/src/editor/context/index.ts +2 -0
- package/src/editor/context/translations-context.tsx +28 -0
- package/src/editor/controls/bubble-menu-content.tsx +394 -0
- package/src/editor/controls/callout-menu.tsx +169 -0
- package/src/editor/controls/character-count.tsx +36 -0
- package/src/editor/controls/commands-list.tsx +117 -0
- package/src/editor/controls/drag-handle.tsx +98 -0
- package/src/editor/controls/emoji-picker.tsx +62 -0
- package/src/editor/controls/index.ts +5 -0
- package/src/editor/controls/table-menu.tsx +322 -0
- package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.css +115 -0
- package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.tsx +116 -0
- package/src/editor/dialogs/add-katex-dialog/index.ts +1 -0
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +105 -0
- package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +177 -0
- package/src/editor/dialogs/add-link-dialog/index.ts +1 -0
- package/src/editor/dialogs/add-video-dialog/add-video-dialog.css +146 -0
- package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +157 -0
- package/src/editor/dialogs/add-video-dialog/index.ts +1 -0
- package/src/editor/dialogs/image-dialog/image-dialog.css +101 -0
- package/src/editor/dialogs/image-dialog/image-dialog.tsx +276 -0
- package/src/editor/dialogs/image-dialog/index.ts +1 -0
- package/src/editor/dialogs/index.ts +4 -0
- package/src/editor/editor.tsx +247 -0
- package/src/editor/handlers/create-image-upload-handler.ts +24 -0
- package/src/editor/handlers/dialog-handlers.ts +143 -0
- package/src/editor/handlers/index.ts +8 -0
- package/src/editor/index.ts +3 -0
- package/src/extensions/base/callout.ts +138 -0
- package/src/extensions/base/code-block.ts +24 -0
- package/src/extensions/base/compatibility.ts +493 -0
- package/src/extensions/base/copyable-text.ts +56 -0
- package/src/extensions/base/custom-quote.ts +174 -0
- package/src/extensions/base/figure.ts +141 -0
- package/src/extensions/base/index.ts +19 -0
- package/src/extensions/base/link.ts +18 -0
- package/src/extensions/base/myth-reality.ts +45 -0
- package/src/extensions/base/prompt.ts +109 -0
- package/src/extensions/base/video.ts +29 -0
- package/src/extensions/editor/callout.ts +43 -0
- package/src/extensions/editor/code-block.ts +9 -0
- package/src/extensions/editor/commands.ts +43 -0
- package/src/extensions/editor/copyable-text.ts +16 -0
- package/src/extensions/editor/custom-quote.ts +54 -0
- package/src/extensions/editor/figure.ts +20 -0
- package/src/extensions/editor/get-editor-extensions.ts +113 -0
- package/src/extensions/editor/index.ts +25 -0
- package/src/extensions/editor/prompt.ts +43 -0
- package/src/extensions/editor/video.ts +23 -0
- package/src/extensions/render/get-render-extensions.ts +74 -0
- package/src/extensions/render/index.ts +6 -0
- package/src/extensions/views/components/callout.tsx +70 -0
- package/src/extensions/views/components/code-block.tsx +73 -0
- package/src/extensions/views/components/copy-button.tsx +52 -0
- package/src/extensions/views/components/copyable-text.tsx +11 -0
- package/src/extensions/views/components/custom-quote.tsx +64 -0
- package/src/extensions/views/components/figure.tsx +63 -0
- package/src/extensions/views/components/index.ts +7 -0
- package/src/extensions/views/components/prompt.tsx +43 -0
- package/src/extensions/views/components/video.tsx +40 -0
- package/src/extensions/views/node-views/callout.tsx +51 -0
- package/src/extensions/views/node-views/code-block.tsx +30 -0
- package/src/extensions/views/node-views/copyable-text.tsx +10 -0
- package/src/extensions/views/node-views/custom-quote.tsx +66 -0
- package/src/extensions/views/node-views/figure.tsx +38 -0
- package/src/extensions/views/node-views/index.ts +7 -0
- package/src/extensions/views/node-views/prompt.tsx +35 -0
- package/src/extensions/views/node-views/video.tsx +19 -0
- package/src/icons/Check.tsx +12 -0
- package/src/icons/Copy.tsx +15 -0
- package/src/icons/DragIcon.tsx +10 -0
- package/src/icons/Edit.tsx +12 -0
- package/src/icons/Quote.tsx +10 -0
- package/src/icons/X.tsx +13 -0
- package/src/icons/color/TiptapCalloutError.tsx +25 -0
- package/src/icons/color/TiptapCalloutInfo.tsx +18 -0
- package/src/icons/color/TiptapCalloutSuccess.tsx +49 -0
- package/src/icons/color/TiptapCalloutWarning.tsx +28 -0
- package/src/icons/color/index.ts +4 -0
- package/src/icons/index.ts +60 -0
- package/src/icons/menu/AddImage.tsx +10 -0
- package/src/icons/menu/AlignCenter.tsx +10 -0
- package/src/icons/menu/AlignLeft.tsx +10 -0
- package/src/icons/menu/AlignRight.tsx +10 -0
- package/src/icons/menu/ArrowDown.tsx +13 -0
- package/src/icons/menu/ArrowLeft.tsx +13 -0
- package/src/icons/menu/ArrowRight.tsx +13 -0
- package/src/icons/menu/ArrowUp.tsx +13 -0
- package/src/icons/menu/Blockquote.tsx +7 -0
- package/src/icons/menu/Bold.tsx +10 -0
- package/src/icons/menu/BulletList.tsx +10 -0
- package/src/icons/menu/CalloutError.tsx +10 -0
- package/src/icons/menu/CalloutInfo.tsx +12 -0
- package/src/icons/menu/CalloutSuccess.tsx +10 -0
- package/src/icons/menu/CalloutWarning.tsx +12 -0
- package/src/icons/menu/ClearFormatting.tsx +10 -0
- package/src/icons/menu/Code.tsx +10 -0
- package/src/icons/menu/CodeBlock.tsx +10 -0
- package/src/icons/menu/Copyable.tsx +10 -0
- package/src/icons/menu/CustomQuote.tsx +17 -0
- package/src/icons/menu/DeleteColumn.tsx +17 -0
- package/src/icons/menu/DeleteRow.tsx +17 -0
- package/src/icons/menu/Details.tsx +10 -0
- package/src/icons/menu/H2.tsx +10 -0
- package/src/icons/menu/H3.tsx +10 -0
- package/src/icons/menu/H4.tsx +10 -0
- package/src/icons/menu/H5.tsx +10 -0
- package/src/icons/menu/H6.tsx +10 -0
- package/src/icons/menu/Highlight.tsx +13 -0
- package/src/icons/menu/HorizontalRule.tsx +7 -0
- package/src/icons/menu/Italic.tsx +10 -0
- package/src/icons/menu/Link.tsx +10 -0
- package/src/icons/menu/LinkOff.tsx +10 -0
- package/src/icons/menu/Math.tsx +10 -0
- package/src/icons/menu/MergeCells.tsx +10 -0
- package/src/icons/menu/MythReality.tsx +10 -0
- package/src/icons/menu/OrderedList.tsx +10 -0
- package/src/icons/menu/Prompt.tsx +18 -0
- package/src/icons/menu/Redo.tsx +14 -0
- package/src/icons/menu/SplitCells.tsx +7 -0
- package/src/icons/menu/Strikethrough.tsx +10 -0
- package/src/icons/menu/Subscript.tsx +10 -0
- package/src/icons/menu/Superscript.tsx +10 -0
- package/src/icons/menu/Table.tsx +14 -0
- package/src/icons/menu/TableColumnHeading.tsx +7 -0
- package/src/icons/menu/TableRowHeading.tsx +10 -0
- package/src/icons/menu/TextColor.tsx +13 -0
- package/src/icons/menu/Underline.tsx +10 -0
- package/src/icons/menu/Undo.tsx +14 -0
- package/src/icons/menu/Video.tsx +14 -0
- package/src/icons/menu/index.ts +49 -0
- package/src/render/index.ts +22 -0
- package/src/render/static-renderer-config.tsx +206 -0
- package/src/render/tiptap-interactivity-provider.tsx +78 -0
- package/src/render/tiptap-render.tsx +102 -0
- package/src/styles/components.css +10 -0
- package/src/styles/editor/base.css +39 -0
- package/src/styles/editor/callout-menu.css +81 -0
- package/src/styles/editor/controls.css +57 -0
- package/src/styles/editor/emoji-picker.css +113 -0
- package/src/styles/editor/index.css +9 -0
- package/src/styles/editor/menus.css +16 -0
- package/src/styles/editor/states.css +43 -0
- package/src/styles/editor/typography.css +165 -0
- package/src/styles/editor/utilities.css +71 -0
- package/src/styles/editor/variables.css +70 -0
- package/src/styles/extensions/callout.css +155 -0
- package/src/styles/extensions/code-block.css +109 -0
- package/src/styles/extensions/commands-list.css +83 -0
- package/src/styles/extensions/custom-quote.css +103 -0
- package/src/styles/extensions/details.css +87 -0
- package/src/styles/extensions/figure.css +66 -0
- package/src/styles/extensions/index.css +11 -0
- package/src/styles/extensions/math.css +32 -0
- package/src/styles/extensions/myth-reality.css +59 -0
- package/src/styles/extensions/prompt.css +75 -0
- package/src/styles/extensions/table.css +253 -0
- package/src/styles/extensions/video.css +53 -0
- package/src/styles/styles.css +3 -0
- package/src/translations/en.json +158 -0
- package/src/translations/ru.json +158 -0
- package/src/types/image-handler.ts +33 -0
- package/src/types/index.ts +1 -0
- package/src/utils/copy-to-clipboard.ts +23 -0
- package/src/utils/custom-emoji.ts +174 -0
- package/src/utils/detect-video-hosting.ts +106 -0
- package/src/utils/get-shared-extensions.ts +45 -0
- package/src/utils/get-translations.ts +25 -0
- package/src/utils/handle-exit-node.ts +61 -0
- package/src/utils/image-src-resolver.ts +74 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/migrate-legacy-callouts.ts +59 -0
- 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,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
|
+
}
|