Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
 * Permet de montrer dans un texte les erreurs les plus communes.
 *
 * Auteur: Phe
 *
 * TODO:
 *
 * Permettre des auto-corrections.
 *
 * Le support en mode édition est très expérimental, il manque un bouton refresh à
 * la liste des erreurs pour éviter d'avoir à faire un preview quand on a corrigé.
 * Certaines erreurs ne seront visible que dans le html, « . <minuscule> » par
 * exemple s'il y a y un \n dans le wikicode entre le . et la minuscule.
 * Le wikicode n'est pas strippé ce qui doit produire des tas de faux positifs.
 *
 * En mode édition cliquer sur un lien se positionne toujours sur la première
 * erreur.
 *
 * En mode édition il arrive fréquemment qu'une erreur détecté ne puisse
 * être localisé dans la boîte d'édition.
 *
 * Le contenu d'une textarea est un nœud texte qui est splitter en plusieurs
 * nœud html, le browser à l'air assez futé pour comprendre qu'un textarea ne
 * peut contenir qu'un nœud texte et ignore les trucs insérer mais ça ne
 * va pas du tout (incompatibilité avec le gadget auteur form par exemple -->
 * ns:104 ignoré pour l'instant).
 *
 */

/*
 * http://stackoverflow.com/questions/5458655/jquery-scroll-textarea-to-given-position 
 *
 * Construit un div avec la même font que la textarea pouvoir calculer la position de scroll.
 */
jQuery.fn.scrollToText = function(search) {
    var text = $(this).text();
    var charNo = text.indexOf(search);
    var anch = '<span id="anch"></span>';
    text = text.substring(0, charNo) + anch + text.substring(charNo);

    var copyDiv = $('<div></div>')
                    .append(text.replace(/\n/g, '<br />'))
                    .css('width', $(this).innerWidth()) // width without scrollbar
                    .css('font-size', $(this).css('font-size'))
                    .css('font-family', $(this).css('font-family'))
                    .css('padding', $(this).css('padding'));

    copyDiv.insertAfter($(this));
    var pos = copyDiv.find('SPAN#anch').offset().top - copyDiv.find('SPAN#anch').closest('DIV').offset().top;
    pos = pos - Math.round($(this).innerHeight() / 2);
    $(this).scrollTop(pos);
    copyDiv.remove();
};

select_multiple_text = {

    precompile_regexp : function (search) {
        var temp = [];
        for (var i in search)
            temp.push(new RegExp(search[i], "g"));
        return temp;
    },

    build_popup : function() {
        var html = "";
        $('span.Erreurs-communes').each(function (index, el) {
            //$(this).removeAttr('id');
            html += "<li><a href='javascript:select_multiple_text.scroll_to_pos(" + index + ");'>" + $(this).text() + "</a></li>";
            $(this).attr('id', 'Err-' + index);
        });
        
        return html;
    },

    last_scroll_pos : 0,

    scroll_to_pos : function(index) {
        var found = false;
        if ($.inArray(mw.config.get('wgAction'), [ 'submit', 'edit' ]) != -1) {
            var textbox = document.getElementById("wpTextbox1");
            if (textbox) {
                 var text = $("#Err-" + index).text();
                 var pos = textbox.value.indexOf(text);
                 if (pos != -1) {
                    textbox.selectionStart = pos;
                    textbox.selectionEnd = pos + text.length;
                    textbox.focus();
                    $('#wpTextbox1').scrollToText(text);
                    found = true;
                }
            }
        }
        if (!found) {
            $("#Err-" + select_multiple_text.last_scroll_pos).css("background-color", "");
            window.location.hash = "#Err-" + index;
            $("#Err-" + index).css("background-color", "#00FF00");
            select_multiple_text.last_scroll_pos = index;
            // index * 2 + 1 is voodoo making assumption on the html struct of the
            // popup, see build_popup(), there is surely a simpler and safer way.
            $('#err-communes-popup').find('*:eq(' + (index * 2 + 1) + ')').focus();
        }
    },

    close_popup : function() {
        $("#err_summary").remove();
        return;
    },

    open_popup : function() {
        if ($("#err_summary").remove().length)
            return;

        var elt = $("<div id='err_summary'></div>");
        elt.css({ 
            "position" : "fixed",
            "min-width" : "16em",
            "max-width" : "35em",
            "max-height" : "35em",
            "right" : "0.5em",
            "background-color" : "#FFFFFF",
            "z-index" : 1,
            "border" : "1px solid",
            "padding" : "8px"
        });

        var str = '<div style="float:right;"><a href="javascript:select_multiple_text.close_popup();"><img src="//upload.wikimedia.org/wikipedia/commons/9/97/WikEd_close.png"/></a></div><h4>'+ 'Erreur(s) possible' + ' :</h4><div style="overflow:auto;min-width:16em;max-width:35em;max-height:32em;"><ul id="err-communes-popup">' + select_multiple_text.build_popup() + '</ul></div>'; 

        elt.html(str);

        $("#bodyContent").prepend(elt);
    },

    match : function (text, regexp) {
        var m = text.match(regexp);
        if (m) {
            var start_match = text.search(regexp);
            var end_match = start_match + m[0].length;
            return [ start_match, end_match, m[0] ];
        }
        return null;
    },

    change_text_node : function(el, search, first) {
        for (var cur = first; cur < search.length; ++cur) {
            var match = this.match(el.nodeValue, search[cur]);
            if (match) {
                var tail = $(el).clone()[0];

                el.nodeValue = el.nodeValue.substring(0, match[0]);
                tail.nodeValue = tail.nodeValue.substring(match[1]);

                // FIXME: how to setup the text part of the span w/o injecting it in html ?
                $(el).after(tail).after($("<span class='Erreurs-communes'>" + match[2] + "</span>"));

                select_multiple_text.change_text_node(el, search, cur + 1);
                select_multiple_text.change_text_node(tail, search, cur);
            }
        }
    },

    /* CODE EXPÉRIMENTAL POUR SURLIGNER LES SCANILLES DÈS LE MODE ÉDITION
    
    match_text : function (text, search) {
        // Il faut faire ressembler le wikicode à du html, il manque plein de
        // truc encore...
        text = text.replace(/'''/g, "");
        text = text.replace(/''/g, "");
        // Kludge
        text = text.replace(/\n\n/g, "#<foo>#");
        text = text.replace(/\n/g, " ");
        text = text.replace(/#<foo>#/g, "\n\n");

        // Indispensable pour éviter un timeout du script sur une grosse page contenant
        // beaucoup de wikicode.
        var new_text = '';
        var last_match = 0;
        var splitter = new RegExp("<math>.*</math>|<[a-zA-z0-9 =\"']>|[</[a-zA-z0-9 =\"']+>|style=\".*\"|&nbsp;|&mdash;|<!--.*-->|\n:[:]*|\n;[;]*|[[][[].*]]", "gm");
        while ((result = splitter.exec(text)) != null) {
            new_text += text.slice(last_match, splitter.lastIndex - result[0].length);
            for (var i = 0; i < result[0].length; ++i)
                new_text += ' ';
            last_match = splitter.lastIndex;
        }
        new_text += text.slice(last_match);

        text = new_text;

        // Les nœud invisible servent à stocker à stocker les erreurs trouvés de
        // façon à réutiliser le même code que lorsque wgAction == submit.
        var $node = $('#bodyContent');
        for (var i = 0; i < search.length; ++i) {
            var pos = 0;
            while ((match = this.match(text.slice(pos), search[i])) != null) {
                pos += match[1];
                $node.append($("<span class='Erreurs-communes' style='display:none;'>" + match[2] + "</span>"));
            }
        }
    }, */

    // Predicate to filter page we act on based upon a user filter and some other criteria.
    filter_page : function(user_filter) {
        if ($.inArray(mw.config.get("wgAction"), [ 'view', 'submit' ]) == -1)
            return false;

        if (!user_filter.test(mw.config.get("wgPageName")))
            return false;

        if (/\.(css|js)$/.test(mw.config.get("wgPageName")))
            return false;

        if (/^Wikisource:/.test(mw.config.get("wgPageName")))
            return false;

        if ($("#wikiDiff").length)
            return false;

        /* FIXME: filtre les cats à cause de Page:pagename et Livre:livre.djvu
         * mais c'est trop drastique comme solution */
        if (mw.config.get("wgNamespaceNumber") == 14)
            return false;

        if (mw.config.get("wgNamespaceNumber") == 6)
            return false;

        if (mw.config.get("wgNamespaceNumber") == 112)
            return false;

        if (mw.config.get("wgNamespaceNumber") == 828)
            return false;

        if (mw.config.get("wgNamespaceNumber") == 2600) // Topic ou Sujet
            return false;

        if (mw.config.get("wgNamespaceNumber") == 2 && mw.config.get("wgPageName").indexOf('/') == -1)
            return false;

        if (mw.config.get("wgNamespaceNumber") % 2 != 0)
            return false;

        /* Incompatibilité avec le gadget auteur form, voir plus haut. */
        if (mw.config.get("wgNamespaceNumber") == 102 && mw.config.get("wgAction") == 'edit')
            return false;

        return true;
    },

    filter_node : function() {
        if ($(this).parent().hasClass('Erreurs-communes'))
            return false;
        return this.nodeType == 3;
    },

    get_text_nodes : function () {
        var content_id = "#mw-content-text";
        if ($("#wikiPreview").length)
            content_id = "#wikiPreview";

        // TODO: Bencher sur //fr.wikisource.org/wiki/L’Encyclopédie/Volume_1
        // ou tout autre page qui contient un grand nombre de nœud.
        return $(content_id)
                   .find("*")
                   .not($(".no_erreurs_communes, .no_erreurs_communes *"))
                   .not($(".diff *"))
                   .not($(".printfooter *"))
                   .not($(".previewnote *"))
                   .not($("#modernisations *"))
                   .not($("#authorityControl *"))    
                   .not($("#dynamic_links *"))
                   .not($("#contentSub *"))
                   .not($("#ws-data *"))
                   .not($("pre, pre *"))
                   .not($("code, code *"))
                   .not($("script"))
                   .not($("style")) // TemplateStyles
                   .not($(".mwe-math-element *"))
                   .not($(".fn, .fn *"))
                   .not($(".reference *"))
                   .not($(".abbr, .abbr *"))
                   .not($(":not(:lang(la))"))
                   .contents()
                   .filter(select_multiple_text.filter_node);
    },

    portlet_link_added : false,

    exec : function(user_filter, search) {
        if (!select_multiple_text.filter_page(user_filter))
            return;

        search = select_multiple_text.precompile_regexp(search);

        var text_nodes = select_multiple_text.get_text_nodes();

        $.each(text_nodes, function(i, el) {
            select_multiple_text.change_text_node(el, search, 0);
        });

        var error_count = $('span.Erreurs-communes').length;

        if (error_count) {
            if (!select_multiple_text.portlet_link_added) {
            	mw.loader.using('mediawiki.util', function() {
                mw.util.addPortletLink ("p-tb", "javascript:select_multiple_text.open_popup();",
                                "", "option-commons-errors", "");
            	});
            }

            $('#option-commons-errors a').text('Erreur(s) possible' + " (" + error_count + ")");

            select_multiple_text.portlet_link_added = true;
        }
    },

    default_select : function() {
		
		// Ces variables servent simplement à abréger et à rendre plus parlant le code ci-dessous //
		
		/* MINUSCULE, MAJUSCULE, LETTRE
		(à utiliser entre crochets) */
	    var char_min = 'a-zœæſ';
	    var char_maj = 'A-Zή';
	    var char = char_min + char_maj;
	    
	    /* PONCTUATION française
	    (à utiliser entre parenthèses) */
	    var ponct_forte = '[\\.]\\s+|\\s+[\\!\\?\\«\\»]\\s+' ;
	    var ponct_faible = '$|[,\\)\\]]\\s+|\\s+[\\(\\[]|\\s+[\\;\\:—]\\s+' ;
	    var ponct = "^|" + ponct_forte + '|' + ponct_faible ;
	
		/* TEST DE DÉBUT DE MOT (à mettre en début de regex)
		(respectivement qui commence par une consonne, par une voyelle, par une lettre quelconque) */
	    var deb_mot_cons = '(?<=\\s|^|\\(|\\[|\\>)';
	    var deb_mot_voy = '(?<=\\s|^|\\(|\\[|\\>|’)';
	    var deb_mot = '(?<=\\s|^|\\(|\\[|\\>|’|\\-)';
	    
	    /* TEST DE FIN DE MOT 
	    (à mettre en fin de regex) */
	    var fin_mot = '(?=\\s|,|\\.|\\)|\\]|\\<|\\||\\-)';
	    
	    /* LISTES DES EXPRESSIONS DOUTEUSES À SURLIGNER :
	    dans les commentaires, « *patati < patata » doit se lire : « surlignez Patati, qui est probablement une erreur pour Patata » */

        select_multiple_text.exec(/.*/,
            [
        	
        	/* PONCTUATION */
        	
        	"('|‘|’{2,})", // problèmes d'apostrophes
        	"\\.{2,}", // suites de points
            "[!:,;?][,;]+", // suites de signes de ponctuation interdites
        	"\\t", // tabulations
        	"\\.+…+|…+\\.+", // mélanges de points et points de suspension
        	"(\\{\\{|\\}\\})", // modèles mal fermés
        	'\\"', // guillemets droits
        	'«\\s*«|»\\s*»', // doubles guillemets
        	"[\\^~–\\@„■•]", // symboles typographiques impossibles en français
        	"\\s+[\\.\\,\\)\\]]", // mauvais espacement avant la ponctuation basse
        	"[\\(\\[]\\s+", // mauvais espacement après la ponctuation basse
        	"[" + char + "][\\!\\?\\:\\;]", // mauvais espacement avant la ponctuation haute
        	"[\\!\\?\\;\\:\\.,][" + char + "]", // mauvais espacement après la ponctuation haute
            "(\\s’|’ )", // mauvais espacement autour des apostrophes
            "([\\s\\,\\!\\?]\\-|(?<!(\\s|^)[A-Z])\\.\\-|\\-\\s+[^$])", // mauvais espacement autour des traits-d'union
            "(?<!\\s|^)—", // mauvais espacement avant les tirets cadratins
            "(—(?!\\s|\\,|$)|[^\\u00A0]—,)", // mauvais espacement après les tirets cadratins
            "(?<!\\s|^)\\&", // mauvais espacement avant les esperluettes
            "\\&(?!c\\.|\\s|\\,|$)", // mauvais espacement après les esperluettes
            
            
            // SUCCESSIONS DE LETTRES SUSPECTES
            
            "([" + char_min + "-][" + char_min + "][^0-9A-Zpvcgm°.][.] [" + char_min + "]" + "|" + "(ma|ta|sa|ce|se|ni|va|pu|et|le|la|de|du|un|te|je|tu|il|me|ne|ou|où|ok|à|y|a)[.] [" + char_min + "])", // point suivi d'une minuscule (si précédé d'un mot de plus de trois lettres)
        	"([A-ZÀ-Üa-zà-ÿ][Α-Ωα-ω]|[Α-Ωα-ω][A-ZÀ-Üa-zà-ÿ]|[A-ZÀ-Üa-zà-ÿ][0-9]|[0-9][A-ZÀ-Üa-zà-ÿ]|[Ѐ-ӿ])", // mélanges d'alphabets
            "[£$€ϐϵϑϰϕϖϱ]", // symboles mathématiques et monétaires
        	"[" + char_min + "][" + char_maj + "]+", // majuscules dans mot en minuscules
        	"(’[0-9]|[0-9]’)", // apostrophes au lieu de prime (ou chiffre au lieu de lettre)
            "[fffiflffifflſtst]", // ligatures
            "[ȦȧÀàĖėÈèİıÌìíȮȯÒòç]", // diacritiques
        	"(?!nec)(?<=^|\\s)(['’" + char + "]+)\\s+\\1(\\s|\\.|\\…|,)", // doublons
        	"([abdefghjklmnopqrstuvwyzà-ÿçœæſñABDEFGHJKLMNOPQRSTUVWYZÀ-ÜÇŒÆÑ])(\\1\\1+)", // lettres triples
        	
            "cbe", // *-cbe- < -che- 
            "cn[lt]", // *-cnl- < -enl-, *-cnt- < -ent-
            "nl" + fin_mot, // *-nl < -nt en fin de mot
            "œ" + fin_mot, // *-œ < -æ en fin de mot
            "[Qq]n", // *-qn- < -qu-
            "(?<=" + ponct_forte + ")" + "11" + fin_mot, // *11 < Ii
            
            
            ]);
    }
};

if ( !mw.user.isAnon() ) {
	$(document).ready(select_multiple_text.default_select);
}