Merge branch 'dev'
Build Plugin JAR File / build (push) Successful in 1m5s Details
Build Plugin JAR File / github-release (push) Has been skipped Details

# Conflicts:
#	.gitea/workflows/workflow.yaml
This commit is contained in:
zhengyi 2023-12-07 19:12:43 +08:00
commit 2d238d964d
19 changed files with 303 additions and 100 deletions

View File

@ -2,7 +2,7 @@ name: Build Plugin JAR File
on:
push:
branches: [ main ]
branches: [ main, dev ]
release:
types:
- created

View File

@ -2,7 +2,20 @@
---
本插件将Vditor整合进Halo支持所见即所得编辑模式。
# This Version 🏷v1.2.0
> ⚠️ 破坏性: 设置中有关渲染器的设置项会恢复到默认值,您需要重新进行设置
- 🚸 将设置拆分为编辑器和渲染两个部分
- ✨ 为编辑器添加多国语言支持
- ✨ 添加启用编辑器代码块的选项
- ✨ 添加跟随Joe 3.0主题切换暗色模式的选项
- 🐛 修复单页未对内容进行渲染的问题
- 🐛 修复所见即所得模式下部分内容被小工具栏遮挡的问题 [issue by musnows](https://github.com/justice2001/halo-plugin-vditor/issues/2)
---
本插件将Vditor整合进Halo支持所见即所得编辑模式。 Support English (*Only Editor)!
编辑器支持数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、graphviz 渲染、plantumlUML图
@ -45,13 +58,23 @@
- [x] 添加打字机模式
- [x] 支持附件选取插入
- [x] 支持暗色主题渲染
- [ ] 跟随主题的暗色模式(定向适配)
- [ ] `🧪` 支持多媒体渲染
- [x] `💻` 拆分配置为编辑器配置与渲染配置两部分
- [x] `💻` 添加多国语言支持 `手搓翻译函数实现`
- [ ] `💻` 支持代码高亮及复制
- [x] `💻` 跟随主题的暗色模式(joe主题)
- [ ] 将Vditor前台渲染资源全量引入本地
- [ ] 添加配置是否使用CDN加载前台资源
- [ ] 支持代码高亮及复制
- [ ] 支持多媒体渲染
- [ ] 添加多国语言支持
- [ ] 内置渲染器与ToolBench不兼容
- [ ] `🐛` 内置渲染器与ToolBench不兼容
- [ ] `📄` 自定义暗色模式触发方式
- [ ] `📄` 支持AI编写与修改
**注释**
- `🧪` 当前已在某个版本进行实验性发布
- `📄` 一个想法如果你有什么比较好的建议可以在ISSUE中提出
- `💻` 计划在下个版本推出
- `🐛` 这是一个BUG但是比较难处理
## 🙏 鸣谢
@ -114,6 +137,12 @@ halo:
- "/path/to/plugin-starter"
```
## 🧑‍💼发布
您可以在 [Gitea ISSUE](https://git.mczhengyi.top/zhengyi/halo-plugin-vditor/issues) 看到该项目的进展
项目发布在 [Github](https://github.com/justice2001/halo-plugin-vditor) 和 [Gitee](https://gitee.com/zhengyi59/halo-plugin-vditor)
## 📄参考文档
- [Halo官方文档](https://docs.halo.run)

View File

@ -11,7 +11,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@halo-dev/components": "^1.5.0",
"@halo-dev/components": "^1.7.0",
"@halo-dev/console-shared": "^2.8.0",
"canvas-confetti": "^1.6.0",
"vditor": "^3.9.6",

View File

@ -2,8 +2,8 @@ lockfileVersion: '6.0'
dependencies:
'@halo-dev/components':
specifier: ^1.5.0
version: 1.5.0(vue@3.3.4)
specifier: ^1.7.0
version: 1.9.0(vue@3.3.4)
'@halo-dev/console-shared':
specifier: ^2.8.0
version: 2.10.0(vue@3.3.4)
@ -533,28 +533,36 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@floating-ui/core@0.3.1:
resolution: {integrity: sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==}
/@floating-ui/core@1.5.0:
resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
dependencies:
'@floating-ui/utils': 0.1.6
dev: false
/@floating-ui/dom@0.1.10:
resolution: {integrity: sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==}
/@floating-ui/dom@1.1.1:
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
dependencies:
'@floating-ui/core': 0.3.1
'@floating-ui/core': 1.5.0
dev: false
/@floating-ui/utils@0.1.6:
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
dev: false
/@halo-dev/api-client@2.10.0:
resolution: {integrity: sha512-c1fgp+xzE1gQ7O36miWU2Oz+Uh1G4Hqs3clxLSxBD7vuvktgndZGZblNDi5morHB9UkiauAIQP0+PH9L9jwBfA==}
dev: false
/@halo-dev/components@1.5.0(vue@3.3.4):
resolution: {integrity: sha512-zVRY2AzeE83fR5omZO8q6R/kAAxZ7iSgVNcLxTjOXSPl0SP3HD4LWXoloO1rWoa/O/JUlFY2WqBAEWnu8miCGA==}
/@halo-dev/components@1.9.0(vue@3.3.4):
resolution: {integrity: sha512-Rc6zK7Uno+kLqvmZkv/NIv1EUVcFzS5FR7s0ai3LHbwybFF2BNul9HEEslS3Z5a9C0M0arnvkjpUlyBmrWbfRQ==}
peerDependencies:
vue: ^3.2.37
vue-router: ^4.0.16
vue: ^3.3.4
vue-router: ^4.2.4
dependencies:
floating-vue: 2.0.0-beta.20(vue@3.3.4)
floating-vue: 2.0.0-beta.24(vue@3.3.4)
vue: 3.3.4
transitivePeerDependencies:
- '@nuxt/kit'
dev: false
/@halo-dev/console-shared@2.10.0(vue@3.3.4):
@ -2058,12 +2066,16 @@ packages:
resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==}
dev: true
/floating-vue@2.0.0-beta.20(vue@3.3.4):
resolution: {integrity: sha512-N68otcpp6WwcYC7zP8GeJqNZVdfvS7tEY88lwmuAHeqRgnfWx1Un8enzLxROyVnBDZ3TwUoUdj5IFg+bUT7JeA==}
/floating-vue@2.0.0-beta.24(vue@3.3.4):
resolution: {integrity: sha512-URSzP6YXaF4u1oZ9XGL8Sn8puuM7ivp5jkOUrpy5Q1mfo9BfGppJOn+ierTmsSUfJEeHBae8KT7r5DeI3vQIEw==}
peerDependencies:
'@nuxt/kit': ^3.2.0
vue: ^3.2.0
peerDependenciesMeta:
'@nuxt/kit':
optional: true
dependencies:
'@floating-ui/dom': 0.1.10
'@floating-ui/dom': 1.1.1
vue: 3.3.4
vue-resize: 2.0.0-alpha.1(vue@3.3.4)
dev: false

View File

@ -0,0 +1,8 @@
import type {I18nLang} from "@/type/i18n";
const lang: I18nLang = {
"insert_image": "Insert Image"
}
export default lang

View File

@ -0,0 +1,8 @@
import type {I18nLang} from "@/type/i18n";
const lang: I18nLang = {
"insert_image": "插入图片"
}
export default lang

View File

@ -0,0 +1,7 @@
import type {I18nLang} from "@/type/i18n";
const lang: I18nLang = {
"insert_image": "插入圖片"
}
export default lang

View File

@ -3,6 +3,7 @@ export declare type EditorConfig = {
enable_render: boolean;
defaultRenderMode: "ir" | "wysiwyg" | "sv" | undefined;
typeWriterMode: boolean;
codeBlockPreview: boolean;
};
};
@ -12,4 +13,6 @@ export declare type Options = {
after: () => void;
input: () => void;
showAttachment: () => void;
language: string;
codeBlockPreview: boolean;
}

1
console/src/type/i18n.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare type I18nLang = {[key: string]: string}

View File

@ -0,0 +1,19 @@
import zhCN from "@/i18n/zh-CN";
import type {I18nLang} from "@/type/i18n";
import zhTW from "@/i18n/zh-TW";
import enUS from "@/i18n/en-US";
const langDict: {[key: string]: I18nLang} = {
"zh_CN": zhCN,
"zh_TW": zhTW,
"en_US": enUS
}
/**
*
* @param key key
* @param lang
*/
export function t(key: string, lang: keyof II18n): string {
return langDict[lang][key]
}

View File

@ -1,5 +1,6 @@
import type {Options} from "@/type/editor";
import {mdiImage} from "@/utils/icon";
import {t} from "@/utils/i18n-utils";
export function getOptions(options: Options): IOptions {
return {
@ -7,6 +8,7 @@ export function getOptions(options: Options): IOptions {
mode: options.defaultRenderMode,
typewriterMode: options.typeWriterMode,
icon: "material",
lang: getLanguage(options.language),
toolbarConfig: {
pin: true,
},
@ -15,13 +17,14 @@ export function getOptions(options: Options): IOptions {
},
after: options.after,
input: options.input,
toolbar: getToolbar(options.showAttachment),
toolbar: getToolbar(options.showAttachment, getLanguage(options.language)),
counter: {
enable: true
},
preview: {
markdown: {
toc: true
toc: true,
codeBlockPreview: options.codeBlockPreview
}
},
outline: {
@ -34,7 +37,17 @@ export function getOptions(options: Options): IOptions {
}
}
function getToolbar(showAttachmentCb: () => void): (string | IMenuItem)[] | undefined {
function getLanguage(lang="zh-CN"):keyof II18n {
switch (lang) {
case "zh-CN": return "zh_CN";
case "zh-TW": return "zh_TW";
case "en-US": return "en_US";
case "es": return "en_US";
default: return "zh_CN";
}
}
function getToolbar(showAttachmentCb: () => void, lang: keyof II18n): (string | IMenuItem)[] | undefined {
return [
"emoji",
"headings",
@ -59,7 +72,7 @@ function getToolbar(showAttachmentCb: () => void): (string | IMenuItem)[] | unde
{
name: "attachment",
icon: mdiImage,
tip: "插入图片",
tip: t("insert_image", lang),
tipPosition: "n",
hotkey: "⇧⌘P",
click: showAttachmentCb

View File

@ -52,6 +52,10 @@ const attachmentSelect = (attachments: AttachmentLike[]) => {
onMounted(async () => {
let mode: "ir" | "wysiwyg" | "sv" | undefined = "ir"
let typeWriterMode: boolean = false
let codeBlockPreview: boolean = true
// :
const lang = localStorage.getItem("locale") || "zh-CN"
try {
const response = await fetch(
@ -60,6 +64,7 @@ onMounted(async () => {
const editorConfig: EditorConfig = await response.json();
mode = editorConfig.basic.defaultRenderMode
typeWriterMode = editorConfig.basic.typeWriterMode
codeBlockPreview = editorConfig.basic.codeBlockPreview
} catch (e) {
// ignore this
}
@ -70,7 +75,9 @@ onMounted(async () => {
vditor.value.setValue(props.raw || "# Title Here")
},
input: debounceOnUpdate,
showAttachment: () => attachmentSelectorModalShow.value = true
showAttachment: () => attachmentSelectorModalShow.value = true,
language: lang,
codeBlockPreview: codeBlockPreview
}))
})
@ -93,4 +100,10 @@ onMounted(async () => {
#plugin-vditor-mde ol {
list-style: decimal;
}
/** Fix content was covered by vditor panel in wysiwyg mode */
#plugin-vditor-mde button,
#plugin-vditor-mde input {
line-height: normal;
}
</style>

View File

@ -1,50 +0,0 @@
package top.mczhengyi.vditor;
import com.google.common.base.Throwables;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.ReactivePostContentHandler;
@Component
@AllArgsConstructor
@Slf4j
public class VditorPostContentHandler implements ReactivePostContentHandler {
private final ReactiveSettingFetcher reactiveSettingFetcher;
@Override
public Mono<PostContentContext> handle(PostContentContext contentContext) {
return reactiveSettingFetcher.fetch("basic", BasicConfig.class)
.map(basicConfig -> {
if (basicConfig.enable_render) {
contentContext.setContent(renderScript(basicConfig) + "\n" + contentContext.getContent());
}
return contentContext;
})
.onErrorResume(e -> {
log.error("VditorHeadProcessor process failed", Throwables.getRootCause(e));
return Mono.empty();
});
}
private String renderScript(BasicConfig basicConfig) {
return """
<script src="/plugins/vditor-mde/assets/static/method.min.js"></script>
<script src="/plugins/vditor-mde/assets/static/render.js" id="render-script"
data-dark="%s" data-mediaRender="%s"></script>
""".formatted(basicConfig.darkMode, basicConfig.mediaRender);
}
@Data
public static class BasicConfig {
Boolean enable_render;
String defaultRenderMode;
Boolean typeWriterMode;
String darkMode;
Boolean mediaRender;
}
}

View File

@ -0,0 +1,10 @@
package top.mczhengyi.vditor.bean;
import lombok.Data;
@Data
public class RenderConfig {
Boolean enableRender;
String darkMode;
Boolean mediaRender;
}

View File

@ -0,0 +1,34 @@
package top.mczhengyi.vditor.extension;
import com.google.common.base.Throwables;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.ReactivePostContentHandler;
import top.mczhengyi.vditor.bean.RenderConfig;
import top.mczhengyi.vditor.utils.ScriptUtils;
@Component
@AllArgsConstructor
@Slf4j
public class VditorPostContentHandler implements ReactivePostContentHandler {
private final ReactiveSettingFetcher reactiveSettingFetcher;
@Override
public Mono<PostContentContext> handle(PostContentContext contentContext) {
return reactiveSettingFetcher.fetch("render", RenderConfig.class)
.map(renderConfig -> {
if (renderConfig.getEnableRender()) {
contentContext.setContent(ScriptUtils.renderScript(renderConfig) + "\n" + contentContext.getContent());
}
return contentContext;
})
.onErrorResume(e -> {
log.error("VditorHeadProcessor process failed", Throwables.getRootCause(e));
return Mono.just(contentContext);
});
}
}

View File

@ -0,0 +1,32 @@
package top.mczhengyi.vditor.extension;
import com.google.common.base.Throwables;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.ReactiveSinglePageContentHandler;
import top.mczhengyi.vditor.bean.RenderConfig;
import top.mczhengyi.vditor.utils.ScriptUtils;
@Component
@AllArgsConstructor
@Slf4j
public class VditorSinglePageContentHandler implements ReactiveSinglePageContentHandler {
private final ReactiveSettingFetcher reactiveSettingFetcher;
@Override
public Mono<SinglePageContentContext> handle(SinglePageContentContext contentContext) {
return reactiveSettingFetcher.fetch("render", RenderConfig.class)
.map(renderConfig -> {
if (renderConfig.getEnableRender()) {
contentContext.setContent(ScriptUtils.renderScript(renderConfig) + "\n" + contentContext.getContent());
}
return contentContext;
})
.onErrorResume(e -> {
log.error("VditorHeadProcessor process failed", Throwables.getRootCause(e));
return Mono.empty();
});
}
}

View File

@ -0,0 +1,43 @@
package top.mczhengyi.vditor.utils;
import top.mczhengyi.vditor.bean.RenderConfig;
public class ScriptUtils {
public static String renderScript(RenderConfig renderConfig) {
StringBuilder script = new StringBuilder();
script.append(basicScript(renderConfig));
// 如果是跟随Joe 3.0则注入脚本
if ("joe".equals(renderConfig.getDarkMode()))
script.append(joeDarkMode());
return script.toString();
}
public static String basicScript(RenderConfig renderConfig) {
return """
<script src="/plugins/vditor-mde/assets/static/method.min.js"></script>
<script src="/plugins/vditor-mde/assets/static/render.js" id="render-script"
data-dark="%s" data-mediaRender="%s"></script>
""".formatted(renderConfig.getDarkMode(), renderConfig.getMediaRender());
}
public static String joeDarkMode() {
return """
<script>
window.addEventListener("load", () => {
var html = document.getElementsByTagName("html")[0]
if (!html) return
setTheme(html.dataset.mode)
var callback = (mutation) => {
if (mutation[0].attributeName=="data-mode") {
console.log("CHANGED")
var mode = mutation[0].target.dataset.mode
setTheme(mode)
}
}
var observer = new MutationObserver(callback)
observer.observe(html, {attributes:true})
})
</script>
""";
}
}

View File

@ -3,8 +3,19 @@ kind: ExtensionDefinition
metadata:
name: ext-def-vditor-post-content
spec:
className: top.mczhengyi.vditor.VditorPostContentHandler
className: top.mczhengyi.vditor.extension.VditorPostContentHandler
# 文章内容扩展点的名称,固定值
extensionPointName: reactive-post-content-handler
displayName: "VditorPostContentHandler"
description: "Vditor render support for post content"
description: "Vditor render support for post content"
---
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: ext-def-vditor-singlepage-content
spec:
className: top.mczhengyi.vditor.extension.VditorSinglePageContentHandler
# 自定义页面内容扩展点的名称,固定值
extensionPointName: reactive-singlepage-content-handler
displayName: "VditorSinglePageContentHandler"
description: "Vditor render support for single page content."

View File

@ -5,13 +5,8 @@ metadata:
spec:
forms:
- group: basic
label: 基本设置
label: 编辑器
formSchema:
- $formkit: checkbox
name: enable_render
label: 内置渲染器
help: 内置渲染器会对前台文章页面的公式、图表进行渲染
value: true
- $formkit: select
name: defaultRenderMode
label: 默认渲染模式
@ -29,18 +24,33 @@ spec:
label: 打字机模式
help: 启用打字机模式将会始终将光标保持在视野中心位置
value: true
- $formkit: select
name: darkMode
label: 暗色模式
help: Vditor渲染的组件进入暗色模式的方式
value: disabled
options:
- label: 禁用暗色模式
value: disabled
- label: 跟随系统
value: system
- $formkit: checkbox
name: mediaRender
label: "*实验性 渲染媒体标签"
help: 启用该功能会搜索文章内可解析的链接进行解析, 这可能会导致您的渲染出现问题!
value: false
name: codeBlockPreview
label: 编辑器代码块渲染
help: 关闭后代码块(包括图表)在所见即所得和即时渲染模式下将不会被渲染
value: true
- group: render
label: 渲染
formSchema:
- $formkit: checkbox
name: enableRender
label: 内置渲染器
help: 内置渲染器会对前台文章页面的公式、图表进行渲染
value: true
- $formkit: select
name: darkMode
label: 暗色模式
help: Vditor渲染的组件进入暗色模式的方式
value: disabled
options:
- label: 禁用暗色模式
value: disabled
- label: 跟随系统
value: system
- label: 跟随Joe 3.0
value: joe
- $formkit: checkbox
name: mediaRender
label: "*实验性 渲染媒体标签"
help: 启用该功能会搜索文章内可解析的链接进行解析, 这可能会导致您的渲染出现问题!
value: false