Subversion Repositories Applications.dictionnaire

Rev

Rev 17 | Rev 20 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
10 raphael 1
/*
2
  Surligneur de mot (glossaire)
3
 
4
  0) tout d'abord la liste des mots est récupée (getMotsADefinitions) et stockée dans motsAyantDefinition
5
 
6
  1) chaque mot de la liste subie une transformation progressive en pattern:
7
  * les mots de moins de 3 caractères sont inchangés (exemple: "nu")
8
  * les mots finissant en "eux/euse" sont généricisés
9
  * les autres mots se voit ajoutée la possibilité de matcher sur \\w{0,2}
10
 
11
  2) puis plusieurs RegExp sont baties à partir de la liste de pattern préliminaires
12
  * regexp_complete permet d'assurer que le mot:
13
  ** commence en début de ligne
14
  ** suit un espace
15
  ** ne suit pas un caractère
16
  [ mais ne s'assure pas que le mot ne suit PAS un caractère accentué, cf notes à propos de \\b et l'UTF-8 ]
17
 
18
  * regexp_thin permet de s'assurer que le mot:
19
  ** ne se trouve pas déjà encapsulé dans un <span rel="" class="definition_term">X</span>
20
  ** ne se trouve pas comme attribut "rel" d'un <span rel="X" class="definition_term"></span>
21
  ** n'est pas directement précédé d'un accent (parmis ceux utilisés dans la BDD, cf exclureSpan)
22
  ** n'est pas directement précédé d'un caractère alphabétique
23
  Et tente d'extraire une partie de contexte significative (jusqu'à 24 caractère car initialement le
24
  test d'inclusion dans .definition_term était faire plus tardivement.
25
 
26
  3) remplacerDefinitions() prend la liste des nodes susceptibles de nécessiter un traitement
27
  et ignore ceux ayant déjà été traités [max_passages] fois (4 par défaut)
28
 
29
  4) pour chaque node:
30
  * le compteur de passage est incrémenté
16 aurelien 31
  * necessiteSubstitution() est appellé
10 raphael 32
 
16 aurelien 33
  == necessiteSubstitution() ==:
10 raphael 34
  ** Utilise regexp_thin pour extraire les mots restant à définir ainsi qu'une partie de leur contexte.
35
  ** Utilise regexp_complete pour extraire les mots au plus proche de leur bodures avec un moindre risque d'erreur
36
  ** exlue les faux positifs possibles de regexp_thin
37
  ** renvoi les matches de regexp_thin (qui présente plus de contexte)
38
  =============================
39
 
16 aurelien 40
  * pour chaque correspondance retournée par necessiteSubstitution(),
10 raphael 41
  le mot d'origine (tel que dans motsAyantDefinition)
42
  est déterminé en utilisant le levenstein (un cache est mis en place, cf texte_to_mot() dans helpers.js).
43
 
44
  * rechercherEtRemplacerMotParDefinition() est appelée pour effectuer la substitution dans le node
45
 
46
 
47
  Contraintes rencontrées:
48
  1) exclure les mots déjà défini via la regexp général plutôt qu'au coup par coup.
49
  2) vérifier les bordures
50
  3) vérifier bordure d'accents:
51
  \\b(é) dans une regexp ne peut matcher une chaîne JS UTF-8
52
  http://stackoverflow.com/questions/2881445/utf-8-word-boundary-regex-in-javascript
53
*/
54
 
1 aurelien 55
var dictionnaire = new Array();
3 aurelien 56
var motsAyantDefinition = null;
1 aurelien 57
var mouseX = null;
58
var mouseY = null;
3 aurelien 59
var active = false;
12 raphael 60
var set = '#zone-droite p, #zone-droite span:not(.definition_term), #zone-droite td, #zone-droite pre, #zone-droite div, #zone-droite li';
16 aurelien 61
var urlPage = window.location.href;
1 aurelien 62
 
10 raphael 63
// Note: utiliser \\b plutôt que \\W pour matcher les bordures de mots
64
// en incluant début et fin de ligne
65
var regexp_complete = null;
66
var regexp_thin = null;
67
 
