转载

改造了一个Markdown在线编辑器,现在它终于让我感到完美了!

http://segmentfault.com/ 怎么老是莫名其妙地挂掉?网页会莫名其妙地打不开。

好吧,我且不说这事了。今天我花了一天时间在改造一个Markdown 在线编辑器,终于把它改造得满符合我的想法了。哈哈,好有成就感。我曾经在网上试用了很多markdown在线编辑器,发现绝大部分都有一个毛病:在输入框里敲下Tab键,它不是自动插入一个tab制表符,而是焦点自动跳到下一个链接处了。这对经常要写代码的我简直是抓狂。好在我终于找到了一个个在线编辑器 http://lab.lepture.com/editor/ ,它对Tab键的处理恰好好处。但是我觉得还不够完美,于是自己动手改造它。

先说下我做了点什么修改,看图:

改造了一个Markdown在线编辑器,现在它终于让我感到完美了!

我加了一几个按钮:插入视频,插入音乐,插入代码,并为它们一一分配了快捷键。并且还为Ctrl+S分配了快速提交功能。

其次是粘贴功能,这是我今天改造的重头戏。我觉得把网页上的内容粘贴到这个在线编辑器里,还得手工把它修改成Markdown代码,太费事了。于是希望能够自动完成。另外,上传图片,本来它是没有图片上传功能的,只能手工输入图片地址。费事啊!我也把这个功能集成到粘贴功能里了。

首先,需要在在线编辑器中绑定 onPaste 事件。

我看到那个editor.js中第1580行中有 onKeyPress 事件绑定。我先给它加了一个 onPaste 事件绑定。

javascript on(d.input, "input", bind(fastPoll, cm));  on(d.input, "keydown", operation(cm, onKeyDown));  on(d.input, "keypress", operation(cm, onKeyPress));  on(d.input, "paste", operation(cm,onPaste)); // 这句是我添加的  on(d.input, "focus", bind(onFocus, cm));  on(d.input, "blur", bind(onBlur, cm));  

然后 ,需要写一个 onPaste 函数。我把它写在onKeyPress函数后面。

我先是想实现在粘贴时自动把HTML代码转换成Markdown的功能。于是写了这么一个函数。

