Järjestelmäviesti:Gadget-Artikkeliyhdistaja.js

Huomautus: Selaimen välimuisti pitää tyhjentää asetusten tallentamisen jälkeen, jotta muutokset tulisivat voimaan.

  • Firefox ja Safari: Napsauta Shift-näppäin pohjassa Päivitä, tai paina Ctrl-F5 tai Ctrl-R (⌘-R Macilla)
  • Google Chrome: Paina Ctrl-Shift-R (⌘-Shift-R Macilla)
  • Internet Explorer ja Edge: Napsauta Ctrl-näppäin pohjassa Päivitä tai paina Ctrl-F5
  • Opera: Paina Ctrl-F5.
(mw.config.get("wgAction") === "edit") 
&& (function () {
    
    (function () {
        /**
         * Otsikoiden järjestysdata.
         **/
        window.sectOrder = {};
        
        var titles = [];

        titles[2] = {
            "Kansainvälinen": [-20],
            "Suomi":          [-10],
            // muut             [0],
            "Lähteet":        [+10],
            "Viitteet":       [+20],
        };
        titles[3] = {
            // muut             [0],
            "Lähteet":        [+10],
            "Viitteet":       [+20],
        };
        titles[4] = {
            "Ääntäminen":       [-100],
            "Taivutus":          [-90],
            "Huomautukset":      [-80],
            "Etymologia":        [-70],
            "Käännökset":        [-60],
            "Lainaukset":        [-50],
            "Liittyvät sanat":   [-40],
            "Idiomit":           [-30],
            "Katso myös":        [-20],
            "Aiheesta muualla":  [-10],
            // muut                [0],
            "Lähteet":           [+10],
            "Viitteet":          [+20],
        };
        titles[5] = {
            "Rinnakkaiset kirjoitusasut":       [-140],
            "Rinnakkaismuodot":                 [-130],
            "Lyhenteet":                        [-120],
            "Synonyymit":                       [-110],
            "Vastakohdat":                      [-100],
            "Johdokset":                         [-90],
            "Yhdyssanat":                        [-80],
            "Tästä johtuvat tytärkielten sanat": [-70],
            "Lainat muissa kielissä":            [-60],
            "Yläkäsitteet":                      [-50],
            "Vieruskäsitteet":                   [-40],
            "Alakäsitteet":                      [-30],
            "Osakäsitteet":                      [-20],
            "Kokonaiskäsitteet":                 [-10],
            // muut                                [0]
        };


        /**
         * Palauttaa arvoparin, jolla otsikkon järjestystä voi verrata 
         * toiseen otsikkoon.
         **/
        function getCmpValue(title, level) {
            var cmpVal = titles[level] && titles[level][title];
            if ( cmpVal ) {
                return [cmpVal[0], title];
            }
            return [0, title];
        }

        /**
         * Palauttaa 0, jos otsikot ovat samat,
         *          -1, jos sect1 tulee ennen sect2:ta,
         *          +1, jos sect1 tulee sect2:n jälkeen.
         **/
        sectOrder.compareTitles = function (sect1, sect2) {
            var a = getCmpValue(sect1.title, sect1.level),
                b = getCmpValue(sect2.title, sect2.level),
                i = 0;

            console.assert ( sect1.level === sect2.level );

            while ( i < 2 ) { 
                if ( a[i] === b[i] ) {
                    i++;
                } else if ( a[i] < b[i] ) {
                    return -1;
                } else { // ( a[i] > b[i] )
                    return +1;
                }
            }
            return 0;
        };
    }());

    (function () {
        /**
         * Yksinkertainen osioparseri.
         **/
        window.sectParser = {};

        /**
         * Parseroi wikitekstin ja palauttaa taulukon, jossa jokainen osio on
         * esitetty muodossa:
         *   { 
         *     title:   <otsikko, =-merkit ja rivinvaihto>, // Esim. "Suomi" 
         *     level:   <otsikon taso>                      // Esim. 2
         *     content: <sisältö ilman aliosoita>           // Esim. "{{fi-subs|ka|na}}\n# lintu\n"
         *   }
         * TODO: luokkien yhdistäminen
         **/
        sectParser.parseText = function (text) {
            var osat, otst, osot, classes;

            console.assert ( typeof(text) === "string", "Virheellinen parametri" );

            main_end = text.search(/\n(\[\[Luokka:[^\n]+\]\]\s*\n*)+$/);
            if ( main_end === -1 ) { 
                main_end = text.length; 
            }
            classes = text.substring(main_end);
            text = text.substring(0, main_end);

            osat = text.split(/\B=+[^\n]+=+\n/) || text;  // Osioiden sisällöt.
            otst = text.match(/\B=+[^\n]+=+\n/g) || [];   // Osioiden otsikot.

            otst.unshift('PAGENAME\n');                 // Osa ennen ensimmäistä otsikkoa
            osot = otst.map(function (elt, i) {
                m = elt.match(/^(=*) *(.*?) *(\1)\n$/);
                return {
                    title: m[2],
                    content: osat[i],
                    level: Math.min(m[1].length, m[3].length)
                };
            });

            osot.push({ title: "LUOKAT", content: classes, level: 0 });

            return osot;
        };
    }());

    (function () {
        /**
         * Artikkeleiden yhdistäjä.
         **/
        window.articleMerger = {};
        
        /**
         * Palauttaa +1, jos title1 tulee ennen title2:ta;
         *           -1, jos title2 tulee ennen title1:tä;
         *            0, jos otsikot ovat samat.
         **/
        function compare(sect1, sect2) {
            console.assert ( sect1 && sect2, "Virheelliset parametrit" );
            console.assert ( sect1.level === sect2.level, "Eritasoiset otsikot" );
            
            return sectOrder.compareTitles(sect1, sect2);
        }

        /**
         * Tarkistaa onko annettu osio tyhjä (sisältää vain tyhjiä merkkejä).
         * @param sect: Osiotaulukon alkio, esim. { title: "otsikko", level: 3, content: "sisältö" }
         **/
        function isEmpty(sect) {
            console.assert ( sect, "Virheellinen parametri" );
            
            return sect.content.match(/^[ \n\t]*$/);
        }

        /**
         * Poistaa tyhjät osiot unmerged-osiotaulukosta.
         **/
        function removeEmpty(sects) {
            var top,
                out = [],
                level = -1;

            top = sects.pop();
            while ( top ) {
                if ( !isEmpty(top) || top.level <= level ) {
                    out.push(top);
                    level = top.level - 1;
                }
                top = sects.pop();
            }

            return out.reverse();
        }

        /**
         * @param sects1:   <osio-[]> Alkuperäisen sivun osiot. 
         * @param sects2:   <osio-[]> Lisättävät osiot.
         * @param settings: <str>     Yhdistysmoodi:
         *                              "2":  Ei yhdistetä mahdollisen olemassa olevan kieliosion kanssa.
         *                              "3":  Yhdistetään kieliosiot, muttei sanaluokkaosiota, jos 
         *                                    olemassa.
         *                              "3+": Lisätään olemassa olevan sanaluokkaosion perään.
         *                              "4":  Yhdistetään kieli- ja sanaluokkaosiot, muttei alempia.
         *                              "5":  Yhdistetään kieli-, sanaluokka- ja 4-tason osiot, muttei 
         *                                    alempia.
         * @return:       <{}> jossa .merged sisältää uuden sivun osiot,
         *                      .unmerged sisältää osiot, joita ei voitu yhdistää
         **/
        function mergeSects(sects1, sects2, mode) {
            var merged   = [], // Yhdistetyt osiot
                unmerged = [], // Osiot, joita ei voitu yhdistää.
                top1,
                top2,
                cmp,
                base,
                addOnConflict,  // Lisätäänkö uusi osio konfliktitilanteessa edellisen perään.
                mergeLevel;     // Taso, johon asti osiot yhdistetään.

            if ( mode ) {
                mode = mode + "";
            } else {
                mode = "3"; // Oletus. Yhdistetään kieliosiot, muttei sanaluokkaosioita.
            }

            mergeLevel = parseInt(mode);
            addOnConflict = (mode.substring(mode.length-1) === "+");
            
            // Tehdään taulukoista pinot, josta saa osion kerrallaan ylhäältä 
            // alaspäin pop()lla.
            sects1 = sects1.reverse();
            sects2 = sects2.reverse();

            // Ensimmäiset osiot. Pitää olla artikkelitason osio (level = 0).
            top1 = sects1.pop();
            top2 = sects2.pop();

            while ( top1 || top2 ) {
                if ( !top2 ) {
                    merged.push(top1);
                    top1 = sects1.pop();
                } else if ( !top1 ) {
                    merged.push(top2);
                    top2 = sects2.pop();
                } else if ( top1.level > top2.level ) {
                    merged.push(top1);
                    top1 = sects1.pop();
                } else if ( top2.level > top1.level ) {
                    merged.push(top2);
                    top2 = sects2.pop();
                } else {
                    cmp = compare(top1, top2);
                    if ( cmp < 0 ) {                          // Alkuperäinen ensin
                        base = top1.level;
                        do {
                            merged.push(top1);
                            top1 = sects1.pop();
                        } while ( top1 && top1.level > base );
                    } else if ( cmp > 0 ) {                   // Uusi ensin
                        base = top2.level;
                        do {
                            merged.push(top2);
                            top2 = sects2.pop();
                        } while ( top2 && top2.level > base );
                    } else {                    // Otsikot samat. 
                        // Yhdistetään osiot tai jätetään yhdistämättä riippuen osion
                        // tasosta.
                        if ( top1.level < mergeLevel ) {                 // Jos taso 2, käsitellään alaosiot erikseen.
                            // Tarkistetaan muuttuuko osion alkuosa (osa ennen ensimmäistä aliosiota).
                            // Jos lisättävällä osiolla on tässä sisältöä, tarkistetaan onko olemassa olevalla.
                            // Jos ei korvataan se uudella, muuten lisätään uusi unmergediin.
                            if ( !isEmpty(top2) ) {
                                if ( isEmpty(top1) ) {
                                    merged.push(top2);
                                    unmerged.push(top1);  // Lisätään, jotta puurakenne pysyy. Poistetaan myöhemmin,
                                    // jos alaosioita ei tule.
                                } else {
                                    merged.push(top1);
                                    unmerged.push(top2);
                                }
                            } else {
                                merged.push(top1);
                                unmerged.push(top2);
                            }
                            top1 = sects1.pop();
                            top2 = sects2.pop();
                        } else {                                // 3:s tasosta ylöspäin jätetään yhdistämättä.
                            base = top1.level;
                            do {
                                merged.push(top1);
                                top1 = sects1.pop();
                            } while ( top1 && top1.level > base );

                            if ( addOnConflict ) {
                                do {
                                    merged.push(top2);
                                    top2 = sects2.pop();
                                } while ( top2 && top2.level > base );
                            } else {
                                do {
                                    unmerged.push(top2);
                                    top2 = sects2.pop();
                                } while ( top2 && top2.level > base );
                            }
                        }
                    }
                }

            }
            return { merged: merged, unmerged: removeEmpty(unmerged) };
        }

        /**
         * Toistaa merkkijonoa str times kertaa.
         **/
        function repeat(str, times) {
            return (new Array(times+1)).join(str);
        }
        
        /**
         * Tulostaa osiotaulukon tekstinä.
         **/
        function sectsToText(sects) {
            var output = [],
                level,
                i;

            for ( i = 0; i < sects.length; i++ ) {
                if ( sects[i].level > 0 ) {
                    level = repeat("=", sects[i].level);
                    output.push(level);
                    output.push(sects[i].title);
                    output.push(level);
                    output.push("\n");
                }
                // Varmistetaan, että osioiden väliin jää tyhjä rivi (paitsi, jos osio on tyhjä).
                if ( sects[i].content !== "" && sects[i].content !== "\n" ) {
                    sects[i].content = sects[i].content.replace(/\n*$/, "\n\n");
                }
                output.push(sects[i].content);
            } 

            return output.join("");
        }

        /**
         * Yhdistää annetut tekstit ja palauttaa yhdistetyn ja yhdistämättömän osan.
         * @param text1: <str> Alkuperäinen teksti.
         * @param text2: <str> Siihen yhdistettävä teksti.
         * @param mode:  <str> Yhdistysmoodi.
         * @return:      <{}>, jossa .merged <str> yhdistetty teksti, .unmerged <str>
         *                       osat, joita ei saatu yhdistettyä.
         **/
        articleMerger.mergeText = function(text1, text2, mode) {
            var sects1 = sectParser.parseText(text1),
                sects2,
                result;

            sects2 = sectParser.parseText(text2);
            result = mergeSects(sects1, sects2, mode);;

            if ( result.unmerged.length > 0 ) {
                return { merged: sectsToText(result.merged), unmerged: sectsToText(result.unmerged) };
            } else {
                return { merged: sectsToText(result.merged), unmerged: null };
            }
        };

        /**
         * Skrollaa annetulle tekstilaatikon riville.
         **/
        function scrollToLine($textarea, lineNumber) {
            var lineHeight = parseInt($textarea.css('line-height'));
            $textarea.scrollTop(lineNumber * lineHeight);      
        }

        /**
         * Palauttaa merkkijonojen str1 ja str2 yhteisen alun.
         **/
        function getCommonPrefix(str1, str2) {
            var l = Math.min(str1.length, str2.length),
                i = 0;
            while ( i < l && str1.charAt(i)=== str2.charAt(i)) i++;
            return str1.substring(0, i);
        }
        
        /**
         * Palauttaa ensimmäisen muuttuneen rivin indeksin.
         **/
        function getFirstChangedLine(str1, str2) {
            var pref = getCommonPrefix(str1, str2),
                a = pref.split("\n");
            return a.length - 1;
        }
        
        /**
         * Yhdistää annetun tekstin editointilaatikon artikkeliin moodilla mode.
         * @param text2: Yhdistettävä artikkeli. TODO tarkistus, että kokonainen
         * @param mode:  Yhdistysmoodi.
         **/
        articleMerger.mergeToArticle = function (text2, mode) {
            var $textbox = $("#wpTextbox1"),
                $divMain = $("#articleMerger"),
                $taUnmerged = $("#articleMerger-unmerged"),
                $statusImg,
                $statusText,
                orig = $textbox.val(),
                result = articleMerger.mergeText(orig, text2, mode);           // Alkuperäinen teksti.

            window.articleMerger.originalText = $textbox.val();
            if ( $divMain.length === 0 ) {
                $divMain = $('<div id="articleMerger" style="border-bottom-width: 1px; border-bottom-style: solid;">'
                           + '<img src=""/>'
                           + '<span id="articleMerger-status">'
                           + '</span>'
                           + '</div>');
                $("#wpTextbox1").before($divMain);
            }
            
            $statusText = $divMain.find("span");
            $statusImg = $divMain.find("img");
            
            if  ( $taUnmerged.length === 0 ) {
                $taUnmerged = $('<textarea id="articleMerger-unmerged" rows="10" '
                                + 'style="background-color: blanchedalmond; margin-bottom: 5px; border-tyle: inset; border-width: 2px;">'
                              + '</textarea>');
                $divMain.append($taUnmerged);
                $taUnmerged.hide();
            }

            $taUnmerged.val(result.unmerged);
            if ( result.unmerged ) {
                $taUnmerged.show();
                $statusText.text(" " + "Seuraavia osioita ei voitu yhdistää automaattisesti: ");
                $statusImg.attr(
                    'src',
                    '//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Symbol_opinion_vote.svg/15px-Symbol_opinion_vote.svg.png'
                );
            } else {
                
                $taUnmerged.hide();
                $statusText.text(" " + "Artikkelipohja yhdistetty artikkeliin onnistuneesti!");
                $statusImg.attr(
                    'src',
                    'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Symbol_keep_vote.svg/15px-Symbol_keep_vote.svg.png'
                );
            }

            $textbox.val(result.merged);
            scrollToLine($textbox, getFirstChangedLine(orig, result.merged));
        };

    }());

    mw.loader.using( 'mediawiki.api' ).then(function () {
        /**
         * Yhdistettävän artikkelin lataus.
         **/
        window.mergeLoader = {};
        
        /**
         * Palauttaa urlin parametrit assosiatiivisena taulukkona.
         **/
        function getParams() {
            var params = {},
                i,
                list = window.location.search.substring(1).split(/[&=]/);

            for ( i = 0; i < list.length; i += 2 ) {
                key = decodeURIComponent(list[i].replace(/\+/g, " "));
                val = decodeURIComponent(list[i + 1].replace(/\+/g, " "));
                if ( key.substring(key.length - 2) === '[]' ) {
                    key = key.substring(0, key.length - 2);
                    if ( !(key in params) ) {
                        params[key] = [];
                    }
                    params[key].push(val);
                } else {
                    params[key] = val;
                }
            }
            return params;
        }  
        
        /**
         * Tekee parametrien korvaukset ym. preloadattuun tekstiin.
         **/
        function preparePreload(content, preloadparams) {
            // Poistetaan noinclude- ja include-tagit, muttei niiden sisältöä.
            content = content.replace(/<\/?noinclude\/?>/g, '');
            content = content.replace(/<\/?includeonly\/?>/g, '');

            // Korvataan muuttujanimet arvoilla.
            content = content.replace(/\$([0-9]+)/g, function (m, n) {
                var index = n - 1;
                if ( preloadparams.length > index ) {
                    return preloadparams[index];
                } else {
                    // Palautetaan muuttujanimi, jos parametreja on liian
                    // vähän.
                    return m; 
                }
            });
            return content + "\n\n";
        }  
        
        /**
         * Lataa tekstin, korvaa muuttujat ja kutsuu funktiota f_callback sillä.
         * @param: title        <string> Ladattavan preload-pohjan nimi.
         **/
        mergeLoader.forPreloadedText = function (title) {

            return new Promise(function (resolve, reject) {
                var api = new mw.Api({
                    ajax: {
                        headers: {
                            'Api-User-Agent': 'Artikkeliyhdistäjä/0.2'
                        }
                    }
                });

                console.assert(title, "Virheelliset parametrit");
                
                api.get({
                    format: 'json',
                    prop:   'revisions',
                    rvprop: 'content',
                    titles: title
                }).done(function (data) {
                    var curPageData = (data && data.query && data.query.pages &&
                                       data.query.pages[Object.keys(data.query.pages) [0]]),
                        content = (curPageData && curPageData.revisions &&
                                   curPageData.revisions.length > 0 && curPageData.revisions[0]['*']);
                    
                    if ( content ) {
                        resolve(content);
                    } else {
                        reject("Artikkelia ei ole")
                    }
                });
            });
        };

        /**
         * Yhdistää annetun artikkelin nykyiseen, kun ollaan editointinäkymässä.
         * @param title:         Yhdistettävän artikkelin nimi.
         * @param mode:          (valinnainen, oletus = "3") Yhdistämismoodi (taso).
         * @param preloadparams: (valinnainen) $n-merkit korvaavat arvot.
         **/
        mergeLoader.mergeWith = function (title, mode, preloadparams) {
            preloadparams = preloadparams || [];
            mode = mode || "3";
            
            mergeLoader.forPreloadedText(title)
                       .then(function (content) {
                           content = preparePreload(content, preloadparams);
                           articleMerger.mergeToArticle(content, mode);
                       }).catch(function (reason) {
                           $("#mw-content-text").prepend($('<div class="errorbox">'
                                                         + '<strong>Artikkelin <a href="/w/index.php?title=' 
                                                         + encodeURIComponent(title) 
                                                         + '&action=edit&redlink=1" class="new">'
                                                         + title
                                                         + '</a> lataaminen ei onnistunut</strong>. '
                                                         + 'Syy: ' + reason + '.'
                                                         + '</div>')); 
                       });
        };
        

        // Käynnistetään vain, jos muokataan olemassa olevaa sivua. Jos luodaan
        // uutta sivua, annetaan sisäisen preload-systeemin hoitaa.
        var params = getParams();
        if ( params && params.redlink !== "1" ) {
            (function () {
                if ( params.preload ) {
                    mergeLoader.mergeWith(params.preload, params.mergemode, params.preloadparams);
                }
            })();
        }
    });
    
}());