Skip to content

开发一个markdown编辑器

前言:心血来潮,突然想写一个markdown编辑器,于是就有了这篇文章,本文为边开发边写,因此会显得较为杂乱,待后期会做一个整理 ==文章暂时在飞书文档中进行更新,待完成后再更新到此处== [markdown编辑器][https://ryykhmjhfc.feishu.cn/docx/HfGBd9I8EoJdbpxXnjQc9hsun1f]

对于这个编辑器的大致思路,我有两个设想

方案一

  1. 准备两个div,一个作为显示的编辑区,一个作为隐藏的中间件,或者用一个变量存起来也行,编辑区用来显示渲染后的内容,中间件用来保存原本的形式,如一级标题,在编辑区中显示的就是h1标签,在中间件中就是# 一级标题
  2. 当在编辑区进入输出时,阻止它的输入行为,将输入行为插入到中间件中,再通过中间件重新渲染到编辑区中

方案二

  1. 准备一个div作为编辑区,所有操作将在该编辑区直接进行
  2. 特殊按键的处理,如在输入标题、有序列表、无序列表这些时,都有一个空格,那么在输入空格的时候,进行一个处理,如果判断本行为一级标题,就将本行的标签改成h1,样式也改为一级标题的样式

在这两种方式中,我认为方式一是更简单的,他相当于将整段文本进行处理,而他唯一的难点在于如何通过编辑区的光标位置,定位到中间件中,举一个简单的例子,当我们的光标处于一个一级标题中,效果如下

一级标题

当光标位于一后面时,得到的光标位置应该是在本行中的1,但我们直接使用这个1来对中间件进行定位的话,显然是不对的,因为在中间件中,他的原文本应该# 一级标题,那么他的光标位置应该为3 这时我们会想,根据不同的标签,进行不同的加法布局可以了吗?如一级标题就是+2,三级标题就是+4 是的,在处理标题,列表这些时,处理方式还是较为简单的,但如果处理的是复杂一点的呢?且不说我认为最复杂的表格,当我们处理的是一些行内元素时,比如下面这个 这里是一段行内元素 当我们的光标位于行字后面时,我们想要得到的位置应该是7,但实际上我们获取到的会是1,这是因为在通过selection.getRangeAt(0)所获取到的位置,是基于当前标签的位置,在这个行内标签中,我们想将样式显示出来,就必须用一个span标签或其他标签将他包裹起来,然后再添加样式,这就导致我们获取到的会是一个span标签,而不是这一整行的p标签或其他标签,因此得到的位置,也会是错误的,当然这种情况在方案二中也会遇到 并且,每一次输入都需要将整段文本进行重新渲染,然后在重新定位光标位置,在性能上也会有着明显的劣势 方案一的另一个问题就是语法上的冲突,比如我想生成一个代码块,那么就要输入三个,但是当我们输入到第二个的时候,由于是整篇文章一起渲染的原因,就会直接渲染成行内代码了 说完方案一,说一说方案二 方案二是一个实时渲染,只对当前标签进行处理的方案,但显然为每一个按键都添加处理的话,也是不合适的,因此我们可以只针对一些特殊的按键进行处理,如空格、回车等等,但也因为这个原因,方案二需要处理的情况将会更多 总的来说,方案一需要处理的情况更少,唯一需要解决的问题就是定位的问题,而这个问题我认为还是比较头疼的,并且使用方案一的话,每一次输入都要对整篇文章进行重新渲染,在性能上劣势也更明显,因此我选择了方案二

具体实现方法

  1. 准备一个div作为编辑区
  2. 禁止文本拖拽,contenteditable="true"的标签,其内的内容是可以拖拽的,需要禁止掉
  3. 特殊键处理
  4. 空格
  5. 一 ~ 六级标题、有序列表、无序列表,当前光标位置为语义符+1,语义符为[#, ##, ###, ####, #####, ######, *, -, +, (index).]
  6. 回车
  7. 当前行为1 ~ 6级标题、普通文本
  8. 当前光标位于本行开头
  9. 向前插入行
  10. 当前光标位于本行末尾
  11. 直接换行
  12. 当前光标位于本行中间
  13. 当前光标位于行内元素内
  14. 将本行的所有行内元素,转换为普通文本,重新定位光标,拆分文本并换行
  15. 当前光标位于行内元素外
  16. 拆分文本并换行
  17. 当前行为有序列表
  18. 换行,且新行自动设为有序列表,仍遵循上面的换行规则
  19. 当前行为无序列表
  20. 换行,且新行自动设为无序列表,仍遵循上面的换行规则
  21. 当前行为引用文本
  22. 换行,且新行自动设为引用文本,仍遵循上面的换行规则
  23. 在包含缩进的有序列表、无序列表、引用文本中的特殊处理
  24. crtl + 回车
  25. 直接换行,不管光标位于何处
  26. 退格与删除
  27. 按退格键时,若文本已为空,至少保留一行普通文本标签
  28. 按退格键时,若光标处于本行开头且上一行不为空,将本行的内容插入到上一行并将本行的格式处理为上一行的格式
  29. 按删除键时,若光标处于本行末尾且下一行不为空,将下一行的内容插入到本行并将下一行的格式处理为本行的格式
  30. 退格键在包含缩进的有序列表、无序列表、引用文本中的特殊处理
  31. 方向键右和下的处理
  32. 按下方向键右,且光标处于文末时,添加一行
  33. 按下方向键下,且光标处于最后一行,添加一行
  34. 行内元素的处理
  35. 光标位于行内元素时,将语义符暂时显示出来,此时可对语义符进行修改,光标离开后重新隐藏
  36. 行内元素的渲染较为特殊,比如当我想输入一个粗体,则应该输入粗体,但输入习惯因人而异,我可能先输入两个*,然后输入文本,最后再输入两个*,也有可能输入四个*,然后回到第二个*后面,再输入文本,这样看来就需要对每一个按键都进行事件处理,显然这不是我想要的目前想到的处理方案为
  37. 按下回车时,判断本行是否拥有行内元素,进行渲染
  38. 光标移动时,即键盘中的上下左右四个按键,以及鼠标点击时,判断整篇文章是否拥有行内元素,进行渲染
  39. 特殊按键,包括[^, ~, `, *, ], ), =],按下这些按键时判断本行是否拥有行内元素,进行渲染
  40. 超文本的修改处理
  41. 光标回到图片、超链接时,要将图片、超链接的原始文本显示出来,离开时重新隐藏
  42. 光标回到代码块中时,显示可替换代码类型
  43. 光标回到表格中时,显示可替换表格的对齐方式
  44. 自动补全,需自动补全的符号包括[', ", (, [, {]
  45. 自动缩进,在含有缩进的有序列表和无序列表中,按下回车需要保存原来的缩进
  46. 粘贴处理,需要将粘贴内容处理为对应的语法的纯文本形式再粘贴,如复制粘贴一张图片,要变成![加载失败提示文本][图片链接 "鼠标悬浮提示文本"]
  47. 快捷键处理,如ctrl + 1为设置/取消一级标题
  48. 接管上下左右按键、回车、删除、退格,以及最头疼的鼠标点击,如在无序列表中,因为是ul包含li,按方向键有可能会将光标从li移动到非编辑区的ul中
  49. 接管tab
  50. 在有序列表、无序列表、引用文本中,为创建节点缩进
  51. 在表格中,为切换单元格
  52. 其他情况为添加一个制表符
  53. 接管撤销(ctrl + z)与恢复(ctrl + y)
  54. 渲染行内元素冲突的问题,如斜体与粗体,斜体粗体,判断是否为连续的*,中间没有其他内容,如果是,则不渲染为行内元素

猪脑过载,先写这么多=。=

块级元素

效果语法
一级标题# 一级标题
二级标题## 二级标题
三级标题### 三级标题
四级标题#### 四级标题
五级标题##### 五级标题
六级标题###### 六级标题
无序列表-/+/* 无序列表
有序列表数字. 有序列表
引用> 引用文本
代码块代码类型\n代码片段\n
表格

行内元素

效果语法
图片![加载失败提示文本][图片链接 "鼠标悬浮提示文本"];后中括号可用括号代替
链接[显示文本][链接];后中括号可用括号代替
引用
上标^上标^
下标~下标~
行内代码行内代码
高亮==高亮==
斜体斜体
粗体粗体
粗斜体粗斜体