使用Codemirror打造Markdown编辑器
前几天突然想给自己的在线编译器加一个Markdown编辑功能,于是花了两三天敲敲打打初步实现了这个功能。
一个Markdown编辑器需要有如下常用功能:

- 粗体
- 斜体
- 中划线
- 标题
- 链接
- 图片
- 引用
- 代码
- 有序列表
- 无序列表
- 横线
看上去想实现这些功能有点复杂,但是Codemirror提供了很多API可以更方便地修改编辑内容。
在阐述我是如何实现这些功能前,我先将实现时用到的API列出来。
cm.somethingSelected()
是否选中编辑器内的任何文本。
cm.listSelections()
选中的文本信息。
cm.getRange(from: {line, ch}, to: {line, ch}, ?separator: string)
在编辑器中的给定点之间获取文本。
cm.replaceRange(replacement: string, from: {line, ch}, to: {line, ch}, ?origin: string)
用replacement替换给定点之间的文本 。
cm.setCursor(pos: {line, ch}|number, ?ch: number, ?options: object)
设置光标位置。
cm.getCursor(?start: string)
获取光标位置 。
cm.setSelection(anchor: {line, ch}, ?head: {line, ch}, ?options: object)
设置一个选择范围。
cm.getLine(n: integer)
获取某行文本内容。
上面的API中,cm为Codemirror实例,也就是编辑器实例。line为行数,ch为列数(该行第几个字符)。
功能实现
首先是粗体,斜体,中划线和代码,这四个功能实现的方法是相同的。
当用户触发添加粗体、斜体、中划线或代码事件时,流程如下:

如上图所示,先来说说光标没选中文本时的处理:
- 使用
cm.getCursor()找到光标位置 - 使用
cm.getRange()判断前后是否有匹配字符串(匹配字符串代表粗体、斜体、中划线或和代码的字符串:**、*、~~和'``') 。
- 前面或后面有匹配字符串
- 使用cm.replaceRange()清除匹配字符串
- 前面或后面没有匹配字符串
- 使用cm.replaceSelection()添加匹配字符串
具体代码和注释如下:
const changePos = matchStr.length
let preAlready = false, aftAlready = false // 前后是否已经有相应样式标识,如**,`,~等
const cursor = cm.getCursor()
const { line: curLine, ch: curPos } = cursor // 获取光标位置
// 判断前后是否有matchStr
cm.getRange({ line: curLine, ch: curPos - changePos }, cursor) ===
matchStr && (preAlready = true)
cm.getRange(cursor, { line: curLine, ch: curPos + changePos }) ===
matchStr && (aftAlready = true)
// 去除前后的matchStr
if (aftAlready && preAlready) {
cm.replaceRange('', cursor, { line: curLine, ch: curPos + changePos })
cm.replaceRange('', { line: curLine, ch: curPos - changePos }, cursor)
cm.setCursor({ line: curLine, ch: curPos - changePos })
} else if (!preAlready && !aftAlready) {
// 前后都没有matchStr
cm.replaceSelection(matchStr + matchStr)
cm.setCursor({ line: curLine, ch: curPos + changePos})
}
cm.focus()
来看看效果:

