mardora 1.2.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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chunk-3OCUX4OO.js +7690 -0
  4. package/dist/chunk-3OCUX4OO.js.map +1 -0
  5. package/dist/chunk-3ZOCCFDL.cjs +74 -0
  6. package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
  7. package/dist/chunk-7JOEPNEV.cjs +7740 -0
  8. package/dist/chunk-7JOEPNEV.cjs.map +1 -0
  9. package/dist/chunk-BIKZQZ6W.js +33 -0
  10. package/dist/chunk-BIKZQZ6W.js.map +1 -0
  11. package/dist/chunk-EQJESPP2.js +234 -0
  12. package/dist/chunk-EQJESPP2.js.map +1 -0
  13. package/dist/chunk-G4SE26YY.js +70 -0
  14. package/dist/chunk-G4SE26YY.js.map +1 -0
  15. package/dist/chunk-KNDWF2DP.cjs +35 -0
  16. package/dist/chunk-KNDWF2DP.cjs.map +1 -0
  17. package/dist/chunk-MLBEBFHB.cjs +2971 -0
  18. package/dist/chunk-MLBEBFHB.cjs.map +1 -0
  19. package/dist/chunk-P7JFCYU3.js +905 -0
  20. package/dist/chunk-P7JFCYU3.js.map +1 -0
  21. package/dist/chunk-SWFUKJDO.cjs +243 -0
  22. package/dist/chunk-SWFUKJDO.cjs.map +1 -0
  23. package/dist/chunk-WFVCG4LD.cjs +926 -0
  24. package/dist/chunk-WFVCG4LD.cjs.map +1 -0
  25. package/dist/chunk-XL6WFGJT.js +2901 -0
  26. package/dist/chunk-XL6WFGJT.js.map +1 -0
  27. package/dist/editor/index.cjs +277 -0
  28. package/dist/editor/index.cjs.map +1 -0
  29. package/dist/editor/index.d.cts +186 -0
  30. package/dist/editor/index.d.ts +186 -0
  31. package/dist/editor/index.js +4 -0
  32. package/dist/editor/index.js.map +1 -0
  33. package/dist/index.cjs +405 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +13 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/index.cjs +12 -0
  40. package/dist/lib/index.cjs.map +1 -0
  41. package/dist/lib/index.d.cts +16 -0
  42. package/dist/lib/index.d.ts +16 -0
  43. package/dist/lib/index.js +3 -0
  44. package/dist/lib/index.js.map +1 -0
  45. package/dist/mardora-DCwjomil.d.cts +640 -0
  46. package/dist/mardora-DCwjomil.d.ts +640 -0
  47. package/dist/plugins/index.cjs +104 -0
  48. package/dist/plugins/index.cjs.map +1 -0
  49. package/dist/plugins/index.d.cts +740 -0
  50. package/dist/plugins/index.d.ts +740 -0
  51. package/dist/plugins/index.js +7 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/preview/index.cjs +38 -0
  54. package/dist/preview/index.cjs.map +1 -0
  55. package/dist/preview/index.d.cts +101 -0
  56. package/dist/preview/index.d.ts +101 -0
  57. package/dist/preview/index.js +5 -0
  58. package/dist/preview/index.js.map +1 -0
  59. package/dist/types-NBsaxl4d.d.cts +71 -0
  60. package/dist/types-Pw2SWWAR.d.ts +71 -0
  61. package/package.json +92 -0
  62. package/src/editor/attachments/extension.ts +181 -0
  63. package/src/editor/attachments/format.ts +63 -0
  64. package/src/editor/attachments/index.ts +3 -0
  65. package/src/editor/attachments/types.ts +37 -0
  66. package/src/editor/heading-fold/config.ts +25 -0
  67. package/src/editor/heading-fold/extension.ts +268 -0
  68. package/src/editor/heading-fold/extract.ts +88 -0
  69. package/src/editor/heading-fold/index.ts +5 -0
  70. package/src/editor/heading-fold/theme.ts +85 -0
  71. package/src/editor/heading-fold/types.ts +24 -0
  72. package/src/editor/i18n.ts +13 -0
  73. package/src/editor/icons/index.ts +367 -0
  74. package/src/editor/index.ts +16 -0
  75. package/src/editor/mardora.ts +257 -0
  76. package/src/editor/media-lightbox-theme.ts +146 -0
  77. package/src/editor/media-lightbox.ts +125 -0
  78. package/src/editor/plugin.ts +294 -0
  79. package/src/editor/selection-toolbar/activation.ts +123 -0
  80. package/src/editor/selection-toolbar/commands.ts +279 -0
  81. package/src/editor/selection-toolbar/extension.ts +564 -0
  82. package/src/editor/selection-toolbar/i18n.ts +164 -0
  83. package/src/editor/selection-toolbar/index.ts +7 -0
  84. package/src/editor/selection-toolbar/menu.ts +252 -0
  85. package/src/editor/selection-toolbar/position.ts +43 -0
  86. package/src/editor/selection-toolbar/theme.ts +195 -0
  87. package/src/editor/selection-toolbar/types.ts +155 -0
  88. package/src/editor/slash/default-commands.ts +190 -0
  89. package/src/editor/slash/extension.ts +319 -0
  90. package/src/editor/slash/index.ts +7 -0
  91. package/src/editor/slash/insertions.ts +26 -0
  92. package/src/editor/slash/menu.ts +123 -0
  93. package/src/editor/slash/position.ts +61 -0
  94. package/src/editor/slash/query.ts +33 -0
  95. package/src/editor/slash/theme.ts +113 -0
  96. package/src/editor/slash/types.ts +40 -0
  97. package/src/editor/table-of-contents/extension.ts +202 -0
  98. package/src/editor/table-of-contents/extract.ts +53 -0
  99. package/src/editor/table-of-contents/index.ts +7 -0
  100. package/src/editor/table-of-contents/panel.ts +83 -0
  101. package/src/editor/table-of-contents/slug.ts +50 -0
  102. package/src/editor/table-of-contents/storage.ts +35 -0
  103. package/src/editor/table-of-contents/theme.ts +153 -0
  104. package/src/editor/table-of-contents/types.ts +44 -0
  105. package/src/editor/theme.ts +72 -0
  106. package/src/editor/utils.ts +176 -0
  107. package/src/editor/view-plugin.ts +189 -0
  108. package/src/index.ts +5 -0
  109. package/src/lib/index.ts +2 -0
  110. package/src/lib/input-handler.ts +47 -0
  111. package/src/plugins/code-plugin.theme.ts +545 -0
  112. package/src/plugins/code-plugin.ts +1892 -0
  113. package/src/plugins/emoji-plugin.ts +140 -0
  114. package/src/plugins/heading-plugin.ts +194 -0
  115. package/src/plugins/hr-plugin.ts +102 -0
  116. package/src/plugins/html-plugin.ts +353 -0
  117. package/src/plugins/image-plugin.ts +806 -0
  118. package/src/plugins/index.ts +71 -0
  119. package/src/plugins/inline-plugin.ts +311 -0
  120. package/src/plugins/link-plugin.ts +509 -0
  121. package/src/plugins/list-plugin.ts +492 -0
  122. package/src/plugins/math-plugin.ts +526 -0
  123. package/src/plugins/mermaid-plugin.ts +513 -0
  124. package/src/plugins/paragraph-plugin.ts +38 -0
  125. package/src/plugins/quote-plugin.ts +733 -0
  126. package/src/plugins/table-controls-theme.ts +126 -0
  127. package/src/plugins/table-controls.ts +423 -0
  128. package/src/plugins/table-model.ts +661 -0
  129. package/src/plugins/table-plugin.ts +2111 -0
  130. package/src/preview/context.ts +45 -0
  131. package/src/preview/css-generator.ts +64 -0
  132. package/src/preview/default-renderers.ts +29 -0
  133. package/src/preview/index.ts +29 -0
  134. package/src/preview/preview.ts +41 -0
  135. package/src/preview/renderer.ts +184 -0
  136. package/src/preview/syntax-theme.ts +112 -0
  137. package/src/preview/toc.ts +23 -0
  138. package/src/preview/types.ts +89 -0
