Précédent Suivant Index

Chapitre 2 :   Texte formaté

2.1   Description et but du module

Après le mode HTML, il est intéressant de traduire LATEX vers un mode texte simple, sans indication de formatage dans le fichier de sortie. En effet, le document sera visible par tout le monde, sans nécessiter de navigateur internet ou de visualiseur postscript ou dvi. De plus, un fichier texte est plus court que l'équivalent en LATEX ou en HTML, sauf éventuellement pour certaines structures comme les tableaux. J'ai donc réalisé une extension d'HEVEA rendant possible une sortie de texte formaté.

Lorsqu'on veut formater du texte, il faut interpréter le source LATEX, non pas pour poser des tags et laisser le formatage à un visualiseur externe, mais pour traiter les environnements et réaliser effectivement les changements de style. Dès le départ, on peut remarquer qu'une bonne partie du traitement du texte va être impossible à rendre en mode texte. Cela concerne les changements de fonte et de style comme le passage en italique. Le texte est sans couleur, et de taille fixe. Cela donnera aux documents un aspect plus sobre. Dans certains cas, pourtant, il est possible de rendre un changement de fonte, comme pour l'environnement verbatim, ou le texte va être mis entre guillemets.

Pour bâtir ce module de sortie, j'ai procédé par étapes successives, en adaptant le modèle HTML qui fonctionnait. En effet, il ne faut pas, en rajoutant un module et en adaptant le programme au cas particulier, rajouter des erreurs dans les modules qui existent déjà. il a fallu mettre au point dans un premier temps une interface commune au deux modules de sortie, extensible éventuellement à d'autres langages de sortie. Cette interface a été décrite dans la section 1.3. Je vais maintenant expliquer comment est réalisé le formatage de texte.

Le module Text est implémenté dans le fichier text.ml. Il fonctionne essentiellement sous deux modes, selon le type du bloc en cours. Un bloc ( ou un de ses parents ) peut en effet être déclaré temporaire ou non. Les blocs temporaires ne sont pas formatés, alors que les autres blocs sont formatés systématiquement dans l'environnement en cours. Un traitement spécial est réservé aux tableaux, qui posent des problèmes de formatage particuliers. Il faut en effet formater récursivement les cellules, puis le tableau dans l'environnement du bloc parent. La gestion des formules mathématiques est faite à l'aide d'un modèle similaire à celui utilisé en mode HTML, c'est à dire grâce à des tableaux qui vont permettre de positionner les formules.

2.2   Fonctionnement

Mis à part le cas des blocs temporaires, tous les caractères envoyés au module de sortie sont aussitôt traîtés et mis en forme, puis écrits dans le fichier de sortie. On retarde l'écriture dans le bloc d'une ligne au maximum. La gestion des environnements LATEX par les blocs consiste alors essentiellement à modifier les paramètres de mise en forme et à garantir le bon positionnement du texte à l'intérieur du bloc en cours.

2.2.1   Formatage

Les caractères sont imprimés à l'aide de la fonction put_char, ou de put pour les chaînes. Ces deux fonctions redirigent un à un les caractères vers une fonction do_put_char qui les envoie soit vers une procédure de formatage, soit vers la procédure d'écriture dans le bloc dans le cas d'un bloc temporaire :

let do_put_char c =
  if !cur_out.temp || (Out.is_null !cur_out.out) 
  then do_do_put_char c
  else do_put_char_format c
;;
Il faut afficher le texte en suivant quelques règles de formatage. L'unité de texte étant le paragraphe,on descend ensuite au niveau de la ligne pour effectuer les alignements. Il faut en effet déterminer les lignes, en les remplissant le plus possible, avant de définir ce qui va délimiter les paragraphes.

Les paragraphes sont formatés comme en figure 2.1, et doivent remplir les critères suivants :


Figure 2.1 : Format d'un paragraphe

Pour décrire cet environnement, on a les variables et les types suivants, qui sont sauvegardées à chaque ouverture de bloc dans des piles individuelles et restaurées à la fin du bloc.

