Aller au contenu principal
divagations - Retour à l'accueil

Enrichir la syntaxe Markdown – Suite et fin ?

Guillaume Barbier

Temps de lecture : ~ 9 minutes

J'ai peu de temps pour avancer aujourd'hui mais je vais essayer d'avancer sur ce qu'il reste dans ma liste :

  • Coloration syntaxique pour le code
  • Notes de bas de page
  • Changement de langue au sein du document
  • Gestion automatique des liens externes
  • Emojis
  • Exposants et indices
  • Ratures
  • Table des matières
  • 🔜 Blocs d'information, remarque, etc…
  • 🔜 Remplacements automatiques de conforts (ajout d'espaces insécables, etc.)

Blocs d'information

L'idée est de pouvoir ajouter des blocs d'informations, de remarques ou d'alerte dans mes articles (je suis un grand adepte des digressions et autres notes plus ou moins directement liées au contenu principal.)

J'envisage aussi d'utiliser ce système pour une idée que j'ai pour ce blog : la possibilité d'insérer dans nos articles des réflexions voir des dialogues entre Myriam et moi.

J'ai déjà identifié un plugin markdown potentiel (markdown-it-admon(Opens in a new window)), mais plusieurs points me turlupinent :

  • comment restituer le sens de ces encarts aux technologies d'assistances ? Doivent-ils porter des en-têtes (et remonter dans la table des matières ?)
  • sémantiquement est-ce que ces blocs font "partie" du contenu principal ou sont ils des digressions inutiles à la compréhension de l'ensemble (ce qui me semble correspondre à la définition d'une balise <aside>(S'ouvre dans un nouvelle fenêtre))

Je n'ai pas réussis à trouver d'articles traitant spécifiquement de ce type de contenus, mais j'ai quand même glané les informations suivantes :

En me basant sur ces recherches, il me semble qu'il faudrait que je vise un code proche de ceci :

<div role="note" aria-labelledby="note1">
  <p id="note1">Note</p>
  <p>Contenu de la note…</p>
</div>
<div role="note" aria-labelledby="note2">
  <p id="note2">Note : avec un titre</p>
  <p>Contenu de cette deuxième note…</p>
</div>

Pour mon idée de dialogues insérés dans les articles, j'ai hésité à utiliser des <blockquote> mais le motif du rôle "note" me semble tout de même plus approprié.

Il me reste maintenant à vérifier si markdown-it-admon permet suffisamment de personnalisation pour obtenir un code approchant. Malheureusement le plugin ne semble pas proposer de customisation, tout au plus une option non documenté pour modifier (si j'ai bien compris) les marques utilisées en markdown pour déclencher la création des blocs.

Il me reste donc trois choix :

  1. reprendre le code du plugin pour créer mon propre plugin
  2. utiliser le plugin et créer un transformer pour corriger le code généré (approche la plus simple mais pas forcément la plus propre/économe)
  3. me rabattre sur la création d'un custom tag Liquid (Ces balises personnalisés ont l'air plus simples à coder mais sont moins rapide/agréables à utiliser).

J'ai décidé de tenter l'approche numéro 2, et de capitaliser sur les transformers que j'ai appris à écrire pour automatiser mes liens externes

Attention !

Avertissement : Titre et type de bloc

Si un titre est fourni, le type de bloc n'est pas affiché (juste précisé dans une classe). Si l'auteurice ajoute un titre à son bloc, il lui faudra préciser le type dans le titre.

La rédaction de ma fonction transformer est au final assez simple et consiste à :

  1. Récupérer la liste des éléments avec la classe admonition
  2. Sur chacun de ces éléments ajouter le rôle "note" ainsi que le couple aria-labelledby / id sur l'élément et son titre
  3. Pour garantir l'unicité des ID, je les base sur un compteur incrémenté à chaque itération sur les éléments trouvés.
export const admonitionA11ySupplement = (html) => {
  const { document } = parseHTML(html);
  const admonitions = [...document.querySelectorAll(".admonition")];
  let admonitionCounter = 0;

  if(admonitions.length > 0) {
    admonitions.map((admonition) => {
      admonitionCounter++
      let admonitionTitleId = "adm-" + admonitionCounter + "-note"
      let admonitionFirstChild = admonition.firstElementChild
      admonition.setAttribute("role", "note")
      admonition.setAttribute("aria-labelledby", admonitionTitleId)
      if(admonitionFirstChild?.className.includes("admonition-title")) {
        admonitionFirstChild.id = admonitionTitleId
      } else {
        console.warn(admonition.className + "isn't correctly formatted (missing title)")
      }
    })
  } else {
    return html;
  }
  return document.toString();
}

Astuce

Astuce : Les avantages d'une structure fixe

Les blocs admonitions générés par notre plugin ont toujours la même structure, le premier élément étant toujours le titre du bloc.

Grâce à cette structure fixe, je peux me permettre d'éviter une coûteuse boucle et me contenter de récupérer le premier élément du bloc. J'ajoute quand même une vérification et un message d'avertissement (dans le console) au cas où le bloc aurait été généré "à la main".

J'ajoute un un peu de CSS et voilà ! Mes blocs de remarques sont disponibles !

Remplacements automatiques de conforts

Lors de l'écriture de mes entrées de journal, j'ai remarqué de nombreuses saisies répétitives, invariables (je saisi à chaque fois la même chose), qui gagneraient à être automatisées :

  • Les espaces insécables avant certains signes de ponctuations : points d’exclamations, d'interrogation, deux points ou, plus rares, les points virgules[1]
  • les termes récurrents en langue étrangère. J'utilise beaucoup de noms anglais dans mon journal (nom de technologies, de méthodes, concepts, etc.). c'est devenu plus simple d'ajouter la langue anglaise sur ces contenus, mais comptes-tenus de la récurrence de certains, il serait agréable de pouvoir se créer un dictionnaire de termes récurrents, à passer en anglais.

Je manque de temps aujourd'hui, je me contenterais donc d'implémenter l'automatisme pour l'espaces insécable devant les signes de ponctuations.

La modification est a priori simple, mais j'ai quand même plusieurs difficultés à contourner :

  1. il faut que mes modifications se cantonnent aux contenus textes de mes éléments HTML. Je ne veux pas de cette modification dans mes attributs <aria-label> ou la description de la page.
  2. La propriété qui permet de ne lire que le contenu texte d'un élément HTML (textContent[2]) :
    - Reprend tout le contenu, mis à plat (sans HTML) de l'élément
    - Si on l'utilise pour définir du contenu, elle efface TOUT le HTML du contenu
    - Bref, la propriété dont j'espérais me servir est inutilisable.

Donc, pour limiter la casse, je repars sur la même base que pour mes transformers précédents et je limite la recherche aux éléments susceptibles de contenir directement du texte (j'ignore donc les <div>, <ul> et autres balises servant a poser la structure de la page…).

Ensuite, je me résous à parcourir un à un (enfin, c'est mon code qui va le faire, l'objectif c'est quand même de m'épargner ce taf 😉) les enfants des éléments sélectionnés, à la recherche de nœuds "texte", c'est à dire le texte lui-même de l'élément. En faisant cela, je m'assure de ne toucher que à du contenu "sans HTML" (en gros le contenu entre les balises HTML) pour y appliquer ma modification en toute sécurité.

Il ne me reste plus qu'à chercher et remplacer le schéma "espace + ponctuation (:;?!)" par la même ponctuation mais précédée par une espace insécable. Sauf que je ne peux pas mettre mon fidèle &nbsp;, parce que si je fais ça, le code qui va ressortir sera : &amp;nbsp; et s'affichera ainsi, dans toutes mes pages et très très visiblement : &nbsp;

Heureusement, la solution n'est pas aussi complexe que je le craignais[3] : Les contenu de type texte accepte la version UTF-8 de l'espace insécable (\u00A0) et génère en sortie l'équivalent décimal de mon &nbsp; : &#160;. Pas aussi "sexy" mais tout aussi efficace.

export const autoNbsp = (html) => {
  const { document } = parseHTML(html);
  const allTexts = [...document.querySelectorAll("h1, h2, h3, h4, h5, h6, p, span, li, a, strong, b, em, i")];
  const lookFor = / ([:?!;])/

  if(allTexts.length > 0) {
    allTexts.map((text) => {
      text.normalize()
      text.childNodes.forEach((child) => {
        if (child.nodeType == 3) { // "3" means this is a TEXT_NODE
          child.nodeValue = child.nodeValue.replace(lookFor, "\u00A0$1")
        }
      })
    })
  } else {
    return html;
  }
  return document.toString();
}

Astuce

Astuce : "Normaliser" les éléments pour optimiser la boucle sur les textes

Par défaut, un document HTML contient beaucoup de texte inutile (retours à la ligne, indentation, …) liés à la façon dont on l'écris. Ces textes "vides" seront parcourus par ma boucle même s'ils ne contiennent rien.

Pour gagner quelques millisecondes sur la génération de mon site, j'applique donc la méthode normalize(). Cette méthode supprime tous ces textes "vides"de l'élément, réduisant ainsi la liste d'éléments parcourus.

Ce que je n'ai pas intégré : Les abréviations

Il y a un grand classique des éditeurs markdown que je n'ai pas implémenté : c'est la gestion des abréviations.

L'implémentation classique est bien disponible (voir markdown-it-abbr(S'ouvre dans un nouvelle fenêtre)) mais comme pour le reste du web, elle se base sur le schéma balise <abbr> + attribut title.

Et je l'ai déjà dis, l'attribut title (ou plutôt son support) est nul 💩.

Ce qui me dérange le plus, c'est qu'il ne soit absolument pas pris en charge par les mobiles, ce qui est un peu embêtant sachant qu'aujourd'hui la majorité du traffic passe par eux (moi-même, je fais essentiellement ma veille sur mobile).

Je souhaite donc prendre le temps d'implémenter mon propre schéma d'abréviations, en me basant sur les réflexions de François-Xavier Lair dans Title, ce faux ami de l’accessibilité(S'ouvre dans un nouvelle fenêtre)


  1. Tous ces signes ont pour point commun de nécessiter un espace avec le mot qui les précèdent, ce qui les menace d'être fait orphelins (signe seul en début de ligne, suite à un retour à la ligne malheureux). Dans le web où nos interfaces sont fluides, ce genre de retour à la lige non maîtrisés sont fréquents et nous le résolvons en général en remplaçant l'espace précédent par un espaces insécables (&nbsp;). Cette solution fonctionne bien mais est pénible à la longue (surtout quand on commence beaucoup de liste ou qu'on aime s'exclamer !) ↩︎

  2. Je met de côté innerText qui est un héritage IE au comportement chelou et qui a de toute façon les mêmes problèmes que textContent ↩︎

  3. Ma crainte était que j'ai à découper chaque contenu texte en plusieurs nœuds pour ensuite insérer au milieu un contenu HTML (mon espace insécable) ↩︎