javascript   function onPaste(e){  if(!e.clipboardData)return true;  //IE浏览器不支持e.clipboardData对象,无奈  if(e.clipboardData.types=='text/plain')return true;  // 如果剪贴板中的内容是纯文本内容,直接粘贴。  else if(e.clipboardData.types=='text/plain,text/html'){  // 如果剪贴板中的内容是HTML内容,则需要对它进行一番改造  var html=e.clipboardData.getData('text/html');  html=html.replace(/<html>(/r?/n)+<body>(/r?/n)+<!--StartFragment-->(.*?)<!--EndFragment-->(/r?/n)+<//body>(/r?/n)+<//html>/,"$3");  html=toMarkdown(html);  // toMarkdown函数 http://segmentfault.com/a/1190000002723901 在这里已经写了  var cm=this;   _replaceSelection(cm, false, html,'');  e.preventDefault();  }      }  

这里有一个很详细的剪贴板js原生对象的介绍: http://wizard.ae.krakow.pl/~jb/localio.html

本来这样算是大功告成了,但是我又觉得还有点不甘心,因为我希望以后粘贴图片方便点。

于是我继续修改这个 onPaste 函数,并加了一个图片上传功能。

javascript   function onPaste(e){  if(!e.clipboardData)return true;  if(e.clipboardData.types=='text/plain')return true;  else if(e.clipboardData.types=='text/plain,text/html'){  var html=e.clipboardData.getData('text/html');  html=html.replace(/<html>(/r?/n)+<body>(/r?/n)+<!--StartFragment-->(.*?)<!--EndFragment-->(/r?/n)+<//body>(/r?/n)+<//html>/,"$3");  html=toMarkdown(html);  var cm=this;   _replaceSelection(cm, false, html,'');  e.preventDefault();  }  else if(e.clipboardData.types=='text/html,Files'){   imgReader(e.clipboardData.items[1])      e.preventDefault();      }   else if(e.clipboardData.types=='Files'){      imgReader(e.clipboardData.items[0])   }       }   function imgReader(item){       if(item.kind=='file'&&item.type=='image/png'){       var file = item.getAsFile(),reader = new FileReader();       reader.onload = function( e ){   var img = new Image();   img.src = e.target.result;   document.body.appendChild( img );   // 把图片放在网页最下面,以便预览   $.post('saveremoteimg.php',{'urls':e.target.result},function(data){       _replaceSelection(editor.codemirror,false , '![', ']('+data+')/n');       })   };     reader.readAsDataURL(file);     } };  

saveremoteimg.php的源码是:

php<?php header('Content-Type: text/html; charset=UTF-8'); $attachDir='upload';//上传文件保存路径,结尾不要带/ $dirType=1;//1:按天存入目录 2:按月存入目录 3:按扩展名存目录  建议使用按天存 $maxAttachSize=2097152;//最大上传大小,默认是2M $upExt="jpg,jpeg,gif,png";//上传扩展名 ini_set('date.timezone','Asia/Shanghai');//时区 //保存远程文件 function saveRemoteImg($sUrl){  global $upExt,$maxAttachSize;  $reExt='('.str_replace(',','|',$upExt).')';  if(substr($sUrl,0,10)=='data:image'){//base64编码的图片,可能出现在firefox粘贴,或者某些网站上,例如google图片   if(!preg_match('/^data:image//'.$reExt.'/i',$sUrl,$sExt))return false;   $sExt=$sExt[1];   $imgContent=base64_decode(substr($sUrl,strpos($sUrl,'base64,')+7));  }  else{//url图片   if(!preg_match('//.'.$reExt.'$/i',$sUrl,$sExt))return false;   $sExt=$sExt[1];   $imgContent=getUrl($sUrl);  }  if(strlen($imgContent)>$maxAttachSize)return false;//文件体积超过最大限制  $sLocalFile=getLocalPath($sExt);  file_put_contents($sLocalFile,$imgContent);  //检查mime是否为图片,需要php.ini中开启gd2扩展  $fileinfo= @getimagesize($sLocalFile);  if(!$fileinfo||!preg_match("/image//".$reExt."/i",$fileinfo['mime'])){   @unlink($sLocalFile);   return false;  }  return $sLocalFile; } //抓URL数据 function getUrl($sUrl,$jumpNums=0){  $arrUrl = parse_url(trim($sUrl));  if(!$arrUrl)return false;  $host=$arrUrl['host'];  $port=isset($arrUrl['port'])?$arrUrl['port']:80;  $path=$arrUrl['path'].(isset($arrUrl['query'])?"?".$arrUrl['query']:"");  $fp = @fsockopen($host,$port,$errno, $errstr, 30);  if(!$fp)return false;  $output="GET $path HTTP/1.0/r/nHost: $host/r/nReferer: $sUrl/r/nConnection: close/r/n/r/n";  stream_set_timeout($fp, 60);  @fputs($fp,$output);  $Content='';  while(!feof($fp))  {   $buffer = fgets($fp, 4096);   $info = stream_get_meta_data($fp);   if($info['timed_out'])return false;   $Content.=$buffer;  }  @fclose($fp);  global $jumpCount;//重定向  if(preg_match("/^HTTP///d./d (301|302)/is",$Content)&&$jumpNums<5)  {   if(preg_match("/Location:(.*?)/r/n/is",$Content,$murl))return getUrl($murl[1],$jumpNums+1);  }  if(!preg_match("/^HTTP///d./d 200/is", $Content))return false;  $Content=explode("/r/n/r/n",$Content,2);  $Content=$Content[1];  if($Content)return $Content;  else return false; } //创建并返回本地文件路径 function getLocalPath($sExt){  global $dirType,$attachDir;  switch($dirType)  {   case 1: $attachSubDir = 'day_'.date('ymd'); break;   case 2: $attachSubDir = 'month_'.date('ym'); break;   case 3: $attachSubDir = 'ext_'.$sExt; break;  }  $newAttachDir = $attachDir.'/'.$attachSubDir;  if(!is_dir($newAttachDir))  {   @mkdir($newAttachDir, 0777);   @fclose(fopen($newAttachDir.'/index.htm', 'w'));  }  PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);  $newFilename=date("YmdHis").mt_rand(1000,9999).'.'.$sExt;  $targetPath = $newAttachDir.'/'.$newFilename;  return $targetPath; } $arrUrls=explode('|',$_POST['urls']); $urlCount=count($arrUrls); for($i=0;$i<$urlCount;$i++){  $localUrl=saveRemoteImg($arrUrls[$i]);  if($localUrl)$arrUrls[$i]=$localUrl; } echo implode('|',$arrUrls); ?>  

想一想觉得还有点想改造的。在行内插入代码是需要在文字左右加两个点(键盘上Tab键上方的那个键),但是我发现在中文输入法中,它是自动打出·的,需要切换到英文输入状态才能打出想要的那个点。多敲一次键盘对我来说都是抓狂。我必须继续改造它,让它能像切换粗体或斜体那样用快捷键来实现。这倒好办。再写一个 toggleCode函数,添加在toggleItalic 函数下面:

javascriptfunction toggleCode(editor) {   var cm = editor.codemirror;   var stat = getState(cm);    var text;   var start = '`';   var end = '`';    var startPoint = cm.getCursor('start');   var endPoint = cm.getCursor('end');   if (stat.code) {     text = cm.getLine(startPoint.line);     start = text.slice(0, startPoint.ch);     end = text.slice(startPoint.ch);     start = start.replace(/^(.*)?(`)(/S+.*)?$/, '$1$3');     end = end.replace('`','');     startPoint.ch -= 1;     endPoint.ch -= 1;     cm.setLine(startPoint.line, start + end);   } else {     text = cm.getSelection();     cm.replaceSelection(start + text + end);      startPoint.ch += 1;     endPoint.ch += 1;   }   cm.setSelection(startPoint, endPoint);   cm.focus(); } 

然后在shortcuts数组中添加一项 'Cmd-Y': toggleCode ,改成这样子:

javascriptvar shortcuts = {   'Cmd-B': toggleBold,   'Cmd-I': toggleItalic,   'Cmd-Y': toggleCode,  // 这项是我加的   'Cmd-K': drawLink,   'Cmd-Alt-I': drawImage,   'Cmd-Q': drawCode, // 这项也是我加入的   'Cmd-/'': toggleBlockquote,   'Cmd-Alt-L': toggleOrderedList,   'Cmd-L': toggleUnOrderedList,   'Cmd-P': togglePreview }; 

与此同时,getStatus函数需要改成这样:

javascriptfunction getState(cm, pos) {   pos = pos || cm.getCursor('start');   var stat = cm.getTokenAt(pos);   if (!stat.type) return {};    var types = stat.type.split(' ');    var ret = {}, data, text;   for (var i = 0; i < types.length; i++) {     data = types[i];     if (data === 'strong') {       ret.bold = true;     } else if (data === 'variable-2') {       text = cm.getLine(pos.line);       if (/^/s*/d+/./s/.test(text)) {         ret['ordered-list'] = true;       } else {         ret['unordered-list'] = true;       }     } else if (data === 'atom') {       ret.quote = true;     } else if (data === 'comment'){ // 这句是我加上去的       ret.code = true;   // 这句也是我加上去的     } else if (data === 'em') {       ret.italic = true;     }   }   return ret; } 

我觉得工具栏中没有按钮提示很不好。于是改改改~,改成下面这样:

javascriptvar toolbar = [   {name: 'bold', action: toggleBold, shortcut:'Toggle Bold(Cmd-B)'},   {name: 'italic', action: toggleItalic, shortcut:'Toggle Italic(Cmd-I)'},   '|',    {name: 'quote', action: toggleBlockquote, shortcut: 'toggle Blockquote(Cmd-/')'},   {name: 'unordered-list', action: toggleUnOrderedList, shortcut:'Toggle UnorderList(Cmd-Alt-L)'},   {name: 'ordered-list', action: toggleOrderedList, shortcut:'Toggle OrderList(Cmd-L)'},   '|',    {name: 'link', action: drawLink, shortcut:'Insert Link(Cmd-K)'},   {name: 'image', action: drawImage, shortcut: 'Insert Image(Cmd-Alt-I)'},   {name: 'play', action: drawVideo, shortcut: 'Insert Video'},   {name: 'music', action: drawAudio, shortcut: 'Insert Audio'},   {name: 'code', action: drawCode, shortcut: 'Insert Code(Cmd-Q)'},   '|',    {name: 'info', action: 'http://lab.lepture.com/editor/markdown'},   {name: 'preview', action: togglePreview, shortcut: 'Toggle Preview'},   {name: 'fullscreen', action: toggleFullScreen, shortcut: 'Toggle FullScreen'} ];  

其实我发现原来的程序里有个小bug,就是用Ctrl+B或者Ctrl+I切换粗体、斜体的时候,第一次按Ctrl+B,会在选中块去的前后各加两个星号,而第二次按Ctrl+B的时候,前面的星号去掉了,后面的星号却没变化。我仔细看,发现原来的代码中正则表达式写错了。

我修改了 toggleBoldtoggleItalic 函数,现在总算正常了。

javascriptfunction toggleBold(editor) {   var cm = editor.codemirror;   var stat = getState(cm);    var text;   var start = '**';   var end = '**';    var startPoint = cm.getCursor('start');   var endPoint = cm.getCursor('end');   if (stat.bold) {     text = cm.getLine(startPoint.line);     start = text.slice(0, startPoint.ch);     end = text.slice(startPoint.ch);      start = start.replace(/^(.*)?(/*|/_){2}(/S+.*)?$/, '$1$3');     end = end.replace(/(/*|/_){2}/, '');// 这句是我修改过的     startPoint.ch -= 2;     endPoint.ch -= 2;     cm.setLine(startPoint.line, start + end);   } else {     text = cm.getSelection();     cm.replaceSelection(start + text + end);      startPoint.ch += 2;     endPoint.ch += 2;   }   cm.setSelection(startPoint, endPoint);   cm.focus(); }  function toggleItalic(editor) {   var cm = editor.codemirror;   var stat = getState(cm);    var text;   var start = '*';   var end = '*';    var startPoint = cm.getCursor('start');   var endPoint = cm.getCursor('end');   if (stat.italic) {     text = cm.getLine(startPoint.line);     start = text.slice(0, startPoint.ch);     end = text.slice(startPoint.ch);      start = start.replace(/^(.*)?(/*|/_)(/S+.*)?$/, '$1$3');     end = end.replace(/(/*|/_)/, ''); // 这句是我修改过的     startPoint.ch -= 1;     endPoint.ch -= 1;     cm.setLine(startPoint.line, start + end);   } else {     text = cm.getSelection();     cm.replaceSelection(start + text + end);      startPoint.ch += 1;     endPoint.ch += 1;   }   cm.setSelection(startPoint, endPoint);   cm.focus(); } 

现在很疲惫,不过总算改得令自己满意了。掌柜的站长也改进一下segmentfault.com的在线编辑器吧。

正文到此结束
Loading...