From 2e7f69bb623aa9a57868ad029aef645973cfba2e Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sat, 26 Apr 2025 15:59:08 +0800 Subject: [PATCH] feat(plugin-md-power): add createContainerSyntaxPlugin for custom container rendering (#564) --- .../__test__/createContainerPlugin.spec.ts | 36 +++++++- .../src/node/container/chat.ts | 78 +++-------------- .../src/node/container/createContainer.ts | 87 +++++++++++++++++++ 3 files changed, 136 insertions(+), 65 deletions(-) diff --git a/plugins/plugin-md-power/__test__/createContainerPlugin.spec.ts b/plugins/plugin-md-power/__test__/createContainerPlugin.spec.ts index 4f3efa00..61b38a0a 100644 --- a/plugins/plugin-md-power/__test__/createContainerPlugin.spec.ts +++ b/plugins/plugin-md-power/__test__/createContainerPlugin.spec.ts @@ -1,6 +1,6 @@ import MarkdownIt from 'markdown-it' import { describe, expect, it } from 'vitest' -import { createContainerPlugin } from '../src/node/container/createContainer.js' +import { createContainerPlugin, createContainerSyntaxPlugin } from '../src/node/container/createContainer.js' describe('createContainerPlugin', () => { it('should work with default options', () => { @@ -20,3 +20,37 @@ describe('createContainerPlugin', () => { expect(md.render(':::test\ncontent\n:::')).toContain('class="test"') }) }) + +describe('createContainerSyntaxPlugin', () => { + it('should work with default options', () => { + const md = new MarkdownIt() + createContainerSyntaxPlugin(md, 'test') + const rendered = md.render(':::test\ncontent\n:::') + expect(rendered).toContain('class="custom-container test"') + expect(rendered).toContain('content') + }) + + it('should work with more than 3 markers', () => { + const md = new MarkdownIt() + createContainerSyntaxPlugin(md, 'test') + expect(md.render('::::test\ncontent\n::::')).toContain('class="custom-container test"') + expect(md.render(':::::test\ncontent\n:::::')).toContain('class="custom-container test"') + }) + + it('should work with custom render', () => { + const md = new MarkdownIt() + createContainerSyntaxPlugin(md, 'test', (tokens, index) => `
${tokens[index].content} ${tokens[index].meta.title}
`) + const rendered = md.render(':::test title="title"\ncontent\n:::') + expect(rendered).toContain('class="test"') + expect(rendered).toContain('content\n title') + }) + + it('should not work', () => { + const md = new MarkdownIt() + createContainerSyntaxPlugin(md, 'test') + + expect(md.render('::test\ncontent\n::')).not.toContain('class="custom-container test"') + expect(md.render(':::text\ncontent\n:::')).not.toContain('class="custom-container text"') + expect(md.render(':::test\ncontent\n:::')).not.toContain('class="custom-container test"') + }) +}) diff --git a/plugins/plugin-md-power/src/node/container/chat.ts b/plugins/plugin-md-power/src/node/container/chat.ts index d66506af..db967016 100644 --- a/plugins/plugin-md-power/src/node/container/chat.ts +++ b/plugins/plugin-md-power/src/node/container/chat.ts @@ -10,15 +10,9 @@ * ::: */ import type { PluginSimple } from 'markdown-it' -import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs' -import type Token from 'markdown-it/lib/token.mjs' import type { Markdown, MarkdownEnv } from 'vuepress/markdown' -import { resolveAttrs } from '.././utils/resolveAttrs.js' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js' - -interface ChatMeta { - title?: string -} +import { createContainerSyntaxPlugin } from './createContainer.js' interface ChatMessage { sender: 'user' | 'self' @@ -27,63 +21,6 @@ interface ChatMessage { content: string[] } -export const chatPlugin: PluginSimple = (md) => { - md.block.ruler.before('fence', 'chat_def', chatDef) - - md.renderer.rules.chat_container = (tokens: Token[], idx: number, _, env) => { - const { meta, content } = tokens[idx] - const { title } = meta as ChatMeta - const messages = parseChatContent(content) - return `
-
-

${title || 'Chat'}

-
-
- ${chatMessagesRender(md, env, messages)} -
-
` - } -} - -function chatDef(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { - const start = state.bMarks[startLine] + state.tShift[startLine] - const max = state.eMarks[startLine] - let pos = start - - if (state.src.slice(pos, pos + 3) !== ':::') - return false - - pos += 3 - - const info = state.src.slice(start + 3, max).trim() - if (!info.startsWith('chat')) - return false - - /* istanbul ignore if -- @preserve */ - if (silent) - return true - - let line = startLine - let content = '' - while (++line < endLine) { - if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === ':::') { - break - } - - content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n` - } - - const token = state.push('chat_container', '', 0) - token.meta = resolveAttrs(info).attrs - token.content = content - token.markup = '::: chat' - token.map = [startLine, line + 1] - - state.line = line + 1 - - return true -} - function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessage[]): string { let currentDate = '' return messages.map(({ sender, username, date, content }) => { @@ -142,3 +79,16 @@ function parseChatContent(content: string): ChatMessage[] { } return messages } + +export const chatPlugin: PluginSimple = md => createContainerSyntaxPlugin( + md, + 'chat', + (tokens, idx, _, env) => `
+
+

${tokens[idx].meta?.title || 'Chat'}

+
+
+ ${chatMessagesRender(md, env, parseChatContent(tokens[idx].content))} +
+
`, +) diff --git a/plugins/plugin-md-power/src/node/container/createContainer.ts b/plugins/plugin-md-power/src/node/container/createContainer.ts index bb598aca..6fa2304a 100644 --- a/plugins/plugin-md-power/src/node/container/createContainer.ts +++ b/plugins/plugin-md-power/src/node/container/createContainer.ts @@ -1,6 +1,8 @@ import type { RenderRule } from 'markdown-it/lib/renderer.mjs' +import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs' import type { Markdown } from 'vuepress/markdown' import container from 'markdown-it-container' +import { resolveAttrs } from '../utils/resolveAttrs.js' type RenderRuleParams = Parameters extends [...infer Args, infer _] ? Args : never @@ -27,3 +29,88 @@ export function createContainerPlugin( md.use(container, type, { render }) } + +/** + * 创建一个自定义的容器规则,内容不会交给 markdown-it 处理。 + * 需要自定义 content 的处理逻辑 + * ```md + * ::: type + * xxxx <-- content: 这部分的内容不会交给 markdown-it 处理 + * ::: + * ``` + * + * @example + * ```ts + * const example = createContainerSyntaxPlugin(md, 'example', (tokens, index, options, env) => { + * const { content, meta } = tokens[index] + * return `
${meta.title} | ${content}
` + * }) + * ``` + */ +export function createContainerSyntaxPlugin( + md: Markdown, + type: string, + render?: RenderRule, +) { + const maker = ':' + const markerMinLen = 3 + + function defineContainer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + const start = state.bMarks[startLine] + state.tShift[startLine] + const max = state.eMarks[startLine] + let pos = start + + // check marker + if (state.src[pos] !== maker) + return false + + pos += markerMinLen + + for (pos = start + 1; pos <= max; pos++) { + if (state.src[pos] !== maker) + break + } + + if (pos - start < markerMinLen) + return false + + const markup = state.src.slice(start, pos) + const info = state.src.slice(pos, max).trim() + + // ::: type + if (!info.startsWith(type)) + return false + + /* istanbul ignore if -- @preserve */ + if (silent) + return true + + let line = startLine + let content = '' + while (++line < endLine) { + if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === markup) { + break + } + + content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n` + } + + const token = state.push(`${type}_container`, '', 0) + token.meta = resolveAttrs(info.slice(type.length)).attrs + token.content = content + token.markup = `${markup} ${type}` + token.map = [startLine, line + 1] + + state.line = line + 1 + + return true + } + + const defaultRender: RenderRule = (tokens, index) => { + const { content } = tokens[index] + return `
${content}
` + } + + md.block.ruler.before('fence', `${type}_definition`, defineContainer) + md.renderer.rules[`${type}_container`] = render ?? defaultRender +}