[{"data":1,"prerenderedAt":1760},["ShallowReactive",2],{"post-\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering":3,"post-link-en-external":1755},{"id":4,"title":5,"body":6,"contentId":1749,"date":1750,"description":1751,"extension":118,"meta":1752,"navigation":259,"path":1753,"rawbody":1754,"robots":1755,"schemaOrg":1755,"seo":1756,"sitemap":1757,"stem":1758,"__hash__":1759},"posts\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering.md","Markdown Exit Breaks the Rules With Async Rendering",{"type":7,"value":8,"toc":1742},"minimark",[9,18,21,24,35,44,47,52,55,82,1739],[10,11,12,13],"p",{},"This article is the continuation of ",[14,15,17],"link-to-article",{"id":16},"264d08e1-fb05-40c0-b298-0ba1dbbd3f14"," and focuses on a more concrete use case: automatically optimizing images in Markdown files.",[10,19,20],{},"It is highly technical, and unless you're dealing with Markdown files, this could be a little abstract. Nevertheless, I highly encourage you to read both the previous and the current article, as it could give you some nice ideas.",[22,23],"hr",{},[10,25,26,27,34],{},"Why write a dedicated article about it? Because, until now, this wasn't possible. The main Markdown parser in the JavaScript world is ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",[32],"nofollow","markdown-it",", and it is fully synchronous.",[10,36,37,38,43],{},"This might not be an issue, but it means that ",[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],"all rules must be synchronous"," as well. You can't have asynchronous rules. You can't perform a fetch within a rule, and you can't parallelize anything.",[10,45,46],{},"A little recap is needed to make sure we're all on the same page.",[48,49,51],"h2",{"id":50},"writing-a-markdown-parser","Writing a Markdown parser",[10,53,54],{},"I won't go into details, but essentially, the parser will split the Markdown text into tokens and then apply rules, which are basically functions, to those tokens to generate the HTML.",[10,56,57,58,62,63,66,67,70,71,74,75,77,78,81],{},"For example, the following text ",[59,60,61],"code",{},"**bold text**"," will be tokenized into: ",[59,64,65],{},"['strong_open', 'text', 'strong_close']"," and the corresponding rules will generate the HTML: ",[59,68,69],{},"\u003Cstrong>bold text\u003C\u002Fstrong>",". The token ",[59,72,73],{},"strong_open"," will be processed by the ",[59,76,73],{}," rule, which returns ",[59,79,80],{},"\u003Cstrong>",", and so on.",[83,84,87,90,101,349,360,364,367,382,385,394,398,406,409,417,424,427,443,446,481,484,620,624,632,635,783,786,843,846,852,860,1099,1105,1108,1117,1134,1142,1277,1280,1347,1350,1356,1481,1484,1618,1625,1662,1669,1676,1680,1683,1686,1728,1733,1736],"callout-read-more",{"text":85,"href":86},"Explore the emphasis rule implementation in markdown-exit","https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit\u002Fblob\u002F3e0cfeb7926355f6896fcd4dd7afd797e7f6689f\u002Fpackages\u002Fmarkdown-exit\u002Fsrc\u002Fparser\u002Finline\u002Frules\u002Femphasis.ts",[10,88,89],{},"The rule we've been talking about since the beginning transforms a token into its corresponding HTML representation. The interesting part is that you can attach your own rules to the parser to extend its functionality.",[10,91,92,93,96,97,100],{},"Imagine you want to automatically apply the ",[59,94,95],{},"loading=\"lazy\""," attribute to all images in your Markdown files. You could write a rule that targets the ",[59,98,99],{},"image"," rule.",[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,145,177,230,254,261,298,303,335,341],{"__ignoreMap":107},[111,112,115,119,123,127,130,132,135,138,142],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sTEyZ","md",[111,120,122],{"class":121},"sMK4o",".",[111,124,126],{"class":125},"s2Zo4","use",[111,128,129],{"class":117},"(",[111,131,129],{"class":121},[111,133,118],{"class":134},"sHdIc",[111,136,137],{"class":121},")",[111,139,141],{"class":140},"spNyl"," =>",[111,143,144],{"class":121}," {\n",[111,146,148,151,154,157,160,162,165,167,170,172,174],{"class":113,"line":147},2,[111,149,150],{"class":140},"  const",[111,152,153],{"class":117}," imageRule",[111,155,156],{"class":121}," =",[111,158,159],{"class":117}," md",[111,161,122],{"class":121},[111,163,164],{"class":117},"renderer",[111,166,122],{"class":121},[111,168,169],{"class":117},"rules",[111,171,122],{"class":121},[111,173,99],{"class":117},[111,175,176],{"class":121},"!\n",[111,178,180,183,185,187,189,191,193,195,197,200,203,206,209,211,214,216,219,221,224,226,228],{"class":113,"line":179},3,[111,181,182],{"class":117},"  md",[111,184,122],{"class":121},[111,186,164],{"class":117},[111,188,122],{"class":121},[111,190,169],{"class":117},[111,192,122],{"class":121},[111,194,99],{"class":125},[111,196,156],{"class":121},[111,198,199],{"class":121}," (",[111,201,202],{"class":134},"tokens",[111,204,205],{"class":121},",",[111,207,208],{"class":134}," idx",[111,210,205],{"class":121},[111,212,213],{"class":134}," options",[111,215,205],{"class":121},[111,217,218],{"class":134}," env",[111,220,205],{"class":121},[111,222,223],{"class":134}," self",[111,225,137],{"class":121},[111,227,141],{"class":140},[111,229,144],{"class":121},[111,231,233,236,239,241,244,248,251],{"class":113,"line":232},4,[111,234,235],{"class":140},"    const",[111,237,238],{"class":117}," token",[111,240,156],{"class":121},[111,242,243],{"class":117}," tokens",[111,245,247],{"class":246},"swJcz","[",[111,249,250],{"class":117},"idx",[111,252,253],{"class":246},"]\n",[111,255,257],{"class":113,"line":256},5,[111,258,260],{"emptyLinePlaceholder":259},true,"\n",[111,262,266,269,271,274,276,279,283,285,287,290,293,295],{"class":263,"line":265},[113,264],"highlighted",6,[111,267,268],{"class":117},"    token",[111,270,122],{"class":121},[111,272,273],{"class":125},"attrSet",[111,275,129],{"class":246},[111,277,278],{"class":121},"'",[111,280,282],{"class":281},"sfazB","loading",[111,284,278],{"class":121},[111,286,205],{"class":121},[111,288,289],{"class":121}," '",[111,291,292],{"class":281},"lazy",[111,294,278],{"class":121},[111,296,297],{"class":246},") ",[111,299,301],{"class":113,"line":300},7,[111,302,260],{"emptyLinePlaceholder":259},[111,304,306,310,312,314,316,318,320,322,324,326,328,330,332],{"class":113,"line":305},8,[111,307,309],{"class":308},"s7zQu","    return",[111,311,153],{"class":125},[111,313,129],{"class":246},[111,315,202],{"class":117},[111,317,205],{"class":121},[111,319,208],{"class":117},[111,321,205],{"class":121},[111,323,213],{"class":117},[111,325,205],{"class":121},[111,327,218],{"class":117},[111,329,205],{"class":121},[111,331,223],{"class":117},[111,333,334],{"class":246},")\n",[111,336,338],{"class":113,"line":337},9,[111,339,340],{"class":121},"  }\n",[111,342,344,347],{"class":113,"line":343},10,[111,345,346],{"class":121},"}",[111,348,334],{"class":117},[10,350,351,352,355,356,359],{},"Now, the following Markdown ",[59,353,354],{},"![alt text](image.jpg)"," will be rendered as ",[59,357,358],{},"\u003Cimg src=\"image.jpg\" alt=\"alt text\" loading=\"lazy\" \u002F>",". This is a dream: automatically apply changes to files without touching them.",[48,361,363],{"id":362},"synchronous-limitations","Synchronous limitations",[10,365,366],{},"This is a dream until you realize that all of this happens synchronously. Rules are applied one by one, without any way to parallelize the process. For the highlighting rule, this can significantly reduce performance because you can't render all the code blocks at once—it's one at a time.",[368,369,370],"blockquote",{},[10,371,372,375,376,381],{},[111,373,374],{},"!NOTE","\nAnthony Fu was well aware of this and he created ",[28,377,380],{"href":378,"rel":379},"https:\u002F\u002Fgithub.com\u002Fantfu\u002Fmarkdown-it-async",[32],"markdown-it-async"," as a thin wrapper to make it possible.",[10,383,384],{},"Apart from parallelizing the process, synchronous rules mean that you can't perform any asynchronous operations within a rule. For example, you can't fetch data from an API. This can quickly become limiting.",[10,386,387,388,393],{},"But, with ",[28,389,392],{"href":390,"rel":391},"https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit",[32],"markdown-exit",", a drop-in replacement and complete rewrite of markdown-it using TypeScript, this is now possible. Asynchronous rules are now a reality.",[48,395,397],{"id":396},"optimizing-images-on-the-fly","Optimizing images on the fly",[10,399,400,401,405],{},"In ",[28,402,404],{"href":403},"\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite","the previous article",", we leveraged a Vite plugin to automatically apply optimizations to images in our HTML. This was a good and useful start.",[10,407,408],{},"For me, this wasn't enough because I mostly deal with Markdown files, and building a custom rule is a better choice. That's what we'll see now.",[10,410,411,412],{},"All the source code is available on GitHub: ",[413,414,416],"git-hub-link",{"repo":415},"Barbapapazes\u002Fmarkdown-exit-unpic",". To keep this article simple, I won't show unrelated code like the project setup.",[10,418,419,420,423],{},"Let's imagine we have a ",[59,421,422],{},"src\u002Findex.ts"," file. This file is our main script, and the goal is to render raw Markdown to HTML while optimizing images.",[10,425,426],{},"We want to run the following command:",[102,428,432],{"className":429,"code":430,"language":431,"meta":107,"style":107},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","node src\u002Findex.ts\n","sh",[59,433,434],{"__ignoreMap":107},[111,435,436,440],{"class":113,"line":114},[111,437,439],{"class":438},"sBMFI","node",[111,441,442],{"class":281}," src\u002Findex.ts\n",[10,444,445],{},"with this Markdown content:",[102,447,450],{"className":448,"code":449,"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,451,452,460,464],{"__ignoreMap":107},[111,453,454,457],{"class":113,"line":114},[111,455,456],{"class":121},"# ",[111,458,459],{"class":438},"Hello World\n",[111,461,462],{"class":113,"line":147},[111,463,260],{"emptyLinePlaceholder":259},[111,465,466,469,472,475,479],{"class":113,"line":179},[111,467,468],{"class":121},"![",[111,470,471],{"class":281},"This is a placeholder",[111,473,474],{"class":121},"](",[111,476,478],{"class":477},"sQLHv","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666",[111,480,334],{"class":121},[10,482,483],{},"and have the following output:",[102,485,489],{"className":486,"code":487,"language":488,"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,490,491,513],{"__ignoreMap":107},[111,492,493,496,499,502,505,508,510],{"class":113,"line":114},[111,494,495],{"class":121},"\u003C",[111,497,498],{"class":246},"h1",[111,500,501],{"class":121},">",[111,503,504],{"class":117},"Hello World",[111,506,507],{"class":121},"\u003C\u002F",[111,509,498],{"class":246},[111,511,512],{"class":121},">\n",[111,514,515,517,519,522,525,528,531,534,537,540,543,545,548,550,553,555,558,560,562,564,566,569,571,573,575,577,580,582,584,587,589,592,594,596,599,601,604,606,608,611,613,616,618],{"class":113,"line":147},[111,516,495],{"class":121},[111,518,10],{"class":246},[111,520,521],{"class":121},">\u003C",[111,523,524],{"class":246},"img",[111,526,527],{"class":140}," src",[111,529,530],{"class":121},"=",[111,532,533],{"class":121},"\"",[111,535,536],{"class":281},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666?auto=format",[111,538,539],{"class":117},"&amp;",[111,541,542],{"class":281},"fit=crop",[111,544,539],{"class":117},[111,546,547],{"class":281},"q=80",[111,549,539],{"class":117},[111,551,552],{"class":281},"w=800",[111,554,533],{"class":121},[111,556,557],{"class":140}," alt",[111,559,530],{"class":121},[111,561,533],{"class":121},[111,563,471],{"class":281},[111,565,533],{"class":121},[111,567,568],{"class":140}," loading",[111,570,530],{"class":121},[111,572,533],{"class":121},[111,574,292],{"class":281},[111,576,533],{"class":121},[111,578,579],{"class":140}," width",[111,581,530],{"class":121},[111,583,533],{"class":121},[111,585,586],{"class":281},"2567",[111,588,533],{"class":121},[111,590,591],{"class":140}," height",[111,593,530],{"class":121},[111,595,533],{"class":121},[111,597,598],{"class":281},"2000",[111,600,533],{"class":121},[111,602,603],{"class":140}," style",[111,605,530],{"class":121},[111,607,533],{"class":121},[111,609,610],{"class":281},"background-size: cover; background-image: url(data:image\u002Fbmp;base64,Qk32BAAAAAAAADYAAAAoAAAACAAAAAgAAAABABgAAAAAAMAAAAATCwAAEwsAAAAAAAAAAAAACQYICgcIDAoKEQ8PFBISFRISEQ4OCgYGCAYGDAoKExESGhgYHRsbHBoaFhMTDAgICAcGDgwMGBcXIB8fIyIiISAgGhgYDgoLCQcGDw0NGRgYIiEhJiUlJCIiHBoaDwsMCgcHDgwMFxYWHx4eIyIiISAgGhgYDgsLCQYGDAkJEhAQGBcXGxoaGxkZFRMTDAgICAUECQYGDAkJEA0OEhAQEhAQDwwMCQYFBwUEBwUECQYGCwcJDQoKDQsKCwgHCQUE);",[111,612,533],{"class":121},[111,614,615],{"class":121},">\u003C\u002F",[111,617,10],{"class":246},[111,619,512],{"class":121},[48,621,623],{"id":622},"building-a-custom-async-rule","Building a custom async rule",[10,625,626,627,122],{},"The core logic of the rule won't differ from what we've built in the Vite plugin. Instead of reading the file from the local filesystem, we will fetch it from Unsplash to simulate a remote bucket. Then, we will retrieve the width and height, and generate the blurhash. At the end, we will also apply some custom query parameters to avoid the client loading the full image, like you can do with ",[28,628,631],{"href":629,"rel":630},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F",[32],"Cloudflare Images",[10,633,634],{},"First, we need to create a new markdown-exit instance and render the Markdown. This is really straightforward.",[102,636,639],{"className":637,"code":638,"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,640,641,665,669,684,688,704,731,735,744,748,764,770,775],{"__ignoreMap":107},[111,642,643,646,649,652,655,658,660,662],{"class":113,"line":114},[111,644,645],{"class":308},"import",[111,647,648],{"class":121}," {",[111,650,651],{"class":117}," createMarkdownExit",[111,653,654],{"class":121}," }",[111,656,657],{"class":308}," from",[111,659,289],{"class":121},[111,661,392],{"class":281},[111,663,664],{"class":121},"'\n",[111,666,667],{"class":113,"line":147},[111,668,260],{"emptyLinePlaceholder":259},[111,670,671,674,677,679,681],{"class":113,"line":179},[111,672,673],{"class":140},"const",[111,675,676],{"class":117}," md ",[111,678,530],{"class":121},[111,680,651],{"class":125},[111,682,683],{"class":117},"()\n",[111,685,686],{"class":113,"line":232},[111,687,260],{"emptyLinePlaceholder":259},[111,689,690,693,696,699,702],{"class":113,"line":256},[111,691,692],{"class":140},"async",[111,694,695],{"class":140}," function",[111,697,698],{"class":125}," render",[111,700,701],{"class":121},"()",[111,703,144],{"class":121},[111,705,706,708,711,713,716,718,720,723,725,728],{"class":113,"line":265},[111,707,150],{"class":140},[111,709,710],{"class":117}," html",[111,712,156],{"class":121},[111,714,715],{"class":308}," await",[111,717,159],{"class":117},[111,719,122],{"class":121},[111,721,722],{"class":125},"renderAsync",[111,724,129],{"class":246},[111,726,727],{"class":121},"`",[111,729,730],{"class":281},"# Hello World\n",[111,732,733],{"class":113,"line":300},[111,734,260],{"emptyLinePlaceholder":259},[111,736,737,740,742],{"class":113,"line":305},[111,738,739],{"class":281},"![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)",[111,741,727],{"class":121},[111,743,334],{"class":246},[111,745,746],{"class":113,"line":337},[111,747,260],{"emptyLinePlaceholder":259},[111,749,750,753,755,758,760,762],{"class":113,"line":343},[111,751,752],{"class":117},"  console",[111,754,122],{"class":121},[111,756,757],{"class":125},"log",[111,759,129],{"class":246},[111,761,488],{"class":117},[111,763,334],{"class":246},[111,765,767],{"class":113,"line":766},11,[111,768,769],{"class":121},"}\n",[111,771,773],{"class":113,"line":772},12,[111,774,260],{"emptyLinePlaceholder":259},[111,776,778,781],{"class":113,"line":777},13,[111,779,780],{"class":125},"render",[111,782,683],{"class":117},[10,784,785],{},"Nothing fancy in this script. However, the output is the following:",[102,787,789],{"className":486,"code":788,"language":488,"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,790,791,807],{"__ignoreMap":107},[111,792,793,795,797,799,801,803,805],{"class":113,"line":114},[111,794,495],{"class":121},[111,796,498],{"class":246},[111,798,501],{"class":121},[111,800,504],{"class":117},[111,802,507],{"class":121},[111,804,498],{"class":246},[111,806,512],{"class":121},[111,808,809,811,813,815,817,819,821,823,825,827,829,831,833,835,837,839,841],{"class":113,"line":147},[111,810,495],{"class":121},[111,812,10],{"class":246},[111,814,521],{"class":121},[111,816,524],{"class":246},[111,818,527],{"class":140},[111,820,530],{"class":121},[111,822,533],{"class":121},[111,824,478],{"class":281},[111,826,533],{"class":121},[111,828,557],{"class":140},[111,830,530],{"class":121},[111,832,533],{"class":121},[111,834,471],{"class":281},[111,836,533],{"class":121},[111,838,615],{"class":121},[111,840,10],{"class":246},[111,842,512],{"class":121},[10,844,845],{},"There are some missing parts, but that's a good start.",[10,847,848,849,851],{},"To change the image rendering behavior, we will hook into the ",[59,850,99],{}," rule, adjust the token to our needs, and use the default implementation to generate the output.",[10,853,854,855,857,858,100],{},"To create a new rule, we can use the ",[59,856,126],{}," function. It's like creating a Vue plugin. Then, bind the function we want to the ",[59,859,99],{},[102,861,863],{"className":637,"code":862,"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,864,865,887,905,909,921,925,950,974,1021,1049,1053,1059,1063,1075,1082,1087,1092],{"__ignoreMap":107},[111,866,867,869,872,874,877,879,881,883,885],{"class":113,"line":114},[111,868,645],{"class":308},[111,870,871],{"class":308}," type",[111,873,648],{"class":121},[111,875,876],{"class":117}," MarkdownExit",[111,878,654],{"class":121},[111,880,657],{"class":308},[111,882,289],{"class":121},[111,884,392],{"class":281},[111,886,664],{"class":121},[111,888,889,891,893,895,897,899,901,903],{"class":113,"line":147},[111,890,645],{"class":308},[111,892,648],{"class":121},[111,894,651],{"class":117},[111,896,654],{"class":121},[111,898,657],{"class":308},[111,900,289],{"class":121},[111,902,392],{"class":281},[111,904,664],{"class":121},[111,906,907],{"class":113,"line":179},[111,908,260],{"emptyLinePlaceholder":259},[111,910,911,913,915,917,919],{"class":113,"line":232},[111,912,673],{"class":140},[111,914,676],{"class":117},[111,916,530],{"class":121},[111,918,651],{"class":125},[111,920,683],{"class":117},[111,922,923],{"class":113,"line":256},[111,924,260],{"emptyLinePlaceholder":259},[111,926,927,929,931,933,935,937,939,942,944,946,948],{"class":113,"line":265},[111,928,118],{"class":117},[111,930,122],{"class":121},[111,932,126],{"class":125},[111,934,129],{"class":117},[111,936,129],{"class":121},[111,938,118],{"class":134},[111,940,941],{"class":121},":",[111,943,876],{"class":438},[111,945,137],{"class":121},[111,947,141],{"class":140},[111,949,144],{"class":121},[111,951,952,954,956,958,960,962,964,966,968,970,972],{"class":113,"line":300},[111,953,150],{"class":140},[111,955,153],{"class":117},[111,957,156],{"class":121},[111,959,159],{"class":117},[111,961,122],{"class":121},[111,963,164],{"class":117},[111,965,122],{"class":121},[111,967,169],{"class":117},[111,969,122],{"class":121},[111,971,99],{"class":117},[111,973,176],{"class":121},[111,975,976,978,980,982,984,986,988,990,992,995,997,999,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019],{"class":113,"line":305},[111,977,182],{"class":117},[111,979,122],{"class":121},[111,981,164],{"class":117},[111,983,122],{"class":121},[111,985,169],{"class":117},[111,987,122],{"class":121},[111,989,99],{"class":125},[111,991,156],{"class":121},[111,993,994],{"class":140}," async",[111,996,199],{"class":121},[111,998,202],{"class":134},[111,1000,205],{"class":121},[111,1002,208],{"class":134},[111,1004,205],{"class":121},[111,1006,213],{"class":134},[111,1008,205],{"class":121},[111,1010,218],{"class":134},[111,1012,205],{"class":121},[111,1014,223],{"class":134},[111,1016,137],{"class":121},[111,1018,141],{"class":140},[111,1020,144],{"class":121},[111,1022,1023,1025,1027,1029,1031,1033,1035,1037,1039,1041,1043,1045,1047],{"class":113,"line":337},[111,1024,309],{"class":308},[111,1026,153],{"class":125},[111,1028,129],{"class":246},[111,1030,202],{"class":117},[111,1032,205],{"class":121},[111,1034,208],{"class":117},[111,1036,205],{"class":121},[111,1038,213],{"class":117},[111,1040,205],{"class":121},[111,1042,218],{"class":117},[111,1044,205],{"class":121},[111,1046,223],{"class":117},[111,1048,334],{"class":246},[111,1050,1051],{"class":113,"line":343},[111,1052,340],{"class":121},[111,1054,1055,1057],{"class":113,"line":766},[111,1056,346],{"class":121},[111,1058,334],{"class":117},[111,1060,1061],{"class":113,"line":772},[111,1062,260],{"emptyLinePlaceholder":259},[111,1064,1065,1067,1069,1071,1073],{"class":113,"line":777},[111,1066,692],{"class":140},[111,1068,695],{"class":140},[111,1070,698],{"class":125},[111,1072,701],{"class":121},[111,1074,144],{"class":121},[111,1076,1078],{"class":113,"line":1077},14,[111,1079,1081],{"class":1080},"sHwdD","  \u002F\u002F ...\n",[111,1083,1085],{"class":113,"line":1084},15,[111,1086,769],{"class":121},[111,1088,1090],{"class":113,"line":1089},16,[111,1091,260],{"emptyLinePlaceholder":259},[111,1093,1095,1097],{"class":113,"line":1094},17,[111,1096,780],{"class":125},[111,1098,683],{"class":117},[10,1100,1101,1102,1104],{},"Our function does nothing apart from calling the original ",[59,1103,99],{}," rule, which is already a good step because we don't want to reimplement it ourselves.",[10,1106,1107],{},"Then, we can grab the current token and try to add an attribute to it.",[10,1109,1110,1111,1113,1114,1116],{},"The ",[59,1112,202],{}," variable contains all available tokens. It's an array of tokens. To access the current one, we need to use the ",[59,1115,250],{}," variable, which is the current index of the token.",[102,1118,1120],{"className":637,"code":1119,"language":106,"meta":107,"style":107},"const token = tokens[idx]\n",[59,1121,1122],{"__ignoreMap":107},[111,1123,1124,1126,1129,1131],{"class":113,"line":114},[111,1125,673],{"class":140},[111,1127,1128],{"class":117}," token ",[111,1130,530],{"class":121},[111,1132,1133],{"class":117}," tokens[idx]\n",[10,1135,1136,1137,1139,1140,122],{},"Now, we can modify the token. For example, we can add a ",[59,1138,282],{}," attribute with the value ",[59,1141,292],{},[102,1143,1145],{"className":637,"code":1144,"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,1146,1147,1193,1209,1213,1240,1244,1273],{"__ignoreMap":107},[111,1148,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183,1185,1187,1189,1191],{"class":113,"line":114},[111,1150,118],{"class":117},[111,1152,122],{"class":121},[111,1154,164],{"class":117},[111,1156,122],{"class":121},[111,1158,169],{"class":117},[111,1160,122],{"class":121},[111,1162,99],{"class":125},[111,1164,156],{"class":121},[111,1166,994],{"class":140},[111,1168,199],{"class":121},[111,1170,202],{"class":134},[111,1172,205],{"class":121},[111,1174,208],{"class":134},[111,1176,205],{"class":121},[111,1178,213],{"class":134},[111,1180,205],{"class":121},[111,1182,218],{"class":134},[111,1184,205],{"class":121},[111,1186,223],{"class":134},[111,1188,137],{"class":121},[111,1190,141],{"class":140},[111,1192,144],{"class":121},[111,1194,1195,1197,1199,1201,1203,1205,1207],{"class":113,"line":147},[111,1196,150],{"class":140},[111,1198,238],{"class":117},[111,1200,156],{"class":121},[111,1202,243],{"class":117},[111,1204,247],{"class":246},[111,1206,250],{"class":117},[111,1208,253],{"class":246},[111,1210,1211],{"class":113,"line":179},[111,1212,260],{"emptyLinePlaceholder":259},[111,1214,1215,1218,1220,1222,1224,1226,1228,1230,1232,1234,1236,1238],{"class":113,"line":232},[111,1216,1217],{"class":117},"  token",[111,1219,122],{"class":121},[111,1221,273],{"class":125},[111,1223,129],{"class":246},[111,1225,278],{"class":121},[111,1227,282],{"class":281},[111,1229,278],{"class":121},[111,1231,205],{"class":121},[111,1233,289],{"class":121},[111,1235,292],{"class":281},[111,1237,278],{"class":121},[111,1239,334],{"class":246},[111,1241,1242],{"class":113,"line":256},[111,1243,260],{"emptyLinePlaceholder":259},[111,1245,1246,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1269,1271],{"class":113,"line":265},[111,1247,1248],{"class":308},"  return",[111,1250,153],{"class":125},[111,1252,129],{"class":246},[111,1254,202],{"class":117},[111,1256,205],{"class":121},[111,1258,208],{"class":117},[111,1260,205],{"class":121},[111,1262,213],{"class":117},[111,1264,205],{"class":121},[111,1266,218],{"class":117},[111,1268,205],{"class":121},[111,1270,223],{"class":117},[111,1272,334],{"class":246},[111,1274,1275],{"class":113,"line":300},[111,1276,769],{"class":121},[10,1278,1279],{},"The output of our script is now the following:",[102,1281,1283],{"className":486,"code":1282,"language":488,"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,1284,1285,1301],{"__ignoreMap":107},[111,1286,1287,1289,1291,1293,1295,1297,1299],{"class":113,"line":114},[111,1288,495],{"class":121},[111,1290,498],{"class":246},[111,1292,501],{"class":121},[111,1294,504],{"class":117},[111,1296,507],{"class":121},[111,1298,498],{"class":246},[111,1300,512],{"class":121},[111,1302,1303,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1339,1341,1343,1345],{"class":113,"line":147},[111,1304,495],{"class":121},[111,1306,10],{"class":246},[111,1308,521],{"class":121},[111,1310,524],{"class":246},[111,1312,527],{"class":140},[111,1314,530],{"class":121},[111,1316,533],{"class":121},[111,1318,478],{"class":281},[111,1320,533],{"class":121},[111,1322,557],{"class":140},[111,1324,530],{"class":121},[111,1326,533],{"class":121},[111,1328,471],{"class":281},[111,1330,533],{"class":121},[111,1332,568],{"class":140},[111,1334,530],{"class":121},[111,1336,533],{"class":121},[111,1338,292],{"class":281},[111,1340,533],{"class":121},[111,1342,615],{"class":121},[111,1344,10],{"class":246},[111,1346,512],{"class":121},[10,1348,1349],{},"Easy, right?",[10,1351,1352,1353],{},"Based on our expected behavior, we need to add the width, height, blurhash, and some query parameters to the image. To do so, we will use the same logic as in ",[14,1354,1355],{"id":16},", except that we will fetch the image from Unsplash, as you could do with any remote bucket.",[102,1357,1359],{"className":637,"code":1358,"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,1360,1361,1401,1418,1422],{"__ignoreMap":107},[111,1362,1363,1365,1368,1370,1372,1375,1378,1380,1383,1385,1388,1390,1393,1395,1398],{"class":113,"line":114},[111,1364,673],{"class":140},[111,1366,1367],{"class":117}," img ",[111,1369,530],{"class":121},[111,1371,715],{"class":308},[111,1373,1374],{"class":125}," fetch",[111,1376,1377],{"class":117},"(src)",[111,1379,122],{"class":121},[111,1381,1382],{"class":125},"then",[111,1384,129],{"class":117},[111,1386,1387],{"class":134},"res",[111,1389,141],{"class":140},[111,1391,1392],{"class":117}," res",[111,1394,122],{"class":121},[111,1396,1397],{"class":125},"bytes",[111,1399,1400],{"class":117},"())\n",[111,1402,1403,1405,1408,1410,1412,1415],{"class":113,"line":147},[111,1404,673],{"class":140},[111,1406,1407],{"class":117}," data ",[111,1409,530],{"class":121},[111,1411,715],{"class":308},[111,1413,1414],{"class":125}," getPixels",[111,1416,1417],{"class":117},"(img)\n",[111,1419,1420],{"class":113,"line":179},[111,1421,260],{"emptyLinePlaceholder":259},[111,1423,1424,1426,1429,1431,1434,1437,1439,1442,1445,1447,1450,1452,1455,1457,1460,1462,1464,1466,1469,1471,1475,1477,1479],{"class":113,"line":232},[111,1425,673],{"class":140},[111,1427,1428],{"class":117}," blurhash ",[111,1430,530],{"class":121},[111,1432,1433],{"class":125}," encode",[111,1435,1436],{"class":117},"(Uint8ClampedArray",[111,1438,122],{"class":121},[111,1440,1441],{"class":125},"from",[111,1443,1444],{"class":117},"(data",[111,1446,122],{"class":121},[111,1448,1449],{"class":117},"data)",[111,1451,205],{"class":121},[111,1453,1454],{"class":117}," data",[111,1456,122],{"class":121},[111,1458,1459],{"class":117},"width",[111,1461,205],{"class":121},[111,1463,1454],{"class":117},[111,1465,122],{"class":121},[111,1467,1468],{"class":117},"height",[111,1470,205],{"class":121},[111,1472,1474],{"class":1473},"sbssI"," 4",[111,1476,205],{"class":121},[111,1478,1474],{"class":1473},[111,1480,334],{"class":117},[10,1482,1483],{},"Now, we can set the width, height, and style attributes on the token.",[102,1485,1487],{"className":637,"code":1486,"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,1488,1489,1509,1513,1545,1575],{"__ignoreMap":107},[111,1490,1491,1493,1495,1498,1500,1502,1504,1507],{"class":113,"line":114},[111,1492,645],{"class":308},[111,1494,648],{"class":121},[111,1496,1497],{"class":117}," blurhashToDataUri",[111,1499,654],{"class":121},[111,1501,657],{"class":308},[111,1503,289],{"class":121},[111,1505,1506],{"class":281},"@unpic\u002Fplaceholder",[111,1508,664],{"class":121},[111,1510,1511],{"class":113,"line":147},[111,1512,260],{"emptyLinePlaceholder":259},[111,1514,1515,1518,1520,1522,1524,1526,1528,1530,1532,1534,1536,1538,1540,1543],{"class":113,"line":179},[111,1516,1517],{"class":117},"token",[111,1519,122],{"class":121},[111,1521,273],{"class":125},[111,1523,129],{"class":117},[111,1525,278],{"class":121},[111,1527,1459],{"class":281},[111,1529,278],{"class":121},[111,1531,205],{"class":121},[111,1533,1454],{"class":117},[111,1535,122],{"class":121},[111,1537,1459],{"class":117},[111,1539,122],{"class":121},[111,1541,1542],{"class":125},"toString",[111,1544,1400],{"class":117},[111,1546,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567,1569,1571,1573],{"class":113,"line":232},[111,1548,1517],{"class":117},[111,1550,122],{"class":121},[111,1552,273],{"class":125},[111,1554,129],{"class":117},[111,1556,278],{"class":121},[111,1558,1468],{"class":281},[111,1560,278],{"class":121},[111,1562,205],{"class":121},[111,1564,1454],{"class":117},[111,1566,122],{"class":121},[111,1568,1468],{"class":117},[111,1570,122],{"class":121},[111,1572,1542],{"class":125},[111,1574,1400],{"class":117},[111,1576,1577,1579,1581,1583,1585,1587,1590,1592,1594,1597,1600,1603,1606,1609,1611,1614,1616],{"class":113,"line":256},[111,1578,1517],{"class":117},[111,1580,122],{"class":121},[111,1582,273],{"class":125},[111,1584,129],{"class":117},[111,1586,278],{"class":121},[111,1588,1589],{"class":281},"style",[111,1591,278],{"class":121},[111,1593,205],{"class":121},[111,1595,1596],{"class":121}," `",[111,1598,1599],{"class":281},"background-size: cover; background-image: url(",[111,1601,1602],{"class":121},"${",[111,1604,1605],{"class":125},"blurhashToDataUri",[111,1607,1608],{"class":117},"(blurhash)",[111,1610,346],{"class":121},[111,1612,1613],{"class":281},");",[111,1615,727],{"class":121},[111,1617,334],{"class":117},[10,1619,1620,1621,1624],{},"Finally, we can adjust the ",[59,1622,1623],{},"src"," attribute to add some query parameters.",[102,1626,1628],{"className":637,"code":1627,"language":106,"meta":107,"style":107},"token.attrSet('src', `${src}?auto=format&fit=crop&q=80&w=800`)\n",[59,1629,1630],{"__ignoreMap":107},[111,1631,1632,1634,1636,1638,1640,1642,1644,1646,1648,1651,1653,1655,1658,1660],{"class":113,"line":114},[111,1633,1517],{"class":117},[111,1635,122],{"class":121},[111,1637,273],{"class":125},[111,1639,129],{"class":117},[111,1641,278],{"class":121},[111,1643,1623],{"class":281},[111,1645,278],{"class":121},[111,1647,205],{"class":121},[111,1649,1650],{"class":121}," `${",[111,1652,1623],{"class":117},[111,1654,346],{"class":121},[111,1656,1657],{"class":281},"?auto=format&fit=crop&q=80&w=800",[111,1659,727],{"class":121},[111,1661,334],{"class":117},[10,1663,1664,1665,1668],{},"These queries are specific to Unsplash, but you could do the same with ",[28,1666,631],{"href":629,"rel":1667},[32]," or any image optimization service.",[10,1670,1671,1672,1675],{},"A quick note on this approach: it works fine. Parallelism slightly improves performance because most of the computation happens in the ",[59,1673,1674],{},"encode"," function that transforms the image into a blurhash synchronously. On my computer, fetching the image takes about 100ms, while encoding it takes about 3 seconds.",[48,1677,1679],{"id":1678},"going-further","Going further",[10,1681,1682],{},"The current implementation is simple, and that's intentional. Now that you know how to optimize your images and create custom rules, you can easily adapt this approach to your needs, and even create new ones that fit your specific use cases.",[10,1684,1685],{},"To deal with the performance bottleneck, you have many possibilities. Three of them are:",[1687,1688,1689,1697,1707],"ol",{},[1690,1691,1692,1696],"li",{},[1693,1694,1695],"strong",{},"Local pre-processing and local storage",": If you store images within your code source, you can create a script that will go through all images and generate a JSON file containing the width, height, and the blurhash. Then, in the rule, you'll just have to read this file, and no more computation is needed. This will reduce the build time as well.",[1690,1698,1699,1702,1703],{},[1693,1700,1701],{},"Local pre-processing and remote storage",": If your images are stored remotely, you can create a similar script that will fetch the images, process them, and generate the JSON file. This file can be pushed to a bucket for later use. In the rule, instead of reading a file, you can fetch it from the bucket. ",[1704,1705,1706],"em",{},"Without asynchronous rules, this wasn't possible!",[1690,1708,1709,1712,1713],{},[1693,1710,1711],{},"Remote pre-processing and remote storage",": If your images are stored remotely, you can create a workflow that will be triggered each time a new file is added. The workflow generates the JSON file and stores it in the same bucket. In the rule, you can fetch the JSON file from the bucket and use it directly. ",[1704,1714,1715,1716,1721,1722,1727],{},"The maximal optimization. With ",[28,1717,1720],{"href":1718,"rel":1719},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fbuckets\u002Fevent-notifications\u002F",[32],"R2 notifications"," and ",[28,1723,1726],{"href":1724,"rel":1725},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fqueues\u002Fevent-subscriptions\u002Fevents-schemas\u002F#r2",[32],"Queues",", this is simple to implement.",[10,1729,1730],{},[1704,1731,1732],{},"You could also create a worker pool using Node threads, but it is overkill for this use case.",[10,1734,1735],{},"I'm sure there are a lot of other ways to automate image optimization. Now, it's up to you to find the best approach that fits your workflow.",[10,1737,1738],{},"Hope you enjoyed the article! If you implement something similar, keep me posted!",[1589,1740,1741],{},"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 .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}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}",{"title":107,"searchDepth":147,"depth":147,"links":1743},[1744,1745,1746,1747,1748],{"id":50,"depth":147,"text":51},{"id":362,"depth":147,"text":363},{"id":396,"depth":147,"text":397},{"id":622,"depth":147,"text":623},{"id":1678,"depth":147,"text":1679},"d64b06de-1e87-41a9-a93f-aa6e42c6bdbc","2025-11-19","Previously markdown-it required synchronous rendering and rules. markdown-exit supports async rendering and rules, enabling richer features and parallelism.",{},"\u002Fposts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering","---\ncontentId: d64b06de-1e87-41a9-a93f-aa6e42c6bdbc\ntitle: Markdown Exit Breaks the Rules With Async Rendering\ndescription: Previously markdown-it required synchronous rendering and rules. markdown-exit supports async rendering and rules, enabling richer features and parallelism.\ndate: 2025-11-19\n---\n\nThis article is the continuation of \u003CLinkToArticle id=\"264d08e1-fb05-40c0-b298-0ba1dbbd3f14\" \u002F> and focuses on a more concrete use case: automatically optimizing images in Markdown files.\n\nIt is highly technical, and unless you're dealing with Markdown files, this could be a little abstract. Nevertheless, I highly encourage you to read both the previous and the current article, as it could give you some nice ideas.\n\n---\n\nWhy write a dedicated article about it? Because, until now, this wasn't possible. The main Markdown parser in the JavaScript world is [markdown-it](https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it), and it is fully synchronous.\n\nThis might not be an issue, but it means that [all rules must be synchronous](https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it\u002Fblob\u002Fmaster\u002Fdocs\u002Fdevelopment.md#i-need-async-rule-how-to-do-it) as well. You can't have asynchronous rules. You can't perform a fetch within a rule, and you can't parallelize anything.\n\nA little recap is needed to make sure we're all on the same page.\n\n## Writing a Markdown parser\n\nI won't go into details, but essentially, the parser will split the Markdown text into tokens and then apply rules, which are basically functions, to those tokens to generate the HTML.\n\nFor example, the following text `**bold text**` will be tokenized into: `['strong_open', 'text', 'strong_close']` and the corresponding rules will generate the HTML: `\u003Cstrong>bold text\u003C\u002Fstrong>`. The token `strong_open` will be processed by the `strong_open` rule, which returns `\u003Cstrong>`, and so on.\n\n\u003CCalloutReadMore\n  text=\"Explore the emphasis rule implementation in 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\nThe rule we've been talking about since the beginning transforms a token into its corresponding HTML representation. The interesting part is that you can attach your own rules to the parser to extend its functionality.\n\nImagine you want to automatically apply the `loading=\"lazy\"` attribute to all images in your Markdown files. You could write a rule that targets the `image` rule.\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\nNow, the following Markdown `![alt text](image.jpg)` will be rendered as `\u003Cimg src=\"image.jpg\" alt=\"alt text\" loading=\"lazy\" \u002F>`. This is a dream: automatically apply changes to files without touching them.\n\n## Synchronous limitations\n\nThis is a dream until you realize that all of this happens synchronously. Rules are applied one by one, without any way to parallelize the process. For the highlighting rule, this can significantly reduce performance because you can't render all the code blocks at once—it's one at a time.\n\n> [!NOTE]\n> Anthony Fu was well aware of this and he created [markdown-it-async](https:\u002F\u002Fgithub.com\u002Fantfu\u002Fmarkdown-it-async) as a thin wrapper to make it possible.\n\nApart from parallelizing the process, synchronous rules mean that you can't perform any asynchronous operations within a rule. For example, you can't fetch data from an API. This can quickly become limiting.\n\nBut, with [markdown-exit](https:\u002F\u002Fgithub.com\u002Fserkodev\u002Fmarkdown-exit), a drop-in replacement and complete rewrite of markdown-it using TypeScript, this is now possible. Asynchronous rules are now a reality.\n\n## Optimizing images on the fly\n\nIn [the previous article](\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite), we leveraged a Vite plugin to automatically apply optimizations to images in our HTML. This was a good and useful start.\n\nFor me, this wasn't enough because I mostly deal with Markdown files, and building a custom rule is a better choice. That's what we'll see now.\n\nAll the source code is available on GitHub: \u003CGitHubLink repo=\"Barbapapazes\u002Fmarkdown-exit-unpic\" \u002F>. To keep this article simple, I won't show unrelated code like the project setup.\n\nLet's imagine we have a `src\u002Findex.ts` file. This file is our main script, and the goal is to render raw Markdown to HTML while optimizing images.\n\nWe want to run the following command:\n\n```sh\nnode src\u002Findex.ts\n```\n\nwith this Markdown content:\n\n```md\n# Hello World\n\n![This is a placeholder](https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762707826575-d48c35b9b666)\n```\n\nand have the following output:\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## Building a custom async rule\n\nThe core logic of the rule won't differ from what we've built in the Vite plugin. Instead of reading the file from the local filesystem, we will fetch it from Unsplash to simulate a remote bucket. Then, we will retrieve the width and height, and generate the blurhash. At the end, we will also apply some custom query parameters to avoid the client loading the full image, like you can do with [Cloudflare Images](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F).\n\nFirst, we need to create a new markdown-exit instance and render the Markdown. This is really straightforward.\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\nNothing fancy in this script. However, the output is the following:\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\nThere are some missing parts, but that's a good start.\n\nTo change the image rendering behavior, we will hook into the `image` rule, adjust the token to our needs, and use the default implementation to generate the output.\n\nTo create a new rule, we can use the `use` function. It's like creating a Vue plugin. Then, bind the function we want to the `image` rule.\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\nOur function does nothing apart from calling the original `image` rule, which is already a good step because we don't want to reimplement it ourselves.\n\nThen, we can grab the current token and try to add an attribute to it.\n\nThe `tokens` variable contains all available tokens. It's an array of tokens. To access the current one, we need to use the `idx` variable, which is the current index of the token.\n\n```ts\nconst token = tokens[idx]\n```\n\nNow, we can modify the token. For example, we can add a `loading` attribute with the value `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\nThe output of our script is now the following:\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\nEasy, right?\n\nBased on our expected behavior, we need to add the width, height, blurhash, and some query parameters to the image. To do so, we will use the same logic as in \u003CLinkToArticle id=\"264d08e1-fb05-40c0-b298-0ba1dbbd3f14\" \u002F>, except that we will fetch the image from Unsplash, as you could do with any remote bucket.\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\nNow, we can set the width, height, and style attributes on the 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\nFinally, we can adjust the `src` attribute to add some query parameters.\n\n```ts\ntoken.attrSet('src', `${src}?auto=format&fit=crop&q=80&w=800`)\n```\n\nThese queries are specific to Unsplash, but you could do the same with [Cloudflare Images](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fimages\u002Ftransform-images\u002Ftransform-via-url\u002F) or any image optimization service.\n\nA quick note on this approach: it works fine. Parallelism slightly improves performance because most of the computation happens in the `encode` function that transforms the image into a blurhash synchronously. On my computer, fetching the image takes about 100ms, while encoding it takes about 3 seconds.\n\n## Going further\n\nThe current implementation is simple, and that's intentional. Now that you know how to optimize your images and create custom rules, you can easily adapt this approach to your needs, and even create new ones that fit your specific use cases.\n\nTo deal with the performance bottleneck, you have many possibilities. Three of them are:\n\n1. **Local pre-processing and local storage**: If you store images within your code source, you can create a script that will go through all images and generate a JSON file containing the width, height, and the blurhash. Then, in the rule, you'll just have to read this file, and no more computation is needed. This will reduce the build time as well.\n2. **Local pre-processing and remote storage**: If your images are stored remotely, you can create a similar script that will fetch the images, process them, and generate the JSON file. This file can be pushed to a bucket for later use. In the rule, instead of reading a file, you can fetch it from the bucket. _Without asynchronous rules, this wasn't possible!_\n3. **Remote pre-processing and remote storage**: If your images are stored remotely, you can create a workflow that will be triggered each time a new file is added. The workflow generates the JSON file and stores it in the same bucket. In the rule, you can fetch the JSON file from the bucket and use it directly. _The maximal optimization. With [R2 notifications](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fbuckets\u002Fevent-notifications\u002F) and [Queues](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fqueues\u002Fevent-subscriptions\u002Fevents-schemas\u002F#r2), this is simple to implement._\n\n_You could also create a worker pool using Node threads, but it is overkill for this use case._\n\nI'm sure there are a lot of other ways to automate image optimization. Now, it's up to you to find the best approach that fits your workflow.\n\nHope you enjoyed the article! If you implement something similar, keep me posted!\n",null,{"title":5,"description":1751},{"loc":1753},"posts\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering","BwjYq7Gh-KwV5efwECmlp_1CvWKrmk2JkIsPjVpWmgk",1790086900646]