Subversion Repositories Applications.dictionnaire

Rev

Rev 23 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
  Surligneur de mot (glossaire)

  0) tout d'abord la liste des mots est récupée (getMotsADefinitions) et stockée dans motsAyantDefinition

  1) chaque mot de la liste subie une transformation progressive en pattern:
  * les mots de moins de 3 caractères sont inchangés (exemple: "nu")
  * les mots finissant en "eux/euse" sont généricisés
  * les autres mots se voit ajoutée la possibilité de matcher sur \\w{0,2}

  2) puis plusieurs RegExp sont baties à partir de la liste de pattern préliminaires
  * regexp_complete permet d'assurer que le mot:
  ** commence en début de ligne
  ** suit un espace
  ** ne suit pas un caractère
  [ mais ne s'assure pas que le mot ne suit PAS un caractère accentué, cf notes à propos de \\b et l'UTF-8 ]

  * regexp_thin permet de s'assurer que le mot:
  ** ne se trouve pas déjà encapsulé dans un <span rel="" class="definition_term">X</span>
  ** ne se trouve pas comme attribut "rel" d'un <span rel="X" class="definition_term"></span>
  ** n'est pas directement précédé d'un accent (parmis ceux utilisés dans la BDD, cf exclureSpan)
  ** n'est pas directement précédé d'un caractère alphabétique
  Et tente d'extraire une partie de contexte significative (jusqu'à 24 caractère car initialement le
  test d'inclusion dans .definition_term était faire plus tardivement.

  3) remplacerDefinitions() prend la liste des nodes susceptibles de nécessiter un traitement
  et ignore ceux ayant déjà été traités [max_passages] fois (4 par défaut)

  4) pour chaque node:
  * le compteur de passage est incrémenté
  * necessiteSubstitution() est appellé

  == necessiteSubstitution() ==:
  ** Utilise regexp_thin pour extraire les mots restant à définir ainsi qu'une partie de leur contexte.
  ** Utilise regexp_complete pour extraire les mots au plus proche de leur bodures avec un moindre risque d'erreur
  ** exlue les faux positifs possibles de regexp_thin
  ** renvoi les matches de regexp_thin (qui présente plus de contexte)
  =============================

  * pour chaque correspondance retournée par necessiteSubstitution(),
  le mot d'origine (tel que dans motsAyantDefinition)
  est déterminé en utilisant le levenstein (un cache est mis en place, cf texte_to_mot() dans helpers.js).

  * rechercherEtRemplacerMotParDefinition() est appelée pour effectuer la substitution dans le node


  Contraintes rencontrées:
  1) exclure les mots déjà défini via la regexp général plutôt qu'au coup par coup.
  2) vérifier les bordures
  3) vérifier bordure d'accents:
  \\b(é) dans une regexp ne peut matcher une chaîne JS UTF-8
  http://stackoverflow.com/questions/2881445/utf-8-word-boundary-regex-in-javascript
*/

var dictionnaire = new Array();
var motsAyantDefinition = null;
var mouseX = null;
var mouseY = null;
var active = false;
var set = '#zone-droite p, #zone-droite span:not(.definition_term), #zone-droite td, #zone-droite pre, #zone-droite div, #zone-droite li, .layout-content, .layout-content p, .layout-content span:not(.definition_term), .layout-content td, .layout-content pre, .layout-content div, .layout-content li';
var urlPage = window.location.href;

// Note: utiliser \\b plutôt que \\W pour matcher les bordures de mots
// en incluant début et fin de ligne
var regexp_complete = null;
var regexp_thin = null;

// nombre maximum de passages de la détection sur un node donné
var max_passages = 4;

// echo $(mysql<<<"SELECT cle FROM definitions"|tr "[a-z,-]" o|sed -e 's/o//g' -e "s/\(.\)/\1\n/g"|sort -u)
// ç é è ê î ï ô (2013/07/03)
// TODO: http://xregexp.com/plugins/
var     exclureSpan = '([^' +
        '(?:class="definition_term">)' + // mot contextualisé déjà dictionnarisé
        '(?:rel=")' + // la valeur du rel="" d'un mot déjà dictionnarisé
        'çéèêîïô' + // les accents, non-traités dans la regexp générale et qui ne doivent pas
        // être pris pour des bordures de mots
        '\\w' +
        ']|^){1,24}';

// sert à filtrer plus rapidement les nodes inutiles pour l'application
// du dictionnaire
var exclureBlancs = new RegExp('^\\s*$');

