Merge pull request 'v1.5.0 Releases' (PR#43) from dev into main
Build Plugin JAR File / build (push) Successful in 1m23s Details
Build Plugin JAR File / github-release (push) Has been skipped Details

Reviewed-on: #43
This commit is contained in:
zhengyi 2024-01-13 20:25:06 +08:00
commit f0eb2f3307
36 changed files with 982 additions and 513 deletions

View File

@ -52,6 +52,10 @@ jobs:
version=${{ github.event.release.tag_name }}
version=${version#v}
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
else
build=${{ github.run_number }}
echo 当前版本的构建号为: $build
sed -i "s/\(version=[0-9]*\.[0-9]*\.[0-9]*\)-SNAPSHOT/\1-build.$build-SNAPSHOT/g" gradle.properties
fi
./gradlew clean build -x test
- name: Archive plugin-starter jar

View File

@ -1,5 +1,15 @@
# halo-plugin-vditor
---
## NEXT VERSION
- ✨ 使用新的快速插入表单,界面更美观
- ✨ 支持渲染其他插件/主题的自定义组件(需要开发者支持)
- ✨ 允许用户禁用HTML代码块隐藏的特性
---
本插件将Vditor整合进Halo支持所见即所得编辑模式。 Support English (*Only Editor)!
编辑器支持数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、graphviz 渲染、plantumlUML图
@ -16,6 +26,7 @@
- 支持字符计数
- 支持在前台注入渲染脚本(需在设置中开启)
- 更多强大的语法功能请 [->到这<-](https://github.com/Vanessa219/vditor) 查看 (部分功能仍未支持)
- 支持独有的自定义语法,详细语法请参考 [这里](https://github.com/justice2001/vditor-halo-render#语法参考)
## 💻使用方式
@ -29,13 +40,15 @@
下面是当前已知的兼容性问题
- 在同时使用ToolBench插件时数学公式、脑图、图表、流程图、甘特图、时序图、五线谱无法正常渲染。`会尝试修复`
- 在同时使用ToolBench插件时数学公式、脑图、图表、流程图、甘特图、时序图、五线谱无法正常渲染。`由于此插件修改了页面结构,所以不会修复`
- Vditor渲染器不会根据主题暗色模式进行改变当前仅支持跟随系统暗色模式
## 📒TODO
> 如果可以支持的功能将会加入到这个TODO列表中列表中没有的功能也未必是不能支持的可能只是开发者没有想到
> 插件的进度在 [Gitea](https://git.mczhengyi.top/zhengyi/halo-plugin-vditor/issues) 进行管理当前Gitea不支持您评论如果您想讨论Issue可以直接在Github上打开一个Issue并表明要讨论的Gitea Issue您的回复将会被同步到Gitea。
- [x] 能够在Halo中运行Vditor编辑器
- [x] 支持数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、graphviz 渲染、plantumlUML图
- [x] 添加编辑器默认展示模式设置
@ -43,23 +56,15 @@
- [x] 添加打字机模式
- [x] 支持附件选取插入
- [x] 支持暗色主题渲染
- [ ] `🧪` 支持多媒体渲染
- [x] `💻` 拆分配置为编辑器配置与渲染配置两部分
- [x] `💻` 添加多国语言支持 `手搓翻译函数实现`
- [ ] `💻` 支持代码高亮及复制
- [x] `💻` 跟随主题的暗色模式(joe主题)
- [ ] 将Vditor前台渲染资源全量引入本地
- [ ] 添加配置是否使用CDN加载前台资源
- [ ] `🐛` 内置渲染器与ToolBench不兼容
- [ ] `📄` 自定义暗色模式触发方式
- [ ] `📄` 支持AI编写与修改
**注释**
- `🧪` 当前已在某个版本进行实验性发布
- `📄` 一个想法如果你有什么比较好的建议可以在ISSUE中提出
- `💻` 计划在下个版本推出
- `🐛` 这是一个BUG但是比较难处理
- [x] 支持多媒体渲染
- [x] 拆分配置为编辑器配置与渲染配置两部分
- [x] 添加多国语言支持 `手搓翻译函数实现`
- [x] 跟随主题的暗色模式(joe主题)
- [x] 将Vditor前台渲染资源全量引入本地
- [ ] 支持代码高亮及复制
- [ ] 自定义暗色模式触发方式
- [ ] 支持AI编写与修改
- [ ] ~~内置渲染器与ToolBench不兼容~~
## 🙏 鸣谢
@ -98,10 +103,18 @@ cd path/to/plugin-starter
```bash
# 下载依赖包
# macOS/Linux执行:
chmod a+x download_dist.sh
./download_dist.sh
# Windows
# 要求安装7Zip并将7Zip的文件夹加入Path环境变量
./download_dist.bat
```
```bash
# macOS / Linux
./gradlew pnpmInstall

View File

@ -39,3 +39,15 @@ build {
// build frontend before build
tasks.getByName('compileJava').dependsOn('buildFrontend')
}
halo {
version = '2.11.3'
superAdminUsername = 'admin'
superAdminPassword = 'admin'
externalUrl = 'http://localhost:8090'
docker {
// windows npipe:////./pipe/docker_engine
url = 'npipe:////./pipe/docker_engine'
apiVersion = '1.42'
}
}

View File

@ -11,11 +11,12 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@formkit/vue": "^1.4.0",
"@halo-dev/api-client": "^2.11.0",
"@halo-dev/components": "^1.10.0",
"@halo-dev/console-shared": "^2.11.0",
"@zhengyi/vditor": "3.9.10",
"canvas-confetti": "^1.6.0",
"@zhengyi/vditor": "3.9.9",
"vue": "^3.3.4"
},
"devDependencies": {

View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@formkit/vue':
specifier: ^1.4.0
version: 1.4.0(typescript@4.7.4)
'@halo-dev/api-client':
specifier: ^2.11.0
version: 2.11.0
@ -15,8 +18,8 @@ dependencies:
specifier: ^2.11.0
version: 2.11.0(vue-router@4.2.5)(vue@3.3.12)
'@zhengyi/vditor':
specifier: 3.9.9
version: 3.9.9
specifier: 3.9.10
version: 3.9.10
canvas-confetti:
specifier: ^1.6.0
version: 1.9.2
@ -472,6 +475,98 @@ packages:
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
dev: false
/@formkit/core@1.4.0:
resolution: {integrity: sha512-LQYICzYWTC+ZXPyfizHDRJTBx6WLM3eRk9T4tzU8YPV58AYWPq3E4dbN5CKl7mPApAcJX6NxQOCYuNrdNKZvQA==}
dependencies:
'@formkit/utils': 1.4.0
dev: false
/@formkit/dev@1.4.0:
resolution: {integrity: sha512-y71zSZGAWdqjuj5p7IvptJXgDXYFTZrJSjZ9HeQjNYJ1n3Nwc7latTzJSEQyOHuboSzRWafATYNeB3pqiTvBYg==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/utils': 1.4.0
dev: false
/@formkit/i18n@1.4.0:
resolution: {integrity: sha512-NRqw3ummnboUVkxN68jdy/T/mvgnf0/8v76160V3dTdt4OK9Q1Eq0a7pgonm0gBqisezX/0pELRWD0NPgUZSPw==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/utils': 1.4.0
'@formkit/validation': 1.4.0
dev: false
/@formkit/inputs@1.4.0:
resolution: {integrity: sha512-6IvgjOZnvtYq2oSEXkarDCaabxl4o29FhFmD64d0lC8WTaVLcRPKgo9BsLaVvLYfJgVKDzFyPIMGLFNhTsiJ/Q==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/utils': 1.4.0
dev: false
/@formkit/observer@1.4.0:
resolution: {integrity: sha512-sNjLqi+deN2TCnuRDWdOJ1OjTyxavFKg9pVK1E1KRrbQr0pvPDaS4qYIjrIIWg0YMsQiea487r03elLVesMWWw==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/utils': 1.4.0
dev: false
/@formkit/rules@1.4.0:
resolution: {integrity: sha512-NEVDjN89Zwx26Ze0gwqd/Bml3d4QIzp3SDps3uND9IE1Ssd3yvvSSjF5jVeHLE7ZMDLUDfOepf5xyqhPE1UbqA==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/utils': 1.4.0
'@formkit/validation': 1.4.0
dev: false
/@formkit/themes@1.4.0:
resolution: {integrity: sha512-isyHZALh6S3BM+0G5NeRQkI12gydDRXEkI6oGt5uTn3Ju2olLt8RwOXNYgJ9lWSb8HrfuHh7/89SozdIRNVuCA==}
peerDependencies:
tailwindcss: ^3.2.0
unocss: ^0.31.0
windicss: ^3.0.0
peerDependenciesMeta:
tailwindcss:
optional: true
unocss:
optional: true
windicss:
optional: true
dependencies:
'@formkit/core': 1.4.0
dev: false
/@formkit/utils@1.4.0:
resolution: {integrity: sha512-HAkULL7/0PnRZmMJyFHZ3wxxTYl+tuJrSk13/LQyxB77luwlq8sCvlGaF5cz+0JX70HSVVc0ZJjJT4o0uTVeYQ==}
dev: false
/@formkit/validation@1.4.0:
resolution: {integrity: sha512-KF25aU5ouwg+dFryyDGPTEFgik2TzbiT824PkIQJn7Fgepplo3Dj/UvjHmM2u0hBvIE2CS6XtgE0XVhn1kRG+A==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/observer': 1.4.0
'@formkit/utils': 1.4.0
dev: false
/@formkit/vue@1.4.0(typescript@4.7.4):
resolution: {integrity: sha512-qxLJLG/rc0Mv75h9aaBu1SIFo8AZq1mMCa+u4aWQdXf0k/e8Mfuyhkch25c9pfbzSjEpFzs7G7L5U1gMuTcmtQ==}
dependencies:
'@formkit/core': 1.4.0
'@formkit/dev': 1.4.0
'@formkit/i18n': 1.4.0
'@formkit/inputs': 1.4.0
'@formkit/observer': 1.4.0
'@formkit/rules': 1.4.0
'@formkit/themes': 1.4.0
'@formkit/utils': 1.4.0
'@formkit/validation': 1.4.0
vue: 3.3.12(typescript@4.7.4)
transitivePeerDependencies:
- tailwindcss
- typescript
- unocss
- windicss
dev: false
/@halo-dev/api-client@2.11.0:
resolution: {integrity: sha512-i3PFETsPdHYnTgk3jORu00t43/rCesmqpdZg38/Hq2AdgxhPkE7rghYGdoZRLdanvVC0HM1Axn18Zd7kdizxVA==}
dev: false
@ -1026,8 +1121,8 @@ packages:
resolution: {integrity: sha512-r+aD7wvdXVRyFcw/yI/PbdIqSsC+Bnql2WSKZa8CK+ntpTvc2tybn6Wxsfw96C71QS+Q/MPe8ufwZBjtRd+hhg==, tarball: https://git.mczhengyi.top/api/packages/zhengyi/npm/%40zhengyi%2Fhalo-render/-/1.1.0/halo-render-1.1.0.tgz}
dev: false
/@zhengyi/vditor@3.9.9:
resolution: {integrity: sha512-YHlWEbVkDvctAYUAghIISPjTPqxwubLuFlgbm1L6Oq3sGdIt4YZKUJA7Psv/JHaasX/tIvvcgZyUygSjjRBzMw==, tarball: https://git.mczhengyi.top/api/packages/zhengyi/npm/%40zhengyi%2Fvditor/-/3.9.9/vditor-3.9.9.tgz}
/@zhengyi/vditor@3.9.10:
resolution: {integrity: sha512-8bAUlH4bsuUNbkrxAuxTL0XMNoLre/mab5HF8cR9wND7e/Xj56l2qz7aEARklK6oRtOlCoTYK6JEkLdozUfTkg==, tarball: https://git.mczhengyi.top/api/packages/zhengyi/npm/%40zhengyi%2Fvditor/-/3.9.10/vditor-3.9.10.tgz}
dependencies:
'@zhengyi/halo-render': 1.1.0
diff-match-patch: 1.0.5

View File

@ -23,6 +23,7 @@ const lang: I18nLang = {
title: "Title",
link: "Link",
password: "Password",
quick_insert: "Quick Insert",
};
export default lang;

View File

@ -23,6 +23,7 @@ const lang: I18nLang = {
title: "标题",
link: "链接",
password: "密码",
quick_insert: "快速插入",
};
export default lang;

View File

@ -23,6 +23,7 @@ const lang: I18nLang = {
title: "標題",
link: "鏈接",
password: "密碼",
quick_insert: "快速插入",
};
export default lang;

View File

@ -1,75 +0,0 @@
<script setup lang="ts">
import { VModal, VButton, VSpace } from "@halo-dev/components";
import { t } from "@/utils/i18n-utils";
import { ref } from "vue";
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
(event: "done", value: string): void;
(event: "close"): void;
}>();
const platform = ref("baidu");
const name = ref("");
const link = ref("");
const password = ref("");
const generateCode = () => {
let code = "\n\n```halo\n";
code += `drive:${platform.value}\n`;
code += `name: ${name.value}\n`;
code += `link: ${link.value}\n`;
if (password.value) {
code += `password: ${password.value}\n`;
}
emit("done", code + "```\n\n");
};
</script>
<template>
<VModal
:visible="props.open"
:layer-closable="false"
:title="t('insert_drive')"
@close="emit('close')"
>
<v-space align="start" direction="column" spacing="xs" style="width: 100%">
<label for="type" class="vditor-mde-label">
<span>{{ t("platform") }}</span>
<select id="platform" v-model="platform" class="vditor-mde-select">
<option value="baidu">{{ t("baidu_net_disk") }}</option>
<option value="ali">{{ t("ali_drive") }}</option>
</select>
</label>
<label for="name" class="vditor-mde-label">
<span>{{ t("title") }}</span>
<input id="name" v-model="name" type="text" class="vditor-mde-input" />
</label>
<label for="link" class="vditor-mde-label">
<span>{{ t("link") }}</span>
<input id="link" v-model="link" type="text" class="vditor-mde-input" />
</label>
<label for="password" class="vditor-mde-label">
<span>{{ t("password") }}</span>
<input
id="password"
v-model="password"
type="text"
class="vditor-mde-input"
/>
</label>
</v-space>
<template #footer>
<v-space align="center" direction="row" spacing="xs">
<v-button type="primary" @click="generateCode">
{{ t("confirm") }}
</v-button>
<v-button type="default" @click="emit('close')">
{{ t("close") }}
</v-button>
</v-space>
</template>
</VModal>
</template>

View File

@ -1,72 +0,0 @@
<script setup lang="ts">
import { VModal, VButton, VSpace } from "@halo-dev/components";
import { t } from "@/utils/i18n-utils";
import { ref } from "vue";
const URL_NOT_SHOW = ["github", "gitee"];
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
(event: "done", value: string): void;
(event: "close"): void;
}>();
const url = ref("");
const platform = ref("github");
const owner = ref("");
const repo = ref("");
const generateCode = () => {
emit(
"done",
"\n\n```halo\n" +
`git:[${url.value}@${platform.value}/${owner.value}/${repo.value}]` +
"\n```\n\n"
);
};
</script>
<template>
<VModal
:visible="props.open"
:layer-closable="false"
:title="t('insert_git')"
@close="emit('close')"
>
<v-space align="start" direction="column" spacing="xs" style="width: 100%">
<label v-if="URL_NOT_SHOW.indexOf(platform) === -1" for="url">
<span>URL</span>
<input id="url" v-model="url" type="text" class="vditor-mde-input" />
</label>
<label for="type" class="vditor-mde-label">
<span>{{ t("platform") }}</span>
<select id="platform" v-model="platform" class="vditor-mde-select">
<option value="github">GitHub</option>
<option value="gitee">Gitee</option>
</select>
</label>
<label for="owner" class="vditor-mde-label">
<span>{{ t("owner") }}</span>
<input id="owner" v-model="owner" type="text" class="vditor-mde-input" />
</label>
<label for="repo" class="vditor-mde-label">
<span>{{ t("repo") }}</span>
<input id="repo" v-model="repo" type="text" class="vditor-mde-input" />
</label>
</v-space>
<template #footer>
<v-space align="center" direction="row" spacing="xs">
<v-button type="primary" @click="generateCode">
{{ t("confirm") }}
</v-button>
<v-button type="default" @click="emit('close')">
{{ t("close") }}
</v-button>
</v-space>
</template>
</VModal>
</template>
<style scoped></style>

View File

@ -0,0 +1,89 @@
<script setup lang="ts">
import { FormKit, FormKitSchema } from "@formkit/vue";
import { ref, watch } from "vue";
import { t } from "@/utils/i18n-utils";
import { VButton, VModal, VSpace } from "@halo-dev/components";
import type { Schema } from "@/type/editor";
const data = ref<{ [key: string]: string }>({});
const loadKey = ref("");
let idCount = 0;
const props = defineProps<{
open: boolean;
schema: Schema;
}>();
const emit = defineEmits<{
(event: "done", value: string | null): void;
(event: "close"): void;
}>();
const generateCode = () => {
if (props.schema.template) {
let code = props.schema.template || "";
const formkit = props.schema.formKit;
formkit.forEach((form: { [key: string]: string }) => {
code = code.replace(
`$${form.name}$`,
data.value[form.name] || form.value
);
});
emit("done", htmlEncode(code));
return;
}
props.schema.handler && props.schema.handler(data.value);
emit("done", null);
};
// FormKit IDSchema
watch(props, (val, old) => {
if (props.open) {
if (old.schema.id === val.schema.id) {
loadKey.value = `${val.schema.id}-${idCount++}`;
} else {
idCount = 0;
loadKey.value = props.schema.id;
}
console.log("This Load Key: " + loadKey.value);
props.schema.formKit.forEach((form: { [key: string]: string }) => {
data.value[form.name] = form.value;
});
}
});
const htmlEncode = (str: string) => {
let s = "";
if (str.length === 0) {
return "";
}
s = str.replace(/</g, "&lt;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/"/g, "&quot;");
return s;
};
</script>
<template>
<VModal
:visible="props.open"
:layer-closable="false"
:title="schema.name"
@close="emit('close')"
>
<FormKit v-model="data" type="form">
<FormKitSchema :key="loadKey" :schema="schema.formKit" :data="data" />
</FormKit>
<template #footer>
<v-space align="center" direction="row" spacing="xs">
<v-button type="primary" @click="generateCode">
{{ t("confirm") }}
</v-button>
<v-button type="default" @click="emit('close')">
{{ t("close") }}
</v-button>
</v-space>
</template>
</VModal>
</template>
<style scoped></style>

View File

@ -1,59 +0,0 @@
<script setup lang="ts">
import { VModal, VButton, VSpace } from "@halo-dev/components";
import { ref } from "vue";
import { t } from "@/utils/i18n-utils";
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
(event: "done", value: string): void;
(event: "close"): void;
}>();
const type = ref("default");
const content = ref("");
const generateCode = () => {
emit(
"done",
"\n\n```halo\n" + `tips:${type.value}\n${content.value}\n` + "```\n\n"
);
};
</script>
<template>
<VModal
:visible="props.open"
:layer-closable="false"
:title="t('insert_tips')"
@close="emit('close')"
>
<v-space align="start" direction="column" spacing="xs" style="width: 100%">
<label for="type" class="vditor-mde-label">
<span>{{ t("type") }}</span>
<select id="type" v-model="type" class="vditor-mde-select">
<option value="default">{{ t("default") }}</option>
<option value="danger">{{ t("danger") }}</option>
<option value="warn">{{ t("warning") }}</option>
<option value="info">{{ t("info") }}</option>
<option value="success">{{ t("success") }}</option>
</select>
</label>
<label for="content" class="vditor-mde-label">
<span>{{ t("content") }}</span>
<textarea id="content" v-model="content" class="vditor-mde-textarea" />
</label>
</v-space>
<template #footer>
<v-space align="center" direction="row" spacing="xs">
<v-button type="primary" @click="generateCode">
{{ t("confirm") }}
</v-button>
<v-button type="default" @click="emit('close')">
{{ t("close") }}
</v-button>
</v-space>
</template>
</VModal>
</template>

View File

@ -0,0 +1,42 @@
import type { Schema } from "@/type/editor";
import { t } from "@/utils/i18n-utils";
const schema: Schema = {
type: "template",
id: "drive",
icon: "",
name: t("insert_drive"),
formKit: [
{
$formkit: "select",
name: "platform",
label: t("platform"),
value: "baidu",
options: {
baidu: t("baidu_net_disk"),
ali: t("ali_drive"),
},
},
{
$formkit: "text",
name: "title",
label: t("title"),
value: "",
},
{
$formkit: "text",
name: "link",
label: t("link"),
value: "",
},
{
$formkit: "text",
name: "password",
label: t("password"),
},
],
template:
"```halo\ndrive:$platform$\nname:$title$\nlink:$link$\npassword:$password$\n```",
};
export default schema;

44
console/src/schema/git.ts Normal file
View File

@ -0,0 +1,44 @@
import type { Schema } from "@/type/editor";
import { t } from "@/utils/i18n-utils";
const schema: Schema = {
type: "template",
id: "git",
icon: "",
name: t("insert_git"),
formKit: [
{
$formkit: "select",
id: "platform",
name: "platform",
label: t("platform"),
value: "github",
options: {
github: "GitHub",
gitee: "Gitee",
},
},
{
$formkit: "text",
name: "url",
label: "URL",
value: "",
if: "false",
},
{
$formkit: "text",
name: "owner",
label: t("owner"),
value: "",
},
{
$formkit: "text",
name: "repo",
label: t("repo"),
value: "",
},
],
template: "```halo\ngit:[$url$@$platform$/$owner$/$repo$]\n```",
};
export default schema;

View File

@ -0,0 +1,28 @@
import type { Schema } from "@/type/editor";
const schema: Schema = {
type: "template",
id: "joe-progress",
icon: "",
name: "Joe Progress Bar",
formKit: [
{
$formkit: "text",
name: "percentage",
label: "Percentage",
help: "This is the percentage for progress bar.",
value: "0%",
},
{
$formkit: "color",
name: "color",
label: "Color",
help: "This is the color for progress bar.",
value: "#ffffff",
},
],
template:
'<joe-progress percentage="$percentage$" color="$color$"></joe-progress>',
};
export default schema;

View File

@ -0,0 +1,34 @@
import type { Schema } from "@/type/editor";
import {t} from "@/utils/i18n-utils";
const schema: Schema = {
type: "template",
id: "tips",
icon: "",
name: t("insert_tips"),
formKit: [
{
$formkit: "select",
name: "type",
label: t("type"),
help: "This is the percentage for progress bar.",
value: "default",
options: {
default: t("default"),
info: t("info"),
success: t("success"),
warn: t("warning"),
danger: t("danger"),
},
},
{
$formkit: "textarea",
name: "content",
label: t("content"),
value: "",
},
],
template: "```halo\n" + `tips:$type$\n$content$\n` + "```",
};
export default schema;

View File

@ -1,12 +1,3 @@
export declare type EditorConfig = {
basic: {
enable_render: boolean;
defaultRenderMode: "ir" | "wysiwyg" | "sv" | undefined;
typeWriterMode: boolean;
codeBlockPreview: boolean;
};
};
export declare type Options = {
defaultRenderMode: "ir" | "wysiwyg" | "sv" | undefined;
typeWriterMode: boolean;
@ -16,5 +7,40 @@ export declare type Options = {
language: string;
codeBlockPreview: boolean;
uploadImage?: (files: File[]) => string | null | Promise;
openModal: (name: string) => void;
openModal: (schema: Schema) => void;
quickInsertList: QuickInsert[];
enableQuickInsert: boolean;
};
export interface Schema {
type: "template";
id: string;
icon?: string;
name: string;
formKit: Array;
template?: string;
// 解析后处理
afterHandle?: (data: { [key: string]: string }, code: string) => string;
// 覆盖解析
handler?: (data: { [key: string]: string }) => string;
}
export interface QuickInsert {
// 展示名称
name: string;
// 鼠标移入时的提示文本
tip: string;
// 提供者
provider: string;
// 插入按钮的图标
icon: string;
// 配置结构
schema: Schema[];
inject?: Inject[];
}
export interface Inject {
id: string;
type: "script" | "style";
url?: string;
}

View File

@ -0,0 +1,29 @@
export declare type EditorConfig = {
basic: {
enable_render: boolean;
defaultRenderMode: "ir" | "wysiwyg" | "sv" | undefined;
typeWriterMode: boolean;
codeBlockPreview: boolean;
enableQuickInsert: boolean;
quickInsertUrl: [];
disableHTMLBlockPreview: boolean;
};
extension: {
allowImageType: string;
};
};
export const defaultEditorConfig: EditorConfig = {
basic: {
enable_render: true,
defaultRenderMode: "ir",
typeWriterMode: true,
codeBlockPreview: true,
enableQuickInsert: false,
quickInsertUrl: [],
disableHTMLBlockPreview: false,
},
extension: {
allowImageType: "png,jpg,jpeg,bmp,gif,webp,svg",
}
}

View File

@ -0,0 +1,10 @@
export function getCursor() {
return window.getSelection()?.getRangeAt(0);
}
export function setCursor(range: Range | undefined) {
if (!range) return;
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}

View File

@ -0,0 +1,27 @@
export const addScript = (url: string, id: string): HTMLElement => {
const headElement = document.getElementsByTagName("head")[0];
const scriptElement = document.createElement("script");
scriptElement.id = id;
scriptElement.src = url;
headElement.append(scriptElement);
return scriptElement;
};
export const addStyleSheet = (url: string, id: string): HTMLElement => {
const headElement = document.getElementsByTagName("head")[0];
const linkElement = document.createElement("link");
linkElement.id = id;
linkElement.rel = "stylesheet";
linkElement.href = url;
headElement.append(linkElement);
return linkElement;
};
export const addStyle = (style: string, id: string): HTMLElement => {
const headElement = document.getElementsByTagName("head")[0];
const styleElement = document.createElement("style");
styleElement.id = id;
styleElement.innerHTML = style;
headElement.append(styleElement);
return styleElement;
};

View File

@ -0,0 +1,23 @@
import type { QuickInsert } from "@/type/editor";
/**
*
* @param quickInsertUrls
* @return
*/
export const fetchAllQuickInsert = async (
quickInsertUrls: { url: string }[]
): Promise<QuickInsert[]> => {
const quickInsertList: QuickInsert[] = [];
// Get Default Path
for (const qi of quickInsertUrls) {
try {
const response = await fetch(qi.url);
const quickInsertJson: QuickInsert = await response.json();
quickInsertList.push(quickInsertJson);
} catch (e) {
// ignore this
}
}
return quickInsertList;
};

View File

@ -0,0 +1,16 @@
import type { Inject } from "@/type/editor";
import { addScript, addStyleSheet } from "@/utils/dom-utils";
export const quickInsertInject = (injectList: Inject[], id: string): void => {
injectList.forEach((inject) => {
const injectId = `${id}-${inject.id}`;
switch (inject.type) {
case "script":
addScript(inject.url || "", injectId);
break;
case "style":
addStyleSheet(inject.url || "", injectId);
break;
}
});
};

View File

@ -1,12 +1,27 @@
import type { Options } from "@/type/editor";
import {mdiGrid, mdiImage} from "@/utils/icon";
import type { Options, QuickInsert, Schema } from "@/type/editor";
import { mdiGrid, mdiImage } from "@/utils/icon";
import { t } from "@/utils/i18n-utils";
import tips from "@/schema/tips";
import git from "@/schema/git";
import drive from "@/schema/drive";
export function getOptions(options: Options): IOptions {
const cdn =
`${window.location.protocol}//${window.location.host}` +
`/plugins/vditor-mde/assets/static`;
console.log(`Your CDN IS: ${cdn}`);
// Get Toolbar
const toolbar = getToolbar(
options.showAttachment,
options.openModal,
getLanguage(options.language)
);
if (options.enableQuickInsert) {
options.quickInsertList.forEach((insert: QuickInsert) => {
toolbar.splice(-1, 0, buildQuickInsertToolbar(options.openModal, insert));
});
}
// Build Options
return {
height: "100%",
mode: options.defaultRenderMode,
@ -22,11 +37,7 @@ export function getOptions(options: Options): IOptions {
},
after: options.after,
input: options.input,
toolbar: getToolbar(
options.showAttachment,
options.openModal,
getLanguage(options.language)
),
toolbar: toolbar,
counter: {
enable: true,
},
@ -70,9 +81,9 @@ export function getLanguage(lang = "zh-CN"): keyof II18n {
function getToolbar(
showAttachmentCb: () => void,
openModal: (name: string) => void,
openModal: (schema: Schema) => void,
lang: keyof II18n
): (string | IMenuItem)[] | undefined {
): (string | IMenuItem)[] {
return [
"emoji",
"headings",
@ -118,17 +129,17 @@ function getToolbar(
{
name: "insert_tips",
icon: t("insert_tips"),
click: () => openModal("tips"),
click: () => openModal(tips),
},
{
name: "insert_git",
icon: t("insert_git"),
click: () => openModal("git"),
click: () => openModal(git),
},
{
name: "insert_drive",
icon: t("insert_drive"),
click: () => openModal("drive"),
click: () => openModal(drive),
},
],
},
@ -138,3 +149,24 @@ function getToolbar(
},
];
}
function buildQuickInsertToolbar(
openModal: (schema: Schema) => void,
quickInsertList: QuickInsert
): IMenuItem {
const children: IMenuItem[] = [];
quickInsertList.schema.forEach((sch: Schema) => {
children.push({
icon: (sch.icon || "") + sch.name,
name: sch.id,
click: () => openModal(sch),
});
});
return {
name: quickInsertList.name,
tip: quickInsertList.tip,
icon: quickInsertList.icon,
tipPosition: "n",
toolbar: children,
};
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
# Halo-Vditor语法介绍
## ⚠️ 这个文档已经过时,您可以[这里](https://github.com/justice2001/vditor-halo-render#语法参考)查看最新文档
> 当前halo-render还在测试中halo-render也可以接入任何markdown编辑器中
> 在维护过程中,我会尽量保证语法保持不变

128
doc/rfc/custom-insert.md Normal file
View File

@ -0,0 +1,128 @@
# 快速插入 设计方案
> Issue https://github.com/justice2001/halo-plugin-vditor/issues/11
## 目标
- 应该为用户提供一个可以存储自定义插入按钮配置的设置项
- 应当允许用户关闭自定义插入按钮的功能
- 为开发者维护一个form表单来管理插入资源
## 设计
使用json格式来存储自定义插入按钮的配置并使用FormKit来生成表单用户填写提交表单后自动将表单数据替换至预设的文本内容中。
### 配置格式
配置文件主要存储了提供者、icon、提示文本等信息以及插入配置其基础格式如下
```json
{
"name": "Joe Theme",
"tip": "Joe自定义模块",
"provider": "halo-theme-joe-3.0",
"icon": "...",
"schema": [],
"inject": []
}
```
- `name`:快速插入的标识名称
- `tip`:鼠标移入图标显示的文本
- `provider`:标识该配置文件的提供者,暂无实际作用
- `icon`您可以在此处直接插入svg图标用于在工具栏展示
- `schema`具体的插入结构详细配置见下方Schema格式
- `inject`: 注入附加的js或样式表文件详细配置见下方Inject章节
### Schema格式
Schema存储了每个快速插入功能的插入处理规则如生成表单、插入预设文本等功能。
```json
{
"type": "template",
"id": "joe-progress",
"icon": "",
"name": "Joe Progress Bar",
"formkit": [
{
"$formkit": "text",
"name": "percentage",
"label": "Percentage",
"help": "This is the percentage for progress bar."
},
{
"$formkit": "text",
"name": "color",
"label": "Color",
"help": "This is the color for progress bar."
}
],
"template": "<joe-progress percentage=\"$percentage$\" color=\"$color$\"></joe-progress>"
}
```
- `type`:该按钮的处理类型,暂时规划下面几种
- `template`根据formkit生成表单并将值替换至预设模版中
- `static`:(暂未实现)插入特定的文本内容
- `id`:模块标识,在插件中暂无实际作用
- `icon`图标会自动插入到name左侧
- `name`在toolbar下拉列表中展示的名称
- `formkit`template模式下需要遵守FormKit Schema规范其中value被定义为缺省值
- `template`:模版文本,使用`$varible$`识别变量会被formkit中同name的值替换
# Inject格式
Inject是提供给利用HTML自定义标签或自定义样式表方式进行渲染的主题/插件实现实时渲染的配置。
该数组内的所有资源将在Vditor初始化时进行加载。
例如`joe`主题利用了`customElements`来实现的。
```json
[
{
"id": "inject-js",
"type": "script",
"url": "/plugins/vditor-mde/assets/static/inject-demo.js"
},
{
"id": "inject-css",
"type": "style",
"url": "/plugins/vditor-mde/assets/static/inject-demo.css"
}
]
```
- `id`: 注入表示用于注入的标签加入id选择器id选择器为: <config-id>-<inject-id>
- `type`: 注入类型,当前支持注入脚本和样式文件
- `script`: 注入脚本
- `style`: 注入样式表
- `url`: 脚本和样式文件的URL
### 配置文件示例
[quick-insert-demo.json](quick-insert-demo.json)
### 如何使用
将json文件放置在插件或主题的静态资源目录下并将访问该文件的URL填入halo-vditor配置中即可。
### 表单方案
与Halo相同的使用FormKit Schema方案便于Halo开发者使用但不应支持特别复杂的语法应当支持一些较为基础的语法为优。
Halo使用的是yml格式语法而为了方便配置文件编辑插件暂时指定json为配置格式。
### 存储方案
对配置存储方案有下列四种:
- 使用依赖于Halo原生的附件管理未验证是否可行
- 提供配置输入框,让用户粘贴配置文件
- 🌟 提供URL输入框让用户自行根据主题或插件配置文档输入URL
- 🌟 自动识别某个特定目录是否存储配置文件(适用主题端)
## 多语言适配
WIP(v1.5.x暂时不做规划)

View File

@ -0,0 +1,51 @@
{
"name": "Joe Theme",
"tip": "Joe自定义模块",
"provider": "halo-theme-joe-3.0",
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 -960 960 960\" width=\"24\"><path d=\"M480-80q-26 0-47-12.5T400-126q-33 0-56.5-23.5T320-206v-142q-59-39-94.5-103T190-590q0-121 84.5-205.5T480-880q121 0 205.5 84.5T770-590q0 77-35.5 140T640-348v142q0 33-23.5 56.5T560-126q-12 21-33 33.5T480-80Zm-80-126h160v-36H400v36Zm0-76h160v-38H400v38Zm-8-118h58v-108l-88-88 42-42 76 76 76-76 42 42-88 88v108h58q54-26 88-76.5T690-590q0-88-61-149t-149-61q-88 0-149 61t-61 149q0 63 34 113.5t88 76.5Zm88-162Zm0-38Z\"/></svg>",
"schema": [
{
"type": "template",
"id": "joe-progress",
"icon": "",
"name": "进度条",
"formKit": [
{
"$formkit": "number",
"min": 0,
"max": 100,
"step": 1,
"name": "percentage",
"label": "进度",
"help": "This is the percentage for progress bar.",
"value": 50
},
{
"$formkit": "color",
"name": "color",
"label": "颜色",
"help": "This is the color for progress bar.",
"value": "#fb6c28"
}
],
"template": "<joe-progress percentage=\"$percentage$%\" color=\"$color$\"></joe-progress>"
}
],
"inject": [
{
"id": "inject-js",
"type": "script",
"url": "/plugins/vditor-mde/assets/static/inject-demo.js"
},
{
"id": "inject-css",
"type": "style",
"url": "/plugins/vditor-mde/assets/static/inject-demo.css"
},
{
"id": "inject-font",
"type": "style",
"url": "/themes/theme-Joe3/assets/lib/font-awesome/css/font-awesome.min.css"
}
]
}

11
download_dist.bat Normal file
View File

@ -0,0 +1,11 @@
cd src/main/resources/static
if exist dist rmdir /s /q dist
echo %cd%
bitsadmin /transfer vditorDownloadJob https://git.mczhengyi.top/zhengyi/-/packages/npm/@zhengyi%%2Fvditor/3.9.10/files/262 %cd%\vditor.tgz
7z x vditor.tgz
7z x vditor.tar
move /y %cd%\package\dist %cd%
rmdir /s /q package
del vditor.tar
del vditor.tgz
pause

View File

@ -4,7 +4,7 @@ cd src/main/resources/static
rm -rf dist
pwd
curl -o vditor.tgz \
https://git.mczhengyi.top/zhengyi/-/packages/npm/@zhengyi%2Fvditor/3.9.9/files/258
https://git.mczhengyi.top/zhengyi/-/packages/npm/@zhengyi%2Fvditor/3.9.10/files/262
tar -xzvf vditor.tgz
mv package/dist .
rm -rf package

View File

@ -1 +1 @@
version=1.4.0-SNAPSHOT
version=1.5.0-SNAPSHOT

View File

@ -14,8 +14,9 @@ public class ScriptUtils {
public static String basicScript(RenderConfig renderConfig) {
return """
<link rel="stylesheet" type="text/css" href="/plugins/vditor-mde/assets/static/dist/index.css" id="vditor-style" />
<script src="/plugins/vditor-mde/assets/static/dist/method.min.js"></script>
<script src="/plugins/vditor-mde/assets/static/render.js" id="render-script"
<script src="/plugins/vditor-mde/assets/static/render.js" id="vditor-render"
data-dark="%s" data-mediaRender="%s"></script>
""".formatted(renderConfig.getDarkMode(), renderConfig.getMediaRender());
}

View File

@ -29,6 +29,36 @@ spec:
label: 编辑器代码块渲染
help: 关闭后代码块(包括图表)在所见即所得和即时渲染模式下将不会被渲染
value: true
- $formkit: checkbox
id: enableQuickInsert
name: enableQuickInsert
label: 启用快速插入功能
help: 开启此选项后,将会加入插件或主题提供的快速插入按钮
value: false
- $formkit: repeater
if: "$enableQuickInsert"
name: quickInsertUrl
label: 快速插入链接
help: 在下面的选项框中填入主题或插件给出的配置地址,即可使用
value: [ ]
children:
- $formkit: text
name: url
label: URL
value: ""
- $formkit: checkbox
name: disableHTMLBlockPreview
label: 禁用HTML代码块隐藏
help: 开启此选项后HTML代码块将会一直显示
value: false
- group: extension
label: 文件格式
formSchema:
- $formkit: text
name: allowImageType
label: 允许的图片格式
help: 自定义允许上传的图片格式
value: "png,jpg,jpeg,bmp,gif,webp,svg"
- group: render
label: 渲染
formSchema:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
customElements.define(
"joe-cloud",
class JoeCloud extends HTMLElement {
constructor() {
super();
this.options = {
type: this.getAttribute("type") || "default",
title: this.getAttribute("title") || "默认标题",
url: this.getAttribute("url"),
password: this.getAttribute("password"),
};
const type = {
default: "默认网盘",
360: "360网盘",
bd: "百度网盘",
ty: "天翼网盘",
ct: "城通网盘",
wy: "微云网盘",
github: "Github仓库",
gitee: "Gitee仓库",
lz: "蓝奏云网盘",
ad: "阿里云盘",
};
this.innerHTML = `
<span class="joe_cloud">
<div class="joe_cloud__logo _${this.options.type}"></div>
<div class="joe_cloud__describe">
<div class="joe_cloud__describe-title">${this.options.title}</div>
<div class="joe_cloud__describe-type">来源${
type[this.options.type] || "默认网盘"
}${
this.options.password ? " | 提取码:" + this.options.password : ""
}</div>
</div>
<a class="joe_cloud__btn" href="${
this.options.url
}" target="_blank" rel="noopener noreferrer nofollow">
<i class="fa fa-download"></i>
</a>
</span>
`;
}
}
);

View File

@ -0,0 +1,70 @@
{
"name": "Joe Theme",
"tip": "Joe自定义模块",
"provider": "halo-theme-joe-3.0",
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 -960 960 960\" width=\"24\"><path d=\"M480-80q-26 0-47-12.5T400-126q-33 0-56.5-23.5T320-206v-142q-59-39-94.5-103T190-590q0-121 84.5-205.5T480-880q121 0 205.5 84.5T770-590q0 77-35.5 140T640-348v142q0 33-23.5 56.5T560-126q-12 21-33 33.5T480-80Zm-80-126h160v-36H400v36Zm0-76h160v-38H400v38Zm-8-118h58v-108l-88-88 42-42 76 76 76-76 42 42-88 88v108h58q54-26 88-76.5T690-590q0-88-61-149t-149-61q-88 0-149 61t-61 149q0 63 34 113.5t88 76.5Zm88-162Zm0-38Z\"/></svg>",
"schema": [
{
"type": "template",
"id": "joe-cloud",
"icon": "",
"name": "网盘资源",
"formKit": [
{
"$formkit": "select",
"name": "cloud-type",
"label": "云盘类型",
"help": "Choose the type of cloud service.",
"value": "default",
"options": [
{"label": "默认网盘", "value": "default"},
{"label": "百度网盘", "value": "bd"},
{"label": "阿里网盘", "value": "ad"},
{"label": "蓝奏云网盘", "value": "lz"},
{"label": "微云网盘", "value": "wy"},
{"label": "Github仓库", "value": "github"},
{"label": "Gitee仓库", "value": "gitee"}
]
},
{
"$formkit": "text",
"name": "cloud-title",
"label": "网盘名称",
"help": "留空则显示默认标题"
},
{
"$formkit": "url",
"name": "cloud-url",
"label": "跳转链接",
"help": "网盘链接地址",
"value": ""
},
{
"$formkit": "text",
"name": "cloud-password",
"label": "密码",
"help": "网盘的访问密码,无则留空",
"value": ""
}
],
"template": "<joe-cloud type=\"$cloud-type$\" url=\"$cloud-url$\" password=\"$cloud-password$\" title=\"$cloud-title$\"></joe-cloud>"
}
],
"inject": [
{
"id": "inject-js",
"type": "script",
"url": "/plugins/vditor-mde/assets/static/inject-demo.js"
},
{
"id": "inject-css",
"type": "style",
"url": "/plugins/vditor-mde/assets/static/inject-demo.css"
},
{
"id": "inject-font",
"type": "style",
"url": "/themes/theme-Joe3/assets/lib/font-awesome/css/font-awesome.min.css"
}
]
}

View File

@ -6,24 +6,22 @@ window.addEventListener("load", () => {
let dark = initDarkMode()
setTheme(dark?"dark":"light")
// Math Render
document.querySelectorAll(".language-math").forEach(el => {
Vditor.mathRender(coverThis(el), {
cdn: CDN
})
})
const root = document.getElementById("vditor-render").parentElement
root.classList.add("vditor-reset")
// Render
render("language-mindmap", Vditor.mindmapRender, dark)
render("language-mermaid", Vditor.mermaidRender, dark)
render("language-echarts", Vditor.chartRender, dark)
render("language-abc", Vditor.abcRender)
render("language-graphviz", Vditor.graphvizRender)
render("language-flowchart", Vditor.flowchartRender)
render("language-halo", Vditor.haloRender)
const renderTheme = dark?"dark":"classic"
Vditor.mathRender(root, {cdn: CDN})
Vditor.mindmapRender(root, CDN, renderTheme)
Vditor.mermaidRender(root, CDN, renderTheme)
Vditor.chartRender(root, CDN, renderTheme)
Vditor.abcRender(root, CDN)
Vditor.graphvizRender(root, CDN)
Vditor.flowchartRender(root, CDN)
Vditor.haloRender(root, CDN)
// Render Media
let mediaRenderOption = document.getElementById("render-script").dataset.mediarender
let mediaRenderOption = document.getElementById("vditor-render").dataset.mediarender
if (mediaRenderOption==="true") {
let article = document.getElementById("render-script").parentElement;
let article = document.getElementById("vditor-render").parentElement;
Vditor.mediaRender(article)
}
})
@ -34,7 +32,7 @@ window.addEventListener("load", () => {
* @returns {boolean} 初始暗黑模式状态
*/
function initDarkMode() {
let darkModeChange = document.getElementById("render-script").dataset.dark
let darkModeChange = document.getElementById("vditor-render").dataset.dark
let dark = false
// 检测暗黑模式策略
switch (darkModeChange) {
@ -72,37 +70,4 @@ function initSystemDarkMode() {
*/
function setTheme(theme) {
Vditor.setContentTheme(theme, THEME_PREFIX)
}
/**
* 通用渲染
* @param selector 选择器
* @param callback 渲染方法
* @param dark 暗色模式null为不配置
*/
function render(selector, callback, dark=null) {
let mindmap = document.getElementsByClassName(selector)
for (let i = 0; i < mindmap.length;i++) {
const el = coverThis(mindmap[i])
if (dark) {
callback(el, CDN, dark?"dark":"classic")
} else {
callback(el, CDN)
}
}
}
/**
* 符合Vditor渲染器要求
* 需要在外套一层div
* @param el 元素
* @returns {*}
*/
function coverThis(el) {
let copy = el.cloneNode(true)
el.innerHTML = ""
el.className = "vditor-reset"
el.dataset.code = ""
el.append(copy)
return el
}