feat(plugin-md-power): add table container, close #652 (#655)

This commit is contained in:
pengzhanbo 2025-07-26 09:11:51 +08:00 committed by GitHub
parent b120633453
commit 371834640b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 380 additions and 0 deletions

View File

@ -130,6 +130,7 @@ export default defineUserConfig({
// artPlayer: true, // 启用嵌入 artPlayer 本地视频 语法 @[artPlayer](url)
// audioReader: true, // 启用嵌入音频朗读功能 语法 @[audioReader](url)
// icon: { provider: 'iconify' }, // 启用内置图标语法 ::icon-name::
// table: true, // 启用表格增强容器语法 ::: table
// codepen: true, // 启用嵌入 codepen 语法 @[codepen](user/slash)
// replit: true, // 启用嵌入 replit 语法 @[replit](user/repl-name)
// codeSandbox: true, // 启用嵌入 codeSandbox 语法 @[codeSandbox](id)

View File

@ -35,6 +35,7 @@ export const themeGuide: ThemeNote = defineNoteConfig({
items: [
'basic',
'extensions',
'table',
'icons',
'mark',
'plot',

View File

@ -27,6 +27,7 @@ export const theme: Theme = plumeTheme({
annotation: true,
abbr: true,
table: true,
timeline: true,
collapse: true,
chat: true,

View File

@ -0,0 +1,151 @@
---
title: table 增强
icon: mdi:table-plus
createTime: 2025/07/25 16:57:42
permalink: /guide/markdown/table/
badge: 新
---
## 概述
markdown 默认的表格功能相对比较简单,但在实际使用场景中,常常需要在表格中添加一些额外的信息,比如表格的标题;
或者额外的功能,如复制表格的内容等。
在不破坏表格语法的前提下,主题提供了 `::: table` 容器,可以方便的对表格进行扩展。
::: tip 表格增强容器在持续开发中,如果有其他的功能建议请在 [Issue](https://github.com/pengzhanbo/vuepress-theme-plume/issues) 中反馈。
:::
## 配置
该功能默认不启用,您需要在 `theme` 配置中启用它。
```ts title=".vuepress/config.ts"
export default defineUserConfig({
theme: plumeTheme({
markdown: {
// table: true, // 启用默认功能
table: {
// 表格默认对齐方式 'left' | 'center' | 'right'
align: 'left',
// 表格宽度是否为最大内容宽度
// 行内元素不再自动换行,超出容器宽度时表格显示滚动条
maxContent: false,
/**
* 复制为 html/markdown
* true 相当于 `all`,相当于同时启用 html 和 markdown
*/
copy: true, // true | 'all' | 'html' | 'md'
}
},
})
})
```
## 语法
直接将 表格 包裹在 `:::table` 中即可。
```md
:::table title="标题" align="center" max-content copy="all"
| xx | xx | xx |
| -- | -- | -- |
| xx | xx | xx |
:::
```
### Props
:::: field-group
::: field name="title" type="string" optional
表格标题,显示在表格的下方
:::
::: field name="align" type="'left' | 'center' | 'right'" optional default="'left'"
表格对齐方式
:::
::: field name="copy" type="boolean | 'all' | 'html' | 'md'" optional default="true"
在表格的右上角显示复制按钮,可以复制为 html / markdown
- `true` 等同于 `all`
- `false` 不显示复制按钮
- `all` 同时启用 `html``md`
- `html` 启用复制为 html
- `md` 启用复制为 markdown
:::
::: field name="maxContent" type="boolean" optional default="false"
行内元素不再自动换行,超出容器宽度时表格显示滚动条
:::
::::
## 示例
**输入:**
```md
::: table title="这是表格标题"
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Row 2 | Data | Info |
:::
```
**输出:**
::: table title="这是表格标题"
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Row 2 | Data | Info |
:::
**输入:**
```md
::: table title="这是表格标题" align="center"
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Row 2 | Data | Info |
:::
```
**输出:**
::: table title="这是表格标题" align="center"
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Row 2 | Data | Info |
:::
**输入:**
```md
:::table title="这是表格标题" max-content
| ID | Description | Status |
|----|-----------------------------------------------------------------------------|--------------|
| 1 | This is an extremely long description that should trigger text wrapping in most table implementations. | In Progress |
| 2 | Short text | ✅ Completed |
:::
```
**输出:**
:::table title="这是表格标题" max-content
| ID | Description | Status |
|----|-----------------------------------------------------------------------------|--------------|
| 1 | This is an extremely long description that should trigger text wrapping in most table implementations. | In Progress |
| 2 | Short text | ✅ Completed |
:::

View File

@ -0,0 +1,146 @@
<script setup lang="ts">
import { decodeData } from '@vuepress/helper/client'
import { useClipboard, useToggle } from '@vueuse/core'
import { computed, useTemplateRef } from 'vue'
const props = defineProps<{
/** 表格标题 */
title?: string
/** 对其方式 */
align?: 'left' | 'center' | 'right'
/** 复制为 html/markdown */
copy?: false | 'all' | 'html' | 'md'
/** 最大化内容 */
maxContent?: boolean
/** @internal */
markdown?: string
}>()
const tableEl = useTemplateRef('table')
const rawContent = computed(() => props.markdown ? decodeData(props.markdown) : '')
const [isHTMLCopied, toggleHTMLCopy] = useToggle()
const [isMDCopied, toggleMDCopy] = useToggle()
const { copy: copyTable } = useClipboard()
function onCopy(type: 'html' | 'md') {
copyTable(type === 'md' ? rawContent.value : tableEl.value?.innerHTML || '')
type === 'html' ? toggleHTMLCopy(true) : toggleMDCopy(true)
setTimeout(() => {
type === 'html' ? toggleHTMLCopy(false) : toggleMDCopy(false)
}, 1500)
}
</script>
<template>
<div class="vp-table" :class="{ [align || 'left']: true }">
<div class="table-container">
<div class="table-content">
<div v-if="copy" class="table-toolbar">
<button
v-if="copy === 'all' || copy === 'html'"
type="button"
aria-label="Copy Table as HTML"
@click="onCopy('html')"
>
<span :class="isHTMLCopied ? 'vpi-table-copied' : 'vpi-table-copy'" />
<span>HTML</span>
</button>
<button
v-if="copy === 'all' || copy === 'md'"
type="button"
aria-label="Copy Table as Markdown"
@click="onCopy('md')"
>
<span :class="isMDCopied ? 'vpi-table-copied' : 'vpi-table-copy'" />
<span>Markdown</span>
</button>
</div>
<div ref="table" :class="{ 'max-content': maxContent }">
<slot />
</div>
</div>
<p v-if="title" class="table-title">
{{ title }}
</p>
</div>
</div>
</template>
<style>
.vp-table {
display: flex;
max-width: 100%;
margin: 16px 0;
}
.vp-table.left {
justify-content: flex-start;
}
.vp-table.center {
justify-content: center;
}
.vp-table.right {
justify-content: flex-end;
}
.vp-table .table-container,
.vp-table .table-content {
width: fit-content;
max-width: 100%;
}
.vp-table .table-content {
margin: 0 auto;
}
.vp-table .table-title {
margin: 8px auto;
font-weight: 500;
text-align: center;
}
.vp-table .table-container table {
margin: 0;
}
.vp-table .table-toolbar {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.vp-table .table-toolbar button {
display: flex;
gap: 4px;
align-items: center;
font-size: 14px;
color: var(--vp-c-text-3);
cursor: pointer;
transition: var(--vp-t-color);
transition-property: color;
}
.vp-table .table-toolbar button:hover {
color: var(--vp-c-text-2);
}
.vpi-table-copy {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M20.829 12.861c.171-.413.171-.938.171-1.986s0-1.573-.171-1.986a2.25 2.25 0 0 0-1.218-1.218c-.413-.171-.938-.171-1.986-.171H11.1c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C7.5 9.209 7.5 9.839 7.5 11.1v6.525c0 1.048 0 1.573.171 1.986c.229.551.667.99 1.218 1.218c.413.171.938.171 1.986.171s1.573 0 1.986-.171m7.968-7.968a2.25 2.25 0 0 1-1.218 1.218c-.413.171-.938.171-1.986.171s-1.573 0-1.986.171a2.25 2.25 0 0 0-1.218 1.218c-.171.413-.171.938-.171 1.986s0 1.573-.171 1.986a2.25 2.25 0 0 1-1.218 1.218m7.968-7.968a11.68 11.68 0 0 1-7.75 7.9l-.218.068M16.5 7.5v-.9c0-1.26 0-1.89-.245-2.371a2.25 2.25 0 0 0-.983-.984C14.79 3 14.16 3 12.9 3H6.6c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C3 4.709 3 5.339 3 6.6v6.3c0 1.26 0 1.89.245 2.371c.216.424.56.768.984.984c.48.245 1.111.245 2.372.245H7.5'/%3E%3C/svg%3E");
}
.vpi-table-copied {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m9 20.42l-6.21-6.21l2.83-2.83L9 14.77l9.88-9.89l2.83 2.83z'/%3E%3C/svg%3E");
}
.vp-table .table-content .max-content {
max-width: 100%;
overflow-x: auto;
}
.vp-table .table-content .max-content table {
width: max-content;
}
</style>

View File

@ -14,6 +14,7 @@ import { fileTreePlugin } from './fileTree.js'
import { langReplPlugin } from './langRepl.js'
import { npmToPlugins } from './npmTo.js'
import { stepsPlugin } from './steps.js'
import { tablePlugin } from './table.js'
import { tabs } from './tabs.js'
import { timelinePlugin } from './timeline.js'
@ -67,4 +68,7 @@ export async function containerPlugin(
if (options.field)
fieldPlugin(md)
if (options.table)
tablePlugin(md, isPlainObject(options.table) ? options.table : {})
}

View File

@ -0,0 +1,29 @@
import type { Markdown } from 'vuepress/markdown'
import type { TableContainerOptions } from '../../shared/table.js'
import { encodeData } from '@vuepress/helper'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerSyntaxPlugin } from './createContainer.js'
export interface TableContainerAttrs extends TableContainerOptions {
title?: string
}
/**
*
*/
export function tablePlugin(md: Markdown, options: TableContainerOptions = {}): void {
createContainerSyntaxPlugin(md, 'table', (tokens, index, _, env) => {
const meta = { copy: true, maxContent: false, ...options, ...tokens[index].meta } as TableContainerAttrs & { markdown?: string }
const content = tokens[index].content
if (meta.copy) {
meta.copy = meta.copy === true ? 'all' : meta.copy
if (meta.copy === 'all' || meta.copy === 'md') {
meta.markdown = encodeData(content.trim())
}
}
return `<VPTable ${stringifyAttrs(meta)}>${md.render(content, env)}</VPTable>`
})
}

View File

@ -126,6 +126,11 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('VPField', VPField)`)
}
if (options.table) {
imports.add(`import VPTable from '${CLIENT_FOLDER}components/VPTable.vue'`)
enhances.add(`app.component('VPTable', VPTable)`)
}
const setupIcon = prepareIcon(imports, options.icon)
return app.writeTemp(

View File

@ -7,6 +7,7 @@ import type { NpmToOptions } from './npmTo.js'
import type { PDFOptions } from './pdf.js'
import type { PlotOptions } from './plot.js'
import type { ReplOptions } from './repl.js'
import type { TableContainerOptions } from './table.js'
export interface MarkdownPowerPluginOptions {
/**
@ -240,6 +241,15 @@ export interface MarkdownPowerPluginOptions {
*/
caniuse?: boolean | CanIUseOptions
/**
* table
*
* - `copy`: html markdown
*
* @default false
*/
table?: boolean | TableContainerOptions
// enhance
/**
*

View File

@ -0,0 +1,31 @@
export interface TableContainerOptions {
/**
*
* - 'left':
* - 'center':
* - 'right':
*
* @default 'left'
*/
align?: 'left' | 'center' | 'right'
/**
*
* - true: `all` html markdown
* - 'all': html markdown
* - 'html': html
* - 'md': markdown
* - `false`:
*
* @default true
*/
copy?: boolean | 'all' | 'html' | 'md'
/**
*
*
*
*
* @default false
*/
maxContent?: boolean
}

View File

@ -57,6 +57,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
'plot',
'repl',
'replit',
'table',
'timeline',
'collapse',
'chat',