给Markdown编辑器HyperMD添加新功能

HyperMD是一个独特的Markdown编辑器。
和其他的编辑器不同,它的特色在于编辑和预览一体,在输入Markdown语法后自动在原位置渲染,当编辑指针指向/鼠标点击被渲染后的元素时,将自动展开Markdown编码以供编辑。
这里可以在线尝试。

可是,虽然这是个很棒的idea,但这是一个已经被抛弃的项目,它的作者laobubu已经不再维护它,转而维护他的新作品MarkdownIME。后者我认为只能胜任简单的书写,而不是频繁地用Markdown编辑较长的内容——无法修改已经渲染的标签,而且想要修改格式简直是噩梦一般。做个评论框和普通的WYSIWYG编辑器配合还是可以的。

而HyperMD已经较难使用了。原因在于它有一些bug。我着手修复了其中的一部分。

基本功能修复

开始从github上pull的版本根本不能用。很多bug比如

控制台频频弹出错误(每次点击都会)、
公式不能正常显示/折叠、
代码区字体不是等宽的。

然后搞了一个东西可以按目录结构完整地保存远端网页。
用到的东西:

  1. phantomjs,访问一下网页并延时一段时间,记录所有请求。
  2. nodejs,将上一步的请求按照目录全部抓取。

这样就能用了。
不过因为这样模块不全所以可能会出bug。试试安个codemirror结果发现问题重现。
怀疑是不再支持新版的codemirror.开始尝试codemirror的各个版本。

如何列出一个包的所有版本?

cnpm view codemirror versions

然后二分尝试。
5.3x 是最新版但是有上述问题。
3.11.x 的无法使用,缺少某个函数。版本过低
5.10 的可以用,结果图片被识别成链接。。而且缩进有问题
5.20.0 没问题。但是会爆出一些overlay的错误。我魔改了一下。

于是选定 5.20.0

还依赖marked和mathjax。这些没问题最新版就可以了。

拖拽和粘贴

完工。

粘贴

原来是有的,但是并不能用。稍微改了一下。
相关代码在 /hypermd/addon/paste-image.js

Paste.prototype.pasteHandle = function (cm, ev) {
  var cd = ev.clipboardData || window.clipboardData;
  if (!cd || !cd.items || 1 != cd.items.length || cd.types[0] != 'Files') return;

  this.doInsert(cd.items[0].getAsFile());

  ev.preventDefault();
}

在chrome下不知为何,粘贴文件是无法从 clipboardData.files 里拿到file的。但是如果粘贴的是剪贴板里的图片数据却可以通过 clipboardData.items[0].getAsFile() 拿到值。所以文件是无法粘贴的,但是可以粘贴个截图什么的还是比较好用。

测试:

拖拽

原来是不支持的。我也加在 paste-image.js 里了。
首先添加挂载\卸载的事件。

Paste.prototype.bind = function () {
  this.cm.on('paste', this._pasteHandle);
  this.cm.on('drop', this._dropHandle);		//add
}

Paste.prototype.unbind = function () {
  this.cm.off('paste', this._pasteHandle);
  this.cm.off('drop', this._dropHandle);	//add
}
function Paste(cm) {
  this.cm = cm;
  this.enabled = false;
  this.uploadTo = 'sm.ms';

  this._pasteHandle = this.pasteHandle.bind(this);
  this._dropHandle = this.dropHandle.bind(this);	//add
  this.updateUploader(this.uploadTo);
}

然后是正文:

/**
 * 'drop' event handler
 *
 * @param ???
 */
Paste.prototype.dropHandle = function (cm, ev) {
  //console.log('dropped');
  if(!ev || !ev.dataTransfer || !ev.dataTransfer.files.length) return;
  var self = this, file = ev.dataTransfer.files[0];

  setTimeout(function() {
  	self.doInsert(file)
  }, 300);

  //ev.preventDefault();
}

可以看到,拖拽的文件是通过 event.dataTransfer.files 传入的。

这里有个有意思的地方。注册的drop事件是在光标移动之前进行的。所以如果直接 doInsert 或者最后来个 ev.preventDefault(); ,倒是能插入图片,但是插入的位置就错了——插入到了拖拽前光标所在的位置(而不是拖拽时指向的位置)。

所以选择一个很笨但是很有效的办法,就是延时解决。还不能延时0ms,那样好像不够。300ms根本觉察不出来。

测试:

好像动态gif会有显示问题。原因或许是多次的重复渲染导致频繁从头开始。

匹配bug和输入跳动bug

甚至直接用demo文章就能体现问题。

***Note**: This complex approach is temporary. but don't worry,*

找到这一行,鼠标点点,发现什么了...

恐怕深在markdown解析器内部——三个 *** 同时出现,则分两种情况,得看strong的标签在前还是在后。可是这个解释器直接按照在前处理了。所以弃。

但是还是改得好看了一点——

  1. 原来是往左往右搜到第一个具有classList包含关系的标签停止。现在我加了一个栈,预先走一遍看文本所在的最深的标签类型,然后搜的时候只盯着那个类型跑。
  2. 原来的方案中,在一行的末尾用输入法输入文字时,这一行里的标签会忽闪忽闪的,十分不友好。于是加了一个挺长的判断,大意是如果是** * ~~ 以及反引号这些特殊的标签,必须满足text具备它们所包含的属性。

这些改动都在 hide-token.js 里面的函数 patchFormattingSpan里。比较长。

其他

原来的加载过程是先拉一个index.js再用require.js异步加载模块。而模块。。估计有好几十个js小文件。于是本地加载都很慢。

所以写了一个合并js并自动压缩的程序。按照官方说明的加载顺序直接把所有js拼成一个文件,md.js,直接引用就可以加载所有HyperMD所需的脚本。直接拼起来有1.51M,自动调用下uglify.js压缩一下,然后大概是772K。基本满足要求了。

所以呢?

现在还有些光标不对应的奇怪bug。
虽然还是很喜欢它的latex公式功能...
如果有机会就写一个用它当编辑器的blog把它嵌进ghost博客里吧。