@@ -0,0 +1,155 @@
1
+ import type { MardoraIconName } from "../icons";
2
+ import type { MardoraLocale } from "../i18n";
3
+ import type { SelectionToolbarMessages } from "./i18n";
4
+
5
+ export type MardoraSelectionToolbarConfig = {
6
+ enabled?: boolean;
7
+ locale?: MardoraLocale;
8
+ };
9
+
10
+ export type SelectionToolbarPlacement = "top" | "bottom";
11
+
12
+ export type SelectionToolbarAnchorRect = {
13
+ left: number;
14
+ right: number;
15
+ top: number;
16
+ bottom: number;
17
+ };
18
+
19
+ export type SelectionToolbarViewport = {
20
+ width: number;
21
+ height: number;
22
+ };
23
+
24
+ export type SelectionToolbarFloatingSize = {
25
+ width: number;
26
+ height: number;
27
+ };
28
+
29
+ export type SelectionToolbarBoundary = {
30
+ left: number;
31
+ right: number;
32
+ top: number;
33
+ bottom: number;
34
+ };
35
+
36
+ export type SelectionToolbarLayoutInput = {
37
+ anchor: SelectionToolbarAnchorRect;
38
+ viewport: SelectionToolbarViewport;
39
+ boundary?: SelectionToolbarBoundary;
40
+ floating: SelectionToolbarFloatingSize;
41
+ };
42
+
43
+ export type SelectionToolbarLayout = {
44
+ placement: SelectionToolbarPlacement;
45
+ left: number;
46
+ top: number;
47
+ maxHeight: number;
48
+ };
49
+
50
+ export type SelectionToolbarActionId =
51
+ | "block-type"
52
+ | "bold"
53
+ | "italic"
54
+ | "strike"
55
+ | "underline"
56
+ | "code"
57
+ | "highlight"
58
+ | "color"
59
+ | "link"
60
+ | "ordered-list"
61
+ | "unordered-list"
62
+ | "task-list";
63
+
64
+ export type SelectionToolbarButton = {
65
+ id: SelectionToolbarActionId;
66
+ label: string;
67
+ icon: MardoraIconName;
68
+ text?: string;
69
+ active?: boolean;
70
+ };
71
+
72
+ export type TextChange = {
73
+ from: number;
74
+ to: number;
75
+ insert: string;
76
+ };
77
+
78
+ export type TextSelection = {
79
+ anchor: number;
80
+ head?: number;
81
+ };
82
+
83
+ export type TextCommandResult = {
84
+ changes: TextChange | TextChange[];
85
+ selection?: TextSelection;
86
+ };
87
+
88
+ export type InlineFormatInput = {
89
+ doc: string;
90
+ from: number;
91
+ to: number;
92
+ marker?: string;
93
+ htmlTag?: "u";
94
+ spanStyle?: {
95
+ property: "color" | "background-color";
96
+ value: string;
97
+ };
98
+ clear?: boolean;
99
+ };
100
+
101
+ export type ParsedSelectionLink =
102
+ | { kind: "markdown-link"; title: string; url: string }
103
+ | { kind: "url"; title: string; url: string }
104
+ | { kind: "text"; title: string; url: string };
105
+
106
+ export type LinkChangeInput = {
107
+ from: number;
108
+ to: number;
109
+ title: string;
110
+ url: string;
111
+ remove?: boolean;
112
+ };
113
+
114
+ export type SelectionToolbarListKind = "ordered" | "unordered" | "task";
115
+ export type SelectionToolbarBlockType = "text" | "heading-1" | "heading-2" | "heading-3" | "heading-4" | "heading-5" | "heading-6";
116
+
117
+ export type SelectionToolbarPanel = "toolbar" | "link" | "color" | "highlight" | "block-type";
118
+
119
+ export type SelectionToolbarPaletteItem = {
120
+ id: string;
121
+ label: string;
122
+ value: string | null;
123
+ };
124
+
125
+ export type SelectionToolbarLinkState = {
126
+ title: string;
127
+ url: string;
128
+ canRemove: boolean;
129
+ error?: string;
130
+ copied?: boolean;
131
+ };
132
+
133
+ export type SelectionToolbarMenuState = {
134
+ panel: SelectionToolbarPanel;
135
+ buttons: SelectionToolbarButton[];
136
+ blockType: SelectionToolbarBlockType;
137
+ blockTypes: Array<{ type: SelectionToolbarBlockType; label: string; icon: MardoraIconName }>;
138
+ textColors: SelectionToolbarPaletteItem[];
139
+ highlightColors: SelectionToolbarPaletteItem[];
140
+ link: SelectionToolbarLinkState;
141
+ messages: SelectionToolbarMessages;
142
+ };
143
+
144
+ export type SelectionToolbarMenuCallbacks = {
145
+ onAction: (id: SelectionToolbarActionId) => void;
146
+ onBlockType: (type: SelectionToolbarBlockType) => void;
147
+ onColor: (value: string | null) => void;
148
+ onHighlight: (value: string | null) => void;
149
+ onLinkInput: (field: "title" | "url", value: string) => void;
150
+ onLinkSubmit: () => void;
151
+ onLinkCopy: () => void;
152
+ onLinkOpen: () => void;
153
+ onLinkRemove: () => void;
154
+ onCancelPanel: () => void;
155
+ };
@@ -0,0 +1,190 @@
1
+ import type { MardoraSlashCommand } from "./types";
2
+ import { buildSlashReplacement } from "./insertions";
3
+ import type { MardoraAttachmentKind } from "../attachments";
4
+ import type { MardoraLocale } from "../i18n";
5
+
6
+ type LocalizedSlashCommandCopy = {
7
+ title: string;
8
+ aliases: string[];
9
+ };
10
+
11
+ const commandCopy: Record<MardoraLocale, Record<string, LocalizedSlashCommandCopy>> = {
12
+ "zh-CN": {
13
+ paragraph: { title: "文本", aliases: ["text", "plain", "wenben"] },
14
+ "heading-1": { title: "标题 1", aliases: ["h1", "heading", "heading1", "biaoti", "标题"] },
15
+ "heading-2": { title: "标题 2", aliases: ["h2", "heading", "heading2", "biaoti", "标题"] },
16
+ "heading-3": { title: "标题 3", aliases: ["h3", "heading", "heading3", "biaoti", "标题"] },
17
+ "heading-4": { title: "标题 4", aliases: ["h4", "heading", "heading4", "biaoti", "标题"] },
18
+ "heading-5": { title: "标题 5", aliases: ["h5", "heading", "heading5", "biaoti", "标题"] },
19
+ "heading-6": { title: "标题 6", aliases: ["h6", "heading", "heading6", "biaoti", "标题"] },
20
+ quote: { title: "引用", aliases: ["quote", "blockquote", "yinyong"] },
21
+ "callout-note": {
22
+ title: "说明 Callout",
23
+ aliases: ["callout", "note", "info", "告警", "说明", "提示", "shuoming", "tishi"],
24
+ },
25
+ "callout-tip": {
26
+ title: "技巧 Callout",
27
+ aliases: ["callout", "tip", "hint", "告警", "技巧", "提示", "jiqiao", "tishi"],
28
+ },
29
+ "callout-important": { title: "重要 Callout", aliases: ["callout", "important", "告警", "重要", "zhongyao"] },
30
+ "callout-warning": { title: "警告 Callout", aliases: ["callout", "warning", "warn", "告警", "警告", "jinggao"] },
31
+ "callout-caution": {
32
+ title: "严重警告 Callout",
33
+ aliases: ["callout", "caution", "danger", "告警", "严重", "风险", "yanzhong", "fengxian"],
34
+ },
35
+ "code-block": { title: "代码块", aliases: ["code", "codeblock", "fence", "代码", "代码块", "daima"] },
36
+ "ordered-list": { title: "有序列表", aliases: ["ol", "ordered", "numbered", "youxu", "有序"] },
37
+ "unordered-list": { title: "项目符号列表", aliases: ["ul", "bullet", "unordered", "bulleted", "wuxu", "无序"] },
38
+ "task-list": { title: "待办清单", aliases: ["todo", "task", "check", "daiban", "待办"] },
39
+ table: { title: "表格", aliases: ["table", "biaoge"] },
40
+ divider: { title: "分隔线", aliases: ["hr", "divider", "line", "fengexian", "分隔"] },
41
+ link: { title: "链接", aliases: ["link", "url", "lianjie"] },
42
+ file: { title: "文件", aliases: ["file", "wenjian"] },
43
+ image: { title: "图片", aliases: ["image", "img", "tu", "tupian", "图片"] },
44
+ video: { title: "视频", aliases: ["video", "shipin"] },
45
+ audio: { title: "音频", aliases: ["audio", "music", "yinpin"] },
46
+ },
47
+ "en-US": {
48
+ paragraph: { title: "Text", aliases: ["文本", "text", "plain", "wenben"] },
49
+ "heading-1": { title: "Heading 1", aliases: ["标题", "h1", "heading", "heading1", "biaoti"] },
50
+ "heading-2": { title: "Heading 2", aliases: ["标题", "h2", "heading", "heading2", "biaoti"] },
51
+ "heading-3": { title: "Heading 3", aliases: ["标题", "h3", "heading", "heading3", "biaoti"] },
52
+ "heading-4": { title: "Heading 4", aliases: ["标题", "h4", "heading", "heading4", "biaoti"] },
53
+ "heading-5": { title: "Heading 5", aliases: ["标题", "h5", "heading", "heading5", "biaoti"] },
54
+ "heading-6": { title: "Heading 6", aliases: ["标题", "h6", "heading", "heading6", "biaoti"] },
55
+ quote: { title: "Quote", aliases: ["引用", "quote", "blockquote", "yinyong"] },
56
+ "callout-note": { title: "Note callout", aliases: ["说明", "提示", "告警", "callout", "note", "info"] },
57
+ "callout-tip": { title: "Tip callout", aliases: ["技巧", "提示", "告警", "callout", "tip", "hint"] },
58
+ "callout-important": { title: "Important callout", aliases: ["重要", "告警", "callout", "important"] },
59
+ "callout-warning": { title: "Warning callout", aliases: ["警告", "告警", "callout", "warning", "warn"] },
60
+ "callout-caution": { title: "Caution callout", aliases: ["严重", "风险", "告警", "callout", "caution", "danger"] },
61
+ "code-block": { title: "Code block", aliases: ["代码", "代码块", "code", "codeblock", "fence", "daima"] },
62
+ "ordered-list": { title: "Numbered list", aliases: ["有序", "ol", "ordered", "numbered", "youxu"] },
63
+ "unordered-list": { title: "Bulleted list", aliases: ["无序", "ul", "bullet", "unordered", "bulleted", "wuxu"] },
64
+ "task-list": { title: "To-do list", aliases: ["待办", "todo", "task", "check", "daiban"] },
65
+ table: { title: "Table", aliases: ["表格", "table", "biaoge"] },
66
+ divider: { title: "Divider", aliases: ["分隔", "hr", "divider", "line", "fengexian"] },
67
+ link: { title: "Link", aliases: ["链接", "link", "url", "lianjie"] },
68
+ file: { title: "File", aliases: ["文件", "file", "wenjian"] },
69
+ image: { title: "Image", aliases: ["图片", "image", "img", "tu", "tupian"] },
70
+ video: { title: "Video", aliases: ["视频", "video", "shipin"] },
71
+ audio: { title: "Audio", aliases: ["音频", "audio", "music", "yinpin"] },
72
+ },
73
+ };
74
+
75
+ function commandMeta(
76
+ locale: MardoraLocale,
77
+ id: string,
78
+ group: MardoraSlashCommand["group"],
79
+ icon: string,
80
+ hint: string
81
+ ): Omit<MardoraSlashCommand, "run"> {
82
+ const copy = commandCopy[locale][id];
83
+ if (!copy) {
84
+ throw new Error(`Missing slash command copy: ${locale}:${id}`);
85
+ }
86
+
87
+ return {
88
+ id,
89
+ group,
90
+ title: copy.title,
91
+ aliases: copy.aliases,
92
+ icon,
93
+ hint,
94
+ };
95
+ }
96
+
97
+ function markdownCommand(
98
+ command: Omit<MardoraSlashCommand, "run">,
99
+ marker: string,
100
+ cursorOffset: number = marker.length
101
+ ): MardoraSlashCommand {
102
+ return {
103
+ ...command,
104
+ run: ({ view, queryRange }) => {
105
+ const replacement = buildSlashReplacement({ marker, cursorOffset }, { ...queryRange, query: "" });
106
+ view.dispatch({
107
+ changes: replacement.changes,
108
+ selection: { anchor: replacement.selectionAnchor },
109
+ scrollIntoView: true,
110
+ });
111
+ view.focus();
112
+ return true;
113
+ },
114
+ };
115
+ }
116
+
117
+ function calloutCommand(
118
+ locale: MardoraLocale,
119
+ id: string,
120
+ icon: string,
121
+ type: "NOTE" | "TIP" | "IMPORTANT" | "WARNING" | "CAUTION"
122
+ ): MardoraSlashCommand {
123
+ return markdownCommand(commandMeta(locale, id, "basic", icon, `[!${type}]`), `> [!${type}]\n> `);
124
+ }
125
+
126
+ function mediaCommand(command: Omit<MardoraSlashCommand, "run">, kind: MardoraAttachmentKind): MardoraSlashCommand {
127
+ return {
128
+ ...command,
129
+ run: (context) => {
130
+ if (context.requestAttachment?.(kind, context)) {
131
+ return true;
132
+ }
133
+
134
+ const fallbackByKind: Record<MardoraAttachmentKind, string> = {
135
+ image: "![image](url)",
136
+ video: '<video src="url" controls></video>',
137
+ audio: '<audio src="url" controls></audio>',
138
+ file: "[filename](url)",
139
+ };
140
+ const fallback = fallbackByKind[kind];
141
+ const replacement = buildSlashReplacement(
142
+ { marker: fallback, cursorOffset: kind === "file" ? 1 : fallback.indexOf("url") },
143
+ { ...context.queryRange, query: "" }
144
+ );
145
+
146
+ context.view.dispatch({
147
+ changes: replacement.changes,
148
+ selection: { anchor: replacement.selectionAnchor },
149
+ scrollIntoView: true,
150
+ });
151
+ context.view.focus();
152
+ return true;
153
+ },
154
+ };
155
+ }
156
+
157
+ export function getDefaultSlashCommands(locale: MardoraLocale = "zh-CN"): MardoraSlashCommand[] {
158
+ return [
159
+ markdownCommand(commandMeta(locale, "paragraph", "basic", "type", ""), "", 0),
160
+ markdownCommand(commandMeta(locale, "heading-1", "basic", "heading-1", "#"), "# "),
161
+ markdownCommand(commandMeta(locale, "heading-2", "basic", "heading-2", "##"), "## "),
162
+ markdownCommand(commandMeta(locale, "heading-3", "basic", "heading-3", "###"), "### "),
163
+ markdownCommand(commandMeta(locale, "heading-4", "basic", "heading-4", "####"), "#### "),
164
+ markdownCommand(commandMeta(locale, "heading-5", "basic", "heading-5", "#####"), "##### "),
165
+ markdownCommand(commandMeta(locale, "heading-6", "basic", "heading-6", "######"), "###### "),
166
+ markdownCommand(commandMeta(locale, "quote", "basic", "text-quote", ">"), "> "),
167
+ calloutCommand(locale, "callout-note", "info", "NOTE"),
168
+ calloutCommand(locale, "callout-tip", "lightbulb", "TIP"),
169
+ calloutCommand(locale, "callout-important", "badge-alert", "IMPORTANT"),
170
+ calloutCommand(locale, "callout-warning", "triangle-alert", "WARNING"),
171
+ calloutCommand(locale, "callout-caution", "octagon-alert", "CAUTION"),
172
+ markdownCommand(commandMeta(locale, "code-block", "basic", "code-xml", "```"), "```\n\n```", 4),
173
+ markdownCommand(commandMeta(locale, "ordered-list", "basic", "list-ordered", "1."), "1. "),
174
+ markdownCommand(commandMeta(locale, "unordered-list", "basic", "list", "-"), "- "),
175
+ markdownCommand(commandMeta(locale, "task-list", "basic", "list-todo", "[]"), "- [ ] "),
176
+ markdownCommand(
177
+ commandMeta(locale, "table", "basic", "table", "| |"),
178
+ "| Column 1 | Column 2 |\n| --- | --- |\n| | |\n",
179
+ 2
180
+ ),
181
+ markdownCommand(commandMeta(locale, "divider", "basic", "minus", "---"), "---\n"),
182
+ markdownCommand(commandMeta(locale, "link", "basic", "link", "[]()"), "[]()", 1),
183
+ mediaCommand(commandMeta(locale, "file", "media", "file", "file"), "file"),
184
+ mediaCommand(commandMeta(locale, "image", "media", "image", "img"), "image"),
185
+ mediaCommand(commandMeta(locale, "video", "media", "play", "video"), "video"),
186
+ mediaCommand(commandMeta(locale, "audio", "media", "music-2", "audio"), "audio"),
187
+ ];
188
+ }
189
+
190
+ export const defaultSlashCommands: MardoraSlashCommand[] = getDefaultSlashCommands("zh-CN");
@@ -0,0 +1,319 @@
1
+ import { Extension, Prec } from "@codemirror/state";
2
+ import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
3
+ import type { MardoraAttachmentKind, MardoraAttachmentUploader } from "../attachments";
4
+ import { uploadAttachmentFile } from "../attachments";
5
+ import type { MardoraLocale } from "../i18n";
6
+ import { resolveMardoraLocale } from "../i18n";
7
+ import { getDefaultSlashCommands } from "./default-commands";
8
+ import { createSlashMenuElement, getSlashMessages } from "./menu";
9
+ import { computeSlashMenuLayout } from "./position";
10
+ import { detectSlashQuery, filterSlashCommands } from "./query";
11
+ import { slashMenuTheme } from "./theme";
12
+ import type {
13
+ MardoraSlashCommand,
14
+ MardoraSlashCommandsConfig,
15
+ MardoraSlashMessages,
16
+ MardoraSlashQuery,
17
+ } from "./types";
18
+
19
+ export type MardoraSlashRuntimeConfig = MardoraSlashCommandsConfig & {
20
+ attachmentUploader?: MardoraAttachmentUploader | undefined;
21
+ inheritedLocale?: MardoraLocale | undefined;
22
+ };
23
+
24
+ export type ResolvedMardoraSlashRuntimeConfig = Required<Pick<MardoraSlashRuntimeConfig, "commands">> &
25
+ MardoraSlashRuntimeConfig & {
26
+ locale: MardoraLocale;
27
+ messages: MardoraSlashMessages;
28
+ };
29
+
30
+ export function createSlashRuntimeConfig(config: MardoraSlashRuntimeConfig = {}): ResolvedMardoraSlashRuntimeConfig {
31
+ const locale = resolveMardoraLocale(config.locale ?? config.inheritedLocale);
32
+ return {
33
+ ...config,
34
+ locale,
35
+ commands: config.commands ?? getDefaultSlashCommands(locale),
36
+ messages: getSlashMessages(locale),
37
+ };
38
+ }
39
+
40
+ function requestFile(kind: MardoraAttachmentKind): Promise<File | null> {
41
+ return new Promise((resolve) => {
42
+ const input = document.createElement("input");
43
+ input.type = "file";
44
+ input.accept = kind === "image" ? "image/*" : kind === "video" ? "video/*" : kind === "audio" ? "audio/*" : "*/*";
45
+ input.addEventListener("change", () => resolve(input.files?.[0] ?? null), { once: true });
46
+ input.click();
47
+ });
48
+ }
49
+
50
+ class SlashCommandViewPlugin {
51
+ private query: MardoraSlashQuery | null = null;
52
+ private commands: MardoraSlashCommand[] = [];
53
+ private activeIndex = 0;
54
+ private menu: HTMLElement | null = null;
55
+ private renderVersion = 0;
56
+ private previousCaretColor: string | null = null;
57
+
58
+ constructor(
59
+ private readonly view: EditorView,
60
+ private readonly config: ResolvedMardoraSlashRuntimeConfig
61
+ ) {
62
+ this.view.dom.ownerDocument.addEventListener("keydown", this.handleDocumentKeydown, true);
63
+ this.updateState();
64
+ }
65
+
66
+ update(update: ViewUpdate): void {
67
+ if (update.docChanged || update.selectionSet || update.viewportChanged) {
68
+ this.updateState();
69
+ }
70
+ }
71
+
72
+ destroy(): void {
73
+ this.view.dom.ownerDocument.removeEventListener("keydown", this.handleDocumentKeydown, true);
74
+ this.renderVersion += 1;
75
+ this.removeMenu();
76
+ }
77
+
78
+ move(delta: number): boolean {
79
+ if (!this.query || this.commands.length === 0) return false;
80
+ this.activeIndex = (this.activeIndex + delta + this.commands.length) % this.commands.length;
81
+ this.syncActiveItem("center");
82
+ return true;
83
+ }
84
+
85
+ close(): boolean {
86
+ if (!this.query) return false;
87
+ this.query = null;
88
+ this.commands = [];
89
+ this.renderVersion += 1;
90
+ this.removeMenu();
91
+ return true;
92
+ }
93
+
94
+ selectActive(): boolean {
95
+ if (!this.query || this.commands.length === 0) return false;
96
+ return this.select(this.activeIndex);
97
+ }
98
+
99
+ handleKeydown(event: KeyboardEvent): boolean {
100
+ if (event.key === "ArrowDown") {
101
+ return this.move(1);
102
+ }
103
+
104
+ if (event.key === "ArrowUp") {
105
+ return this.move(-1);
106
+ }
107
+
108
+ if (event.key === "Enter") {
109
+ return this.selectActive();
110
+ }
111
+
112
+ if (event.key === "Escape") {
113
+ return this.close();
114
+ }
115
+
116
+ return false;
117
+ }
118
+
119
+ private select(index: number): boolean {
120
+ const command = this.commands[index];
121
+ if (!this.query || !command) return false;
122
+
123
+ const queryRange = { from: this.query.from, to: this.query.to };
124
+ this.close();
125
+
126
+ return command.run({
127
+ view: this.view,
128
+ queryRange,
129
+ requestAttachment: (kind, context) => this.requestAttachment(kind, context.queryRange),
130
+ });
131
+ }
132
+
133
+ private requestAttachment(kind: MardoraAttachmentKind, queryRange: { from: number; to: number }): boolean {
134
+ if (!this.config.attachmentUploader) {
135
+ return false;
136
+ }
137
+
138
+ void requestFile(kind).then((file) => {
139
+ if (!file || !this.config.attachmentUploader) return;
140
+ void uploadAttachmentFile(this.view, file, {
141
+ kind,
142
+ source: "slash",
143
+ range: queryRange,
144
+ uploader: this.config.attachmentUploader,
145
+ });
146
+ });
147
+
148
+ return true;
149
+ }
150
+
151
+ private readonly handleDocumentKeydown = (event: KeyboardEvent): void => {
152
+ if (!this.query || event.defaultPrevented) return;
153
+
154
+ const handled = this.handleKeydown(event);
155
+ if (!handled) return;
156
+
157
+ event.preventDefault();
158
+ event.stopPropagation();
159
+ };
160
+
161
+ private updateState(): void {
162
+ const cursor = this.view.state.selection.main.head;
163
+ const query = detectSlashQuery(this.view.state.doc.toString(), cursor);
164
+
165
+ if (!query) {
166
+ this.query = null;
167
+ this.commands = [];
168
+ this.renderVersion += 1;
169
+ this.removeMenu();
170
+ return;
171
+ }
172
+
173
+ if (!this.query || this.query.query !== query.query || this.query.from !== query.from) {
174
+ this.activeIndex = 0;
175
+ }
176
+
177
+ this.query = query;
178
+ this.commands = filterSlashCommands(this.config.commands, query.query);
179
+ this.activeIndex = Math.min(this.activeIndex, Math.max(0, this.commands.length - 1));
180
+ this.renderMenu();
181
+ }
182
+
183
+ private renderMenu(): void {
184
+ if (!this.query) return;
185
+ const renderVersion = ++this.renderVersion;
186
+ const queryFrom = this.query.from;
187
+ this.removeMenu();
188
+
189
+ this.view.requestMeasure({
190
+ read: (view) => view.coordsAtPos(queryFrom),
191
+ write: (coords) => {
192
+ if (renderVersion !== this.renderVersion || !this.query || !coords) return;
193
+
194
+ this.removeMenu();
195
+ this.menu = createSlashMenuElement(
196
+ { commands: this.commands, activeIndex: this.activeIndex, messages: this.config.messages },
197
+ {
198
+ onHover: (index) => {
199
+ this.activeIndex = index;
200
+ this.syncActiveItem("nearest");
201
+ },
202
+ onSelect: (index) => {
203
+ this.select(index);
204
+ },
205
+ }
206
+ );
207
+
208
+ const layout = computeSlashMenuLayout({
209
+ anchor: { left: coords.left, top: coords.top, bottom: coords.bottom },
210
+ viewport: {
211
+ width: this.view.dom.ownerDocument.defaultView?.innerWidth ?? window.innerWidth,
212
+ height: this.view.dom.ownerDocument.defaultView?.innerHeight ?? window.innerHeight,
213
+ },
214
+ });
215
+ this.menu.dataset.mardoraSlashPlacement = layout.placement;
216
+ this.menu.style.left = `${layout.left}px`;
217
+ this.menu.style.maxHeight = `${layout.maxHeight}px`;
218
+ this.menu.style.top = layout.top === null ? "" : `${layout.top}px`;
219
+ this.menu.style.bottom = layout.bottom === null ? "" : `${layout.bottom}px`;
220
+ this.view.dom.classList.add("cm-mardora-slash-open");
221
+ this.hideEditorCaret();
222
+ this.view.dom.appendChild(this.menu);
223
+ this.syncActiveItem("nearest");
224
+ },
225
+ });
226
+ }
227
+
228
+ private syncActiveItem(scrollMode: "center" | "nearest"): void {
229
+ if (!this.menu) return;
230
+
231
+ const items = this.menu.querySelectorAll<HTMLElement>(".cm-mardora-slash-item");
232
+ let activeItem: HTMLElement | null = null;
233
+
234
+ items.forEach((item) => {
235
+ const itemIndex = Number(item.dataset.mardoraSlashIndex);
236
+ const isActive = itemIndex === this.activeIndex;
237
+ item.classList.toggle("cm-mardora-slash-item-active", isActive);
238
+ item.setAttribute("aria-selected", String(isActive));
239
+ if (isActive) {
240
+ activeItem = item;
241
+ }
242
+ });
243
+
244
+ if (activeItem) {
245
+ this.scrollActiveItemIntoView(activeItem, scrollMode);
246
+ }
247
+ }
248
+
249
+ private scrollActiveItemIntoView(activeItem: HTMLElement, mode: "center" | "nearest"): void {
250
+ const list = this.menu?.querySelector<HTMLElement>(".cm-mardora-slash-list");
251
+ if (!list) return;
252
+
253
+ const itemTop = activeItem.offsetTop;
254
+ const itemBottom = itemTop + activeItem.offsetHeight;
255
+ const visibleTop = list.scrollTop;
256
+ const visibleBottom = visibleTop + list.clientHeight;
257
+
258
+ if (mode === "center") {
259
+ list.scrollTop = itemTop - (list.clientHeight - activeItem.offsetHeight) / 2;
260
+ return;
261
+ }
262
+
263
+ if (itemTop < visibleTop) {
264
+ list.scrollTop = itemTop;
265
+ return;
266
+ }
267
+
268
+ if (itemBottom > visibleBottom) {
269
+ list.scrollTop = itemBottom - list.clientHeight;
270
+ }
271
+ }
272
+
273
+ private hideEditorCaret(): void {
274
+ if (this.previousCaretColor === null) {
275
+ this.previousCaretColor = this.view.contentDOM.style.caretColor;
276
+ }
277
+ this.view.contentDOM.style.caretColor = "transparent";
278
+ }
279
+
280
+ private restoreEditorCaret(): void {
281
+ if (this.previousCaretColor === null) return;
282
+ this.view.contentDOM.style.caretColor = this.previousCaretColor;
283
+ this.previousCaretColor = null;
284
+ }
285
+
286
+ private removeMenu(): void {
287
+ this.menu?.remove();
288
+ this.menu = null;
289
+ this.view.dom.classList.remove("cm-mardora-slash-open");
290
+ this.restoreEditorCaret();
291
+ }
292
+ }
293
+
294
+ export function slashCommands(config: MardoraSlashRuntimeConfig = {}): Extension[] {
295
+ if (config.enabled === false) {
296
+ return [];
297
+ }
298
+
299
+ const normalizedConfig = createSlashRuntimeConfig(config);
300
+
301
+ const plugin = ViewPlugin.define((view) => new SlashCommandViewPlugin(view, normalizedConfig));
302
+
303
+ return [
304
+ slashMenuTheme,
305
+ plugin,
306
+ Prec.highest(
307
+ EditorView.domEventHandlers({
308
+ keydown(event, view) {
309
+ const value = view.plugin(plugin);
310
+ if (!value) return false;
311
+
312
+ const handled = value.handleKeydown(event);
313
+ if (handled) event.preventDefault();
314
+ return handled;
315
+ },
316
+ })
317
+ ),
318
+ ];
319
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types";
2
+ export * from "./query";
3
+ export * from "./insertions";
4
+ export * from "./default-commands";
5
+ export * from "./menu";
6
+ export * from "./theme";
7
+ export * from "./extension";