附cherry-markdown官网及api使用示例
官网:https://github.com/Tencent/cherry-markdown/blob/main/README.CN.md
考虑到复用性,我在插件的基础上做了二次封装,步骤如下:
1.下载 (一定要指定版本0.8.22,否则会报错: [vitel Internal server error: Failed to parse soERRORrce for import analysis because the content contains invalid Js syntax. If you are using JSX, make sure to name the file with the...)
npm i cherry-markdown@0.8.22
如果需要开启 mermaid
画图、表格自动转图表功能,需要同时添加mermaid
与echarts
包。
目前Cherry推荐的插件版本为echarts@5.3.3
、mermaid@9.4.3
# 安装mermaid依赖开启mermaid画图功能
npm i mermaid@9.4.3
# 安装echarts依赖开启表格自动转图表功能
npm i echarts@5.3.3
基础应用代码示例
<template>
<div @click.prevent.stop>
<div id="markdown-container"></div>
</div>
</template>
<script>
import 'cherry-markdown/dist/cherry-markdown.css';
import Cherry from 'cherry-markdown';
const cherryInstance = new Cherry({
id: 'markdown-container',
value: '# welcome to cherry editor!',
});
</script>
关于 mermaid(插件注册必须在Cherry实例化之前完成)
核心构建包不包含 mermaid 依赖,需要手动引入相关插件。
import 'cherry-markdown/dist/cherry-markdown.css';
import Cherry from 'cherry-markdown/dist/cherry-markdown.core';
import CherryMermaidPlugin from 'cherry-markdown/dist/addons/cherry-code-block-mermaid-plugin';
import mermaid from 'mermaid';
// 插件注册必须在Cherry实例化之前完成
Cherry.usePlugin(CherryMermaidPlugin, {
mermaid, // 传入mermaid引用
// mermaidAPI: mermaid.mermaidAPI, // 也可以传入mermaid API
// 同时可以在这里配置mermaid的行为,可参考mermaid官方文档
// theme: 'neutral',
// sequence: { useMaxWidth: false, showSequenceNumbers: true }
});
const cherryInstance = new Cherry({
id: 'markdown-container',
value: '# welcome to cherry editor!',
});
2.附上组件代码
<template>
<div @click.prevent.stop style="position: relative; width: 100%; height: 100%; z-index: 9999999999;text-align: left;">
<!-- ------- markdown组件容器 --------- -->
<div ref="markdownContainerRef" :id="mdId" class="cherry_container scroll"
:style="{ 'left': toc_Visiable ? '262px' : '0px' }" @scroll="onScroll">
</div>
<!-- ------- 显示目录 --------- -->
<!-- <div class="toc_container" v-show="toc_Visiable">
<div class="toc_header">目录</div>
<div class="toc_list">
<div v-for="(link, index) in toc_List" :key="link.id" class="toc_list_container"
:style="{ 'padding-left': link.level * 20 + 'px' }" @click="jump(index)">{{ link.text }}
</div>
</div>
</div>
<div class="top-contrl">
<div v-if="!toc_Visiable" @click="showToc(true)">显示目录</div>
<div v-else @click="showToc(false)">隐藏目录</div>
</div> -->
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeMount, watch, onBeforeUnmount, toRefs, reactive, nextTick } from 'vue';
import Axios from 'axios';
import 'cherry-markdown/dist/cherry-markdown.css';
import Cherry from 'cherry-markdown'
// import Cherry from 'cherry-markdown/dist/cherry-markdown.core';
import * as echarts from "echarts";
import pinyin from '../assets/pinyinHelper.js'; // 引入pinyin函数
import CherryMermaidPlugin from 'cherry-markdown/dist/addons/cherry-code-block-mermaid-plugin';
import mermaid from 'mermaid';
// 组件传值
const props = defineProps({
// markdown文本
markDownConent: {
type: String,
default: '',
},
// markdown组件容器ID
mdId: {
type: String,
default: 'markdownContainer',
},
// 编辑器的显示模式 view|edit
displayModal: {
type: String,
default: 'view',
},
// 用来控制目录显示或隐藏
tocVisiable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['input', 'md-change']);
const { mdId, displayModal, tocVisiable, markDownConent } = toRefs(props);
// 组件内部变量
const content = ref('');
const markdownContainerRef = ref(null); //dom 元素
const markDown_Conent = ref('');
const toc_Visiable = ref(tocVisiable.value); //目录 显隐
const cherrInstance = ref(null); //Cherry MarkDown实例
const initCherryMD = async (value) => {
markDown_Conent.value = value || markDownConent.value;
cherrInstance.value = new Cherry({
id: mdId.value,
value: markDown_Conent.value,
externals: {
echarts: echarts,
},
fileUpload,
callback: {
changeString2Pinyin: pinyin,
// afterChange,
afterInit,
beforeImageMounted,
onClickPreview: function (e) {
const { target } = e;
if (target.tagName === 'IMG') {
console.log('click img', target);
const tmp = new Viewer(target, {
button: false,
navbar: false,
title: [1, (image, imageData) => `${image.alt.replace(/#.+$/, '')} (${imageData.naturalWidth} × ${imageData.naturalHeight})`],
hidden() {
tmp.destroy()
},
});
tmp.show();
}
}
},
toolbars: {
showToolbar:displayModal.value=="previewOnly"?false:true,
toolbar: ['bold', 'italic', 'strikethrough', '|', 'color', 'header', 'ruby', '|', 'list', 'panel', 'detail'],
// 定义侧边栏,默认为空
sidebar: [],
// 定义顶部右侧工具栏,默认为空
toolbarRight: [],
// 定义选中文字时弹出的“悬浮工具栏”,默认为 ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color']
bubble: false,
// 定义光标出现在行首位置时出现的“提示工具栏”,默认为 ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code']
float: false,
// 定义顶部工具栏
toolbar: [
'bold',
'italic',
{
strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby', 'boldAndItalicName'],
},
'size',
'|',
'color',
'header',
'|',
'ol',
'ul',
'checklist',
'panel',
'justify',
'detail',
'|',
'formula',
{
insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'pdf', 'word', 'ruby'],
},
'graph',
'togglePreview',
'settings',
'codeTheme',
'export',
// {
// customMenuBName: ['ruby', 'audio', 'video', 'boldAndItalicName'],
// },
// 'customMenuCName',
'theme',
],
// 定义侧边栏,默认为空
sidebar: ['mobilePreview', 'copy', 'theme'],
// 定义顶部右侧工具栏,默认为空
toolbarRight: ['fullScreen', '|'],
// 定义选中文字时弹出的“悬浮工具栏”,默认为 ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color']
bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false
showToolbar: true,
// 定义光标出现在行首位置时出现的“提示工具栏”,默认为 ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code']
// float: false,
toc: {
updateLocationHash: false, // 要不要更新URL的hash
defaultModel: 'full', // pure: 精简模式/缩略模式,只有一排小点; full: 完整模式,会展示所有标题
},
customMenu: {
boldAndItalicName: boldAndItalic,
customMenuBName: customMenuB,
customMenuCName: customMenuC,
},
},
editor: {
id: `${mdId.value}editor`,
name: 'cherry-text',
autoSave2Textarea: true,
defaultModel: displayModal.value,
},
previewer: {
// 自定义markdown预览区域class
// className: 'markdown'
},
// 预览页面不需要绑定事件
isPreviewOnly: false,
// 预览区域跟随编辑器光标自动滚动
autoScrollByCursor: true,
// 外层容器不存在时,是否强制输出到body上
forceAppend: true,
// The locale Cherry is going to use. Locales live in /src/locales/
locale: 'zh_CN',
keydown: [],
// cherry初始化后是否检查 location.hash 尝试滚动到对应位置
autoScrollByHashAfterInit: false,
});
};
/**
* 自定义一个自定义菜单
* 点第一次时,把选中的文字变成同时加粗和斜体
* 保持光标选区不变,点第二次时,把加粗斜体的文字变成普通文本
*/
const boldAndItalic = Cherry.createMenuHook('加粗斜体', {
iconName: 'font',
onClick: function (selection) {
// 获取用户选中的文字,调用getSelection方法后,如果用户没有选中任何文字,会尝试获取光标所在位置的单词或句子
let $selection = this.getSelection(selection) || '同时加粗斜体';
// 如果是单选,并且选中内容的开始结束内没有加粗语法,则扩大选中范围
if (!this.isSelections && !/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
this.getMoreSelection('***', '***', () => {
const newSelection = this.editor.editor.getSelection();
const isBoldItalic = /^\s*(\*\*\*)[\s\S]+(\1)/.test(newSelection);
if (isBoldItalic) {
$selection = newSelection;
}
return isBoldItalic;
});
}
// 如果选中的文本中已经有加粗语法了,则去掉加粗语法
if (/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
return $selection.replace(/(^)(\s*)(\*\*\*)([^\n]+)(\3)(\s*)($)/gm, '$1$4$7');
}
/**
* 注册缩小选区的规则
* 注册后,插入“***TEXT***”,选中状态会变成“***【TEXT】***”
* 如果不注册,插入后效果为:“【***TEXT***】”
*/
this.registerAfterClickCb(() => {
this.setLessSelection('***', '***');
});
return $selection.replace(/(^)([^\n]+)($)/gm, '$1***$2***$3');
}
});
/**
* 定义一个空壳,用于自行规划cherry已有工具栏的层级结构
*/
const customMenuB = Cherry.createMenuHook('实验室', {
iconName: '',
});
/**
* 定义一个自带二级菜单的工具栏
*/
const customMenuC = Cherry.createMenuHook('帮助中心', {
iconName: 'question',
onClick: (selection, type) => {
switch (type) {
case 'shortKey':
console.log("🚀 ~ selection:", selection)
return `${selection}快捷键看这里:https://codemirror.net/5/demo/sublime.html`;
case 'github':
return `${selection}我们在这里:https://github.com/Tencent/cherry-markdown`;
case 'release':
return `${selection}我们在这里:https://github.com/Tencent/cherry-markdown/releases`;
default:
return selection;
}
},
subMenuConfig: [
{ noIcon: true, name: '快捷键', onclick: (event) => { cherrInstance.value.toolbar.menus.hooks.customMenuCName.fire(null, 'shortKey') } },
{ noIcon: true, name: '联系我们', onclick: (event) => { cherrInstance.value.toolbar.menus.hooks.customMenuCName.fire(null, 'github') } },
{ noIcon: true, name: '更新日志', onclick: (event) => { cherrInstance.value.toolbar.menus.hooks.customMenuCName.fire(null, 'release') } },
]
});
const fileUpload = (file) => {
var formData = new FormData();
formData.append("file", file);
Axios.post("/api/common/file/upload", formData, {
headers: {
"Content-Type": "multipart/form-data"
},
}).then((response) => {
if (response.code == 0) {
let imgMdStr = `![${response.data.file_name}](${response.data.ref_url})`;
console.log(imgMdStr);
cherrInstance.value.insert(imgMdStr);
}
});
};
const afterChange = (e) => {
content.value = e;
// getTitles();
// const mdHtml = getCherryMarkdownHtml();
// const mdTxt = e;
// const mdContent = getCherryMarkdownContent();
// emit('input', mdContent);
// emit('md-change', mdHtml, mdTxt, mdContent);
};
// 初始化事件回调
const afterInit = (e) => {
//console.log(e)
};
// 图片加载回调
const beforeImageMounted = (e, src) => {
//console.log('bfImageMt', e, src)
return {
[e]: src
};
};
/**
* 设置markdown编辑器内容,全部覆盖
* @param {Object} content 要设置的内容
* @param {Object} keepCursor 自动设置焦点到内容
*/
const setMarkdown = (content, keepCursor) => {
if (!cherrInstance.value) {
initCherryMD(content);
return;
}
// setMarkdown(content:string, keepCursor = false)
cherrInstance.value.setMarkdown(content, keepCursor);
};
const setValue = (content) => {
if (!cherrInstance.value) {
initCherryMD(content);
return;
}
// setValue(content:string, keepCursor = false)
cherrInstance.value.setValue(content);
};
const getCherryMarkdownContent = () => {
const result = cherrInstance.value.getMarkdown();// 获取markdown内容
return result;
};
const getCherryMarkdownHtml = () => {
const result = cherrInstance.value.getHtml();
return result;
};
/**
* @description: MarkDown转出'pdf'|'img'
* @param {string} type:{'pdf'|'img'}
* @return {*}
*/
const exportMD = (type = 'pdf') => {
cherrInstance.value.export(type);
};
/**
* model{'edit&preview'|'editOnly'|'previewOnly'}
*/
const switchModel = (model) => {
if (isInit()) {
cherrInstance.value.switchModel(model);
}
};
const isInit = () => {
if (cherrInstance.value) {
return true;
}
console.warning('编辑器未初始化,请检查');
return false;
};
const insert = (content, isSelect = false, anchor = [], focus = true) => {
console.log(content);
cherrInstance.value.insert(content, isSelect, anchor, focus);
};
const toc_List = ref([]); //存放目录
// 获取目录
const getTitles = () => {
toc_List.value = cherrInstance.value.getToc();
};
// // 自定义目录相关
// const curTab = ref(0);
// // 调用获取目录
// const showToc = (val) => {
// if (val) {
// getTitles();
// }
// toc_Visiable.value = val;
// };
// // 目录的定位滚动
// let scrollObj = reactive({
// distance: 0,
// totalY: 0,
// step: 0,
// })
// const jump = (index) => {
// curTab.value = index;
// let anchorName = toc_List.value[index].id;
// let anchorElem = document.getElementById(anchorName);
// let firstElem = document.getElementById(toc_List.value[0].id);
// scrollObj.totalY = anchorElem.offsetTop - firstElem.offsetTop;
// scrollObj.distance = document.querySelector('.cherry-previewer').scrollTop;
// scrollObj.step = scrollObj.totalY / 50;
// if (scrollObj.totalY > scrollObj.distance) {
// smoothDown(document.querySelector('.cherry-previewer'));
// } else {
// let newTotal = scrollObj.distance - scrollObj.totalY;
// scrollObj.step = newTotal / 50;
// smoothUp(document.querySelector('.cherry-previewer'));
// }
// };
// const smoothDown = (element) => {
// if (scrollObj.distance < scrollObj.totalY) {
// scrollObj.distance += scrollObj.step;
// element.scrollTop = scrollObj.distance;
// setTimeout(smoothDown.bind(this, element), 2);
// } else {
// element.scrollTop = scrollObj.totalY;
// }
// };
// const smoothUp = (element) => {
// if (scrollObj.distance > scrollObj.totalY) {
// scrollObj.distance -= scrollObj.step;
// element.scrollTop = scrollObj.distance;
// setTimeout(smoothUp.bind(this, element), 2);
// } else {
// element.scrollTop = scrollObj.totalY;
// }
// };
// const onScroll = (e) => {
// getTitles();
// if (!toc_List.value || toc_List.value.length < 1) return;
// let firstElem = document.getElementById(toc_List.value[0].id);
// for (let i = toc_List.value.length - 1; i >= 0; i--) {
// let anchorElem = document.getElementById(toc_List.value[i].id);
// let judge = e.target.scrollTop >= anchorElem.offsetTop - firstElem.offsetTop;
// if (judge) {
// curTab.value = i;
// break;
// }
// }
// };
onBeforeUnmount(() => {
destroyInstance()
});
onBeforeMount(async () => {
// 插件注册必须在Cherry实例化之前完成
await Cherry.usePlugin(CherryMermaidPlugin, {
mermaid, // 传入mermaid引用
// mermaidAPI: mermaid.mermaidAPI, // 也可以传入mermaid API
// 同时可以在这里配置mermaid的行为,可参考mermaid官方文档
// theme: 'neutral',
// sequence: { useMaxWidth: false, showSequenceNumbers: true }
});
});
onMounted(() => {
initCherryMD()
});
watch(
() => props.markDownConent,
async (newValue, oldValue) => {
markDown_Conent.value = newValue
if (cherrInstance.value) {
await destroyInstance();
}
await initCherryMD(newValue)
// if (displayModal.value === 'edit') {
// nextTick(() => {
// cherrInstance.value.setMarkdown(newValue, 1);
// })
// }
// 自定义目录相关
// showToc(toc_Visiable.value);
},
// { immediate: true }
)
const destroyInstance = (val) => {
// cherrInstance.value.destroy()
while (markdownContainerRef.value.firstChild) {
markdownContainerRef.value.removeChild(markdownContainerRef.value.firstChild);
}
cherrInstance.value = null;
};
// 使用defineExpose暴露给父组件
defineExpose({
// 可以暴露更多变量或方法
initCherryMD,
setMarkdown,
setValue,
})
</script>
<style scoped lang="scss">
//特殊样式,而不通用请在这里写样式
.cherry_container {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
}
.toc_container {
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
width: 260px;
background-color: #fff;
.toc_header {
height: 48px;
line-height: 48px;
background-color: #20304b;
text-align: center;
font-size: 16px;
color: #fff;
}
.toc_list {
position: absolute;
top: 60px;
right: 0px;
bottom: 0px;
left: 0px;
overflow: auto;
&::-webkit-scrollbar {
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-thumb {
background-color: #607d8b;
border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background: #40a0ff;
}
.toc_list_container {
padding-top: 6px;
cursor: pointer;
}
.toc_list_container:hover {
color: red;
}
}
}
.top-contrl {
position: absolute;
top: 10px;
right: 8px;
height: 30px;
padding-right: 5px;
color: red;
font-weight: bold;
font-size: 20px;
z-index: 2;
}
</style>
3、组件使用代码
<template>
<div class="markdown-view">
<MarkdownCom ref="MDRef" :markDownConent="markDownConent" :mdId="mdId" :tocVisiable="tocVisiable"
:displayModal="displayModal" @input='mdInput' @md-change='mdChange' />
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import Axios from 'axios';
import MarkdownCom from '../components/MarkdownCom';
const MDRef = ref(null);
let markDownConent = ref("");
const mdId = ref('mdId');
const tocVisiable = ref(false);
// edit&preview: 双栏编辑预览模式
// editOnly: 纯编辑模式(没有预览,可通过toolbar切换成双栏或预览模式)
// previewOnly: 预览模式(没有编辑框,toolbar只显示“返回编辑”按钮,可通过toolbar切换成编辑模式)
const displayModal = ref('edit&preview');
onMounted(async () => {
await getData()
})
const mdInput = (mdContent) => {
// console.log("🚀 ~ mdInput ~ mdContent:", mdContent)
}
const mdChange = (mdHtml, mdTxt, mdContent) => {
// console.log("🚀 ~ mdChange ~ mdHtml, mdTxt, mdContent:", mdHtml, mdTxt, mdContent)
}
const getData = async () => {
const abc = await Axios.get("/public/example.md")
// const abc = await fetch('/public/README.md')
// const def = await abc.text()
markDownConent.value = abc.data;
// nextTick(() => {
// MDRef.value.initCherryMD(def)
// })
// setTimeout(() => {
// markDownConent.value = def;
// }, 1000)
}
</script>
<style lang="scss" scoped>
.markdown-view {
width: 100%;
height: 100%;
}
</style>