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.
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):
{
arg1}{
arg2}
…{
argn}
Le premier argument d’une commande peut être optionnel, auquel cas on l’invoque ainsi:
[
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 :
{
” ou “[
” qui introduit les arguments
peut être précédé d’espaces, de sauts de ligne et même de commentaires…{
” 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
”).
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.
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.)
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.
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.