ByteMD编辑器源码阅读

背景

最近在研究编辑器偶尔看到了ByteMD挺有意思的,于是产生了兴趣,便研究了一下源码,想解决一下下面的疑惑。

ByteMD源码地址: github.com/bytedance/b…

Demo地址: bytemd.js.org/playground/

通过本文可以主要能够揭开如下的疑惑。

  • 如何实现左右两边同步滚动的?

  • 如何设计的如此灵活,自定义插件的实现方式?

  • Svelet框架入门与探索。【已经作为独立文章】

前置知识

  • 编辑器是基于 CodeMirror 来实现的。 CodeMirror教程

  • unified是一个使用语法树处理文本的接口。它支持remark (Markdown)、retext(自然语言)和rehype (HTML),并允许在格式之间进行处理。

如果不太了解的同学可以先去了解下。

自定义插件的实现方式?

unified是一个通过使用语法树来进行解析、检查、转换和序列化文本内容的接口,可以处理Markdown、HTML和自然语言。它作为一个独立的执行接口,负责执行器的角色,调用其生态上相关的插件完成具体任务。

周边生态中的插件已经有300+,可以说是异常的健壮。

ByteMD编辑器的,左侧编辑区域是一个Markdown编辑器,右边是预览区,用于显示生成的HTML文件。

中间的处理过程如下:

  1. 将MarkDown文本解析为 Markdown AST

  2. Markdown AST 可以被一些必须的 remark plugins 进行操作

  3. Markdown AST 被转化为 HTML AST

  4. HTML AST 会进行安全化处理

  5. HTML AST 会被一些 rehype plugins进行处理加工

  6. HTML AST 被生成 HTML 字符串

  7. HMTL文本被渲染后还能被一些插件进行操作

用官方的图进行表达如下:

image

OK,我们可以自定义一些插件(函数),这些插件会被注入在红色标记的节点中,在文本转化过程中会执行相应的插件逻辑,进而影响最终HTML的生成。

如何实现精确的同步滚动?

过程相当巧妙,思路是:

  • 建立起 左侧编辑器 与 右侧预览页面 中的一级节点 在各自可滚动距离中所占的 位置比例。一一对应关系。

  • 一侧滚动,通过滚动测计算出最顶行在左侧节点所占的比例, 计算出在另一侧对应节点间的比例。

  • 最终计算出另一侧滚动的距离。

上面看上去会有点迷,下面是具体的核心逻辑代码:

  1. 计算左右两边顶层关键节点的在编辑器中位置(是个小于1数字)

无论是左边引起的滚动,还是右边引起的滚动,都会调用这个函数。

const updateBlockPositions = throttle(() => {
      editPs = []
      previewPs = []


      // Get an {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
      const scrollInfo = editor.getScrollInfo()
      /*
      {
          "left": 0,  // 编辑器左边滚动
          "top": 12,  // 编辑器向上滚动
          "height": 2090, // 编辑器内容总高度
          "width": 584, // 宽度
          "clientHeight": 347, //用户可见区的高度
          "clientWidth": 584 //用户视图区宽度
      }
      */
      const body = previewEl.childNodes[0] // 获得预览的dom
      if (!(body instanceof HTMLElement)) return

      // hast是html语法抽象, 从preview的插件中取出来的
      const leftNodes = hast.children.filter(
        (v): v is Element => v.type === 'element'
      )
      // 实际的预览下的html
      const rightNodes = [...body.childNodes].filter(
        (v): v is HTMLElement => v instanceof HTMLElement
      )
      for (let i = 0; i < leftNodes.length; i++) { // 左边和右边一定是一样的
        const leftNode = leftNodes[i]
        const rightNode = rightNodes[i]

        // if there is no position info, move to the next node
        if (!leftNode.position) {
          continue
        }
        // 已滚动距离与总的可滚动距离的比值相等

        const left =
          editor.heightAtLine(leftNode.position.start.line - 1, 'local') /
          (scrollInfo.height - scrollInfo.clientHeight)
        const right =
          (rightNode.offsetTop - body.offsetTop) /
          (previewEl.scrollHeight - previewEl.clientHeight)
        if (left >= 1 || right >= 1) {
          break
        }

        editPs.push(left)
        previewPs.push(right)
      }
      // 同时都滚动到底部
      editPs.push(1)
      previewPs.push(1)
      // console.log(editPs, previewPs);
    }, 1000)

1.1 左侧编辑器节点获得是从hast中(html的ast),里面自身带有节点的位置信息,因此通过编辑器就可以获得。

const left = editor.heightAtLine(leftNode.position.start.line - 1, 'local') /
    (scrollInfo.height - scrollInfo.clientHeight)

