在阐述我是如何实现这些功能前,我先将实现时用到的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为列数(该行第几个字符)。
首先是粗体,斜体,中划线和代码,这四个功能实现的方法是相同的。
当用户触发添加粗体、斜体、中划线或代码事件时,流程如下:
如上图所示,先来说说光标没选中文本时的处理:
具体代码和注释如下:
在光标选中文本的情况下,处理过程相对来说要复杂一些:
接下来我说说如何实现引用,无序列表和有序列表。
我是按照VSCode的markdown插件的机制来处理这三种格式。当用户操作引用,无序列表和有序列表时的处理流程如下:
具体代码和注释如下:
至于有序列表,需要先去除当前行前面的空格和制表符,再判断是否以数字.开头,如果有,便取出数字,下一行的数字逐步递增。其他的地方和无序列表差不多。
说完了编辑,再说说预览功能,一个支持markdown的编辑器怎么能没有同步滚动呢?
想要实现同步滚动有两种方案:
均匀滚动就是计算编辑窗口和预览窗口的滚动条高度以及这两个高度的比例,比方说编辑窗口的滚动条高度为2000px,预览窗口滚动条高度为4000px。
那么每当编辑窗口的滚动条滚动1px,那么预览窗口就滚动2px。
这样写的好处是方便且性能好,但缺陷很明显:
如果我在文档中加入了图片,并且图片很大,那么就会导致编辑窗口中显示的代码和预览窗口中的元素是不对应的,在代码很多,滚动条拉的越下的时候效果越明显。我这里拿Editor.md这个工具来举个例子:
我把滚动条拉的很下,我们可以发现编辑窗口显示的是Emoji的内容,但预览窗口显示的却是与之无关的公式和流程图。而公式和流程图所对应的代码实际上在Emoji的下面,这就比较影响编辑体验了。
考虑到这个弊端,我决定使用元素顶部定位方法。
元素顶部定位的原理是这样的:
Codemirror可以利用编辑器滚动条的位置来获取当前显示在最顶部的行的行号:
constscrollInfo=cm.getScrollInfo()constlineNumber=cm.lineAtHeight(scrollInfo.top,'local')获取了行号,就可以获取该行及以上的所有代码。
然后我们使用marked工具将代码编译成HTML字符串,并使用DOMParser将其转换成真正的DOM元素:
constrange=cm.getRange({line:0,ch:null},{line:lineNumber,ch:null})constmarked=require('marked')constdoc=newDOMParser().parseFromString(marked(range),'text/html')为了考虑性能,我们不能匹配所有标签,因此需要制定一个匹配标签的集合:
matchTagsStr='p,h1,h2,h3,h4,h5,h6,li,pre,blockquote,hr,table,code>span'上面的这些就是我们必须要匹配的标签和选择器,我们将doc中符合匹配条件的元素选出来:
constmatchEleList=doc.body.querySelectorAll(matchTagsStr)那么matchEleList中最后的元素就是我们在预览视口中看到的第一个元素。
接下来的过程非常简单:
constcm=this.cm//监听codemirror编辑器滑动事件cm.on('scroll',()=>{//获取之前代码解析出来的匹配列表长度constlength=matchEleList.lengthif(length){//如果length不为0,获取iframe中符合matchTagsStr选择器的元素列表constmatchIframeEleList=window.document.body.querySelectorAll(matchTagsStr)if(length===1)length--//获取matchIframeEleList中第length个元素并将视口滑动到该元素的位置if(matchIframeEleList.length){consttarget=matchIframeEleList[length]target.scrollIntoView()}}else{window.scrollTo(0,0)}})上述代码只是为了帮助理解,实际代码比上面的要复杂。
到此我们就实现了滚动编辑窗口,预览窗口也滚动到对应位置的效果了:
这样,一个简单但好用的markdown编辑器就完成啦!
Codemirror是一个扩展性很强的代码编辑工具,通过Codemirror提供的api我们可以实现很多功能。
这是我的在线编辑器地址和github仓库地址:
我还是个前端小白,如果觉得那些地方需要优化和改进,望指教!