Jump to content

User:Enterprisey/fancy-diffs.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Enterprisey (talk | contribs) at 09:02, 8 December 2019 (just for posterity, here was one direction I took the rewrite in. Thank goodness this didn't work out). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// vim: ts=4 sw=4 et ai
( function () {
    var api;
    var DC_CLS = ' class="diffchange diffchange-inline"';

    function makeExpand( type, name ) {
        return '<span class="fd-expand" data-' + type + '="' + name.replace( /"/g, "&quot;" ) + '">(show)</span>';
    }

    function insertAt( str1, idx, str2, replace ) {
        return str1.substring( 0, idx ) + str2 + str1.substring( idx + ( replace ? str2.length : 0 ) );
    }

    function processText( text ) {
        var spanData = {
            idxs: [],
            lengths: [],
            innerLengths: [],
            tags: [],
            inserted: [],
        };
        var SPAN_RGX = /<(ins|del) class="diffchange diffchange-inline">([^<]+?)($|<\/\1>)/g;
        var spanMatch;
        do {
            spanMatch = SPAN_RGX.exec( text );
            if( spanMatch ) {
                spanData.idxs.push( spanMatch.index );
                spanData.lengths.push( spanMatch[0].length );
                spanData.innerLengths.push( spanMatch[2].length );
                spanData.tags.push( spanMatch[1] );
                spanData.inserted.push( false );
                text = text.substring( 0, spanMatch.index ) + spanMatch[2] + text.substring( spanMatch.index + spanMatch[0].length );
            }
        } while( spanMatch );

        var replacements = [
            {
                regex: /\[\[(.+?)(?:\|.+?)?\]\]/g,
                handler: function ( match, name ) {
                    var html = "<a href='" + mw.util.getUrl( name ) + "'>" + match +
                        "</a>";
                    if( name.indexOf( "File:" ) === 0 || name.indexOf( "Image:" ) === 0 ) {
                        html += makeExpand( "img", name );
                    }

                    return html;
                }
            },
            {
                regex: /\{\{(.+?)(?:\|.+?)?\}\}/g,
                handler: function ( match, name ) {
                    var fullName = name;
                    if( name.indexOf( "#" ) === 0 ) {
                        fullName = name.replace( /^#invoke:/, "Module:" );
                    } else if( name.indexOf( ":" ) < 0 ) {
                        fullName = "Template:" + name;
                    }

                    return "{{<a href='" + mw.util.getUrl( fullName ) + "'>" + name + "</a>" + match.substring( 2 + name.length ); // "}}"
                }
            },
            {
                regex: /(?:(?:https|http|gopher|irc|ircs|ftp|news|nnttp|worldwind|telnet|svn|git|mms):\/\/|mailto:)([!#$&-;=?-\[\]_a-z~]|%[0-9a-fA-F]{2})+/g,
                handler: function ( match ) {
                    return '<a href="' + match + '">' + match + '</a>';
                }
            }
        ];

        var nextText;
        debugger;
        for( var replacementIdx = 0; replacementIdx < replacements.length; replacementIdx ++ ) {
            var regex = replacements[replacementIdx].regex,
                handler = replacements[replacementIdx].handler;
            var match;
            var nextText = text;
            var adjustment = 0;
            do {
                match = regex.exec( text );
                if( match ) {
                    var matchIndex = match.index + adjustment;
                    var dataIdxs = spanData.idxs.map( function ( startIdx, dataIdx ) {
                            var spanOverlaps = matchIndex < ( startIdx + spanData.lengths[ dataIdx ] ) ||
                                ( matchIndex + match[0].length ) < startIdx;
                            return spanOverlaps ? dataIdx : null;
                        } ).filter( function ( x ) { return x !== null; } );
                    var replacement = handler( match[0], match[1] );
                    
                    // Calculate with indexes and adjustments to compensate for the inserted <a>
                    var aElemStartIdx = replacement.indexOf( "<a" );
                    var aElemStartTagLength = replacement.indexOf( ">" ) - aElemStartIdx + 1;
                    var aElemEndIdx = replacement.indexOf( "</a>" ) - aElemStartTagLength;

                    // Adjust so that they're valid indices for "text"
                    aElemStartIdx += matchIndex;
                    aElemEndIdx += matchIndex;

                    var adjustIdxForReplacement = function ( idx ) {
                        if( idx > aElemEndIdx ) {
                            return idx + 4 + aElemStartTagLength;
                        } else if( idx > aElemStartIdx ) {
                            return idx + aElemStartTagLength;
                        } else {
                            return idx;
                        }
                    };

                    nextText = nextText.substring( 0, matchIndex ) + replacement +
                        nextText.substring( matchIndex + match[0].length );
                    adjustment += replacement.length - match[0].length;
                    for( var dataIdxIdx = dataIdxs.length - 1; dataIdxIdx >= 0; dataIdxIdx-- ) {
                        var dataIdx = dataIdxs[dataIdxIdx];
                        var startTag = "<" + spanData.tags[dataIdx] + DC_CLS + ">",
                            endTag = "</" + spanData.tags[dataIdx] + ">";
                        nextText = insertAt( nextText, adjustIdxForReplacement( spanData.idxs[dataIdx] +
                            spanData.innerLengths[dataIdx] ), endTag );
                        nextText = insertAt( nextText, adjustIdxForReplacement( spanData.idxs[dataIdx] ), startTag );

                        // Handle the case where our inserted <ins> or <del> contains the start or end tag of the <a>
                        var numExtraInsertedTags = 0;
                        nextText = nextText.replace( /(<(ins|del) class="diffchange diffchange-inline">)([^<]*?)(<(?:\/a|a[^<>]*?)>)([^<]*?)<\/\2>/g,
                            function ( match, openingTag, tagName, innerContentBefore, aElemTag, innerContentAfter ) {
                            numExtraInsertedTags++;
                            return openingTag + innerContentBefore + "</" +
                                tagName + ">" + aElemTag + "<" + tagName + DC_CLS + ">" +
                                innerContentAfter + "</" + tagName + ">";
                        } );
                        adjustment += ( 1 + numExtraInsertedTags ) * ( startTag.length + endTag.length );
                    }

                    // Move all of the <ins> and <del> indices forwards because we inserted text
                    spanData.idxs = spanData.idxs.map( function ( idx ) { return ( idx > matchIndex ) ? ( idx + adjustment ) : idx; } );

                    // All the <ins> and <del> el's that were entirely included within our replacement can be deleted now
                    //var includedSpanIdxs = spanData.idxs.map( function ( startIdx, dataIdx ) {
                    //        var spanOverlaps = matchIndex < ( startIdx + spanData.lengths[ dataIdx ] ) ||
                    //            ( matchIndex + match[0].length ) > startIdx;
                    //        return spanOverlaps ? dataIdx : null;
                    //    } ).filter( function ( x ) { return x !== null; } );
                    var includedSpanIdxs = dataIdxs;
                    for( var i = includedSpanIdxs.length - 1; i >= 0; i-- ) {
                        spanData.idxs.splice( includedSpanIdxs[i], 1 );
                        spanData.lengths.splice( includedSpanIdxs[i], 1 );
                        spanData.innerLengths.splice( includedSpanIdxs[i], 1 );
                        spanData.tags.splice( includedSpanIdxs[i], 1 );
                    }
                }
            } while( match );
            text = nextText;
        }

        // Put back all the remaining <ins> and <del> tags that didn't overlap with a link replacement
        for( var dataIdx = spanData.idxs.length - 1; dataIdx >= 0; dataIdx-- ) {
            var startTag = "<" + spanData.tags[dataIdx] + DC_CLS + ">",
                endTag = "</" + spanData.tags[dataIdx] + ">";
            text = insertAt( text, spanData.idxs[dataIdx] +
                spanData.innerLengths[dataIdx], endTag );
            text = insertAt( text, spanData.idxs[dataIdx], startTag );
        }

        return text;
    }

    function processDiff( diffTable ) {
        var rows = diffTable.querySelectorAll( "tr" );
rowLoop:
        for( var rowIdx = 0, numRows = rows.length; rowIdx < numRows; rowIdx++ ) {
            var row = rows[rowIdx];
            if( row.tagName.toLowerCase() === "colgroup" ) {
                return;
            }
            for( var cellIdx = 0, numCells = row.children.length; cellIdx < numCells; cellIdx++ ) {
                var td = row.children[cellIdx];
                if( td.querySelector( "a" ) ) continue;
                switch( td.className ) {
                    case "diff-context":
                        if( td.children && td.children.length ) {
                            var text = processText( td.children[0].innerHTML );
                            td.children[0].innerHTML = text;
                            row.children[cellIdx + 2].innerHTML = text;
                            continue rowLoop;
                        }
                        break;
                    case "diff-deletedline":
                    case "diff-addedline":
                        if( td.children && td.children.length ) {
                            td.children[0].innerHTML = processText( td.children[0].innerHTML );
                        }
                        break;
                }
            }
        }

        var expandSpans = diffTable.querySelectorAll( "span.fd-expand" );
        for( var spanIdx = 0, numSpans = expandSpans.length; spanIdx < numSpans; spanIdx++ ) {
            var span = expandSpans[spanIdx];
            span.addEventListener( "click", function () {
                if( !this.nextElementSibling || this.nextElementSibling.tagName.toLowerCase() !== "div" || this.nextElementSibling.className !== "fd-img" ) {
                    api.get( {
                        action: "query",
                        titles: this.dataset.img,
                        prop: "imageinfo",
                        iiprop: "url"
                    } ).done( function ( data ) {
                        if( data.query && data.query.pages ) {
                            var url = data.query.pages[ Object.keys( data.query.pages )[0] ].imageinfo[0].url;
                            var div = document.createElement( "div" );
                            div.className = "fd-img";
                            var img = document.createElement( "img" );
                            img.src = url;
                            img.style["max-width"] = "100%";
                            div.appendChild( img );
                            this.parentNode.insertBefore( div, this.nextSibling );
                        }
                    }.bind( this ) );
                    this.textContent = "(hide)";
                } else {
                    if( this.nextElementSibling.style.display === "none" ) {
                        this.nextElementSibling.style.display = "";
                        this.textContent = "(hide)";
                    } else {
                        this.nextElementSibling.style.display = "none";
                        this.textContent = "(show)";
                    }
                }
            } );
        }
    }

    $.when(
        $.ready,
        mw.loader.using( [ "mediawiki.api", "mediawiki.util" ] )
    ).then( function () {
        var table = document.querySelector( "table.diff" );
        api = new mw.Api();
        mw.util.addCSS( ".fd-expand { cursor: pointer; text-decoration: underline; background-color: #faf3; }" );
        if( table ) {
            processDiff( table );
        }
        mw.hook( "diff-update" ).add( processDiff );
        mw.hook( "new-diff-table" ).add( processDiff );
    } );
} )();