Comprendre la syntaxe du Markdown

Partie I : Les paragraphes

L’objectif de cet article est de découvrir la syntaxe du Markdown et de commencer à réfléchir à la manière dont nous pourrions transformer cette spécification en code.

Essayons de réduire son contenu à l’essentiel.

Note : C’est le premier article qui contient du code et des blocs de code. J’ai fait au plus simple pour l’instant, mais je prévois d’ajouter de la colorisation syntaxique et d’autres ajustements aux blocs de code.

Modification : 15 avril 2020 – Bon, je viens juste d’ajouter la colorisation syntaxique et la numérotation des lignes comme effet de bord de la migration du site vers Hugo. Youpi !

Types d’éléments

En première lecture, on remarque déjà que le Markdown supporte deux types d’éléments :

Si ces types d’éléments vous disent quelque chose, c’est parce que ce sont les mêmes que pour l’HTML.

Élements de type bloc

Paragraphes

D’après la spécification rédigée par John Gruber :

Un paragraphe se compose simplement d’une ou plusieurs lignes consécutives, séparées par une ou plusieurs lignes vides. (Une ligne vide est une ligne qui apparait vide à la lecture — une ligne qui ne comporte que des espaces ou des tabulations est considérée comme vide). Un paragraphe standard ne devrait pas être indenté avec des espaces ou des tabulations.

Et juste là, nous avons la définition de deux de ce qui deviendra nos symboles ou nos nœuds une fois qu’ils auront été identifiés par notre parseur :

Avec ces informations, et pour simplifier notre travail, nous pouvons déjà prendre deux décisions au sujet de notre parseur :

On peut écrire l’expression régulière qui représente une ligne vide comme suit :

/^[\t ]*$/;

Afin de s’assurer que notre expression régulière est correcte, on va utiliser une page HTML toute simple, avec le code ci-dessous. Si vous avez la flemme de le faire de votre côté (et un bon programmeur est un programmeur paresseux), je vous ai fait une page toute prête.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>Vérification de l’expression régulière</title>
  </head>
  <body>
    <script>
      /*
       * Ceci est le contenu de test que nous allons utiliser pour vérifier
       * que notre expression régulière détecte correctement les lignes.
       * Elle devrait nous retourner 4 lignes.
       */
      const content = `
            \t    \t    \t
            Un peu de contenu
    
            \t
            Du contenu en plus`;
    
      // C’est l’expression régulière que l’on veut tester
      const regex = /^[\t ]*$/;
    
      // On divise le contenu pour le lire ligne par ligne
      let lines = content.split('\n');
      document.write(`${lines.length} lignes trouvées!<br/><br/>`);
    
      // On itère sur les lignes et on détecte les lignes vides
      lines.forEach((line, index) => {
        document.write(`${index}: `);
        if (line.match(regex)) {
          document.write('Cette ligne est vide!<br/>');
        } else {
          document.write('Cette ligne contient du texte!<br/>');
        }
      });
    </script>
  </body>
</html>

Si tout se passe bien, on devrait avoir le résultat suivant :

6 lignes trouvées!

0 : Cette ligne est vide!
1 : Cette ligne est vide!
2 : Cette ligne contient du texte!
3 : Cette ligne est vide!
4 : Cette ligne est vide!
5 : Cette ligne contient du texte!

Génial ! On sait maintenant détecter les lignes vides, les limites de chaque paragraphe. Passons à notre tâche suivante : extraire les paragraphes.

Il suffit de mettre à jour le code javascript avec les lignes ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const blankLine = /^[\t ]*$/;

let lines = content.split('\n');
let paragraphFound = 0;

lines.forEach((line, index) => {
  if (!line.match(blankLine)) {
    paragraphFound++;
    document.write(`Paragraphe ${paragraphFound} : ${line}<br/>`);
  }
});

Exécutons le code ci-dessus (vous pouvez le voir en action sur cette page), et vérifions que tout fonctionne comme on veut. Notre code devrait afficher les lignes suivantes :

Paragraphe 1 : Un peu de contenu
Paragraphe 2 : Du contenu en plus

Parfait ! Mais ne nous emballons pas. Étendons notre contenu de test afin de s’assurer que la détection des paragraphes fonctionne correctement. Il suffit d’ajouter quelques lignes de contenu à notre variable content comme ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const content = `
  \t    \t    \t     
  Un peu de contenu
              
  \t
  Du contenu en plus

  Un paragraphe sur plusieurs
  lignes avec un peu de contenu

  Un paragraphe avec un  
  saut de ligne`;

