générer de l'HTML à partir de C++
(niveau : facile)


Table des matières

Introduction
CSS (Cascading Style Sheets)
Le programme
Fonction ParseLine
Limitations
Annexe : code de CHTMLBuilder


Introduction

Lorsque l'on écrit des articles comme celui-ci, ou que l'on fait un site web traitant de programmation, on est amené à inclure dans les pages html des extraits de code source, voire des fichiers entiers, comme c'est le cas en annexe. Ces sources sont évidemment plus lisibles si on utilise une coloration syntaxique, c'est à dire si les mots-clés du langage apparaissent dans une couleur, les chaînes de caractères dans une autre, et ainsi de suite pour les nombres, les opérateurs, les commentaires.

Le but du programme fourni (exe+sources) (167 Ko) est de générer automatiquement, à partir d'un fichier .cpp ou .h donné, la page HTML qui affichera le code de ce fichier en respectant la coloration syntaxique du C++. Le source est fourni au cas où vous auriez besoin d'y faire des modifications : il s'agit d'un projet Visual C++ 6 pouvant être facilement porté sur d'autres compilateurs, puisque son principal travail consiste simplement à analyser les lignes d'un fichier texte.


CSS (Cascading Style Sheets)

Le code HTML généré ne change pas la couleur courante avec des balises <font color="#......">, mais à l'aide de <span class=keyword> (par exemple). Cette instruction signale au programme qui affiche la page qu'il doit utiliser les styles correspondant à la classe spécifiée (ici : "keyword"). Les classes de styles (qui n'ont rien à voir avec des classes C++) sont décrites dans un fichier dont l'extension est .css, qui doit être inclus entre les balises <head> et </head> par une commande telle que :
<link rel="stylesheet" type="text/css" href="mystyles.css">.

L'utilisation des styles présente l'avantage suivant : si vous décidez un jour de modifier la couleur des commentaires (par exemple) qui apparaissent dans les sources C++ affichés dans vos pages html, il vous suffit de changer dans le fichier .css la classe qui définit les styles des commentaires, et c'est tout. De plus, ceci ne concerne pas uniquement la couleur : vous pouvez choisir d'afficher les opérateurs en caractères gras, ou pourquoi pas les chaînes de caractères en italique, sans toucher à vos fichiers html.

Résumons : l'exe fourni génère des pages html, qui se réfèrent à un fichier de styles mystyles.css (mais vous pouvez changer le nom) pour colorer le code C++ qu'elles contiennent. Ce fichier doit se trouver dans le même répertoire que les pages html (ou alors il faut préciser son chemin dans la commande <link>). Il est généralement commun à toutes les pages (ce n'est pas une obligation), ce qui permet de redéfinir un style pour un site entier en ne touchant qu'à un seul fichier. Enfin, en ce qui concerne la structure de ce fichier, elle est très simple : à chaque style correspondent quelques lignes de texte, par exemple pour ma classe "keyword" (mot-clé C++) :

.keyword
{
 color: #0000ff;
 font-family: courier;
 font-size: 10pt;
}
Deux exemples complets sont inclus dans le fichier zip ci-dessus.


Le programme

Le programme associé à cet article est une simple boîte de dialogue, avec un unique bouton destiné à choisir le fichier source. Le fichier généré porte le même nom suivi de l'extension .htm, ce qui donne par exemple MonSource.cpp.htm ou MonSource.h.htm. Le projet C++ utilise quelques classes de Fairy que je ne vais pas détailler ici, la partie qui nous intéresse est gérée par la classe CHTMLBuilder, dont le code est en annexe.

Cette classe possède 3 fonctions principales (et publiques), qui sont appelées ainsi dans CParseCppDlg::OnSelectfile :

  CHTMLBuilder HTML(MemFile);
  HTML.WriteHeader(dlgLoad.GetPathName()+".htm");
  HTML.Parse();
  HTML.WriteFooter();

WriteHeader et WriteFooter s'occupent d'écrire l'en-tête et la fin de la page html, c'est à dire ce qui encadre le code C++ lui-même, Parse lit le source et appelle pour chaque ligne la fonction ParseLine. Celle-ci utilise quelques méthodes pour l'aider à faire son travail :

- IsDigit détermine si un caractère est un chiffre
- IsKeyword compare le mot qui va être ajouté au fichier .htm avec les mots-clés du langage, qui sont contenus dans un tableau statique. Cette recherche n'est pas optimisée, le tableau est parcouru en entier ou jusqu'à ce que le mot soit reconnu
- IsNumber vérifie si le mot courant est un nombre. Pour la coloration syntaxique de Visual C++, un nombre est une suite de caractères qui commence par un chiffre, ou un point (exemple : .5f). 987aaz est donc un nombre, ainsi que 0xAB9F (avec cette règle l'hexidécimal est donc pris en compte)
- PutWord ajoute le mot courant accumulé dans m_szWord à la page html, en vérifiant s'il s'agit d'un mot-clé ou d'un nombre. Nous allons voir que c'est ParseLine qui se charge de découper la ligne en une suite de mots, séparés par des espaces, des tabulations, ou des opérateurs.


Fonction ParseLine

Pour analyser du code, par exemple celui d'un langage de script, une approche très courante consiste à construire un graphe qui définit les règles (= la grammaire) de ce langage. Cela permet notamment d'établir la précédence des opérateurs, la définition de ce que le langage considère comme étant un nombre ou une variable, etc ; pour en savoir plus, cherchez le mot "compilateur" sur internet, et vous devriez trouver des choses intéressantes et parfois très pointues. Le but de mon programme n'étant pas de "comprendre" ce qui a été codé, mais de le colorer, il n'est nul besoin de s'embarquer dans des choses aussi compliquées, et le fonctionnement de ParseLine reste simple.

La fonction commence par initialiser quelques variables pour la nouvelle ligne, puis entre dans une boucle effectuée jusqu'à ce qu'il n'y ait plus de caractère à traiter. Les tests de cette boucle sont les suivants :

- le caractère lu est-il un espace ou une tabulation ? Si oui le mot courant est terminé.
- y a-t-il un bloc de commentaire ouvert (par /*) ? Si c'est le cas le caractère est envoyé dans le fichier html, on ne recherche pas les mots-clés, chaînes, nombres, etc dans les commentaires. Si on détecte un */ le bloc se termine. Un bloc peut s'étendre sur plusieurs lignes, ceci est pris en compte par la variable-membre m_boCommBlock.
- y a-t-il une chaîne de caractères ouverte (par ") ? Pour fermer une telle chaîne il faut rencontrer un autre caractère ", non précédé d'un anti-slash car \" sert à afficher le caractère " lui-même.
- même chose pour une chaîne délimitée par des simples quotes : '.
- le début d'un commentaire simple (//) est vérifié. Un tel commentaire s'étend jusqu'à la fin de la ligne, il est inutile de tester les autres caractères, ils sont immédiatement écrits dans la page html.
- puisque l'on n'est ni dans un commentaire ni dans une chaîne, on regarde s'il n'y en a pas un ou une qui commence.
- dernier test : le caractère lu fait-il partie d'un opérateur ? Il est pour cela comparé au contenu d'un tableau statique. Cas particulier : le point, qui lorsqu'il est présent en début de mot peut être soit un opérateur, soit le commencement d'un nombre à virgule (dans ce cas il est suivi d'un chiffre, c'est ainsi que la distinction est faite).
- aucun des tests précédents n'a voulu du caractère : il est ajouté au mot courant, qui peut être un mot-clé, un nombre, ou autre chose (nom de variable, de classe, de fonction...), c'est PutWord qui le déterminera lorsque la fin de ce mot aura été atteinte.


Limitations

Ce programme n'est nullement exhaustif, quelques cas ne sont pas traités :

- le \ en fin de ligne (macros)
- dans Visual, un simple quote unique (c'est à dire une chaîne non fermée) sur une ligne fait que le reste de cette ligne est de la couleur des mots non standards (variables etc...). Je ne vois pas de raison à cela, et les simples quotes sont traités comme les doubles.
- dans Visual, 0...1.2 est un nombre, dans mon programme ce ne sera pas le cas (mais qui pourrait taper des choses pareilles en C++ ?...)
- attention si votre code source contient des balises html, dans des chaînes de caractères ou des commentaires : elles vont être écrites sans modification dans la page html, puis interprétées par le logiciel qui les affiche ! Tout ceci est normal, mais conduit à des résultats désagréables, lorsqu'un <html> est par exemple rencontré au milieu de la page. Ce cas devrait être rare, mais se produit pour HTMLBuilder.cpp, qui contient des lignes comme :
  m_OutputFile.PutString("<html>\n");
La solution consiste à remplacer les caractères < par un & suivi de lt; (less than). L'effet inverse existe également : le browser remplace ce qu'il croit reconnaître comme des caractères spéciaux html par leur équivalent ascii, ce qui n'est pas forcément désiré (mais une fois de plus, très rare dans du code C++).


Annexe : code de CHTMLBuilder

HTMLBuilder.h
HTMLBuilder.cpp

HTMLBuilder.h
// HTMLBuilder.h: interface for the CHTMLBuilder class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_HTMLBUILDER_H__CB40BC70_31B4_11D6_9CD7_444553540000__INCLUDED_)
#define AFX_HTMLBUILDER_H__CB40BC70_31B4_11D6_9CD7_444553540000__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "global/typedefs.h"

class CHTMLBuilder  
{
  public:
                    CHTMLBuilder        (Mythos::MemFile& MemFile);
    virtual        ~CHTMLBuilder        (void);

    void            WriteHeader         (const char* pszFile);
    void            WriteFooter         (void);
    void            Parse               (void);

  protected:

    void            ParseLine           (char* pszLine);
    bool            IsDigit             (const char cChar);
    bool            IsKeyword           (void);
    bool            IsNumber            (void);
    void            PutWord             (void);

  protected:

    Mythos::MemFile&m_MemFile;
    Mythos::File    m_OutputFile;

    bool            m_boCommBlock;
    char            m_szWord[1024];                         // current accumulated word
    DWORD           m_dwWordLen;                            // nb accumulated chars
};

#endif // !defined(AFX_HTMLBUILDER_H__CB40BC70_31B4_11D6_9CD7_444553540000__INCLUDED_)

HTMLBuilder.cpp
// HTMLBuilder.cpp: implementation of the CHTMLBuilder class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "HTMLBuilder.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//

static char szOperator[] = "!%&()*+,-./:;<=>?[]^{|}~\0";

static char szKeywords[][32] =
{
  "for","if","else","continue","do","while","goto",
  "switch","case","break","default","return",
  "new","delete","inline",

  "bool","char","double","float","int","long","short","void",
  "false","true",
  "const","unsigned","signed","volatile","mutable",
  "auto","extern","static","register",

  "#include","#if","#ifdef","#ifndef","#else","#elif","#endif","#define","#undef",
  "#pragma","once","defined",

  "struct","union","enum","typedef","sizeof",
  "this","explicit","operator","private","public","protected","friend","class","virtual",
  "template","using","namespace","typename","typeid","uuid","__uuidof","interface",
  "const_cast","static_cast","dynamic_cast","reinterpret_cast",

  "__asm","__based","__cdecl","__declspec","__fastcall","__inline","__stdcall","naked",
  "__single_inheritance","__multiple_inheritance","__virtual_inheritance",
  "__int8","__int16","__int32","__int64",
  "dllexport","dllimport",
  "thread","throw","try","catch",
  "__try","__leave","__finally","__except",
  NULL
};

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CHTMLBuilder::CHTMLBuilder(Mythos::MemFile& MemFile) : m_MemFile(MemFile)
{
}

CHTMLBuilder::~CHTMLBuilder()
{
}

//

void CHTMLBuilder::WriteHeader(const char* pszFile)
{
  if(!m_OutputFile.Open(pszFile,Mythos::IFile::_WRITE_TEXT_)) return;

  m_OutputFile.PutString("<html>\n");
  m_OutputFile.PutString("<head>\n");
  m_OutputFile.PutString("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">\n");
  m_OutputFile.PutString("<link rel=\"stylesheet\" type=\"text/css\" href=\"mystyles.css\">\n");
  m_OutputFile.PutString("</head>\n");
  m_OutputFile.PutString("\n");
  m_OutputFile.PutString("<body>\n");
  m_OutputFile.PutString("<table border=0 cellspacing=0 cellpadding=0 width=\"100%\">\n");
  m_OutputFile.PutString("<tr><td bgcolor=\"#ffffff\">\n");
  m_OutputFile.PutString("<span class=source>\n");
  m_OutputFile.PutString("<pre>\n");
}

//

void CHTMLBuilder::WriteFooter()
{
  m_OutputFile.PutString("</pre>\n");
  m_OutputFile.PutString("</span>\n");
  m_OutputFile.PutString("</td></tr></table>\n");
  m_OutputFile.PutString("\n");
  m_OutputFile.PutString("</body>\n");
  m_OutputFile.PutString("</html>\n");

  m_OutputFile.Close();
}

//

void CHTMLBuilder::Parse()
{
  char szLine[1024];
  m_boCommBlock = false;

  while(true)
  {
    if(!m_MemFile.GetString(szLine,1024)) break;
    ParseLine(szLine);
    m_OutputFile.PutChar('\n');
  }
}

//

void CHTMLBuilder::ParseLine(char* pszLine)
{
  char* pszChar  = pszLine;
  bool  boString = false;
  bool  boQuote  = false;
  DWORD dwAnti   = 0;                                       // consecutive "\"
  char  cChar    = 0;
  char  cPrev;

  m_dwWordLen    = 0;

  while(*pszChar)
  {
    cPrev = cChar;
    cChar = *pszChar++;
    if(cPrev == '\\') dwAnti++;
    else              dwAnti = 0;
    
    if(cChar == ' ')
    {                                                       // space
      PutWord();
      m_OutputFile.PutChar(' ');
      continue;
    }
    if(cChar == 9)
    {                                                       // tab
      PutWord();
      m_OutputFile.PutChar(cChar);                          // tab length can be modified here, eg: m_OutputFile.PutString("  ");
      continue;
    }

    // comment block

    if(m_boCommBlock)
    {                                                       // can only end with "*/"
      if((cChar != '*') || (*pszChar != '/'))
      {
        m_OutputFile.PutChar(cChar);
        continue;
      }
      m_OutputFile.PutString("*/</span>");
      pszChar++;
      m_boCommBlock = false;
      continue;
    }

    // string block

    if(boString)
    {                                                       // can only end with '"'
      if((cChar != '"') || (dwAnti & 1))
      {                                                     // not ", or previous char is '\'
        m_OutputFile.PutChar(cChar);
        continue;
      }

      m_OutputFile.PutString("\"</span>");
      boString = false;
      continue;
    }

    // quote block

    if(boQuote)
    {                                                       // can only end with ' (and should, if we have started a block)
      if((cChar != '\'') || (dwAnti & 1))
      {
        m_OutputFile.PutChar(cChar);
        continue;
      }

      m_OutputFile.PutString("'</span>");
      boQuote = false;
      continue;
    }

    // comment starts

    if(cChar == '/')
    {
      if(*pszChar == '/')
      {                                                     // simple comment (//)
        PutWord();
        m_OutputFile.PutString("<span class=comment>//");
        m_OutputFile.PutString(pszChar+1);
        m_OutputFile.PutString("</span>");
        return;                                             // goes till the end of the line
      }

      if(*pszChar == '*')
      {                                                     // comment block starts (/*)
        PutWord();
        m_OutputFile.PutString("<span class=commblock>/*");
        m_boCommBlock = true;
        pszChar++;
        continue;
      }
    }

    // string starts

    if(cChar == '"')
    {
      PutWord();
      m_OutputFile.PutString("<span class=string>\"");
      boString = true;
      continue;
    }

    // quote starts

    if(cChar == '\'')
    {
      PutWord();
      m_OutputFile.PutString("<span class=string>'");
      boQuote = true;
      continue;
    }

    // operator

    if(strchr(szOperator,cChar))
    {
      if((cChar != '.') || !IsDigit(*pszChar))
      {                                                     // '.' can start a number
        PutWord();
        m_OutputFile.PutString("<span class=operator>");
        m_OutputFile.PutChar(cChar);
        while(*pszChar && strchr(szOperator,*pszChar))
        {
          // special case : '.' can start a number
          if((*pszChar == '.') && IsDigit(*(pszChar+1))) break;

          cPrev = cChar;
          cChar = *pszChar++;
          m_OutputFile.PutChar(cChar);
        }
        m_OutputFile.PutString("</span>");
        continue;
      }
    }

    //

    m_szWord[m_dwWordLen++] = cChar;
  }

  PutWord();
}

//

bool CHTMLBuilder::IsDigit(const char cChar)
{
  return((cChar >= '0') && (cChar <= '9'));
}

//

bool CHTMLBuilder::IsKeyword()
{
  for(DWORD dwI = 0; szKeywords[dwI][0] != 0; dwI++)
  {
    if(!strcmp(szKeywords[dwI],m_szWord))
    {
      return true;
    }
  }
  return false;
}

//

bool CHTMLBuilder::IsNumber()
{
  return(IsDigit(m_szWord[0]) || (m_szWord[0] == '.'));
}

//

void CHTMLBuilder::PutWord()
{
  if(!m_dwWordLen) return;
  m_szWord[m_dwWordLen] = 0;

  // keyword

  if(IsKeyword())
  {
    m_OutputFile.PutString("<span class=keyword>");
    m_OutputFile.PutString(m_szWord);
    m_OutputFile.PutString("</span>");
  }

  // number

  else if(IsNumber())
  {
    m_OutputFile.PutString("<span class=number>");
    m_OutputFile.PutString(m_szWord);
    m_OutputFile.PutString("</span>");
  }

  // text

  else
  {
    m_OutputFile.PutString(m_szWord);
  }

  //

  m_dwWordLen = 0;
}

haut de la page