Précédent Index Suivant

5   Les commandes

Les commandes LATEX forment la vaste majorité des éléments actifs dont le traitement entraîne des actions plus spécifiques que l'émission de caractères.

5.1   Syntaxe

Par souci de simplification et compte tenu de la pratique des auteurs de documents LATEX (et non de fichiers de style) les noms de commandes reconnus par HEVEA sont décrit par l'expression régulière suivante :
let command =
  '\\' (('@' ? ['A'-'Z' 'a'-'z']+ '*'?) | [^ 'A'-'Z' 'a'-'z'])
C'est à dire que les noms de commande commencent toujours par ``\'', suivi d'une suite non-vide de caractères alphabétiques ou d'un unique caractère non-alphabétique (cf. la commande accent aigu ``\'''), la suite de caractères alphabétiques pouvant être préfixée par ``@'' (cas des commandes internes d'HEVEA), ou suivie de ``*'' (cas des variantes étoilées de LATEX).

Selon [4], les commandes prennent des arguments et sont invoquées ainsi (où command est un nom de commande):
command{arg1}{arg2}...{argn}
Le premier argument d'une commande peut être optionnel, auquel cas on l'invoque ainsi:
command[arg1]{arg2}{...{argn}
Il faut noter que les délimiteurs d'arguments ``{'' et ``}'' (ou ``['' et ``]'') peuvent apparaître à l'intérieur des arguments, à condition d'être bien parenthésés.

La lecture de quelques fichiers source LATEX montre qu'en pratique, la syntaxe de l'invocation de commande est moins rigide. On rencontre fréquemment les variations suivantes : HEVEA traite donc les commandes LATEX, plus les trois variations ci-dessus, rien n'est fait pour reconnaître la syntaxe la plus générale des macros TeX.

5.2   Commandes internes

L'analyseur lexical d'HEVEA reconnaît les occurrences des noms de commande. À chaque nom de commande est associé le nombre et la valeur par défaut des arguments optionnels et le nombre total d'arguments de la commande. Cette information est suffisante pour récupérer les arguments en attente dans le flot d'entrée courant. Les arguments sont lus l'un après l'autre par un nouvel analyseur lexical défini dans le module Save et qui suit les règles de la section précédente. L'analyseur du module Save comprend en gros une entrée arg qui renvoie une chaîne contenant l'argument en tête du flot d'entrée (sans ses délimiteurs) et une entrée opt qui fait de même pour un argument optionnel. Remarquons que l'analyseur principal ignore les subtilités de la reconnaissance des arguments réalisée par le module Save et qu'il s'en trouve simplifié d'autant.

L'analyseur principal contient une simple clause pour reconnaître tous les noms de commande, le nom reconnu étant ensuite recherché parmi les commandes directes c'est à dire les commandes directement réalisées par l'analyseur principal :
| command
    {let lxm = lexeme lexbuf in
    begin match lxm with
      :
    end}
Le choix de reconnaître les commandes directes par un filtrage ``match...'' plutôt que par de nombreuses clauses de l'analyseur limite sérieusement la taille de ce dernier, et ceci sans augmenter sensiblement les temps d'exécution comme je l'ai vérifié.

Dans le cas d'une commande directe, les arguments sont lus explicitement et l'action est réalisée par du code Caml. Cette procédure concerne en premier lieu les commandes internes d'HEVEA. Voici par exemple la clause du match ci-dessus qui reconnaît la commande interne \@print utilisable dans le source LATEX pour écrire directement dans la sortie d'HEVEA :
      | "\\@print" ->
           let arg = Save.arg lexbuf in
           Html.put arg ; main lexbuf
Il existe de nombreuses autres commandes internes et en particulier \@open et \@close qui sont utilisables pour appeler les fonctions cruciales du gestionnaire de sortie open_block et close_block à partir du source LATEX.

Certaines des commandes pré-définies de LATEX sont également reconnues par l'analyseur principal. Voici par exemple la reconnaissance de la commande LATEX \typeout, qui écrit son argument sur la console :
      | "\\typeout" ->
          let what = Save.arg lexbuf in
          prerr_endline what ;
          main lexbuf
(La fonction Caml prerr_endline affiche son argument dans la sortie d'erreur du programme.)

5.3   Appel par nom

Cette section décrit la réalisation par HEVEA des commandes définies par l'utilisateur. On se limite à des commandes utilisateur à nargs arguments, tous non-optionnels. Une telle commande se définit ainsi:
\newcommand{command}[nargs]{body}
(La valeur par défaut de l'argument optionnel nargs est zéro.)

Vu de TeX, l'appel de commande donne lieu à une simple réécriture du flot d'entrée selon un principe de macro expansion: le corps de commande body est mis en tête du flot d'entrée, certains lexèmes (les paramètres formels #1, #2,... ,#9) étant remplacés par les arguments donnés à l'appel (ou paramètres effectifs). Si l'on fait abstraction de certaines constructions TeX qui modifient ce processus, il s'agit là d'une réalisation de l'appel par nom, selon la règle de copie la plus simple.

La réalisation de cet appel par nom par HEVEA est différente: d'une part, le flot d'entrée d'HEVEA est un flot de caractères (et non de lexèmes comme en TeX) ; d'autre part, compte tenu d'une limitation bien compréhensible des analyseurs lexicaux de Caml, il n'est pas possible d'ajouter des éléments devant le flot d'entrée courant. On va donc, d'une part, retarder la substitution des paramètres formels, afin de préserver les limites de lexèmes ; et, d'autre part, réaliser l'illusion d'un flot d'entrée continu. Pour donner cette illusion, on utilise la fonction de bibliothèque Caml Lexing.from_string qui crée un flot d'entrée à partir d'une chaîne passée en argument. On se donne donc la définition auxiliaire suivante, qui applique l'analyseur passé en premier argument sur la chaîne passée en second argument :
let scan_this lexfun s =
  lexfun (Lexing.from_string s)
Le module Macros gère dynamiquement un environnement des commandes. À chaque nom de commande enregistré sont associés, un motif qui résume les informations sur les arguments de la commande (nombre, valeur par défaut des arguments optionnels), ainsi qu'un corps de commande. On enregistre et on récupère une définition de commande par les deux fonctions suivantes:
val def_macro: string -> pat -> string -> unit
val find_macro: string -> pat * string
Voici ensuite un source simplifié qui schématise la la réalisation de l'appel de commande :
  | command
      {let lxm = lexeme lexbuf in  
      begin match lxm with
       :
(* Appel de commande, cas général *)
      | _ ->
         let pat,body = Macros.find_macro name in
         push env_stack !env_args ;
         env_args :=  make_env pat lexbuf ;
         scan_this main body ;
         env_args := pop env_stack ;
         main lexbuf
       end}
(* Fin du flot d'entrée *)
  | eof {()}
Le fragment ci-dessus utilise deux nouvelles variables globales, une référence vers un tableau de chaînes ``env_args'' et une pile de ces tableaux ``env_stack''. Le tableau ``env_args'' incarne la liaison courante entre paramètres formels et paramètres effectifs. Initialement il n'y a ni corps de commande ni arguments et la référence ``env_args'' contient un tableau de taille zéro. L'environnement ``env_args'' est initialisé lors de l'appel de commande, par l'appel de fonction ``make_env pat lexbuf'' dont nous nous contenterons d'admettre qu'il lit les arguments en attente dans le flot courant ``lexbuf'' à l'aide de l'analyseur Save.arg. Notez que l'environnement actif à la reconnaissance de l'appel de commande est sauvé (par push) et restauré (par pop) avant et après l'analyse du corps de cette commande. En effet, le ``main lexbuf'' final doit se faire dans un contexte identique à celui qui prévalait avant la reconnaissance de l'appel.

La pseudo-expression régulière ``eof'' signale la fin du flot d'entrée. Dans le cas d'un corps de commande, elle indique la fin du corps et l'analyse se termine par l'action vide ``{()}''. Par conséquent, l'analyse d'un corps de commande ``scan_this main body'' termine, la traduction du corps de commande substitué ayant été au préalable générée par effet de bord.

Le remplacement des paramètres formels par leur valeurs est retardé jusqu'à ce que l'analyseur principal découvre leurs occurrences :
  | '#' ['1'-'9']
      {let lxm = lexeme lexbuf in
      let arg = !env.(Char.code (lxm.[1]) - Char.code '1') in
      let old_env = !env_args in
      env_args := pop env_stack ;     
      scan_this main arg ;
      push env_stack !env ;
      env_args := old_env ;
      main lexbuf}
L'action de la clause ci-dessus réalise la substitution en récupérant le paramètre effectif ``arg'' à sa place dans l'environnement ``env_args''. La chaîne ``arg'' est ensuite analysée, mais elle peut elle-même contenir des paramètres formels à instancier. Ces paramètres font référence à l'environnement tel qu'il était au moment de l'appel de la commande dont le corps est en cours d'analyse, c'est à dire au moment de la création de ``env_args''. L'environnement correspondant, qui se trouve en sommet de la pile ``env_stack'' est donc restauré par ``env_args := pop env_stack ;'' avant l'analyse de ``arg''.

Il nous reste à examiner la reconnaissance des définitions de commande. On se donne d'abord une fonction make_pat qui renvoie un motif à partir d'une liste d'arguments par défaut et d'un nombre total d'argument, ainsi qu'une fonction save_opt qui lit un argument optionnel dans un flot d'entrée et renvoie un argument par défaut en cas d'échec. L'analyseur principal comporte ensuite la clause suivante :
| "\\newcommand"
   {let name = Save.arg lexbuf in
    let nargs = save_opt "0" lexbuf in
    let body  = Save.arg lexbuf in
    Macro.def_macro (make_pat [] (string_of_int nargs)) body ;
    main lexbuf}
Cette présentation simplifiée de la réalisation des commandes est suffisante pour comprendre un des mécanismes centraux d'HEVEA. Les constructions supplémentaires effectivement réalisées sont les commandes utilisateur avec argument optionnel, la redéfinition de commande et une gestion à la LATEX des espaces qui suivent les commandes sans arguments (ces espaces n'apparaissent pas dans le HTML produit).

Il importe de remarquer qu'HEVEA ne réalise pas toute la fonctionnalité des macros de TeX (et donc des commandes de LATEX). En particulier, les commandes doivent apparaître suivies de tous leurs arguments.

5.4   Environnements de LATEX

HEVEA traite les environnements LATEX tels que décrits dans [4, sections C.8.2 et C.8.3]. Je présente le cas simplifié d'un environnement sans argument.

La définition d'environnement est donc 
\newenvironment{env}{body1}{body2}
Elle provoque la création de deux commandes \env et \endenv de corps respectifs body1 et body2. L'ouverture d'environnement ``\begin{env}'' déclenche l'appel de \env, tandis que la fermeture ``\end{env}'' se traduit par un appel à \endenv. Enfin, les environnements définissent la portée des définitions de commande, comme en LATEX. Ceci est réalisé par l'enregistrement du nom des commandes définies dans une liste à purger par la fonction close_env dont je ne détaillerai pas la nouvelle définition (voir la section 4.3 pour l'ancienne définition). Voici des clauses simplifiées pour ``\begin'' et ``\end'' :
| "\\begin"
   {let env = Save.arg lexbuf in
   open_env env ;
   scan_this main ("\\"^env) ;
   main lexbuf}
| "\\end"
   {let env = Save.arg lexbuf in
   scan_this main ("\\end"^env) ;
   close_env env ;
   main lexbuf}
Le code ci-dessus est subtil. En effet, l'environnement env est ouvert avant d'exécuter la commande initiale \env, tandis que env est fermé après l'exécution de la commande finale \endenv. Cela rend possible les définitions d'environnement qui ouvrent et ferment d'autres environnements, comme par exemple :
\newenvironment{monquote}{\begin{quote}\em}{\end{quote}}
La reconnaissance de ``\begin{monquote}'' entraîne l'initialisation de l'environnement courant à "monquote" (c'est à dire que la référence ``cur_env'' contient ``"mon_quote"''), puis l'analyse de ``\monquote'' qui à son tour entraîne celle de ``\begin{quote}''. L'environnement courant "monquote" est donc immédiatement empilé et remplacé par "quote". Normalement, si les environnements LATEX sont correctement imbriqués, la reconnaissance de ``\end{monquote}'' se fera alors que l'environnement courant est "quote". Mais, la commande ``\endmonquote'' sera exécutée, son corps contient ``\end{quote}'' qui fermera l'environnement quote des sorte que l'environnement courant sera revenu à "monquote" au moment du contrôle.


Précédent Index Suivant