À nouveau, si tout se passe bien lors de l’exécution du code, nous aurons réussi à extraire quatre paragraphes de notre contenu de test. Mais vous avez peut-être déjà remarqué un problème potentiel avec notre code. Et vous avez raison. Exécutons le code. Voici ce qu’on obtient :

Paragraphe 1 : Un peu de contenu
Paragraphe 2 : Du contenu en plus
Paragraphe 3 : Un paragraphe sur plusieurs
Paragraphe 4 : lignes avec un peu de contenu
Paragraphe 5 : Un paragraphe avec un  
Paragraphe 6 : saut de ligne

Comme indiqué plus tôt, d’après la spécification Markdown, les lignes vides séparent les paragraphes. Mais pour l’instant, on utilise le caractère de retour chariot (\n) comme séparateur. Ajustons ce comportement.

Il va tout d’abord falloir stocker les lignes du paragraphe en cours dans une variable (une zone tampon, aussi appelé buffer), et traiter les lignes du buffer une fois que l’on rencontre une ligne vide. Il nous faudra également implémenter la fonctionnalité de retour à la ligne forcé :

Quand vous souhaitez insérer une balise <br/> en Markdown, il suffit de terminer la ligne avec deux espaces ou plus, puis de faire un retour à la ligne.

Pour implémenter cette fonctionnalité, il suffit d’utiliser le code javascript suivant (ou afficher cette page) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let paragraphFound = 0;

// On initialise le buffer
let currentParagraph = [];

lines.forEach((line, index) => {
  // Si la ligne en cours n’est pas une ligne vide, on la pousse sur le buffer
  if (!line.match(blankLine)) {
    currentParagraph.push(line);
  }

  // Si la ligne en cours est une ligne vide, ou la dernière ligne du document
  if (
    (line.match(blankLine) || index === lines.length - 1) &&
    currentParagraph.length > 0
  ) {
    paragraphFound++;
    /*
     * On assemble les différentes lignes du paragraphes en utilisant un caractère
     * de saut de ligne et on remplace les doubles espaces suivi d’un saut de 
     * ligne avec un retour à la ligne forcé et enfin on remplace tous les saut
     * de ligne par un espace.
     *
     * Cela nous permet de faire un rendu conforme à la spécification.
     */
    document.write(
      `Paragraph ${paragraphFound}: ${currentParagraph
        .join('\n')
        .replace('  \n', '<br/>')
        .replace('\n', ' ')}<br/>`
    );
    currentParagraph = [];
    return;
  }
});

On obtient un résultat prometteur :

Paragraphe 1 : Un peu de contenu
Paragraphe 2 : Du contenu en plus
Paragraphe 3 : Un paragraphe sur plusieurs lignes avec un peu de contenu
Paragraphe 4 : Un paragraphe avec un saut de ligne

Cet article est plutôt long, mais si vous êtes arrivé jusqu’ici, voici votre récompense : nous sommes maintenant capable de faire le rendu des paragraphes !

Vous venez de créer votre tout premier, mais incomplet, parseur et moteur de rendu Markdown.

Pour faire le rendu de vos paragraphes, ajoutez le code suivant à votre page lien pour les paresseux :

1
2
3
4
let currentParagraph = [];

// Créez une référence vers le corps du document HTML
let body = document.querySelector('body');
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Créez un élément de type paragraphe
let paragraph = document.createElement('p');
paragraph.innerHTML = currentParagraph
  .join('\n')
  .replace('  \n', '<br/>')
  .replace('\n', ' ')
  // Tronquez la chaîne pour nettoyer la sortie
  .trim();

// Ajoutez votre paragraphe au corps du document.
body.appendChild(paragraph);

currentParagraph = [];
return;

Et on a fini ! Vous pouvez accéder à la page au dernier lien, télécharger le code (avec l’option “Enregistrer la page” dans le menu de votre navigateur) et vous aurez l’intégralité du code que nous avons écrit ensemble dans cet article.

Jouez avec, modifiez-le ou cassez-le. Puis montrez moi vos résultats sur Twitter.

Dans le prochain article de cette série, nous découvrirons comment gérer les titres.