type align_t = Left | Center | Right
type flags_t = {
     (...)
    mutable align : align_t;
    mutable in_align : bool;
    mutable hsize : int;
    mutable x : int;
    mutable x_start : int;
    mutable x_end : int;
    mutable last_space : int;
    mutable first_line : int;
    mutable underline : string;
     (...)
  }

let line = String.create (!Parse_opts.width +2);;
La variable x représente la position courante dans la ligne en cours. in_align est utilisée pour savoir si on est à l'intérieur d'un environnement LATEX center, flushleft ou flushright. Cela sera utilisé dans la gestion des blocs.

last_space contient la position du dernier espace rencontré sur la ligne. Cet endroit correspond en effet à la position la plus proche où on peut couper le texte en cas de débordement de la ligne. underline est une chaîne qui sert à souligner les titres. Si la chaine est vide, on ne souligne pas. Sinon, on souligne avec les caractères de cette chaîne.

line est le tampon de la ligne courante. On le remplit au fur et à mesure jusqu'à ce qu'il déborde ou qu'on finisse la ligne. Il est initialisé avec la taille maximale de la ligne pour le document, plus deux pour gérer les dépassements de capacité éventuels.

On reconnait le début d'un paragraphe au fait que la ligne précédente n'a pas été coupée, c'est à dire on a recu un caractère de fin de ligne `\n' pour terminer la ligne. En effet, en LATEX, il faut une succession de au moins deux caractères `\n' pour changer de paragraphe. Cela est reconnu dans le module Scan, qui envoie au module de destination un seul caractère `\n'. Etant donné que c'est le seul cas où on recevra un tel caractère, c'est caractéristique d'un changement de paragraphe.

On procède donc en deux étapes. Tout d'abord, on reçoit les caractères un à un et on les rajoute dans le tampon line.
let do_put_char_format c =
  if c=' ' then  flags.last_space <- flags.x;
  if flags.x =(-1) then init_line (flags.x_start+flags.first_line)
  line.[flags.x]<-c;
A chaque fois on teste les trois cas suivants: La fonction init_line utilisée ici permet de positionner l'écriture au début de la ligne :
let init_line n =
  for i = 0 to n-1 do line.[i]<-' ' done;
  flags.x <- n;
  flags.last_space <- n-1
;;

Ensuite, une fois qu'une ligne est complète, elle est envoyée à do_put_line. Cette fonction va réaliser l'alignement, en testant align, et en rajoutant éventuellement des espaces devant la chaîne pour aligner la ligne. Il n'est pas nécessaire de rajouter des espaces à la fin de la ligne si celle ci ne remplit pas complètement l'espace entre x_start et x_end :
 let ligne = match flags.align with
  | Left -> s
  | Center ->
      let sp = (flags.hsize - (length -flags.x_start))/2 in
      String.concat "" [String.make sp ' '; s]
  | Right ->
      let sp = flags.hsize - length + flags.x_start in
      String.concat "" [ String.make sp ' '; s]
  in
  do_do_put ligne;
Il faut éventuellement rajouter le soulignage. Pour souligner en mode texte, il faut mettre les caractères qui vont souligner l'expression à la ligne en dessous. Ce qui nécessite évidemment de sauter une ligne. On crée alors une chaine de même taille que celle à souligner et on la positionne ensuite de la même manière.

2.2.2   Blocs

