自定义代码块样式,基于marked与自定义指令动态增加语言显示与copy功能

需求:

在开发一个基于大模型的QA系统时,由于在回答编程相关问题时,模型会输出使用markdown形式表示的代码代码,如下图所示,不利于查看,故借助Marked.js将markdown形式的字符串转换为html形式,由于是需要实时渲染故选择marked.js,可以达到极致的轻量化,相对于mavon-editor(包太大),vue-marked(功能少)来说更加适合本项目。

marked.js

一个功能齐全的markdown解析器和编译器,用JavaScript编写。 专为速度而设计。

  • 快速构建
  • 用于解析markdown的低级编译器,无需长时间缓存或阻塞
  • 非常轻量,同时实现支持的falses和规格的所有降价功能
  • 支持浏览器,服务器或命令行界面(CLI)

在这里插入图片描述
但是轻量化的同时也带来一些问题,就是没有其他的一些附加功能,如代码行数限制,代码复制等等没有提供,官网中也有一些扩展,可以满足大部分的需求,如代码高亮(marked-highlight),在使用了现在有一些扩展后,效果如下,个人还需要语言显示、代码复制,故就基于marked扩展了功能,开发所使用的环境是Vue3+Typescript,其他环境应该可以基于我们代码进行修改实现同样的功能。
在这里插入图片描述

实现

思路:

这里将具体的实现分为两步:静态绘制动态注册
首先是静态绘制,利用在markdown转html的时候的返回结果,将其中的pre标签进行注入一个自定义标签用于占位,以方便后续的操作。
第二步是动态注册,在已有元素的基础上进行动态值与函数的设置与注册。

具体实现

在原有marked的基础上进行“增强”,具体如下:

enhanceCodeBlock(marked(chatItem.content))

enhanceCodeBlock函数:

//增强代码块
const enhanceCodeBlock = (content: string) => {
  // console.log(content)
  //在pre块中增加一个元素用于显示
  let enhance = content.replace(/<pre><code/g, '<pre><div class="enhance"><div class="lang">CODE</div><div class="copyCode">Copy<i class="el-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M128 320v576h576V320H128zm-32-64h640a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32zM960 96v704a32 32 0 0 1-32 32h-96v-64h64V128H384v64h-64V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32zM256 672h320v64H256v-64zm0-192h320v64H256v-64z"></path></svg></i></div></div><code')
  // console.log(enhance)
  return enhance
}

这里利用拦截器的思想对其进行增强,在得到结果后使用正则匹配“<pre><code”,在这其中添加自己需要的元素节点,我这里添加了一个enhance类的div标签,其中还包括有一个语言显示块和一个预留的复制按钮,这里根据自己实际的需要进行增加,注意我这里的/g不省略的原因是我的一段文本中可能有多个代码块,在具体的实际场景中可能做法不同。
再加下一定的css样式最终效果如下:

pre .enhance {
  display: flex;
  color: #fff ;
  padding: 0px 10px ;
  border-radius: 5px 5px 0 0 ;
  font-size: 14px ;
  // font-weight: 600 ;
  background: #343541de;
  justify-content:space-between;

  .copyCode{
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.5s ease-in-out;

    &:hover{
      color: #bae9a4d7;
    }

    i{
      font-size: 16px;
      margin-left: 5px;
    }
  }
}

在这里插入图片描述
到这里基本上第一步就完成了,接下来是第二步,动态注册,思路也比较清晰,就是在元素生成完成后,查找前面自定义的标签元素,进行值的设置与点击事件的监听。这里的重点就是找到合适的时机进行渲染,如果是结果固定,在获取结果进行渲染即可(但我试了在多个地方渲染均不生效,后面再来解决这个问题),但由于我这里在与后端交互时使用的是流式输出,所以需要实时渲染,故而采用Vue的自定义指令,由于其特性当其绑定的元素被插入到 DOM 中时,会立即执行一些行为非常适合这个场景,自定义指令允许我们在渲染的 DOM 元素上应用自定义的行为。这里可以选择全局注册app.directive(name, options) ,也可以局部注册,我这里只需要在这一个页面上使用,故使用的具体注册。具体请看官网
下面直接上代码:

//自定义指令增强代码块功能
const vEnhanceCode = (el: HTMLElement) => {
  let codeBlocks = el.querySelectorAll('pre');
  // console.log(codeBlocks)
  codeBlocks.forEach((codeBlock, i) => {
    // console.log(codeBlock)
    // 获取代码块中的语言标识
    const code = codeBlock.querySelector("code") as any
    let lang = code["result"].language as string
    //首字母大写
    if (lang) {
      lang = lang.charAt(0).toUpperCase() + lang.slice(1)
    }
    // 获取增强入口
    let enhance = codeBlock.querySelector(".enhance")
    if (enhance) {
      // 判断是否已经注册
      const origin = enhance.querySelector(".lang")!.innerHTML
      if (origin != "CODE") {
        return
      }
      // 替换预定义的Code标识
      enhance.querySelector(".lang")!.innerHTML = lang
      // 注册复制按钮事件
      let copyCode = enhance.querySelector(".copyCode")
      // 判断是否有注册事件
      if (copyCode) {
        copyCode.removeEventListener("click", () => { })
      }
      copyCode!.addEventListener("click", () => {
        copy(code.innerText)
      })
    }
  })
}

这里代码都比较清晰,需要注意的点就是在注册监听事件的时候,最好判断一下当前是否注册,如果是结果固定的没什么影响,但是像我这种实时渲染的不加判断会导致事件一直注册,最后导致在点击复制代码时,n个函数被调用,导致浏览器直接无响应(这是一个悲伤的故事)。
最后在元素上加上自定义的指定就大功告成啦!
最终的结构如下:

<div class="chatContent" v-highlight v-enhance-code v-html="enhanceCodeBlock(marked(chatItem.content))"></div>

在这里插入图片描述
下面是演示:

QQ录屏20230830200346

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值