motiko / Beautify Salesforce Debug View

// ==UserScript==
// @name         Beautify Salesforce Debug View
// @namespace    SFDC
// @version      0.2.9
// @description Beautify Salesforce Debug View
// @author       motiko
// @license      MIT
// @match        https://*.salesforce.com/p/setup/layout/ApexDebugLogDetailEdit/*
// @require /src/libs/motiko/beautify.js
// @require /src/libs/motiko/beautify-html.js
// @resource debug_css https://raw.githubusercontent.com/motiko/sfdc-debug-logs/master/monkey/debug.css
// @grant    GM_addStyle
// @grant    GM_getResourceText
// @grant    GM_setValue
// @grant    GM_getValue
// @grant    GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==

(function(){

if(typeof GM_getResourceText === "function"){
    var debug_css = GM_getResourceText("debug_css");
    GM_addStyle(debug_css);
    /*
    try{
        var debug_css_local = GM_getResourceText("debug_css_local");
        GM_addStyle(debug_css_local);
    }catch(e){}
     */
}

var selectedText,
    currentResult,
    maxResult,
    keyPrefixes = [],
    sid = document.cookie.match(/(^|;\s*)sid=(.+?);/)[2],
    idRegex = /\b[a-zA-Z0-9]{18}\b|\b[a-zA-Z0-9]{15}\b/g,
    debugDescRegex = /(\d\d:\d\d:\d\d\.\d{3}\s+\(\d{8}\))\|(\w+)\|/,
    logEntryToDivTagClass = [{
        logEntry: '|USER_DEBUG',
        divClass: 'debug'
    },{
        logEntry: '|SYSTEM_',
        divClass: 'system systemMethodLog searchable'
    },{
        logEntry: '|SOQL_EXECUTE_',
        divClass: 'soql searchable wrap'
    },{
        logEntry: '|SOQL_EXECUTE_END',
        divClass: 'soql searchable wrap'
    },{
        logEntry: '|METHOD_',
        divClass: 'method methodLog searchable'
    },{
        logEntry: '|CONSTRUCTOR_',
        divClass: 'method methodLog searchable'
    },{
        logEntry: '|EXCEPTION_',
        divClass: 'err searchable'
    },{
        logEntry: '|FATAL_ERROR',
        divClass: 'err searchable'
    },{
        logEntry: '|CODE_UNIT',
        divClass: 'method searchable'
    },{
        logEntry: '|CALLOUT',
        divClass: 'callout searchable'
    },{
        logEntry: '|VALIDATION_',
        divClass: 'method searchable'
    },{
        logEntry: '|EXECUTION_',
        divClass: 'rest searchable'
    },{
        logEntry: '|DML_BEGIN',
        divClass: 'rest searchable'
    },{
        logEntry: '|DML_END',
        divClass: 'rest searchable'
    },{
        logEntry: '|ENTERING_MANAGED_PKG',
        divClass: 'system systemMethodLog searchable'
    }
    ];

function setSetting(key,value){
    if(typeof GM_setValue === "function"){
        GM_setValue(key,value);
    }else{
        localStorage.setItem(key,value);
    }
}

function getSetting(key){
    if(typeof GM_getValue === "function"){
        if(GM_getValue(key) === undefined)
            return false;
        return GM_getValue(key);
    }else{
        return localStorage.getItem(key);
    }
}

init();

function init(){
    document.body.addEventListener('keyup',keyUpListener);
    //document.body.addEventListener('mouseup',searchSelectedText);
    var codeElement = document.querySelector('pre');
    var debugText = escapeHtml(codeElement.textContent);
    var res = debugText.split('\n').map(addTagsToKnownLines);
    res = res.reduce(toMultilineDivs);
    var codeBlock = document.querySelector('pre');
    codeBlock.innerHTML = '<div class="monokai" id="debugText">' + res + '</div>';
    document.querySelector('.oLeft').style.display ="none";
    var oRight = document.querySelector('.oRight');
    oRight.insertBefore(codeBlock,oRight.firstChild);
    addControllersContainer();
    addDropDowns();
    /*if(!getSetting('dontShowHint')){
        addSearchHint();
    }*/
    removeIllegalIdLinks();
    var debugElements = document.getElementsByClassName('debug');
    var userDebugDivs = toArray(debugElements);
    userDebugDivs.forEach(function(debugDiv){
        setTimeout(addExpnasionButtonsForUserDebugDivs.bind(null,debugDiv),0);
    });
    addCollapseAllButton();
    addCheckboxes();
}

function addControllersContainer(){
    var container = document.createElement('div');
    container.id = 'controllersContainer';
    addToTop(container);
}

function toArray(elemntList){
    return [].slice.call(elemntList);
}

function addSearchHint(){
    var hintContainer = document.createElement('div');
    hintContainer.id = 'hintContainer';
    var hint = document.createElement('span');
    hint.innerHTML = ['<h4>Dear user,</h4><br/>',
        '<h7>Please note we changed keyboard shortcuts for your convenience.</h7> ',
        '<ul><li><b>Alt+Shift+d</b> - open debug logs </li>',
        '<li><b>Shift+d</b> - open debug logs in new tab </li></ul>',
        '<p> For full list of keyboard shortcuts please visit our <a href="https://github.com/motiko/sfdc-debug-logs">Github page</a></p>'].join('');
    var hideTip = document.createElement('button');
    hideTip.textContent = 'X';
    hideTip.title = 'Close';
    hideTip.className = 'closeButton';
    hideTip.onclick = function(){
        hintContainer.style.display = 'none';
        setSetting('dontShowHint',true);
    };
    hintContainer.appendChild(hideTip);
    hintContainer.appendChild(hint);
    addToTop(hintContainer);
}

function addDropDowns(){
    addController(generateStyleSelect());
    addController(generateFontSelect());
    var savedStyle = getSetting('style');
    if(savedStyle){
        var styleSelection = document.querySelector('#styleSelection');
        styleSelection.value = savedStyle;
        styleSelection.onchange();
    }
    var savedFontSize = getSetting('fontSize');
    var fontSize = savedFontSize || 18;
    var fontSizeSelection = document.querySelector('#fontSelection');
    fontSizeSelection.value = fontSize;
    fontSizeSelection.onchange();
}

function generateFontSelect(){
    var selectStyleContainer = document.createElement('span');
    selectStyleContainer.id = 'selectFontContainer';
    var label = document.createElement('label');
    label.textContent = 'Font Size:';
    label.for = 'fontSelection';
    var dropDown = document.createElement('select');
    dropDown.id = 'fontSelection';
    var size;
    for(size=12;size<29;size++){
        var opt = document.createElement('option');
        opt.value = size;
        opt.textContent = size;
        dropDown.appendChild(opt);
    }
    dropDown.onchange = function(event){
        document.querySelector('#debugText').style.fontSize = this.value + 'px';
        setSetting('fontSize',this.value);
    };
    selectStyleContainer.appendChild(label);
    selectStyleContainer.appendChild(dropDown);
    return selectStyleContainer;
}

function generateStyleSelect(){
    var selectStyleContainer = document.createElement('span');
    selectStyleContainer.id = 'selectStyleContainer';
    var label = document.createElement('label');
    label.textContent = 'Pick Style: ';
    label.for = 'styleSelection';
    var dropDown = document.createElement('select');
    dropDown.id = 'styleSelection';
    var styles = [{name:'monokai',label:'Monokai'},{name:'bw',label:'Black/White'},{name:'emacs',label:'Emacs'}];
    styles.forEach(function(style){
        var opt = document.createElement('option');
        opt.value = style.name;
        opt.textContent = style.label;
        dropDown.appendChild(opt);
    });
    dropDown.onchange = function(event){
        document.querySelector('#debugText').className = this.value;
        setSetting('style',this.value);
    };
    selectStyleContainer.appendChild(label);
    selectStyleContainer.appendChild(dropDown);
    return selectStyleContainer;
}

function addCheckboxes(){
    var showSystemLabel = document.createElement('label');
    showSystemLabel.className = 'toggleHidden';
    showSystemLabel.innerHTML = '<input type="checkbox" name="checkbox" id="showSystem"/>Show <u>S</u>ystem Methods</label>';
    var showMethodLogLabel = document.createElement('label');
    showMethodLogLabel.className = 'toggleHidden';
    showMethodLogLabel.innerHTML = '<input type="checkbox" name="checkbox" checked="checked" id="showUserMethod" />Show <u>U</u>ser Methods</label>';
    var showTimeStamp = document.createElement('label');
    showTimeStamp.className = 'toggleHidden';
    showTimeStamp.innerHTML = '<input type="checkbox" name="checkbox" id="showTimestamps"/>Show <u>T</u>imestamps</label>';
    addToTop(showMethodLogLabel);
    addToTop(showSystemLabel);
    addToTop(showTimeStamp);
    document.getElementById('showTimestamps').checked = JSON.parse(getSetting('showTimeStamp'));
    document.getElementById('showTimestamps').onchange = toggleTimestamp;
    document.getElementById('showUserMethod').onchange = toogleHidden('methodLog');
    document.getElementById('showSystem').onchange = toogleHidden('systemMethodLog');
}

function toggleTimestamp(){
    var oldValue = JSON.parse(getSetting('showTimeStamp'));
    setSetting('showTimeStamp',!oldValue);
    document.location.reload();
}

function addCollapseAllButton(){
    var button = document.createElement('button');
    button.innerHTML = '<u>E</u>xpand All';
    button.onclick = colapseAll;
    button.id = 'collapseAllButton';
    button.className = 'myButton';
    addToTop(button);
}

function colapseAll(){
    toArray(document.querySelectorAll('.expandUserDebugBtn.collapsed')).forEach(function(button){
        setTimeout(expandUserDebug.bind(button),0);
    });
    this.innerHTML = 'Collaps<u>e</u> All';
    this.onclick = function(){
        toArray(document.querySelectorAll('.expandUserDebugBtn.expanded')).forEach(function(button){
            setTimeout(button.onclick.bind(button),0);
        });
        this.innerHTML = '<u>E</u>xpand All';
        this.onclick = colapseAll;
    };
}

function addToTop(nodeElement){
    document.querySelector('.codeBlock').insertBefore(nodeElement,document.getElementById('debugText'));
}

function addController(controller){
    document.querySelector('#controllersContainer').appendChild(controller);
}

function keyUpListener(event){
    /*if(event.keyCode  == 70){ // 'f'
        if(currentResult === undefined || currentResult === maxResult){
            currentResult = -1;
        }
        currentResult++;
        goToResult(currentResult);
    }
    else if(event.keyCode  == 66){ // 'b'
        if(currentResult === undefined || currentResult === 0){
            currentResult = maxResult + 1;
        }
        currentResult--;
        goToResult(currentResult);
    }
    else*/
    if(event.keyCode  == 27){ // 'esc'
        removeHighlightingOfSearchResults();
    }
    else if(event.keyCode  == 85){ // 'u'
        clickOn(document.querySelector('#showUserMethod'));
    }
    else if(event.keyCode  == 83){ // 's'
        clickOn(document.querySelector('#showSystem'));
    }
    else if(event.keyCode  == 69){ // 'e'
        clickOn(document.querySelector('#collapseAllButton'));
    }
    else if(event.keyCode  == 84){ // 't'
        clickOn(document.querySelector('#showTimestamps'));
    }
}

function clickOn(nodeElement){
    var event = document.createEvent('MouseEvents');
    event.initEvent('click', true, true);
    nodeElement.dispatchEvent(event, true);
}

function goToResult(resultNum){
    document.location.replace('#result' + resultNum);
    document.body.addEventListener('keyup',keyUpListener);
    var previouslySelectdElement = document.querySelector('.currentResult');
    if(previouslySelectdElement){
        previouslySelectdElement.classList.remove('currentResult');
    }
    document.querySelector('span.highlightSearchResult[data-number="' + resultNum + '"]')
            .classList.add('currentResult');
}

function removeHighlightingOfSearchResults(){
    currentResult = 0;
    maxResult = 0;
    toArray(document.querySelectorAll('.highlightSearchResult') ).forEach(function(span){
        var highlightedText = span.textContent;
        var textNode = document.createTextNode(highlightedText);
        span.parentElement.insertBefore(textNode,span);
        span.parentElement.removeChild(span);
    });
    toArray(document.querySelectorAll('.searchResultAnchor') ).forEach(function(a){
        a.parentElement.removeChild(a);
    });
}

function searchSelectedText(event){
    if(event.button == 2 || !event.altKey){
        return;
    }
    selectedText = document.getSelection().toString();
    removeHighlightingOfSearchResults();
    if(!selectedText){
        currentResult = 0;
        maxResult = 0;
        return;
    }
    selectedText = escapeHtml(selectedText);
    var searchableElements = toArray(document.querySelectorAll('#debugText .searchable') );
    var resultNum =0 ;
    searchableElements.filter(notHidden).filter(contains(selectedText)).forEach(function(div){
        var regExp = new RegExp(escapeRegExp(selectedText),'gi');
        div.innerHTML = div.textContent.replace(regExp,function(match){
            var resultString = '<a name="result' + resultNum +
             '" class="searchResultAnchor" data-number="' +
             resultNum + '"></a><span class="highlightSearchResult" data-number="' + resultNum + '">' +
             match +'</span>';
            resultNum++;
            return resultString;
        });
        div.innerHTML = div.innerHTML.replace(idRegex,withLegalIdLink);
        maxResult = resultNum-1;
    });
    markNearestSearchResult();
}

function notHidden(element){
    return element.offsetParent  !== null;
}

function markNearestSearchResult(){
    var visibleSearchResults =  toArray(document.querySelectorAll('.searchResultAnchor'))
                                .filter(isVisibleElement);
    if(visibleSearchResults.length > 0){
        var mouseY = event.clientY;
        var closest = visibleSearchResults.map(function(anchor){
            var anchorY = anchor.getBoundingClientRect().top;
            return {element: anchor,distance:Math.abs(mouseY - anchorY)};
        }).reduce(function(found,current){
            if(current.distance < found.distance){
                return current;
            }
            return found;
        });
        currentResult = parseInt(closest.element.dataset.number,10);
        document.querySelector('span.highlightSearchResult[data-number="' + currentResult + '"]')
            .classList.add('currentResult');
    }
}

function isVisibleElement(element){
    return element.getBoundingClientRect().top >= 0;
}

function addExpnasionButtonsForUserDebugDivs(userDebugDiv){
    var debugLevel = userDebugDiv.innerHTML.match(/\[\d+\](\|[A-Z]+\|)/);
    if(!debugLevel) return;
    debugLevel = debugLevel[1];
    var debugParts = userDebugDiv.innerHTML.split(debugLevel);
    userDebugDiv.innerHTML = '<span class="debugHeader searchable">' +
            debugParts[0] +  debugLevel +'</span> <span class="debugContent searchable">' +
            debugParts[1] + '</span>';
    var debugText = unescapeHtml(debugParts[1]);
    if(looksLikeHtml(debugText) || looksLikeSfdcObject(debugText) || isJsonString(debugText)){
        var buttonExpand = document.createElement('button');
        buttonExpand.onclick = expandUserDebug;
        buttonExpand.onmouseup = haltEvent;
        buttonExpand.className = 'expandUserDebugBtn collapsed myButton';
        buttonExpand.textContent = '+';
        userDebugDiv.insertBefore(buttonExpand,userDebugDiv.children[0]);
    }
}

function toMultilineDivs(prevVal,curLine,index){
    if(index == 1){ // handling first line
        return '<div class="rest">' + prevVal + '</div>' + curLine ;
    }
    else if(curLine.lastIndexOf('</div>') ==  curLine.length - '</div>'.length && curLine.length - '</div>'.length != -1){ // current line ends with <div> tag all good
        return prevVal + curLine;
    }
    else{ // expanding <div> to mutliline (e.g. LIMIT_USAGE_FOR_NS is multiline and cant recognise each line separately or USER_DEBUG with /n)
        return prevVal.substr(0,prevVal.length - '</div>'.length) + '\n' + curLine + '</div>';
    }
}

function addTagsToKnownLines(curLine){
    if(curLine.indexOf('Execute Anonymous:') === 0){
        return '<div class="system searchable">' + curLine + '</div>';
    }
    if(curLine.search(idRegex) > -1){
        curLine = curLine.replace(idRegex,'<a href="/$&" class="idLink">$&</a>');
    }
    var timeStampIndex = curLine.indexOf('|');
    var cutLine;
    if(timeStampIndex > -1){
        cutLine = curLine.substr(timeStampIndex + 1);
    }
    var resultTag = '';
    var i;
    for(i=0; i<logEntryToDivTagClass.length ; i++){
        if(curLine.indexOf(logEntryToDivTagClass[i].logEntry) > -1){
            resultTag = '<div class="' + logEntryToDivTagClass[i].divClass + '">' +
            (JSON.parse(getSetting('showTimeStamp')) ? curLine : cutLine) + '</div>';
            break;
        }
    }
    if(resultTag){
        return resultTag;
    }
    var splitedDebugLine = curLine.split('|');
    if(!splitedDebugLine || splitedDebugLine.length <= 2){
        return curLine;
    }
    return '<div class="rest searchable">' + curLine +'</div>';
}

function haltEvent(event){
    event.stopPropagation();
}

function removeIllegalIdLinks(){
    request('/services/data/v29.0/sobjects').then(function(response){
        var sobjects = JSON.parse(response).sobjects;
        keyPrefixes = sobjects.filter(function(sobj){
            return (sobj.keyPrefix !== undefined);
        }).map(function(sobjectDescribe){
            return sobjectDescribe.keyPrefix;
        });
        keyPrefixes.push('03d'); // validation rule
        var idLinks = document.getElementsByClassName('idLink');
        toArray(idLinks).forEach(function(link){
            if(!isLegalId(link.textContent)){
                link.className = 'disableClick';
            }
        });
    });
}

function isLegalId(id){
    return ( keyPrefixes.indexOf( id.substr(0,3) ) > -1 );
}

function toogleHidden(className){
    return function(event){
        var systemLogs =toArray(document.getElementsByClassName(className)) ;
        systemLogs.forEach(function(systemLog){
            systemLog.style.display = event.srcElement.checked ? 'block' : 'none';
        });
    };
}

function expandUserDebug(){
    var debugNode = this.nextElementSibling.nextElementSibling;
    var  oldHtmlVal =  debugNode.innerHTML;
    var debugNodeText = debugNode.textContent;
    if(looksLikeHtml(debugNodeText)){
        debugNode.textContent  = html_beautify(debugNodeText);
    }else if(looksLikeSfdcObject(debugNodeText)){
        debugNode.textContent  = js_beautify(sfdcObjectBeautify(debugNodeText));
    }else if(isJsonString(debugNodeText)){
        debugNode.textContent  = js_beautify(debugNodeText);
    }
    if(debugNodeText.search(idRegex) > -1){
        debugNode.innerHTML = debugNode.textContent.replace(idRegex,withLegalIdLink);
    }
    this.textContent = '-';
    this.classList.add('expanded');
    this.classList.remove('collapsed');
    this.onclick = function(){
        debugNode.innerHTML = oldHtmlVal;
        this.classList.remove('expanded');
        this.classList.add('collapsed');
        this.textContent = '+';
        this.onclick = expandUserDebug;
        this.onmouseup = haltEvent;
    };
}

function withLegalIdLink(id){
    if(isLegalId(id)){
        return '<a href="/' + id + '" class="idLink">' + id + '</a>';
    }
    else{
        return id;
    }
}

function sfdcObjectBeautify(string){
    string = string.replace(/={/g,':{');
    return string.replace(/([{| |\[]\w+)=(.+?)(?=, |},|}\)|:{|])/g,function(match,p1,p2){
        return p1 + ":'" + p2  + "'" ;
    });
}

function looksLikeSfdcObject(string){
    return string.match(/\w+:{\w+=.+,?\s*}/);
}

function looksLikeHtml(source) {
    var trimmed = source.replace(/^[ \t\n\r]+/, '');
    return (trimmed && trimmed.substring(0, 1) === '<');
}

function isJsonString(str) {
    try {
        JSON.parse(str);
    } catch (e) {
        return false;
    }
    return true;
}

function escapeHtml(text) {
    var map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
    };

    return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}


function unescapeHtml(str) {
    return str.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, '\'');
}


function escapeRegExp(str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

function contains(searchString){
    return function(nodeElem){
        return nodeElem.innerHTML.indexOf(searchString) > -1;
    };
}

function request(url,method){
    method = method || 'GET';
    if(typeof GM_xmlhttpRequest === "function"){
        return new Promise(function(fulfill,reject){
            GM_xmlhttpRequest({
                method:method,
                url:url,
                headers:{
                    Authorization:'Bearer ' + sid,
                    Accept:'*/*'
                },
                onload:function(response){
                    if( response.status.toString().indexOf('2') === 0){
                        fulfill(response.response);
                    }else{
                        reject(Error(response.statusText));
                    }
                },
                onerror:function(response){
                    rejected(Error("Network Error"));
                }
            });
        });
    }
    return new Promise(function(fulfill,reject){
        var xhr = new XMLHttpRequest();
        xhr.open(method,url);
        xhr.onload = function(){
            if( xhr.status.toString().indexOf('2') === 0){
                fulfill(xhr.response);
            }else{
                reject(Error(xhr.statusText));
            }
        };
        xhr.onerror = function(){
            rejected(Error("Network Error"));
        };
        xhr.setRequestHeader('Authorization','Bearer ' + sid);
        xhr.send();
    });
}
})();