import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { analyserInline, analyserMarkdown, aplatirMarkdown, basculerCase, urlSure, } from '../js/markdown.js'; // Ce module ne touche pas au DOM : c'est ce qui le rend testable ici, le rendu // proprement dit vivant dans `notes.js`. // --- blocs ------------------------------------------------------------------ describe('analyserMarkdown, blocs', () => { test('reconnaît un titre et son niveau', () => { const [bloc] = analyserMarkdown('### Point hebdo'); assert.equal(bloc.type, 'titre'); assert.equal(bloc.niveau, 3); assert.deepEqual(bloc.contenu, [{ type: 'texte', valeur: 'Point hebdo' }]); }); test('un dièse sans espace n est pas un titre', () => { const [bloc] = analyserMarkdown('#12 du bon de commande'); assert.equal(bloc.type, 'paragraphe'); }); test('garde les retours à la ligne d un paragraphe', () => { // Markdown standard recolle ces deux lignes ; on ne le suit pas ici, voir // le commentaire d'`analyserInlineMultiligne`. const [bloc] = analyserMarkdown('première ligne\nseconde ligne'); assert.equal(bloc.contenu.filter((noeud) => noeud.type === 'saut').length, 1); }); test('sépare deux paragraphes par une ligne vide', () => { const blocs = analyserMarkdown('un\n\ndeux'); assert.equal(blocs.length, 2); assert.equal(blocs[0].type, 'paragraphe'); assert.equal(blocs[1].type, 'paragraphe'); }); test('distingue le séparateur d une liste à tirets', () => { assert.equal(analyserMarkdown('---')[0].type, 'separateur'); assert.equal(analyserMarkdown('- ceci')[0].type, 'liste'); }); test('un bloc de code prend son contenu à la lettre', () => { const [bloc] = analyserMarkdown('```js\nconst a = **b**;\n```'); assert.equal(bloc.type, 'code'); assert.equal(bloc.langage, 'js'); assert.deepEqual(bloc.lignes, ['const a = **b**;']); }); test('un bloc de code non refermé court jusqu à la fin', () => { // On est peut-être en train de l'écrire : avaler la suite est moins // déroutant que d'afficher la clôture ouvrante comme un paragraphe. const [bloc] = analyserMarkdown('```\nen cours'); assert.equal(bloc.type, 'code'); assert.deepEqual(bloc.lignes, ['en cours']); }); test('une citation est réanalysée, listes comprises', () => { const [bloc] = analyserMarkdown('> Le client dit :\n> - un\n> - deux'); assert.equal(bloc.type, 'citation'); assert.equal(bloc.blocs[0].type, 'paragraphe'); assert.equal(bloc.blocs[1].type, 'liste'); assert.equal(bloc.blocs[1].elements.length, 2); }); }); // --- listes ----------------------------------------------------------------- describe('analyserMarkdown, listes', () => { test('sépare une liste à puces d une liste numérotée', () => { const blocs = analyserMarkdown('- a\n1. b'); assert.equal(blocs.length, 2); assert.equal(blocs[0].ordonnee, false); assert.equal(blocs[1].ordonnee, true); }); test('détache les cases à cocher', () => { const [bloc] = analyserMarkdown('- [ ] à faire\n- [x] fait\n- ni l un ni l autre'); assert.deepEqual( bloc.elements.map((element) => element.coche), [false, true, null] ); assert.deepEqual(bloc.elements[0].contenu, [{ type: 'texte', valeur: 'à faire' }]); }); test('imbrique une sous-liste à l indentation', () => { const [bloc] = analyserMarkdown('- parent\n - enfant'); assert.equal(bloc.elements.length, 1); assert.equal(bloc.elements[0].blocs[0].type, 'liste'); assert.equal(bloc.elements[0].blocs[0].elements[0].contenu[0].valeur, 'enfant'); }); test('chaque élément porte le numéro de sa ligne source', () => { // C'est ce numéro qui permet à `basculerCase` de réécrire la bonne ligne, // y compris dans une sous-liste. const [, bloc] = analyserMarkdown('intro\n\n- [ ] premier\n- [x] second\n - [ ] fils'); assert.equal(bloc.elements[0].ligne, 2); assert.equal(bloc.elements[1].ligne, 3); assert.equal(bloc.elements[1].blocs[0].elements[0].ligne, 4); }); }); // --- contenu d une ligne ---------------------------------------------------- describe('analyserInline', () => { test('reconnaît le gras, l italique et le code', () => { const noeuds = analyserInline('**a** *b* `c`'); assert.deepEqual( noeuds.filter((noeud) => noeud.type !== 'texte').map((noeud) => noeud.type), ['gras', 'italique', 'code'] ); }); test('le code littéral l emporte sur l emphase', () => { const [noeud] = analyserInline('`**pas du gras**`'); assert.equal(noeud.type, 'code'); assert.equal(noeud.valeur, '**pas du gras**'); }); test('laisse intact un identifiant à tirets bas', () => { // Sans la garde d'`ouvreEmphase`, le milieu passerait en italique. const noeuds = analyserInline('date_debut_reelle'); assert.deepEqual(noeuds, [{ type: 'texte', valeur: 'date_debut_reelle' }]); }); test('laisse intact un astérisque isolé', () => { assert.deepEqual(analyserInline('3 * 4 = 12'), [{ type: 'texte', valeur: '3 * 4 = 12' }]); }); test('une marque non refermée reste du texte', () => { // Une note à moitié écrite est l'état normal d'une note. assert.deepEqual(analyserInline('**en cours'), [{ type: 'texte', valeur: '**en cours' }]); }); test('la contre-oblique neutralise la marque suivante', () => { assert.deepEqual(analyserInline('\\*littéral\\*'), [{ type: 'texte', valeur: '*littéral*' }]); }); test('lit un lien et son libellé', () => { const [noeud] = analyserInline('[le CR](https://ex.fr/cr)'); assert.equal(noeud.type, 'lien'); assert.equal(noeud.url, 'https://ex.fr/cr'); assert.deepEqual(noeud.contenu, [{ type: 'texte', valeur: 'le CR' }]); }); test('des crochets seuls ne font pas un lien', () => { assert.deepEqual(analyserInline('[à voir]'), [{ type: 'texte', valeur: '[à voir]' }]); }); test('un lien au schéma refusé retombe sur son libellé', () => { // Le texte survit, le lien non : effacer la ligne punirait le lecteur d'une // note qu'il n'a pas écrite. const [noeud] = analyserInline('[clic](javascript:alert(1))'); assert.equal(noeud.type, 'texte'); assert.equal(noeud.valeur, 'clic'); }); }); describe('urlSure', () => { test('accepte http, https et mailto', () => { assert.equal(urlSure('https://ex.fr'), 'https://ex.fr'); assert.equal(urlSure('http://ex.fr'), 'http://ex.fr'); assert.equal(urlSure('mailto:a@ex.fr'), 'mailto:a@ex.fr'); }); test('refuse les schémas exécutables, casse et espaces compris', () => { // Liste blanche et non liste noire : un planning s'échange, et rien ne // garantit que la note affichée a été écrite par celui qui la lit. assert.equal(urlSure('javascript:alert(1)'), null); assert.equal(urlSure(' JavaScript:alert(1)'), null); assert.equal(urlSure('data:text/html,