feat(plugin-md-power): add createContainerSyntaxPlugin for custom container rendering (#564)
This commit is contained in:
parent
58381ba294
commit
2e7f69bb62
@ -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) => `<div class="test">${tokens[index].content} ${tokens[index].meta.title}</div>`)
|
||||
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"')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 `<div class="vp-chat">
|
||||
<div class="vp-chat-header">
|
||||
<p class="vp-chat-title">${title || 'Chat'}</p>
|
||||
</div>
|
||||
<div class="vp-chat-content">
|
||||
${chatMessagesRender(md, env, messages)}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
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<ChatMeta>(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) => `<div class="vp-chat">
|
||||
<div class="vp-chat-header">
|
||||
<p class="vp-chat-title">${tokens[idx].meta?.title || 'Chat'}</p>
|
||||
</div>
|
||||
<div class="vp-chat-content">
|
||||
${chatMessagesRender(md, env, parseChatContent(tokens[idx].content))}
|
||||
</div>
|
||||
</div>`,
|
||||
)
|
||||
|
||||
@ -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<RenderRule> 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 `<div class="example">${meta.title} | ${content}</div>`
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
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 `<div class="custom-container ${type}">${content}</div>`
|
||||
}
|
||||
|
||||
md.block.ruler.before('fence', `${type}_definition`, defineContainer)
|
||||
md.renderer.rules[`${type}_container`] = render ?? defaultRender
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user