Le fameux « Algorithme de Collation Unicode (UCA) » expose cruellement les enjeux du tri lexicographique :

“Collation implementations must deal with the often-complex linguistic conventions that communities of people have developed over the centuries for ordering text in their language, and provide for common customizations based on user preferences. [...] Many people see the Unicode code charts, and expect the characters in their language to be in the ‘correct’ order in the code charts. Because collation varies by language —not just by script—, it is not possible to arrange code points for characters so that simple binary string comparison produces the desired collation order for all languages. Because multi-level sorting is a requirement, it is not even possible to arrange code points for characters so that simple binary string comparison produces the desired collation order for any particular language. Separate data tables are required for correct sorting order.”

Unicode n'est pas censé refléter l'ordre lexicographique de votre langue ! (Capture d'écran sous BabelMap.)

Cela étant, et considérant les problèmes de performance liés au scripting, le principal défi auquel nous devons faire face est celui de synthétiser un algorithme acceptable en termes d'efficacité, sans pour autant noyer notre code dans l'océan infini des tables Unicode. Ainsi, je ne pense pas qu'il serait sage d'adresser l'intégralité de la base UCA dans un script InDesign ! Et même dans ce cas, il nous faudrait encore implémenter le « tailoring », outil d'appoint permettant de réajuster l'algorithme principal afin de se conformer aux réquisits de chaque langue — cf. Unicode Common Locale Data Repository.

À la recherche d'un compromis

À ma connaissance, Peter Kahrel est le seul auteur qui ait trouvé une solution pratique et robuste au problème du classement alphabétique dans InDesign. Son « Language-Aware Paragraph Sorter » est un script ciselé qui supporte plusieurs langues et un jeu d'options impressionnant : tri « lettre par lettre » (vs « mot par mot »), sensibilité à un style de caractère, tri rétrograde, suppression des doublons, etc. Mieux, Peter a inventé une syntaxe aussi simple qu'élégante permettant de spécifier et de personnaliser les règles de collation propre à chaque langue :

Syntaxe mise en œuvre dans le script de tri lexicographique de Peter Kahrel.

Note. — Les utilisateurs non anglophones du trieur de Peter Kahrel rencontreront probablement un petit problème d'adressage des langues depuis le fichier sortorders.txt qui est fourni avec le script d'origine. Cela tient probablement au fait que la collection Application.languagesWithVendors sollicitée par le script retourne des informations localisées linguistiquement, alors que la table sortorders.txt a été ajustée à l'interface US d'InDesign. Pour permettre au script de reconnaître les langues telles qu'elles sont nommées en français, vous devez éditer sortorders.txt et remplacer les premiers champs de la table par leur correspondant dans l'interface française. Par exemple, [No Language] devient [Aucune langue], Polish devient Polonais, etc. Les langues disponibles sous InDesign sont répertoriées dans la palette Caractères, rubrique Langue. Une fois cette opération effectuée, le script affichera correctement les correspondances entre la langue sélectionnée et son alphabet :

Boîte de dialogue du script de Peter Kahrel (après réaiguillage des langues).

M'inspirant du travail de Peter Kahrel, j'ai alors cherché à réaliser un composant JavaScript que je puisse réutiliser dans mes propres projets, chaque fois que j'aurais à appliquer un tri alphabétique « sensible à la langue ». L'idée était d'implémenter une sorte d'algorithme de collation simplifié, basé sur des règles générales de classement lexicographique en Europe — European Ordering Rules (EOR) — et s'intéressant spécifiquement aux alphabets latins ou dérivés du latin. Je souhaitais traiter l'ensemble des caractères situés dans les blocs latins d'Unicode, y compris les blocs étendus, les formes spéciales et certaines lettres de l'IPA (alphabet phonétique international) susceptibles d'être translittérées dans l'alphabet de base. Je souhaitais également gérer un algorithme de classement multi-niveaux (premier ordre, diacritiques, casse). Et bien sûr avec prise en charge spécifique de différentes langues.

L'interface TCollator

Ma quête a débouché sur un objet expérimental, TCollator, qui intègre le maximum de fonctionnalités sans utiliser l'UCA. Pour ce faire, je me suis principalement abreuvé à trois sources : l'utilitaire de recherche avancée de BabelMap (excellent navigateur Unicode d'Andrew West), les Mimer SQL Unicode Collation Charts et un large réservoir d'articles Wikipedia relatifs à ces sujets, en particulier « Collating sequence » qui en offre un bon panorama.

TCollator constitue une bibliothèque minimale. Il vous suffit d'inclure le fichier au début de votre propre script, de cette façon :

#include 'collator.jsxinc'
 
// Votre code ici
 

C'est tout ! La bibliothèque crée uniquement une variable nommée TCollator, qui se comporte comme une interface. Toutes les méthodes sont statiques (c'est-à-dire que vous ne créez pas d'instances de l'objet). Voyons comment cela fonctionne sur un simple exemple :

// Mon 1er script avec TCollator
 
#include 'collator.jsxinc'
 
var arr = ["HLM","ÉDF","muze","où","œuf",
    "oval","muß","à-ha","a-ha"];
 
// Tri JS par defaut
arr.sort();
alert( "Tri JS par défaut:\r\r" +
    arr.join('\r') );
 
// Tri avec TCollator
arr.sort(TCollator.compare);
alert( "Tri avec TCollator:\r\r" +
    arr.join('\r') );
 

Résultat :

Tri lexicographique : à gauche, tri JS par défaut ; à droite, tri via TCollator.

Dans le fragment de code ci-dessus, l'instruction importante est bien sûr :
arr.sort(TCollator.compare).

TCollator.compare est une propriété statique pointant sur une fonction, laquelle fonction nous transmettons à arr.sort(). Il s'ensuit que la méthode Array.sort n'invoque plus la routine de comparaison par défaut de JavaScript, mais notre routine TCollator.compare, qui compare deux chaînes de caractères selon nos propres critètes de classement.

Ainsi, alors que JS estime à tort que le mot "muze" se classe avant "muß" ( "muze" < "muß" ! ), la fonction TCollator.compare apporte une réponse opposée... et correcte :

var cmp = TCollator.compare("muß","muze") ;
 
if( cmp < 0 ) alert( "muß < muze");
if( cmp > 0 ) alert( "muß > muze");
if( cmp == 0 ) alert( "muß == muze");
 
// => muß < muze
 

Par quel miracle cela fonctionne-t-il ? En profondeur, TCollator embarque une sorte de base de données stockant les traits caractéristiques d'un grand nombre de caractères latins. Le composant sait, par exemple, que 'ß' (U+00DF) est une lettre minuscule qu'on peut tenir pour équivalente au digramme 'ss'  sous certaines conditions. Vous avez d'ailleurs la possibilité d'interroger directement cette base de connaissance grâce à l'utilitaire TCollator.charInfos() :

#include 'collator.jsxinc';
 
alert( TCollator.charInfos("ß") );
 

Réponse :

La méthode utilitaire TCollator.charInfos( ).

TCollator.charInfos(myCharacter) renvoie un object rassemblant diverses informations sur le caractère soumis en argument :

PROPRIÉTÉ TYPE DESCRIPTION
case string "LOWERCASE", "TITLECASE", "UPPERCASE", ou "SMALLCAP".
character string Le caractère lui-même.
collationInfo string Détails additionnels.
diacritics array Tableau de chaînes parmi :
"ACUTE", "GRAVE", "BREVE", "CIRCUMFLEX", "CARON", "RING", "DIAERESIS", "DOUBLE ACCENT", "TILDE", "DOT", "CEDILLA", "OGONEK", "MACRON", "STROKE", "HOOK", "SWITCH", "MODIFIER", "SPECIAL".
expanded string Forme élémentaire décomposée d'un multigramme. Par ex. :
U+00E6 -> ae, U+01C4 -> DZ,
U+00DF -> ss, U+FB03 -> ffi
group string "LATIN", "SPECIAL", "NUMERAL", ou "PUNCTUATION"
isMultigram bool Indique si le caractère est un digramme, un trigramme, une ligature.
isSupported bool Indique si le caractère est pris en charge par TCollation.
kind string Classe élémentaire à laquelle appartient la lettre. (Certaines translittérations sont possibles.)
unicode string Rang Unicode sous la forme "UUUU" (ex. : "012A").

Note. — L'objet retourné possède également une méthode toString() personnalisée, ce qui simplifie l'affichage des informations via alert( ). Si vous envisagez d'utiliser TCollator.charInfos() dans votre script, n'oubliez pas de consulter la propriété isSupported avant de vous engager dans d'autres traitements.

Affiner les critères de classement

Chaque fois que c'est possible, un caractère est converti et reclassé parmi ceux de l'alphabet fondamental, ce qui permet de lui attribuer un poids par défaut lors du mécanisme de comparaison. Cela produit généralement des résultats très satisfaisants, même lorsque vous triez des mots étrangers contenant des caractères ou diacritiques inhabituels. TCollator agit par défaut au niveau 3, c'est-à-dire qu'il discrimine les diacritiques et la casse des lettres selon les principes de la spécification EOR. Par exemple, trier le tableau ["boc", "BÔB", "bõb", "bob","BOB","bôb", "BOA"] via TCollator.compare débouchera sur le classement suivant :

BOA
bob
BOB
bòb
bôb
BÔB
bõb
boc
 

Si vous n'avez pas besoin de tels raffinements, vous pouvez améliorer les performances en réduisant le niveau de collation grâce à la méthode TCollation.reset() :

TCollator.reset( langCode , level ).

• langCode (String; Déf. : "DEF"). L'un des codes fournis par TCollator.getLocales() (voir infra).

• level (Number; Déf. : 3): Niveau ou « profondeur » de collation, un entier compris entre 0 et 3 :

LEVEL DESCRIPTION
0 Niveau élémentaire ne distinguant pas les chiffres, la casse ni les diacritiques (sauf ceux spécialement isolés dans l'alphabet de la langue sélectionnée). On a donc par défaut :
õ = ò = O (quelle que soit la casse)
et: 0 = 1 = 2 ...
1 Similaire au niveau 0, sauf que les chiffres sont différenciés :
0 < 1 < 2 ...
2 Applique le niveau 1, puis les règles EOR (European Ordering Rules) pour discriminer les diacritiques et certaines formes spéciales qui ne seraient pas gérés dans la langue sélectionnée :
bob < bòb < bôb < bõb ...
3 Applique le niveau 2, puis discrimine la casse si besoin :
bob < BOB < bôb < BÔB ...

Remarquez, à titre d'exemple, que le caractère Dz (U+01F2 – CAPITAL LETTER D WITH SMALL LETTER Z) sera analysé par défaut comme un digramme possédant une casse de titre (titlecase), basé sur D et étendu à "Dz". (Consultez TCollator.charInfos("\u01F2") pour visualiser ces détails.) En clair, à moins qu'une langue adresse expressément ce caractère comme une lettre distincte de son alphabet (le slovaque, par ex.), TCollator verra U+01F2 comme strictement équivalent à "Dz". Au niveau 3, cela conduit donc à :
dz < U+01F2 = Dz < DZ

Le premier argument de la méthode TCollator.reset(langCode,level) prend par défaut la valeur "DEF", qui implémente un mécanisme générique convenant à la plupart des systèmes d'écriture dérivés de l'alphabet latin, ou dans le cas d'un lexique multilingue. Cependant, vous pourriez avoir besoin d'appliquer les règles de classement d'une langue en particulier. Ce point est crucial dans les alphabets qui introduisent des lettres supplémentaires ou traitent certains digrammes comme des lettres séparées. Par exemple, en danois et en norvégien, Æ, Ø et Å se rangent après Z, et Aa s'analyse comme un équivalent de Å. Jusqu'en 1997, l'espagnol classait les digrammes CH et LL comme des lettres à part entières (niveau 1), tandis que Ñ demeure une lettre distincte rangée après N.

Pour faire face à cette diversité, TCollator propose une quarantaine d'alphabets différents. Chacun est associé à un code de deux ou trois lettres qu'il suffira de passer en argument à TCollator.reset() pour initialiser l'algorithme dans cette langue particulière.

Note. — Pour restaurer la configuration générique, utilisez :
TCollator.reset('DEF'), ou simplement TCollator.reset().

La liste complète des alphabets / langues supportés est accessible sous forme de tableau (Array) par TCollator.getLocales(). Chaque élément de ce tableau est à son tour un tableau de deux chaînes de caractères : d'abord un nom descriptif, ensuite le code à utiliser.

NOM CODE NOM CODE
[Default] DEF Indonesian DEF
Albanian SQ Irish Gaelic DEF
Basque ES2 Italian DEF
Bosnian BCS Kurdish (Hawar) KU
Breton BR Latvian LV
Croatian BCS Lithuanian LT
Czech CS Luxembourgish DEF
Danish DNA Malay DEF
Dutch (IJ=Y) NL1 Maltese MT
Dutch (Modern) NL2 Moldavian RO
English DEF Norwegian DNA
Esperanto EO Polish PL
Estonian ET Portuguese DEF
Faroese FO Romanian RO
Filipino (Modern) TL Serbian BCS
Finnish (Multilingual) FI2 Slovak SK
Finnish (Official) FI1 Slovene SL
French DEF Spanish (Modern) ES2
Frisian FY Spanish (Traditional) ES1
German (Decompose umlauts) DE2 Swedish (Modern) SV
German (Dictionary) DEF Swedish (V=W) FI1
Greenlandic KL Turkish TR
Hungarian HU Turkmen TK
Icelandic IS Welsh CY

À titre d'illustration, configurons TCollator pour ordonner une liste de mots norvégiens :

#include 'collator.jsxinc';
 
// Config : norvegien - niveau 3
TCollator.reset('DNA',3);
 
var myStrings = ["åbner","brænder","jeg","altså","ordet",
    "vild","bryst","ære","øje","brød","fjord"];
 
myStrings.sort(TCollator.compare);
 
alert( myStrings.join('\r') );
 
/* RESULTAT :
========
 altså
 bryst
 brænder
 brød
 fjord
 jeg
 ordet
 vild
 ære
 øje
 åbner
========
*/
 

Exemple de script InDesign

Voici un autre script simple qui exploite TCollator pour extraire et ordonner rapidement une liste de mots à partir du texte sélectionné dans le document actif :

#include 'collator.jsxinc'
 
function alphaSort()
{
    var s = app.selection,
        i, t, str;
 
    if( !s.length ) return false;
 
    if( !('contents' in s=s[0]) )
        {
        if( !('parentStory' in s) ) return false;
        s = s.parentStory;
        }
 
    s = s.contents.
        replace(/[\s\.,;:!?]+/g,'|').
        split('|');
 
    if( s.length > 1000 ) s.length = 1000;
    s.sort(TCollator.compare);
 
    i = s.length;
    t = false;
    while( i-- )
        {
        str = s[i];
        if( str && str!=t)
            {
            t = str;
            continue;
            }
        s.splice(i,1);
        }
 
    return (s.length && s)||false;
}
 
var res = alphaSort();
if( false===res )
    alert( "Please select some text!" );
else
    alert( "Alphabetical Sort (default settings):\r\r" +
        res.join(' | ') );
 

Rappelons que TCollator est conçu comme un outil primitif pour élaborer des scripts plus complexes. Il n'interagit pas en lui-même avec l'UI d'InDesign (de telle sorte que vous pourriez probablement réutiliser cette bibliothèque dans d'autres environnement ExtendScript de la suite CS). Si vous souhaitez trier des paragraphes au sein d'un document, utilisez le script de Peter Kahrel mentionné plus haut, ou créez votre propre script en sollicitant le composant TCollator comme dans l'exemple ci-dessus.

Avertissement

TCollator est actuellement un pur outil expérimental, en version beta. Merci d'avance de vos retours, surtout si vous testez cet utilitaire dans plusieurs des langues / alphabets proposés. Je ne saurais garantir que l'algorithme de comparaison respecte en tous points les conventions lexicographiques et usages culturels pratiqués ici et là.

TCollator gère un cache interne pour maintenir des performances acceptables avec des listes plafonnant autour de quelques milliers de mots. Si vous observez des ralentissements après plusieurs tris basés sur Array.sort(TCollator.compare) au sein d'un même script, invoquez TCollator.reset pour vider le cache.