9

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 函数下面:

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的在线编辑器吧。


樊潇洁
415 声望23 粉丝

笨鸟先飞