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 :
-
Le caractère ``
{
'' ou ``[
'' qui introduit les arguments
peut être précédé d'espaces, de sauts de ligne et même de commentaires...
- Les délimiteurs ``
{
'' et ``}
'' peuvent être omis
lorsque l'argument d'une commande
à un seul argument est un seul caractère
(cf. ``\'el\`eve
''), ou un nom de commande (cf. ``\^\i
'').
- Certaines commandes internes prennent plus d'un argument
optionnel.
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
\end
env 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 à
\end
env.
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 \end
env.
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.