Ignorer la casse, les diacritiques, et les ligatures dans un autocompléteur GWT
Autant, en Java, supprimer les diacritiques et ligatures est un jeu d'enfant, il suffit de ces quelques ligne
(voir ci-dessous),
autant, dans une application Web avec
GWT, ce n'est pas aussi immédiat.
Nous allons voir comment réaliser cela dans un autocompléteur grâce auquel
l'utilisateur peut sélectionner le nom d'un pays sans se soucier de commencer
à l'écrire : avec ou sans accents, en majuscules ou minuscules.
Donc, en pur Java cela donnerait :
public static String removeDiacritics(String input) {
// Normalizer.normalise() converts each accented
// character into 1 non-accented character followed
// by 1 or more characters representing the accent(s)
// alone. These characters representing only
// an accent belong to the Unicode category
// CombiningDiacriticalMarks. The call to replaceAll
// strips out all characters in that category.
String normalized = Normalizer.normalize(input, Form.NFKD);
String cleared = normalized
.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
return cleared;
}
- Le premier problème, quand on passe au monde Web, est que l'émulateur GWT ne connait pas
la classe
Normalizer
et que les navigateurs ne connaissent pas les propriétés
de block Unicode.
- Le second est de trouver où substituer la recherche du texte saisi par l'utilisateur
dans l'autocompléteur par un texte alternatif sans accents ni ligatures.
Effet souhaité
Plutôt qu'un long discours, l'idée est d'obtenir ceci :
|
Séquence obtenue en quelques centaines de millisecondes après la saisie des premiers caractères |
Si on clique dans la zone de saisie pour taper du texte, on obtient la liste des pays (image de gauche);
en saisissant les premiers caractères, la liste se réduit (au milieu);
puis le texte est substitué par le premier item possible (à droite). L'utilisateur peut valider ou continuer à saisir
des caractères ou utiliser les flêches pour sélectionner l'item de son choix.
Dans cet exemple, les caractères saisis sont sans accents et en minuscule, mais le même effet serait obtenu avec
des caractères accentués et/ou en majuscule.
Vous pouvez vous-même
expérimenter le widget ici.
La conversion d'une chaîne en caractères latins
Comme dit précédemment, Javascript dans le browser ne permet pas de traiter les diacritiques.
Quelques écrits mentionnent
des solutions plus ou moins viables et complètes, et j'ai jeté mon dévolu sur
celle décrite ici.
Ce document précise que bien que le format Unicode définisse une
table de décomposition
des caractères accentués, les ligatures (AE / OE) y sont absentes.
Pour créer sa table de mapping, l'auteur a recouru à
un script Perl qui contient la liste des noms des caractères à mapper.
Retour à GWT. La solution paraît bonne, il suffit de la transposer en pur Java.
Je commence par importer la librairie
ICU
(International Components for Unicode) car les librairies de base Java ne savent
pas obtenir un caractère depuis son nom :
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>56.1</version>
<scope>compile</scope>
</dependency>
Il n'est évidemment pas question de compiler cela en Javascript, mais simplement d'utiliser
ICU pour créer la table des caractères à substituer. GWT sait faire cela très bien
avec les
générateurs.
Nous disposons donc de la liste des noms Unicode des caractères à transformer :
LATIN CAPITAL LETTER A
LATIN CAPITAL LETTER A WITH ACUTE
...
LATIN SUBSCRIPT SMALL LETTER V
LATIN SUBSCRIPT SMALL LETTER X
... il y en a tout de même plus de 1000 (
liste complète ici).
L'idée est donc d'utiliser dans le générateur la librairie ICU pour retrouver un caractère à partir de son nom :
UCharacter.getCharFromName()
, puis -toujours à partir de son nom- trouver par quoi le substituer.
Par exemple, à partir de "
LATIN CAPITAL LETTER A WITH ACUTE
", on arrive à obtenir le mapping
"Á" -> "A"
,
à partir de "
LATIN CAPITAL LIGATURE OE
" le mapping
"Œ" -> "OE"
et ainsi de suite.
L'élégance de cette solution est que le nom contient à la fois une référence au caractères (code point) Unicode
et par quoi il faut le remplacer (mis en gras) qu'on peut retrouver par le biais de judicieuses expressions régulières.
Pour convertir une chaîne dans notre application Web, le code Java se résume à ceci :
public abstract class LatinMapper {
public static String latinize(String string) {
StringBuilder result = new StringBuilder();
for (int codePoint : unicodeCodePoints(string)) {
String alt = new String(Character.toChars(codePoint));
JSONValue val = MAP.get(alt);
if (val != null) {
alt = val.isString().stringValue();
}
result.append(alt);
}
return result.toString();
}
private static JSONObject MAP = map().isObject();
protected static native JSONValue map() /*-{
var o = {
'Á': 'A', // LATIN CAPITAL LETTER A WITH ACUTE
'Ă': 'A', // LATIN CAPITAL LETTER A WITH BREVE
// ...
'ᵥ': 'v', // LATIN SUBSCRIPT SMALL LETTER V
'ₓ': 'x', // LATIN SUBSCRIPT SMALL LETTER X
};
return @com.google.gwt.json.client.JSONObject::new(Lcom/google/gwt/core/client/JavaScriptObject;)(o);
}-*/;
}
La méthode unicodeCodePoints()
n'est pas affichée ici pour économiser de la place
car elle ne présente que peu d'intérêt : chacun sait qu'un "code point" Unicode peut couvrir
1 ou 2 caractères et GWT supporte ces fonctions très bien.
La partie intéressante est celle qui crée un objet natif Javascript avec tous les mappings.
Comme il y en a plus de 1000, c'est le générateur qui va se charger de surcharger cette
méthode avec la liste complète obtenue depuis les noms.
D'abord la déclaration du générateur :
<generate-with
class="com.github.alternet.demo.countries
.generator.LatinMapperGenerator">
<when-type-assignable
class="com.github.alternet.demo.countries
.client.LatinMapper"/>
</generate-with>
... ensuite il reste quelques adaptations à faire dans LatinMapper
:
la méthode native va devenir non statique de manière à être surchargeable,
et la variable "MAP
" va être initialisée à partir de l'instance
générée :
private static JSONObject MAP;
static {
LatinMapper mapper = GWT.create(LatinMapper.class);
MAP = mapper.map().isObject();
}
Le widget autocompléteur
J'ai utilisé
Sencha GXT qui dispose
d'un autocompléteur qu'il nous suffit d'adapter.
Tout d'abord,
nos données :
{ "AF":"Afghanistan",
"ZA":"Afrique du Sud",
"AL":"Albanie",
"DZ":"Alg\u00e9rie",
"DE":"Allemagne",
...
}
public class Country {
private String code;
private String name;
public Country() {}
public Country(String code, String name) {
this.code = code;
this.name = name;
}
// + getters and setters
}
Il suffit de charger les données dans un magasin :
public ListStore<Country> loadCountries() {
final ListStore<Country> countries = new ListStore<Country> (...)
...
// launch an HTTP request with a callback that parse
// the JSON result :
if (200 == response.getStatusCode()) {
JSONValue val = JSONParser.parseLenient(response.getText());
for (String code : val.isObject().keySet()) {
countries.add(
new Country(
code,
val.isObject().get(code)
.isString().stringValue()));
}
}
}
... on pourrait presque s'amuser à construire une liste des pays en dur comme pour
les mappings de caractères, mais en général ceux-ci sont stockés en BD, ce qui est
somme toute plus évolutif.
Il reste encore à déclarer les méthodes d'accès aux propriétés qui seront
générées par GXT :
interface CountryProperties extends PropertyAccess<Country> {
ModelKeyProvider<Country> code();
LabelProvider<Country> name();
CountryProperties INSTANCE = GWT.create(CountryProperties.class);
}
Et nous sommes prêts pour créer l'autocompléteur !
De ce côté là, la seule difficulté n'est pas de l'adapter, mais
plutôt de trouver la bonne méthode à surcharger. Il m'a fallu
quelques errements en debug mode pour repérer la fonction. Il suffit
de surcharger la méthode ComboBoxCell.itemMatchesQuery()
:
ListStore<Country> countries = loadCountries();
ComboBox<Country> autocompleter = new ComboBox<Country>(
new ComboBoxCell<Country>(
countries,
CountryProperties.INSTANCE.name(),
new LabelProviderSafeHtmlRenderer<Country>
(CountryProperties.INSTANCE.name())
) {
@Override
protected boolean itemMatchesQuery(Country item,
String query) {
String value = getPropertyEditor().render(item);
if (value != null) {
String latinValue = LatinMapper.latinize(value)
.toLowerCase().replace('-', ' ');
String latinQuery = LatinMapper.latinize(query)
.toLowerCase().replace('-', ' ');
return latinValue.startsWith(latinQuery);
}
return false;
}
}
);
Ici, je me paye le luxe de substituer en plus les tirets par des blancs.
Toute autre adaptation en fonction des données à traiter peut être envisagée.
Voilà !
Le code du widget est ici.
Semantic Mismatch
ERR-19 : InheritenceException
Unable to create an instance
of both CUL and CHEMISE
-- See log for details
Aucun commentaire:
Enregistrer un commentaire