Tutoriel HsIndex

Lecture du fichier d’index

Afin de lire les fichiers d’index, il est nécessaire de créer un parser (analyseur syntaxique). J’utilise pour cela la bibliothèque parsec disponible avec l’installation standard de Haskell-plateform. Cette bibliothèque est bien conçu et contient toutes les fonctions pour générer des parser du plus simple au plus complexe.

Format d’un fichier d’index

Un fichier d’index se présente sous la forme suivante

Pour un index un français:

\indexentry{cadre|hyperpage}{9}
\indexentry{charpente!ossature|hyperpage}{9}
\indexentry{charpente!structure|hyperpage}{9}
\indexentry{carburant|hyperpage}{9}

Ou encore pour un index en russe:

\indexentry{\IeC {\cyrs }\IeC {\cyrr }\IeC {\cyre }\IeC {\cyrd }\IeC {\cyrn }\IeC {\cyre }\IeC {\cyre }|hyperpage}{6}
\indexentry{\IeC {\cyra }\IeC {\cyrk }\IeC {\cyrs }\IeC {\cyri }\IeC {\cyro }\IeC {\cyrm }|hyperpage}{6}
\indexentry{\IeC {\cyro }\IeC {\cyrs }\IeC {\cyrsftsn }|hyperpage}{6}
\indexentry{\IeC {\cyrb }\IeC {\cyra }\IeC {\cyrb }\IeC {\cyrb }\IeC {\cyri }\IeC {\cyrt }|hyperpage}{6}

Lecture de la commande \indexentry

Le parser devra relire la commande \indexentry qui se compose comme suit :

  1. Premier argument LaTeX
    • Une entrée d’index.
    • Des sous-entrées d’index séparées par des points d’exclamation !.
    • Une barre verticale suivie de l’étiquette hyperpage.
  2. Deuxième argument LaTeX
    • Le numéro de page.

Commande \indexentry avec une entrée simple

Voyons comment analyser une ligne de commande simple (pas de sous-entrées)

Tout d’abord, on utilise la fonction string de Parsec qui impose une chaîne de caractères.

  string "\\indexentry"

Le paquet imakeidx génère parfois des espaces entre le début de commandes et les arguments. Pour sauter ces espaces on utilise la fonction de recherche char ' ' en association avec le combinateur many qui permet de rechercher (facultativement) des occurrences du parser passé en argument.

  many (char ' ')

pour le contenu entre accolades on pourrait faire deux scan avec char '{' et char '}' comme ceci :

  char '{'
  ...
  char '}'

Mais il est préférable d’utiliser la fonction between de Parsec qui permet de parser des éléments entre deux motifs. Et dans la mesure ou ce motif sera réutilisé par la suite, il vaut mieux créer une fonction dédiée pour parser ce motif:

braces = between (char '{') (char '}')

Pour scanner le contenu de l’index, on appliquera plusieurs fois de manière successives une série de parser spécifiques pour chaque caractère pour prendre en compte les caractères spéciaux du paquet imakeidx.

Pour cela on utilisera le combinateur many1 pour rechercher (impérativement) une ou plusieurs occurrences du contenu. Le combinateur choice permet de tester des parser l’un après l’autre et de renvoyer le résultat du premier parser qui réussi. Ici, on utilise la fonction <- qui permet de récupérer le résultat de parsing et de le conserver.

itm <- many1 (choice pars)

On continue ensuite par détecter la chaîne de caractères :

string "|hyperpage"

Et enfin le deuxième argument contenant le numéro de page. Ici aussi, on conserve le résultat du parser avec la fonction <- :

  n <- braces (many1 digit)

La fonction final sera donc :

parseIDX :: [Parser Char] -> Parser IndexItem
parseIDX pars = do
  string "\\indexentry"
  many (char ' ')
  itm <- braces (do itm <- many1 (choice pars)
                    string "|hyperpage"
                    return itm)
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [read n] [])

La fonction return permet de retourner le résultat de la fonction directement avec le type IndexItem

Commande \indexentry avec des sous-entrées

Pour lire les commandes \indexentry avec des sous-entrées, on utilisera deux version modifiées de la fonction parseIDX :

parseIDXSub :: [Parser Char] -> Parser IndexItem
parseIDXSub pars = do
  string "\\indexentry"
  many (char ' ')
  (itm, sub) <- braces (do  itm <- many1 (choice pars)
                            char '!'
                            sub <- many1 (choice pars)
                            string "|hyperpage"
                            return (itm, sub))
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [] [IndexSubItem sub sub [read n] []])