在光标选中文本的情况下,处理过程相对来说要复杂一些:
- 使用
cm.listSelections()[0]获取第一组选中的文本,返回光标的起始位置与结束位置 - 判断所选文字的开头和结尾的位置,因为光标的起始位置是相对位置而不是绝对位置,也就是说当你从上到下,从左到右来选择文本的时候,光标起始位置所选文本开头,否则就是末尾。
- 使用
cm.getRange()判断前后是否有匹配字符串
- 前面或后面有匹配字符串
- 使用cm.replaceRange()清除匹配字符串
- 前面或后面没有匹配字符串
- 使用cm.replaceSelection()添加匹配字符串 - 更新光标选取位置
具体代码和注释如下:
const changePos = matchStr.length // matchStr为传入参数,可以是'**','*','~~','`'或者其他符合markdown语法的字符串
let preAlready = false,aftAlready = false
if (cm.somethingSelected()) {
// 如果选中了文本
const selectContent = cm.listSelections()[0] // 第一个选中的文本
let { anchor, head } =selectContent // 前后光标位置
head.line >= anchor.line &&head.sticky === 'before' &&([head, anchor] = [anchor, head])
let { line: preLine, ch: prePos } = head
let { line: aftLine, ch: aftPos } = anchor
// 判断前后是否有matchStr
cm.getRange({ line: preLine, ch: prePos - changePos }, head) ===
matchStr && (preAlready = true)
cm.getRange(anchor, { line: aftLine, ch: aftPos + changePos }) ===
matchStr && (aftAlready = true)
// 去除前后的matchStr
aftAlready &&
cm.replaceRange('', anchor, { line: aftLine, ch: aftPos + changePos })
preAlready &&
cm.replaceRange('', { line: preLine, ch: prePos - changePos }, head)
if (!preAlready && !aftAlready) {
// 前后都没有matchStr
cm.setCursor(anchor)
cm.replaceSelection(matchStr)
cm.setCursor(head)
cm.replaceSelection(matchStr)
prePos += changePos
aftPos += aftLine === preLine ? changePos : 0
cm.setSelection(
{ line: aftLine, ch: aftPos },
{ line: preLine, ch: prePos }
)
} else if (!preAlready) {
// 只有后面有matchStr
cm.setCursor(head)
cm.replaceSelection(matchStr)
prePos += changePos
aftPos += aftLine === preLine ? changePos : 0
cm.setSelection(
{ line: aftLine, ch: aftPos },
{ line: preLine, ch: prePos }
)
} else if (!aftAlready) {
// 只有前面有matchStr
cm.setCursor({ line: aftLine, ch: aftPos - changePos })
cm.replaceSelection(matchStr)
prePos -= changePos
aftPos -= aftLine === preLine ? changePos : 0
cm.setSelection(
{ line: aftLine, ch: aftPos },
{ line: preLine, ch: prePos }
)
}
cm.focus()
}
来看看效果:

接下来我说说如何实现引用,无序列表和有序列表。
我是按照VSCode的markdown插件的机制来处理这三种格式。当用户操作引用,无序列表和有序列表时的处理流程如下:

- 判断是否选中文本
- 已经选中文本,找到位置
- 已经选中多行
- 循环将每行前面加上>、-或数字.使其变为列表项
- 已经选中单行
- 将选中文本转换为列表项
- 没选中文本,找到光标位置
- 该行已经是列表
- 将列表向下延伸一行
- 该行不是列表
- 无操作
具体代码和注释如下:
function addList (cm, matchStr) {
// 添加引用和无序列表, matchStr为传入参数,可以是
if (cm.somethingSelected()) {
const selectContent = cm.listSelections()[0] // 第一个选中的文本
let { anchor, head } =selectContent
head.line >= anchor.line &&head.sticky === 'before' &&([head, anchor] = [anchor, head])
let preLine = head.line
let aftLine = anchor.line
if (preLine !== aftLine) {
// 选中了多行,在每行前加上匹配字符
let pos = matchStr.length
for (let i = preLine;i <= aftLine;i++) {
cm.setCursor({ line: i, ch: 0 })
cm.replaceSelection(matchStr)
i === aftLine && (pos += cm.getLine(i).length)
}
cm.setCursor({ line: aftLine, ch: pos })
cm.focus()
} else {
// 检测开头是否有匹配的字符串,有就将其删除
const preStr = cm.getRange({ line: preLine, ch: 0 }, head)
if (preStr === matchStr) {
cm.replaceRange('', { line: preLine, ch: 0 }, head)
} else {
const selectVal = cm.getSelection()
let replaceStr = `\n\n${matchStr}${selectVal}\n\n`
cm.replaceSelection(replaceStr)
cm.setCursor({ line: preLine + 2, ch: (matchStr + selectVal).length})
}
}
} else {
const cursor = cm.getCursor()
let { line: curLine, ch: curPos } = cursor // 获取光标位置
let preStr = cm.getRange({ line: curLine, ch: 0 }, cursor)
let preBlank = ''
if (/^( |\t)+/.test(preStr)) {
// 有序列表标识前也许会有空格或tab缩进
preBlank = preStr.match(/^( |\t)+/)[0]
}
curPos && (matchStr = `\n${preBlank}${matchStr}`) && ++curLine
cm.replaceSelection(matchStr )
cm.setCursor({ line: curLine, ch: matchStr.length - 1})
}
cm.focus()
}
来看看效果:

至于有序列表,需要先去除当前行前面的空格和制表符,再判断是否以数字. 开头,如果有,便取出数字 ,下一行的数字逐步递增。其他的地方和无序列表差不多。
具体代码和注释如下:
function addOrderList (cm) {
// 添加有序列表
if (cm.somethingSelected()) {
const selectContent = cm.listSelections()[0] // 第一个选中的文本
let { anchor, head } = selectContent
head.line >= anchor.line &&head.sticky === 'before' &&([head, anchor] = [anchor, head])
let preLine = head.line
let aftLine = anchor.line
if (preLine !== aftLine) {
// 选中了多行,在每行前加上匹配字符
let preNumber = 0
let pos = 0
for (let i = preLine;i <= aftLine;i++) {
cm.setCursor({ line: i, ch: 0 })
const replaceStr = `${++preNumber}. `
cm.replaceSelection(replaceStr)
if (i === aftLine) {
pos += (replaceStr + cm.getLine(i)).length
}
}
cm.setCursor({ line: aftLine, ch: pos })
cm.focus()
} else {
const selectVal = cm.getSelection()
let preStr = cm.getRange({ line: preLine, ch: 0 }, head)
let preNumber = 0
let preBlank = ''
if (/^( |\t)+/.test(preStr)) {
// 有序列表标识前也许会有空格或tab缩进
preBlank = preStr.match(/^( |\t)+/)[0]
preStr = preStr.trimLeft()
}
if (/^\d+(\.) /.test(preStr)) {
// 是否以'数字. '开头,找出前面的数字
preNumber = Number.parseInt(preStr.match(/^\d+/)[0])
}
let replaceStr = `\n${preBlank}${preNumber + 1}. ${selectVal}\n`
cm.replaceSelection(replaceStr)
cm.setCursor({ line: preLine + 1, ch: replaceStr.length})
}
} else {
const cursor = cm.getCursor()
let { line: curLine, ch: curPos } = cursor // 获取光标位置
let preStr = cm.getRange({ line: curLine, ch: 0 }, cursor)
let preNumber = 0
let preBlank = ''
if (/^( |\t)+/.test(preStr)) {
// 有序列表标识前也许会有空格或tab缩进
preBlank = preStr.match(/^( |\t)+/)[0]
preStr = preStr.trimLeft()
}
if (/^\d+(\.) /.test(preStr)) {
// 是否以'数字. '开头,找出前面的数字
preNumber = Number.parseInt(preStr.match(/^\d+/)[0])
}
let replaceStr = `\n${preBlank}${preNumber + 1}. `
cm.replaceSelection(replaceStr)
cm.setCursor({ line: curLine + 1, ch: replaceStr.length - 1})
}
}
来看看效果:

如果你明白了上面的功能是怎么实现的,那么标题、链接、图片、横线的实现方法我想你也明白了。
该编辑器还没有编辑窗口和预览窗口同步滚动的功能,马克飞象的同步滚动效果我不知道该如何实现,如果有那位大神知道,望指教。
进入编辑器在点击侧边栏的设置,选择预处理。

把HTML的预处理语言换成Markdown就可以开启Markdown编辑模式了。

