フロントサイドエンジニアという選択肢

HTMLコーダー → ECサイト運営 → システムエンジニア という経歴の著者がフロントサイトエンジニアという職業に今後の活路を見出し、その道に進むために取得した技術を貯めておくブログ

ソースコードに自動でhtmlタグ(とCSSクラス)を割り当てるスクリプト その2

以前作成したスクリプトの不具合等を修正してjQueryモジュールにしてみました。せっかくなので今回はGitHubを使ってファイルを管理してみました。
ただ、はてなの無料アカウントではjQueryモジュールを使えないようなので、DOMのみで機能するバージョンも作成してみました。
GitHubではどちらのバージョンも公開しております。

ファイル置き場

https://github.com/jmqys/js_code_highlight がアドレスになります。

github.com

以前からの変更点
  • 一部、改行コードが消されてしまう現象があったので修正。
  • ソースをまるごと選択しようとすると、行番号まで入ってしまうので、ソースのみを選択できるように修正。
  • 関数の引数にも色分けを適用
  • /*~*/のコメントにも対応
使用例

こんなかんじになります。

// 全体の文字サイズ
var fontSize = 12;

// 全体の行の高さ
var lineHeight = 15;

var reservedWords = 
[
    'break', 'case', 'catch', 'continue', 'debugger', 'default',
    'delete', 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 
    'instanceof', 'new', 'return', 'switch' ,'document', 'body' ,'this',
    'var'
];

var set_jscode_highlight = function(elements, isUseLineNumbers, isEnableHtmlTags){
    if(elements.length > 0){
        for(var i=0; i<elements.length; i++){
            setHighlight(elements[i]);
        }
    }
    else{
        setHighlight(elements);
    }

    // 共通処理    
    function setHighlight(element){
        element.classList.add('jscode_highlight');
        var sourceText = new InnerSourceCode(
            element.innerHTML, 
            isEnableHtmlTags || false);
        element.innerHTML = sourceText.getHtml();
        
        setCss(element);
        
        if(isUseLineNumbers !== false){
            createLineNum(element);
        }
    }
}


