メモを揉め

お勉強の覚書。

javascriptでjavascriptのシンタックスハイライト

  • このブログにコードを載せる際のシンタックスハイライトの為に作った。
  • 基本的にはキーワードを正規表現で拾ってきて<span>タグで囲むだけのもの。
  • 正規表現リテラルと文字列リテラルは複雑になったので分けて処理。
  • 結局思い通りに動くまで2日か3日ぐらいかかった(それから更に修正してもっとかかった)、まだきっと不具合ありそうだけどとりあえず使えるようにはなった。
  • 正規表現を多用したので少し慣れた。

MarkupJS.js デモページ

処理の流れ

  1. &;<>をエスケープシーケンスに変換。
  2. 正規表現リテラル正規表現で取得して<span>タグで囲んだものに置換。
  3. キーワード、演算子、括弧、コメントアウト正規表現で取得して、<span>タグで囲んだものに置換。
  4. シングルクォート、ダブルクォートのインデックスを正規表現で取得し、その位置で全文を分割。構文を判断しながら<span>タグを挿入する。
  5. <pre><code>タグで囲んで出力。

ブログ側CSSの設定

 

pre {
    color: #cfbfad;
    background-color: #272822;
}

.Coloring_keyword {
    color: #ff007f;
}

.Coloring_operator {
    color: #ff007f;
}

.Coloring_bracket {
    color: #ffffff;
}

.Coloring_comment {
    color: #ffffff;
}

.Coloring_regexp {
    color: #ff8d05;
}

.Coloring_strings {
    color: #ece461;
}

.Coloring_functionName {
    color: #a7ec21;
}

.Coloring_color {
    color: #0cff00;
}

 

&;<>をエスケープシーケンスに変換

  • htmlのタグとして認識されないように、<>を変換。
  • &;はエスケープシーケンス自体に含まれるのでjavascriptのものと区別するため。

 

function replaceTag(codeStr) {
  return codeStr.replace(/[;&<>]/gm, replacer);

  function replacer(s) {
    var esc = {
        ";" : "&#059;",
        "&" : "&amp;",
        "<" : "&lt;",
        ">" : "&gt;"
    };
    return esc[s];
  }
}

正規表現リテラルマークアップ

  • /を算術演算子と区別するため、左辺は括弧の内側もしくは行頭の場合、右辺は括弧の外側ではない場合にマッチするように指定。大体の場合これでマッチするのでとりあえず良しとする。
  • <span>タグにはクラスとして"Coloring_regexp"を付ける。

 