我还是个前端小白,如果觉得那些地方需要优化和改进,望指教!
使用Codemirror打造Markdown编辑器的更多相关文章
- sublime-text3打造markdown编辑器
编辑插件 sublime自带的markdown语法高亮并不是很友好,推荐安装Markdown Editing,github主页然后在视图->语法里选择MarkdownEditing启用,支持三种 ...
- Visual Studio Code打造Markdown编辑器
1.准备工作: OS:Windows10 专业版或企业版 安装:Visua Studio Code,版本 1.23 (2018-5-3) 官网下载:https://github.com/Microso ...
- 打造自己的Markdown编辑器
原文链接: http://www.errdev.com/post/5/ Markdown以其简洁的语法赢得了广大程序猿的喜爱,搜了一下github上相关的web编辑器,星星比较多的 Stackedi ...
- 使用Atom打造无懈可击的Markdown编辑器
一直以来都奢想拥有一款全能好用的Markdown编辑器,直到遇到了Atom.废话不多说,直接开搞! 1. 安装Atom 下载安装Atom:https://atom.io/ 2. 增强预览(markdo ...
- Atom打造优雅的MarkDown 编辑器
1.下载Atom https://atom.io/ 2.安装Atom 双击自动安装,会默认安装到C盘,无法修改. 3.安装simplified-chinese-menu 插件 这是一个可以将软件汉化的 ...
- 好用的Markdown编辑器一览 readme.md 编辑查看
https://github.com/pandao/editor.md https://pandao.github.io/editor.md/examples/index.html Editor.md ...
- NanUI for Winform 使用示例【第二集】——做一个所见即所得的Markdown编辑器
经过了这一个多星期的调整与修复,NanUI for .NET Winform的稳定版已经发布.应广大群友的要求,现已将NanUI的全部代码开源. GitHub: https://github.com/ ...
- #第一用Markdown编辑器#
Markdown初次使用 This is a simple Markdown editor based on 'Markdown' it's * italic * style. it's also _ ...
- 让Flask-admin支持markdown编辑器
前言 flask-admin 算是一个很不错的 flask 后台管理了,用它来做博客系统的管理后端再合适不过了,节约时间成本,避免重复造轮子,但是作为一个程序员,写文章怎么可以没有 markdown ...
随机推荐
- ASP.NET Core 3.1+MySQL 部署到docker上面使用docker-compose+DockerFile
一.新建DockerFile文件 选择Linux版本 FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base WORKDIR ...
- python3(二十四) subClas
""" 继承的多态 """ __author__ = 'shaozhiqi' # -----------------父类---------- ...
- [一起面试AI]NO.9 如何判断函数凸或非凸
首先定义凸集,如果x,y属于某个集合M,并且所有的θx+(1-θ)f(y)也属于M,那么M为一个凸集.如果函数f的定义域是凸集,并且满足 f(θx+(1-θ)y)≤θf(x)+(1-θ)f(y) 则该 ...
- C#多线程(4):进程同步Mutex类
Mutex 类 构造函数和方法 系统只能运行一个程序的实例 解释一下上面的示例 接替运行 进程同步示例 另外 Mutex 类 Mutex 中文为互斥,Mutex 类叫做互斥锁.它还可用于进程间同步的同 ...
- 【Java】 语言基础习题汇总 [2] 面向对象
30 面向对象的三条主线和面向对象的编程思想? 类与类的成员 : 属性.方法.构造器.代码块.内部类. 面向对象的三大特征:封装.继承.多态[如果还有一个,那就是抽象] 关键字:this.super. ...
- 使用d3.js的时候,如何用zoom translate scale限制拖拽范围
红色代表需要改写的代码 1.添加定义图像大小和容器的大小及坐标 d3.behavior.zoom = function () { var moveCanvas={ width: , height: , ...
- ST表(求解静态RMQ问题)
例题:https://www.acwing.com/problem/content/1272/ ST表类似于dp. 定义st[i][j]表示以i为起点,长度位2^j的一段区间,即[ i , i + 2 ...
- 美化你的终端利器Iterm2
Iterm2是特别好用的一款终端,支持自定义字体和高亮,让日常开发,充满愉悦. 安装iterm2(mac版) brew tap caskroom/cask brew cask install iter ...
- git分支,git commit,git流程
1. git分支命令规范 1. Master 主分支 2. Dev 开发分支 3. Feature 功能分支(例如:feature-x) 4. Release 预发布分支(例如:release-1.2 ...
- 【题解】P4570 [BJWC2011]元素 - 线性基 - 贪心
P4570 [BJWC2011]元素 声明:本博客所有题解都参照了网络资料或其他博客,仅为博主想加深理解而写,如有疑问欢迎与博主讨论✧。٩(ˊᗜˋ)و✧*。 题目描述 给你 \(n\) 个二元组 \( ...