68
// nombre maximum de passages de la détection sur un node donné
69
var max_passages = 4;
70
 
71
// echo $(mysql<<<"SELECT cle FROM definitions"|tr "[a-z,-]" o|sed -e 's/o//g' -e "s/\(.\)/\1\n/g"|sort -u)
72
// ç é è ê î ï ô (2013/07/03)
73
// TODO: http://xregexp.com/plugins/
74
var	exclureSpan = '([^' +
75
	'(?:class="definition_term">)' + // mot contextualisé déjà dictionnarisé
76
	'(?:rel=")' + // la valeur du rel="" d'un mot déjà dictionnarisé
77
	'çéèêîïô' + // les accents, non-traités dans la regexp générale et qui ne doivent pas
78
	// être pris pour des bordures de mots
79
	'\\w' +
80
	']|^){1,24}';
81
 
19 raphael 82
// sert à filtrer plus rapidement les nodes inutiles pour l'application
83
// du dictionnaire
84
var exclureBlancs = new RegExp('^\\s*$');
10 raphael 85
 
3 aurelien 86
function afficherLienDefinitions() {
87
	html = '<div id="conteneur_activation_definition"><a href="#">rechercher les définitions</a></div>';
88
	$('#conteneur_activation_definition').live('click', function(event) {
89
		event.preventDefault();
10 raphael 90
		//supprimerToutesDefinitions();
3 aurelien 91
		if(motsAyantDefinition == null) {
92
			getMotsADefinitions();
10 raphael 93
			ajouterListenerDefinitions();
3 aurelien 94
		} else {
7 raphael 95
			$(set).remplacerDefinitions(motsAyantDefinition);
3 aurelien 96
		}
97
	});
98
	$('body').append(html);
99
}
100
 
101
$.fn.remplacerDefinitions = function(mots) {
10 raphael 102
    this.each(function() {
103
        $(this).contents().filter(function() {
104
        	return (this.nodeType == 3 && ! (($(this).data("count") || 0) > max_passages));
105
        }).each(function() {
19 raphael 106
			// évite les nodes dont le texte est undefined (dumb example <script>)
107
			if(!$(this).text()) return true;
108
			// évite les nodes dont le texte est vide ou blanc
109
			if(exclureBlancs.test(texte)) return true;
110
 
111
			// incrémente le compteur de passage
112
			/* TODO: cette ligne cause un problème à IE-8
113
			   (peut-être uniquement relié au onChange() en cas de passage sur
114
			   des composants ajoutés postérieurement au chargement initial de la
115
			   page via AJAX. à tester. */
10 raphael 116
			$(this).data("count", ($(this).data("count") || 0) + 1);
19 raphael 117
 
10 raphael 118
			ttexte = texte = $(this).text();
16 aurelien 119
			matches_list = necessiteSubstitution(texte)[0];
10 raphael 120
			if(!matches_list) return true; // TODO: data("count") = max_passages ?
121
 
122
			for(i in matches_list) {
123
				couple = matches_list[i];
124
				mot_contextualise = couple[2];
125
				cle = texte_to_mot(mot_contextualise);
126
 
127
				if(! (ttexte = rechercherEtRemplacerMotParDefinition(texte, cle, couple))) break;
128
				texte = ttexte;
129
			}
130
			$(this).replaceWith(texte);
131
		});
132
	});
133
	return this;
1 aurelien 134
}
135
 
10 raphael 136
function rechercherEtRemplacerMotParDefinition(texte, mot, couple) {
137
	var full_contexte = couple[0],
138
	prefix_solo = couple[1],
139
	mot_contextualise = couple[2];
3 aurelien 140
 
10 raphael 141
	// cf exclureSpan
142
	if(new RegExp('((?:class="definition_term">)|(?:rel=")|(?:[çéèêîïô]))(' + mot + "\\w{0,2}" + ')' + "\\b", "ig").test(full_contexte)) return;
143
 
144
	templateMotADefinition = formaterTemplateMotADefinition(mot_contextualise, mot);
145
	bloc_replace = full_contexte.replace(mot_contextualise, templateMotADefinition);
146
	return texte.replace(full_contexte, bloc_replace);
3 aurelien 147
}
148
 