Lorsqu'on ouvre un environnement LATEX, ou une séquence d'instructions entourées d'accolades `{' et `}', on va ouvrir des blocs dans le module de sortie. En mode texte, on a deux types de blocs :
Les blocs temporaires
sont utilisés essentiellement pour analyser des arguments grâce à l'analyseur principal du module Scan. Le résultat se retrouve dans le module de sortie. Dans le cas du texte, il ne faut pas le formater, et de plus il faut le sauvegarder dans un tampon à part. Cet état est hérité par tous les blocs crées à l'intérieur d'un bloc temporaire.
Les autres blocs
correspondent soit à des changements de style ou à des cas particuliers comme les listes, les tableaux. Leur résultat sera formaté et envoyé dans le fichier de sortie aussitôt.
Pour gérer la sortie, on utilise, de même que dans les autres modules, le module Out, qui permet d'abstraire la nature exacte de la sortie. Cela peut être un fichier, un tampon ou le vide. Au début de la lecture du fichier LATEX, on initialise une sortie avec le fichier de sortie. On ne recrée ensuite une sortie qu'en cas de bloc temporaire. L'état de la sortie est empilé à chaque ouverture de bloc et dépilé à la fin.
let open_block bloc arg =
  push out_stack (bloc,arg,!cur_out);
  try_flush_par ();
  if !cur_out.temp || bloc="TEMP" then begin
    cur_out :=
      newstatus
        !cur_out.nostyle
        !cur_out.active
        [] true;
  end;
  try_open_block bloc arg;
;;
Ensuite, il faut gérer chaque bloc spécifiquement. On commence par empiler les variables de formatage vues plus haut, puis, selon le bloc, on empile des variables supplémentaires avant de les initialiser. Par exemple, pour l'environnement quote, on doit positionner l'alignement à gauche, et la ligne doit commencer plus tard, sans retrait de la première ligne:
 | "QUOTE" ->
      begin
        finit_ligne ();
        push align_stack flags.align;
        push in_align_stack flags.in_align;
        flags.in_align<-true;
        flags.align <- Left;
        flags.first_line<-0;
        flags.x_start<- flags.x_start + 20 * flags.hsize / 100;
        flags.hsize <- flags.x_end - flags.x_start+1;
      end
On notera l'appel à la fonction finit_ligne, qui permet de passer à la ligne suivante si une ligne est en cours, sans rajouter d'espace si on est déjà au début d'une ligne. Cela permet de faire la distinction entre les retours à la ligne `impératifs' donnés par l'envoi de `\n' et ces fins de ligne qui ne doivent pas s'enchaîner pour préserver l'aspect de la page.

Enfin, à la fin du bloc, il faut restorer l'état précédent en dépilant l'environnement. Puis, dans le cas d'un bloc temporaire, il faut recopier le bloc qui vient d'être terminé dans le bloc parent. Cette récursivité est nécessaire pour garder les sous-blocs d'un bloc temporaire, mais quand on arrive au bloc temporaire lui-même, il faut faire attention à ne pas recopier dans le fichier de sortie des informations qui doivent rester temporaires. En fait, ce comportement n'a pas lieu car toutes les fermetures de blocs temporaires sont précédées d'un appel à Out.to_string qui rend dans une chaîne le contenu du tampon, tout en le vidant. Il n'y a donc bien plus rien à recopier dans ce cas, et le fichier de sortie reste intact.
Les espacements entre les paragraphes sont réalisés par un appel en début de bloc à la fonction try_flush_par qui rajoute un espacement vertical si nécessaire. Cet espacement a été défini auparavant par un appel à par qui définit le nombre de lignes à sauter avant la prochaine sortie de caractères. En effet, l'espacement est effectué à chaque appel à put ou put_char.
En mode texte, il est très difficile de rendre la notion de modes. On ne peut pas changer de couleur ou passer en italique. Il est encore moins possible de changer de fonte. La gestion de modes implantée pour le module HTML est donc réduite au strict minimum pour garder la compatibilité des modules. La seule exception est l'ouverture d'un mode ``CODE'' qui a lieu lors d'une commande \verb. En effet, dans ce cas, on va afficher le texte entre guillemets pour le distinguer du reste du document. Etant donné que le module Scan reconnait cette commande en déclarant un changement de mode, il faut introduire cette notion de mode dans le module. Par contre, la notion de mode en attente a disparu, puisque les blocs ne sont plus mémorisés avant d'être écrits dans le fichier de sortie. Les autres types de modes ne sont pas reconnus.

2.3   Tableaux

Les tableaux ont un traitement complètement différent du reste du mode texte. En effet, il faut formater le texte à l'intérieur des cellules avant de formater le tableau entier dans la page ou l'environnement parent. Cependant, pour formater une cellule, il faut connaître sa largeur. Or, En LATEX, on peut distinguer deux types de colonnes : les colonnes de type p, b ou t ont un argument qui est la largeur de la colonne et ne posent donc pas de problème. Par contre, pour les colonnes de type l, c ou r, la largeur de la colonne est déterminée par la taille de la plus grande cellule, chacune d'elle étant formatée sur une seule ligne. Il est donc impossible d'afficher `au vol' le tableau, en une seule passe sur les données LATEX.

