Subversion Repositories Applications.dictionnaire

Rev

Rev 23 | 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;
22 delphine 60
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';
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>';
23 killian 88
	$('#conteneur_activation_definition').on('click', function(event) {
3 aurelien 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
20 aurelien 109
			ttexte = texte = $(this).text();
19 raphael 110
			if(exclureBlancs.test(texte)) return true;
111
 
112
			// incrémente le compteur de passage
113
			/* TODO: cette ligne cause un problème à IE-8
114
			   (peut-être uniquement relié au onChange() en cas de passage sur
115
			   des composants ajoutés postérieurement au chargement initial de la
116
			   page via AJAX. à tester. */
10 raphael 117
			$(this).data("count", ($(this).data("count") || 0) + 1);
16 aurelien 118
			matches_list = necessiteSubstitution(texte)[0];
10 raphael 119
			if(!matches_list) return true; // TODO: data("count") = max_passages ?
120
 
121
			for(i in matches_list) {
122
				couple = matches_list[i];
123
				mot_contextualise = couple[2];
124
				cle = texte_to_mot(mot_contextualise);
125
 
126
				if(! (ttexte = rechercherEtRemplacerMotParDefinition(texte, cle, couple))) break;
127
				texte = ttexte;
128
			}
129
			$(this).replaceWith(texte);
130
		});
131
	});
132
	return this;
1 aurelien 133
}
134
 
10 raphael 135
function rechercherEtRemplacerMotParDefinition(texte, mot, couple) {
136
	var full_contexte = couple[0],
137
	prefix_solo = couple[1],
138
	mot_contextualise = couple[2];
3 aurelien 139
 
10 raphael 140
	// cf exclureSpan
141
	if(new RegExp('((?:class="definition_term">)|(?:rel=")|(?:[çéèêîïô]))(' + mot + "\\w{0,2}" + ')' + "\\b", "ig").test(full_contexte)) return;
142
 
143
	templateMotADefinition = formaterTemplateMotADefinition(mot_contextualise, mot);
144
	bloc_replace = full_contexte.replace(mot_contextualise, templateMotADefinition);
145
	return texte.replace(full_contexte, bloc_replace);
3 aurelien 146
}
147
 
10 raphael 148
 
16 aurelien 149
function necessiteSubstitution(texte) {
10 raphael 150
	var liste = [], liste_complete = [];
151
 
152
	// regexp d'extraction du contexte, sans limite aux bords de "mot",
153
	// peu amener des faux positifs, et notamment ne pas matcher au mieux.
154
	// Eg: subsessiles match "sessile" et non "subsessile".
11 raphael 155
	while( (matches_c = regexp_thin.exec(texte)) ) liste.push(matches_c);
10 raphael 156
	if(liste.length == 0) return [null];
157
 
158
	// regexp de validation des bordure de mots, ne conserve que des matches correct
159
	// vis-à-vis de la bordure gauche
11 raphael 160
	while( (matches_c = regexp_complete.exec(texte)) ) liste_complete.push(matches_c[1]);
10 raphael 161
 
162
	// si une match n'est pas dans le tableau des mots autorisés (indexOf)
163
	for(var i = 0; i < liste.length; i++) {
164
		if($.inArray(liste[i][2], liste_complete) == -1) liste.splice(i, 1);
165
	}
166
 
167
	return [liste, liste_complete]
168
}
169
 
3 aurelien 170
function getMotsADefinitions() {
1 aurelien 171
	$.ajax({
10 raphael 172
		url: URL_BASE_SERVICE+'mots/', // provient de dictionnaire.config.js
1 aurelien 173
		success: function(data) {
174
			motsAyantDefinition = data;
10 raphael 175
			batirRegexp(motsAyantDefinition);
7 raphael 176
			$(set).remplacerDefinitions(motsAyantDefinition);
1 aurelien 177
		},
178
		dataType: "JSON",
179
		global: false
180
	});
181
}
182
 
16 aurelien 183
function getMotsADefinitionsEtRemplacer() {
184
	if(motsAyantDefinition == null) {
185
		getMotsADefinitions();
186
	} else {
187
		if(regexp_complete == null || regexp_thin == null) {
188
			batirRegexp(motsAyantDefinition);
189
		}
190
		$(set).remplacerDefinitions(motsAyantDefinition);
191
	}
192
}
193
 
