From c8a467d24a6eb9f9603e20904bf004cca264549a Mon Sep 17 00:00:00 2001 From: purocean Date: Fri, 12 Jul 2024 15:43:53 +0800 Subject: [PATCH] feat: support code group container --- help/FEATURES.md | 13 +++- help/FEATURES_ZH-CN.md | 12 ++- src/renderer/plugins/markdown-container.ts | 88 +++++++++++++++++----- 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/help/FEATURES.md b/help/FEATURES.md index bb4c0f96..76129514 100644 --- a/help/FEATURES.md +++ b/help/FEATURES.md @@ -514,7 +514,7 @@ Support functions similiar to [VuePress Container Block](https://v2.vuepress.vue `type` is required, `title` and `content` are optional. -The supported `types` are: tip, warning, danger, details, group, group-item, row, col, section, div +The supported `types` are: tip, warning, danger, details, code-group, group, group-item, row, col, section, div **Example** @@ -557,6 +557,17 @@ test 3 ::: :::: +::: code-group +```js [test.js] +let a = 1 +``` + +```ts [test.ts] +let a: number = 1 +``` +::: + + ::::: row Columns :::: col TODO ::: warning diff --git a/help/FEATURES_ZH-CN.md b/help/FEATURES_ZH-CN.md index 2f8e1eee..ef706dba 100644 --- a/help/FEATURES_ZH-CN.md +++ b/help/FEATURES_ZH-CN.md @@ -512,7 +512,7 @@ chart.setOption(option, true) `type` 是必需的, `title` 和 `content` 是可选的。 -支持的 `type` 有:tip, warning, danger, details, group, group-item, row, col, section, div +支持的 `type` 有:tip, warning, danger, details, code-group, group, group-item, row, col, section, div **示例** @@ -555,6 +555,16 @@ test 3 ::: :::: +::: code-group +```js [test.js] +let a = 1 +``` + +```ts [test.ts] +let a: number = 1 +``` +::: + ::::: row 分列示例 :::: col TODO ::: warning diff --git a/src/renderer/plugins/markdown-container.ts b/src/renderer/plugins/markdown-container.ts index e8adc417..5169774a 100644 --- a/src/renderer/plugins/markdown-container.ts +++ b/src/renderer/plugins/markdown-container.ts @@ -230,6 +230,7 @@ export default { let groupItemSeq = 0 let groupItemBase = Date.now() let groupItemName = groupItemBase + groupItemSeq + const groupItemContentClass = 'group-item-content' ctx.registerHook('MARKDOWN_BEFORE_RENDER', ({ env }) => { // first render, reset count @@ -240,8 +241,24 @@ export default { groupItemSeq = 0 }) + function buildGroupItem (token: Token, title: string, attrs?: Record) { + const parent = h('div', attrs || Object.fromEntries(token.attrs || []), []) + const radioName = `group-item-${groupItemName}` + const id = `group-item-${groupItemName}-${groupItemIdx++}` + const checked = groupItemIdx === 1 || title.startsWith('*') + + return { + node: h(Fragment, [ + h('input', { key: id, class: 'group-item-radio', id, name: radioName, type: 'radio', 'data-default-checked': checked, checked }), + h('label', { class: 'group-item-label', for: id }, title.replace('*', '').trim() || 'Group Item'), + parent + ]), + parent + } + } + ctx.markdown.registerPlugin(md => { - ['tip', 'warning', 'danger', 'details', 'group-item', 'group', 'row', 'col', 'section', 'div'].forEach(name => { + ['tip', 'warning', 'danger', 'details', 'group-item', 'group', 'row', 'col', 'section', 'div', 'code-group'].forEach(name => { const reg = new RegExp(`^${name}\\s*(.*)$`) md.use(MarkdownItContainer, name, { @@ -265,24 +282,14 @@ export default { const match = token.info.trim().match(reg) const title = md.utils.escapeHtml(match![1]) - const containerClass = `custom-container ${name}` + const isGroup = name === 'group' || name === 'code-group' + + const containerClass = isGroup ? 'custom-container group' : `custom-container ${name}` if (name === 'group-item') { - token.attrJoin('class', 'group-item-content') - const parent = h('div', Object.fromEntries(token.attrs || []), []) - const radioName = `group-item-${groupItemName}` - const id = `group-item-${groupItemName}-${groupItemIdx++}` - const checked = groupItemIdx === 1 || title.startsWith('*') - - return { - node: h(Fragment, [ - h('input', { key: id, class: 'group-item-radio', id, name: radioName, type: 'radio', 'data-default-checked': checked, checked }), - h('label', { class: 'group-item-label', for: id }, title.replace('*', '').trim() || 'Group Item'), - parent - ]), - parent - } - } else if (name === 'group') { + token.attrJoin('class', groupItemContentClass) + return buildGroupItem(token, title) + } else if (isGroup) { groupItemIdx = 0 groupItemSeq++ groupItemName = groupItemBase + groupItemSeq @@ -292,7 +299,7 @@ export default { const titleTag = name === 'details' ? 'summary' : 'p' const titleClass = name === 'details' ? '' : 'custom-container-title' - const children = (title || name === 'group') ? [h(titleTag, { class: titleClass }, title)] : [] + const children = (title || isGroup) ? [h(titleTag, { class: titleClass }, title)] : [] token.attrJoin('class', containerClass) if (title) { @@ -301,15 +308,55 @@ export default { const props: Record = Object.fromEntries(token.attrs || []) - if (name === 'group') { + if (isGroup) { props.key = groupItemName } + if (name === 'code-group') { + for ( + let i = idx + 1; + !( + tokens[i].nesting === -1 && + tokens[i].type === 'container_code-group_close' + ); + ++i + ) { + const token = tokens[i] + if (token.type === 'fence' && token.tag === 'code') { + if (!token.meta) { + token.meta = {} + } + + token.meta.isCodeGroupItem = true + } + } + } + return h(containerTag, props, children) } } }) }) + + const renderFence = md.renderer.rules.fence! + const infoTitleReg = /\[([^\]]*)\]/ + + md.renderer.rules.fence = (tokens, idx, options, env, slf) => { + const token = tokens[idx] + if (token.meta && token.meta.isCodeGroupItem) { + const title = token.info.match(infoTitleReg)?.[1] || token.info || 'txt' + token.info = token.info.replace(infoTitleReg, '').trim() + + const groupItem = buildGroupItem(token, title, { class: groupItemContentClass }) + ;(groupItem.parent.children as any[]).push( + renderFence.call(slf, tokens, idx, options, env, slf) + ) + + return groupItem.node as any + } + + return renderFence.call(slf, tokens, idx, options, env, slf) + } }) ctx.registerHook('VIEW_ON_GET_HTML_FILTER_NODE', ({ node }) => { @@ -322,8 +369,9 @@ export default { /* eslint-disable no-template-curly-in-string */ items.push( - { label: '/ ::: Container', insertText: '${3|:::,::::,:::::|} ${1|tip,warning,danger,details,group,group-item,row,col,section,div|} ${2:Title}\n${4:Content}\n${3|:::,::::,:::::|}\n', block: true }, + { label: '/ ::: Container', insertText: '${3|:::,::::,:::::|} ${1|tip,warning,danger,details,code-group,group,group-item,row,col,section,div|} ${2:Title}\n${4:Content}\n${3|:::,::::,:::::|}\n', block: true }, { label: '/ ::: Group Container', insertText: ':::: group ${1:Title}\n::: group-item Tab 1\ntest 1\n:::\n::: group-item *Tab 2\ntest 2\n:::\n::: group-item Tab 3\ntest 3\n:::\n::::\n', block: true }, + { label: '/ ::: Code Group Container', insertText: '::: code-group ${1:Title}\n${2:```js [test.js]\nlet a = 1\n```\n\n```ts [test.ts]\nlet a: number = 1\n```}\n:::\n', block: true }, { label: '/ ::: Column Container', insertText: ':::: row ${1:Title}\n::: col\ntest 1\n:::\n::: col\ntest 2\n:::\n::::\n', block: true }, ) })