De plus, il faut pouvoir gérer les cas de tableaux imbriqués, qui nécessitent un empilage des données. Les multicolonnes impliquent également de faire un traitement particulier, car leur largeur dépend de la largeur de toutes les colonnes qu'elles recouvrent. Il faudra aussi traiter le cas des bordures. En mode texte, on peut se permettre, contrairement à l'HTML, de rendre les bordures de manière exactement semblable à LATEX. Pour cela, il faut traiter les deux types de bordures : définies par `|' ou par un argument `@'.

On va donc procéder en deux étapes. On va d'abord lire toutes les cellules du tableau. Le module Scan envoie au module Text des informations d'ouverture et de fermeture de tableau, de ligne puis de cellule. L'ouverture d'une cellule a comme argument son formatage, ce qui permet de préparer le formatage. Ensuite, il faut afficher les cellules du tableau, ligne par ligne, en les formatant selon l'indication de largeur calculée lors de la première étape.

2.3.1   Lecture des données

Le module Text reçoit ses données de Scan. On supposera donc que les données arrivent dans le bon ordre, et que par exemple on n'ouvre pas de cellule si une ligne ou une table n'est pas ouverte.

A l'ouverture de la table, on initialise les données, en sauvegardant l'état courant dans une pile, pour permettre les tableaux récursifs. On ouvre aussi un nouveau bloc dans le gestionnaire principal, ce qui a pour effet de sauvegarder les paramètres de formatage, que l'on va pouvoir modifier.

Le type global de la table doit contenir les informations suivantes :

type table_t = {
mutable lines : int; Nombre total de lignes du tableau
mutable cols : int; Nombre de colonnes
mutable width : int; Largeur totale, y compris les bordures
mutable taille : int Table.t; Table avec la taille de chaque colonne
mutable tailles : int array; Tableau des tailles de chaque colonne
mutable table : row_t Table.t; Table contenant les lignes
mutable line : int; Numéro de la ligne en cours
mutable col : int; Numéro de la colonne en cours
mutable in_cell : bool; Sera à true si on est dans une cellule
  };;
Les champs de cet enregistrement seront remplis au fur et à mesure du remplissage du tableau. Certains, comme width, ne pouront être calculés qu'à la fermeture de l'environnement.

Etant donné qu'on ne connait pas à l'avance le nombre de colonnes et le nombre de lignes du tableau, on ne peut pas utiliser directement un tableau de taille déterminée pour modéliser cet objet. J'ai donc utilisé le module Table, qui permet de définir des tableaux de taille variable. Table gère le redimensionnement des tableaux en cas de dépassement de capacité. Son utilisation est très simple:
val create : 'a -> 'a t
val emit : 'a t -> 'a -> unit
val trim : 'a t -> 'a array
On rajoute un élément à la fin de la table avec Table.emit. Puis, une fois la table terminée, on la convertit en tableau avec Table.trim.
Il faut ensuite ouvrir une ligne. Le type décrivant les lignes est :
type cell_set = Tabl of cell_t Table.t | Arr of cell_t array
type row_t = {
    mutable haut : int;
    mutable cells : cell_set;}