function afficherLienDefinitions() {
        html = '<div id="conteneur_activation_definition"><a href="#">rechercher les définitions</a></div>';
        $('#conteneur_activation_definition').on('click', function(event) {
                event.preventDefault();
                //supprimerToutesDefinitions();
                if(motsAyantDefinition == null) {
                        getMotsADefinitions();
                        ajouterListenerDefinitions();
                } else {
                        $(set).remplacerDefinitions(motsAyantDefinition);
                }
        });
        $('body').append(html);
}

$.fn.remplacerDefinitions = function(mots) {
    this.each(function() {
        $(this).contents().filter(function() {
                return (this.nodeType == 3 && ! (($(this).data("count") || 0) > max_passages));
        }).each(function() {
                        // évite les nodes dont le texte est undefined (dumb example <script>)
                        if(!$(this).text()) return true;
                        // évite les nodes dont le texte est vide ou blanc
                        ttexte = texte = $(this).text();
                        if(exclureBlancs.test(texte)) return true;

                        // incrémente le compteur de passage
                        /* TODO: cette ligne cause un problème à IE-8
                           (peut-être uniquement relié au onChange() en cas de passage sur
                           des composants ajoutés postérieurement au chargement initial de la
                           page via AJAX. à tester. */
                        $(this).data("count", ($(this).data("count") || 0) + 1);
                        matches_list = necessiteSubstitution(texte)[0];
                        if(!matches_list) return true; // TODO: data("count") = max_passages ?

                        for(i in matches_list) {
                                couple = matches_list[i];
                                mot_contextualise = couple[2];
                                cle = texte_to_mot(mot_contextualise);

                                if(! (ttexte = rechercherEtRemplacerMotParDefinition(texte, cle, couple))) break;
                                texte = ttexte;
                        }
                        $(this).replaceWith(texte);
                });
        });
        return this;
}

function rechercherEtRemplacerMotParDefinition(texte, mot, couple) {
        var full_contexte = couple[0],
        prefix_solo = couple[1],
        mot_contextualise = couple[2];

        // cf exclureSpan
        if(new RegExp('((?:class="definition_term">)|(?:rel=")|(?:[çéèêîïô]))(' + mot + "\\w{0,2}" + ')' + "\\b", "ig").test(full_contexte)) return;

        templateMotADefinition = formaterTemplateMotADefinition(mot_contextualise, mot);
        bloc_replace = full_contexte.replace(mot_contextualise, templateMotADefinition);
        return texte.replace(full_contexte, bloc_replace);
}


function necessiteSubstitution(texte) {
        var liste = [], liste_complete = [];

        // regexp d'extraction du contexte, sans limite aux bords de "mot",
        // peu amener des faux positifs, et notamment ne pas matcher au mieux.
        // Eg: subsessiles match "sessile" et non "subsessile".
        while( (matches_c = regexp_thin.exec(texte)) ) liste.push(matches_c);
        if(liste.length == 0) return [null];

        // regexp de validation des bordure de mots, ne conserve que des matches correct
        // vis-à-vis de la bordure gauche
        while( (matches_c = regexp_complete.exec(texte)) ) liste_complete.push(matches_c[1]);

        // si une match n'est pas dans le tableau des mots autorisés (indexOf)
        for(var i = 0; i < liste.length; i++) {
                if($.inArray(liste[i][2], liste_complete) == -1) liste.splice(i, 1);
        }

        return [liste, liste_complete]
}

function getMotsADefinitions() {
        $.ajax({
                url: URL_BASE_SERVICE+'mots/', // provient de dictionnaire.config.js
                success: function(data) {
                        motsAyantDefinition = data;
                        batirRegexp(motsAyantDefinition);
                        $(set).remplacerDefinitions(motsAyantDefinition);
                },
                dataType: "JSON",
                global: false
        });
}

function getMotsADefinitionsEtRemplacer() {
        if(motsAyantDefinition == null) {
                getMotsADefinitions();
        } else {
                if(regexp_complete == null || regexp_thin == null) {
                        batirRegexp(motsAyantDefinition);
                }
                $(set).remplacerDefinitions(motsAyantDefinition);
        }
}