得到左侧 某个节点 在左侧编辑器中可滚动的百分比。

1.2 对于已经生成的HTML,节点获得方式

const rightNodes = [...body.childNodes].filter(
    (v): v is HTMLElement => v instanceof HTMLElement
 )

同样 得到 与左侧 某个节点 在 右侧预览界面中可滚动的百分比。

const right =
  (rightNode.offsetTop - body.offsetTop) /
  (previewEl.scrollHeight - previewEl.clientHeight)

1.3 最后同时滚动到底部

// 同时都滚动到底部
  editPs.push(1)
  previewPs.push(1)

这样我们得到了,同一个节点 在 分别在 左边和右边 在可滚动距离中的位置信息。

思考:位置的变化,是在编辑的时候触发的,为什么会在滚动时去计算呢?

  1. 左侧编辑器滚动引起的右侧滚动
  
  const info = editor.getScrollInfo()
  const leftRatio = info.top / (info.height - info.clientHeight) // 左侧当前的滚动比例


  const startIndex = findStartIndex(leftRatio, editPs) // 找到Node的开始位置
  // 算出来滚动距离
  const rightRatio =

    ((leftRatio - editPs[startIndex]) *
      (previewPs[startIndex + 1] - previewPs[startIndex])) /
      (editPs[startIndex + 1] - editPs[startIndex]) +
    previewPs[startIndex]
  // const rightRatio = rightPs[startIndex]; // for testing

  previewEl.scrollTo(
    0,
    rightRatio * (previewEl.scrollHeight - previewEl.clientHeight)
  )

说明: 介绍之前需要先了解下 下面的两个函数。

 const info = editor.getScrollInfo() // 是CodeMirror的API,调用后的结果如下
/*
  {
      "left": 0,  // 编辑器左边滚动
      "top": 12,  // 编辑器向上滚动
      "height": 2090, // 编辑器内容总高度
      "width": 584, // 宽度
      "clientHeight": 347, //用户可见区的高度
      "clientWidth": 584 //用户视图区宽度
  }
  */
  
 // findStartIndex 判断当前行是落在哪个区间内了。
 function findStartIndex(num: number, nums: number[]) {
      let startIndex = nums.length - 2 // 因为是最后一个是同步到底
      for (let i = 0; i < nums.length; i++) {
        if (num < nums[i]) {
          startIndex = i - 1
          break
        }
      }
      startIndex = Math.max(startIndex, 0) // ensure >= 0
      return startIndex
}

核心步骤:

  • leftRatio: 表示 左侧编辑器最顶一行 在 编辑器可滚动距离的位置比例。

  • 然后根据leftRatio 找到出最顶行所处的节点startIndex。

  • 然后计算出rightRatio 方法如下:

  1. 开始行在节点中的位置比例 =(左侧顶行位置 – 所在节点的开始)/ (所在节点结束位置 – 所在节点开始位置)

  2. 右侧对应行的位置(rightRatio) = 开始行在节点中的位置比例 * (对应的预览节点的结束位置 – 对应的预览节点开始位置)+ 对应节点开始位置。 具体公式如下:

  const rightRatio =
    ((leftRatio - editPs[startIndex]) *
      (previewPs[startIndex + 1] - previewPs[startIndex])) /
      (editPs[startIndex + 1] - editPs[startIndex]) +
    previewPs[startIndex]
  • 最后计算滚动距离rightRatio * (previewEl.scrollHeight – previewEl.clientHeight)
  1. 右侧编辑器滚动
currentBlockIndex = findStartIndex( // 最顶部的占位符号
    previewEl.scrollTop / (previewEl.scrollHeight - previewEl.offsetHeight),
    previewPs
  )

  // 这里怎么又算一遍呢
  const rightRatio =

    previewEl.scrollTop / (previewEl.scrollHeight - previewEl.clientHeight)

  const startIndex = findStartIndex(rightRatio, previewPs)

  const leftRatio =
    ((rightRatio - previewPs[startIndex]) *
      (editPs[startIndex + 1] - editPs[startIndex])) /
      (previewPs[startIndex + 1] - previewPs[startIndex]) +
    editPs[startIndex]

  if (isNaN(leftRatio)) {
    return
  }

  const info = editor.getScrollInfo()
  // 调用编辑器的滚动
  editor.scrollTo(0, leftRatio * (info.height - info.clientHeight))

右侧引起左侧的滚动,思路和左侧是一致的。

小结

源码使用Svelte来写的,也不复杂,很适新手去学习, 并且双侧同步的滚动方法设计的很巧妙。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYQgeqZd' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片