diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b571e9b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,97 @@ +# 更新记录 + +### TODO v1.5.2 + +- [ ] 更换vditor版本为官方版本 (pending vditor 3.9.9) +- [ ] 修复所见即所得模式下使用工具栏修改属性不生效的问题 (pending vditor 3.9.9) +- [ ] 为用户/开发者提供接入自定义渲染块的能力 +- [ ] 设置中添加调试快捷开关 + +### v1.5.1 + +- ✨ 添加图集的快捷插入按钮 +- 🐛 修复脑图、图标、echart暗色模式渲染错误的问题 +- 🐛 解决更新插件前台资源缓存的问题 +- 🐛 解决暗色模式渲染异常的问题 +- 🐛 解决主题代码块等样式覆盖的问题 + +### v1.5.0 + +- ✨ 使用新的快速插入表单,界面更美观 +- ✨ 支持渲染其他插件/主题的自定义组件(需要开发者支持) +- ✨ 支持加入快速插入链接(需要开发者支持) +- ✨ 允许用户禁用HTML代码块隐藏的特性 +- ✨ 支持用户自定义上传图片的格式 +- ✨ vditor-halo-render添加组图组件(gallery) +- 🐛 修复图片拖拽插入错位的问题 +- 🐛 修复echarts,mindmap无法渲染的问题 +- 🐛 修复所见即所得模式下超链接颜色丢失的问题 +- 🚀 优化前端渲染逻辑,去除无用的dom查询 + +### v1.4.0 + +- ✨ 支持编辑器加载状态展示 +- ✨ 支持拖拽上传图片 +- ✨ 添加快速插入自定义块的按钮 +- ✨ 添加仅在Markdown文章注入解析器的选项 +- ⬆️ 更新halo-vditor至3.9.9版本(对应vditor 3.9.8) +- ⬆️ 更新vditor-halo-render到1.1.0版本 +- ✨ 重新设计tip、git模块 +- ✨ 添加资源下载模块 +- ✨ 为git添加fork、topic信息 +- ✨ 为git添加缓存 +- ✨ 添加对Gitee的支持 +- 🐛 修复invalid type界面边框与其他模块不对齐的问题 + +### v1.3.2 + +- 🐛 编辑器编写代码块出现闪屏问题 + +### v1.3.1 + +- 🐛 修复Vditor编辑器切换到其他编辑器导致的样式冲突问题 +- 🐛 修复编辑器在Halo中出现页面滚动条的问题 +- 🐛 修复前台数学公式无法渲染的问题 + +### v1.3.0 + +- ✨ 支持更多tips渲染 +- ✨ 支持github仓库信息渲染 +- ✨ 升级vditor为分支halo-vditor版本 https://github.com/justice2001/halo-vditor +- ⬆️ 升级halo-vditor到3.9.8版本(对应vditor3.9.7) +- 🐛 修复页面偶尔出现加载shi问题,使用本地资源 + +### v1.2.0 + +- 🚸 将设置拆分为编辑器和渲染两个部分 +- ✨ 为编辑器添加多国语言支持(简中、繁中、英语) +- ✨ 添加启用编辑器代码块的选项 +- ✨ 添加跟随Joe 3.0主题切换暗色模式的选项 +- 🐛 修复单页未对内容进行渲染的问题 +- 🐛 修复所见即所得模式下部分内容被小工具栏遮挡的问题 + +### v1.1.0 + +- ⬆️ 更新 Halo 最低兼容版本至 2.8.0 +- ✨ 添加默认编辑模式设置 +- ✨ 添加打字机模式设置 +- ✨ 添加插入图片功能(快捷键:⇧⌘P) +- ✨ 添加自动暗色模式的支持 +- 🐛 修复了编辑器中有序列表左侧序号消失的问题 +- 🐛 修复全屏编辑器左侧被Halo导航栏遮挡的问题 +- 🧪 实验性特性: 支持多媒体渲染 + +### v1.0.1 + +- ⚡ 修改为仅在文章中加入vditor render,优化部分页面加载速度 +- ⚡ 修改vditor库从本地加载,优化页面加载速度 +- 🐛 修复由于id重复可能存在的冲突问题 +- ⚰️ 删除了部分无用代码 + +### v1.0.0 + +- ✨ 集成强大的Markdown编辑器Vditor进Halo中 +- ✨ 提供所见即所得(wysiwyg)、即时渲染(ir)、分屏渲染(sv)三种渲染模式 +- ✨ 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、多媒体、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、plantumlUML图 +- ✨ 支持字符计数 +- ✨ 支持在前台注入渲染脚本(需在设置中开启) \ No newline at end of file diff --git a/console/src/i18n/en-US.ts b/console/src/i18n/en-US.ts index 648a8aa..628583d 100644 --- a/console/src/i18n/en-US.ts +++ b/console/src/i18n/en-US.ts @@ -24,6 +24,13 @@ const lang: I18nLang = { link: "Link", password: "Password", quick_insert: "Quick Insert", + insert_gallery: "Insert Gallery", + grid: "Grid", + linear: "Linear", + image_group: "Image Group", + image: "Image", + image_alt: "Alt", + image_alt_ph: "Use File Name If You Dont Place This", }; export default lang; diff --git a/console/src/i18n/zh-CN.ts b/console/src/i18n/zh-CN.ts index e7698a8..d72b13b 100644 --- a/console/src/i18n/zh-CN.ts +++ b/console/src/i18n/zh-CN.ts @@ -24,6 +24,13 @@ const lang: I18nLang = { link: "链接", password: "密码", quick_insert: "快速插入", + insert_gallery: "插入图集", + grid: "九宫格", + linear: "线性", + image_group: "图片组", + image: "图片", + image_alt: "图片介绍", + image_alt_ph: "不填写此项则默认使用图像文件名", }; export default lang; diff --git a/console/src/i18n/zh-TW.ts b/console/src/i18n/zh-TW.ts index f165317..3d7c4d8 100644 --- a/console/src/i18n/zh-TW.ts +++ b/console/src/i18n/zh-TW.ts @@ -24,6 +24,13 @@ const lang: I18nLang = { link: "鏈接", password: "密碼", quick_insert: "快速插入", + insert_gallery: "插入圖集", + grid: "九宮格", + linear: "線性", + image_group: "圖片組", + image: "圖片", + image_alt: "圖片介紹", + image_alt_ph: "不填寫此項則默認使用影像檔名", }; export default lang; diff --git a/console/src/model/TemplateModal.vue b/console/src/model/TemplateModal.vue index dccc58f..dd69e1a 100644 --- a/console/src/model/TemplateModal.vue +++ b/console/src/model/TemplateModal.vue @@ -31,8 +31,11 @@ const generateCode = () => { emit("done", htmlEncode(code)); return; } - props.schema.handler && props.schema.handler(data.value); - emit("done", null); + + emit( + "done", + (props.schema.handler && props.schema.handler(data.value)) || null + ); }; // 修改FormKit ID来实现Schema重载 diff --git a/console/src/schema/gallery.ts b/console/src/schema/gallery.ts new file mode 100644 index 0000000..21dd527 --- /dev/null +++ b/console/src/schema/gallery.ts @@ -0,0 +1,83 @@ +import type { Schema, SchemaData } from "@/type/editor"; +import { t } from "@/utils/i18n-utils"; + +const schema: Schema = { + type: "template", + id: "gallery", + icon: "", + name: t("insert_gallery"), + formKit: [ + { + $formkit: "select", + name: "type", + label: t("type"), + value: "grid", + options: { + grid: t("grid"), + linear: t("linear"), + }, + }, + { + $formkit: "text", + name: "title", + label: t("title"), + value: "", + }, + { + $formkit: "repeater", + name: "attachments", + label: t("image_group"), + min: 1, + value: [{}], + children: [ + { + $formkit: "attachment", + name: "attachment", + label: t("image"), + accepts: ["image/*"], + value: "", + }, + { + $formkit: "text", + name: "attach_title", + label: t("image_alt"), + value: "", + placeholder: t("image_alt_ph"), + }, + ], + }, + ], + handler: (data: SchemaData) => { + // title type list[] + const galleryData = data as GallerySchemaData; + let html = `\`\`\`halo\ngallery:${galleryData.type}`; + if (galleryData.title) { + html += `[${galleryData.title}]`; + } + html += "\n"; + galleryData.attachments?.forEach((att) => { + let title = ""; + if (att.attach_title) { + title = att.attach_title; + } else { + const start = att.attachment.lastIndexOf("/"); + const end = att.attachment.lastIndexOf("."); + title = att.attachment.slice(start, end); + } + html += `![${title}](${att.attachment})\n`; + }); + html += "```"; + return html; + }, +}; + +export interface GallerySchemaData extends SchemaData { + title: string; + type: "grid" | "linear"; + attachments: Array<{ + attachment: string; + attach_title?: string; + }>; +} + +export default schema; diff --git a/console/src/type/editor.d.ts b/console/src/type/editor.d.ts index 6a79c54..6611ff3 100644 --- a/console/src/type/editor.d.ts +++ b/console/src/type/editor.d.ts @@ -22,7 +22,11 @@ export interface Schema { // 解析后处理 afterHandle?: (data: { [key: string]: string }, code: string) => string; // 覆盖解析 - handler?: (data: { [key: string]: string }) => string; + handler?: (data: SchemaData) => string; +} + +export interface SchemaData { + _id?: string; } export interface QuickInsert { diff --git a/console/src/utils/vditor-utils.ts b/console/src/utils/vditor-utils.ts index 8ff25e0..9d32817 100644 --- a/console/src/utils/vditor-utils.ts +++ b/console/src/utils/vditor-utils.ts @@ -4,6 +4,7 @@ import { t } from "@/utils/i18n-utils"; import tips from "@/schema/tips"; import git from "@/schema/git"; import drive from "@/schema/drive"; +import gallery from "@/schema/gallery"; export function getOptions(options: Options): IOptions { const cdn = @@ -141,6 +142,11 @@ function getToolbar( icon: t("insert_drive"), click: () => openModal(drive), }, + { + name: "insert_gallery", + icon: t("insert_gallery"), + click: () => openModal(gallery), + }, ], }, { diff --git a/src/main/java/top/mczhengyi/vditor/extension/VditorPostContentHandler.java b/src/main/java/top/mczhengyi/vditor/extension/VditorPostContentHandler.java index f932460..6efb9a0 100644 --- a/src/main/java/top/mczhengyi/vditor/extension/VditorPostContentHandler.java +++ b/src/main/java/top/mczhengyi/vditor/extension/VditorPostContentHandler.java @@ -3,7 +3,9 @@ package top.mczhengyi.vditor.extension; import com.google.common.base.Throwables; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; import org.springframework.stereotype.Component; +import org.springframework.util.PropertyPlaceholderHelper; import reactor.core.publisher.Mono; import run.halo.app.plugin.ReactiveSettingFetcher; import run.halo.app.theme.ReactivePostContentHandler; @@ -17,13 +19,16 @@ public class VditorPostContentHandler implements ReactivePostContentHandler { private final ReactiveSettingFetcher reactiveSettingFetcher; + private final PluginWrapper pluginWrapper; + @Override public Mono handle(PostContentContext contentContext) { return reactiveSettingFetcher.fetch("render", RenderConfig.class) .map(renderConfig -> { if (renderConfig.getEnableRender()&& (!renderConfig.getOnlyMarkdown() || contentContext.getRawType().equals("markdown"))) { - contentContext.setContent(ScriptUtils.renderScript(renderConfig) + "\n" + contentContext.getContent()); + var content = ScriptUtils.renderScript(renderConfig) + "\n" + contentContext.getContent(); + contentContext.setContent(ScriptUtils.setContentProperty(content, pluginWrapper)); } return contentContext; }) diff --git a/src/main/java/top/mczhengyi/vditor/utils/ScriptBuilder.java b/src/main/java/top/mczhengyi/vditor/utils/ScriptBuilder.java new file mode 100644 index 0000000..5868416 --- /dev/null +++ b/src/main/java/top/mczhengyi/vditor/utils/ScriptBuilder.java @@ -0,0 +1,35 @@ +package top.mczhengyi.vditor.utils; + +/** + * Script Builder + * 用于构造Script、内嵌Script、样式表 + * @author zhengyi59 + */ +public class ScriptBuilder { + private final StringBuilder script; + + ScriptBuilder() { + this.script = new StringBuilder(); + } + + public ScriptBuilder script(String path, String id) { + this.script.append("" + .formatted(path, id)); + return this; + } + + public ScriptBuilder innerScript(String script) { + this.script.append("".formatted(script)); + return this; + } + + public ScriptBuilder stylesheet(String path, String id) { + this.script.append("" + .formatted(path, id)); + return this; + } + + public String getScript() { + return this.script.toString(); + } +} diff --git a/src/main/java/top/mczhengyi/vditor/utils/ScriptUtils.java b/src/main/java/top/mczhengyi/vditor/utils/ScriptUtils.java index c613db5..b60bf4e 100644 --- a/src/main/java/top/mczhengyi/vditor/utils/ScriptUtils.java +++ b/src/main/java/top/mczhengyi/vditor/utils/ScriptUtils.java @@ -1,44 +1,31 @@ package top.mczhengyi.vditor.utils; +import org.pf4j.PluginWrapper; +import org.springframework.util.PropertyPlaceholderHelper; import top.mczhengyi.vditor.bean.RenderConfig; +import java.util.Properties; public class ScriptUtils { + static final PropertyPlaceholderHelper + PROPERTY_PLACEHOLDER_HELPER = new PropertyPlaceholderHelper("${", "}"); + 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(); + ScriptBuilder script = new ScriptBuilder(); + script.stylesheet("vditor-render.css", "style") + .script("dist/method.min.js", "methods") + .script("render.js", "render"); + if (renderConfig.getMediaRender()) + script.script("external/media-render.js", "media"); + if (!renderConfig.getDarkMode().equals("disabled")) { + script.script("dark-mode/dark-%s.js".formatted(renderConfig.getDarkMode()), "dark-mode"); + } + script.innerScript("initRender()"); + return script.getScript(); } - public static String basicScript(RenderConfig renderConfig) { - return """ - - - - """.formatted(renderConfig.getDarkMode(), renderConfig.getMediaRender()); - } - - public static String joeDarkMode() { - return """ - - """; + public static String setContentProperty(String script, PluginWrapper pluginWrapper) { + final Properties properties = new Properties(); + properties.setProperty("version", pluginWrapper.getDescriptor().getVersion()); + return PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(script, properties); } } diff --git a/src/main/resources/static/dark-mode/dark-joe.js b/src/main/resources/static/dark-mode/dark-joe.js new file mode 100644 index 0000000..44fdb35 --- /dev/null +++ b/src/main/resources/static/dark-mode/dark-joe.js @@ -0,0 +1,7 @@ +window.addEventListener("load", () => { + console.log("Use Joe Dark Mode") + let html = document.getElementsByTagName("html")[0] + if (!html) return + const mode = html.dataset.mode + setDarkMode(mode === "dark") +}) \ No newline at end of file diff --git a/src/main/resources/static/dark-mode/dark-system.js b/src/main/resources/static/dark-mode/dark-system.js new file mode 100644 index 0000000..8378b3f --- /dev/null +++ b/src/main/resources/static/dark-mode/dark-system.js @@ -0,0 +1,5 @@ +window.addEventListener("load", () => { + console.log("Use System Dark Mode") + let media = window.matchMedia('(prefers-color-scheme: dark)'); + setDarkMode(media.matches) +}) \ No newline at end of file diff --git a/src/main/resources/static/external/media-render.js b/src/main/resources/static/external/media-render.js new file mode 100644 index 0000000..1ff623e --- /dev/null +++ b/src/main/resources/static/external/media-render.js @@ -0,0 +1,8 @@ +addExternal((conf) => { + console.log("Run External Function: Media Render!") + let mediaRenderOption = document.getElementById("vditor-render").dataset.mediarender + if (mediaRenderOption==="true") { + let article = document.getElementById("vditor-render").parentElement; + Vditor.mediaRender(article) + } +}) \ No newline at end of file diff --git a/src/main/resources/static/render.js b/src/main/resources/static/render.js index f843449..3a9260a 100644 --- a/src/main/resources/static/render.js +++ b/src/main/resources/static/render.js @@ -1,11 +1,32 @@ -const THEME_PREFIX="/plugins/vditor-mde/assets/static/dist/css/content-theme" +const THEME_PREFIX="/plugins/vditor-mde/assets/static/themes" const CDN = "/plugins/vditor-mde/assets/static" -window.addEventListener("load", () => { - // 暗色模式初始化 - let dark = initDarkMode() - setTheme(dark?"dark":"light") +/** 拓展处理 ({dark}) => void */ +let functionList = [] +let darkMode = false +/** + * 处理渲染 + * @param func + */ +function addExternal(func) { + functionList.push(func) +} + +/** + * 设置暗色模式 + * @param {Boolean} dark + */ +function setDarkMode(dark = false) { + darkMode = dark +} + +/** + * 渲染 + * @param dark + */ +function render(dark) { + Vditor.setContentTheme(dark?"dark":"light", THEME_PREFIX) const root = document.getElementById("vditor-render").parentElement root.classList.add("vditor-reset") // Render @@ -18,56 +39,17 @@ window.addEventListener("load", () => { Vditor.graphvizRender(root, CDN) Vditor.flowchartRender(root, CDN) Vditor.haloRender(root, CDN) - // Render Media - let mediaRenderOption = document.getElementById("vditor-render").dataset.mediarender - if (mediaRenderOption==="true") { - let article = document.getElementById("vditor-render").parentElement; - Vditor.mediaRender(article) - } -}) - -/** - * 初始化暗色模式策略 - * 创建所需监听器 - * @returns {boolean} 初始暗黑模式状态 - */ -function initDarkMode() { - let darkModeChange = document.getElementById("vditor-render").dataset.dark - let dark = false - // 检测暗黑模式策略 - switch (darkModeChange) { - // 禁用暗黑模式 - case "disabled": break - // 跟随系统 - case "system": - dark = initSystemDarkMode() - } - return dark -} - - -/** - * 系统模式暗黑模式策略 - * @returns {boolean} - */ -function initSystemDarkMode() { - let media = window.matchMedia('(prefers-color-scheme: dark)'); - let callback = (e) => { - let prefersDarkMode = e.matches; - setTheme(prefersDarkMode?"dark":"light") - }; - if (typeof media.addEventListener === 'function') { - media.addEventListener('change', callback); - } else if (typeof media.addListener === 'function') { - media.addListener(callback); - } - return media.matches + // Run External Plugin + functionList.forEach(func => { + func({ + dark + }) + }) } /** - * 配置主题 - * @param theme 主题 + * 页面加载完成时处理任务 */ -function setTheme(theme) { - Vditor.setContentTheme(theme, THEME_PREFIX) +function initRender() { + window.addEventListener("load", () => render(darkMode)) } \ No newline at end of file diff --git a/src/main/resources/static/themes/dark.css b/src/main/resources/static/themes/dark.css new file mode 100644 index 0000000..7cc3527 --- /dev/null +++ b/src/main/resources/static/themes/dark.css @@ -0,0 +1,13 @@ +/** +This is a theme for halo-plugin-vditor +Modified from vditor content_theme + */ +.vditor-reset .language-abc svg, +.vditor-reset .language-abc path { + fill: currentColor; + color: #d1d5da; +} + +.language-graphviz polygon { + fill: rgba(66, 133, 244, .36); +} diff --git a/src/main/resources/static/themes/light.css b/src/main/resources/static/themes/light.css new file mode 100644 index 0000000..aa1be9a --- /dev/null +++ b/src/main/resources/static/themes/light.css @@ -0,0 +1,4 @@ +/** +This is a theme for halo-plugin-vditor +Modified from vditor content_theme + */ \ No newline at end of file diff --git a/src/main/resources/static/vditor-render.css b/src/main/resources/static/vditor-render.css new file mode 100644 index 0000000..2714f30 --- /dev/null +++ b/src/main/resources/static/vditor-render.css @@ -0,0 +1,39 @@ +.vditor-reset--anchor { + padding-left: 20px; +} +.vditor-reset--error { + color: #d23f31; + font-size: 12px; + display: block; + line-height: 16px; +} + +.vditor-reset .language-math, +.vditor-reset .language-echarts, +.vditor-reset .language-mindmap, +.vditor-reset .language-plantuml, +.vditor-reset .language-mermaid, +.vditor-reset .language-markmap, +.vditor-reset .language-abc, +.vditor-reset .language-flowchart, +.vditor-reset .language-graphviz { + margin-bottom: 16px; +} +.vditor-reset .language-math mjx-container:focus { + outline: none; + cursor: context-menu; +} +.vditor-reset .language-echarts, +.vditor-reset .language-mindmap { + overflow: hidden; + height: 420px; +} +.vditor-reset .language-mermaid, +.vditor-reset .language-markmap, +.vditor-reset .language-flowchart, +.vditor-reset .language-graphviz { + text-align: center; +} +.vditor-reset .language-graphviz parsererror { + overflow: auto; +} \ No newline at end of file