Explorar o código

支持paltuml

qintang %!s(int64=4) %!d(string=hai) anos
pai
achega
c68ebc140b

+ 32 - 14
README.md

@@ -18,7 +18,7 @@
 </p>
 
 <p align="center">
-<a href="https://github.com/Vanessa219/vditor/blob/master/README_en_US.md">English</a> &nbsp;|&nbsp; <a href="https://b3log.org/vditor/demo/index.html">Demo</a>
+<a href="https://github.com/Vanessa219/vditor/blob/master/README_en_US.md">English</a>  |  <a href="https://b3log.org/vditor/demo/index.html">Demo</a>
 </p>
 
 ## 💡 简介
@@ -54,7 +54,7 @@ Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编
 ## ✨  特性
 
 * 支持三种编辑模式:所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv)
-* 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、[多媒体](https://ld246.com/article/1589813914768)、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染
+* 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、[多媒体](https://ld246.com/article/1589813914768)、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、[plantuml](https://plantuml.com)UML图
 * 内置安全过滤、导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能
 * 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持[10+项](https://ld246.com/article/1549638745630#options-preview-markdown)配置
 * 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的[快捷键](https://ld246.com/article/1582778815353)、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义
@@ -192,6 +192,7 @@ Markdown 输出的 HTML 所展现的外观。内置 light,dark,wechat 3 套
 
 #### options
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | undoDelay | 历史记录间隔 | - |
@@ -201,7 +202,7 @@ Markdown 输出的 HTML 所展现的外观。内置 light,dark,wechat 3 套
 | width | 编辑器总宽度,支持 % | 'auto' |
 | placeholder | 输入区域为空时的提示 | '' |
 | lang | 多语言:en_US, ja_JP, ko_KR, zh_CN | 'zh_CN' |
-| input(value: string, previewElement?: HTMLElement) | 输入后触发  | - |
+| input(value: string, previewElement?: HTMLElement) | 输入后触发 | - |
 | focus(value: string) | 聚焦后触发 | - |
 | blur(value: string) | 失焦后触发 | - |
 | esc(value: string) | <kbd>esc</kbd> 按下后触发 | - |
@@ -237,6 +238,7 @@ new Vditor('vditor', {
 })
 ```
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | name | 唯一标示 | - |
@@ -252,6 +254,7 @@ new Vditor('vditor', {
 
 #### options.toolbarConfig
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | hide | 是否隐藏工具栏 | false |
@@ -259,6 +262,7 @@ new Vditor('vditor', {
 
 #### options.counter
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | enable | 是否启用计数器 | false |
@@ -268,6 +272,7 @@ new Vditor('vditor', {
 
 #### options.cache
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | enable | 是否使用 localStorage 进行缓存 | true |
@@ -278,6 +283,7 @@ new Vditor('vditor', {
 
 ⚠️:仅支持 wysiwyg 模式
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | enable | 是否启用评论模式 | false |
@@ -288,6 +294,7 @@ new Vditor('vditor', {
 
 #### options.preview
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | delay | 预览 debounce 毫秒间隔 | 1000 |
@@ -299,6 +306,7 @@ new Vditor('vditor', {
 
 #### options.preview.hljs
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | enable | 是否启用代码高亮 | true |
@@ -307,6 +315,7 @@ new Vditor('vditor', {
 
 #### options.preview.markdown
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | autoSpace | 自动空格 | false |
@@ -324,6 +333,7 @@ new Vditor('vditor', {
 
 #### options.preview.theme
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | current | 当前主题 | "light" |
@@ -332,6 +342,7 @@ new Vditor('vditor', {
 
 #### options.preview.math
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | inlineDigit | 内联数学公式起始 $ 后是否允许数字 | false |
@@ -343,6 +354,7 @@ new Vditor('vditor', {
 默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。
 可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | key | 按钮唯一标识,不能为空 | - |
@@ -353,6 +365,7 @@ new Vditor('vditor', {
 
 #### options.hint
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | delay | 提示 debounce 毫秒间隔 | 200 |
@@ -431,6 +444,7 @@ if (xhr.status === 200) {
 }
 ```
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | url | 上传 url | '' |
@@ -445,17 +459,18 @@ if (xhr.status === 200) {
 | headers | 请求头设置 | - |
 | filename(name: string): string | 文件名安全处理 | name => name.replace(/\W/g, '') |
 | accept | 文件上传类型,同[input accept](https://www.w3schools.com/tags/att_input_accept.asp) | - |
-| validate(files: File[]) => string \| boolean | 校验,成功时返回 true 否则返回错误信息 | - |
-| handler(files: File[]) => string \| null | 自定义上传,当发生错误时返回错误信息 | - |
+| validate(files: File[]) => string\| boolean | 校验,成功时返回 true 否则返回错误信息 | - |
+| handler(files: File[]) => string\| null | 自定义上传,当发生错误时返回错误信息 | - |
 | format(files: File[], responseText: string): string | 对服务端返回的数据进行转换,以满足内置的数据结构 | - |
 | file(files: File[]): File[] | 将上传的文件处理后再返回 | - |
 | setHeaders(): { [key: string]: string } | 上传前使用返回值设置头 | - |
-| extraData: { [key: string]: string \| Blob } | 为 FormData 添加额外的参数 | - |
+| extraData: { [key: string]: string\| Blob } | 为 FormData 添加额外的参数 | - |
 | multiple | 上传文件是否为多个 | true |
 | fieldName | 上传字段名称 | 'file[]' |
 
 #### options.resize
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | enable | 是否支持大小拖拽 | false |
@@ -464,6 +479,7 @@ if (xhr.status === 200) {
 
 #### options.classes
 
+
 |   | 说明 | 默认值 |
 | - | - | - |
 | preview | 预览元素上的 className | '' |
@@ -483,6 +499,7 @@ if (xhr.status === 200) {
 
 #### methods
 
+
 |   | 说明 |
 | - | - |
 | getValue() | 获取 Markdown 内容 |
@@ -494,7 +511,7 @@ if (xhr.status === 200) {
 | enable() | 解除编辑器禁用 |
 | getSelection(): string | 返回选中的字符串 |
 | setValue(markdown: string, clearStack = false) | 设置编辑器内容且选中清空历史栈 |
-| clearStack() | 清空撤销和重做记录栈|
+| clearStack() | 清空撤销和重做记录栈 |
 | renderPreview(value?: string) | 设置预览区域内容 |
 | getCursorPosition():{top: number, left: number} | 获取焦点位置 |
 | deleteValue() | 删除选中内容 |
@@ -505,8 +522,8 @@ if (xhr.status === 200) {
 | enableCache() | 启用缓存 |
 | html2md(value: string) | HTML 转 md |
 | tip(text: string, time: number) | 消息提示。time 为 0 将一直显示 |
-| setPreviewMode(mode: "both" \| "editor") | 设置预览模式 |
-| setTheme(theme: "dark" \| "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) | 设置主题、内容主题及代码块风格 |
+| setPreviewMode(mode: "both"\| "editor") | 设置预览模式 |
+| setTheme(theme: "dark"\| "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) | 设置主题、内容主题及代码块风格 |
 | getCurrentMode(): string | 获取编辑器当前编辑模式 |
 | destroy() | 销毁编辑器 |
 | getCommentIds(): {id: string, top: number}[] | 获取所有评论 |
@@ -555,24 +572,25 @@ options?: IPreviewOptions {
 
 * ⚠️ `method.min.js`  和 `index.min.js` 不可同时引入
 
+
 |   | 说明 |
 | - | - |
 | previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") | 点击图片预览 |
 | mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) | 流程图/时序图/甘特图 |
 | flowchartRender(element: HTMLElement, cdn = options.cdn) | flowchart 渲染 |
 | codeRender(element: HTMLElement, lang: (keyof II18nLang) = "zh_CN") | 为 element 中的代码块添加复制按钮 |
-| chartRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 图表渲染 |
-| mindmapRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 脑图渲染 |
-| abcRender(element: (HTMLElement \| Document) = document, cdn = options.cdn) | 五线谱渲染 |
+| chartRender(element: (HTMLElement\| Document) = document, cdn = options.cdn, theme = options.theme) | 图表渲染 |
+| mindmapRender(element: (HTMLElement\| Document) = document, cdn = options.cdn, theme = options.theme) | 脑图渲染 |
+| abcRender(element: (HTMLElement\| Document) = document, cdn = options.cdn) | 五线谱渲染 |
 | md2html(mdText: string, options?: IPreviewOptions): Promise\<string> | Markdown 文本转换为 HTML,该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vanessa#toc_h3_1) |
 | preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) | 页面 Markdown 文章渲染 |
-| highlightRender(hljsOption?: IHljs, element?: HTMLElement \| Document, cdn = options.cdn) | 为 element 中的代码块进行高亮渲染 |
+| highlightRender(hljsOption?: IHljs, element?: HTMLElement\| Document, cdn = options.cdn) | 为 element 中的代码块进行高亮渲染 |
 | mediaRender(element: HTMLElement) | 为[特定链接](https://ld246.com/article/1589813914768)分别渲染为视频、音频、嵌入的 iframe |
 | mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) | 对数学公式进行渲染 |
 | speechRender(element: HTMLElement, lang?: (keyof II18nLang)) | 对选中的文字进行阅读 |
 | graphvizRender(element: HTMLElement, cdn?: string) | 对 graphviz 进行渲染 |
 | outlineRender(contentElement: HTMLElement, targetElement: Element) | 对大纲进行渲染 |
-| lazyLoadImageRender(element: (HTMLElement \| Document) = document) | 对启用懒加载的图片进行渲染 |
+| lazyLoadImageRender(element: (HTMLElement\| Document) = document) | 对启用懒加载的图片进行渲染 |
 | setCodeTheme(codeTheme: string, cdn = options.cdn) | 设置代码主题,codeTheme 参见 options.preview.hljs.style |
 | setContentTheme(contentTheme: string, path: string) | 设置内容主题,contentTheme 参见 options.preview.theme.list |
 

+ 2 - 2
demo/index.js

@@ -50,8 +50,8 @@ if (window.innerWidth < 768) {
 }
 
 window.vditor = new Vditor('vditor', {
-  _lutePath: `http://192.168.0.107:9090/lute.min.js?${new Date().getTime()}`,
-  // _lutePath: 'src/js/lute/lute.min.js',
+  //_lutePath: `http://192.168.0.107:9090/lute.min.js?${new Date().getTime()}`,
+  _lutePath: 'src/js/lute/lute.min.js',
   toolbar,
   mode: 'wysiwyg',
   height: window.innerHeight + 100,

+ 15 - 0
demo/markdown/zh_CN.md

@@ -223,6 +223,21 @@ $$
 - 快捷键
 ```
 
+### plantuml
+
+```plantuml
+@startuml component
+actor client
+node app
+database db
+
+db -> app
+app -> client
+@enduml
+```
+
+更多图形参考[https://plantuml.com/zh/](https://plantuml.com/zh/)
+
 ### 流程图
 
 ```mermaid

+ 2 - 0
src/assets/scss/_reset.scss

@@ -214,6 +214,7 @@
     .language-math,
     .language-echarts,
     .language-mindmap,
+    .language-plantuml,
     .language-mermaid,
     .language-abc,
     .language-flowchart,
@@ -227,6 +228,7 @@
     }
 
     .language-echarts,
+    .language-plantuml,
     .language-mindmap {
       overflow: hidden;
       height: 420px;

+ 3 - 0
src/method.ts

@@ -9,6 +9,7 @@ import {mathRender} from "./ts/markdown/mathRender";
 import {mediaRender} from "./ts/markdown/mediaRender";
 import {mermaidRender} from "./ts/markdown/mermaidRender";
 import {mindmapRender} from "./ts/markdown/mindmapRender";
+import {plantumlRender} from "./ts/markdown/plantumlRender";
 import {outlineRender} from "./ts/markdown/outlineRender";
 import {md2html, previewRender} from "./ts/markdown/previewRender";
 import {speechRender} from "./ts/markdown/speechRender";
@@ -37,6 +38,8 @@ class Vditor {
     public static abcRender = abcRender;
     /** 脑图渲染 */
     public static mindmapRender = mindmapRender;
+    /** plantuml渲染 */
+    public static plantumlRender = plantumlRender;
     /** 大纲渲染 */
     public static outlineRender = outlineRender;
     /** 为[特定链接](https://github.com/Vanessa219/vditor/issues/7)分别渲染为视频、音频、嵌入的 iframe */

+ 1 - 1
src/ts/constants.ts

@@ -16,7 +16,7 @@ export abstract class Constants {
         "monokailight", "murphy", "native", "paraiso-dark", "paraiso-light", "pastie", "perldoc", "pygments",
         "rainbow_dash", "rrt", "solarized-dark", "solarized-dark256", "solarized-light", "swapoff", "tango", "trac",
         "vim", "vs", "xcode", "ant-design"];
-    public static readonly CODE_LANGUAGES: string[] = ["mermaid", "echarts", "mindmap", "abc", "graphviz", "flowchart", "apache",
+    public static readonly CODE_LANGUAGES: string[] = ["mermaid", "echarts", "mindmap", "plantuml", "abc", "graphviz", "flowchart", "apache",
         "bash", "cs", "cpp", "css", "coffeescript", "diff", "xml", "http", "ini", "json", "java", "javascript", "js",
         "makefile", "markdown", "nginx", "objectivec", "php", "perl", "properties", "python", "ruby", "sql", "shell",
         "dart", "erb", "go", "gradle", "julia", "kotlin", "less", "lua", "matlab", "rust", "scss", "typescript", "ts",

+ 1 - 0
src/ts/markdown/codeRender.ts

@@ -9,6 +9,7 @@ export const codeRender = (element: HTMLElement, lang: keyof II18n = "zh_CN") =>
         }
         if (e.classList.contains("language-mermaid") || e.classList.contains("language-flowchart") ||
             e.classList.contains("language-echarts") || e.classList.contains("language-mindmap") ||
+            e.classList.contains("language-plantuml") ||
             e.classList.contains("language-abc") || e.classList.contains("language-graphviz") ||
             e.classList.contains("language-math") ) {
             return;

+ 1 - 0
src/ts/markdown/highlightRender.ts

@@ -38,6 +38,7 @@ export const highlightRender = (hljsOption?: IHljs, element: HTMLElement | Docum
 
             if (block.classList.contains("language-mermaid") || block.classList.contains("language-flowchat") ||
                 block.classList.contains("language-echarts") || block.classList.contains("language-mindmap") ||
+                block.classList.contains("language-plantuml") ||
                 block.classList.contains("language-abc") || block.classList.contains("language-graphviz") ||
                 block.classList.contains("language-math")) {
                 return;

+ 42 - 0
src/ts/markdown/plantumlRender.ts

@@ -0,0 +1,42 @@
+import {Constants} from "../constants";
+import {addScript} from "../util/addScript";
+
+declare const plantumlEncoder: {
+    encode(options: string): string,
+};
+
+export const plantumlRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
+    const plantumlElements = element.querySelectorAll(".language-plantuml");
+    if (plantumlElements.length === 0) {
+        return;
+    }
+    addScript(`https://cdn.jsdelivr.net/gh/jmnote/[email protected]/dist/plantuml-encoder.min.js`, "vditorPlantumlEncoderScript").then(() => {
+        plantumlElements.forEach((e: HTMLDivElement) => {
+                if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
+                    e.parentElement.classList.contains("vditor-ir__marker--pre")) {
+                    return;
+                }
+                const text = e.innerText.trim();
+                if (!text) {
+                    return;
+                }
+                try {
+                    if (e.getAttribute("data-processed") === "true") {
+                        return;
+                    }
+                    
+                    const encoded = plantumlEncoder.encode(text)
+                    const imageElement = document.createElement("IMG");
+                    imageElement.setAttribute("loading", "lazy")
+                    imageElement.setAttribute("src", 'http://www.plantuml.com/plantuml/svg/~1' + encoded)
+                    e.parentNode.insertBefore(imageElement, e);
+                    e.style.display = 'none';
+                    
+                    e.setAttribute("data-processed", "true");
+                } catch (error) {
+                    e.className = "vditor-reset--error";
+                    e.innerHTML = `plantuml render error: <br>${error}`;
+                }
+            });
+    });
+};

+ 2 - 0
src/ts/markdown/previewRender.ts

@@ -15,6 +15,7 @@ import {mathRender} from "./mathRender";
 import {mediaRender} from "./mediaRender";
 import {mermaidRender} from "./mermaidRender";
 import {mindmapRender} from "./mindmapRender";
+import {plantumlRender} from "./plantumlRender";
 import {setLute} from "./setLute";
 import {speechRender} from "./speechRender";
 
@@ -94,6 +95,7 @@ export const previewRender = async (previewElement: HTMLDivElement, markdown: st
     graphvizRender(previewElement, mergedOptions.cdn);
     chartRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
     mindmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
+    plantumlRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
     abcRender(previewElement, mergedOptions.cdn);
     mediaRender(previewElement);
     if (mergedOptions.speech.enable) {

+ 2 - 0
src/ts/preview/index.ts

@@ -10,6 +10,7 @@ import {mathRender} from "../markdown/mathRender";
 import {mediaRender} from "../markdown/mediaRender";
 import {mermaidRender} from "../markdown/mermaidRender";
 import {mindmapRender} from "../markdown/mindmapRender";
+import {plantumlRender} from "../markdown/plantumlRender";
 import {getEventName} from "../util/compatibility";
 import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
 import {hasClosestByTag} from "../util/hasClosestByHeadings";
@@ -214,6 +215,7 @@ export class Preview {
         graphvizRender(vditor.preview.element.lastElementChild as HTMLElement, vditor.options.cdn);
         chartRender(vditor.preview.element.lastElementChild as HTMLElement, vditor.options.cdn, vditor.options.theme);
         mindmapRender(vditor.preview.element.lastElementChild as HTMLElement, vditor.options.cdn, vditor.options.theme);
+        plantumlRender(vditor.preview.element.lastElementChild as HTMLElement, vditor.options.cdn, vditor.options.theme);
         abcRender(vditor.preview.element.lastElementChild as HTMLElement, vditor.options.cdn);
         mediaRender(vditor.preview.element.lastElementChild as HTMLElement);
         // toc render

+ 1 - 0
src/ts/undo/index.ts

@@ -227,6 +227,7 @@ class Undo {
         cloneElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='1']`)
             .forEach((item: HTMLElement) => {
                 if (item.firstElementChild.classList.contains("language-echarts") ||
+                item.firstElementChild.classList.contains("language-plantuml") ||
                     item.firstElementChild.classList.contains("language-mindmap")) {
                     item.firstElementChild.removeAttribute("_echarts_instance_");
                     item.firstElementChild.removeAttribute("data-processed");

+ 3 - 0
src/ts/util/processCode.ts

@@ -7,6 +7,7 @@ import {highlightRender} from "../markdown/highlightRender";
 import {mathRender} from "../markdown/mathRender";
 import {mermaidRender} from "../markdown/mermaidRender";
 import {mindmapRender} from "../markdown/mindmapRender";
+import {plantumlRender} from "../markdown/plantumlRender";
 
 export const processPasteCode = (html: string, text: string, type = "sv") => {
     const tempElement = document.createElement("div");
@@ -74,6 +75,8 @@ export const processCodeRender = (previewPanel: HTMLElement, vditor: IVditor) =>
         chartRender(previewPanel, vditor.options.cdn, vditor.options.theme);
     } else if (language === "mindmap") {
         mindmapRender(previewPanel, vditor.options.cdn, vditor.options.theme);
+    } else if (language === "plantuml") {
+        plantumlRender(previewPanel, vditor.options.cdn, vditor.options.theme);
     } else if (language === "graphviz") {
         graphvizRender(previewPanel, vditor.options.cdn);
     } else if (language === "math") {