Järjestelmäviesti:Gadget-Tekstileikkeet-testi.js

Wikisanakirjasta

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.
/**
 * Gadget-Tekstileikkeet 0.999
 **/


/** 
 * Koodimoduulien yhteiset osat.
 **/
(function () {
    window.Leikkeet2 = {};
    /**
     * Ohjesivun osoite.
     **/
    Leikkeet2.helpUrl = "//fi.wiktionary.org/w/Ohje:Gadget-Tekstileikkeet";

    /**
     * Käyttäjän oman hallintasivun nimi.
     **/
    Leikkeet2.getManagePageTitle = function () {
	return mw.config.get("wgUserName") + "/" + "Gadget-Tekstileikkeet";
    };

    /**
     * Käyttäjän oman hallintasivun url.
     **/
    Leikkeet2.getManagePageUrl = function () {
	return '//fi.wiktionary.org/wiki/Käyttäjä:' +
	    encodeURIComponent(mw.config.get("wgUserName")) + "/" + "Gadget-Tekstileikkeet";
    };

    
    Leikkeet2.encodeSnippetContent = function (txt) {
	txt = txt.replace(/⦃/g, "{{{");
	txt = txt.replace(/⦄/g, "}}}");
	txt = txt.replace(/∷/g, "|");
	return txt;
    };

    /** 
     * Palauttaa taulukon, jossa on tekstin txt kaikki parametrinimet (esim. "{{{1|}}}"). Taulukko 
     * on muotoa [a, l, t], jossa 
     *   a = alkuindeksi
     *   l = loppuindeksi
     *   t = {- ja }-merkkien määrä kutsussa. Mallineilla 2, parametreilla 3, mutta voi olla
     * myös suurempi arvo, jos kutsuja on monta sisäkkäin.
     */
    function getEncodedParams(txt) {
        /**
         * Usean {- tai }-merkin ryppäät. Rypäs on yhteinäinen jono {-, }-, [- tai ]-merkkejä.
         * Muotoa [a, l, t, p], jossa
         *   a = alkuindeksi
         *   l = loppuindeksi
         *   t = tyyppi ('{' tai '}')
         */
        var ryppaat = [];
        var result;
        var alku = 0;

        /*
         * Etsitään ryppäät tekstistä. Ohitetan yksittäiset {- ja }-merkit.
         */ 
        var pat = /(\{\{+|\}\}+|\[\[|\]\])/;
        while ( (result = pat.exec(txt.substring(alku))) !== null ) {
	    var a = result.index;
	    var l = a + result[0].length;
	    ryppaat.push([alku+a, alku+l, result[0][0]]);
	    alku += l;
        }

        var mkutsut = [];


        /**
         * Ohitetut ryppäät. Vastaan tulevat '{'-merkkiset ryppäät siirretään pinoon, josta
         * niitä vähennetään vastaan tulevilla '}'-ryppäillä.
         **/
        var pino = [];

        pino.top = function (v) {
	    if ( v !== undefined ) {
                pino[pino.length-1] = v;
	    }
	    return pino[pino.length-1];
        };

        ryppaat.first = function (v) {
	    if ( v !== undefined ) {
                ryppaat[0] = v;
	    }
	    return ryppaat[0];
        };

        /**
         * Tallennetaan parametrit ja ohitetaan mallineet ja linkit.
	 * Param v on ryppaat taulukon rivi.
	 */
        function tallenna(v) {
	    if ( v[2] == 3 && v[3] == '{' ) {
		mkutsut.push(v);
	    }
        }


        /*
         * Otetaan jonosta ryväs, jos se on {-merkkinen, siirretään pinoon. Jos }-merkkinen
         * vähennetään pinossa olevista {-merkeistä. 
         */
        while ( ryppaat.first() !== undefined ) {
	    var k = ryppaat.first();
	    ks = k[0];
	    ke = k[1];
	    km = k[2];
	    var kl = ke - ks;

	    if ( km == '{' || km == '[' ) {
                pino.push(ryppaat.shift());

	    } else { /* km == '}' || km == ']' */
                if ( pino.top() === undefined ) { break; }
                var t = pino.top();
		var ts = t[0];
		var te = t[1];
		var tm = t[2];
                var tl = te - ts;

                if ( tm == "[" && km == "}" ) {
		    ryppaat.shift();                     // ohitetaan pariton '}'
                } else if ( tm == "{" && km == "]" ) {
		    pino.pop();                          // ohitetaan pariton '{'
                } else {                                 // joko {}- tai []-parit
		    if ( kl == tl ) {
                        tallenna([ts, ke, tl, tm]);
                        pino.pop();
                        ryppaat.shift();
		    } else if ( kl < tl ) {
                        tallenna([te - kl, ke, kl, tm]);
                        te = te - kl;
			    +                        pino.top([ts, te, tm]);
                        ryppaat.shift();
                        if ( te - ts == 1 ) {            // ohitetaan yksittäiset jäljelle jääneet
			    pino.pop(); 
                        }
		    } else if ( tl < kl ) {
                        tallenna([ts, ks + tl, tl, tm]);
                        ks += tl; 
                        ryppaat.first([ks, ke, km]);
                        pino.pop();
                        if ( ke - ks == 1 ) {            // ohitetaan yksittäiset jäljelle jääneet
			    ryppaat.shift();
                        }
		    }
                }
	    }
        }

        return mkutsut;
    }

    /**
     * Dekoodaa {{{1|kana}}}-tyyppiset ⦃1∷kana⦄-tyyppisiksi.
     **/
    function decodeParameters(txt, mkutsut) {
	var start = 0;
	var out = [];
	var i;

        for ( i in mkutsut ) {
	    // ohitetaan sisäkkäiset
	    if ( start > mkutsut[i][0] ) {
		console.log("Sisäkkäiset kentät");
		continue;
	    }
	    out.push(txt.substring(start, mkutsut[i][0]));
	    out.push(txt.substring(mkutsut[i][0], mkutsut[i][1])
		     .replace(/^{{3}0}{3}$/, "⦃∷⦄")
		     .replace(/^{{3}/, "⦃")
		     .replace(/\|/, "∷")
		     .replace(/}{3}$/, "⦄"));
	    start = mkutsut[i][1];
        }
	
	out.push(txt.substring(start, txt.length));
	
	return out.join("");
    }
    
    Leikkeet2.decodeSnippetContent = function(txt) {

	return decodeParameters(txt, getEncodedParams(txt));
    };


    Leikkeet2.encodeSnippets = function (groupList) {
	return groupList.map(function (group) {
	    return {
		name: group.name,
		list: group.list.map(function (snippet) {
		    return {
			key : snippet.key,
			content : Leikkeet2.encodeSnippetContent(snippet.content) };
		})};
	});
    };

    Leikkeet2.decodeSnippets = function (groupList) {
	return groupList.map(function (group) {
	    return {
		name: group.name,
		list: group.list.map(function (snippet) {
		    return {
			key : snippet.key,
			content : Leikkeet2.decodeSnippetContent(snippet.content) };
		})};
	});
    };
    
    /**
     * Tallentaa leikkeet localstorageen.
     * 
     * @param groupList: ryhmän mukaan järjestetty lista listoista.
     **/
    Leikkeet2.saveSnippetTree = function(groupList) {
	localStorage.setItem('tekstileikkeet', JSON.stringify(groupList));   
    };
    
    /**
     * Lataa leikkeet localstoragesta.
     *
     * @return: ryhmän mukaan järjestetty lista listoista.
     **/
    Leikkeet2.loadSnippetTree = function() { 
	var dataStr = localStorage.getItem('tekstileikkeet');
	var groupList = null;

	try {
	    groupList = JSON.parse(dataStr);
	} catch ( e ) {
	    alert("leikkeitä ei voitu lukea: " + e);
	    return null;
	}

	return groupList;
    };


    /**
     * Palauttaa leikkeet dictinä, jossa avaimena on snippetin nimi.
     *
     * Alkio on leikkeen sisältö. TODO pois
     *
     * @param groupList: ryhmän mukaan järjestetty lista leikelistoista.
     **/
    Leikkeet2.getSnippets = function() {
	var groupList = Leikkeet2.loadSnippetTree();
	var i, j;
	var group;
	var snippet;
	var snippets = {};

	for ( i in groupList ) {
	    group = groupList[i];
	    for ( j in group.list ) {
		snippet = group.list[j];
		//snippet.group = group.name;
		snippets[snippet.key] = snippet.content;
	    }
	}
	return snippets;
    };

    /**
     * Palauttaa leikkeet dictinä, jossa avaimena on snippetin nimi.
     *
     * Alkiot ovat muotoa { "key" : "", "content" : "", "group : "" }.
     *
     * @param groupList: ryhmän mukaan järjestetty lista leikelistoista.
     **/
    Leikkeet2.getSnippetDict = function() {
	var groupList = Leikkeet2.loadSnippetTree();
	var i, j;
	var group;
	var snippet;
	var snippets = {};

	for ( i in groupList ) {
	    group = groupList[i];
	    for ( j in group.list ) {
		snippet = group.list[j];
		snippets[snippet.key] = {
		    "key"     : snippet.key,
		    "group"   : group.name,
		    "content" : snippet.content,
		    "index"   : j };
	    }
	}
	return snippets;
    };

})();
/**
 * Leikkeiden korvaamista tekstilaatikossa käsittelevä koodi.
 **/

