@dxpos/markdown 0.0.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 (93) hide show
  1. package/README.md +77 -0
  2. package/dist/editor/ColorMenu.d.ts +18 -0
  3. package/dist/editor/ColorMenu.d.ts.map +1 -0
  4. package/dist/editor/ColorMenu.js +90 -0
  5. package/dist/editor/SlashMenu.d.ts +24 -0
  6. package/dist/editor/SlashMenu.d.ts.map +1 -0
  7. package/dist/editor/SlashMenu.js +179 -0
  8. package/dist/editor/TextStyleMenu.d.ts +45 -0
  9. package/dist/editor/TextStyleMenu.d.ts.map +1 -0
  10. package/dist/editor/TextStyleMenu.js +65 -0
  11. package/dist/editor/VideoEmbed.d.ts +15 -0
  12. package/dist/editor/VideoEmbed.d.ts.map +1 -0
  13. package/dist/editor/VideoEmbed.js +95 -0
  14. package/dist/editor/VideoMenu.d.ts +12 -0
  15. package/dist/editor/VideoMenu.d.ts.map +1 -0
  16. package/dist/editor/VideoMenu.js +35 -0
  17. package/dist/editor/color-menu/index.d.ts +19 -0
  18. package/dist/editor/color-menu/index.d.ts.map +1 -0
  19. package/dist/editor/color-menu/index.js +90 -0
  20. package/dist/editor/color-menu/text-color-presets.d.ts +9 -0
  21. package/dist/editor/color-menu/text-color-presets.d.ts.map +1 -0
  22. package/dist/editor/color-menu/text-color-presets.js +46 -0
  23. package/dist/editor/index.d.ts +12 -0
  24. package/dist/editor/index.d.ts.map +1 -0
  25. package/dist/editor/index.js +314 -0
  26. package/dist/editor/letter-spacing/index.d.ts +17 -0
  27. package/dist/editor/letter-spacing/index.d.ts.map +1 -0
  28. package/dist/editor/letter-spacing/index.js +35 -0
  29. package/dist/editor/letter-spacing.d.ts +17 -0
  30. package/dist/editor/letter-spacing.d.ts.map +1 -0
  31. package/dist/editor/letter-spacing.js +35 -0
  32. package/dist/editor/slash-menu/index.d.ts +25 -0
  33. package/dist/editor/slash-menu/index.d.ts.map +1 -0
  34. package/dist/editor/slash-menu/index.js +179 -0
  35. package/dist/editor/text-color-presets.d.ts +9 -0
  36. package/dist/editor/text-color-presets.d.ts.map +1 -0
  37. package/dist/editor/text-color-presets.js +46 -0
  38. package/dist/editor/text-style-menu/index.d.ts +46 -0
  39. package/dist/editor/text-style-menu/index.d.ts.map +1 -0
  40. package/dist/editor/text-style-menu/index.js +65 -0
  41. package/dist/editor/theme.d.ts +8 -0
  42. package/dist/editor/theme.d.ts.map +1 -0
  43. package/dist/editor/theme.js +18 -0
  44. package/dist/editor/toolbar/index.d.ts +17 -0
  45. package/dist/editor/toolbar/index.d.ts.map +1 -0
  46. package/dist/editor/toolbar/index.js +206 -0
  47. package/dist/editor/toolbar/items.d.ts +10 -0
  48. package/dist/editor/toolbar/items.d.ts.map +1 -0
  49. package/dist/editor/toolbar/items.js +34 -0
  50. package/dist/editor/toolbar/theme.d.ts +8 -0
  51. package/dist/editor/toolbar/theme.d.ts.map +1 -0
  52. package/dist/editor/toolbar/theme.js +18 -0
  53. package/dist/editor/toolbar-items.d.ts +10 -0
  54. package/dist/editor/toolbar-items.d.ts.map +1 -0
  55. package/dist/editor/toolbar-items.js +34 -0
  56. package/dist/editor/toolbar.d.ts +16 -0
  57. package/dist/editor/toolbar.d.ts.map +1 -0
  58. package/dist/editor/toolbar.js +206 -0
  59. package/dist/editor/use-dom-id.d.ts +3 -0
  60. package/dist/editor/use-dom-id.d.ts.map +1 -0
  61. package/dist/editor/use-dom-id.js +11 -0
  62. package/dist/editor/video-embed/index.d.ts +15 -0
  63. package/dist/editor/video-embed/index.d.ts.map +1 -0
  64. package/dist/editor/video-embed/index.js +95 -0
  65. package/dist/editor/video-embed/video-url.d.ts +8 -0
  66. package/dist/editor/video-embed/video-url.d.ts.map +1 -0
  67. package/dist/editor/video-embed/video-url.js +102 -0
  68. package/dist/editor/video-menu/index.d.ts +13 -0
  69. package/dist/editor/video-menu/index.d.ts.map +1 -0
  70. package/dist/editor/video-menu/index.js +35 -0
  71. package/dist/editor/video-url.d.ts +8 -0
  72. package/dist/editor/video-url.d.ts.map +1 -0
  73. package/dist/editor/video-url.js +102 -0
  74. package/dist/index.d.ts +5 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +4 -0
  77. package/dist/plugin.d.ts +6 -0
  78. package/dist/plugin.d.ts.map +1 -0
  79. package/dist/plugin.js +1 -0
  80. package/dist/plugins/gfm/index.d.ts +3 -0
  81. package/dist/plugins/gfm/index.d.ts.map +1 -0
  82. package/dist/plugins/gfm/index.js +2 -0
  83. package/dist/plugins/highlight/index.d.ts +3 -0
  84. package/dist/plugins/highlight/index.d.ts.map +1 -0
  85. package/dist/plugins/highlight/index.js +2 -0
  86. package/dist/plugins/raw/index.d.ts +3 -0
  87. package/dist/plugins/raw/index.d.ts.map +1 -0
  88. package/dist/plugins/raw/index.js +2 -0
  89. package/dist/view/index.d.ts +9 -0
  90. package/dist/view/index.d.ts.map +1 -0
  91. package/dist/view/index.js +18 -0
  92. package/package.json +61 -0
  93. package/styles.css +760 -0
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_TOOLBAR: readonly ["paragraph", "h1", "h2", "h3", "h4", "h5", "h6", "divider", "bold", "italic", "strike", "highlight", "color", "textStyle", "divider", "list:bullet", "list:ordered", "quote", "code", "divider", "align:left", "align:center", "align:right", "align:justify", "divider", "link", "table", "image", "video", "spacer", "undo", "redo"];
2
+ export type ToolbarItem = (typeof DEFAULT_TOOLBAR)[number] | "theme";
3
+ export type UploadFileType = "image" | "video" | "file";
4
+ export type UploadFileResult = {
5
+ id: number | string;
6
+ url: string;
7
+ extra: Record<string, any>;
8
+ };
9
+ export type UploadFile = (file: File, type: UploadFileType) => Promise<UploadFileResult>;
10
+ //# sourceMappingURL=items.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/items.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,kVAiClB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAErE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC"}
@@ -0,0 +1,34 @@
1
+ export const DEFAULT_TOOLBAR = [
2
+ "paragraph",
3
+ "h1",
4
+ "h2",
5
+ "h3",
6
+ "h4",
7
+ "h5",
8
+ "h6",
9
+ "divider",
10
+ "bold",
11
+ "italic",
12
+ "strike",
13
+ "highlight",
14
+ "color",
15
+ "textStyle",
16
+ "divider",
17
+ "list:bullet",
18
+ "list:ordered",
19
+ "quote",
20
+ "code",
21
+ "divider",
22
+ "align:left",
23
+ "align:center",
24
+ "align:right",
25
+ "align:justify",
26
+ "divider",
27
+ "link",
28
+ "table",
29
+ "image",
30
+ "video",
31
+ "spacer",
32
+ "undo",
33
+ "redo",
34
+ ];
@@ -0,0 +1,8 @@
1
+ export declare const THEME_STORAGE_KEY = "kb-theme";
2
+ export type ColorTheme = "light" | "dark";
3
+ export type ThemePreference = ColorTheme | "system";
4
+ export declare const isColorTheme: (value: string | null) => value is ColorTheme;
5
+ /** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
6
+ export declare const applyThemePreference: (preference: ThemePreference) => void;
7
+ export declare const resolveColorTheme: (preference: ThemePreference) => ColorTheme;
8
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/theme.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAC1C,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEpD,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,GAAG,IAAI,KAAG,KAAK,IAAI,UAE5D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,oBAAoB,GAAI,YAAY,eAAe,SAM/D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,eAAe,KAAG,UAK/D,CAAC"}
@@ -0,0 +1,18 @@
1
+ export const THEME_STORAGE_KEY = "kb-theme";
2
+ export const isColorTheme = (value) => {
3
+ return value === "light" || value === "dark";
4
+ };
5
+ /** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
6
+ export const applyThemePreference = (preference) => {
7
+ if (preference === "system") {
8
+ document.documentElement.removeAttribute("data-theme");
9
+ return;
10
+ }
11
+ document.documentElement.dataset.theme = preference;
12
+ };
13
+ export const resolveColorTheme = (preference) => {
14
+ if (preference !== "system") {
15
+ return preference;
16
+ }
17
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
18
+ };
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_TOOLBAR: readonly ["paragraph", "h1", "h2", "h3", "h4", "h5", "h6", "divider", "bold", "italic", "strike", "highlight", "color", "textStyle", "divider", "list:bullet", "list:ordered", "quote", "code", "divider", "align:left", "align:center", "align:right", "align:justify", "divider", "link", "table", "image", "video", "spacer", "undo", "redo"];
2
+ export type ToolbarItem = (typeof DEFAULT_TOOLBAR)[number] | "theme";
3
+ export type UploadFileType = "image" | "video" | "file";
4
+ export type UploadFileResult = {
5
+ id: number | string;
6
+ url: string;
7
+ extra: Record<string, any>;
8
+ };
9
+ export type UploadFile = (file: File, type: UploadFileType) => Promise<UploadFileResult>;
10
+ //# sourceMappingURL=toolbar-items.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolbar-items.d.ts","sourceRoot":"","sources":["../../src/editor/toolbar-items.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,kVAiClB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAErE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC"}
@@ -0,0 +1,34 @@
1
+ export const DEFAULT_TOOLBAR = [
2
+ "paragraph",
3
+ "h1",
4
+ "h2",
5
+ "h3",
6
+ "h4",
7
+ "h5",
8
+ "h6",
9
+ "divider",
10
+ "bold",
11
+ "italic",
12
+ "strike",
13
+ "highlight",
14
+ "color",
15
+ "textStyle",
16
+ "divider",
17
+ "list:bullet",
18
+ "list:ordered",
19
+ "quote",
20
+ "code",
21
+ "divider",
22
+ "align:left",
23
+ "align:center",
24
+ "align:right",
25
+ "align:justify",
26
+ "divider",
27
+ "link",
28
+ "table",
29
+ "image",
30
+ "video",
31
+ "spacer",
32
+ "undo",
33
+ "redo",
34
+ ];
@@ -0,0 +1,16 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ import { type ToolbarItem } from "./toolbar-items";
3
+ type ToolbarProps = {
4
+ editor: Editor;
5
+ items: readonly ToolbarItem[];
6
+ uploading: boolean;
7
+ videoOpen: boolean;
8
+ videoError: string | null;
9
+ onInsertImage: () => void;
10
+ onVideoOpenChange: (open: boolean) => void;
11
+ onUploadVideo: () => void;
12
+ onInsertVideoUrl: (url: string) => void;
13
+ };
14
+ export declare function Toolbar({ editor, items, uploading, videoOpen, videoError, onInsertImage, onVideoOpenChange, onUploadVideo, onInsertVideoUrl, }: ToolbarProps): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=Toolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/editor/Toolbar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAiD5C,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAcpE,KAAK,YAAY,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,iBAAiB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,CAAC;AAuEF,wBAAgB,OAAO,CAAC,EACtB,MAAM,EACN,KAAuB,EACvB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,gBAAgB,GACjB,EAAE,YAAY,+BAmXd"}
@@ -0,0 +1,206 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEditorState } from "@tiptap/react";
4
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Baseline, Bold, Code, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImageIcon, Italic, Link as LinkIcon, List, ListOrdered, Loader2, Moon, Pilcrow, Quote, Redo2, Strikethrough, Sun, Table, Undo2, } from "lucide-react";
5
+ import { useEffect, useState } from "react";
6
+ import { ColorMenu, PRESET_HIGHLIGHT_COLORS, PRESET_TEXT_COLORS } from "./ColorMenu";
7
+ import { TextStyleMenu } from "./TextStyleMenu";
8
+ import { readCustomTextColors, rememberCustomTextColor, removeCustomTextColor, writeCustomTextColors, } from "./text-color-presets";
9
+ import { applyThemePreference, isColorTheme, resolveColorTheme, THEME_STORAGE_KEY, } from "./theme";
10
+ import { DEFAULT_TOOLBAR } from "./toolbar-items";
11
+ import { VideoMenu } from "./VideoMenu";
12
+ const HEADING_ICONS = {
13
+ 1: Heading1,
14
+ 2: Heading2,
15
+ 3: Heading3,
16
+ 4: Heading4,
17
+ 5: Heading5,
18
+ 6: Heading6,
19
+ };
20
+ function ToolbarButton({ title, active, disabled, onClick, children }) {
21
+ return (_jsx("button", { className: `icon-btn icon-btn--ghost toolbar__btn${active ? " toolbar__btn--active" : ""}`, type: "button", title: title, "aria-label": title, "aria-pressed": active, disabled: disabled, onClick: onClick, children: children }));
22
+ }
23
+ function useToolbarTheme() {
24
+ const [preference, setPreference] = useState("system");
25
+ const [resolved, setResolved] = useState("light");
26
+ useEffect(() => {
27
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
28
+ const next = isColorTheme(stored) ? stored : "system";
29
+ setPreference(next);
30
+ setResolved(resolveColorTheme(next));
31
+ applyThemePreference(next);
32
+ }, []);
33
+ useEffect(() => {
34
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
35
+ const sync = () => setResolved(resolveColorTheme(preference));
36
+ media.addEventListener("change", sync);
37
+ return () => media.removeEventListener("change", sync);
38
+ }, [preference]);
39
+ const toggle = () => {
40
+ const next = resolved === "dark" ? "light" : "dark";
41
+ localStorage.setItem(THEME_STORAGE_KEY, next);
42
+ setPreference(next);
43
+ setResolved(next);
44
+ applyThemePreference(next);
45
+ };
46
+ return { resolved, toggle };
47
+ }
48
+ function textStyleValue(value) {
49
+ return typeof value === "string" && value ? value : null;
50
+ }
51
+ function headingLevel(item) {
52
+ if (!item.startsWith("h")) {
53
+ return null;
54
+ }
55
+ const level = Number(item.slice(1));
56
+ if (level < 1 || level > 6) {
57
+ return null;
58
+ }
59
+ return level;
60
+ }
61
+ export function Toolbar({ editor, items = DEFAULT_TOOLBAR, uploading, videoOpen, videoError, onInsertImage, onVideoOpenChange, onUploadVideo, onInsertVideoUrl, }) {
62
+ const { resolved, toggle } = useToolbarTheme();
63
+ const [customTextColors, setCustomTextColors] = useState([]);
64
+ const state = useEditorState({
65
+ editor,
66
+ selector: (ctx) => ({
67
+ canUndo: ctx.editor.can().undo(),
68
+ canRedo: ctx.editor.can().redo(),
69
+ isParagraph: ctx.editor.isActive("paragraph"),
70
+ headingLevels: [1, 2, 3, 4, 5, 6].map((level) => ctx.editor.isActive("heading", { level })),
71
+ isBold: ctx.editor.isActive("bold"),
72
+ isItalic: ctx.editor.isActive("italic"),
73
+ isStrike: ctx.editor.isActive("strike"),
74
+ isHighlight: ctx.editor.isActive("highlight"),
75
+ highlightColor: ctx.editor.getAttributes("highlight").color ?? null,
76
+ textColor: textStyleValue(ctx.editor.getAttributes("textStyle").color),
77
+ fontFamily: textStyleValue(ctx.editor.getAttributes("textStyle").fontFamily),
78
+ fontSize: textStyleValue(ctx.editor.getAttributes("textStyle").fontSize),
79
+ letterSpacing: textStyleValue(ctx.editor.getAttributes("textStyle").letterSpacing),
80
+ lineHeight: textStyleValue(ctx.editor.getAttributes("textStyle").lineHeight),
81
+ isBulletList: ctx.editor.isActive("bulletList"),
82
+ isOrderedList: ctx.editor.isActive("orderedList"),
83
+ isBlockquote: ctx.editor.isActive("blockquote"),
84
+ isCodeBlock: ctx.editor.isActive("codeBlock"),
85
+ isAlignLeft: ctx.editor.isActive({ textAlign: "left" }),
86
+ isAlignCenter: ctx.editor.isActive({ textAlign: "center" }),
87
+ isAlignRight: ctx.editor.isActive({ textAlign: "right" }),
88
+ isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }),
89
+ isLink: ctx.editor.isActive("link"),
90
+ }),
91
+ });
92
+ useEffect(() => {
93
+ setCustomTextColors(readCustomTextColors());
94
+ }, []);
95
+ const setLink = () => {
96
+ const url = window.prompt("链接地址", editor.getAttributes("link").href ?? "");
97
+ if (url === null) {
98
+ return;
99
+ }
100
+ if (url === "") {
101
+ editor.chain().focus().unsetLink().run();
102
+ return;
103
+ }
104
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
105
+ };
106
+ const renderItem = (item, index) => {
107
+ const level = headingLevel(item);
108
+ if (level) {
109
+ const Icon = HEADING_ICONS[level];
110
+ return (_jsx(ToolbarButton, { title: `标题 ${level}`, active: state.headingLevels[level - 1], onClick: () => editor.chain().focus().toggleHeading({ level }).run(), children: _jsx(Icon, { "aria-hidden": true }) }, `${item}-${index}`));
111
+ }
112
+ switch (item) {
113
+ case "divider":
114
+ return _jsx("span", { className: "toolbar__divider" }, `${item}-${index}`);
115
+ case "spacer":
116
+ return _jsx("span", { className: "toolbar__spacer" }, `${item}-${index}`);
117
+ case "paragraph":
118
+ return (_jsx(ToolbarButton, { title: "\u6BB5\u843D", active: state.isParagraph, onClick: () => editor.chain().focus().setParagraph().run(), children: _jsx(Pilcrow, { "aria-hidden": true }) }, item));
119
+ case "bold":
120
+ return (_jsx(ToolbarButton, { title: "\u7C97\u4F53", active: state.isBold, onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { "aria-hidden": true }) }, item));
121
+ case "italic":
122
+ return (_jsx(ToolbarButton, { title: "\u659C\u4F53", active: state.isItalic, onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { "aria-hidden": true }) }, item));
123
+ case "strike":
124
+ return (_jsx(ToolbarButton, { title: "\u5220\u9664\u7EBF", active: state.isStrike, onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { "aria-hidden": true }) }, item));
125
+ case "highlight":
126
+ return (_jsx(ColorMenu, { label: "\u9AD8\u4EAE", active: state.isHighlight, currentColor: state.highlightColor, presets: PRESET_HIGHLIGHT_COLORS, onPick: (color) => editor.chain().focus().setHighlight({ color }).run(), onClear: () => editor.chain().focus().unsetHighlight().run(), children: _jsx(Highlighter, { "aria-hidden": true }) }, item));
127
+ case "color":
128
+ return (_jsx(ColorMenu, { label: "\u6587\u5B57\u989C\u8272", active: Boolean(state.textColor), currentColor: state.textColor, presets: PRESET_TEXT_COLORS, customPresets: customTextColors, onPick: (color) => editor.chain().focus().setColor(color).run(), onRemember: (color) => {
129
+ setCustomTextColors((current) => {
130
+ const next = rememberCustomTextColor(current, color, PRESET_TEXT_COLORS);
131
+ writeCustomTextColors(next);
132
+ return next;
133
+ });
134
+ }, onRemoveCustom: (color) => {
135
+ setCustomTextColors((current) => {
136
+ const next = removeCustomTextColor(current, color);
137
+ writeCustomTextColors(next);
138
+ return next;
139
+ });
140
+ }, onClear: () => editor.chain().focus().unsetColor().run(), children: _jsx(Baseline, { "aria-hidden": true }) }, item));
141
+ case "textStyle":
142
+ return (_jsx(TextStyleMenu, { fontFamily: state.fontFamily, fontSize: state.fontSize, letterSpacing: state.letterSpacing, lineHeight: state.lineHeight, onFontFamily: (value) => {
143
+ const chain = editor.chain().focus();
144
+ if (value) {
145
+ chain.setFontFamily(value).run();
146
+ return;
147
+ }
148
+ chain.unsetFontFamily().run();
149
+ }, onFontSize: (value) => {
150
+ const chain = editor.chain().focus();
151
+ if (value) {
152
+ chain.setFontSize(value).run();
153
+ return;
154
+ }
155
+ chain.unsetFontSize().run();
156
+ }, onLetterSpacing: (value) => {
157
+ const chain = editor.chain().focus();
158
+ if (value) {
159
+ chain.setLetterSpacing(value).run();
160
+ return;
161
+ }
162
+ chain.unsetLetterSpacing().run();
163
+ }, onLineHeight: (value) => {
164
+ const chain = editor.chain().focus();
165
+ if (value) {
166
+ chain.setLineHeight(value).run();
167
+ return;
168
+ }
169
+ chain.unsetLineHeight().run();
170
+ } }, item));
171
+ case "list:bullet":
172
+ return (_jsx(ToolbarButton, { title: "\u65E0\u5E8F\u5217\u8868", active: state.isBulletList, onClick: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, { "aria-hidden": true }) }, item));
173
+ case "list:ordered":
174
+ return (_jsx(ToolbarButton, { title: "\u6709\u5E8F\u5217\u8868", active: state.isOrderedList, onClick: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { "aria-hidden": true }) }, item));
175
+ case "quote":
176
+ return (_jsx(ToolbarButton, { title: "\u5F15\u7528", active: state.isBlockquote, onClick: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { "aria-hidden": true }) }, item));
177
+ case "code":
178
+ return (_jsx(ToolbarButton, { title: "\u4EE3\u7801\u5757", active: state.isCodeBlock, onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code, { "aria-hidden": true }) }, item));
179
+ case "align:left":
180
+ return (_jsx(ToolbarButton, { title: "\u5DE6\u5BF9\u9F50", active: state.isAlignLeft, onClick: () => editor.chain().focus().setTextAlign("left").run(), children: _jsx(AlignLeft, { "aria-hidden": true }) }, item));
181
+ case "align:center":
182
+ return (_jsx(ToolbarButton, { title: "\u5C45\u4E2D", active: state.isAlignCenter, onClick: () => editor.chain().focus().setTextAlign("center").run(), children: _jsx(AlignCenter, { "aria-hidden": true }) }, item));
183
+ case "align:right":
184
+ return (_jsx(ToolbarButton, { title: "\u53F3\u5BF9\u9F50", active: state.isAlignRight, onClick: () => editor.chain().focus().setTextAlign("right").run(), children: _jsx(AlignRight, { "aria-hidden": true }) }, item));
185
+ case "align:justify":
186
+ return (_jsx(ToolbarButton, { title: "\u4E24\u7AEF\u5BF9\u9F50", active: state.isAlignJustify, onClick: () => editor.chain().focus().setTextAlign("justify").run(), children: _jsx(AlignJustify, { "aria-hidden": true }) }, item));
187
+ case "link":
188
+ return (_jsx(ToolbarButton, { title: "\u94FE\u63A5", active: state.isLink, onClick: setLink, children: _jsx(LinkIcon, { "aria-hidden": true }) }, item));
189
+ case "table":
190
+ return (_jsx(ToolbarButton, { title: "\u8868\u683C", onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(Table, { "aria-hidden": true }) }, item));
191
+ case "image":
192
+ return (_jsx(ToolbarButton, { title: uploading ? "上传中" : "图片", disabled: uploading, onClick: onInsertImage, children: uploading ? _jsx(Loader2, { className: "icon-btn__spin", "aria-hidden": true }) : _jsx(ImageIcon, { "aria-hidden": true }) }, item));
193
+ case "video":
194
+ return (_jsx(VideoMenu, { open: videoOpen, uploading: uploading, error: videoError, onOpenChange: onVideoOpenChange, onUpload: onUploadVideo, onSubmitUrl: onInsertVideoUrl }, item));
195
+ case "undo":
196
+ return (_jsx(ToolbarButton, { title: "\u64A4\u9500", disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { "aria-hidden": true }) }, item));
197
+ case "redo":
198
+ return (_jsx(ToolbarButton, { title: "\u6062\u590D", disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { "aria-hidden": true }) }, item));
199
+ case "theme":
200
+ return (_jsx(ToolbarButton, { title: resolved === "dark" ? "切换浅色" : "切换深色", onClick: toggle, children: resolved === "dark" ? _jsx(Sun, { "aria-hidden": true }) : _jsx(Moon, { "aria-hidden": true }) }, item));
201
+ default:
202
+ return null;
203
+ }
204
+ };
205
+ return (_jsx("div", { className: "toolbar", role: "toolbar", "aria-label": "\u683C\u5F0F", children: items.map((item, index) => renderItem(item, index)) }));
206
+ }
@@ -0,0 +1,3 @@
1
+ /** 生成组件实例内稳定的 DOM id */
2
+ export declare function useDomId(): string;
3
+ //# sourceMappingURL=use-dom-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-dom-id.d.ts","sourceRoot":"","sources":["../../src/editor/use-dom-id.ts"],"names":[],"mappings":"AAIA,wBAAwB;AACxB,wBAAgB,QAAQ,WAOvB"}
@@ -0,0 +1,11 @@
1
+ import { useRef } from "react";
2
+ let seed = 0;
3
+ /** 生成组件实例内稳定的 DOM id */
4
+ export function useDomId() {
5
+ const idRef = useRef(null);
6
+ if (idRef.current === null) {
7
+ seed += 1;
8
+ idRef.current = `wsmd-${seed}`;
9
+ }
10
+ return idRef.current;
11
+ }
@@ -0,0 +1,15 @@
1
+ import { Node } from "@tiptap/core";
2
+ import type { VideoProvider } from "./video-url";
3
+ declare module "@tiptap/core" {
4
+ interface Commands<ReturnType> {
5
+ videoEmbed: {
6
+ setVideoEmbed: (attrs: {
7
+ src: string;
8
+ provider: VideoProvider;
9
+ }) => ReturnType;
10
+ };
11
+ }
12
+ }
13
+ /** 块级视频:本地文件用 video,外站用官方播放器 iframe */
14
+ export declare const VideoEmbed: Node<any, any>;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/video-embed/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,UAAU,EAAE;YACV,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,QAAQ,EAAE,aAAa,CAAA;aAAE,KAAK,UAAU,CAAC;SAChF,CAAC;KACH;CACF;AAMD,uCAAuC;AACvC,eAAO,MAAM,UAAU,gBA8FrB,CAAC"}
@@ -0,0 +1,95 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { TextSelection } from "@tiptap/pm/state";
3
+ const attrString = (value) => {
4
+ return typeof value === "string" ? value : null;
5
+ };
6
+ /** 块级视频:本地文件用 video,外站用官方播放器 iframe */
7
+ export const VideoEmbed = Node.create({
8
+ name: "videoEmbed",
9
+ group: "block",
10
+ atom: true,
11
+ draggable: true,
12
+ selectable: true,
13
+ addAttributes() {
14
+ return {
15
+ src: { default: null },
16
+ provider: { default: "file" },
17
+ };
18
+ },
19
+ parseHTML() {
20
+ return [
21
+ {
22
+ tag: "video[src]",
23
+ getAttrs: (element) => {
24
+ if (!(element instanceof HTMLElement)) {
25
+ return false;
26
+ }
27
+ const src = element.getAttribute("src");
28
+ if (!src) {
29
+ return false;
30
+ }
31
+ return { src, provider: "file" };
32
+ },
33
+ },
34
+ {
35
+ tag: "iframe[data-video-provider]",
36
+ getAttrs: (element) => {
37
+ if (!(element instanceof HTMLElement)) {
38
+ return false;
39
+ }
40
+ const provider = element.getAttribute("data-video-provider");
41
+ const src = element.getAttribute("src");
42
+ if ((provider !== "youtube" && provider !== "bilibili") || !src) {
43
+ return false;
44
+ }
45
+ return { src, provider };
46
+ },
47
+ },
48
+ ];
49
+ },
50
+ renderHTML({ node }) {
51
+ const src = attrString(node.attrs.src);
52
+ const provider = node.attrs.provider;
53
+ if (!src) {
54
+ return ["video", { controls: "true", preload: "metadata" }];
55
+ }
56
+ if (provider === "youtube" || provider === "bilibili") {
57
+ return [
58
+ "iframe",
59
+ mergeAttributes({
60
+ src,
61
+ "data-video-provider": provider,
62
+ allowfullscreen: "true",
63
+ frameborder: "0",
64
+ }),
65
+ ];
66
+ }
67
+ return [
68
+ "video",
69
+ mergeAttributes({
70
+ src,
71
+ controls: "true",
72
+ preload: "metadata",
73
+ }),
74
+ ];
75
+ },
76
+ addCommands() {
77
+ return {
78
+ setVideoEmbed: (attrs) => ({ state, dispatch }) => {
79
+ const type = state.schema.nodes.videoEmbed;
80
+ if (!type) {
81
+ return false;
82
+ }
83
+ const node = type.create(attrs);
84
+ const $from = state.doc.resolve(state.selection.from);
85
+ const insertAt = $from.parent.inlineContent ? $from.after() : state.selection.to;
86
+ if (dispatch) {
87
+ const tr = state.tr.insert(insertAt, node);
88
+ const $after = tr.doc.resolve(Math.min(insertAt + node.nodeSize, tr.doc.content.size));
89
+ dispatch(tr.setSelection(TextSelection.near($after)).scrollIntoView());
90
+ }
91
+ return true;
92
+ },
93
+ };
94
+ },
95
+ });
@@ -0,0 +1,8 @@
1
+ export type VideoProvider = "file" | "youtube" | "bilibili";
2
+ export type VideoEmbedAttrs = {
3
+ provider: VideoProvider;
4
+ src: string;
5
+ };
6
+ /** 把用户粘贴的地址解析成可嵌入的视频节点属性 */
7
+ export declare const parseVideoUrl: (input: string) => VideoEmbedAttrs | null;
8
+ //# sourceMappingURL=video-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-url.d.ts","sourceRoot":"","sources":["../../../src/editor/video-embed/video-url.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AA0FF,4BAA4B;AAC5B,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,eAAe,GAAG,IAkB/D,CAAC"}
@@ -0,0 +1,102 @@
1
+ const YOUTUBE_ID = /^[\w-]{11}$/;
2
+ const BVID = /^BV[\w]+$/i;
3
+ const httpUrl = (input) => {
4
+ const trimmed = input.trim();
5
+ if (!trimmed) {
6
+ return null;
7
+ }
8
+ let url;
9
+ try {
10
+ url = new URL(trimmed);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
16
+ return null;
17
+ }
18
+ return url;
19
+ };
20
+ const bareHost = (url) => {
21
+ return url.hostname.replace(/^www\./, "").toLowerCase();
22
+ };
23
+ const youtubeId = (url) => {
24
+ const host = bareHost(url);
25
+ if (host === "youtu.be") {
26
+ const id = url.pathname.split("/").filter(Boolean)[0];
27
+ return id && YOUTUBE_ID.test(id) ? id : null;
28
+ }
29
+ if (host !== "youtube.com" &&
30
+ host !== "m.youtube.com" &&
31
+ host !== "music.youtube.com" &&
32
+ host !== "youtube-nocookie.com") {
33
+ return null;
34
+ }
35
+ if (url.pathname === "/watch") {
36
+ const id = url.searchParams.get("v");
37
+ return id && YOUTUBE_ID.test(id) ? id : null;
38
+ }
39
+ const parts = url.pathname.split("/").filter(Boolean);
40
+ if ((parts[0] === "embed" || parts[0] === "shorts") && parts[1] && YOUTUBE_ID.test(parts[1])) {
41
+ return parts[1];
42
+ }
43
+ return null;
44
+ };
45
+ const bilibiliEmbed = (url) => {
46
+ const host = bareHost(url);
47
+ if (host === "bilibili.com" || host === "m.bilibili.com") {
48
+ const bv = url.pathname.match(/\/video\/(BV[\w]+)/i);
49
+ if (bv?.[1] && BVID.test(bv[1])) {
50
+ return {
51
+ provider: "bilibili",
52
+ src: `https://player.bilibili.com/player.html?bvid=${bv[1]}&autoplay=0`,
53
+ };
54
+ }
55
+ const av = url.pathname.match(/\/video\/av(\d+)/i);
56
+ if (av?.[1]) {
57
+ return {
58
+ provider: "bilibili",
59
+ src: `https://player.bilibili.com/player.html?aid=${av[1]}&autoplay=0`,
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+ if (host !== "player.bilibili.com") {
65
+ return null;
66
+ }
67
+ const bvid = url.searchParams.get("bvid");
68
+ if (bvid && BVID.test(bvid)) {
69
+ return {
70
+ provider: "bilibili",
71
+ src: `https://player.bilibili.com/player.html?bvid=${bvid}&autoplay=0`,
72
+ };
73
+ }
74
+ const aid = url.searchParams.get("aid");
75
+ if (aid && /^\d+$/.test(aid)) {
76
+ return {
77
+ provider: "bilibili",
78
+ src: `https://player.bilibili.com/player.html?aid=${aid}&autoplay=0`,
79
+ };
80
+ }
81
+ return null;
82
+ };
83
+ /** 把用户粘贴的地址解析成可嵌入的视频节点属性 */
84
+ export const parseVideoUrl = (input) => {
85
+ const url = httpUrl(input);
86
+ if (!url) {
87
+ return null;
88
+ }
89
+ const youtube = youtubeId(url);
90
+ if (youtube) {
91
+ return { provider: "youtube", src: `https://www.youtube.com/embed/${youtube}` };
92
+ }
93
+ const bilibili = bilibiliEmbed(url);
94
+ if (bilibili) {
95
+ return bilibili;
96
+ }
97
+ const path = decodeURIComponent(url.pathname).toLowerCase();
98
+ if (path.endsWith(".mp4") || path.endsWith(".webm")) {
99
+ return { provider: "file", src: url.toString() };
100
+ }
101
+ return null;
102
+ };
@@ -0,0 +1,13 @@
1
+ import { type FC } from "react";
2
+ interface VideoMenuProps {
3
+ open: boolean;
4
+ uploading: boolean;
5
+ error: string | null;
6
+ onOpenChange: (open: boolean) => void;
7
+ onUpload: () => void;
8
+ onSubmitUrl: (url: string) => void;
9
+ }
10
+ /** 工具栏视频菜单:上传文件或粘贴链接 */
11
+ export declare const VideoMenu: FC<VideoMenuProps>;
12
+ export {};
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/video-menu/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEpE,UAAU,cAAc;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,wBAAwB;AACxB,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAyExC,CAAC"}