前几天突然想给自己的在线编译器加一个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})
}
}

来看看效果:

如果你明白了上面的功能是怎么实现的,那么标题、链接、图片、横线的实现方法我想你也明白了。

该编辑器还没有编辑窗口和预览窗口同步滚动的功能,马克飞象的同步滚动效果我不知道该如何实现,如果有那位大神知道,望指教。

这是该编辑器的GitHub以及项目链接

进入编辑器在点击侧边栏的设置,选择预处理。

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

我还是个前端小白,如果觉得那些地方需要优化和改进,望指教!

使用Codemirror打造Markdown编辑器的更多相关文章

  1. sublime-text3打造markdown编辑器

    编辑插件 sublime自带的markdown语法高亮并不是很友好,推荐安装Markdown Editing,github主页然后在视图->语法里选择MarkdownEditing启用,支持三种 ...

  2. Visual Studio Code打造Markdown编辑器

    1.准备工作: OS:Windows10 专业版或企业版 安装:Visua Studio Code,版本 1.23 (2018-5-3) 官网下载:https://github.com/Microso ...

  3. 打造自己的Markdown编辑器

    原文链接:  http://www.errdev.com/post/5/ Markdown以其简洁的语法赢得了广大程序猿的喜爱,搜了一下github上相关的web编辑器,星星比较多的 Stackedi ...

  4. 使用Atom打造无懈可击的Markdown编辑器

    一直以来都奢想拥有一款全能好用的Markdown编辑器,直到遇到了Atom.废话不多说,直接开搞! 1. 安装Atom 下载安装Atom:https://atom.io/ 2. 增强预览(markdo ...

  5. Atom打造优雅的MarkDown 编辑器

    1.下载Atom https://atom.io/ 2.安装Atom 双击自动安装,会默认安装到C盘,无法修改. 3.安装simplified-chinese-menu 插件 这是一个可以将软件汉化的 ...

  6. 好用的Markdown编辑器一览 readme.md 编辑查看

    https://github.com/pandao/editor.md https://pandao.github.io/editor.md/examples/index.html Editor.md ...

  7. NanUI for Winform 使用示例【第二集】——做一个所见即所得的Markdown编辑器

    经过了这一个多星期的调整与修复,NanUI for .NET Winform的稳定版已经发布.应广大群友的要求,现已将NanUI的全部代码开源. GitHub: https://github.com/ ...

  8. #第一用Markdown编辑器#

    Markdown初次使用 This is a simple Markdown editor based on 'Markdown' it's * italic * style. it's also _ ...

  9. 让Flask-admin支持markdown编辑器

    前言 flask-admin 算是一个很不错的 flask 后台管理了,用它来做博客系统的管理后端再合适不过了,节约时间成本,避免重复造轮子,但是作为一个程序员,写文章怎么可以没有 markdown ...

随机推荐

  1. std::lock_guard和std::unique_lock的区别

    std::lock_guard 1 初始化的时候锁定std::mutex std::mutex m_mtx; std::lock_guard<std::mutex> m_lock(m_mt ...

  2. AJ学IOS 之小知识之xcode6自动提示图片插件 KSImageNamed的安装

    AJ分享,必须精品 一:首先看效果 KSImageNamed是让XCode能预览项目中图片的插件 很牛逼,据说写这个插件的牛人在日本~ 主要针对imageNamed:方法 效果如图: 安装: 首先需要 ...

  3. Thinking in Java,Fourth Edition(Java 编程思想,第四版)学习笔记(九)之Interfaces

    Interfaces and abstract classes provide more structured way to separate interface from implementatio ...

  4. svg整体缩放至指定大小

    一.问题 svg画面跑在分辨率低的电脑上,导致不能完全显示. 二.要求 svg要能够根据电脑的屏幕大小自动缩放至适配电脑的尺寸. 三.实现 1.获取本机窗口高度.宽度 let clientWidth ...

  5. 爬虫的新手使用教程(python代理IP)

    前言 Python爬虫要经历爬虫.爬虫被限制.爬虫反限制的过程.当然后续还要网页爬虫限制优化,爬虫再反限制的一系列道高一尺魔高一丈的过程.爬虫的初级阶段,添加headers和ip代理可以解决很多问题. ...

  6. day23作业

    # 作业: # 1.把登录与注册的密码都换成密文形式 info = {"tom":"202cb962ac59075b964b07152d234b70"} def ...

  7. 提高万恶的KPI,切忌要避开这六个低效的编程习惯

    作者:程序员小跃 Slogan:当你的才华还无法撑起你的野心时,那应该静下心来好好学习 上次的翻译,引起了很大的反响,大家都想知道自己和高级工程师的差距,看了我的文章,是不是都在默默地做着比较呢?如果 ...

  8. Eight HDU - 1043 (双向BFS)

    记得上人工智能课的时候老师讲过一个A*算法,计算估价函数(f[n]=h[n]+g[n])什么的,感觉不是很好理解,百度上好多都是用逆向BFS写的,我理解的逆向BFS应该是从终点状态出发,然后把每一种状 ...

  9. C - 剪花布条 (KMP例题)

    一块花布条,里面有些图案,另有一块直接可用的小饰条,里面也有一些图案.对于给定的花布条和小饰条,计算一下能从花布条中尽可能剪出几块小饰条来呢?  Input输入中含有一些数据,分别是成对出现的花布条和 ...

  10. STL入门大全(待编辑)

    前言:这个暑假才接触STL,仿佛开启了新世界的大门(如同学完结构体排序一般的快乐\(≧▽≦)/),终于彻底领悟了大佬们说的“STL大法好”(虽然我真的很菜www现在只学会了一点点...)这篇blog主 ...