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 MarkdownIt from 'markdown-it'
|
||||||
import { describe, expect, it } from 'vitest'
|
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', () => {
|
describe('createContainerPlugin', () => {
|
||||||
it('should work with default options', () => {
|
it('should work with default options', () => {
|
||||||
@ -20,3 +20,37 @@ describe('createContainerPlugin', () => {
|
|||||||
expect(md.render(':::test\ncontent\n:::')).toContain('class="test"')
|
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 { 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 type { Markdown, MarkdownEnv } from 'vuepress/markdown'
|
||||||
import { resolveAttrs } from '.././utils/resolveAttrs.js'
|
|
||||||
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
|
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
|
||||||
|
import { createContainerSyntaxPlugin } from './createContainer.js'
|
||||||
interface ChatMeta {
|
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
sender: 'user' | 'self'
|
sender: 'user' | 'self'
|
||||||
@ -27,63 +21,6 @@ interface ChatMessage {
|
|||||||
content: string[]
|
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 {
|
function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessage[]): string {
|
||||||
let currentDate = ''
|
let currentDate = ''
|
||||||
return messages.map(({ sender, username, date, content }) => {
|
return messages.map(({ sender, username, date, content }) => {
|
||||||
@ -142,3 +79,16 @@ function parseChatContent(content: string): ChatMessage[] {
|
|||||||
}
|
}
|
||||||
return messages
|
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 { 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 type { Markdown } from 'vuepress/markdown'
|
||||||
import container from 'markdown-it-container'
|
import container from 'markdown-it-container'
|
||||||
|
import { resolveAttrs } from '../utils/resolveAttrs.js'
|
||||||
|
|
||||||
type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never
|
type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never
|
||||||
|
|
||||||
@ -27,3 +29,88 @@ export function createContainerPlugin(
|
|||||||
|
|
||||||
md.use(container, type, { render })
|
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