17 raphael 194
// update definitions set cle = trim(cle);
195
// select cle from definitions where cle like '%é' COLLATE 'utf8_bin';
196
// select cle from definitions where cle like '% ou %' COLLATE 'utf8_bin';
10 raphael 197
function batirRegexp(mots) {
198
	var restr = '';
199
	$.each(mots, function(index, mot) {
200
		if(restr != '') restr += '|';
201
 
17 raphael 202
		if(mot == 'ph') {
203
			restr += 'ph[^.]';
204
		}
205
		else if(mot.length < 3) {
10 raphael 206
			restr += mot;
207
		}
17 raphael 208
		// confluent => confluentes
209
		else if(mot.substr(-2) == 'ts') {
210
			restr += mot.slice(0, -1) + "e?s?";
211
		}
212
		else if(mot.substr(-3) == 'ore') {
213
			restr += mot + "s?";
214
		}
10 raphael 215
		// gestion masculin/féminin en "eux/euse"
216
		else if(mot.substr(-1) == 'x') {
217
			restr += mot.slice(0, -1) + "(?:x|se|ses)";
218
		}
219
		else if(mot.substr(-4) == 'euse') { // TODO: fix DB
220
			restr += mot.slice(0, -2) + "(?:x|se|ses)";
221
		}
17 raphael 222
		else if(mot.substr(-2) == 'ée') {
223
			restr += mot.slice(0, -1) + "e?s?";
224
		}
225
		else if(mot.substr(-1) == 'é') {
226
			restr += mot + "e?s?";
227
		}
228
		// verbe vs nom
229
		else if(mot.substr(-4) == 'orme') {
230
			restr += mot + "s?";
231
		}
232
		// verbe 3è p.pluriel en DB: ne devraient pas exister
233
		// mais rampant => rampante
234
		else if(mot.substr(-2) == 'nt') {
235
			restr += mot + "e?s?";
236
		}
237
		else {
238
			// moins de faux positifs:
239
			// restr += mot + "(?:[ées]){0,2}";
240
			// autrement:
10 raphael 241
			restr += mot + "\\w{0,2}";
17 raphael 242
		}
10 raphael 243
	});
244
 
245
	regexp_complete = new RegExp("(?:\\s|^|\\W)(" + restr + ")\\b", "ig"); // accents
246
	regexp_thin = new RegExp(exclureSpan + "(" + restr + ")\\b", "ig");
247
}
248
 
249
function formaterTemplateMotADefinition(motOriginal, motSimplifie) {
1 aurelien 250
	definitionHtml = '<span rel="'+motSimplifie+'" class="definition_term">'
10 raphael 251
		+motOriginal+
252
		'</span>';
1 aurelien 253
	return definitionHtml;
254
}
255
 
256
function ajouterListenerDefinitions() {
23 killian 257
	$('.definition_term').on('mouseover mouseout', function(event) {
10 raphael 258
		if (event.type == 'mouseover') {
259
			event.preventDefault();
260
			afficherDefinition($(this));
261
		} else {
262
			cacherPopupsDefinitions();
263
		}
1 aurelien 264
	});
265
}
266
 
267
function afficherDefinition(element) {
268
	mot = element.attr('rel');
269
	if(dictionnaire[mot] != null) {
3 aurelien 270
		element.after(formaterDefinition(element));
1 aurelien 271
		afficherPopupDefinition();
272
	} else {
273
		chargerDefinitionDistante(element);
274
	}
275
}
276
 
3 aurelien 277
function chargerDefinitionDistante(element) {
278
	date = new Date();
5 aurelien 279
	mot = element.attr('rel');
1 aurelien 280
	$.ajax({
10 raphael 281
		url: URL_BASE_SERVICE+'def/'+mot, // provient de dictionnaire.config.js
1 aurelien 282
		success: function(data) {
283
			retour = data;
284
			definition = retour.valeur;
285
			dictionnaire[mot] = definition;
3 aurelien 286
			element.after(formaterDefinition(element));
1 aurelien 287
			afficherPopupDefinition();
288
		},
289
		dataType: "JSON",
11 raphael 290
		global: false,
291
		async: false
1 aurelien 292
	});
293
}
294
 
295
function afficherPopupDefinition() {
296
	$(".definition_container").css({'top':mouseY + 20,'left':mouseX - 10}).fadeIn('slow');
297
}
298
 
299
function cacherPopupsDefinitions() {
300
	$(".definition_container").remove();
301
}
302
 
303
function formaterDefinition(element) {
304
	mot = element.attr('rel');
305
	data = dictionnaire[mot];
306
	defHtml = '<div class="definition_container">'+
10 raphael 307
		'<span class="definition_container_fleche"></span>'+
308
		'<span class="definition">'+data+'</span>'+
309
		'</div>';
1 aurelien 310
	return defHtml;
311
}
312
 
313
function supprimerToutesDefinitions() {
314
	$('.definition_term').each(function() {
315
		$(this).replaceWith($(this).html());
316
	});
317
	cacherPopupsDefinitions();
318
}
319
 
320
$(document).bind('mousemove', function(e){
321
	mouseX = e.pageX;
322
    mouseY = e.pageY - $(window).scrollTop();
323
});
324
 
14 aurelien 325
$(document).ajaxStop(function() {
16 aurelien 326
	urlPage = $(this)[0].URL;
327
	getMotsADefinitionsEtRemplacer();
14 aurelien 328
});
329
 
1 aurelien 330
$(document).ready(function() {
16 aurelien 331
	$.ajax({
332
		url: URL_BASE_SERVICE + "../../../scripts/helpers.js",
333
		dataType: "script",
334
		global: false
335
	});
336
	getMotsADefinitionsEtRemplacer();
24 killian 337
 
338
	// I'm a dirty nasty hack, you should fix me hard
339
	setTimeout(function() {ajouterListenerDefinitions()}, 2000);
1 aurelien 340
});