Skip to content

feat(rules): support async markdown-it rules #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const md = MarkdownItAsync({
}
})

// Optional, use the custom async rules
md.renderer.asyncRules.rule_key = async () => {
// Your async rule
}

// Note you need to use `renderAsync` instead of `render`
const html = await md.renderAsync(markdown)
```
Expand Down
59 changes: 58 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
Options,
PresetName,
} from 'markdown-it'
import type { Renderer as MarkdownItRender, Token } from 'markdown-it/index.js'
import type Renderer from 'markdown-it/lib/renderer.mjs'
import MarkdownIt from 'markdown-it'

export type PluginSimple = ((md: MarkdownItAsync) => void)
Expand Down Expand Up @@ -40,7 +42,20 @@ function randStr(): string {

export type MarkdownItAsyncPlaceholderMap = Map<string, [promise: Promise<string>, str: string, lang: string, attrs: string]>

type AwaitedFunction<T extends (...args: any[]) => any> = T | ((...args: Parameters<T>) => Promise<ReturnType<T>>)

type RenderRuleAsync = AwaitedFunction<MarkdownItRender.RenderRule>

interface RendererAsync extends Renderer {
asyncRules: Partial<Record<keyof MarkdownItRender.RenderRuleRecord, RenderRuleAsync>>

renderInlineAsync: (this: RendererAsync, tokens: Token[], options: Options, env?: any) => Promise<string>

renderAsync: (this: RendererAsync, tokens: Token[], options: Options, env?: any) => Promise<string>
}

export class MarkdownItAsync extends MarkdownIt {
declare renderer: RendererAsync
placeholderMap: MarkdownItAsyncPlaceholderMap
private disableWarn = false

Expand All @@ -53,6 +68,48 @@ export class MarkdownItAsync extends MarkdownIt {
options.highlight = wrapHightlight(options.highlight, map)
super(...args as [])
this.placeholderMap = map
this.renderer.asyncRules = {}

this.renderer.renderInlineAsync = async function (this: RendererAsync, tokens: Token[], options: Options, env?: any): Promise<string> {
const codes = await Promise.all(
tokens.map(async (token, i) => {
const type = token.type

const rule = this.asyncRules[type] ?? this.rules[type]

if (typeof rule !== 'undefined') {
return rule(tokens, i, options, env, this)
}
else {
return this.renderToken(tokens, i, options)
}
}),
)

return codes.join('')
}

this.renderer.renderAsync = async function (this: RendererAsync, tokens: Token[], options: Options, env?: any): Promise<string> {
const codes = await Promise.all(
tokens.map(async (token, i) => {
const type = token.type

const rule = this.asyncRules[type] ?? this.rules[type]

if (type === 'inline') {
return this.renderInlineAsync(token.children ?? [], options, env)
}
else if (typeof rule !== 'undefined') {
return rule(tokens, i, options, env, this)
}
else {
return this.renderToken(tokens, i, options)
}
}),
)

return codes.join('')
}
}

use(plugin: PluginSimple): this
Expand All @@ -76,7 +133,7 @@ export class MarkdownItAsync extends MarkdownIt {
async renderAsync(src: string, env?: any): Promise<string> {
this.options.highlight = wrapHightlight(this.options.highlight, this.placeholderMap)
this.disableWarn = true
const result = this.render(src, env)
const result = await this.renderer.renderAsync(this.parse(src, env), this.options, env)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We couldn't do this as some plugins rely on replacing the md.render function.

Copy link
Author

@ronny1020 ronny1020 Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reply. I apologize for not reviewing too many plugin examples. Could you recommend some examples to help me improve? I'm confused that if multiple plugins are used, isn't there a risk that earlier plugins might be overridden by later ones? If there is some way to avoid that, maybe, we can use a similar approach.

Copy link
Owner

@antfu antfu Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronny1020 i think we exactly have this hen egg thing here:
there are plugins out there that do the overwriting.
and yeah - that would mean that they are eventually overwritting each other..

maybe they just doing something first and then calling the original?
in this case i think that maybe they are working all one after each other..
????
would need a testcase for this to exactly pin-point it..

maybe one of my test-setups can help as a starting point:
for a complex test-setup you can have a look at

i am happy if you have the motivation to test and setup things..
for my personal use-case for now i switched to a sync-only approach... (so i am in no need currently)

this.disableWarn = false
return replaceAsync(result, placeholderRe, async (match, id) => {
if (!this.placeholderMap.has(id))
Expand Down
27 changes: 27 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,31 @@ describe('markdown-it-async', async () => {

spy.mockRestore()
})

it('async rules', async () => {
const mda = createMarkdownItAsync({
async highlight(str, lang) {
return await codeToHtml(str, {
lang,
theme: 'vitesse-light',
})
},
})

const mock = vi.fn()

Object.entries(mda.renderer.rules).forEach(([key, rule]) => {
if (typeof rule === 'function') {
mda.renderer.asyncRules[key] = async (tokens, index, options, env?: any) => {
await new Promise(resolve => setTimeout(resolve, 10))
mock()
return rule(tokens, index, options, env, mda.renderer)
}
}
})

expect(expectedResult)
.toEqual(await mda.renderAsync(fixture))
expect(mock).toHaveBeenCalled()
})
})