閱讀939 返回首頁    go 微軟 go Office


利用正則實現彩色控製台輸出

最近忙了一陣less的二次開發的工作,期間遇到了不少需要向控製台輸出彩色文字的需求。翻了下以前同事的代碼,發現要麼自己拚轉義字符串,要麼使用一些不太好用的第三方庫,總之都不是很合自己的口味。按照自己的口味,一個好的第三方庫應該滿足如下需求: 要支持豐富的顏色設置,同時設置顏色又不能太累贅,而且要支持console.log的通配符表示方法以減少拚字符串的工作。cli-color和colors的語法類似,都是采用方法來設定字符串顏色:

// colors
console.log("this is an error".error);

// cli-color 
console.log(clc.red('red') + ' plain ' + clc.blue('blue'));

如果字符串中的顏色比較多,而且字符串中還包含動態數據,那麼就需要大量的拚字符串的工作,醜陋且容易出錯,因此這種坑爹的方案直接忽略。

既然這些現成的庫不好用,幹脆就自己寫一個。作為一個前端平時自己接觸的最多的是html,受html語法的啟發,打算采用標簽的形式來設置字符顏色,而不是采用方法的形式。比如要輸出紅綠兩種顏色的文本,可以采用如下方式: Foo.log('<red>red color <green>green color</green></red>')。這種方式有兩個優點: 第一,便於表現豐富的顏色,尤其是顏色嵌套的情況,如果采用cli-color那種方式來表現多個顏色的嵌套,估計拚字符串會讓你想吐;第二,省去了記憶方法名和拚字符串。

