MediaWiki:Gadget-page-contributors.js

Fuomáš: Maŋŋel go almmuhat, soaitá leat dárbbašlaš sihkkut neahttalohkkii gaskaráju vai oainnat rievdadusaid. 

  • Firefox / Safari: Doala Shift dan botta go deattát Reload, dahje deaddil Ctrl-F5 dahje Ctrl-R (⌘-R Mac'as)
  • Google Chrome: Deaddil Ctrl-Shift-R (⌘-Shift-R Mac'as)
  • Internet Explorer / Edge: Doala Ctrl dan botta go deattát Álggat ođđasit, dahje deaddil Ctrl-F5
  • Opera: deaddil Ctrl-F5.
(function(mw, $) {
 
    var i18nData = {
        'en': {
            'contributors': 'Contributors',
            'description':  'Analyze how much each have contributed to the page',
            'in-progress': 'Already in progress!',
            'box-title': 'Contributors at this page',
            'box-warning': 'Calculation of contributors is a heavy calculation which will download a lot of data. This can make your browser non-responsive,'
                + ' or your computer may stop completly. If you have a limited download capasity you may consider alternatives.',
            'execute': 'Compute',
            'cancel': 'Cancel',
            'full-success': 'Success',
            'partial-success': 'Success, reported: ',
            'partial-error': 'Error, reported: ',
            'loaded-revisions': ' revisions loaded…',
            'all-loaded': 'All revisions loaded…',
            'result-fnv': 'Calculate FNV-digests…',
            'result-calculate-cosine': 'Calculate cosine measure…',
            'result-adjust-cosine': 'Adjust cosine measure…',
            'result-accumulate-cosine': 'Accumulate cosine measure…',
            'result-contributors': 'Sort out main contributors…',
            'result-presentation': 'Create presentation…',
            'result-ingres': 'This is a simplified calculation of the individual contributors part of the overall text.'
                + ' It is used $num$ revisions in the analysis of total $tot$ loaded.',
            'result-hidden': ' Among those there are $hidden$ hidden(e) revisions.',
            'result-other': ' It is detected $shunted$ identical revisions, and it is searched $similar$ times after previous similar revisions due to large changes.',
            'result-none': ' None revisions are left out.',
            'entry-norm': '<a href="$uname$">$name$</a> (<a href="$tname$">talk</a> | <a href="$cname$">bidrag</a>) has contributed $amouth$% of the article',
            'entry-norm': '<a href="$uname$">$name$</a> (<a href="$tname$">talk</a> | <a href="$cname$">bidrag</a>) has contributed $amouth$% of the article and is creator',
            'entry-other': 'Other contributors have made $amouth$% of the article',
            'additional-help': 'See <a href="/wiki/Hjelp:Forfattere_av_sider">help page</a> for additional explanation.'
        },
        'nb': {
            'contributors': 'Bidragsytere',
            'description':  'Analyserer hvor mye de enkelte har bidratt på siden',
            'in-progress': 'Allerede under utføring!',
            'box-title': 'Bidragsytere på siden',
            'box-warning': 'Beregning av bidragsytere er en tung beregning som vil laste ned mye data. Dette kan gjøre at nettleseren slutter å reagere, eller'
                + ' at maskinen stopper helt opp. Hvis du har en begrenset mulighet til nedlasting så bør du vurdere andre alternativer.',
            'execute': 'Beregn',
            'cancel': 'Avbryt',
            'full-success': 'Suksess',
            'partial-success': 'Suksess, rapporterte: ',
            'partial-error': 'Feil, rapporterte: ',
            'loaded-revisions': ' revisjoner lastet&hellip;',
            'all-loaded': 'Alle revisjoner lastet&hellip;',
            'result-calculate-cosine': 'Beregn måltall for cosinus&hellip;',
            'result-adjust-cosine': 'Juster måltall for cosinus&hellip;',
            'result-accumulate-cosine': 'Summer måltall for cosinus&hellip;',
            'result-contributors': 'Sorter ut største bidragsytere&hellip;',
            'result-presentation': 'Lag presentasjon&hellip;',
            'result-ingres': 'Dette er en forenklet beregning av de enkelte brukeres andel av den totale tekstmassen.'
                + ' Det er brukt $num$ revisjoner i analysen av de totalt $tot$ som ble lastet.',
            'result-hidden': ' Blant disse er det $hidden$ skjult(e) revisjoner.',
            'result-other': ' Det er påvist $shunted$ identiske revisjoner, og søkt $similar$ ganger etter forutgående lignende revisjoner på grunn av store endringer.',
            'result-none': ' Ingen revisjoner er utelatt.',
            'entry-norm': '<a href="$uname$">$name$</a> (<a href="$tname$">diskusjon</a> | <a href="$cname$">bidrag</a>) har bearbeidet $amouth$% av artikkelen',
            'entry-main': '<a href="$uname$">$name$</a> (<a href="$tname$">diskusjon</a> | <a href="$cname$">bidrag</a>) har bearbeidet $amouth$% av artikkelen og er artikkeloppretter',
            'entry-other': 'Andre bidragsytere har bearbeidet $amouth$% av artikkelen',
            'additional-help': 'Se <a href="/wiki/Hjelp:Forfattere_av_sider">hjelpesiden</a> for ytterligere forklaring.'
        },
    };
    i18nData.no = i18nData.nb;

    var parts = mw.user.options.get( 'language' ).split("-");
    if( i18nData[parts[0]] ) {
        var lang = parts[0];
    }
    else {
        var lang = 'en';
    }

    function i18n( key ) {
        if( i18nData[ lang ][ key ] ) {
            return i18nData[ lang ][ key ];
        }
        else {
            return i18nData[ 'en' ][ key ];
        }
    }

    var query = {
        action:  'query',
        prop:    'revisions',
        titles:  mw.config.get('wgPageName'),
        rvlimit: 10,
        rvprop:  'ids|timestamp|user|size|content',
        format:  'json'
    };
    
    var revisions = [];

    var queryContinue = null;

    var str = null;

    var hidden = 0;
    
    function toHex(n) {
        if (n < 0) n = 0xFFFFFFFF + n + 1;
        return n.toString(16);
    };

    function fnv(str) {
        // this is an implementation of the Fowler-Noll-Vo hash algorithm
        var hash = 2166136261; // the 32 bit offset
        for (var i = 0, l = str.length; i < l; i++) {
            hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
            hash = hash ^ str.charCodeAt(i);
        }
        return hash & 0x0ffffffff;
    };

    // permutations for the lsh algorithm
    var perm = [
        1, 14, 110, 25, 97, 174, 132, 119, 138, 170, 125, 118, 27, 233, 140, 51,
        87, 197, 177, 107, 234, 169, 56, 68, 30, 7, 173, 73, 188, 40, 36, 65,
        49, 213, 104, 190, 57, 211, 148, 223, 48, 115, 15, 2, 67, 186, 210, 28,
        12, 181, 103, 70, 22, 58, 75, 78, 183, 167, 238, 157, 124, 147, 172, 144,
        176, 161, 141, 86, 60, 66, 128, 83, 156, 241, 79, 46, 168, 198, 41, 254,
        178, 85, 253, 237, 250, 154, 133, 88, 35, 206, 95, 116, 252, 192, 54, 221,
        102, 218, 255, 240, 82, 106, 158, 201, 61, 3, 89, 9, 42, 155, 159, 93,
        166, 80, 50, 34, 175, 195, 100, 99, 26, 150, 16, 145, 4, 33, 8, 189,
        121, 64, 77, 72, 208, 245, 130, 122, 143, 55, 105, 134, 29, 164, 185, 194,
        193, 239, 101, 242, 5, 171, 126, 11, 74, 59, 137, 228, 108, 191, 232, 139,
        6, 24, 81, 20, 127, 17, 91, 92, 251, 151, 225, 207, 21, 98, 113, 112,
        84, 226, 18, 214, 199, 187, 13, 32, 94, 220, 224, 212, 247, 204, 196, 43,
        249, 236, 45, 244, 111, 182, 153, 136, 129, 90, 217, 202, 19, 165, 231, 71,
        230, 142, 96, 227, 62, 179, 246, 114, 162, 53, 160, 215, 205, 180, 47, 109,
        44, 38, 31, 149, 135, 0, 216, 52, 63, 23, 37, 69, 39, 117, 146, 184,
        163, 200, 222, 235, 248, 243, 219, 10, 152, 131, 123, 229, 203, 76, 120, 209 ];

     function lsh(str) {
        // this is an implementation of a lsh algorithm, loosley similar to nilsimsa
        var arr = Array();
        for (var i = 0; i <= 0x0ff; i++) {
            arr[i] = 0;
        }
        for (var i = 0, l = str.length-2; i < l; i++) {
             // first we do a Pearson hash of trigram from the text
             var hash = 0;
             hash = perm[ (hash ^ str.charCodeAt(i)) & 0x0ff ];
             hash = perm[ (hash ^ str.charCodeAt(i+1)) & 0x0ff ];
             hash = perm[ (hash ^ str.charCodeAt(i+2)) & 0x0ff ];
             // then we accumulate in the array at indices given by the hash
             arr[hash]++;
        }
        // fold the array
        for (var i = 0; i <= 0x01f; i++) {
            arr[i] += arr[i+0x020] + arr[i+0x040] + arr[i+0x060] + arr[i+0x080] + arr[i+0x0a0] + arr[i+0x0c0] + arr[i+0x0e0];
        }
        var acc = 0;
        var lim = arr.slice(0, 0x01f).sort(function(a,b){return a-b})[0x00f];
        for (var i = 0; i <= 0x01f; i++) {
            acc |= (((lim < arr[i]) ? 0 : 1) << i);
        }
        return acc & 0x0ffffffff;
    };

    function rvec(str) {
        // this is an implementation of a subspace reduction algorithm based upon the Pearson hash
        var arr = new Array();
        for (var i = 0; i <= 0x0ff; i++) {
            arr[i] = 0;
        }
        for (var i = 0, l = str.length-2; i < l; i++) {
             // first we do a Pearson hash of trigram from the text
             var hash = 0;
             hash = perm[ (hash ^ str.charCodeAt(i)) & 0x0ff ];
             hash = perm[ (hash ^ str.charCodeAt(i+1)) & 0x0ff ];
             hash = perm[ (hash ^ str.charCodeAt(i+2)) & 0x0ff ];
             // then we accumulate in the array at indices given by the hash
             arr[hash]++;
        }
        return arr;
    };

    function beans(n) {
        // implementation of a bean counter
        // make an unsigned 32bits int
        if (n < 0) n = 0xFFFFFFFF + n + 1;
        // From Hacker's Delight, p. 66, Figure 5-2
        n = n - ((n >> 1) & 0x55555555);
        n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
        n = (n + (n >> 4)) & 0x0F0F0F0F;
        n = n + (n >> 8);
        n = n + (n >> 16);
        return n & 0x0000003F;
    };

    function cosine(vec1, vec2) {
        // really just a difference measure for the trigram vectors
        var combined = Object();
        for (var x in vec1)
            combined[x] = vec1[x];
        for (var x in vec2) {
            if (combined[x] == null) combined[x] = 0;
            combined[x] -= vec2[x];
        }
        var acc = 0;
        for (var x in combined)
            acc += combined[x] * combined[x];
        return Math.sqrt(acc);
    };

    function rcos(vec1, vec2) {
        // really just a difference measure for the trigram vectors
        var acc = 0;
        for (var i = 0; i <= 0x0ff; i++)
            acc += Math.pow((vec1 ? vec1[i] : 0) - (vec2 ? vec2[i] : 0), 2);
        return Math.sqrt(acc);
    };

    function create() {
        mw.loader.using( ['jquery.ui'], function() {
            $dialog = $('#page-contributors-title');
            if ($dialog.length > 0) {
                $dialog.dialog('open');
            }
            else {
                var formFind = '<div id="page-contributors-title" title="' + i18n( 'box-title' ) + '"><form>';
                formFind += '<p>' + i18n( 'box-warning' ) + '</p>';
                formFind += '</form></div>';
                var messageFind = $( formFind ).appendTo( '#content' );
                out = $( formFind ).find( 'p' );
                messageFind.dialog( {
                    autoOpen: true,
                    modal: true,
                    width: 500,
                    buttons: [ 
                        {
                            id: 'contributors-button-cancel',
                            text: i18n( 'cancel' ),
                            click: abortDownload
                        },
                        {
                            id: 'contributors-button-ok',
                            text: i18n( 'execute' ),
                            click: startDownload
                        }
                    ]
                } );
            }
        } );
    };

    function abortDownload() {
        $.removeSpinner( 'contributors' );
        $('#page-contributors-title').dialog( "close" );
        if ( queryContinue != null) {
            $( '#contributors-button-ok' ).button( 'option', 'disabled', false );
        }
    }

    function startDownload() {
        $( '#contributors-button-ok' ).button( 'option', 'disabled', true );
        mw.loader.using( ['jquery.spinner'], function() {
            $.createSpinner( {
                size: 'large',
                type: 'block',
                id: 'contributors'
            } ).appendTo( '#page-contributors-title' );
            download( queryContinue );
        });
    }

    function download( rvcontinue ) {
        // ajax object
        if (rvcontinue != null) {
            query.rvstartid = rvcontinue;
        }
        $.ajax({
            url: mw.util.wikiScript( 'api' ),
            dataType: 'json',
            data: query,
            context: document.body
        })
        .done(function(data, textStatus) {
            if (textStatus != 'success') {
                alert( i18n( 'partial-success' ) + textStatus );
                $('#page-contributors-title').dialog( "close" );
            }
            var r = data['query'].pages[parseInt(mw.config.get('wgArticleId'))].revisions;
            for (var x in r) {
                if (typeof r[x]['*'] == 'undefined') {
                    hidden++;
                    continue;
                }
                r[x].lsh = lsh(r[x]['*']);
                r[x].fnv = toHex(fnv(r[x]['*']));
                if(r[x].size == 'undefined') r[x].size = r[x]['*'].length;
                r[x].vec = rvec(r[x]['*']);
                r[x]['*'] = null; // conserve space
                revisions.push(r[x]);
            }
            if (data['query-continue']) {
                $('#page-contributors-title p').html( revisions.length + i18n( 'loaded-revisions' ) );
                queryContinue = query.rvstartid = data['query-continue'].revisions.rvcontinue;
                if ( $('#page-contributors-title').dialog( "isOpen" ) ) {
                    download();
                }
            }
            else {
    	        $.removeSpinner( 'contributors' );
                queryContinue = null;
                setResult();
            }
        })
        .fail(function(jqXHR, textStatus, errorThrown) {
            alert(i18n( 'error-partial' ) + textStatus);
            $('#page-contributors-title').dialog( "close" );
        });
    };

    function setResult() {
        // final handler after all ajax calls has completed
        $('#page-contributors-title:parent p').html( i18n( 'all-loaded' ) );

        // variables    
        var digests = Object();
        var users = Object();
        var revids = Object();

        // short form to get the revisions
        var revs = revisions;
        
        // element to use for output
        out = $('#page-contributors-title p');

        // get the individual revisions through a revid-key
        for (var i = 0; i < revs.length; i++) revids[revs[i].revid] = revs[i];

        // get the individual revisions through the fnv-digest
        out.html( i18n( 'result-fnv' ) );
        var shunted = 0;
        for (var i = revs.length-1; 0<=i; i--) {
            if (digests[revs[i].fnv]) {
                revs[i].previousid = digests[revs[i].fnv].revid;
                shunted++;
            }
            digests[revs[i].fnv] = revs[i];
        }

        // cache the cosine measure and set previousid to reflect the span
        out.html( i18n( 'result-calculate-cosine' ) );
        var i = 0;
        while (i<revs.length) {
            var j = i;
            if (revs[i].previousid) {
                revs[i].cosine = rcos(revs[i].vec, revids[revs[i].previousid].vec);
                var previousid = revs[i].previousid;
                while (revs[i].revid > previousid && i<revs.length) i++;
            }
            else if (revs[i].parentid) {
                revs[i].cosine = rcos(revs[i].vec, revids[revs[i].parentid].vec);
                var parentid = revs[i].parentid;
                while (revs[i].revid > parentid && i<revs.length) i++;
            }
            else if (i+1<revs.length) {
                revs[i].cosine = rcos(revs[i].vec, revs[i+1].vec);
                i++;
            }
            else if (i+1==revs.length) {
                revs[i].cosine = rcos(revs[i].vec);
                revs[j].previousid = 0;
                break;                                                        
            }
            if (i != j) revs[j].previousid = revs[i].revid;
        }

        // adjust the cosine measure if lsh is to far off
        out.html( i18n( 'result-adjust-cosine' ) );
        var similar = 0;
        var revision = revs[0];
        while (revision) {
            var previous = revids[revision.previousid];
            if (previous /* && revision.size > 100 */ && 64<revision.cosine) {
                var p=previous;
                var lsh=2147483647;
                var keep = null;
                for (var j = 0; j < 16; j++) {
                    if (p) {
                        var tmp = rcos(revision.vec, p.vec);
                        if (lsh > tmp) {
                            lsh = tmp;
                            keep = p;
                        }
                        p = revids[p.previousid];
                    }
                    else {
                        break;
                    }
                }
                if (keep && keep != previous) {
                    previous = keep;
                    similar++;
                }
            }
            if (previous) {
                revision.previousid = previous.revid;
                revision.cosine = rcos(revision.vec, previous.vec);
            }
            revision = previous;
        }

        // accumulate the cosine measure for each user
        out.html( i18n( 'result-accumulate-cosine' ) );
        var revision = revs[0];
        var num = 0;
        while (revision) {
            if (!users[revision.user]) users[revision.user] = { cosine: 0 };
            users[revision.user].cosine += revision.cosine;
            revision = revids[revision.previousid];
            num++;
        }
        // accumulate the total cosine measure
        var acc = { cosine: 0 };
        for (var x in users) acc.cosine += users[x].cosine;

        // sort out the main contributors
        out.html( i18n( 'result-contributors' ) );
        var authors = Array();
        var pseudonyms = Array();
        var count = 0;
        for (var x in users) pseudonyms.push(x);
        for (var x in pseudonyms.sort(function(a, b){ return users[b].cosine - users[a].cosine })) {
            var name = pseudonyms[x];
            var cosine = Math.round(100*users[name].cosine/acc.cosine);
            if (count++ < 5 && 0 < cosine) authors.push(name);
            else if (5 <= cosine ) authors.push(name);
            else if (revs[revs.length-1].user == name) authors.push(name);
        }

        // print the cosine measure for each user
        out.html( i18n( 'result-presentation' ) );
        str = i18n( 'result-ingres' )
            .replace('$num$', num)
            .replace('$tot$', revisions.length);
        if (0<hidden)
            str += i18n( 'result-hidden' )
                .replace('$hidden$', hidden);
        if (0<shunted || 0<similar)
            str += i18n( 'result-other' )
                .replace('$shunted$', hidden)
                .replace('$similar$', similar);
        else
            str += i18n( 'result-none' );
        str += '<ul>';
        var part = 0;
        for (var x in authors) {
            var name = authors[x];
            part += users[name].cosine;
            var amouth = (0.1<users[name].cosine/acc.cosine ? Math.round(100*users[name].cosine/acc.cosine) : Math.round(1000*users[name].cosine/acc.cosine)/10);
            var entry = (revs[revs.length-1].user == name ? i18n( 'entry-main' ) : i18n( 'entry-norm' ))
                .replace('$name$', name)
                .replace('$uname$', mw.util.getUrl('User:' + name))
                .replace('$tname$', mw.util.getUrl('User_talk:' + name))
                .replace('$cname$', mw.util.getUrl('Special:Contributions/' + name))
                .replace('$amouth$', String(amouth));
            str += '<li>' + entry + '</li>';
        }
        amouth = Math.round(100*(acc.cosine-part)/acc.cosine);
        entry = i18n( 'entry-other' ).replace('$amouth$', String(amouth));
        str += '<li>' + entry + '</li>';
        str += '</ul>';
        str += '<small>' + i18n( 'entry-other' ) + '</small>';
        out.html(str);
    };

    var nsnum = parseInt(mw.config.get('wgNamespaceNumber'));
    function init() {
        // the strange test (2147483647 <= -1 >>> 1) is to verify that we have at least 32 bit ints
        // note that 32 bit ints are according to standard, while it can be longer but not shorter to respect the standard
        if (0 <= nsnum && 0 == nsnum%2 && mw.config.get('wgIsArticle') && (2147483647 <= -1 >>> 1)) {
            $( function() {
                mw.util.addPortletLink(
                    "p-cactions",
                    '#',
                    i18n( 'contributors' ),
                    "t-article-contributors",
                    i18n( 'description' ),
                    null,
                    null
                );
                $('#t-article-contributors').click(create);
            });
        }
    };

    $( document ).ready( init );

} (mediaWiki, jQuery) );