Transformer un texte en url

La réécriture d’urls est quasi-incontournable de nos jours. Qu’il s’agisse de présenter des urls à caractère plus humain, ou de tenter d’améliorer son positionnement (bien que les mots dans l’url n’aient plus véritablement de poids), il est essentiel de pouvoir générer rapidement une url propre.

Mais d’abord, commençons par définir ce qu’est une url propre. Selon moi, il s’agit d’une url :
- ne contenant que des lettres minuscules et des chiffres
- ne contenant aucun accent, ni caractère spécial, ni ponctuation
- dont les mots sont séparés par des tirets
- ne comportant pas deux tirets successifs
- ne commençant et ne finissant pas par des tirets

Il existe une fonction PHP, urlencode(), qui fait en sorte de transformer une chaîne de caractères de manière à pouvoir la passer en url, mais elle ne convient pas pour obtenir quelque chose de propre : les espaces sont transformés en signe « + », ce qui n’est pas très agréable pour la lisibilité, et les caractères spéciaux (accents compris) sont changés en un signe pourcentage, suivi de deux chiffres hexadécimaux (un espace devient ‘%20’, par exemple). C’est suffisant pour avoir une url valide, mais c’est loin d’être lisible, d’où la nécessité de développer quelque chose de plus poussé.

Enlever les accents

Pour commencer, nous allons nous atteler à la suppression des accents. Nombre de fonctions circulent sur Internet [1], mais je ne les trouve pas correctes : elles effectuent un remplacement caractère par caractère, si bien qu’il est nécessaire de renseigner deux fois chaque caractère, en version accentuée et non accentuée. De plus, si un caractère n’est pas prévu dans la liste, il n’est tout simplement pas remplacé.

Je pars d’un constat différent : quand un caractère accentué est codé en HTML, il est toujours formé de la manière suivante :

& + lettre + accent + ;

Il nous suffit donc de lister tous les accents possibles, et nous sommes sûrs de cibler tous les caractères accentués imaginables. Je définis donc une expression régulière qui détectera chaque caractère accentué, et n’en conservera que la lettre. Il faudra bien entendu transformer tous les accents en leur équivalent HTML, avec la fonction htmlentities(). J’y précise le jeu de caractères UTF-8, sans quoi les ligatures « e dans l’o » (œ) ne seront pas correctement transformées.

function enleve_accents($chaine) {
    $reg = '/&(.)(acute|grave|circ|uml|cedil|ring|tilde|slash);/';
    return preg_replace($reg, '1', htmlentities($chaine, ENT_COMPAT, 'UTF-8'));
}

Enlever les ligatures

Les ligatures (e dans l’o, etc) ne sont pas affectées par l’expression régulière utilisée pour les caractères accentués, il nous faut donc une fonction supplémentaire pour les gérer.
J’y gère toutefois une exception : l’eszett allemand (ß) est codé en HTML par « ß », ce qui nous donnerait « sz », alors que les caractères de remplacement normalement utilisés sont « ss ».

function enleve_ligatures($chaine) {
    $chaine = str_replace('ß', 'ss', $chaine);
    $reg = '|&([a-zA-Z]{2})lig;|';
    return preg_replace($reg, '1', $chaine);
}

Suppression des caractères spéciaux

Maintenant que tous les accents sont supprimés, nous allons supprimer les autres caractères spéciaux. Par caractère spécial, nous entendons « tout ce qui n’est pas une lettre ou un chiffre ». Nous allons donc utiliser une autre expression régulière, qui ciblera cette fois-ci les caractères formés de la manière suivante :

& + code + ;

L’expression est assez large, il sera donc indispensable de l’utiliser après les fonctions précédentes, sans quoi nous supprimerions également les accents et les ligatures. Il n’est pas nécessaire de transformer les entités HTML, puisque les fonctions seront enchaînées, et que la première le fait déjà.

function suppr_speciaux($chaine) {
    $reg = '|(&[a-zA-Z0-9]*;)|U';
    return preg_replace($reg, '-', $chaine);
}

Suppression des tirets excédentaires

À ce stade, nous obtenons quelque chose d’approchant nos désirs. Il ne reste plus qu’à réduire les éventuelles chaînes de tirets, et à supprimer les tirets de début et fin de texte.

Nous commençons par mettre notre chaîne de caractères en minuscules :

$texte = strtolower($texte);

Pour transformer les chaînes de tirets, j’utilise encore une fois une expression régulière :

$reg = '|([^a-z0-9]+)|';
$texte = preg_replace($reg, '-', $texte);

Cette expression transforme toute suite de caractères qui n’est ni une lettre, ni un chiffre, en tiret. Elle nous permet par conséquent de transformer une suite de plusieurs tirets en un seul, mais permet également de supprimer des caractères spéciaux qui seraient éventuellement passés entre les mailles du filet précédent, ainsi que tous les signes de ponctuation.

Pour finir, nous supprimons les tirets de début et de fin. La première fois que j’avais rédigé cette fonction, je testais le premier et le dernier caractère, et raccourcissais la chaîne au besoin. Je me suis toutefois demandé si une fonction PHP ne le faisait pas déjà nativement. Après quelques recherches, j’ai découvert que la fonction trim(), utilisée par défaut pour supprimer les espaces, tabulations et retours chariots autour d’une chaîne de caractère, pouvait prendre comme second argument, une liste de caractères supplémentaires à enlever. Le code se simplifie d’un seul coup, pour tenir en une seule ligne :

$texte = trim($texte, '-');

Il ne nous reste plus qu’à assembler le tout dans une fonction, ce qui nous donne :

function nettoie_url($texte) {
    if(!is_utf8($texte))
         $texte = utf8_encode($texte);
    $texte = strtolower(suppr_speciaux(enleve_ligatures(enleve_accents($texte))));
    $reg = '|([^a-z0-9]+)|';
    $texte = preg_replace($reg, '-', $texte);
    return trim($texte, '-');
}

J’ai ajouté une gestion de conversion de jeu de caractère, si le texte n’est pas en UTF-8. J’ai emprunté la fonction is_utf8() au code source de SPIP :

function is_utf8($string) {
return !strlen(
     preg_replace(
          ',[x09x0Ax0Dx20-x7E]' # ASCII
          . '|[xC2-xDF][x80-xBF]' # non-overlong 2-byte
          . '|xE0[xA0-xBF][x80-xBF]' # excluding overlongs
          . '|[xE1-xECxEExEF][x80-xBF]{2}' # straight 3-byte
          . '|xED[x80-x9F][x80-xBF]' # excluding surrogates
          . '|xF0[x90-xBF][x80-xBF]{2}' # planes 1-3
          . '|[xF1-xF3][x80-xBF]{3}' # planes 4-15
          . '|xF4[x80-x8F][x80-xBF]{2}' # plane 16
          . ',sS',
          '', $string));
}

Précision

Cette fonction ne marche que si les fichiers sont encodés en UTF-8. Je fournis la même fonction marchant en ISO dans les commentaires de l’article.


[1] Comme celle-ci, par exemple.