那麼這種設計是否容易實現呢? 在回答這個問題之前我們先簡單說一下實現彩色輸出的原理。向控製台輸出彩色文字主要利用了ansi 中的轉義字符(ANSI轉義字符表)。眾多的轉義字符中有一部分是設置控製台的渲染方式的,其中輸出控製采用如下語法來聲明:\x1b[nmx1b的值是27,在ASC碼表中表示轉義字符,後麵的[nm是模式設置,[m是常量,n為變量。通過設置N的值可以實現不同的輸出設置,下麵為常用的N值

  • 0 開始以暗色模式顯示文本, 文字顏色為用戶設置的控製台默認顏色,一般為白色
  • 1 開始以亮色模式顯示文本, 文字顏色為用戶設置的控製台默認顏色,一般為白色
  • 30 ~ 27 分別以black、red、green、 yellow、blue、pink、cyan、white顏色來顯示文本

需要特別注意的一點:過這些輸出設置不僅對本次輸出起效,而且將一直起效,直至遇到新的設置或控製台退出! 所以在使用的時候一定要記得重置顏色設置,免得影響後麵的控製台輸出。我們通過下麵的demo來檢驗下這些轉義字符的功能。注意劃紅線的語句部分,雖然這條語句中沒有對輸出進行任何設置,但因為上一條命令中設置了控製台顏色,所以這次輸出依然采用的上次的設置。
控製台輸出

敘述了這麼多,終於可以回答上麵的那個問題了:那麼這種設計是否容易實現呢?答案是: 很簡單。我們僅僅需要用這則處理3類字符串就可以了: 轉義字符、顏色開始tag,顏色結束tag。處理策略也很簡單:

  • 遇到由斜線開始的轉義字符, 直接返回斜線後麵的字符
  • 遇到開始Tag, 查看是否為支持的顏色,若不支持,不做處理原樣返回;若支持,返回tag對應到 顏色轉義字符,並將該顏色轉義字符壓棧。
  • 遇到結束Tag, 查看是否是支持的顏色,若不支持,不做處理原樣返回;若支持,彈棧並返回棧頂顏色對應的轉義字符,若棧為空,則設置為係統默認顏色。
  • 其他情況一律不做處理,原樣返回(這一步主要是預防自己沒有預料的一些匹配出現,這個例子中應該用不到,為了保險起見還是留著吧)。
  • 為了防止用戶標簽沒有閉合而影響其他控製台輸出,在最後預防行的設置顏色為默認顏色。

這部分的邏輯已經封並發布到了npm的rich-console模塊,下麵為具體的實現代碼和demo運行結果截圖。順便說一下ANSI中支持的轉義內容還很多比如設置背景色、設置加粗、下劃線等,但這些支持的並不好,Windows下很多都不支持,再加上這些功能更用的比較少,因此這些功能被有意忽略了。
demo效果

/**
 * 獲得帶顏色轉義字符的控製台輸出模板.
 * @param  {String}tmpl        包含標簽的模板字符串
 * @param  {boolean}isBright   是否高亮,default false
 * @return {String}
 * @public
 */
function getRichTmpl(tmpl, isBright){
    if(typeof tmpl == 'object'){ return tmpl; }

    var fontStyle = isBright == true ? '\u001b[1m' : '';
    var ESCAPES  = {
        black  : (fontStyle + '\u001b[30m'),
        red    : (fontStyle + '\u001b[31m'),
        green  : (fontStyle + '\u001b[32m'),
        yellow : (fontStyle + '\u001b[33m'),
        orange : (fontStyle + '\u001b[33m'),
        blue   : (fontStyle + '\u001b[34m'),
        pink   : (fontStyle + '\u001b[35m'),
        cyan   : (fontStyle + '\u001b[36m'),
        white  : (fontStyle + '\u001b[37m'),
        noColor: '\u001b[0m'
    }    

    var NO_COLOR = ESCAPES.noColor;
    var styleStack = [];
    var reg = new RegExp((
         '(\\\\.)'     // 由\表示的轉義字符
       + '|<(\\w+)>'   // 樣式開始標簽
       + '|</(\\w+)>'  // 樣式結束標簽
    ), 'g');

    var handleTag = function(str){
        return str.replace(reg, function(m, $1, $2, $3){
            // 若是轉義字符之間返回\後麵的字符
            if ($1) { return $1.slice(1); }

            // 若為不支持的顏色直接忽略,否則返回樣式開始字符並將樣式壓棧
            if ($2) { 
                var style = ESCAPES[$2];
                if(style){
                    styleStack.push(style);
                    return style;
                }else{
                    return m;
                }
            }              

            // 若為不支持的顏色直接忽略,否則從樣式棧中彈出當前樣式並返回
            // 棧頂樣式,若棧為空返回係統默認樣式
            if ($3) {
                if(ESCAPES[$3]){
                    styleStack.pop();
                    var len = styleStack.length;
                    var topStyle = len > 0 ? styleStack[len - 1] : null;
                    return (topStyle ? topStyle : NO_COLOR);
                }else{
                    return m;
                }
            }

            // others 
            return m;
        }) + NO_COLOR; // 最末尾的兩個重置用來防止用戶標簽不閉合進而汙染整個控製台輸出
    };

    return handleTag(tmpl);
}

/**
 * 向控製台輸出彩色文字.
 * @param {String}cont
 * @example
 *   showColorText(
 *      '<red>%s <green>%s</green>! </red>', 
 *      'hello', 
 *      'wold'
 *   );
 * @public
 */
function output(cont){
    // 若用戶輸入的是一個object則調用係統的console輸出object結構
    if(typeof cont == 'object'){
        console.log(cont);
        return;
    }

    var moreArgs = [].slice.call(arguments, 1);
    moreArgs.unshift(getRichTmpl(cont));
    console.log.apply(console, moreArgs);
}

/**
 * 以紅色文字向控製台輸出錯誤信息.
 * @param  {String|Object}cont
 * @param  {Object...}
 * @public
 */
function outputError(cont){
    if(typeof cont == 'object'){
        console.log(cont);
    }else{
        var moreArgs = [].slice.call(arguments, 1);
        moreArgs.unshift('<red>' + cont + '</red>');
        output.apply(null, moreArgs);
    }
}

module.exports = {
    getRichTmpl: getRichTmpl,
    error: outputError,
    log: output
}

最後更新:2017-10-24 16:33:59

  上一篇:go  如何從seo的維度來選擇網站的關鍵詞
  下一篇:go  我的雲棲社區