// update definitions set cle = trim(cle);
// select cle from definitions where cle like '%é' COLLATE 'utf8_bin';
// select cle from definitions where cle like '% ou %' COLLATE 'utf8_bin';
function batirRegexp(mots) {
        var restr = '';
        $.each(mots, function(index, mot) {
                if(restr != '') restr += '|';

                if(mot == 'ph') {
                        restr += 'ph[^.]';
                }
                else if(mot.length < 3) {
                        restr += mot;
                }
                // confluent => confluentes
                else if(mot.substr(-2) == 'ts') {
                        restr += mot.slice(0, -1) + "e?s?";
                }
                else if(mot.substr(-3) == 'ore') {
                        restr += mot + "s?";
                }
                // gestion masculin/féminin en "eux/euse"
                else if(mot.substr(-1) == 'x') {
                        restr += mot.slice(0, -1) + "(?:x|se|ses)";
                }
                else if(mot.substr(-4) == 'euse') { // TODO: fix DB
                        restr += mot.slice(0, -2) + "(?:x|se|ses)";
                }
                else if(mot.substr(-2) == 'ée') {
                        restr += mot.slice(0, -1) + "e?s?";
                }
                else if(mot.substr(-1) == 'é') {
                        restr += mot + "e?s?";
                }
                // verbe vs nom
                else if(mot.substr(-4) == 'orme') {
                        restr += mot + "s?";
                }
                // verbe 3è p.pluriel en DB: ne devraient pas exister
                // mais rampant => rampante
                else if(mot.substr(-2) == 'nt') {
                        restr += mot + "e?s?";
                }
                else {
                        // moins de faux positifs:
                        // restr += mot + "(?:[ées]){0,2}";
                        // autrement:
                        restr += mot + "\\w{0,2}";
                }
        });

        regexp_complete = new RegExp("(?:\\s|^|\\W)(" + restr + ")\\b", "ig"); // accents
        regexp_thin = new RegExp(exclureSpan + "(" + restr + ")\\b", "ig");
}

function formaterTemplateMotADefinition(motOriginal, motSimplifie) {
        definitionHtml = '<span rel="'+motSimplifie+'" class="definition_term">'
                +motOriginal+
                '</span>';
        return definitionHtml;
}

function ajouterListenerDefinitions() {
        $('.definition_term').on('mouseover mouseout', function(event) {
                if (event.type == 'mouseover') {
                        event.preventDefault();
                        afficherDefinition($(this));
                } else {
                        cacherPopupsDefinitions();
                }
        });
}

function afficherDefinition(element) {
        mot = element.attr('rel');
        if(dictionnaire[mot] != null) {
                element.after(formaterDefinition(element));
                afficherPopupDefinition();
        } else {
                chargerDefinitionDistante(element);
        }
}

function chargerDefinitionDistante(element) {
        date = new Date();
        mot = element.attr('rel');
        $.ajax({
                url: URL_BASE_SERVICE+'def/'+mot, // provient de dictionnaire.config.js
                success: function(data) {
                        retour = data;
                        definition = retour.valeur;
                        dictionnaire[mot] = definition;
                        element.after(formaterDefinition(element));
                        afficherPopupDefinition();
                },
                dataType: "JSON",
                global: false,
                async: false
        });
}

function afficherPopupDefinition() {
        $(".definition_container").css({'top':mouseY + 20,'left':mouseX - 10}).fadeIn('slow');
}

function cacherPopupsDefinitions() {
        $(".definition_container").remove();
}

function formaterDefinition(element) {  
        mot = element.attr('rel');
        data = dictionnaire[mot];
        defHtml = '<div class="definition_container">'+
                '<span class="definition_container_fleche"></span>'+
                '<span class="definition">'+data+'</span>'+
                '</div>';
        return defHtml;
}

function supprimerToutesDefinitions() {
        $('.definition_term').each(function() {
                $(this).replaceWith($(this).html());
        });
        cacherPopupsDefinitions();
}

$(document).bind('mousemove', function(e){
        mouseX = e.pageX;
    mouseY = e.pageY - $(window).scrollTop();
});

$(document).ajaxStop(function() {
        urlPage = $(this)[0].URL;
        getMotsADefinitionsEtRemplacer();
});

$(document).ready(function() {
        $.ajax({
                url: URL_BASE_SERVICE + "../../../scripts/helpers.js",
                dataType: "script",
                global: false
        });
        getMotsADefinitionsEtRemplacer();

        // I'm a dirty nasty hack, you should fix me hard
        setTimeout(function() {ajouterListenerDefinitions()}, 2000);
});