haut va être la hauteur de la cellule et cells est le tableau ou la table contenant les cellules de cette ligne. La création d'une nouvelle ligne va se traduire par une réinitialisation de la table des cellules, de la colonne courante et une mise à jour de la variable de la ligne courante.
La dernière série d'initialisations à faire se situe au niveau de l'ouverture d'une cellule. Cette fois, on va traiter le formatage en lui-même de la cellule puisqu'on va convertir les informations de format données par Scan, en remplissant les champs de la variable !cell :
type align = Top | Middle | Bottom | Base of int
and wrap_t = True | False | Fill
;;
type cell_t = {
mutable ver : align; Alignement vertical
mutable hor : align_t; Alignement horizontal
mutable h : int; Hauteur de la cellule
mutable w : int; Largeur de la cellule
mutable wrap : wrap_t; Indique si la cellule doit être formatée maintenant ou pas
mutable span : int; Nombre de colonnes sur lesquelles s'étend la cellule
mutable text : string; Texte de la cellule
mutable pre : string; Bordures à mettre juste avant la cellule
mutable post : string; Bordures à mettre juste après la cellule
mutable pre_inside : int list; Liste des positions dans pre des bordures de type `@'
mutable post_inside : int list; Liste des positions dans post des bordures de type `@'
  };;
On voit ici qu'il est prévu de gérer l'alignement n'importe où dans la cellule, avec un concept de ligne de base qui donne le positionnement du milieu du texte dans la case en pourcentage de la hauteur de la case.

On commence l'ouverture des cellules en ouvrant un bloc temporaire pour que la sortie des caractères de la cellule soit récupérée dans un tampon. On ouvre un deuxième bloc pour sauvegarder quelques données. Dans le cas où la cellule doit être formatée, on va faire appel au formatage décrit en 2.2.1, en utilisant la variable flags.temp que l'on va positionner à faux, ce qui va rediriger le flot de texte vers les procédures de formatage. On peut en effet formater la cellule tout de suite si on connait toutes ses caractéristiques : alignement, largeur. Il suffit alors de positionner les variables globales de formatage correctement. De plus, les lignes du texte à l'intérieur de la cellule n'ont pas besoin d'avoir une largeur constante pour l'instant car le texte va être recopié et sa taille ajustée lors de la deuxième étape.
Cette gestion de cellules prépare donc l'environnement. Les caractères à mettre dans les cases du tableau vont ensuite arriver par l'intermédiaire de put ou de put_char, et vont être recopiés dans un tampon du module Out. Il faut à la fin de la cellule recopier ce tampon dans la chaîne prévue à cet effet dans la variable !cell.

Cela va être fait lors de la fermeture de la cellule. C'est à ce moment là qu'il faut mettre à jour les informations générales concernant le tableau : on calcule la largeur de la cellule et sa hauteur, puis on compare la largeur de la cellule en cours à la plus grande largeur connue de sa colonne, pour calculer la largeur de cette colonne. Enfin, il faut sauver la cellule prête dans la ligne en cours, par un appel à Table.emit. A la fin d'une ligne, il ne reste qu'à rajouter cette ligne dans la table !table.table.

La largeur des colonnes est sauvegardée dans un tableau. Cependant, de même que pour les cellules, on ne connait pas à l'avance la taille de ce tableau. On recourt encore au module Table et on crée une table avec les informations contenues sur la première ligne. Ensuite, on convertit cette table en tableau pour accéder aux valeurs et comparer les tailles avec les autres lignes. Ce raisonnement ne fonctionne pas lorsqu'on a pas le nombre total de colonnes à la fin de la première ligne, ce qui arrive, d'une part si on a une ligne horizontale obtenue par `\hline', et d'autre part dans un environnement tabbing qui ne prédéfinit aucun nombre de colonnes. Dans ce cas, il est prévu de pouvoir redimensionner le tableau en cas de débordement.

On décrit ainsi tout le tableau. On obtient finalement, dans la variable !table.table, un tableau de tableaux contenant les lignes, puis les cellules du tableau. Toutes les informations de formatages des cellules sont sauvegardées, car elles vont servir lors du deuxième traitement des données vers le fichier texte.

2.3.2   Multicolonnes et Bordures

Multicolonnes