10 raphael 149
 
16 aurelien 150
function necessiteSubstitution(texte) {
10 raphael 151
	var liste = [], liste_complete = [];
152
 
153
	// regexp d'extraction du contexte, sans limite aux bords de "mot",
154
	// peu amener des faux positifs, et notamment ne pas matcher au mieux.
155
	// Eg: subsessiles match "sessile" et non "subsessile".
11 raphael 156
	while( (matches_c = regexp_thin.exec(texte)) ) liste.push(matches_c);
10 raphael 157
	if(liste.length == 0) return [null];
158
 
159
	// regexp de validation des bordure de mots, ne conserve que des matches correct
160
	// vis-à-vis de la bordure gauche
11 raphael 161
	while( (matches_c = regexp_complete.exec(texte)) ) liste_complete.push(matches_c[1]);
10 raphael 162
 
163
	// si une match n'est pas dans le tableau des mots autorisés (indexOf)
164
	for(var i = 0; i < liste.length; i++) {
165
		if($.inArray(liste[i][2], liste_complete) == -1) liste.splice(i, 1);
166
	}
167
 
168
	return [liste, liste_complete]
169
}
170
 
3 aurelien 171
function getMotsADefinitions() {
1 aurelien 172
	$.ajax({
10 raphael 173
		url: URL_BASE_SERVICE+'mots/', // provient de dictionnaire.config.js
1 aurelien 174
		success: function(data) {
175
			motsAyantDefinition = data;
10 raphael 176
			batirRegexp(motsAyantDefinition);
7 raphael 177
			$(set).remplacerDefinitions(motsAyantDefinition);
1 aurelien 178
		},
179
		dataType: "JSON",
180
		global: false
181
	});
182
}
183
 
16 aurelien 184
function getMotsADefinitionsEtRemplacer() {
185
	if(motsAyantDefinition == null) {
186
		getMotsADefinitions();
187
	} else {
188
		if(regexp_complete == null || regexp_thin == null) {
189
			batirRegexp(motsAyantDefinition);
190
		}
191
		$(set).remplacerDefinitions(motsAyantDefinition);
192
	}
193
}
194
 
17 raphael 195
// update definitions set cle = trim(cle);
196
// select cle from definitions where cle like '%é' COLLATE 'utf8_bin';
197
// select cle from definitions where cle like '% ou %' COLLATE 'utf8_bin';
10 raphael 198
function batirRegexp(mots) {
199
	var restr = '';
200
	$.each(mots, function(index, mot) {
201
		if(restr != '') restr += '|';
202
 
17 raphael 203
		if(mot == 'ph') {
204
			restr += 'ph[^.]';
205
		}
206
		else if(mot.length < 3) {
10 raphael 207
			restr += mot;
208
		}
17 raphael 209
		// confluent => confluentes
210
		else if(mot.substr(-2) == 'ts') {
211
			restr += mot.slice(0, -1) + "e?s?";
212
		}
213
		else if(mot.substr(-3) == 'ore') {
214
			restr += mot + "s?";
215
		}
10 raphael 216
		// gestion masculin/féminin en "eux/euse"
217
		else if(mot.substr(-1) == 'x') {
218
			restr += mot.slice(0, -1) + "(?:x|se|ses)";
219
		}
220
		else if(mot.substr(-4) == 'euse') { // TODO: fix DB
221
			restr += mot.slice(0, -2) + "(?:x|se|ses)";
222
		}
17 raphael 223
		else if(mot.substr(-2) == 'ée') {
224
			restr += mot.slice(0, -1) + "e?s?";
225
		}
226
		else if(mot.substr(-1) == 'é') {
227
			restr += mot + "e?s?";
228
		}
229
		// verbe vs nom
230
		else if(mot.substr(-4) == 'orme') {
231
			restr += mot + "s?";
232
		}
233
		// verbe 3è p.pluriel en DB: ne devraient pas exister
234
		// mais rampant => rampante
235
		else if(mot.substr(-2) == 'nt') {
236
			restr += mot + "e?s?";
237
		}
238
		else {
239
			// moins de faux positifs:
240
			// restr += mot + "(?:[ées]){0,2}";
241
			// autrement:
10 raphael 242
			restr += mot + "\\w{0,2}";
17 raphael 243
		}
10 raphael 244
	});
245
 
246
	regexp_complete = new RegExp("(?:\\s|^|\\W)(" + restr + ")\\b", "ig"); // accents
247
	regexp_thin = new RegExp(exclureSpan + "(" + restr + ")\\b", "ig");
248
}
249
 
