@wyttime04/react-vditor 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/Vditor.js +212 -0
- package/dist/VditorPreview.js +36 -0
- package/dist/cdn.js +9 -0
- package/dist/gfm.js +121 -0
- package/dist/index.css +2179 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +13 -0
- package/dist/theme.js +43 -0
- package/dist/uploadImage.js +51 -0
- package/package.json +77 -0
- package/webpack-plugin/index.cjs +44 -0
- package/webpack-plugin/index.d.ts +8 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// react-vditor 的型別。IOptions、IPreviewOptions 是 vditor 提供的全域型別,import vditor 時一併載入
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import type VditorCore from "vditor";
|
|
4
|
+
|
|
5
|
+
/** 主題。auto 跟著作業系統的深色模式 */
|
|
6
|
+
export type VditorTheme = "light" | "dark" | "auto";
|
|
7
|
+
|
|
8
|
+
/** 讓底下所有 <Vditor>、<VditorPreview> 預設用同一個主題;值變動時它們會跟著切換。元件自己的 theme prop 優先 */
|
|
9
|
+
export declare const VditorThemeProvider: (props: {theme: VditorTheme, children?: React.ReactNode}) => React.ReactElement;
|
|
10
|
+
|
|
11
|
+
/** ref 上可以呼叫的方法,名稱與原生 vditor 相同(destroy 由元件管理、setTheme 改用 theme prop,都不開放) */
|
|
12
|
+
export type VditorRef = Pick<VditorCore,
|
|
13
|
+
| "updateToolbarConfig" | "focus" | "blur" | "disabled" | "enable" | "renderPreview"
|
|
14
|
+
| "clearCache" | "disabledCache" | "enableCache" | "tip" | "setPreviewMode" | "deleteValue" | "updateValue"
|
|
15
|
+
| "insertValue" | "insertMD" | "setValue" | "insertEmptyBlock" | "clearStack"
|
|
16
|
+
| "hlCommentIds" | "unHlCommentIds" | "removeCommentIds"
|
|
17
|
+
| "getValue" | "getCurrentMode" | "getSelection" | "getCursorPosition" | "isUploading"
|
|
18
|
+
| "html2md" | "exportJSON" | "getHTML" | "getCommentIds">;
|
|
19
|
+
|
|
20
|
+
/** props 就是 vditor 的 IOptions(theme 換成 VditorTheme),另外加 className、style */
|
|
21
|
+
export type VditorProps = Omit<IOptions, "theme"> & {
|
|
22
|
+
/** 預設取最近的 VditorThemeProvider,沒有就是 light */
|
|
23
|
+
theme?: VditorTheme;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: React.CSSProperties;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** vditor 編輯器。value 是初始值;之後使用端改 value 會套用,打字觸發的 input 回傳值不會被蓋回去 */
|
|
29
|
+
export declare const Vditor: React.ForwardRefExoticComponent<VditorProps & React.RefAttributes<VditorRef>>;
|
|
30
|
+
|
|
31
|
+
/** props 是 Vditor.preview 的 IPreviewOptions(theme 換成 VditorTheme;mode 只給 GFM 以外的渲染器用,拿掉),加上 value、className、style */
|
|
32
|
+
export type VditorPreviewProps = Omit<Partial<IPreviewOptions>, "theme" | "mode"> & {
|
|
33
|
+
/** 要渲染的 Markdown */
|
|
34
|
+
value: string;
|
|
35
|
+
/** 預設取最近的 VditorThemeProvider,沒有就是 light */
|
|
36
|
+
theme?: VditorTheme;
|
|
37
|
+
className?: string;
|
|
38
|
+
style?: React.CSSProperties;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** 只渲染 Markdown、不建立編輯器 */
|
|
42
|
+
export declare const VditorPreview: (props: VditorPreviewProps) => React.ReactElement;
|
|
43
|
+
|
|
44
|
+
/** 預覽用的 HTML 轉換:非 GFM 的區塊一律變成普通程式碼區塊 */
|
|
45
|
+
export declare const gfmTransform: (html: string) => string;
|
|
46
|
+
|
|
47
|
+
/** Markdown 轉 HTML,非 GFM 的區塊跟預覽一樣轉成普通程式碼區塊 */
|
|
48
|
+
export declare const md2html: (markdown: string, options?: Partial<IPreviewOptions>) => Promise<string>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import VditorCore from "vditor";
|
|
2
|
+
import { defaultCdn } from "./cdn.js";
|
|
3
|
+
import { gfmPreviewOptions, gfmTransform } from "./gfm.js";
|
|
4
|
+
export { Vditor } from "./Vditor.js";
|
|
5
|
+
export { VditorPreview } from "./VditorPreview.js";
|
|
6
|
+
export { VditorThemeProvider } from "./theme.js";
|
|
7
|
+
export { gfmTransform };
|
|
8
|
+
|
|
9
|
+
/** Markdown 轉 HTML,非 GFM 的區塊跟預覽一樣轉成普通程式碼區塊(原生 md2html 不吃 transform) */
|
|
10
|
+
export const md2html = async (markdown, options = {}) => {
|
|
11
|
+
const merged = gfmPreviewOptions(options, options.cdn || defaultCdn());
|
|
12
|
+
return merged.transform(await VditorCore.md2html(markdown, merged));
|
|
13
|
+
};
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// 主題:只有 light / dark,外加跟著作業系統的 auto。
|
|
2
|
+
// 決定順序:元件自己的 theme prop → 最近的 <VditorThemeProvider> → light。
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const ThemeContext = /*#__PURE__*/React.createContext(undefined);
|
|
5
|
+
|
|
6
|
+
/** 讓底下所有 <Vditor>、<VditorPreview> 預設用同一個主題;值變動時它們會跟著切換 */
|
|
7
|
+
export const VditorThemeProvider = ({
|
|
8
|
+
theme,
|
|
9
|
+
children
|
|
10
|
+
}) => /*#__PURE__*/React.createElement(ThemeContext.Provider, {
|
|
11
|
+
value: theme
|
|
12
|
+
}, children);
|
|
13
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
14
|
+
|
|
15
|
+
/** 作業系統目前是不是深色模式;系統切換時會重新 render */
|
|
16
|
+
const useSystemDark = enabled => {
|
|
17
|
+
const [dark, setDark] = React.useState(() => enabled && window.matchMedia(DARK_QUERY).matches);
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!enabled) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
23
|
+
const update = () => setDark(query.matches);
|
|
24
|
+
update();
|
|
25
|
+
query.addEventListener("change", update);
|
|
26
|
+
return () => query.removeEventListener("change", update);
|
|
27
|
+
}, [enabled]);
|
|
28
|
+
return dark;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** 算出元件實際要用的主題:"light" 或 "dark" */
|
|
32
|
+
export const useResolvedTheme = theme => {
|
|
33
|
+
const provided = React.useContext(ThemeContext);
|
|
34
|
+
const wanted = theme || provided || "light";
|
|
35
|
+
const systemDark = useSystemDark(wanted === "auto");
|
|
36
|
+
if (wanted === "auto") {
|
|
37
|
+
return systemDark ? "dark" : "light";
|
|
38
|
+
}
|
|
39
|
+
return wanted === "dark" ? "dark" : "light";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 容器上的 class,對應 index.css 裡局部化的主題樣式 */
|
|
43
|
+
export const themeClassName = theme => `react-vditor--${theme}`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 「上傳圖片」按鈕:toolbar 裡寫 "upload-image" 就會換成這顆按鈕。
|
|
2
|
+
// vditor 內建的 upload 只有一顆,檔案類型看的是共用的 upload.accept,所以另做一顆只收圖片的。
|
|
3
|
+
|
|
4
|
+
/** 按鈕的 tip。vditor 語系檔沒有這個字,照各語系「上傳圖片或文件」的譯法拿掉「或文件」;沒列到的語言用英文 */
|
|
5
|
+
const TIPS = {
|
|
6
|
+
en_US: "Upload image",
|
|
7
|
+
zh_CN: "上传图片",
|
|
8
|
+
zh_TW: "上傳圖片"
|
|
9
|
+
};
|
|
10
|
+
export const UPLOAD_IMAGE = "upload-image";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 選完圖片後交給 upload.handler,做法與 vditor 內建上傳在設了 handler 時相同:
|
|
14
|
+
* upload.multiple 不是 true 時只能選一張;handler 回傳字串時當成錯誤訊息顯示。
|
|
15
|
+
* 只支援 upload.handler,不支援 upload.url(那條路是 vditor 內部的 XHR 上傳流程,沒有公開的 API 可以呼叫)。
|
|
16
|
+
*/
|
|
17
|
+
const pickImages = vditor => {
|
|
18
|
+
const {
|
|
19
|
+
handler,
|
|
20
|
+
multiple
|
|
21
|
+
} = vditor.options.upload;
|
|
22
|
+
if (!handler) {
|
|
23
|
+
vditor.tip.show("please config: options.upload.handler");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const input = document.createElement("input");
|
|
27
|
+
input.type = "file";
|
|
28
|
+
input.accept = "image/*";
|
|
29
|
+
input.multiple = multiple === true;
|
|
30
|
+
input.addEventListener("change", async () => {
|
|
31
|
+
// 檔案選擇框可以切成「所有檔案」,所以再過濾一次
|
|
32
|
+
const images = [...input.files].filter(file => file.type.startsWith("image/"));
|
|
33
|
+
if (images.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const message = await handler(images);
|
|
37
|
+
if (typeof message === "string") {
|
|
38
|
+
vditor.tip.show(message);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
input.click();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** "upload-image"(字串或物件)換成完整的按鈕設定;使用端在物件裡給的欄位優先 */
|
|
45
|
+
export const uploadImageItem = (item, lang) => ({
|
|
46
|
+
name: UPLOAD_IMAGE,
|
|
47
|
+
icon: '<svg><use xlink:href="#vditor-icon-upload"></use></svg>',
|
|
48
|
+
tip: TIPS[lang] || TIPS.en_US,
|
|
49
|
+
click: (event, vditor) => pickImages(vditor),
|
|
50
|
+
...(typeof item === "string" ? {} : item)
|
|
51
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wyttime04/react-vditor",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "vditor 的 React 元件:只支援 GFM、完全離線",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/wyttime04/react-vditor.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./index.css": "./dist/index.css",
|
|
19
|
+
"./webpack-plugin": {
|
|
20
|
+
"types": "./webpack-plugin/index.d.ts",
|
|
21
|
+
"require": "./webpack-plugin/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"webpack-plugin"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": [
|
|
32
|
+
"*.css"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"start": "webpack serve --config dev/webpack.config.cjs --open",
|
|
36
|
+
"preview": "npm run build && webpack serve --config dev/webpack.config.cjs --mode production --open",
|
|
37
|
+
"build": "node scripts/build.cjs",
|
|
38
|
+
"build:page": "npm run build && webpack --config dev/webpack.config.cjs --mode production",
|
|
39
|
+
"build:test": "webpack --config test/app/webpack.config.cjs",
|
|
40
|
+
"test:types": "tsc -p tsconfig.json",
|
|
41
|
+
"test": "npm run build:test && npm run test:types && jest"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"vditor": "4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=16.9.0 <20",
|
|
48
|
+
"webpack": "^5.76.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"webpack": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@babel/cli": "^7.26.0",
|
|
57
|
+
"@babel/core": "^7.26.0",
|
|
58
|
+
"@babel/preset-env": "^7.26.0",
|
|
59
|
+
"@babel/preset-react": "^7.26.0",
|
|
60
|
+
"@types/react": "^16.14.0",
|
|
61
|
+
"babel-loader": "^9.2.1",
|
|
62
|
+
"css-loader": "^7.1.5",
|
|
63
|
+
"html-webpack-plugin": "^5.6.8",
|
|
64
|
+
"jest": "^29.7.0",
|
|
65
|
+
"postcss": "^8.5.28",
|
|
66
|
+
"postcss-prefix-selector": "^2.2.1",
|
|
67
|
+
"puppeteer": "^23.0.2",
|
|
68
|
+
"react": "16.9.0",
|
|
69
|
+
"react-dom": "16.9.0",
|
|
70
|
+
"style-loader": "^4.0.0",
|
|
71
|
+
"typescript": "^5.6.0",
|
|
72
|
+
"val-loader": "^6.0.0",
|
|
73
|
+
"webpack": "^5.111.1",
|
|
74
|
+
"webpack-cli": "^5.1.4",
|
|
75
|
+
"webpack-dev-server": "^5.2.0"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 把 vditor 執行期要載入的資源輸出到 webpack output 的 vditor/dist/ 底下。
|
|
2
|
+
// 只輸出 GFM 用得到的部分;附加渲染套件(mermaid、katex、mathjax、echarts…)一律不輸出。
|
|
3
|
+
// 內容主題與 dark 的程式碼主題已經局部化、併進 react-vditor/index.css,這裡也不輸出。
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PLUGIN_NAME = "VditorAssetsPlugin";
|
|
8
|
+
const DIST = path.dirname(require.resolve("vditor/dist/index.css"));
|
|
9
|
+
const INCLUDE = [
|
|
10
|
+
"js/lute", // Markdown 解析引擎
|
|
11
|
+
"js/i18n", // 介面語系
|
|
12
|
+
"js/icons", // 工具列圖示
|
|
13
|
+
"js/highlight.js/highlight.min.js", // 程式碼上色
|
|
14
|
+
"js/highlight.js/third-languages.js", // vditor 自己加的語言
|
|
15
|
+
"js/highlight.js/styles/github.min.css", // vditor 一定會整頁載入這一個,不論設定
|
|
16
|
+
"images", // 圖片 emoji、載入中圖示
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const listFiles = (target) => (fs.statSync(target).isDirectory()
|
|
20
|
+
? fs.readdirSync(target).flatMap((name) => listFiles(path.join(target, name)))
|
|
21
|
+
: [target]);
|
|
22
|
+
|
|
23
|
+
class VditorAssetsPlugin {
|
|
24
|
+
apply(compiler) {
|
|
25
|
+
const {RawSource} = compiler.webpack.sources;
|
|
26
|
+
let files;
|
|
27
|
+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
28
|
+
compilation.hooks.processAssets.tap({
|
|
29
|
+
name: PLUGIN_NAME,
|
|
30
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
|
|
31
|
+
}, () => {
|
|
32
|
+
// watch 模式下每次編譯都會進來,檔案內容不會變,讀一次就好
|
|
33
|
+
files = files || INCLUDE.flatMap((dir) => listFiles(path.join(DIST, dir))).map((file) => ({
|
|
34
|
+
name: `vditor/dist/${path.relative(DIST, file).split(path.sep).join("/")}`,
|
|
35
|
+
source: new RawSource(fs.readFileSync(file)),
|
|
36
|
+
}));
|
|
37
|
+
// minimized:vditor 的檔案本來就壓縮過,不要讓 production build 的 terser 再處理一次
|
|
38
|
+
files.forEach(({name, source}) => compilation.emitAsset(name, source, {minimized: true}));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = VditorAssetsPlugin;
|