[{"data":1,"prerenderedAt":1759},["ShallowReactive",2],{"post-\u002Ffr\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering":3,"post-link-fr-external":1754},{"id":4,"title":5,"body":6,"contentId":1748,"date":1749,"description":1750,"extension":118,"meta":1751,"navigation":258,"path":1752,"rawbody":1753,"robots":1754,"schemaOrg":1754,"seo":1755,"sitemap":1756,"stem":1757,"__hash__":1758},"posts\u002Ffr\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering.md","Markdown Exit brise les règles avec le rendu asynchrone",{"type":7,"value":8,"toc":1741},"minimark",[9,18,21,24,35,44,47,52,55,82,1738],[10,11,12,13],"p",{},"Cet article est la suite de ",[14,15,17],"link-to-article",{"id":16},"a505274a-14a6-471e-95a2-a3dbc64cd170"," et se concentre sur un cas d'utilisation plus concret : l'optimisation automatique des images dans les fichiers Markdown.",[10,19,20],{},"C'est très technique, et à moins que vous ne traitiez des fichiers Markdown, cela pourrait être un peu abstrait. Néanmoins, je vous encourage vivement à lire l'article précédent et celui-ci, car cela pourrait vous donner de bonnes idées.",[22,23],"hr",{},[10,25,26,27,34],{},"Pourquoi écrire un article dédié à ce sujet ? Parce que, jusqu'à présent, ce n'était pas possible. Le principal parseur Markdown dans le monde JavaScript est ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",[32],"nofollow","markdown-it",", et il est entièrement synchrone.",[10,36,37,38,43],{},"Cela pourrait ne pas être un problème, mais cela signifie que ",[28,39,42],{"href":40,"rel":41},"https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it\u002Fblob\u002Fmaster\u002Fdocs\u002Fdevelopment.md#i-need-async-rule-how-to-do-it",[32],"toutes les règles doivent être synchrones"," également. Vous ne pouvez pas avoir de règles asynchrones. Vous ne pouvez pas effectuer de fetch dans une règle, et vous ne pouvez rien paralléliser.",[10,45,46],{},"Un petit récapitulatif est nécessaire pour s'assurer que nous sommes tous sur la même longueur d'onde.",[48,49,51],"h2",{"id":50},"écrire-un-parseur-markdown","Écrire un Parseur Markdown",[10,53,54],{},"Je n'entrerai pas dans les détails, mais essentiellement, le parseur va diviser le texte Markdown en tokens puis appliquer des règles, qui sont essentiellement des fonctions, à ces tokens pour générer le HTML.",[10,56,57,58,62,63,66,67,70,71,74,75,77,78,81],{},"Par exemple, le texte suivant ",[59,60,61],"code",{},"**bold text**"," sera tokenisé en : ",[59,64,65],{},"['strong_open', 'text', 'strong_close']"," et les règles correspondantes généreront le HTML : ",[59,68,69],{},"\u003Cstrong>bold text\u003C\u002Fstrong>",". Le token ",[59,72,73],{},"strong_open"," sera traité par la règle ",[59,76,73],{},", qui retourne ",[59,79,80],{},"\u003Cstrong>",", et ainsi de suite.",[83,84,87,90,101,348,359,363,366,381,384,393,397,405,408,416,423,426,442,445,480,483,619,623,631,634,782,785,842,845,851,859,1098,1104,1107,1116,1133,1141,1276,1279,1346,1349,1355,1480,1483,1617,1624,1661,1668,1675,1679,1682,1685,1727,1732,1735],"callout-read-more",{"text":85,"href":86},"Explorez l'implémentation de la règle d'emphase dans markdown-exit","https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit\u002Fblob\u002F3e0cfeb7926355f6896fcd4dd7afd797e7f6689f\u002Fpackages\u002Fmarkdown-exit\u002Fsrc\u002Fparser\u002Finline\u002Frules\u002Femphasis.ts",[10,88,89],{},"La règle dont nous parlons depuis le début transforme un token en sa représentation HTML correspondante. La partie intéressante est que vous pouvez attacher vos propres règles au parseur pour étendre ses fonctionnalités.",[10,91,92,93,96,97,100],{},"Imaginez que vous vouliez appliquer automatiquement l'attribut ",[59,94,95],{},"loading=\"lazy\""," à toutes les images de vos fichiers Markdown. Vous pourriez écrire une règle qui cible la règle ",[59,98,99],{},"image",".",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight has-highlighted","md.use((md) => {\n  const imageRule = md.renderer.rules.image!\n  md.renderer.rules.image = (tokens, idx, options, env, self) => {\n    const token = tokens[idx]\n\n    token.attrSet('loading', 'lazy') \u002F\u002F [!code highlight]\n\n    return imageRule(tokens, idx, options, env, self)\n  }\n})\n","ts","",[59,109,110,144,176,229,253,260,297,302,334,340],{"__ignoreMap":107},[111,112,115,119,122,126,129,131,134,137,141],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sTEyZ","md",[111,120,100],{"class":121},"sMK4o",[111,123,125],{"class":124},"s2Zo4","use",[111,127,128],{"class":117},"(",[111,130,128],{"class":121},[111,132,118],{"class":133},"sHdIc",[111,135,136],{"class":121},")",[111,138,140],{"class":139},"spNyl"," =>",[111,142,143],{"class":121}," {\n",[111,145,147,150,153,156,159,161,164,166,169,171,173],{"class":113,"line":146},2,[111,148,149],{"class":139},"  const",[111,151,152],{"class":117}," imageRule",[111,154,155],{"class":121}," =",[111,157,158],{"class":117}," md",[111,160,100],{"class":121},[111,162,163],{"class":117},"renderer",[111,165,100],{"class":121},[111,167,168],{"class":117},"rules",[111,170,100],{"class":121},[111,172,99],{"class":117},[111,174,175],{"class":121},"!\n",[111,177,179,182,184,186,188,190,192,194,196,199,202,205,208,210,213,215,218,220,223,225,227],{"class":113,"line":178},3,[111,180,181],{"class":117},"  md",[111,183,100],{"class":121},[111,185,163],{"class":117},[111,187,100],{"class":121},[111,189,168],{"class":117},[111,191,100],{"class":121},[111,193,99],{"class":124},[111,195,155],{"class":121},[111,197,198],{"class":121}," (",[111,200,201],{"class":133},"tokens",[111,203,204],{"class":121},",",[111,206,207],{"class":133}," idx",[111,209,204],{"class":121},[111,211,212],{"class":133}," options",[111,214,204],{"class":121},[111,216,217],{"class":133}," env",[111,219,204],{"class":121},[111,221,222],{"class":133}," self",[111,224,136],{"class":121},[111,226,140],{"class":139},[111,228,143],{"class":121},[111,230,232,235,238,240,243,247,250],{"class":113,"line":231},4,[111,233,234],{"class":139},"    const",[111,236,237],{"class":117}," token",[111,239,155],{"class":121},[111,241,242],{"class":117}," tokens",[111,244,246],{"class":245},"swJcz","[",[111,248,249],{"class":117},"idx",[111,251,252],{"class":245},"]\n",[111,254,256],{"class":113,"line":255},5,[111,257,259],{"emptyLinePlaceholder":258},true,"\n",[111,261,265,268,270,273,275,278,282,284,286,289,292,294],{"class":262,"line":264},[113,263],"highlighted",6,[111,266,267],{"class":117},"    token",[111,269,100],{"class":121},[111,271,272],{"class":124},"attrSet",[111,274,128],{"class":245},[111,276,277],{"class":121},"'",[111,279,281],{"class":280},"sfazB","loading",[111,283,277],{"class":121},[111,285,204],{"class":121},[111,287,288],{"class":121}," '",[111,290,291],{"class":280},"lazy",[111,293,277],{"class":121},[111,295,296],{"class":245},") ",[111,298,300],{"class":113,"line":299},7,[111,301,259],{"emptyLinePlaceholder":258},[111,303,305,309,311,313,315,317,319,321,323,325,327,329,331],{"class":113,"line":304},8,[111,306,308],{"class":307},"s7zQu","    return",[111,310,152],{"class":124},[111,312,128],{"class":245},[111,314,201],{"class":117},[111,316,204],{"class":121},[111,318,207],{"class":117},[111,320,204],{"class":121},[111,322,212],{"class":117},[111,324,204],{"class":121},[111,326,217],{"class":117},[111,328,204],{"class":121},[111,330,222],{"class":117},[111,332,333],{"class":245},")\n",[111,335,337],{"class":113,"line":336},9,[111,338,339],{"class":121},"  }\n",[111,341,343,346],{"class":113,"line":342},10,[111,344,345],{"class":121},"}",[111,347,333],{"class":117},[10,349,350,351,354,355,358],{},"Maintenant, le Markdown suivant ",[59,352,353],{},"![alt text](image.jpg)"," sera rendu comme ",[59,356,357],{},"\u003Cimg src=\"image.jpg\" alt=\"alt text\" loading=\"lazy\" \u002F>",". C'est un rêve : appliquer automatiquement des changements aux fichiers sans les toucher.",[48,360,362],{"id":361},"limitations-synchrones","Limitations Synchrones",[10,364,365],{},"C'est un rêve jusqu'à ce que vous réalisiez que tout cela se passe de manière synchrone. Les règles sont appliquées une par une, sans aucun moyen de paralléliser le processus. Pour la règle de surlignage du code, cela peut réduire considérablement les performances car vous ne pouvez pas rendre tous les blocs de code en même temps, c'est un par un.",[367,368,369],"blockquote",{},[10,370,371,374,375,380],{},[111,372,373],{},"!NOTE","\nAnthony Fu en était bien conscient et il a créé ",[28,376,379],{"href":377,"rel":378},"https:\u002F\u002Fgithub.com\u002Fantfu\u002Fmarkdown-it-async",[32],"markdown-it-async"," pour rendre cela possible.",[10,382,383],{},"En plus de paralléliser le processus, les règles synchrones signifient que vous ne pouvez effectuer aucune opération asynchrone au sein d'une règle. Par exemple, vous ne pouvez pas récupérer de données depuis une API. Cela peut rapidement devenir limitant.",[10,385,386,387,392],{},"Mais, avec ",[28,388,391],{"href":389,"rel":390},"https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit",[32],"markdown-exit",", un remplacement direct et une réécriture complète de markdown-it en utilisant TypeScript, c'est désormais possible. Les règles asynchrones sont maintenant une réalité.",[48,394,396],{"id":395},"optimiser-les-images-à-la-volée","Optimiser les Images à la Volée",[10,398,399,400,404],{},"Dans ",[28,401,403],{"href":402},".\u002Ferase-cls-by-automatically-optimizing-images-with-vite","l'article précédent",", nous avons exploité un plugin Vite pour appliquer automatiquement des optimisations aux images dans notre HTML. C'était un bon début utile.",[10,406,407],{},"Pour moi, ce n'était pas suffisant car je traite principalement des fichiers Markdown, et construire une règle personnalisée est un meilleur choix. C'est ce que nous allons voir maintenant.",[10,409,410,411],{},"Tout le code source est disponible sur GitHub : ",[412,413,415],"git-hub-link",{"repo":414},"Barbapapazes\u002Fmarkdown-exit-unpic",". Pour garder cet article simple, je ne montrerai pas le code non lié comme la configuration du projet.",[10,417,418,419,422],{},"Imaginons que nous ayons un fichier ",[59,420,421],{},"src\u002Findex.ts",". Ce fichier est notre script principal, et le but est de rendre du Markdown brut en HTML tout en optimisant les images.",[10,424,425],{},"Nous voulons exécuter la commande suivante :",[102,427,431],{"className":428,"code":429,"language":430,"meta":107,"style":107},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","node src\u002Findex.ts\n","sh",[59,432,433],{"__ignoreMap":107},[111,434,435,439],{"class":113,"line":114},[111,436,438],{"class":437},"sBMFI","node",[111,440,441],{"class":280}," src\u002Findex.ts\n",[10,443,444],{},"avec ce contenu Markdown :",[102,446,449],{"className":447,"code":448,"language":118,"meta":107,"style":107},"language-md shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Hello World\n\n![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)\n",[59,450,451,459,463],{"__ignoreMap":107},[111,452,453,456],{"class":113,"line":114},[111,454,455],{"class":121},"# ",[111,457,458],{"class":437},"Hello World\n",[111,460,461],{"class":113,"line":146},[111,462,259],{"emptyLinePlaceholder":258},[111,464,465,468,471,474,478],{"class":113,"line":178},[111,466,467],{"class":121},"![",[111,469,470],{"class":280},"This is a placeholder",[111,472,473],{"class":121},"](",[111,475,477],{"class":476},"sQLHv","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666",[111,479,333],{"class":121},[10,481,482],{},"et avoir la sortie suivante :",[102,484,488],{"className":485,"code":486,"language":487,"meta":107,"style":107},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666?auto=format&amp;fit=crop&amp;q=80&amp;w=800\" alt=\"This is a placeholder\" loading=\"lazy\" width=\"2567\" height=\"2000\" style=\"background-size: cover; background-image: url(data:image\u002Fbmp;base64,Qk32BAAAAAAAADYAAAAoAAAACAAAAAgAAAABABgAAAAAAMAAAAATCwAAEwsAAAAAAAAAAAAACQYICgcIDAoKEQ8PFBISFRISEQ4OCgYGCAYGDAoKExESGhgYHRsbHBoaFhMTDAgICAcGDgwMGBcXIB8fIyIiISAgGhgYDgoLCQcGDw0NGRgYIiEhJiUlJCIiHBoaDwsMCgcHDgwMFxYWHx4eIyIiISAgGhgYDgsLCQYGDAkJEhAQGBcXGxoaGxkZFRMTDAgICAUECQYGDAkJEA0OEhAQEhAQDwwMCQYFBwUEBwUECQYGCwcJDQoKDQsKCwgHCQUE);\">\u003C\u002Fp>\n","html",[59,489,490,512],{"__ignoreMap":107},[111,491,492,495,498,501,504,507,509],{"class":113,"line":114},[111,493,494],{"class":121},"\u003C",[111,496,497],{"class":245},"h1",[111,499,500],{"class":121},">",[111,502,503],{"class":117},"Hello World",[111,505,506],{"class":121},"\u003C\u002F",[111,508,497],{"class":245},[111,510,511],{"class":121},">\n",[111,513,514,516,518,521,524,527,530,533,536,539,542,544,547,549,552,554,557,559,561,563,565,568,570,572,574,576,579,581,583,586,588,591,593,595,598,600,603,605,607,610,612,615,617],{"class":113,"line":146},[111,515,494],{"class":121},[111,517,10],{"class":245},[111,519,520],{"class":121},">\u003C",[111,522,523],{"class":245},"img",[111,525,526],{"class":139}," src",[111,528,529],{"class":121},"=",[111,531,532],{"class":121},"\"",[111,534,535],{"class":280},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666?auto=format",[111,537,538],{"class":117},"&amp;",[111,540,541],{"class":280},"fit=crop",[111,543,538],{"class":117},[111,545,546],{"class":280},"q=80",[111,548,538],{"class":117},[111,550,551],{"class":280},"w=800",[111,553,532],{"class":121},[111,555,556],{"class":139}," alt",[111,558,529],{"class":121},[111,560,532],{"class":121},[111,562,470],{"class":280},[111,564,532],{"class":121},[111,566,567],{"class":139}," loading",[111,569,529],{"class":121},[111,571,532],{"class":121},[111,573,291],{"class":280},[111,575,532],{"class":121},[111,577,578],{"class":139}," width",[111,580,529],{"class":121},[111,582,532],{"class":121},[111,584,585],{"class":280},"2567",[111,587,532],{"class":121},[111,589,590],{"class":139}," height",[111,592,529],{"class":121},[111,594,532],{"class":121},[111,596,597],{"class":280},"2000",[111,599,532],{"class":121},[111,601,602],{"class":139}," style",[111,604,529],{"class":121},[111,606,532],{"class":121},[111,608,609],{"class":280},"background-size: cover; background-image: url(data:image\u002Fbmp;base64,Qk32BAAAAAAAADYAAAAoAAAACAAAAAgAAAABABgAAAAAAMAAAAATCwAAEwsAAAAAAAAAAAAACQYICgcIDAoKEQ8PFBISFRISEQ4OCgYGCAYGDAoKExESGhgYHRsbHBoaFhMTDAgICAcGDgwMGBcXIB8fIyIiISAgGhgYDgoLCQcGDw0NGRgYIiEhJiUlJCIiHBoaDwsMCgcHDgwMFxYWHx4eIyIiISAgGhgYDgsLCQYGDAkJEhAQGBcXGxoaGxkZFRMTDAgICAUECQYGDAkJEA0OEhAQEhAQDwwMCQYFBwUEBwUECQYGCwcJDQoKDQsKCwgHCQUE);",[111,611,532],{"class":121},[111,613,614],{"class":121},">\u003C\u002F",[111,616,10],{"class":245},[111,618,511],{"class":121},[48,620,622],{"id":621},"construire-une-règle-asynchrone-personnalisée","Construire une Règle Asynchrone Personnalisée",[10,624,625,626,100],{},"La logique centrale de la règle ne différera pas de ce que nous avons construit dans le plugin Vite. Au lieu de lire le fichier depuis le système de fichiers local, nous le récupérerons depuis Unsplash pour simuler un bucket distant. Ensuite, nous récupérerons la largeur et la hauteur, et générerons le blurhash. À la fin, nous appliquerons également quelques paramètres de requête personnalisés pour éviter que le client ne charge l'image complète, comme vous pouvez le faire avec ",[28,627,630],{"href":628,"rel":629},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F",[32],"Cloudflare Images",[10,632,633],{},"D'abord, nous devons créer une nouvelle instance de markdown-exit et rendre le Markdown. C'est vraiment simple.",[102,635,638],{"className":636,"code":637,"language":106,"meta":107,"style":107},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { createMarkdownExit } from 'markdown-exit'\n\nconst md = createMarkdownExit()\n\nasync function render() {\n  const html = await md.renderAsync(`# Hello World\n\n![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)`)\n\n  console.log(html)\n}\n\nrender()\n",[59,639,640,664,668,683,687,703,730,734,743,747,763,769,774],{"__ignoreMap":107},[111,641,642,645,648,651,654,657,659,661],{"class":113,"line":114},[111,643,644],{"class":307},"import",[111,646,647],{"class":121}," {",[111,649,650],{"class":117}," createMarkdownExit",[111,652,653],{"class":121}," }",[111,655,656],{"class":307}," from",[111,658,288],{"class":121},[111,660,391],{"class":280},[111,662,663],{"class":121},"'\n",[111,665,666],{"class":113,"line":146},[111,667,259],{"emptyLinePlaceholder":258},[111,669,670,673,676,678,680],{"class":113,"line":178},[111,671,672],{"class":139},"const",[111,674,675],{"class":117}," md ",[111,677,529],{"class":121},[111,679,650],{"class":124},[111,681,682],{"class":117},"()\n",[111,684,685],{"class":113,"line":231},[111,686,259],{"emptyLinePlaceholder":258},[111,688,689,692,695,698,701],{"class":113,"line":255},[111,690,691],{"class":139},"async",[111,693,694],{"class":139}," function",[111,696,697],{"class":124}," render",[111,699,700],{"class":121},"()",[111,702,143],{"class":121},[111,704,705,707,710,712,715,717,719,722,724,727],{"class":113,"line":264},[111,706,149],{"class":139},[111,708,709],{"class":117}," html",[111,711,155],{"class":121},[111,713,714],{"class":307}," await",[111,716,158],{"class":117},[111,718,100],{"class":121},[111,720,721],{"class":124},"renderAsync",[111,723,128],{"class":245},[111,725,726],{"class":121},"`",[111,728,729],{"class":280},"# Hello World\n",[111,731,732],{"class":113,"line":299},[111,733,259],{"emptyLinePlaceholder":258},[111,735,736,739,741],{"class":113,"line":304},[111,737,738],{"class":280},"![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)",[111,740,726],{"class":121},[111,742,333],{"class":245},[111,744,745],{"class":113,"line":336},[111,746,259],{"emptyLinePlaceholder":258},[111,748,749,752,754,757,759,761],{"class":113,"line":342},[111,750,751],{"class":117},"  console",[111,753,100],{"class":121},[111,755,756],{"class":124},"log",[111,758,128],{"class":245},[111,760,487],{"class":117},[111,762,333],{"class":245},[111,764,766],{"class":113,"line":765},11,[111,767,768],{"class":121},"}\n",[111,770,772],{"class":113,"line":771},12,[111,773,259],{"emptyLinePlaceholder":258},[111,775,777,780],{"class":113,"line":776},13,[111,778,779],{"class":124},"render",[111,781,682],{"class":117},[10,783,784],{},"Rien d'extraordinaire dans ce script. Cependant, la sortie est la suivante :",[102,786,788],{"className":485,"code":787,"language":487,"meta":107,"style":107},"\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666\" alt=\"This is a placeholder\">\u003C\u002Fp>\n",[59,789,790,806],{"__ignoreMap":107},[111,791,792,794,796,798,800,802,804],{"class":113,"line":114},[111,793,494],{"class":121},[111,795,497],{"class":245},[111,797,500],{"class":121},[111,799,503],{"class":117},[111,801,506],{"class":121},[111,803,497],{"class":245},[111,805,511],{"class":121},[111,807,808,810,812,814,816,818,820,822,824,826,828,830,832,834,836,838,840],{"class":113,"line":146},[111,809,494],{"class":121},[111,811,10],{"class":245},[111,813,520],{"class":121},[111,815,523],{"class":245},[111,817,526],{"class":139},[111,819,529],{"class":121},[111,821,532],{"class":121},[111,823,477],{"class":280},[111,825,532],{"class":121},[111,827,556],{"class":139},[111,829,529],{"class":121},[111,831,532],{"class":121},[111,833,470],{"class":280},[111,835,532],{"class":121},[111,837,614],{"class":121},[111,839,10],{"class":245},[111,841,511],{"class":121},[10,843,844],{},"Il manque quelques parties, mais c'est un bon début.",[10,846,847,848,850],{},"Pour changer le comportement de rendu de l'image, nous allons nous accrocher à la règle ",[59,849,99],{},", ajuster le token à nos besoins, et utiliser l'implémentation par défaut pour générer la sortie.",[10,852,853,854,856,857,100],{},"Pour créer une nouvelle règle, nous pouvons utiliser la fonction ",[59,855,125],{},". C'est comme créer un plugin Vue. Ensuite, liez la fonction que nous voulons à la règle ",[59,858,99],{},[102,860,862],{"className":636,"code":861,"language":106,"meta":107,"style":107},"import type { MarkdownExit } from 'markdown-exit'\nimport { createMarkdownExit } from 'markdown-exit'\n\nconst md = createMarkdownExit()\n\nmd.use((md: MarkdownExit) => {\n  const imageRule = md.renderer.rules.image!\n  md.renderer.rules.image = async (tokens, idx, options, env, self) => {\n    return imageRule(tokens, idx, options, env, self)\n  }\n})\n\nasync function render() {\n  \u002F\u002F ...\n}\n\nrender()\n",[59,863,864,886,904,908,920,924,949,973,1020,1048,1052,1058,1062,1074,1081,1086,1091],{"__ignoreMap":107},[111,865,866,868,871,873,876,878,880,882,884],{"class":113,"line":114},[111,867,644],{"class":307},[111,869,870],{"class":307}," type",[111,872,647],{"class":121},[111,874,875],{"class":117}," MarkdownExit",[111,877,653],{"class":121},[111,879,656],{"class":307},[111,881,288],{"class":121},[111,883,391],{"class":280},[111,885,663],{"class":121},[111,887,888,890,892,894,896,898,900,902],{"class":113,"line":146},[111,889,644],{"class":307},[111,891,647],{"class":121},[111,893,650],{"class":117},[111,895,653],{"class":121},[111,897,656],{"class":307},[111,899,288],{"class":121},[111,901,391],{"class":280},[111,903,663],{"class":121},[111,905,906],{"class":113,"line":178},[111,907,259],{"emptyLinePlaceholder":258},[111,909,910,912,914,916,918],{"class":113,"line":231},[111,911,672],{"class":139},[111,913,675],{"class":117},[111,915,529],{"class":121},[111,917,650],{"class":124},[111,919,682],{"class":117},[111,921,922],{"class":113,"line":255},[111,923,259],{"emptyLinePlaceholder":258},[111,925,926,928,930,932,934,936,938,941,943,945,947],{"class":113,"line":264},[111,927,118],{"class":117},[111,929,100],{"class":121},[111,931,125],{"class":124},[111,933,128],{"class":117},[111,935,128],{"class":121},[111,937,118],{"class":133},[111,939,940],{"class":121},":",[111,942,875],{"class":437},[111,944,136],{"class":121},[111,946,140],{"class":139},[111,948,143],{"class":121},[111,950,951,953,955,957,959,961,963,965,967,969,971],{"class":113,"line":299},[111,952,149],{"class":139},[111,954,152],{"class":117},[111,956,155],{"class":121},[111,958,158],{"class":117},[111,960,100],{"class":121},[111,962,163],{"class":117},[111,964,100],{"class":121},[111,966,168],{"class":117},[111,968,100],{"class":121},[111,970,99],{"class":117},[111,972,175],{"class":121},[111,974,975,977,979,981,983,985,987,989,991,994,996,998,1000,1002,1004,1006,1008,1010,1012,1014,1016,1018],{"class":113,"line":304},[111,976,181],{"class":117},[111,978,100],{"class":121},[111,980,163],{"class":117},[111,982,100],{"class":121},[111,984,168],{"class":117},[111,986,100],{"class":121},[111,988,99],{"class":124},[111,990,155],{"class":121},[111,992,993],{"class":139}," async",[111,995,198],{"class":121},[111,997,201],{"class":133},[111,999,204],{"class":121},[111,1001,207],{"class":133},[111,1003,204],{"class":121},[111,1005,212],{"class":133},[111,1007,204],{"class":121},[111,1009,217],{"class":133},[111,1011,204],{"class":121},[111,1013,222],{"class":133},[111,1015,136],{"class":121},[111,1017,140],{"class":139},[111,1019,143],{"class":121},[111,1021,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046],{"class":113,"line":336},[111,1023,308],{"class":307},[111,1025,152],{"class":124},[111,1027,128],{"class":245},[111,1029,201],{"class":117},[111,1031,204],{"class":121},[111,1033,207],{"class":117},[111,1035,204],{"class":121},[111,1037,212],{"class":117},[111,1039,204],{"class":121},[111,1041,217],{"class":117},[111,1043,204],{"class":121},[111,1045,222],{"class":117},[111,1047,333],{"class":245},[111,1049,1050],{"class":113,"line":342},[111,1051,339],{"class":121},[111,1053,1054,1056],{"class":113,"line":765},[111,1055,345],{"class":121},[111,1057,333],{"class":117},[111,1059,1060],{"class":113,"line":771},[111,1061,259],{"emptyLinePlaceholder":258},[111,1063,1064,1066,1068,1070,1072],{"class":113,"line":776},[111,1065,691],{"class":139},[111,1067,694],{"class":139},[111,1069,697],{"class":124},[111,1071,700],{"class":121},[111,1073,143],{"class":121},[111,1075,1077],{"class":113,"line":1076},14,[111,1078,1080],{"class":1079},"sHwdD","  \u002F\u002F ...\n",[111,1082,1084],{"class":113,"line":1083},15,[111,1085,768],{"class":121},[111,1087,1089],{"class":113,"line":1088},16,[111,1090,259],{"emptyLinePlaceholder":258},[111,1092,1094,1096],{"class":113,"line":1093},17,[111,1095,779],{"class":124},[111,1097,682],{"class":117},[10,1099,1100,1101,1103],{},"Notre fonction ne fait rien à part appeler la règle ",[59,1102,99],{}," originale, ce qui est déjà une bonne étape car nous ne voulons pas la réimplémenter nous-mêmes.",[10,1105,1106],{},"Ensuite, nous pouvons saisir le token actuel et essayer de lui ajouter un attribut.",[10,1108,1109,1110,1112,1113,1115],{},"La variable ",[59,1111,201],{}," contient tous les tokens disponibles. C'est un tableau de tokens. Pour accéder à l'actuel, nous devons utiliser la variable ",[59,1114,249],{},", qui est l'index actuel du token.",[102,1117,1119],{"className":636,"code":1118,"language":106,"meta":107,"style":107},"const token = tokens[idx]\n",[59,1120,1121],{"__ignoreMap":107},[111,1122,1123,1125,1128,1130],{"class":113,"line":114},[111,1124,672],{"class":139},[111,1126,1127],{"class":117}," token ",[111,1129,529],{"class":121},[111,1131,1132],{"class":117}," tokens[idx]\n",[10,1134,1135,1136,1138,1139,100],{},"Maintenant, nous pouvons modifier le token. Par exemple, nous pouvons ajouter un attribut ",[59,1137,281],{}," avec la valeur ",[59,1140,291],{},[102,1142,1144],{"className":636,"code":1143,"language":106,"meta":107,"style":107},"md.renderer.rules.image = async (tokens, idx, options, env, self) => {\n  const token = tokens[idx]\n\n  token.attrSet('loading', 'lazy')\n\n  return imageRule(tokens, idx, options, env, self)\n}\n",[59,1145,1146,1192,1208,1212,1239,1243,1272],{"__ignoreMap":107},[111,1147,1148,1150,1152,1154,1156,1158,1160,1162,1164,1166,1168,1170,1172,1174,1176,1178,1180,1182,1184,1186,1188,1190],{"class":113,"line":114},[111,1149,118],{"class":117},[111,1151,100],{"class":121},[111,1153,163],{"class":117},[111,1155,100],{"class":121},[111,1157,168],{"class":117},[111,1159,100],{"class":121},[111,1161,99],{"class":124},[111,1163,155],{"class":121},[111,1165,993],{"class":139},[111,1167,198],{"class":121},[111,1169,201],{"class":133},[111,1171,204],{"class":121},[111,1173,207],{"class":133},[111,1175,204],{"class":121},[111,1177,212],{"class":133},[111,1179,204],{"class":121},[111,1181,217],{"class":133},[111,1183,204],{"class":121},[111,1185,222],{"class":133},[111,1187,136],{"class":121},[111,1189,140],{"class":139},[111,1191,143],{"class":121},[111,1193,1194,1196,1198,1200,1202,1204,1206],{"class":113,"line":146},[111,1195,149],{"class":139},[111,1197,237],{"class":117},[111,1199,155],{"class":121},[111,1201,242],{"class":117},[111,1203,246],{"class":245},[111,1205,249],{"class":117},[111,1207,252],{"class":245},[111,1209,1210],{"class":113,"line":178},[111,1211,259],{"emptyLinePlaceholder":258},[111,1213,1214,1217,1219,1221,1223,1225,1227,1229,1231,1233,1235,1237],{"class":113,"line":231},[111,1215,1216],{"class":117},"  token",[111,1218,100],{"class":121},[111,1220,272],{"class":124},[111,1222,128],{"class":245},[111,1224,277],{"class":121},[111,1226,281],{"class":280},[111,1228,277],{"class":121},[111,1230,204],{"class":121},[111,1232,288],{"class":121},[111,1234,291],{"class":280},[111,1236,277],{"class":121},[111,1238,333],{"class":245},[111,1240,1241],{"class":113,"line":255},[111,1242,259],{"emptyLinePlaceholder":258},[111,1244,1245,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270],{"class":113,"line":264},[111,1246,1247],{"class":307},"  return",[111,1249,152],{"class":124},[111,1251,128],{"class":245},[111,1253,201],{"class":117},[111,1255,204],{"class":121},[111,1257,207],{"class":117},[111,1259,204],{"class":121},[111,1261,212],{"class":117},[111,1263,204],{"class":121},[111,1265,217],{"class":117},[111,1267,204],{"class":121},[111,1269,222],{"class":117},[111,1271,333],{"class":245},[111,1273,1274],{"class":113,"line":299},[111,1275,768],{"class":121},[10,1277,1278],{},"La sortie de notre script est maintenant la suivante :",[102,1280,1282],{"className":485,"code":1281,"language":487,"meta":107,"style":107},"\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666\" alt=\"This is a placeholder\" loading=\"lazy\">\u003C\u002Fp>\n",[59,1283,1284,1300],{"__ignoreMap":107},[111,1285,1286,1288,1290,1292,1294,1296,1298],{"class":113,"line":114},[111,1287,494],{"class":121},[111,1289,497],{"class":245},[111,1291,500],{"class":121},[111,1293,503],{"class":117},[111,1295,506],{"class":121},[111,1297,497],{"class":245},[111,1299,511],{"class":121},[111,1301,1302,1304,1306,1308,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338,1340,1342,1344],{"class":113,"line":146},[111,1303,494],{"class":121},[111,1305,10],{"class":245},[111,1307,520],{"class":121},[111,1309,523],{"class":245},[111,1311,526],{"class":139},[111,1313,529],{"class":121},[111,1315,532],{"class":121},[111,1317,477],{"class":280},[111,1319,532],{"class":121},[111,1321,556],{"class":139},[111,1323,529],{"class":121},[111,1325,532],{"class":121},[111,1327,470],{"class":280},[111,1329,532],{"class":121},[111,1331,567],{"class":139},[111,1333,529],{"class":121},[111,1335,532],{"class":121},[111,1337,291],{"class":280},[111,1339,532],{"class":121},[111,1341,614],{"class":121},[111,1343,10],{"class":245},[111,1345,511],{"class":121},[10,1347,1348],{},"Facile, non ?",[10,1350,1351,1352],{},"Basé sur notre comportement attendu, nous devons ajouter la largeur, la hauteur, le blurhash, et quelques paramètres de requête à l'image. Pour ce faire, nous utiliserons la même logique que dans ",[14,1353,1354],{"id":16},", sauf que nous récupérerons l'image depuis Unsplash, comme vous pourriez le faire avec n'importe quel bucket distant.",[102,1356,1358],{"className":636,"code":1357,"language":106,"meta":107,"style":107},"const img = await fetch(src).then(res => res.bytes())\nconst data = await getPixels(img)\n\nconst blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n",[59,1359,1360,1400,1417,1421],{"__ignoreMap":107},[111,1361,1362,1364,1367,1369,1371,1374,1377,1379,1382,1384,1387,1389,1392,1394,1397],{"class":113,"line":114},[111,1363,672],{"class":139},[111,1365,1366],{"class":117}," img ",[111,1368,529],{"class":121},[111,1370,714],{"class":307},[111,1372,1373],{"class":124}," fetch",[111,1375,1376],{"class":117},"(src)",[111,1378,100],{"class":121},[111,1380,1381],{"class":124},"then",[111,1383,128],{"class":117},[111,1385,1386],{"class":133},"res",[111,1388,140],{"class":139},[111,1390,1391],{"class":117}," res",[111,1393,100],{"class":121},[111,1395,1396],{"class":124},"bytes",[111,1398,1399],{"class":117},"())\n",[111,1401,1402,1404,1407,1409,1411,1414],{"class":113,"line":146},[111,1403,672],{"class":139},[111,1405,1406],{"class":117}," data ",[111,1408,529],{"class":121},[111,1410,714],{"class":307},[111,1412,1413],{"class":124}," getPixels",[111,1415,1416],{"class":117},"(img)\n",[111,1418,1419],{"class":113,"line":178},[111,1420,259],{"emptyLinePlaceholder":258},[111,1422,1423,1425,1428,1430,1433,1436,1438,1441,1444,1446,1449,1451,1454,1456,1459,1461,1463,1465,1468,1470,1474,1476,1478],{"class":113,"line":231},[111,1424,672],{"class":139},[111,1426,1427],{"class":117}," blurhash ",[111,1429,529],{"class":121},[111,1431,1432],{"class":124}," encode",[111,1434,1435],{"class":117},"(Uint8ClampedArray",[111,1437,100],{"class":121},[111,1439,1440],{"class":124},"from",[111,1442,1443],{"class":117},"(data",[111,1445,100],{"class":121},[111,1447,1448],{"class":117},"data)",[111,1450,204],{"class":121},[111,1452,1453],{"class":117}," data",[111,1455,100],{"class":121},[111,1457,1458],{"class":117},"width",[111,1460,204],{"class":121},[111,1462,1453],{"class":117},[111,1464,100],{"class":121},[111,1466,1467],{"class":117},"height",[111,1469,204],{"class":121},[111,1471,1473],{"class":1472},"sbssI"," 4",[111,1475,204],{"class":121},[111,1477,1473],{"class":1472},[111,1479,333],{"class":117},[10,1481,1482],{},"Maintenant, nous pouvons définir les attributs width, height et style sur le token.",[102,1484,1486],{"className":636,"code":1485,"language":106,"meta":107,"style":107},"import { blurhashToDataUri } from '@unpic\u002Fplaceholder'\n\ntoken.attrSet('width', data.width.toString())\ntoken.attrSet('height', data.height.toString())\ntoken.attrSet('style', `background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});`)\n",[59,1487,1488,1508,1512,1544,1574],{"__ignoreMap":107},[111,1489,1490,1492,1494,1497,1499,1501,1503,1506],{"class":113,"line":114},[111,1491,644],{"class":307},[111,1493,647],{"class":121},[111,1495,1496],{"class":117}," blurhashToDataUri",[111,1498,653],{"class":121},[111,1500,656],{"class":307},[111,1502,288],{"class":121},[111,1504,1505],{"class":280},"@unpic\u002Fplaceholder",[111,1507,663],{"class":121},[111,1509,1510],{"class":113,"line":146},[111,1511,259],{"emptyLinePlaceholder":258},[111,1513,1514,1517,1519,1521,1523,1525,1527,1529,1531,1533,1535,1537,1539,1542],{"class":113,"line":178},[111,1515,1516],{"class":117},"token",[111,1518,100],{"class":121},[111,1520,272],{"class":124},[111,1522,128],{"class":117},[111,1524,277],{"class":121},[111,1526,1458],{"class":280},[111,1528,277],{"class":121},[111,1530,204],{"class":121},[111,1532,1453],{"class":117},[111,1534,100],{"class":121},[111,1536,1458],{"class":117},[111,1538,100],{"class":121},[111,1540,1541],{"class":124},"toString",[111,1543,1399],{"class":117},[111,1545,1546,1548,1550,1552,1554,1556,1558,1560,1562,1564,1566,1568,1570,1572],{"class":113,"line":231},[111,1547,1516],{"class":117},[111,1549,100],{"class":121},[111,1551,272],{"class":124},[111,1553,128],{"class":117},[111,1555,277],{"class":121},[111,1557,1467],{"class":280},[111,1559,277],{"class":121},[111,1561,204],{"class":121},[111,1563,1453],{"class":117},[111,1565,100],{"class":121},[111,1567,1467],{"class":117},[111,1569,100],{"class":121},[111,1571,1541],{"class":124},[111,1573,1399],{"class":117},[111,1575,1576,1578,1580,1582,1584,1586,1589,1591,1593,1596,1599,1602,1605,1608,1610,1613,1615],{"class":113,"line":255},[111,1577,1516],{"class":117},[111,1579,100],{"class":121},[111,1581,272],{"class":124},[111,1583,128],{"class":117},[111,1585,277],{"class":121},[111,1587,1588],{"class":280},"style",[111,1590,277],{"class":121},[111,1592,204],{"class":121},[111,1594,1595],{"class":121}," `",[111,1597,1598],{"class":280},"background-size: cover; background-image: url(",[111,1600,1601],{"class":121},"${",[111,1603,1604],{"class":124},"blurhashToDataUri",[111,1606,1607],{"class":117},"(blurhash)",[111,1609,345],{"class":121},[111,1611,1612],{"class":280},");",[111,1614,726],{"class":121},[111,1616,333],{"class":117},[10,1618,1619,1620,1623],{},"Enfin, nous pouvons ajuster l'attribut ",[59,1621,1622],{},"src"," pour ajouter quelques paramètres de requête.",[102,1625,1627],{"className":636,"code":1626,"language":106,"meta":107,"style":107},"token.attrSet('src', `${src}?auto=format&fit=crop&q=80&w=800`)\n",[59,1628,1629],{"__ignoreMap":107},[111,1630,1631,1633,1635,1637,1639,1641,1643,1645,1647,1650,1652,1654,1657,1659],{"class":113,"line":114},[111,1632,1516],{"class":117},[111,1634,100],{"class":121},[111,1636,272],{"class":124},[111,1638,128],{"class":117},[111,1640,277],{"class":121},[111,1642,1622],{"class":280},[111,1644,277],{"class":121},[111,1646,204],{"class":121},[111,1648,1649],{"class":121}," `${",[111,1651,1622],{"class":117},[111,1653,345],{"class":121},[111,1655,1656],{"class":280},"?auto=format&fit=crop&q=80&w=800",[111,1658,726],{"class":121},[111,1660,333],{"class":117},[10,1662,1663,1664,1667],{},"Ces requêtes sont spécifiques à Unsplash, mais vous pourriez faire la même chose avec ",[28,1665,630],{"href":628,"rel":1666},[32]," ou n'importe quel service d'optimisation d'image.",[10,1669,1670,1671,1674],{},"Une petite note sur cette approche : elle fonctionne bien. Le parallélisme améliore légèrement les performances car la plupart des calculs se produisent dans la fonction ",[59,1672,1673],{},"encode"," qui transforme l'image en blurhash de manière synchrone. Sur mon ordinateur, récupérer l'image prend environ 100ms, tandis que l'encoder prend environ 3 secondes.",[48,1676,1678],{"id":1677},"aller-plus-loin","Aller Plus Loin",[10,1680,1681],{},"L'implémentation actuelle est simple, et c'est intentionnel. Maintenant que vous savez comment optimiser vos images et créer des règles personnalisées, vous pouvez facilement adapter cette approche à vos besoins, et même en créer de nouvelles qui correspondent à vos cas d'utilisation spécifiques.",[10,1683,1684],{},"Pour gérer le goulot d'étranglement des performances, vous avez de nombreuses possibilités. Trois d'entre elles sont :",[1686,1687,1688,1696,1706],"ol",{},[1689,1690,1691,1695],"li",{},[1692,1693,1694],"strong",{},"Pré-traitement local et stockage local"," : Si vous stockez des images dans votre code source, vous pouvez créer un script qui parcourra toutes les images et générera un fichier JSON contenant la largeur, la hauteur et le blurhash. Ensuite, dans la règle, vous n'aurez qu'à lire ce fichier, et plus aucun calcul n'est nécessaire. Cela réduira également le temps de build.",[1689,1697,1698,1701,1702],{},[1692,1699,1700],{},"Pré-traitement local et stockage distant"," : Si vos images sont stockées à distance, vous pouvez créer un script similaire qui récupérera les images, les traitera et générera le fichier JSON. Ce fichier peut être poussé vers un bucket pour une utilisation ultérieure. Dans la règle, au lieu de lire un fichier, vous pouvez le récupérer depuis le bucket. ",[1703,1704,1705],"em",{},"Sans règles asynchrones, ce n'était pas possible !",[1689,1707,1708,1711,1712],{},[1692,1709,1710],{},"Pré-traitement distant et stockage distant"," : Si vos images sont stockées à distance, vous pouvez créer un workflow qui sera déclenché chaque fois qu'un nouveau fichier est ajouté. Le workflow génère le fichier JSON et le stocke dans le même bucket. Dans la règle, vous pouvez récupérer le fichier JSON depuis le bucket et l'utiliser directement. ",[1703,1713,1714,1715,1720,1721,1726],{},"L'optimisation maximale. Avec les ",[28,1716,1719],{"href":1717,"rel":1718},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fbuckets\u002Fevent-notifications\u002F",[32],"notifications R2"," et les ",[28,1722,1725],{"href":1723,"rel":1724},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fqueues\u002Fevent-subscriptions\u002Fevents-schemas\u002F#r2",[32],"Queues",", c'est simple à implémenter.",[10,1728,1729],{},[1703,1730,1731],{},"Vous pourriez aussi créer un pool de workers en utilisant les threads Node, mais c'est excessif pour ce cas d'utilisation.",[10,1733,1734],{},"Je suis sûr qu'il existe de nombreuses autres façons d'automatiser l'optimisation des images. Maintenant, c'est à vous de trouver la meilleure approche qui correspond à votre flux de travail.",[10,1736,1737],{},"J'espère que vous avez apprécié l'article ! Si vous implémentez quelque chose de similaire, tenez-moi au courant !",[1588,1739,1740],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sQLHv, html code.shiki .sQLHv{--shiki-light:#90A4AE;--shiki-light-text-decoration:underline;--shiki-default:#EEFFFF;--shiki-default-text-decoration:underline;--shiki-dark:#BABED8;--shiki-dark-text-decoration:underline}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":107,"searchDepth":146,"depth":146,"links":1742},[1743,1744,1745,1746,1747],{"id":50,"depth":146,"text":51},{"id":361,"depth":146,"text":362},{"id":395,"depth":146,"text":396},{"id":621,"depth":146,"text":622},{"id":1677,"depth":146,"text":1678},"ca2372b3-15b3-4d66-9877-a4dd696580de","2025-11-19","Auparavant, markdown-it nécessitait un rendu et des règles synchrones. markdown-exit prend en charge le rendu et les règles asynchrones.",{},"\u002Ffr\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering","---\ncontentId: ca2372b3-15b3-4d66-9877-a4dd696580de\ntitle: Markdown Exit brise les règles avec le rendu asynchrone\ndescription: Auparavant, markdown-it nécessitait un rendu et des règles synchrones. markdown-exit prend en charge le rendu et les règles asynchrones.\ndate: 2025-11-19\n---\n\nCet article est la suite de \u003CLinkToArticle id=\"a505274a-14a6-471e-95a2-a3dbc64cd170\" \u002F> et se concentre sur un cas d'utilisation plus concret : l'optimisation automatique des images dans les fichiers Markdown.\n\nC'est très technique, et à moins que vous ne traitiez des fichiers Markdown, cela pourrait être un peu abstrait. Néanmoins, je vous encourage vivement à lire l'article précédent et celui-ci, car cela pourrait vous donner de bonnes idées.\n\n---\n\nPourquoi écrire un article dédié à ce sujet ? Parce que, jusqu'à présent, ce n'était pas possible. Le principal parseur Markdown dans le monde JavaScript est [markdown-it](https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it), et il est entièrement synchrone.\n\nCela pourrait ne pas être un problème, mais cela signifie que [toutes les règles doivent être synchrones](https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it\u002Fblob\u002Fmaster\u002Fdocs\u002Fdevelopment.md#i-need-async-rule-how-to-do-it) également. Vous ne pouvez pas avoir de règles asynchrones. Vous ne pouvez pas effectuer de fetch dans une règle, et vous ne pouvez rien paralléliser.\n\nUn petit récapitulatif est nécessaire pour s'assurer que nous sommes tous sur la même longueur d'onde.\n\n## Écrire un Parseur Markdown\n\nJe n'entrerai pas dans les détails, mais essentiellement, le parseur va diviser le texte Markdown en tokens puis appliquer des règles, qui sont essentiellement des fonctions, à ces tokens pour générer le HTML.\n\nPar exemple, le texte suivant `**bold text**` sera tokenisé en : `['strong_open', 'text', 'strong_close']` et les règles correspondantes généreront le HTML : `\u003Cstrong>bold text\u003C\u002Fstrong>`. Le token `strong_open` sera traité par la règle `strong_open`, qui retourne `\u003Cstrong>`, et ainsi de suite.\n\n\u003CCalloutReadMore\n  text=\"Explorez l'implémentation de la règle d'emphase dans markdown-exit\"\n  href=\"https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit\u002Fblob\u002F3e0cfeb7926355f6896fcd4dd7afd797e7f6689f\u002Fpackages\u002Fmarkdown-exit\u002Fsrc\u002Fparser\u002Finline\u002Frules\u002Femphasis.ts\"\n\u002F>\n\nLa règle dont nous parlons depuis le début transforme un token en sa représentation HTML correspondante. La partie intéressante est que vous pouvez attacher vos propres règles au parseur pour étendre ses fonctionnalités.\n\nImaginez que vous vouliez appliquer automatiquement l'attribut `loading=\"lazy\"` à toutes les images de vos fichiers Markdown. Vous pourriez écrire une règle qui cible la règle `image`.\n\n```ts\nmd.use((md) => {\n  const imageRule = md.renderer.rules.image!\n  md.renderer.rules.image = (tokens, idx, options, env, self) => {\n    const token = tokens[idx]\n\n    token.attrSet('loading', 'lazy') \u002F\u002F [!code highlight]\n\n    return imageRule(tokens, idx, options, env, self)\n  }\n})\n```\n\nMaintenant, le Markdown suivant `![alt text](image.jpg)` sera rendu comme `\u003Cimg src=\"image.jpg\" alt=\"alt text\" loading=\"lazy\" \u002F>`. C'est un rêve : appliquer automatiquement des changements aux fichiers sans les toucher.\n\n## Limitations Synchrones\n\nC'est un rêve jusqu'à ce que vous réalisiez que tout cela se passe de manière synchrone. Les règles sont appliquées une par une, sans aucun moyen de paralléliser le processus. Pour la règle de surlignage du code, cela peut réduire considérablement les performances car vous ne pouvez pas rendre tous les blocs de code en même temps, c'est un par un.\n\n> [!NOTE]\n> Anthony Fu en était bien conscient et il a créé [markdown-it-async](https:\u002F\u002Fgithub.com\u002Fantfu\u002Fmarkdown-it-async) pour rendre cela possible.\n\nEn plus de paralléliser le processus, les règles synchrones signifient que vous ne pouvez effectuer aucune opération asynchrone au sein d'une règle. Par exemple, vous ne pouvez pas récupérer de données depuis une API. Cela peut rapidement devenir limitant.\n\nMais, avec [markdown-exit](https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit), un remplacement direct et une réécriture complète de markdown-it en utilisant TypeScript, c'est désormais possible. Les règles asynchrones sont maintenant une réalité.\n\n## Optimiser les Images à la Volée\n\nDans [l'article précédent](.\u002Ferase-cls-by-automatically-optimizing-images-with-vite.md), nous avons exploité un plugin Vite pour appliquer automatiquement des optimisations aux images dans notre HTML. C'était un bon début utile.\n\nPour moi, ce n'était pas suffisant car je traite principalement des fichiers Markdown, et construire une règle personnalisée est un meilleur choix. C'est ce que nous allons voir maintenant.\n\nTout le code source est disponible sur GitHub : \u003CGitHubLink repo=\"Barbapapazes\u002Fmarkdown-exit-unpic\" \u002F>. Pour garder cet article simple, je ne montrerai pas le code non lié comme la configuration du projet.\n\nImaginons que nous ayons un fichier `src\u002Findex.ts`. Ce fichier est notre script principal, et le but est de rendre du Markdown brut en HTML tout en optimisant les images.\n\nNous voulons exécuter la commande suivante :\n\n```sh\nnode src\u002Findex.ts\n```\n\navec ce contenu Markdown :\n\n```md\n# Hello World\n\n![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)\n```\n\net avoir la sortie suivante :\n\n```html\n\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666?auto=format&amp;fit=crop&amp;q=80&amp;w=800\" alt=\"This is a placeholder\" loading=\"lazy\" width=\"2567\" height=\"2000\" style=\"background-size: cover; background-image: url(data:image\u002Fbmp;base64,Qk32BAAAAAAAADYAAAAoAAAACAAAAAgAAAABABgAAAAAAMAAAAATCwAAEwsAAAAAAAAAAAAACQYICgcIDAoKEQ8PFBISFRISEQ4OCgYGCAYGDAoKExESGhgYHRsbHBoaFhMTDAgICAcGDgwMGBcXIB8fIyIiISAgGhgYDgoLCQcGDw0NGRgYIiEhJiUlJCIiHBoaDwsMCgcHDgwMFxYWHx4eIyIiISAgGhgYDgsLCQYGDAkJEhAQGBcXGxoaGxkZFRMTDAgICAUECQYGDAkJEA0OEhAQEhAQDwwMCQYFBwUEBwUECQYGCwcJDQoKDQsKCwgHCQUE);\">\u003C\u002Fp>\n```\n\n## Construire une Règle Asynchrone Personnalisée\n\nLa logique centrale de la règle ne différera pas de ce que nous avons construit dans le plugin Vite. Au lieu de lire le fichier depuis le système de fichiers local, nous le récupérerons depuis Unsplash pour simuler un bucket distant. Ensuite, nous récupérerons la largeur et la hauteur, et générerons le blurhash. À la fin, nous appliquerons également quelques paramètres de requête personnalisés pour éviter que le client ne charge l'image complète, comme vous pouvez le faire avec [Cloudflare Images](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F).\n\nD'abord, nous devons créer une nouvelle instance de markdown-exit et rendre le Markdown. C'est vraiment simple.\n\n```ts\nimport { createMarkdownExit } from 'markdown-exit'\n\nconst md = createMarkdownExit()\n\nasync function render() {\n  const html = await md.renderAsync(`# Hello World\n\n![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)`)\n\n  console.log(html)\n}\n\nrender()\n```\n\nRien d'extraordinaire dans ce script. Cependant, la sortie est la suivante :\n\n```html\n\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666\" alt=\"This is a placeholder\">\u003C\u002Fp>\n```\n\nIl manque quelques parties, mais c'est un bon début.\n\nPour changer le comportement de rendu de l'image, nous allons nous accrocher à la règle `image`, ajuster le token à nos besoins, et utiliser l'implémentation par défaut pour générer la sortie.\n\nPour créer une nouvelle règle, nous pouvons utiliser la fonction `use`. C'est comme créer un plugin Vue. Ensuite, liez la fonction que nous voulons à la règle `image`.\n\n```ts\nimport type { MarkdownExit } from 'markdown-exit'\nimport { createMarkdownExit } from 'markdown-exit'\n\nconst md = createMarkdownExit()\n\nmd.use((md: MarkdownExit) => {\n  const imageRule = md.renderer.rules.image!\n  md.renderer.rules.image = async (tokens, idx, options, env, self) => {\n    return imageRule(tokens, idx, options, env, self)\n  }\n})\n\nasync function render() {\n  \u002F\u002F ...\n}\n\nrender()\n```\n\nNotre fonction ne fait rien à part appeler la règle `image` originale, ce qui est déjà une bonne étape car nous ne voulons pas la réimplémenter nous-mêmes.\n\nEnsuite, nous pouvons saisir le token actuel et essayer de lui ajouter un attribut.\n\nLa variable `tokens` contient tous les tokens disponibles. C'est un tableau de tokens. Pour accéder à l'actuel, nous devons utiliser la variable `idx`, qui est l'index actuel du token.\n\n```ts\nconst token = tokens[idx]\n```\n\nMaintenant, nous pouvons modifier le token. Par exemple, nous pouvons ajouter un attribut `loading` avec la valeur `lazy`.\n\n```ts\nmd.renderer.rules.image = async (tokens, idx, options, env, self) => {\n  const token = tokens[idx]\n\n  token.attrSet('loading', 'lazy')\n\n  return imageRule(tokens, idx, options, env, self)\n}\n```\n\nLa sortie de notre script est maintenant la suivante :\n\n```html\n\u003Ch1>Hello World\u003C\u002Fh1>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666\" alt=\"This is a placeholder\" loading=\"lazy\">\u003C\u002Fp>\n```\n\nFacile, non ?\n\nBasé sur notre comportement attendu, nous devons ajouter la largeur, la hauteur, le blurhash, et quelques paramètres de requête à l'image. Pour ce faire, nous utiliserons la même logique que dans \u003CLinkToArticle id=\"a505274a-14a6-471e-95a2-a3dbc64cd170\" \u002F>, sauf que nous récupérerons l'image depuis Unsplash, comme vous pourriez le faire avec n'importe quel bucket distant.\n\n```ts\nconst img = await fetch(src).then(res => res.bytes())\nconst data = await getPixels(img)\n\nconst blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n```\n\nMaintenant, nous pouvons définir les attributs width, height et style sur le token.\n\n```ts\nimport { blurhashToDataUri } from '@unpic\u002Fplaceholder'\n\ntoken.attrSet('width', data.width.toString())\ntoken.attrSet('height', data.height.toString())\ntoken.attrSet('style', `background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});`)\n```\n\nEnfin, nous pouvons ajuster l'attribut `src` pour ajouter quelques paramètres de requête.\n\n```ts\ntoken.attrSet('src', `${src}?auto=format&fit=crop&q=80&w=800`)\n```\n\nCes requêtes sont spécifiques à Unsplash, mais vous pourriez faire la même chose avec [Cloudflare Images](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F) ou n'importe quel service d'optimisation d'image.\n\nUne petite note sur cette approche : elle fonctionne bien. Le parallélisme améliore légèrement les performances car la plupart des calculs se produisent dans la fonction `encode` qui transforme l'image en blurhash de manière synchrone. Sur mon ordinateur, récupérer l'image prend environ 100ms, tandis que l'encoder prend environ 3 secondes.\n\n## Aller Plus Loin\n\nL'implémentation actuelle est simple, et c'est intentionnel. Maintenant que vous savez comment optimiser vos images et créer des règles personnalisées, vous pouvez facilement adapter cette approche à vos besoins, et même en créer de nouvelles qui correspondent à vos cas d'utilisation spécifiques.\n\nPour gérer le goulot d'étranglement des performances, vous avez de nombreuses possibilités. Trois d'entre elles sont :\n\n1. **Pré-traitement local et stockage local** : Si vous stockez des images dans votre code source, vous pouvez créer un script qui parcourra toutes les images et générera un fichier JSON contenant la largeur, la hauteur et le blurhash. Ensuite, dans la règle, vous n'aurez qu'à lire ce fichier, et plus aucun calcul n'est nécessaire. Cela réduira également le temps de build.\n2. **Pré-traitement local et stockage distant** : Si vos images sont stockées à distance, vous pouvez créer un script similaire qui récupérera les images, les traitera et générera le fichier JSON. Ce fichier peut être poussé vers un bucket pour une utilisation ultérieure. Dans la règle, au lieu de lire un fichier, vous pouvez le récupérer depuis le bucket. _Sans règles asynchrones, ce n'était pas possible !_\n3. **Pré-traitement distant et stockage distant** : Si vos images sont stockées à distance, vous pouvez créer un workflow qui sera déclenché chaque fois qu'un nouveau fichier est ajouté. Le workflow génère le fichier JSON et le stocke dans le même bucket. Dans la règle, vous pouvez récupérer le fichier JSON depuis le bucket et l'utiliser directement. _L'optimisation maximale. Avec les [notifications R2](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fbuckets\u002Fevent-notifications\u002F) et les [Queues](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fqueues\u002Fevent-subscriptions\u002Fevents-schemas\u002F#r2), c'est simple à implémenter._\n\n_Vous pourriez aussi créer un pool de workers en utilisant les threads Node, mais c'est excessif pour ce cas d'utilisation._\n\nJe suis sûr qu'il existe de nombreuses autres façons d'automatiser l'optimisation des images. Maintenant, c'est à vous de trouver la meilleure approche qui correspond à votre flux de travail.\n\nJ'espère que vous avez apprécié l'article ! Si vous implémentez quelque chose de similaire, tenez-moi au courant !\n",null,{"title":5,"description":1750},{"loc":1752},"fr\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering","R21CdeKrTjA9KIP10sk5Po8pk35TULF20HLkHl76u2I",1790086903207]