250
function formaterTemplateMotADefinition(motOriginal, motSimplifie) {
1 aurelien 251
	definitionHtml = '<span rel="'+motSimplifie+'" class="definition_term">'
10 raphael 252
		+motOriginal+
253
		'</span>';
1 aurelien 254
	return definitionHtml;
255
}
256
 
257
function ajouterListenerDefinitions() {
258
	$('.definition_term').live('mouseover mouseout', function(event) {
10 raphael 259
		if (event.type == 'mouseover') {
260
			event.preventDefault();
261
			afficherDefinition($(this));
262
		} else {
263
			cacherPopupsDefinitions();
264
		}
1 aurelien 265
	});
266
}
267
 
268
function afficherDefinition(element) {
269
	mot = element.attr('rel');
270
	if(dictionnaire[mot] != null) {
3 aurelien 271
		element.after(formaterDefinition(element));
1 aurelien 272
		afficherPopupDefinition();
273
	} else {
274
		chargerDefinitionDistante(element);
275
	}
276
}
277
 
3 aurelien 278
function chargerDefinitionDistante(element) {
279
	date = new Date();
5 aurelien 280
	mot = element.attr('rel');
1 aurelien 281
	$.ajax({
10 raphael 282
		url: URL_BASE_SERVICE+'def/'+mot, // provient de dictionnaire.config.js
1 aurelien 283
		success: function(data) {
284
			retour = data;
285
			definition = retour.valeur;
286
			dictionnaire[mot] = definition;
3 aurelien 287
			element.after(formaterDefinition(element));
1 aurelien 288
			afficherPopupDefinition();
289
		},
290
		dataType: "JSON",
11 raphael 291
		global: false,
292
		async: false
1 aurelien 293
	});
294
}
295
 
296
function afficherPopupDefinition() {
297
	$(".definition_container").css({'top':mouseY + 20,'left':mouseX - 10}).fadeIn('slow');
298
}
299
 
300
function cacherPopupsDefinitions() {
301
	$(".definition_container").remove();
302
}
303
 
304
function formaterDefinition(element) {
305
	mot = element.attr('rel');
306
	data = dictionnaire[mot];
307
	defHtml = '<div class="definition_container">'+
10 raphael 308
		'<span class="definition_container_fleche"></span>'+
309
		'<span class="definition">'+data+'</span>'+
310
		'</div>';
1 aurelien 311
	return defHtml;
312
}
313
 
314
function supprimerToutesDefinitions() {
315
	$('.definition_term').each(function() {
316
		$(this).replaceWith($(this).html());
317
	});
318
	cacherPopupsDefinitions();
319
}
320
 
321
$(document).bind('mousemove', function(e){
322
	mouseX = e.pageX;
323
    mouseY = e.pageY - $(window).scrollTop();
324
});
325
 
14 aurelien 326
$(document).ajaxStop(function() {
16 aurelien 327
	urlPage = $(this)[0].URL;
328
	getMotsADefinitionsEtRemplacer();
14 aurelien 329
});
330
 
1 aurelien 331
$(document).ready(function() {
16 aurelien 332
	$.ajax({
333
		url: URL_BASE_SERVICE + "../../../scripts/helpers.js",
334
		dataType: "script",
335
		global: false
336
	});
337
	getMotsADefinitionsEtRemplacer();
10 raphael 338
	ajouterListenerDefinitions();
1 aurelien 339
});