vue3中使用cherry-markdown

附cherry-markdown官网及api使用示例

官网:https://github.com/Tencent/cherry-markdown/blob/main/README.CN.md

api:Cherry Markdown API

考虑到复用性,我在插件的基础上做了二次封装,步骤如下:

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.3mermaid@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>

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农键盘上的梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值