(function(){
    var Wikileike = {
        leikkeet: null
    };

    /**
     * Merkit, joihin haku katkeaa kun etsitään leikekutsun alkukohtaa, ts. merkit jotka eivät 
     * voi kuulua leikeviitteeseen.
     **/
    var KATKOMERKIT = " \n\t:∷⦃⦄|*#;";

    /**
     * Merkki, joka katkaisee leikeviitteen, mutta jota ei lueta viitteen nimeen.
     **/
    var ALKUMERKKI = "$";

    var $textbox;

    /**
     * Koodissa käytettyjä termejä:
     *   leike:              koostuu leikenimestä ja leiketekstistä
     *   leikenimi:          nimi, jolla teittyyn leikkeeseen viitataan
     *   leiketeksti:        leikken sisältämä teksti, jolla leikeviite korvataan
     *   leikeviite:         tekstissä esiintyvä leikenimi
     *   kenttä:             leiketekstissä oleva kenttä, jolle voi antaa arvon; 
     *                       tekstissä joko muotoa ⦃1⦄ tai ⦃1∷arvo⦄, vastaavasti TODO leikesivulla
     *                       muotoa {{{1}}} ja {{{1|arvo}}}; kenttiin viitataan niiden alku- ja
     *                       loppukohtien indekseillä, esim. [2, 4].
     *   sisäkenttä:         kentän ∷- ja ⦄-merkkien väli, ei sisällä itse merkkejä
     *   ulkokenttä:         kentän ⦃- ja ⦄-merkkien väli, mukaan lukien itse merkit
     *   kentän otsikko-osa: kentän ⦃- ja ∷-merkkien väli.
     *   kentän otsikko:     kentän otsikko-osan teksti
     *
     **/

    /**************************
     * Ulos näkyvät funktiot. *
     **************************/

    /**
     * Korvaa valitun tekstin tai kursoria edeltävän tekstin sitä 
     * vastaavalla leikkeellä, jos sellainen on.
     **/
    Wikileike.laajenna_leike = function() {
        var pcur = sel.hae_kohdat();
        var pviit = [];
        var text = $textbox.val();
	var pos = sel.hae_alku();
        var nykykentta = hae_sisin(0, text, pos);
	
        // Jos tekstiä on valittu yritetään laajentaa valittu teksti, muuten
        // etsitään edellinen leikeviittauksen nimessä sallimaton merkki ja
        // valitaan siitä kursorin paikkaan asti.
        if ( pcur[0] == pcur[1] ) {
            pviit[0] = hae_viittauksen_alku(text, pcur[0]);
            pviit[1] = pcur[0];
            sel.valitse(pviit[0], pcur[0]); 
        } else {
            if ( pcur[0] < pcur[1] ) {
                pviit = pcur;
            } else {
                pviit[0] = pcur[1];
                pviit[1] = pcur[0];
            }
        }

        var snippetti = sel.hae_valinta();
        if ( snippetti[0] == ALKUMERKKI ) { snippetti = snippetti.substring(1); }
        var leike = Wikileike.leikkeet[snippetti];
        if ( leike === undefined ) {
            console.log("Tuntematon leike: " + snippetti);
            // Jätetään valinta päälle.
            return;
        }
	console.log("leike: " + JSON.stringify(leike));
        leike = leike.replace(/⦃([^∷]*?)⦄/g, "⦃$1∷⦄", leike);

        setTimeout(function() {

            // Jos snippetti löytyy, korvataan valinta, muuten vain poistetaan 
            // valinta ja siirrytään sen loppuun.
            if ( leike ) {
                sel.korvaa(leike, false);

                // Tutkitaan onko leikkeessä kenttiä. Jos on, siirrytään ekaan 
                // kenttään. Jos ei, siirrytään snippetin loppuun.
                var ekakentta = eka_kentta(leike, 0);
                if ( ekakentta ) {
		    console.log("eka kenttä:", JSON.stringify(ekakentta) );
                    var sekakentta = Wikileike.hae_sisakentta(leike, 
                                                              ekakentta);
		    console.log("s-eka kenttä:", JSON.stringify(sekakentta) );
                    
		    sel.valitse(pviit[0] + sekakentta[0], pviit[0] 
                                + sekakentta[1]);

		    // Jos eka kenttä on nollakenttä, suljetaan samantien.
		    if ( ekakentta[2] == "" ) {
			Wikileike.hyppaa_seuraavaan();
		    }
                } else {
                    sel.siirry(pviit[0] + leike.length); 
                }
            } else {
                sel.siirry(pcur[0]); 
            }
        }, 100);
    };

    /**
     * Siirry seuraavaan kenttään tai sulje kenttä yläkenttä.
     */
    Wikileike.hyppaa_seuraavaan = function() {
        var text = $textbox.val();
        var pos = sel.hae_alku();

        // Haetaan nykyinen kenttä, eli kenttä jossa kursori on.
        var nykykentta = hae_sisin(0, text, pos);

        // Jos ollaan ylimmällä tasolla.
        if ( nykykentta[0] == -1 ) return;

        var nykykentta_nimi = Wikileike.hae_kentan_otsikko(text, nykykentta);
        var nykykentta_s    = Wikileike.hae_sisakentta(text, nykykentta);
        var nykykentta_arvo = text.substring(nykykentta_s[0], nykykentta_s[1]);
        var ylakentta       = hae_ylakentta(text, nykykentta);

        // Etsitään samannimiset kentät ja korvataan niiden sisältö saadulla 
        // kentän sisällöllä.
        aseta_kentat_arvoon(text, ylakentta, nykykentta[1], nykykentta_nimi, 
                            nykykentta_arvo);
        // Päivitetään muuttunut teksti. Nykykenttä on kutenkin sama, 
        // ylakenttä ei.
        text = $textbox.val(); 
        
        // Haetaan seuraava kenttä.
        seurkentta = hae_sisarkentta(text, nykykentta, +1);

        // Siirrytään seuraavaan kenttään. Jos seuraavaa kenttää ei ole tai 
        // se on nollakenttä, suljetaan yläkenttä.
        if ( seurkentta ) {
	    console.log("seurkentta:", JSON.stringify(seurkentta));
            var sisakentta = Wikileike.hae_sisakentta(text, seurkentta);
            sel.valitse(sisakentta[0], sisakentta[1]);
            if ( seurkentta[2] == "" ) { 
                setTimeout(function() {
                    ylakentta = hae_ylakentta(text, nykykentta);
                    sulje_kentta(text, ylakentta, sel.hae_loppu());
                }, 100);
            }
        } else {
            ylakentta = hae_ylakentta(text, nykykentta);
            sulje_kentta(text, ylakentta, sel.hae_loppu());
        }
    };

    /**
     * Siirry edelliseen kenttään.
     **/
    Wikileike.hyppaa_edelliseen = function() {
        var text = $textbox.val();
        var pos = sel.hae_alku();

        // Haetaan kenttä, jossa kursori on.
        var nykykentta = hae_sisin(0, text, pos);

        // Haetaan edellinen kenttä.
        var edkentta = hae_sisarkentta(text, nykykentta, -1);

        // Siirrytään edelliseen kenttään, jos sellainen on, muuten ei tehdä 
        // mitään.
        if ( edkentta ) {
            var edkentta_s = Wikileike.hae_sisakentta(text, edkentta);
            sel.valitse(edkentta_s[0], edkentta_s[1]);
        }
    };

    /**
     * Palauttaa viittauksen otsikko-osan alku- ja loppukohdan.
     * Esim. "⦃3∷oletusarvo⦄" -> [1,2].
     **/
    Wikileike.hae_kentan_otsikkoOsa = function(text, kentta) {
        if ( kentta.otsikkoOsa !== undefined ) return kentta.otsikkoOsa;

        var a = kentta[0], l = kentta[1];
        
        // Uloin taso.
        if ( a == -1 ) {
            kentta.otsikkoOsa = [a, a];
            return kentta.otsikkoOsa;
        }

        // Etsitään mahdollinen ensimmäinen sisäinen alkava viite, ettei 
        // vahingossa lueta sen ∷-merkkiä.
        var raja = text.indexOf("⦃", a+1);
        if ( raja == -1 || raja > l ) { raja = l; }

        var p = text.indexOf("∷", a+1);
        if ( p != -1 && p < raja ) {
            kentta.otsikkoOsa = [a+1, p];
            return kentta.otsikkoOsa;
        }

        // Ei parametriosaa.
        kentta.otsikkoOsa = kentta;
        return kentta;
    };

    /**
     * Hae sisäkentän otsikon teksti.
     * Viite on muotoa ⦃otsikko∷sisäkenttä⦄.
     * Esim. ⦃3∷oletusarvo⦄ -> 3.
     */
    Wikileike.hae_kentan_otsikko = function(text, kentta) {
        var pp = Wikileike.hae_kentan_otsikkoOsa(text, kentta);
        if ( pp ) {
            return text.substring(pp[0], pp[1]);
        }
        return null;
    };

    /**
     * Hakee kentan parametriosan ääripäät.
     * Ts. "∷"- ja "⦄"-merkin tai "⦃"- ja "⦄"-merkin välin.
     * Esim. "⦃3∷oletusarvo⦄" -> [3,11].
     * Olettaa, että text[a] == "⦃" ja text[l] == "⦄" eikä ⦃ ja ∷-merkin 
     * välissä ole muita hakasulkumerkkejä.
     * Viitteet voivat olla muodossa ⦃sisäkenttä⦄ tai ⦃otsikko∷sisäkenttä⦄. 
     * Num-osassa ei saa olla muita viitteitä.
     */
    Wikileike.hae_sisakentta = function(text, kentta) {
        var a = kentta[0], l = kentta[1];

        if ( kentta.sisakentta === undefined ) { 

            // Jos kyseessä on koko teksti, on sisäkenttä myös koko teksti.
            if ( a == -1 && l == text.length+1 ) {
                kentta.sisakentta = [a+1, l-1];
            } else {
                var oo = Wikileike.hae_kentan_otsikkoOsa(text, kentta);
                assert ( oo !== null, "hae_sisakentta: Kenttä ilman otsikko-osaa!" );
                kentta.sisakentta = [oo[1]+1, l-1];
            }
        }
        return kentta.sisakentta;
    };


    /**********************
     * Sisäiset funktiot. *
     **********************/

    function assert(ehto, viesti) {
        if ( !ehto ) {
            alert("ASSERT-VIRHE: " + viesti);
        }
    }
    
    /**
     * Lyhenteet textSelectionin metodeille.
     */
    var sel = {
        valitse: function(alku, loppu) {
            if ( loppu === undefined ) { loppu = alku; }
            $textbox.textSelection('setSelection', 
                                   { 'start': alku, 'end': loppu  }); 
        },
        siirry: function(kohta) {
            $textbox.textSelection('setSelection', 
                                   { 'start': kohta, 'end': kohta  }); 
        },
        hae_valinta: function() {
            return $textbox.textSelection('getSelection');
        },
        /**
         * Palauttaa valinnan alku- ja loppukohdat taulukkona.
         */
        hae_kohdat: function() {
            return $textbox.textSelection('getCaretPosition', 
                                          { 'startAndEnd' : true });
        },
        /**
         * Palauttaa valinnan pelkän alkukohdan.
         */
        hae_alku: function() {
            return $textbox.textSelection('getCaretPosition', 
                                          { 'startAndEnd' : false });
        },

        hae_loppu: function() {
            var ppos = sel.hae_kohdat();
            return ppos[1];
        },

        korvaa: function(teksti, valitse) {
            var ppos = sel.hae_kohdat();
            var spos = ppos[0];
            var epos = ppos[1];
            //var text = $textbox.val();


	    $textbox.textSelection('replaceSelection', teksti);
            //$textbox.val(text.substring(0, spos) + teksti 
            //             + text.substring(epos, text.length));

            epos = spos + teksti.length;
            if ( valitse ) {
                sel.valitse(spos, epos); 
                $textbox.focus();
            } 
        }

    };

    /**
     * Palauttaa leike-kutsun alkukohdan.
     **/
    function hae_viittauksen_alku(text, pos) {
        for ( var i = pos-1; i >= 0; i-- ) {
            if ( text[i] == ALKUMERKKI ) {
                return i;
            } else if ( KATKOMERKIT.indexOf(text[i]) != -1 ) {
                return i+1;
            }
        }
        return 0;
    }
    
    function hae_seuraava(tasap, teksti, pos, asti) {
        var tp = 0;
        if ( asti === undefined ) { asti = teksti.length; }
        for (var i = pos; i < asti; i++ ) {
            if ( teksti[i] == '⦃' ) {
                tp++;
            } else if ( teksti[i] == '⦄' ) {
                tp--;
                // Ei mieltä jatkaa jos menee alle perustason.
                if ( tp < 0 && tasap == +1 ) { return -1; }
            }
            if ( tp == tasap ) {
                return i;
            }
        }
        return -1;
    }

    function hae_edellinen(tasap, teksti, pos, asti) {
        var tp = 0;
        if ( asti === undefined ) { asti = 0; } 
        for (var i = pos-1; i >= asti; i-- ) {
            if ( teksti[i] == '⦃' ) {
                tp++;
                // Ei mieltä jatkaa jos menee alle perustason.
                if ( tp > 0 && tasap == -1 ) { return -1; } 
            } else if ( teksti[i] == '⦄' ) {
                tp--;
            }
            if ( tp == tasap ) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Laskee positiota 'pos' vastaavan uuden position 'taulukoi'-funktion 
     * tekemässä taulukossa 'kentät', kun kenttämerkit on poistettu. 
     */
    function laske_uusi_pos(kentat, pos) {
        var upos = 0, vpos = 0;
        for ( var e in kentat ) {
            var ulen = kentat[e][4] - kentat[e][3];
            var vlen = kentat[e][2] - kentat[e][1];
            upos += (kentat[e][1] - vpos);
            
            if ( kentat[e][1] >= pos ) {
                return upos + (pos - vpos);
            } else if ( kentat[e][3] >= pos ) {
                return upos;
            } else {
                if ( kentat[e][4] >= pos ) {
                    return upos + (pos - kentat[e][3]);
                } else if ( kentat[e][2] >= pos ) {
                    return upos + ulen;
                }
            }

            upos += ulen;
            vpos = kentat[e][2];
        }
        return upos;
    }

    /**
     * Poistaa kenttämerkit ja korvaa kentät niiden sisällöllä.
     */
    function poista_kentat(text, kentat) {
        var upos = 0;
        var a = 0, text_o = [];
        for ( var e in kentat ) {
            var sisalto = text.substring(kentat[e][3], kentat[e][4]);
            text_o.push(text.substring(a, kentat[e][1]) + sisalto);
            a = kentat[e][2];
        }
        text_o.push(text.substring(a));

        return text_o.join("");
    }

    /**
     * Poistaa kenttämerkit ja korvaa kentät niiden sisällöllä.
     * Palauttaa uuden tekstin ja 'pos'in arvon sovitettuna uuteen tekstiin.
     */
    function siivoa(text, pos) {
        var ulkokentta = [-1, text.length+1];
        var kentat = hae_alakentat(text, ulkokentta, 0);
        var utext = poista_kentat(text, kentat);
        var upos = laske_uusi_pos(kentat, pos);
        return [utext, upos];
    }

    /**
     * Sulkee annetun kentän.
     * kentta[0],[1]: kentan ulkokohdat
     * pos: kursorin kohta
     **/
    function sulje_kentta(text, kentta, pos) {
        var sisakentta = Wikileike.hae_sisakentta(text, kentta);
        sel.valitse(sisakentta[0], sisakentta[1]); 
        var valittu = sel.hae_valinta();
        var ret = siivoa(valittu, pos-sisakentta[0]);
        var siivottu = ret[0];
        var upos = ret[1];
        
        setTimeout(function() {
            sel.korvaa(siivottu);
            // Siirrytään loppuun.
            sel.siirry(sisakentta[0] + upos); 
        }, 100);
    }

    /**
     * Palauttaa sisimmän kentän jolla 'pos' on.
     * FIX toimii vain jos 'taso' = 0
     */
    function hae_sisin(taso, text, pos) {
        var a = hae_edellinen(taso + 1, text, pos);
        var l = hae_seuraava(-(taso + 1), text, pos);
        if ( a == -1 ) a = -1;
        if ( l == -1 ) l = text.length + 1; else l += 1;
        return [a, l];
    }

    /**
     * Hakee seuraavan kentan alku- ja loppukohdat. kentta[0] ja [1]:n täytyy 
     * osoittaa olemassa olevan kentan alku- ja loppukohtiin.
     */
    function seuraava_kentta(text, kentta, ylakentta) {
        var a  = kentta[0], l = kentta[1], 
            ya = ylakentta[0], yl = ylakentta[1];
        var spos = hae_seuraava(+1, text, l, yl);
        if ( spos != -1 ) {
            var viite = hae_sisin(0, text, spos+1);
            return viite;
        }
        return null;
    }

    /**
     * Palauttaa kentän sisältämät kentät taulukkona.
     **/
    function hae_alakentat(text, kentta, alkaen) {
        assert ( alkaen >= kentta[0] && alkaen < kentta[1], "hae_alakentat" );
        var sisakentta = Wikileike.hae_sisakentta(text, kentta);
        var kentat = [];
        var alakentta = [alkaen, alkaen];
        while ( (alakentta = seuraava_kentta(text, alakentta, kentta)) 
                != null) {
            var nimi = Wikileike.hae_kentan_otsikko(text, alakentta);
            var s = Wikileike.hae_sisakentta(text, alakentta);
            kentat.push([nimi, alakentta[0], alakentta[1], s[0], s[1]]);
        }
        return kentat;
    }

    /**
     * Hakee ensimmäisen kentan.
     */
    function eka_kentta(text, pos) {
        var eka = seuraava_kentta(text, [pos, pos], [-1, text.length]);
        if ( eka ) {
            var ylakentta = hae_ylakentta(text, eka);
            var kentat = taulukoi(text, ylakentta, pos);
	    console.log("Kentat(eka kentta): ", kentat);
            if ( kentat.length > 0 ) {
                var kentta = [ kentat[0][1], kentat[0][2], kentat[0][0] ];
                return kentta;
            }
        }
        return null;
    }
    
    /**
     * Palauttaa kentan 'kentta' yläkentän eli kentän, jossa 'kentta' 
     * sijaitsee. Olettaa että 'kentta'[0],[1] on kentän alku- ja 
     * loppukohdat.
     */
    function hae_ylakentta(text, kentta) {
        return hae_sisin(0, text, kentta[1]);
    }

    /**
     * Hakee edellisen kentan alku- ja loppukohdat. 'kentta'[0],[1] täytyy 
     * osoittaa olemassa olevan kentan alku- ja loppukohtiin.
     **/
    function edellinen_kentta(text, kentta) {
        var spos = hae_edellinen(-1, text, kentta[0]);
        if ( spos != -1 ) {
            var kentta = hae_sisin(0, text, spos-1);
            return kentta;
        }
        return null;
    }

    /**
     * Palauttaa kentän 'nykykentta' indeksin 'taulukoi'-funktion tekemässä 
     * taulkossa 'kentat'.
     */
    function hae_kentan_i(kentat, nykykentta) {
        for ( var i = 0; i < kentat.length; i++ ) {
            if ( kentat[i][1] == nykykentta[0] 
                 && kentat[i][2] == nykykentta[1] ) {
                return i;
            }
        }

        assert ( nyky_i != -1, "hae_kentan_i: nykykenttää ei löydy" );
        return -1;
    }

    /**
     * Tekee kentästä 'ylakentta' taulukon alkaen kohdasta 'alkaen'. Järjestää 
     * taulukon ensisijaisesti kentän numeron ja toissijaisesti sijainnin 
     * mukaan.
     */
    function taulukoi(text, ylakentta, alkaen) {
        assert ( alkaen >= ylakentta[0] && alkaen < ylakentta[1], "taulukoi");

        var kentat = hae_alakentat(text, ylakentta, alkaen);
        kentat.sort(function(a, b) { 
            if ( a[0] == b[0] ) { 
                return a[1] - b[1]; 
            } else if ( a[0] == "" ) { 
                return +1;
            } else if ( b[0] == "" ) { 
                return -1;
            } else { 
                return a[0] - b[0]; 
            } 
        });
        return kentat;
    }

    /**
     * Palauttaa kentän 'nykykenttä' n:nnen seuraavan tai edellisen 
     * sisarkentän tai null, jos kenttiä ei ole.
     * Esim. suunta = -1 palauttaa edellisen kentän.
     */
    function hae_sisarkentta(text, nykykentta, suunta) {
        var ylakentta = hae_ylakentta(text, nykykentta);
        
        var kentat = taulukoi(text, ylakentta, ylakentta[0]+1);
        if ( suunta < 0 ) {
            kentat.reverse(); 
        }
	console.log("Kentat(sisarkentta): ", kentat);	

        var nyky_i = hae_kentan_i(kentat, nykykentta);
        var indeksi = nyky_i+Math.abs(suunta);
        if ( indeksi < kentat.length && kentat[indeksi] ) {
            var tmp = kentat[indeksi];
            return [tmp[1], tmp[2], tmp[0]];
        }

        // Seuraavaa kenttää ei ole.
        return null;
    }
    
    /**
     * Asettaa kenttien, joiden nimi on 'nimi', sisällöksi 'arvon' kentän 
     * 'ylakentta' alla alkaen kohdasta 'alkaen'. 
     * Huom! Muuttaa tekstilaatikon sisältöä.
     */
    function aseta_kentat_arvoon(text, ylakentta, alkaen, nimi, arvo) {
        var kentat = hae_alakentat(text, ylakentta, alkaen);
        var a = 0, text_o = [];
        for ( e in kentat ) {
            if ( kentat[e][0] == nimi ) {
                text_o.push(text.substring(a, kentat[e][3]) + arvo);
                a = kentat[e][4];
            }
        }
        text_o.push(text.substring(a));
        $textbox.val(text_o.join(""));
    }

    /**
     * Palauttaa true tai false sen mukaan, onko kursorin ympäristön 
     * merkin poisto sallittu.
     * suunta: kursoriin lisättävä etäisyys (+1/-1).
     **/
    function sallittu(suunta) {
        var text = $textbox.val();
        var ppos = sel.hae_kohdat();

        // Sallitaan, jos tekstiä on valittu ja painetaan <backspace>a, 
        // vaikka edellinen olisi kielletty merkki, koska <backspace>n 
        // painaminen tässä tilanteessa poistaa vain valitun tekstin 
        // eikä edellistä merkkiä.
        if ( suunta < 0 && ppos[0] != ppos[1] ) return true;
        
        if ( ppos[0] >= 0 && ppos[0] < text.length ) {
            var merkki = text[ppos[0] + suunta];
            if ( merkki == "∷" || merkki == "⦃" || merkki == "⦄" ) {
                return false;
            }
        }
        return true;
    }

    function run($) {

        $textbox = $("#wpTextbox1");

	/**
	 * Näppäimistösyötteen käsittely.
	 **/
        $textbox.on('keydown', $textbox, function(e) { 
            var keyCode = e.keyCode || e.which; 

            // Estetään kenttämerkkien tahaton poisto.
            if ( keyCode == 8 ) {         // <backspace>
                if ( !sallittu(-1) ) {
                    return false;
                }
            } else if ( keyCode == 46 ) { // <del>
                if ( !sallittu(0) ) {
                    return false;
                }
            } else if ( keyCode == 9 ) {  // <tab>
                e.preventDefault();
                if ( e.shiftKey ) {
                    Wikileike.hyppaa_edelliseen();
                } else {
                    Wikileike.hyppaa_seuraavaan();
                }
            } else if ( keyCode == 27 ) {  // <esc>
                e.preventDefault();
                Wikileike.laajenna_leike();
            }
            
            //console.log("which: " + e.which);
        });


	/**
	 * Tekstin tallennuksen käsittely.
	 *
         * Siivotaan varmuuden vuoksi ja tarkistetaan jäikö merkkejä 
         * ennen kuin teksti lähetetään.
	 **/
        $("#editform").on('submit', function () {
            var ret = siivoa($textbox.val());
            var text = ret[0], upos = ret[1];
            if ( text.search(/[⦃⦄∷]/) != -1 ) {
                if ( confirm("Kaikkia kenttiä ei voitu sulkea. "
			     + "Poistetaanko ⦃⦄∷-merkit väkisin ja tallennetaan sivu?") ) {
		    $textbox.val(text.replace(/⦃.*?∷/, "").replace(/[⦃⦄∷]/g, ""));
		    return true;
		}
                return false;
            } else {
                $textbox.val(text);
                return true;
            }
        });

        Wikileike.leikkeet = Leikkeet2.getSnippets();
    }

    window.Wikileike = Wikileike;

    if ( (mw.config.get("wgAction") === "edit" || mw.config.get("wgAction") === "submit") 
         && !(mw.config.get("wgPageName").endsWith(".js") 
              || mw.config.get("wgPageName").startsWith("Moduuli:")) ) {
        jQuery(document).ready(run);
    }

})();

/**
 * Sivujen muokkaustilassa näytettävä käyttöliittymäkoodi. 
 *
 * Näyttää leikkeet luettelona ja linkit ohje- ja hallintasivulle työkalupalkissa.
 **/
(function(){

    /**
     * Täyttää listan localStoragesta ladatun listan mukaiseksi.
     * 
     * @param $leikelista: select-elementti (jQuery-kääreessä)
     **/
    function populateList($leikelista) {
        var i, j,
            keys,
	    $optSnp,
	    $optGrp,
	    tree = Leikkeet2.loadSnippetTree();
	
        $leikelista.append('<option>- Lisää tekstileike -</option>');

	for ( i in tree ) {
	    group = tree[i];
	    $optGrp = $('<optgroup/>');
	    $optGrp.attr("label", group.name);	    
	    for ( j in group.list ) {
		snippet = group.list[j];
		$optSnp = $('<option/>');
		$optSnp.html(snippet.key);
		$optSnp.attr("title", snippet.content);
		$optGrp.append($optSnp);		
	    }
	    $leikelista.append($optGrp);
	}
    };

    /**
     * Tekee hakasuluilla ympäröidyn linkin.
     * 
     * @param text:   linkin teksti
     * @param title:  linkin title-attribuutti
     * @param url:    linkin url
     * @param fontsz: fontin koko css-muodossa tai null
     **/
    function makeLinkButton(text, title, url, fontsz) {
        var $lnk,
	    $span;
	
	$lnk = $('<a href="">' + text + '</a></div>');
	$span = $('<span class="ohjenappi"/>');
	$span.html([ "[", $lnk, "]" ]);
	if ( fontsz ) {
	    $lnk.css("font-size", fontsz);
	}
        $lnk.attr("target", "_blank");
        $lnk.attr("href", url);
        $lnk.attr("title", title);
	
	return $span;
    }

    /**
     * Pääohjelma.
     **/
    function run() {
        var $toolbar,
	    $ohje_span,
	    $manage_span,
	    $leikelista = $('<select/>'),
            $toolbar = $('<span id="wl-alue"/>'),
	    $textbox = $("#wpTextbox1");
	

	$ohje_span = makeLinkButton('?', 'Avaa "Tekstileikkeet"-pienoisohjelman ohje.', Leikkeet2.helpUrl, null);
	$manage_span = makeLinkButton('Hallitse leikkeitä',
				      'Siirry leikkeiden hallintasivulle.', Leikkeet2.getManagePageUrl(), "90%");	

        $toolbar.append([ $leikelista, " ", $manage_span, " ", $ohje_span ]);

        /* Jos joku toinen gadgetti on jo lisännyt työkalupalkin, lisätään siihen
	   muuten luodaan oma. */
	(function () {
	    var $tb = $("#tyokalupakki");
            if ( $tb.length > 0 ) {
		$tb.append([ '<span class="tyokaluerotin" style="color: gray;">'
			     + ' | </span>', $toolbar ]);
            } else {
		$tb = $('<div id="tyokalupakki" />');
		$textbox.before($tb);
		$("#tyokalupakki").append($toolbar);
	    }
	}());

	populateList($leikelista);

	/**
	 * Leikelistasta valinta.
	 *
	 * Kutsutaan, kun leikelistasta valitaan uusi leike.
	 **/
        $leikelista.change(function () {
            var snippetname = $(this).val();

	    // Valitaan "- Lisää leike -" -teksti takaisin.
            $(this).children(':nth(0)').prop('selected', true);
	    
            $textbox.textSelection('replaceSelection', snippetname);
	    $textbox.focus();
            Wikileike.laajenna_leike();
        });
    }


    if ( mw.config.get("wgAction") == "edit" || mw.config.get("wgAction") == "submit" 
         && !(mw.config.get("wgPageName").endsWith(".js") 
              || mw.config.get("wgPageName").startsWith("Moduuli:")) 
	 && (mw.config.get("wgPageName") != "Järjestelmäviesti:Gadget-Tekstileikkeet.js") ) {
        jQuery(document).ready(run);
    }

})();

/**
 * Leikkeiden hallintasivun koodi.
 **/
if ( mw.config.get("wgTitle") === Leikkeet2.getManagePageTitle() ) {
    
    (function () {
	var __g = {
	    /**
	     * Asetetaan trueksi, kun tehdään muutoksia, ja falseksi
	     * tallennettaessa.
	     **/
	    stateChanged : false,

	    /**
	     * Dict varatuista leikkeennimistä. Esim. { "leike" : { }, ... }.
	     **/
	    snippets : {},

	    /**
	     * Dict varatuista ryhmännimistä. Esim. { "ryhmä" : true, ... }.
	     **/
	    g_groups : {}
	};
	
	
	/**
	 * Kysyy varmistusta ja poistaa leikkeen.
	 **/
	function deleteLnk_askDeleteSnippet() {
	    var $lnk = $(this);
	    var $container = $lnk.parent();
	    var $span = $container.find('.wl-snippet-key');
	    var $pre = $container.find('.wl-snippet-val');	
	    var snippetName = $span.text();
	    var snippetContent = $pre.text();
	    
	    if ( window.confirm('Haluatko varmasti poistaa leikkeen "' + snippetName +
				'"?\nSisältö:\n    ' + snippetContent.replace(/\n/g, "\n    ")) ) {
		$container.remove();
		updateWorkingData();
	    }

	    return false;
	}

	/**
	 * Kysyy varmistusta ja poistaa ryhmän.
	 **/
	function deleteLnk_askDeleteGroup() {
	    var $lnk = $(this);
	    var $container = $lnk.parent();
	    var $groupLbl = $container.find('.wl-group-name');
	    var groupName = $groupLbl.text();
	    var groupContent = Array.map($container.find('.wl-snippet-key'), function (item) { return item.innerHTML }).join(",\n  * ");
	    
	    if ( window.confirm('Haluatko varmasti poistaa ryhmän "' + groupName +
				'"?\nSisältö:\n    ' + groupContent) ) {
		$container.remove();
		updateWorkingData();
	    }

	    return false;
	} 
	
	/**
	 * Tallentaa leikkeen ja sulkee editoinnin.
	 **/
	function saveBtn_saveEditSnippet() {
	    var $closeBtn  = $(this);
	    var $editor    = $closeBtn.parents('.wl-editor');
	    var $container = $editor.parent();
	    var $groupContainer = $container.parent().parent();
	    var $span = $container.find('.wl-snippet-key');
	    var $pre = $container.find('.wl-snippet-val');
	    var $inp = $editor.find('input[type="text"]');
	    var $ta  = $editor.find('textarea');
	    var $groupLabel = $groupContainer.find('.wl-group-name');
	    var oldName = $span.text();
	    var newName = $inp.val();
	    var content = $ta.val();
	    var group = $groupLabel.text();

	    if ( newName === "" ) {
		alert("Leikkeellä pitää olla nim!i");
		return false;
	    }
	    
	    if ( newName != oldName && (newName in __g.snippets) ) {
		alert('Leike "' + newName + '" on jo olemassa!');
		return false;
	    }
	    
	    if ( content === "" ) {
		alert('Leikkeellä pitää olla sisältö!');
		return false;
	    }

	    $span.text(newName);
	    $pre.text(content);

	    // Palautetaan siirrettävyys, jos se oli pois päältä.
	    // Koskee vain tuontia.
	    if ( $span.parent().hasClass('snippet-conflicting') ) {
		$span.parent().addClass('snippet-new');
		$span.parent().removeClass('snippet-conflicting')
	    }
	    
	    $editor.remove();
	    
	    updateWorkingData();

	    // Laukaistaan eventti listassa.
	    $container.change();
	    
	    return false;
	}
	

	/**
	 * Tallentaa ryhmän ja sulkee editoinnin. Tallennus-painikkeesta.
	 **/
	function saveBtn_saveEditGroup() {
	    var $closeBtn  = $(this);
	    var $editor    = $closeBtn.parents('.wl-editor');
	    var $container = $editor.parent();
	    var $groupLbl = $container.find('.wl-group-name');
	    var $inp = $editor.find('input[type="text"]');
	    var oldName = $groupLbl.text();
	    var newName = $inp.val();

	    if ( newName == "" ) {
		alert("Ryhmällä pitää olla nimi!");
		return;
	    }
	    
	    if ( newName in __g.groups ) {
		alert('Ryhmä nimeltä "' + newName + '" on jo olemassa.') ;
		$inp.focus();
		return false;
	    }

	    $groupLbl.text(newName);
	    $editor.remove();

	    updateWorkingData();

	    // Laukaistaan eventti listassa.
	    $container.change();
	    
	    return false;
	}


	/**
	 * Sulkee leikkeen editoinnin tallentamatta. Peruutus-painikkeesta.
	 **/
	function cancelBtn_cancelEditSnippet() {
	    var $closeBtn  = $(this);
	    var $editor    = $closeBtn.parents('.wl-editor');
	    var $container = $editor.parent();
	    var $editLnk   = $container.find('a[title="Muokkaa leikettä"]');
	    var $span      = $container.find('.wl-snippet-key');

	    // Jos span oli tyhjä, oli kyseessä uuden leikkeen luonti. Poistetaan koko höskä.
	    if  ( $span.text() == "" ) {
		$container.remove();
	    } else {
		$editor.remove();
	    }

	    return false;
	}

	/**
	 * Sulkee ryhmän editoinnin tallentamatta. TODO
	 **/
	function cancelBtn_cancelEditGroup() {
	    var $closeBtn  = $(this);
	    var $editor    = $closeBtn.parents('.wl-editor');
	    var $container = $editor.parent();
	    var $groupLbl  = $container.find('.wl-group-name');
	    var $editLnk   = $container.find('a[title="Muokkaa ryhmää"]');
	    var name       = $groupLbl.text();

	    // Jos groupLbl oli tyhjä, oli kyseessä uuden ryhmän luonti. Poistetaan koko höskä.
	    if  ( name == "" ) {
		$container.remove();
	    } else {
		$editor.remove();
	    }

	    return false;
	}

	/**
	 * Avaa editorielementin annetulle .snippet- tai .group-elementille.
	 **/
	function openEditor($container, text1, text2, saveBtn_saveEdit, cancelBtn_cancelEdit) {
	    var $editor = $('<div class="wl-editor" style="overflow: auto; width: 100%;"></div>');
	    var $buttonCont = $('<div style="float: right;"></div>');
	    
	    $editor.append('<input type="text" value="' + text1 + '" />');
	    if ( text2 !== null ) {
		$editor.append('<textarea>' + text2 + '</textarea>');
	    }
	    $buttonCont.append('<input type="button" value="Tallenna" />');
	    $buttonCont.append('<input type="button" value="Peruuta" />');
	    $editor.append($buttonCont);
	    
	    $buttonCont.find('input[value="Tallenna"]').on('click', saveBtn_saveEdit);
	    $buttonCont.find('input[value="Peruuta"]').on('click', cancelBtn_cancelEdit);
	    
	    // Lisätään poistonapin jälkeiseksi elementiksi.
	    $container.children('a[title="Poista leike"], a[title="Poista ryhmä"]').after($editor);
	    
	    $editor.find('input[type="text"]').focus();
	}
	
	/**
	 * Avaa leikkeen editoitavaksi.
	 **/
	function editLnk_openEditSnippet() {
	    var $editLnk = $(this);
	    var $container = $editLnk.parent();
	    var $span = $container.find('.wl-snippet-key');
	    var $pre  = $container.find('.wl-snippet-val');
	    var key = $span.text();
	    var val = $pre.text();
	    var $editor = $container.children('.wl-editor');

	    if ( $editor.length > 0 ) {
		$editor.remove();
	    } else {
		openEditor($container, key, val, saveBtn_saveEditSnippet, cancelBtn_cancelEditSnippet);
	    }
	    return false;
	}

	/**
	 * Avaa ryhmän editoitavaksi.
	 **/
	function editLnk_openEditGroup() {
	    var $editLnk = $(this);
	    var $container = $editLnk.parent();
	    var $groupLbl = $container.find('.wl-group-name');
	    var groupName = $groupLbl.text();
	    var $editor = $container.children('.wl-editor');	

	    if ( $editor.length > 0 ) {
		$editor.remove();
	    } else {
		openEditor($container, groupName, null, saveBtn_saveEditGroup, cancelBtn_cancelEditGroup);
	    }
	    return false;
	}

	/**
	 * Tekee leikelistan elemetin.
	 **/
	function makeSnippetItem(key, val) {
	    var $item = $('<li class="wl-snippet"></li>');
	    $item.append([ '<span class="wl-snippet-key">' + key + '</span>',
			   ' ',
			   '<a href="" title="Muokkaa leikettä">[m]</a>',
			   '<a href="" title="Poista leike">[p]</a>',
			   '<pre class="wl-snippet-val"">' + val + '</pre>' ]);
	    $item.find('a[title="Muokkaa leikettä"]').on('click', editLnk_openEditSnippet);
	    $item.find('a[title="Poista leike"]').on('click', deleteLnk_askDeleteSnippet);
	    
	    return $item;
	}

	/**
	 * Tekee leikelistan, jossa on lisäysnappi valmiina.
	 **/
	function makeSnippetList() {
	    var $list = $('<ul class="wl-snippet-list"></ul>');
	    var $adder = $('<li class="wl-snippet-adder"></li>');
	    $adder.append('<a href="" title="Uusi leike">[uusi leike]</a>');
	    $adder.children('a[title="Uusi leike"]').on('click', newLnk_openAddSnippet);
	    
	    $list.append($adder);
	    if ( $list.sortable ) {
		$list.sortable({ connectWith: ".wl-snippet-list",
				 items: ".wl-snippet, .snippet-new",
				 change: list_sortChange });
	    }
	    return $list;
	}

	/**
	 * Tekee ryhmälistan elementin, jossa on sisälle tyhjä leikelista.
	 **/
	function makeGroupItem(name) {
	    var $snippetList = makeSnippetList();
	    var $groupItem = $('<li class="wl-group mw-collapsible"></li>').append([
		'<b class="wl-group-name">' + name + '<b/>',
		' ',
		'<a href="" title="Muokkaa ryhmää">[m]</a>',
		'<a href="" title="Poista ryhmä">[p]</a>',
		' ',
		$snippetList ]);
	    $groupItem.find('a[title="Muokkaa ryhmää"]').on('click', editLnk_openEditGroup);
	    $groupItem.find('a[title="Poista ryhmä"]').on('click', deleteLnk_askDeleteGroup);
	    
	    
	    return $groupItem;
	}


	/**
	 * Avaa leikkeen editoitavaksi.
	 **/
	function newLnk_openAddSnippet() {
	    var $newLnk = $(this);
	    var $adder = $newLnk.parent();
	    var $item = makeSnippetItem("", "");

	    $adder.before($item);
	    
	    $item.children('a[title="Muokkaa leikettä"]').click();

	    return false;
	}
	

	/**
	 * Avaa ryhmän editoitavaksi.
	 **/
	function newLnk_openAddGroup() {
	    var $newLnk = $(this);
	    var $groupAdder  = $newLnk.parent();
	    var $groupItem   = makeGroupItem("");
	    
	    $groupAdder.before($groupItem);
	    
	    $groupItem.children('a[title="Muokkaa ryhmää"]').click();

	    return false;
	}

	/**
	 * Tallentaa puunäkymän leikkeet localStorageen.
	 **/
	function saveSnippetTree() {
	    var groupList = getGroupList($('#wl-group-list'));

	    var out = Leikkeet2.decodeSnippets(groupList);
	    Leikkeet2.saveSnippetTree(out);
	    __g.stateChanged = false;
	    //window.location.reload();
	}

	/**
	 * Tallentaa puunäkymän leikkeet localStorageen.
	 **/
	function loadSnippetTree() {
	    var groupList = Leikkeet2.loadSnippetTree();
	    __g.stateChanged = false;

	    if ( groupList ) {
		return Leikkeet2.encodeSnippets(groupList);
	    }
	    return null;
	}

	/**
	 * Lukee groupListin tiedot sivun elementeistä.
	 *
	 * @return: esim. [ { "name" : "ryhmä a", 
	 *                    "index" : 0, 
	 *                    "list" : [ { "key" : "leike a1", 
	 *                                 "index" : 0, 
	 *                                 "content": "AAAAA" }, 
	 *                               { "key" : "leike a2", 
	 *                                 "index" : 1, 
	 *                                 "content" : "AAA" } ] }, 
	 *                  { "name" : "ryhmä b", ... ] } ]
	 **/
	function getGroupList($listRoot) {
	    var rootList = [];
	    $listRoot.children(".wl-group").each(function () {
		var $this = $(this);
		var group = $this.children('.wl-group-name').text();
		rootList.push({
		    "name" : group,
		    "index": rootList.length,
		    "list" : [] });
		
		$this.children('.wl-snippet-list').children('.wl-snippet').each(function () {
		    var $this = $(this);
		    rootList[rootList.length - 1].list.push({
			"key"     : $this.children('.wl-snippet-key').text(),
			"index"   : rootList[rootList.length - 1].list.length,
			"content" : $this.children('.wl-snippet-val').text() });
		});
	    });

	    return rootList;
	}

	/**
	 * Päivittää varattujen nimien dictit annetun ryhmälistan mukaiseksi.
	 *
	 * @param groupList: ryhmän mukaan järjestetty lista leikelistoista.
	 **/
	function loadWorkingData(groupList) {
	    var i, j;
	    var group;
	    var snippet;

	    __g.snippets = {};
	    __g.groups   = {};
	    
	    for ( i in groupList ) {
		group = groupList[i];
		for ( j in group.list ) {
		    snippet = group.list[j];
		    __g.snippets[snippet.key] = group.list[j];
		}
		__g.groups[group.name] = groupList[i];
	    }

	    __g.stateChanged = true;
	}
	
	/**
	 * Päivittää varattujen nimien datan nykyisen puun mukaiseksi.
	 **/
	function updateWorkingData() {
	    var byGroup = getGroupList($('#wl-group-list'));
	    loadWorkingData(byGroup);
	}
	
	/**
	 * "Kääntää" taulukon siten, että tiedot on järjestetty ryhmän mukaan.
	 * TODO: Onko turha?
	 **/
	function snippetDictToGroupList(snippets) {
	    var turned = {};
	    var group;
	    var content;
	    var key;
	    var byGroup = [];
	    
	    for ( key in snippets ) {
		group   = snippets[key].group;
		content = snippets[key].content;
		index = snippets[key].index;
		
		if ( !(group in turned) ) {
		    turned[group] = [];
		}
		
		turned[group].push({ "key" : key,
				     "content" : content,
				     "index" : index });
	    }

	    for ( key in turned ) {
		// Lajitellaan nousevaan järjestykseen avaimen mukaan.
		byGroup.push({
		    "name" : key,
		    "list" : turned[key].sort(function (a, b) { return a.index - b.index; }),
		    "index": byGroup.length
		});
	    }
	    
	    return byGroup.sort(function (a, b) { return a.index - b.index; });
	}

	/**
	 * Kutsutaan kun ryhmä- tai leikelistaa muutetaan.
	 **/
	function list_sortChange(e) {
	    var $list = $(this)
	    var $adder = $list.children('.wl-snippet-adder, .wl-group-adder');

	    // Varmistetaan, että lisäysnappi jää alimmaksi.
	    $list.append($adder.detach());
	    __g.stateChanged = true;
	}

	/**
	 * Kutsutaan kun nimi (TODO) tai jokin jäsenistä muuttuu.
	 **/
	function group_onChange() {
	    var $this = $(this);
	    if ( ! __g.groups[$this.children('.wl-group-name').text()] ) {
		return;
	    }
	    // Tarkistetaan onko jäseniä joita ei voi liikuttaa.
	    if ( $this.children('.wl-snippet_conflicting').length == 0 ) {
		$this.removeClass('group-conflicting').addClass('group');
	    } else {
		$this.addClass('group-conflicting').removeClass('group');
	    }
	}


	/**
	 * Antaa koko puun JSON-muodossa kopiotavaksi.
	 **/
	function openExportSnippetTree() {
	    var $imex = $('#wl-import-export');
	    var $ta   = $('#wl-import-export-data');
	    var groupList  = getGroupList($('#wl-group-list'));
	    var $groupList = $('#wl-group-list');
	    
	    $ta.val(JSON.stringify(Leikkeet2.decodeSnippets(groupList), null, 2));
	    $(".wl-snippet-ui-edit").css('display', 'none');
	    $(".wl-snippet-ui-export").css('display', '');	    
	    $ta.focus().select();
	}

	/**
	 * Antaa koko puun JSON-muodossa kopiotavaksi.
	 **/
	function openImportSnippetTree() {
	    var $imex      = $('#wl-import-export');
	    var $ta        = $('#wl-import-export-data');
	    var $groupList = $('#wl-group-list');

	    $(".wl-snippet-ui-edit").css('display', 'none');	    
	    $(".wl-snippet-ui-import").css('display', '');
	    $ta.val("");
	    $ta.focus();
	}

	function closeImportExport() {
	    $(".wl-snippet-ui-import").css('display', 'none');
	    $(".wl-snippet-ui-export").css('display', 'none');	    
	    $(".wl-snippet-ui-edit").css('display', '');	    
	}

	/**
	 * Rakentaa editoitavan puun annetusta datasta annettuun elementtiin.
	 **/
	function makeSnippetTree($groupList, groupList) {
	    var $snippetList;	
	    var $groupItem;	
	    var $snippetItem;
	    var $snippetAdder;
	    var i, j;
	    var group;

	    $groupList.empty();

	    for ( i in groupList ) {
		__g.groups[groupList[i].name] = true;
		group         = groupList[i].list;
		$groupItem    = makeGroupItem(groupList[i].name);
		$snippetList  = $groupItem.children(':last-child');
		$snippetAdder = $snippetList.children(":last-child");
		
		for ( j in group ) {
		    $snippetItem = makeSnippetItem(group[j].key, group[j].content);
		    if ( group[j].flag === "new" ) {
			$snippetItem.addClass('snippet-new');
		    } else if ( group[j].flag === "deleted" ) {
			$snippetItem.addClass('snippet-deleted');
		    } else if ( group[j].flag === "conflict" ) {
			$snippetItem.addClass('snippet-conflicting');
			// Poistetaan siirrettävyys kunnes nimi on muutettu. TODO
			$snippetItem.removeClass('snippet');
			$groupItem.removeClass('group');
		    } 
		    $snippetAdder.before($snippetItem);
		}
		$groupItem.on('change', group_onChange);
		$groupList.append($groupItem);
	    }

	    $groupList.append($('<li class="wl-group-adder"></li>')
			      .append($('<a href="">[uusi ryhmä]</a>')
				      .on('click', newLnk_openAddGroup)));
	    if ( $groupList.sortable ) {
		$groupList.sortable({
		    connectWith: ".wl-group-list",
		    change: list_sortChange,
		    items : '.wl-group',
		    placeholder: "sortable-placeholder" });
	    }
	}

	function importSnippetTree() {
	    var $ta = $('#wl-import-export-data');
	    var importedGroupList = JSON.parse($ta.val());

	    __g.stateChanged = true;	    
	    makeSnippetTree($('#wl-group-list'), importedGroupList);
	    closeImportExport();
	}

	/**
	 * Koodi, jota kutsutaan kun sivu on ladattu.
	 **/
	function main() {
	    var $div       = $('<div id="wl-main"></div>');
	    var $divEdit   = $('<div class="wl-snippet-ui-edit"></div>');
	    var $groupList = $('<ul class="wl-group-list" id="wl-group-list"></ul>');
	    // Importissa ja exportissa käytettävä tekstilaatikko.
	    var $ta        = $('<textarea id="wl-import-export-data"></textarea>'); 
	    var $divImEx   = $('<div id="wl-import-export" class="wl-snippet-ui-import snippet-ui-export"></div>');
	    var groupList  = loadSnippetTree();
	    
	    loadWorkingData(groupList);
	    __g.stateChanged = false;
	    makeSnippetTree($groupList, groupList);

	    $divImEx.append([
		$('<h2 class="wl-snippet-ui-import">Tuo</h2>'),
		$('<h2 class="wl-snippet-ui-export">Vie</h2>'),		
		$('<p class="wl-snippet-ui-import">Kopioi tallenetut leikkeet alla olevaan laatikkoon.<br/><br/>Huom. OK:n painaminen korvaa vanhat leikkeet uusilla!</p>'),
		$ta,
		$('<button class="wl-snippet-ui-export">Sulje</button>').on('click', closeImportExport), 		
		$('<button class="wl-snippet-ui-import">Peruuta</button>').on('click', closeImportExport),
		$('<button class="wl-snippet-ui-import">OK</button>').on('click', importSnippetTree) ]);
	    
	    $divEdit.append([
		'<h2>Leikkeet</h2>',
		$('<button title="Tallenna muutokset">Tallenna</button>').on('click', saveSnippetTree),
		$groupList,
		"<br/><hr/>",
		$('<button title="Tuo tallennetut leikkeet">Tuo</button>').on('click', openImportSnippetTree),
		$('<button title="Ota kopio leikkeistä">Vie</button>').on('click', openExportSnippetTree) ]);

	    $div.append([ $divEdit,
			  $divImEx ]);

	    $("#mw-content-text").replaceWith($div);

	    $(".wl-snippet-ui-import").css('display', 'none');
	    $(".wl-snippet-ui-export").css('display', 'none');
	    
	    // Kysytään käyttäjältä varmistusta ennen sivulta siirtymistä, jos tietoja
	    // on tallentamatta.
	    window.addEventListener('beforeunload', function (e) {
		if ( __g.stateChanged ) {
		    e.preventDefault();
		    e.returnValue = '';
		}
	    });
	}
	
	jQuery(document).ready(main);
    }());
}