Une multicolonne est définie en LATEX par une commande \multicolumn qui prend en argument le nombre de colonnes sur lesquelles elle s'étend, un argument de formatage et le texte à mettre dans la cellule. Le module Scan reconnait ces colonnes et crée alors une cellule avec un `span' supérieur à un, qui donne le nombre de colonnes sautées par la cellule. On crée alors la cellule normalement, en rajoutant cette information de span.

Pourtant, on ne peut plus calculer une taille de colonne, puisque la taille de cette cellule doit être égale à la somme des tailles des colonnes sous-jacentes. A la fermeture de la cellule, on ne peut calculer qu'une taille minimum pour la somme par rapport au formatage du texte de la cellule. On voit alors qu'il va être nécessaire de revenir sur toutes les multicolonnes à la fin du tableau pour déterminer leur largeur exacte. A cette fin, on va sauvegarder dans une liste les colonnes de début, de fin et la largeur (minimale) de cette cellule.
 if !cell.span = 1 then begin
  (...)
 end else begin
   if !table.line=0 then
     for i = 1 to !cell.span do
      Table.emit !table.taille 0
     done;
   multi:=(!table.col,!table.col+ !cell.span-1,!cell.w):: !multi;
 end;
On voit aussi que si on est dans la première ligne, pour respecter le nombre de colonnes dans le tableau de tailles, on rajoute des zéros dans la table des tailles. Ceci permet d'avoir un nombre de cases correct tout en laissant la détermination exacte des largeurs colonnes à plus tard.

Ensuite, à la fermeture du tableau, il faut reprendre toutes les multicolonnes rencontrées pour ajuster les tailles. En effet, on a une information de taille minimale pour la somme des tailles des colonnes. Donc, soit cette somme est supérieure à la taille minimale, soit elle est inférieure. Dans ce dernier cas, il faut agrandir toutes les colonnes recouvertes par la multicolonne. Dans le cas où toutes les colonnes étaient de taille nulle, on agrandit uniquement la première. Sinon, on les agrandit toutes proportionnellement. En répétant cette opération sur toutes les multicolonnes, on arrive à une détermination possible des tailles des colonnes.

Bordures

Les bordures sont de deux types : standard ou inside. Elles sont transmises ainsi au module de destination par le module Scan, par l'intermédiaire de make_border et make_inside.

La première étape consiste à remplir correctement les champs pre et post de !cell. En effet, le champ couvert par une cellule est déterminé, d'après [6], par la cellule elle-même plus la ou les bordure(s) éventuelle(s) qui suivent immédiatement cette cellule. Un cas particulier reste pour la première colonne, puisqu'elle couvre la cellule et les bordures qui sont avant et après la cellule.

Ainsi, lorsque Scan envoie l'ordre de placer une bordure, si on est pas encore dans la première colonne, il faut remplir le champ pre. Sinon, c'est le champ post qui est concerné. Ces deux variables sont en fait des chaînes qui vont représenter la bordure elle-même. On rajoute le caractère correspondant à la bordure à la fin de la chaîne. Cela permet de gérer plusieurs bordures entre deux colonnes de manière simple.

Les bordures inside, définies en LATEX par un argument @ dans le format de tableau, ont le même traitement que les bordures standard. On ne fait que rajouter dans une liste la position des caractères de bordure de type `inside'. Ceci servira plus tard pour afficher la colonne car les bordures de ce type ne doivent être affichées qu'une seule fois en haut de la cellule, dans le cas ou la cellule a une hauteur supérieure à un caractère.

Ensuite, lors de la deuxième étape du tableau, il faut afficher ces bordures. Cela est réalisé en appelant, avant et après l'affichage du contenu de la cellule, la fonction put_border avec les arguments correspondant à la bordure précédant ou suivant la cellule. Celle-ci imprime la bordure sauf si c'est une bordure inside et que l'on est pas sur la première ligne.

Les bordures horizontales sont créees par la commande \hline et remplissent toujours toute la largeur du tableau. On représente donc ces bordures en rajoutant une ligne au tableau avec une seule cellule, contenant le caractère `-', de champ span égal au nombre de colonnes du tableau, et de champ wrap positionné à Fill. C'est ce champ qui va être testé ensuite pour déterminer s'il faut remplir la cellule avec le caractère qu'elle contient, ce qui fera bien une ligne horizontale de la longueur voulue.