// innerHTMLを管理するオブジェクト
function InnerSourceCode(innerHtml, isHtmlTagEnable)
{
    // 変数名を取得する
    var variableNames = getAllVariableNames(innerHtml);

    // コメント部分と本文の分割を行うオブジェクト
    var comment = new CommentSecluder(isHtmlTagEnable);
    
    // 一行ごとに処理を行うため、innerHTMLを一行づつ分けて配列にする
    if(isHtmlTagEnable) {
        innerHtml.replace(/
/,'\n'); } var _processingArray = innerHtml.split('\n'); // 関数名とその引数を取得 var functions = getAllFunctions(_processingArray); // マークアップ処理開始 for(var pn in _processingArray) { // コメントの処理、一行を本文とコメントに分ける // _processingArray[pn][0] = 本文 // _processingArray[pn][1] = コメント _processingArray[pn] = comment.seclude(_processingArray[pn]); if(comment.commentMode){ // /* ~ */の間は処理をしない continue; } // htmlタグの処理 if(!isHtmlTagEnable) { _processingArray[pn][0] = _processingArray[pn][0].replace( /<([^<]*)>/g, '<span class="tags">&lt;$1&gt;</span>'); } // タグ以外の<と>を置き換える _processingArray[pn][0] = _processingArray[pn][0].replace(/</g,'&lt;').replace(/>/g,'&gt;'); // 予約語にスタイルをあてる for(var rn in reservedWords) { _processingArray[pn][0] = setTagsIfMatched( _processingArray[pn][0], reservedWords[rn], 'reservedWord'); } // 事前に取得した関数名にスタイルをあてる for(var fn in functions) { // 関数名にスタイルを充てる _processingArray[pn][0] = setTagsIfMatched( _processingArray[pn][0], functions[fn].name, 'functionName'); // 関数内の引数にスタイルを充てる if(pn >= functions[fn].start-0 && pn <= functions[fn].end-0){ for(var an in functions[fn].args){ _processingArray[pn][0] = setTagsIfMatched( _processingArray[pn][0], functions[fn].args[an], 'argument'); } } } // 変数名の処理 for(var vn in variableNames) { _processingArray[pn][0] = setTagsIfMatched( _processingArray[pn][0], variableNames[vn], 'variableName'); } } // END for // 各行にcodeタグを挿入して、一行テキストにして渡す。 this.getHtml = function() { var html = ''; for(var pn in _processingArray) { // 空行は表示されないので、スペースを挿入する if(_processingArray[pn] == '') { _processingArray[pn] = '&nbsp;'; } // コメントの入っている行は配列になっているので統合する。 if(_processingArray[pn] instanceof Array) { _processingArray[pn] = _processingArray[pn].join(''); } html += '<code>' + _processingArray[pn] + '</code>'; } return html; } // 行数をカウントして返す this.countLines = function(){ return _processingArray.length; } } // END InnerSourceCode // コメント部分の隔離を行うクラス // 処理している行がコメントなのかどうか把握しておく必要があるため // インスタンス化して状態を管理する function CommentSecluder(isHtmlTagEnable) { var htmlTagEnable = isHtmlTagEnable; this.commentMode = false; // textを[code, comment]の形にする this.seclude = function(text) { var regs = new RegExp('(\\/\\/)|(\\/\\*)|(\\*\\/)'); var match = text.match(regs); // コメントが存在すればその境目でsplit if(match) { text = text.split(match[0],2); // コメント部分のタグを置き換える if(!htmlTagEnable){ text[1] = text[1].replace(/</g,'&lt;').replace(/>/g,'&gt;'); } text[1] = markUp('span', 'comment', match[0] + text[1]); if(match[0] == ('/' + '*')) { this.commentMode = true; } if(match[0] == ('*' + '/') || match.input.indexOf(('*' + '/')) != -1) { this.commentMode = false; } } // commentMode = trueの時は、処理意している行全てをコメントとして扱う else if(this.commentMode) { text = new Array(1).concat(text); text[0] = '&nbsp;'; text[1] = markUp('span', 'comment', text[1]); } // コメントがない場合も行を配列の形式にする else { text = new Array(text,''); } if(text[0] == '') { text[0] = '&nbsp;'; } return text; }; }; // END commentSecluder // 各関数を表すオブジェクト function FunctionInfo(name, args, startLine){ this.name = name; this.args = args; this.start = startLine; this.end = undefined; this.settingCount = 0; this.update = function(endPoint){ if(this.settingCount == 0){ delete this.settingCount; this.end = endPoint; } } }; // innerHtmlからfunction名を取得する(最新) function getAllFunctions(textLines){ var nameRegs = new RegExp('function [\\(\\w\\s\\$]+(?=\\()'); var argsRegs = new RegExp('\\([,\\s\\w]+(?=\\))'); var bracketRegs = new RegExp('[{}]','g'); var noNameFuncRegs = new RegExp('function\\s*\\([\\w\\s\\,\\$]+(?=\\))'); var functionList = new Array(); // すべての行を対象に、関数を見つけたらfunctioinInfoオブジェクトを作成 for(var tn in textLines){ // 関数の定義が行われているかどうか調べる if(nameRegs.test(textLines[tn])){ var args, name = (textLines[tn].match(nameRegs))[0].replace(/function /, '').trim(); // 引数の取得 if(argsRegs.test(textLines[tn])){ args = (textLines[tn].match(argsRegs))[0].replace(/[\(|\s]/g, '').split(','); } // functionInfoオブジェクトを作成する if(name != ''){ functionList.push(new FunctionInfo(name, args, tn)); } } // 無名関数に対応 if(noNameFuncRegs.test(textLines[tn])){ var name = '_no name function'; var args = (textLines[tn].match(noNameFuncRegs))[0].replace(/function\s*\(/,'').split(','); functionList.push(new FunctionInfo(name, args, tn)); } // 各関数の引数の利用範囲を調べる for(var n in functionList){ if(functionList[n].hasOwnProperty('settingCount')){ if(bracketRegs.test(textLines[tn])){ var countMatch = textLines[tn].match(bracketRegs); for(var cn in countMatch){ if(countMatch == '{'){ functionList[n].settingCount++; } if(countMatch == '}'){ functionList[n].settingCount--; } } functionList[n].update(tn); } } } } return functionList; }// END getAllFunctions // htmlTextから変数名を取得する function getAllVariableNames(htmlText) { var varRegExp = /var ([\w]*)/g; var match = htmlText.match(varRegExp); var names = []; for(var i in match) { var name = match[i].replace(/var /, '').trim(); if(reservedWords.indexOf(name) == -1 && names.indexOf(name) == -1 && name != '') { names.push(name); } } return names; } // END getAllVariableNames // lineText内にtargetTextが含まれればspanタグをあてる function setTagsIfMatched(lineText, targetText, cssClassName) { if(targetText == '$'){ targetText = '\\$'; } var targetRegExp = [ new RegExp('(\\W)(' + targetText + ')(\\W)','g'), new RegExp('(^)(' + targetText + ')(\\W)','g'), new RegExp('(\\W)(' + targetText + ')($)','g'), new RegExp('(^)(' + targetText + ')($)','g') ] for(var tn in targetRegExp) { if(targetRegExp[tn].test(lineText)) { return lineText.replace( targetRegExp[tn], // 置き換える文字を作成する function() { var ret = markUp('span', cssClassName, arguments[2]); return (arguments[1]?arguments[1]:'') + ret + (arguments[3]?arguments[3]:''); }) } } return lineText; } // END setTagsIfMatched // テキストにHtmlタグを適用する function markUp(tagName , className, innerText) { return '<' + tagName + (className?' class="' + className + '"':'') + '>' + innerText + '</' + tagName + '>'; } // END markUp // 行番号のテキストを作成する function createLineNum(element) { var styles = element.style; // 行数のテキストを作成 var innerTxt = ''; var num = element.childElementCount; for(var i=1; i<=num; i++) { innerTxt += (i + '<br />'); } // 要素を追加 var lineDiv = document.createElement('div'); lineDiv.classList.add('lineNums'); lineDiv.style.position = 'absolute'; lineDiv.style.top = element.offsetTop + element.clientTop + 'px'; lineDiv.style.left = element.offsetLeft + element.clientLeft + 'px'; lineDiv.style.marginTop = styles.marginTop; lineDiv.style.paddingTop = styles.paddingTop; lineDiv.style.height = element.clientHeight; lineDiv.style.overflowY = 'hidden'; lineDiv.innerHTML = innerTxt; element.appendChild(lineDiv); element.addEventListener('scroll', function(){ this.lastChild.scrollTop = this.scrollTop; }); // 追加した要素の分だけ本文を右にずらす var paddingLeft = element.lastChild.clientWidth + 0; paddingLeft += element.style.paddingLeft + 'px'; element.style.paddingLeft = paddingLeft; // windowサイズを変更した時の処理 var currentSize = window.innerWidth; window.addEventListener('resize', function(){ if(currentSize != window.innerWidth){ element.lastChild.style.top = element.offsetTop + element.clientTop; currentSize = window.innerWidth; } }); } // EMD createLineNum // 全体共通のフォントサイズと行高を設定する function setCss(element) { element.style.fontSize = fontSize + 'px'; element.style.lineHeight = lineHeight + 'px'; } // END setCss

はてなでは<と>で半角英数字を囲むと、自動でタグとして扱われてしまうので、ソースコード内で&lt;や&gt;に置き換える必要があります。 また、キーワードとして扱われる文字は自動でリンクが挿入されるので、以下のスクリプトでタグを削除してから関数に渡しています。

var element = document.getElementsByTagName('pre');
for(var i=0; i<element.length; i++){
    element[i].innerHTML = element[i].innerHTML.replace(/<a class\=[^>]*>(.*)<\/a>/g,'$1');
}
set_jscode_highlight(element, false);
バグ

はてなで使用すると行番号が思い通り動いてくれなかったので、とりあえずはてな内では行番号を非表示にします。 今後、余裕ができたら改修します。