MDX : des composants gardés, et dessinés si vous voulez
Ce bloc n’exécute rien, et c’est la décision dont tout le reste découle. MDX, c’est du Markdown avec du JSX dedans ; évaluer du JSX demande un runtime, un compilateur, et du code arbitraire venu d’un fichier qu’on vous a donné — dans un éditeur dont toute la promesse est que vos notes sont des fichiers lisibles sans lui. C’est la même réponse que pour l’iframe d’une page déposée, pour la même raison.
Ce que ça achète est ce qui manquait : un .mdxs’ouvre ici et se referme identique. Sans ce bloc, une balise de composant est de la prose — <Callout type="warning"> est lu comme un paragraphe, ses chevrons sont échappés à l’enregistrement, et le fichier cesse d’être du MDX.
Sans rien de plus
Importez le bloc et un composant s’affiche comme ce qu’il est : son nom et sa source, dans une carte. C’est le comportement par défaut et il ne demande aucune configuration. La majuscule est la règle de JSX, pas une invention d’ici : <div> reste de la prose — Markdown la porte déjà très bien — et <Callout> devient un bloc.
Quand l’hôte sait dessiner
L’autre moitié, et celle qui rend le bloc utile plutôt que conservateur. Le fichier fournit les données — un nom, des attributs, le texte entre les balises. L’hôte fournit le code, qu’il a écrit lui-même et à qui il fait déjà confiance. <Counter start={3} /> devient un vrai compteur cliquable, et pas un caractère du fichier n’a été exécuté pour ça.
import { createMdx } from '@nbe/blocks-mdx/dom'
import { EditorView } from '@nbe/dom'
const mdx = createMdx({
components: {
// le fichier donne le nom et les attributs ; ceci donne le comportement
Counter: ({ props, setProps }) => {
const n = Number(props.start ?? 0)
const el = document.createElement('div')
const plus = Object.assign(document.createElement('button'), { textContent: '+' })
plus.addEventListener('click', () => setProps({ start: n + 1 }))
el.append(`${props.label ?? 'Compteur'} : ${n}`, plus)
return el
},
},
})
new EditorView(el, editor, { blocks: [...builtinBlocks, mdx] })C’est la même division que tous les autres crochets d’hôte de cet éditeur : onStoreAsset n’invente pas une politique de stockage, onSearchPages n’invente pas un magasin de pages. Un nom qui n’est pas dans la table retombe sur la carte de source, donc ajouter des composants est purement additif : un montage existant ne change pas.
Ce qu’un composant reçoit
type MdxComponentRenderer = (ctx: {
name: string // <Counter …> → 'Counter'
props: Record<string, unknown> // les attributs, lus
children: string // le texte entre les balises
source: string // la source entière, verbatim
setProps(patch: Record<string, unknown>): void
}) => HTMLElement | null // null : « je ne sais pas dessiner ça »Rendre null est une vraie réponse, pas un chemin d’erreur : un rendu enregistré pour Chart peut refuser un Chartdont il ne sait pas lire les données, et le lecteur voit la source plutôt qu’un dessin cassé.
Comment les attributs sont lus
<Counter start={3} label="Vues" wide total={count + 1} />
// props === {
// start: 3, // {…} qui est du JSON → la valeur
// label: 'Vues', // "…" → une chaîne
// wide: true, // attribut nu → true (la règle de JSX)
// total: 'count + 1', // {…} qui n'est pas du JSON → le texte, jamais évalué
// }Un {…} est du JSON quand il en est — ce qui couvre les nombres, les booléens, les tableaux et les objets, c’est-à-dire tout ce que quelqu’un écrit comme valeur littérale. Quand ce n’en est pas, la valeur arrive en texte et n’est jamais évaluée. Un rendu peut en faire ce qu’il veut, y compris rien.
L’état qui survit à un rechargement
Un composant qui garde son compte dans une fermeture revient à zéro dès qu’on rouvre le fichier. setProps écrit l’état là où il appartient : dans la balise. Le fichier dit alors où en est le compteur, n’importe quel autre outil MDX lit la même valeur, et rouvrir le fichier ramène le composant tel qu’on l’a laissé.
// avant
<Counter start={3} label="Vues" />
// setProps({ start: 5 }) — et le fichier dit maintenant :
<Counter start={5} label="Vues" />C’est une vraie modification — annulable, et elle salit le document — parce que c’en est une. Un composant qui stocke quelque chose que personne n’a voulu garder ne doit pas appeler setProps.
Les deux autres endroits possibles ont été écartés, et pour des raisons concrètes : un marqueur en commentaire casserait l’aller-retour octet pour octet qui est la raison d’être du bloc, et une clé dans l’en-tête YAML demanderait un identifiant stable que ce bloc ne persiste pas.
La règle qui rend ça sûr
Seules les clés modifiées sont réécrites. Tout le reste garde exactement les octets avec lesquels il a été écrit — c’est la règle que Frontmatter applique déjà aux clés que personne n’a touchées.
// setProps({ start: 9 }) sur :
<Counter start={3} total={count + 1} />
// écrit ceci, et surtout PAS total="count + 1" :
<Counter start={9} total={count + 1} />Sans elle : {count + 1} se lit comme la chaîne count + 1, donc réécrire tous les attributs l’émettrait en total="count + 1" — une expression transformée en littéral, dans un fichier que personne ne pensait avoir changé.
Dans le menu « / »
Une entrée Composant, plus une par composant enregistré, chacune marquée mdx à droite : « Composant » ne dit pas à quel monde il appartient, et allonger le libellé coûterait la chose qu’un lecteur parcourt vraiment.
Aux exports
| Dans le fichier | Dans l’éditeur | En Markdown | En HTML |
|---|---|---|---|
| Un nom que l’hôte connaît | le composant, dessiné par l’hôte | la balise, octet pour octet | sa source, échappée |
| Un nom inconnu | une carte avec sa source | la balise, octet pour octet | sa source, échappée |
| <div>, en minuscule | de la prose | de la prose | de la prose |
L’export HTML montre la source échappée — montrée, jamais exécutée, la même promesse que dans l’éditeur — et l’état voyage avec, puisqu’il est dans la balise. Le bloc y porte son identifiant comme tous les autres, donc un lien vers lui fonctionne.
Le reste d’un vrai fichier .mdx traverse aussi : un import, un export const, une expression {frontmatter.title} en début de ligne sont de la prose pour cet éditeur, et de la prose qui ressort telle quelle.
Ce que ce n’est pas
- Ce n’est pas un compilateur MDX. Rien n’est évalué, ni les composants, ni les expressions, ni les
importen tête de fichier. - Un composant dessiné par l’hôte est du code de l’hôte : c’est lui qui décide de ce qu’il fait, et c’est à lui de ne pas y mettre ce qu’il ne veut pas exécuter.
- Un bloc MDX n’a pas de curseur — comme une image. On le sélectionne en appuyant dessus, et Entrée ouvre un paragraphe en dessous.
Le bac à sable monte un Counter et un Callout : cliquez le compteur, ouvrez l’onglet Markdown, et regardez la balise changer.