2.3.3   Affichage

Après avoir préparé le tableau en mémoire, il faut le formater et l'envoyer dans le fichier de sortie. Cette opération se fait à la fermeture du tableau. On reprend les lignes du tableau une par une. Puis, on affiche ligne de caractère par ligne de caractère, et enfin cellule par cellule. On a donc trois boucles imbriquées qui vont décrire le tableau. Il faut tenir compte de l'alignement horizontal et vertical de chaque cellule. Grâce aux informations de hauteur et de largeur de la cellule, on peut positionner chacune d'elle dans la case appropriée.

Cela est réalisé grâce à deux fonctions qui gèrent l'alignement vertical pour l'une et horizontal pour l'autre. text_out donne en effet comme résultat vrai si la ligne de caractère doit être affichée, par rapport à l'alignement vertical de la cellule. Dans ce cas on passe les arguments nécessaires à la fonction put_ligne qui s'occupera de l'alignement horizontal et de l'affichage. Si on n'affiche pas de ligne, il faut quand même mettre un nombre suffisant d'espaces pour ne pas perdre l'alignement.

La différence entre le formatage réalisé ici et celui expliqué en 2.2.1 est que dans le cas d'un tableau, les cellules ont une largeur fixée et doivent être remplies. Il faut donc rajouter éventuellement des espaces à la fin du texte de la cellule pour terminer la colonne et préserver l'alignement.

2.4   Mathématiques

En mode texte, l'affichage de formules mathématiques est assez approximatif, puisqu'on ne dispose d'aucun symbole spécifique. Il faut donc trouver un moyen de rendre les formules de la manière la plus lisible possible. On aura deux modes de fonctionnement, selon que l'on est en mode display ou pas. La différence essentielle se situe au niveau des symboles et du placement des éléments.

Sans le mode display, le texte doit être formaté sur une ligne. La plupart des symboles seront alors écrits. Les indices et exposants seront notés suivant la même convention qu'en TeX.

Par exemple, ò23 xdx=5/2 sera écrit : int_2^3 xdx = 5/2.

Le mode display est complètement différent. La formule précédente sera formatée ainsi :
                                /3     5
                                |  xdx=-
                                /2     2
On voit que les symboles sont rendus par des assemblages de caractères, ce qui est satisfaisant pour la plupart des équations.

L'alignement va être effectué grâce aux tableaux implémentés en 2.3. on peut distinguer les groupements horizontaux et verticaux.

2.4.1   Les displays

Comme décrit dans l'interface en 1.3.3, les displays correspondent au groupement horizontal des formules.

A l'ouverture d'un environnement mathématique, on ouvre un display qui va englober toute la formule. D'autres displays seront ensuite ouverts à l'intérieur de l'équation pour rassembler certains passages.

Une ouverture de display va être traduite par l'ouverture d'un tableau de texte, que l'on va remplir au fur et à mesure. J'ai essayé de respecter au maximum l'interface de création des tableaux. Cela permet de rendre les deux parties du programme totalement indépendantes, et limite l'apparition d'erreurs si l'on modifie par la suite le code relatif aux tableaux. On ouvre donc le tableau, puis une ligne et une cellule. Le format de cette cellule sera centré horizontalement et verticalement.

Contrairement au module HTML, il n'y a pas besoin ici d'optimiser le nombre de tableaux effectivement ouverts. En effet, en HTML, on évite d'ouvrir des tableaux vides ou contenant une seule case s'ils sont englobés dans une case d'un autre tableau. Dans le cas du texte, ce n'est pas gênant, car ces imbrications resteront en mémoire et n'auront aucune incidence sur la sortie finale de la formule. De plus cela permet de simplifier le code gérant les displays.

2.4.2   Alignements verticaux

