改造了一個Markdown在線編輯器,如今它終於讓我感到完美了!

http://segmentfault.com/ 怎麼總是莫名其妙地掛掉?網頁會莫名其妙地打不開。
好吧,我且不說這事了。今天我花了一天時間在改造一個Markdown 在線編輯器,終於把它改造得滿符合個人想法了。哈哈,好有成就感。我曾經在網上試用了不少markdown在線編輯器,發現絕大部分都有一個毛病:在輸入框裏敲下Tab鍵,它不是自動插入一個tab製表符,而是焦點自動跳到下一個連接處了。這對常常要寫代碼的我簡直是抓狂。好在我終於找到了一個在線編輯器 http://lab.lepture.com/editor/,它對Tab鍵的處理剛好好處。可是我以爲還不夠完美,因而本身動手改造它。
先說下我作了點什麼修改,看圖:
圖片描述
我加了一幾個按鈕:插入視頻,插入音樂,插入代碼,併爲它們一一分配了快捷鍵。而且還爲Ctrl+S分配了快速提交功能。
其次是粘貼功能,這是我今天改造的重頭戲。我以爲把網頁上的內容粘貼到這個在線編輯器裏,還得手工把它修改爲Markdown代碼,太費事了。因而但願可以自動完成。另外,上傳圖片,原本它是沒有圖片上傳功能的,只能手工輸入圖片地址。費事啊!我也把這個功能集成到粘貼功能裏了。
首先,須要在在線編輯器中綁定onPaste事件。
我看到那個editor.js中第1580行中有onKeyPress事件綁定。我先給它加了一個onPaste事件綁定。javascript

javascripton(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的功能。因而寫了這麼一個函數。php

javascriptfunction 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函數,並加了一個圖片上傳功能。html

javascriptfunction 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的源碼是:java

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,改爲這樣子:segmentfault

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函數,如今總算正常了。markdown

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的在線編輯器吧。app

相關文章
相關標籤/搜索