function coloringRegExp (codeStr) {
  var regReg = /([{(=[]\s*|^\s*)(\/(?!\/).+\/[gmi]{0,3})(\s*)(?![([{])/gm;
  return codeStr.replace(regReg, "$1<span class=\"Coloring_regexp\">$2</span>$3");
}

キーワード、演算子、括弧、コメントアウトマークアップ

  • 各キーワードにマッチする正規表現の文字列を配列に入れる、色ごとに配列を分割して変数に代入。
  • 関数宣言、名前付き関数式の関数名はfunctionもマッチしてしまうのでやむなく別の正規表現に分けた。
  • 色に関係なくすべてのマッチ部分を<span>タグで囲みクラス"Coloring_color"を付ける。
  • 色ごとに<span>タグも含めてマッチする正規表現を作成し、"color"の部分をそれぞれの色に書き換える。
  • 正規表現リテラルはキーワードに入っているが、すでにマークアップされているので"Coloring_color"クラスの<span>タグを外す。
    正規表現リテラル内の様々な記号等にマッチしないようにするための回避策。

 

function coloring (codeStr) {
  var keyword = [
        "\\bbreak\\b",
        "\\bcase\\b",
        "\\bconst\\b",
        "\\bcontinue\\b",
        "\\bdebugger\\b",
        "\\bdelete\\b",
        "\\bdo\\b",
        "\\bwhile\\b",
        "\\bexport\\b",
        "\\bfor\\b",
        "\\bin\\b",
        "\\bfunction\\b",
        "\\bif\\b",
        "\\binstanceof\\b",
        "\\belse\\b",
        "\\breturn\\b",
        "\\bswitch\\b",
        "\\bthrow\\b",
        "\\bthis\\b",
        "\\btry\\b",
        "\\btypeof\\b",
        "\\bundefined\\b",
        "\\bcatch\\b",
        "\\bvar\\b",
        "\\bvoid\\b",
        "\\bwith\\b",
        "\\btrue\\b",
        "\\bfalse\\b",
        "\\bnull\\b",
        "\\bNaN\\b"
      ],
      operator = [
        "=(?!\"Coloring_|=)",
        "\\+=",
        "-=",
        "\\*=",
        "/=",
        "&=",
        "\\^=",
        "\\|=",
        "%=",
        "(?:<)+=",
        "(?:>)+=",
        "\\++",
        "-+",
        "/(?!/|\\*|span>)",
        "\\*(?!/)",
        "%",
        "\\.",
        "\\,",
        "\\?",
        "\\^",
        "\\|+",
        "(?:&)+",
        "~",
        "(?:<)+",
        "(?:>)+",
        "\\!",
        "!=+",
        "=={1,2}",
        ":",
        ";"
      ],
      bracket = [
        "\\(",
        "\\)",
        "\\{",
        "\\}",
        "\\[",
        "\\]"
      ],
      comment = [
        "//.*(?=$|</span>)",
        "/\\*(?:.*\n)*?.*?\\*/"
      ],
      regexp = [
        "<span class=\"Coloring_regexp\">/.+/[gmi]{0,3}\\s*</span>"
      ],
      syntax = keyword.concat(operator, bracket, comment, regexp)
  ;

  var functionNameReg = /(function +)([\w$]+)( *\()/gm,
      colorReg    = new RegExp(                               "(" +   syntax.join("|") + ")"       , "gm"),
      keywordReg  = new RegExp("<span class=\"Coloring_color\">(" +  keyword.join("|") + ")</span>", "gm"),
      operatorReg = new RegExp("<span class=\"Coloring_color\">(" + operator.join("|") + ")</span>", "gm"),
      bracketReg  = new RegExp("<span class=\"Coloring_color\">(" +  bracket.join("|") + ")</span>", "gm"),
      commentReg  = new RegExp("<span class=\"Coloring_color\">(" +  comment.join("|") + ")</span>", "gm"),
      regexpReg   = new RegExp("<span class=\"Coloring_color\">(" +   regexp.join("|") + ")</span>", "gm");

  return codeStr.replace(functionNameReg, '$1<span class="Coloring_functionName">$2</span>$3')
                .replace(colorReg       , '<span class="Coloring_color">$1</span>'           )
                .replace(keywordReg     , '<span class="Coloring_keyword">$1</span>'         )
                .replace(operatorReg    , '<span class="Coloring_operator">$1</span>'        )
                .replace(bracketReg     , '<span class="Coloring_bracket">$1</span>'         )
                .replace(commentReg     , '<span class="Coloring_comment">$1</span>'         )
                .replace(regexpReg      , '$1'                                               )
  ;
}

文字列リテラルマークアップ

  • "'\n///**/"regexp"クラスを持つ<span>タグ、を正規表現で検索し、文字列リテラルと判断した箇所のインデックスを配列に記録する。
  • 記録したインデックスの位置で全文を分割し文字列リテラル<span>タグで囲む、同時に文字列リテラル内の<span>タグを外す。

 

function coloringQuote(codeStr) {

  var quoteReg = /(?:<span class=|<span class="Coloring_\w+)?(?:\\?["'\n]|\/\/|\/\*|\*\/|<span class="Coloring_regexp">|<\/span>)(?:Coloring_\w+">)?/gm,
      deleteMarkReg = /<span class="Coloring_\w+">(.+?)<\/span>/g,
      beginLnCmntReg = /<span class="Coloring_comment">\/\//g,
      endLnCmntReg = /<\/span>((?:\n.*)+)/gm,
      beginMltLnCmntReg = /<span class="Coloring_comment">\/\*/g,
      endMltLnCmntReg = /\*\/<\/span>/gm,
      arr = [],
      openCloseIndex = [],
      status = {
        isOpened : false,
        isCommented : false,
        isRegExp : false,
        charOfBegin : "",
        isLnCmntInQuote : false,
        isMltLnCmntInQuote : false
      },
      markupStr = "",
      splitStr = "",
      reColorStr = "",
      colorStr = "",
      from,
      to,
      c,
      i;

  while (arr = quoteReg.exec(codeStr)) {
    c = arr[0];
    if (status.isCommented) {
      if (status.charOfBegin === "//" && c === "\n") {
        status.isCommented = false;
      } else if (status.charOfBegin === "/*" && c === "*/") {
        status.isCommented = false;
      }
    } else if (status.isOpened) {
      if (c === status.charOfBegin || c === "\n") {
        openCloseIndex.push(arr.index + (c === status.charOfBegin));
        status.isOpened = false;
      }
    } else if (status.isRegExp) {
      if (c === "</span>") {
        status.isRegExp = false;
      }
    } else {
      if (c === "//" || c === "/*") {
        status.isCommented = true;
        status.charOfBegin = c;
      } else if (c === "\n") {
        continue;
      } else if (c === "\\\"" || c === "\\'") {
        continue;
      } else if (c === "<span class=\"Coloring_regexp\">") {
        status.isRegExp = true;
      } else if (c.length > 1) {
        continue;
      } else {
        openCloseIndex.push(arr.index);
        status.charOfBegin = c;
        status.isOpened = true;
      }
    }
  }

  for (i = from = 0; i < openCloseIndex.length + 1; i++) {
    to = openCloseIndex[i];
    splitStr = codeStr.slice(from, to);
    if (i & 1) {
      splitStr = "<span class=\"Coloring_strings\">" + splitStr.replace(deleteMarkReg, "$1") + "</span>";
      if (!status.isMltLnCmntInQuote && beginLnCmntReg.test(splitStr)) {
        splitStr = splitStr.replace(beginLnCmntReg, "//");
        status.isLnCmntInQuote = true;
      } else if (!status.isLnCmntInQuote && beginMltLnCmntReg.test(splitStr)) {
        splitStr = splitStr.replace(beginMltLnCmntReg, "/*");
        status.isMltLnCmntInQuote = true;
      }
    }
    if (status.isLnCmntInQuote) {
      if (endLnCmntReg.test(splitStr)) {
        splitStr.replace(endLnCmntReg, function (matchStr, p1, offset, s) {
          reColorStr = s.slice(0, offset);
          colorStr = p1;
        });
        status.isLnCmntInQuote = false;
      } else {
        reColorStr = splitStr;
        colorStr = "";
      }
      if (i + 1 & 1) {
        reColorStr = coloring(reColorStr);
      }
      splitStr = reColorStr + colorStr ;
    } else if (status.isMltLnCmntInQuote) {
      if (endMltLnCmntReg.test(splitStr)) {
        splitStr = splitStr.replace(endMltLnCmntReg, "*/");
        status.isMltLnCmntInQuote = false;
      }
      if (i + 1 & 1) {
        splitStr = coloring(splitStr);
      }
    }
    markupStr += splitStr;
    from = to;
  }

  return markupStr;
}

最後に<pre><code>で囲んで出力して終了

思ったこと

  • 探してみたら便利なツールがすでにたくさんある。
  • でも正規表現をたくさん使えてよかった、先読みとかすごい便利。
  • string.replaceの第二引数で関数を使うのも初めてだったけどめっちゃ便利。