Pour les quatre fonctions de l'interface nécessitant des alignements verticaux, il faut créer de nouveaux tableaux. Les fonctions open_vdisplay, close_vdisplay s'occupent de cela. Ensuite, open_vdisplay_row et close_vdisplay_row mettent les lignes en place, en ouvrant dans chaque case un display horizontal, pour que l'analyse des formules se fasse toujours dans un environnement où un display est ouvert.

Indices et exposants

Le fonctionnement des trois types d'indices et d'exposants est très similaire. Je ne détaillerai donc que le type standard. On commence par changer de case pour permettre l'alignement vertical entre les cases. Il faut ensuite ouvrir un tableau vertical, puis trois lignes. La première va être formatée en haut à gauche, et va contenir l'exposant éventuel. La deuxième sera alignée au milieu et aura la base de l'expresison. La troisième, alignée en bas, contiendra l'indice.

Il ne faut pas oublier d'analyser les indices et exposants avant de les afficher, car ils peuvent contenir des commandes LATEX. Le display vertical est refermé à la fin.

Fractions

Le cas des fractions est un peu plus délicat, puisqu'il faut déterminer le numérateur et le dénominateur. J'ai appliqué la même méthode qu'en HTML. On va insérer un display vertical au début de la case en cours. Cela est fait en interceptant, dans la fonction insert_vdisplay, le display en cours en remplacant le tampon de sortie du tableau par un autre tampon.

On recrée alors un nouveau display vide, puis un display vertical à l'intérieur. On recopie dans ce dernier le contenu du tableau récupéré au début. La première ligne est alors fermée, car le numérateur est en place.

La deuxième ligne est en fait une bordure horizontale créee par la fonction make_hline. Enfin, on ouvre la troisième ligne. Cependant, il faut la fermer au bon moment, c'est à dire à la fermeture du display englobant la fraction.

Pour fermer la dernière ligne au bon moment, il faut intercepter le prochain appel de la fonction close_display. Cette fonction va fermer des blocs par l'intermédiaire des fonctions de fermeture de tableau, ce qui va dépiler les environnement de ces blocs. Donc, en placant sur la pile de ces environnements une information de type Freeze, on va indiquer à close_display qu'il faut éxécuter une fonction f plutôt que fermer le display :
type stack_item =
  Normal of string * string * status
| Freeze of (unit -> unit)
;;
Le type Normal de la pile out_stack correspond au nom du bloc, à ses arguments et au tampon de sortie. Freeze contient uniquement une fonction à éxécuter à la place de fermer un display. Evidemment, si l'on trouve un type Freeze alors qu'on ne devrait pas, on lève une exception PopFreeze, car dans ce cas, la commande \over est mal parenthésée et donc le fichier LATEX est incorrect. Il n'y a qu'une seule exception, pour la fonction item_display, qui doit fermer une cellule, et en ouvrir une autre. S'il y a un Freeze sur la pile, on l'enlève pour faire l'opération, puis on le remet sur la pile à la fin.

2.4.3   Symboles

Les symboles utilisés sont définis dans les fichiers symb-text.hva, symb-eng.hva et symb-fra.hva. Ces deux derniers fichiers permettenet de distinguer le mode anglais du mode francais, pour les noms des symboles.

Les symboles existant en ASCII sont définis par leur code. Pour les autres, on a le plus souvent un symbole avoisinant, ou alors une description en texte.

Les symboles utilisés en mode display et qui doivent être un peu plus grand, comme les intégrales ou les sommes, sont réalisés à l'intérieur d'une \mbox :
\newcommand{\@displayint}{\mbox{/\\|\\/}}
\newcommand{\@displaysum}{\mbox{--\\\char92\\/\\--}}
Ces deux commandes permettront de rendre ainsi les symboles :
                                   -- 
                              /    \  
                              |    /  
                              /    -- 
Ces approximations des symboles suffisent le plus souvent, mais ne peuvent pas être agrandies si la formule qui suit est trop haute.


Précédent Suivant Index