parseIDXSubSub :: [Parser Char] -> Parser IndexItem
parseIDXSubSub pars = do
  string "\\indexentry"
  many (char ' ')
  (itm, sub, ssub) <- braces (do  itm <- many1 (choice pars)
                                  char '!'
                                  sub <- many1 (choice pars)
                                  char '!'
                                  ssub <- many1 (choice pars)
                                  string "|hyperpage"
                                  return (itm, sub, ssub))
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [] [IndexSubItem sub sub [] [IndexSubSubItem ssub ssub [read n]]])

On essaiera d’appliquer successivement ces fonctions en utilisant de façon combinées l’opérateur <|> qui permet d’appliquer plusieurs parser de manière successive jusqu’à ce qu’un parser réussisse et la fonction try qui permet de tester un parser sans consommer de caractères (Voir la notice de Parsec les explications détaillées sur son fonctionnement).

On aura alors :

parseIndexItem pars =  try (parseIDXSubSub pars)
                   <|> try (parseIDXSub pars)
                   <|>     (parseIDX pars)

Lecture des caractères non ascii

Le parser devra également relire les commandes LaTeX permettant de générer les caractères étrangers (non-ascii). En effet, le paquet imakeidx génère du code LaTeX en utilisant des caractères ASCII (et pas Unicode). Il est donc nécessaire de convertir ces commandes en caractères Unicode correspondant. Par exemple, la commande \IeC {\^e } permet de générer le caractère ê et la commande \IeC {\CYRD } permet de générer la lettre cyrillique Д.

Une liste de correspondance sera créé en dur dans le code sous la forme:

lstSubs :: [(String, Char)]
lstSubs = 
  [
    ("\\^e", "ê")
  , ("\\'e", "é")
  
  ...
  ]

La commande \IeC commune à toutes commandes sera traitée dans une fonction prenant comme argument la chaîne de caractère à tester et le caractère Unicode équivalent.

parseIeC :: String -> Char -> Parser Char
parseIeC s r = do
  string "\\IeC"
  many (char ' ')
  braces (do  many (char ' ')
              char '\\'
              string s
              many (char ' '))

  return r

Comme on peut le voir, la fonction :

  1. Recherche la chaîne de caractères \IeC.
  2. Saute des espaces.
  3. Les accolades ouvrantes et fermantes avec la fonction braces.
  4. Le caractère \.
  5. La chaîne de caractère passée en argument.
  6. Si le parser réussi, la fonction retournera le caractère passé en argument avec return.

Pour tester l’ensemble des caractères, on définit une fonction qui permet de générer une liste de parser incorporés dans une fonction try.

lstParseIeC :: [(String, Char)] -> [Parser Char]
lstParseIeC  =  map (\(f,r) -> try (parseIeC f r)) 

Fonction finale

La fonction que l’on appellera en amont sera :

parseIndexFile :: Maybe [Parser Char] -> Parser [IndexItem]
parseIndexFile Nothing = do
  emptyLines
  itms <- endBy (parseIndexItem parseCharL) endOfLineP
  emptyLines
  eof
  return itms

Avec quelques fonctions annexes :

emptyLines = many emptyLine

emptyLine = do
  many (oneOf " \t")
  endOfLineP

endOfLineP :: Parser String
endOfLineP =    try (string "\n")
            <|> try (string "\r\n")

La fonction parseIndexFile commence par sauter des lignes vides (facultatives) et à capturer des entrées de lexiques séparées par des fin de lignes. La fonction endBy permet de rechercher et de capturer des motifs séparés et terminés par le même motif. La fonction se termine après avoir sauter des lignes vides (facultatives) et détecté la fin du fichier avec eof.

La fonction endOfLineP permet de détecter une fin de ligne au format Unix/Linux (Caractère newline \n simple ) ou une fin de ligne au format Window (Caractère retour chariot \r suivi de nouvelle ligne \n).

La fonction emptyLine recherche avec many des occurrences (facultatives) d’espaces et de tabulations qui se termine par une fin de ligne Unix ou Window avec endOfLineP. Le choix des caractères acceptés se fait avec la fonction oneOf.

La fonction empyLines est simplement une recherche de plusieurs lignes vides.

Voila pour l’analyseur syntaxique du fichier d’index.