http://segmentfault.com/ 怎么老是莫名其妙地挂掉?网页会莫名其妙地打不开。
好吧,我且不说这事了。今天我花了一天时间在改造一个Markdown 在线编辑器,终于把它改造得满符合我的想法了。哈哈,好有成就感。我曾经在网上试用了很多markdown在线编辑器,发现绝大部分都有一个毛病:在输入框里敲下Tab键,它不是自动插入一个tab制表符,而是焦点自动跳到下一个链接处了。这对经常要写代码的我简直是抓狂。好在我终于找到了一个个在线编辑器 http://lab.lepture.com/editor/ ,它对Tab键的处理恰好好处。但是我觉得还不够完美,于是自己动手改造它。
先说下我做了点什么修改,看图:
我加了一几个按钮:插入视频,插入音乐,插入代码,并为它们一一分配了快捷键。并且还为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 函数下面:
javascript
function 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
,改成这样子:
javascript
var 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函数需要改成这样:
javascript
function 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; }
我觉得工具栏中没有按钮提示很不好。于是改改改~,改成下面这样:
javascript
var 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的时候,前面的星号去掉了,后面的星号却没变化。我仔细看,发现原来的代码中正则表达式写错了。
我修改了 toggleBold
和 toggleItalic
函数,现在总算正常了。
javascript
function 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的在线编辑器吧。