mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(plugin-md-power): add collapse syntax support (#535)
This commit is contained in:
parent
dd5c984578
commit
cca923a235
@ -45,6 +45,7 @@ export const themeGuide = defineNoteConfig({
|
||||
'tabs',
|
||||
'timeline',
|
||||
'demo-wrapper',
|
||||
'collapse',
|
||||
'npm-to',
|
||||
'caniuse',
|
||||
'include',
|
||||
|
||||
@ -29,6 +29,7 @@ export const theme: Theme = plumeTheme({
|
||||
annotation: true,
|
||||
abbr: true,
|
||||
timeline: true,
|
||||
collapse: true,
|
||||
imageSize: 'all',
|
||||
pdf: true,
|
||||
caniuse: true,
|
||||
|
||||
@ -161,6 +161,12 @@ export default defineUserConfig({
|
||||
- **默认值**: `false`
|
||||
- **详情**: 是否启用时间线容器语法
|
||||
|
||||
### collapse
|
||||
|
||||
- **类型**: `boolean`
|
||||
- **默认值**: `false`
|
||||
- **详情**: 是否启用折叠面板容器语法
|
||||
|
||||
### demo
|
||||
|
||||
- **类型**: `boolean`
|
||||
|
||||
269
docs/notes/theme/guide/markdown/collapse.md
Normal file
269
docs/notes/theme/guide/markdown/collapse.md
Normal file
@ -0,0 +1,269 @@
|
||||
---
|
||||
title: 折叠面板
|
||||
icon: carbon:collapse-categories
|
||||
createTime: 2025/03/22 22:27:22
|
||||
permalink: /guide/markdown/collapse/
|
||||
badge:
|
||||
type: tip
|
||||
text: 1.0.0-rc.137 +
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
在 markdown 中,使用 `::: collapse` 容器,包含 markdown 无序列表语法,实现 ==折叠面板== 。
|
||||
|
||||
- 支持通过 `accordion` 设置为 ==手风琴== 模式
|
||||
|
||||
## 启用
|
||||
|
||||
该功能默认不启用,你需要在 `theme` 配置中启用。
|
||||
|
||||
```ts title=".vuepress/config.ts"
|
||||
export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
markdown: {
|
||||
collapse: true, // [!code ++]
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
在 markdown 中,使用 `::: collapse` 容器,包含 markdown 无序列表语法,每一项为一个单独的可折叠区域。
|
||||
|
||||
```md title="collapse.md"
|
||||
::: collapse
|
||||
- 标题 1 <!-- 标题,点击控制 展开/折叠 -->
|
||||
<!-- 标题与内容必须空一行 -->
|
||||
内容 <!-- 内容,被折叠的区域-->
|
||||
|
||||
- 标题 2
|
||||
|
||||
内容
|
||||
:::
|
||||
```
|
||||
|
||||
对于列表的每一个项:
|
||||
|
||||
- 从 __首行开始__ 到 __首个空行__,均为 __标题__
|
||||
|
||||
- __首个空行之后__: 正文内容
|
||||
|
||||
:::important 请注意添加正确的缩进
|
||||
:::
|
||||
|
||||
__一个简单的例子:__
|
||||
|
||||
__输入:__
|
||||
|
||||
```md
|
||||
::: collapse
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
|
||||
## 配置
|
||||
|
||||
在 `::: collapse` 容器语法之后,跟随配置项:
|
||||
|
||||
- `accordion` :折叠面板设置为 ==手风琴== 模式,在手风琴模式下,只允许展开一个面板,点击其他面板会关闭之前的面板。
|
||||
- `expand` :默认展开面板,在手风琴模式下无效。
|
||||
|
||||
在列表项,标题之前,可通过特殊标记 `:+` / `:-` 来设置当前项是否 __展开 / 折叠__。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
__输入:__
|
||||
|
||||
```md
|
||||
::: collapse
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
|
||||
### 默认全部展开
|
||||
|
||||
添加 `expand` 选项,默认展开所有面板
|
||||
|
||||
__输入:__
|
||||
|
||||
```md /expand/
|
||||
::: collapse expand
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse expand
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
:::
|
||||
|
||||
### 手风琴模式
|
||||
|
||||
添加 `accordion` 选项,设置为手风琴模式,只允许展开一个面板,点击其他面板会关闭之前的面板
|
||||
|
||||
```md /accordion/
|
||||
::: collapse accordion
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse accordion
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
|
||||
### `:+` 标记项为展开
|
||||
|
||||
折叠面板默认全部关闭,可以使用 `:+` 标记项初始状态为展开。
|
||||
|
||||
__输入:__
|
||||
|
||||
```md /:+/
|
||||
::: collapse
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- :+ 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- :+ 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- :+ 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- :+ 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
|
||||
### `:-` 标记项为折叠
|
||||
|
||||
折叠面板配置 `expand` 时默认全部展开,可以使用 `:-` 标记项初始状态为折叠。
|
||||
|
||||
__输入:__
|
||||
|
||||
```md /:-/
|
||||
::: collapse expand
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- :- 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
```
|
||||
|
||||
__输出:__
|
||||
|
||||
::: collapse expand
|
||||
|
||||
- 标题 1
|
||||
|
||||
正文内容
|
||||
|
||||
- :- 标题 2
|
||||
|
||||
正文内容
|
||||
|
||||
- 标题 3
|
||||
|
||||
正文内容
|
||||
:::
|
||||
@ -0,0 +1,32 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`collapsePlugin > should work 1`] = `
|
||||
"<VPCollapse><VPCollapseItem expand :index="0"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="1"><template #title><code>code</code>标题</template><p>内容</p>
|
||||
<ul>
|
||||
<li>列表 1</li>
|
||||
<li>列表 2</li>
|
||||
</ul>
|
||||
</VPCollapseItem><VPCollapseItem :index="2"><template #title><code>code</code> 标题</template><p>内容</p>
|
||||
</VPCollapseItem></VPCollapse>"
|
||||
`;
|
||||
|
||||
exports[`collapsePlugin > should work with accordion 1`] = `
|
||||
"<VPCollapse accordion><VPCollapseItem :index="0"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="1"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="2"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem></VPCollapse><VPCollapse accordion :index="0"><VPCollapseItem :index="0"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="1"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="2"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem></VPCollapse><VPCollapse accordion :index="1"><VPCollapseItem :index="0"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem expand :index="1"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="2"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem></VPCollapse>"
|
||||
`;
|
||||
|
||||
exports[`collapsePlugin > should work with expand 1`] = `
|
||||
"<VPCollapse><VPCollapseItem expand :index="0"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem expand :index="1"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem><VPCollapseItem :index="2"><template #title>标题</template><p>内容</p>
|
||||
</VPCollapseItem></VPCollapse>"
|
||||
`;
|
||||
93
plugins/plugin-md-power/__test__/collapse.spec.ts
Normal file
93
plugins/plugin-md-power/__test__/collapse.spec.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collapsePlugin } from '../src/node/container/collapse.js'
|
||||
|
||||
describe('collapsePlugin', () => {
|
||||
const md = new MarkdownIt().use(collapsePlugin)
|
||||
it('should work', () => {
|
||||
const code = `\
|
||||
::: collapse
|
||||
- :+ 标题
|
||||
|
||||
内容
|
||||
|
||||
- :- \`code\`标题
|
||||
|
||||
内容
|
||||
- 列表 1
|
||||
- 列表 2
|
||||
|
||||
- \`code\` 标题
|
||||
|
||||
内容
|
||||
:::
|
||||
`
|
||||
expect(md.render(code)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should work with expand', () => {
|
||||
const code = `\
|
||||
::: collapse expand
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- :- 标题
|
||||
|
||||
内容
|
||||
:::
|
||||
`
|
||||
expect(md.render(code)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should work with accordion', () => {
|
||||
const code = `\
|
||||
::: collapse accordion
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
:::
|
||||
|
||||
::: collapse accordion expand
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
:::
|
||||
|
||||
::: collapse accordion
|
||||
- 标题
|
||||
|
||||
内容
|
||||
|
||||
- :+ 标题
|
||||
|
||||
内容
|
||||
|
||||
- 标题
|
||||
|
||||
内容
|
||||
:::
|
||||
`
|
||||
expect(md.render(code)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
31
plugins/plugin-md-power/src/client/components/VPCollapse.vue
Normal file
31
plugins/plugin-md-power/src/client/components/VPCollapse.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
import { INJECT_COLLAPSE_KEY } from '../options.js'
|
||||
|
||||
const props = defineProps<{
|
||||
accordion?: boolean
|
||||
index?: number
|
||||
}>()
|
||||
|
||||
const currentIndex = ref<number | undefined>(props.index)
|
||||
|
||||
provide(INJECT_COLLAPSE_KEY, {
|
||||
accordion: props.accordion ?? false,
|
||||
index: currentIndex,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-collapse">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-collapse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
118
plugins/plugin-md-power/src/client/components/VPCollapseItem.vue
Normal file
118
plugins/plugin-md-power/src/client/components/VPCollapseItem.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue'
|
||||
import { inject, ref, watch } from 'vue'
|
||||
import { INJECT_COLLAPSE_KEY } from '../options.js'
|
||||
import VPFadeInExpandTransition from './VPFadeInExpandTransition.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
expand?: boolean
|
||||
index: number
|
||||
}>()
|
||||
|
||||
const collapse = inject<{
|
||||
accordion: boolean
|
||||
index: Ref<number | undefined>
|
||||
}>(INJECT_COLLAPSE_KEY)
|
||||
|
||||
if (__VUEPRESS_DEV__ && !collapse) {
|
||||
throw new Error('<VPCollapseItem /> must be used inside <VPCollapse />')
|
||||
}
|
||||
|
||||
const expand = ref(
|
||||
collapse?.accordion && typeof collapse.index.value !== 'undefined'
|
||||
? props.index === collapse.index.value
|
||||
: props.expand,
|
||||
)
|
||||
|
||||
if (collapse?.accordion) {
|
||||
watch(collapse?.index, () => {
|
||||
expand.value = collapse?.index.value === props.index
|
||||
})
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (collapse?.accordion) {
|
||||
if (collapse.index.value === props.index && expand.value) {
|
||||
expand.value = false
|
||||
}
|
||||
else {
|
||||
collapse!.index.value = props.index!
|
||||
expand.value = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
expand.value = !expand.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-collapse-item" :class="{ expand }">
|
||||
<div class="vp-collapse-header" @click="toggle">
|
||||
<span class="vpi-chevron-right" />
|
||||
<p class="vp-collapse-title">
|
||||
<slot name="title" />
|
||||
</p>
|
||||
</div>
|
||||
<VPFadeInExpandTransition>
|
||||
<div v-show="expand" class="vp-collapse-content">
|
||||
<div class="vp-collapse-content-inner">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</VPFadeInExpandTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-collapse-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 16px;
|
||||
border-top: solid 1px var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.vp-collapse-item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.vp-collapse-header {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vp-collapse-header .vpi-chevron-right {
|
||||
align-self: baseline;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform var(--vp-t-color);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.vp-collapse-item.expand .vpi-chevron-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.vp-collapse-header .vp-collapse-title {
|
||||
flex: 1 2;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.vp-collapse-content-inner {
|
||||
padding-top: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.vp-collapse-content-inner > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vp-collapse-content-inner > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { Transition, TransitionGroup } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
group?: boolean
|
||||
appear?: boolean
|
||||
mode?: 'in-out' | 'out-in' | 'default'
|
||||
onLeave?: () => void
|
||||
onAfterLeave?: () => void
|
||||
onAfterEnter?: () => void
|
||||
width?: boolean
|
||||
}>()
|
||||
|
||||
function handleBeforeLeave(el: HTMLElement): void {
|
||||
if (props.width) {
|
||||
el.style.maxWidth = `${el.offsetWidth}px`
|
||||
}
|
||||
else {
|
||||
el.style.maxHeight = `${el.offsetHeight}px`
|
||||
}
|
||||
void el.offsetWidth
|
||||
}
|
||||
|
||||
function handleLeave(el: HTMLElement): void {
|
||||
if (props.width) {
|
||||
el.style.maxWidth = '0'
|
||||
}
|
||||
else {
|
||||
el.style.maxHeight = '0'
|
||||
}
|
||||
void el.offsetWidth
|
||||
props.onLeave?.()
|
||||
}
|
||||
|
||||
function handleAfterLeave(el: HTMLElement): void {
|
||||
if (props.width) {
|
||||
el.style.maxWidth = ''
|
||||
}
|
||||
else {
|
||||
el.style.maxHeight = ''
|
||||
}
|
||||
props.onAfterLeave?.()
|
||||
}
|
||||
|
||||
function handleEnter(el: HTMLElement): void {
|
||||
el.style.transition = 'none'
|
||||
if (props.width) {
|
||||
const memorizedWidth = el.offsetWidth
|
||||
el.style.maxWidth = '0'
|
||||
void el.offsetWidth
|
||||
el.style.transition = ''
|
||||
el.style.maxWidth = `${memorizedWidth}px`
|
||||
}
|
||||
else {
|
||||
const memorizedHeight = el.offsetHeight
|
||||
el.style.maxHeight = '0'
|
||||
void el.offsetWidth
|
||||
el.style.transition = ''
|
||||
el.style.maxHeight = `${memorizedHeight}px`
|
||||
}
|
||||
void el.offsetWidth
|
||||
}
|
||||
|
||||
function handleAfterEnter(el: HTMLElement): void {
|
||||
if (props.width) {
|
||||
el.style.maxWidth = ''
|
||||
}
|
||||
else {
|
||||
el.style.maxHeight = ''
|
||||
}
|
||||
props.onAfterEnter?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="group ? TransitionGroup : Transition"
|
||||
:name="width ? 'fade-in-width-expand' : 'fade-in-height-expand'"
|
||||
:mode
|
||||
:appear
|
||||
@enter="handleEnter"
|
||||
@after-enter="handleAfterEnter"
|
||||
@before-leave="handleBeforeLeave"
|
||||
@leave="handleLeave"
|
||||
@after-leave="handleAfterLeave"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.fade-in-height-expand-leave-from,
|
||||
.fade-in-height-expand-enter-to,
|
||||
.fade-in-width-expand-leave-from,
|
||||
.fade-in-width-expand-enter-to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-in-height-expand-leave-to,
|
||||
.fade-in-height-expand-enter-from {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-in-height-expand-leave-active {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
opacity cubic-bezier(0, 0, 0.2, 1) 0.3s,
|
||||
margin-top cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
margin-bottom cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
padding-top cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
padding-bottom cubic-bezier(0.4, 0, 0.2, 1) 0.3s;
|
||||
}
|
||||
|
||||
.fade-in-height-expand-enter-active {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
opacity cubic-bezier(0.4, 0, 1, 1) 0.3s,
|
||||
margin-top cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
margin-bottom cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
padding-top cubic-bezier(0.4, 0, 0.2, 1) 0.3s,
|
||||
padding-bottom cubic-bezier(0.4, 0, 0.2, 1) 0.3s;
|
||||
}
|
||||
|
||||
.fade-in-width-expand-leave-to,
|
||||
.fade-in-width-expand-enter-from {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.fade-in-width-expand-leave-active {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-width cubic-bezier(0.4, 0, 0.2, 1) 0.2s 0.1s,
|
||||
opacity cubic-bezier(0.4, 0, 0.2, 1) 0.2s,
|
||||
margin-right cubic-bezier(0.4, 0, 0.2, 1) 0.2s 0.1s,
|
||||
margin-left cubic-bezier(0.4, 0, 0.2, 1) 0.2s 0.1s;
|
||||
}
|
||||
|
||||
.fade-in-width-expand-enter-active {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-width cubic-bezier(0.4, 0, 0.2, 1) 0.2s,
|
||||
opacity cubic-bezier(0.4, 0, 0.2, 1) 0.2s 0.1s,
|
||||
margin-right cubic-bezier(0.4, 0, 0.2, 1) 0.2s,
|
||||
margin-left cubic-bezier(0.4, 0, 0.2, 1) 0.2s;
|
||||
}
|
||||
</style>
|
||||
@ -30,3 +30,7 @@ if (installed.mpegtsjs) {
|
||||
export const INJECT_TIMELINE_KEY = Symbol(
|
||||
__VUEPRESS_DEV__ ? 'timeline' : '',
|
||||
)
|
||||
|
||||
export const INJECT_COLLAPSE_KEY = Symbol(
|
||||
__VUEPRESS_DEV__ ? 'collapse' : '',
|
||||
)
|
||||
|
||||
119
plugins/plugin-md-power/src/node/container/collapse.ts
Normal file
119
plugins/plugin-md-power/src/node/container/collapse.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* ::: collapse accordion
|
||||
* - + 标题
|
||||
* 内容
|
||||
* - - 标题
|
||||
* 内容
|
||||
* :::
|
||||
*/
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import { resolveAttrs } from '.././utils/resolveAttrs.js'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
interface CollapseMeta {
|
||||
accordion?: boolean
|
||||
expand?: boolean
|
||||
}
|
||||
|
||||
interface CollapseItemMeta {
|
||||
expand?: boolean
|
||||
index?: number
|
||||
}
|
||||
|
||||
export function collapsePlugin(md: Markdown): void {
|
||||
createContainerPlugin(md, 'collapse', {
|
||||
before: (info, tokens, index) => {
|
||||
const { attrs } = resolveAttrs<CollapseMeta>(info)
|
||||
const idx = parseCollapse(tokens, index, attrs)
|
||||
const { accordion } = attrs
|
||||
|
||||
return `<VPCollapse${accordion ? ' accordion' : ''}${idx !== undefined ? ` :index="${idx}"` : ''}>`
|
||||
},
|
||||
after: () => `</VPCollapse>`,
|
||||
})
|
||||
md.renderer.rules.collapse_item_open = (tokens, idx) => {
|
||||
const token = tokens[idx]
|
||||
const { expand, index } = token.meta as CollapseItemMeta
|
||||
return `<VPCollapseItem${expand ? ' expand' : ''}${` :index="${index}"`}>`
|
||||
}
|
||||
md.renderer.rules.collapse_item_close = () => '</VPCollapseItem>'
|
||||
md.renderer.rules.collapse_item_title_open = () => '<template #title>'
|
||||
md.renderer.rules.collapse_item_title_close = () => '</template>'
|
||||
}
|
||||
|
||||
function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): number | void {
|
||||
const listStack: number[] = [] // 记录列表嵌套深度
|
||||
let idx = -1 // 记录当前列表项下标
|
||||
let defaultIndex: number | undefined
|
||||
let hashExpand = false
|
||||
for (let i = index + 1; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
if (token.type === 'container_collapse_close') {
|
||||
break
|
||||
}
|
||||
// 列表层级追踪
|
||||
if (token.type === 'bullet_list_open') {
|
||||
listStack.push(0) // 每个新列表初始层级为0
|
||||
if (listStack.length === 1)
|
||||
token.hidden = true
|
||||
}
|
||||
else if (token.type === 'bullet_list_close') {
|
||||
listStack.pop()
|
||||
if (listStack.length === 0)
|
||||
token.hidden = true
|
||||
}
|
||||
else if (token.type === 'list_item_open') {
|
||||
const currentLevel = listStack.length
|
||||
// 仅处理根级列表项(层级1)
|
||||
if (currentLevel === 1) {
|
||||
token.type = 'collapse_item_open'
|
||||
tokens[i + 1].type = 'collapse_item_title_open'
|
||||
tokens[i + 3].type = 'collapse_item_title_close'
|
||||
|
||||
idx++
|
||||
|
||||
const inlineToken = tokens[i + 2]
|
||||
const firstToken = inlineToken.children![0]
|
||||
let flag: string = ''
|
||||
let expand: boolean | undefined
|
||||
if (firstToken.type === 'text') {
|
||||
firstToken.content = firstToken.content.trim().replace(/^:[+\-]\s*/, (match) => {
|
||||
flag = match.trim()
|
||||
return ''
|
||||
})
|
||||
}
|
||||
if (attrs.accordion) {
|
||||
if (!hashExpand && flag === ':+') {
|
||||
expand = hashExpand = true
|
||||
defaultIndex = idx
|
||||
}
|
||||
}
|
||||
else if (flag === ':+') {
|
||||
expand = true
|
||||
}
|
||||
else if (flag === ':-') {
|
||||
expand = false
|
||||
}
|
||||
else {
|
||||
expand = !!attrs.expand
|
||||
}
|
||||
|
||||
token.meta = {
|
||||
index: idx,
|
||||
expand,
|
||||
} as CollapseItemMeta
|
||||
}
|
||||
}
|
||||
else if (token.type === 'list_item_close') {
|
||||
const currentLevel = listStack.length
|
||||
if (currentLevel === 1) {
|
||||
token.type = 'collapse_item_close'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attrs.accordion && attrs.expand && !hashExpand) {
|
||||
defaultIndex = 0
|
||||
}
|
||||
return defaultIndex
|
||||
}
|
||||
@ -1,21 +1,27 @@
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import container from 'markdown-it-container'
|
||||
|
||||
type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never
|
||||
|
||||
export interface ContainerOptions {
|
||||
before?: (info: string, tokens: Token[], idx: number) => string
|
||||
after?: (info: string, tokens: Token[], idx: number) => string
|
||||
before?: (info: string, ...args: RenderRuleParams) => string
|
||||
after?: (info: string, ...args: RenderRuleParams) => string
|
||||
}
|
||||
|
||||
export function createContainerPlugin(md: Markdown, type: string, options: ContainerOptions = {}) {
|
||||
const render = (tokens: Token[], index: number): string => {
|
||||
export function createContainerPlugin(
|
||||
md: Markdown,
|
||||
type: string,
|
||||
{ before, after }: ContainerOptions = {},
|
||||
) {
|
||||
const render: RenderRule = (tokens, index, options, env): string => {
|
||||
const token = tokens[index]
|
||||
const info = token.info.trim().slice(type.length).trim() || ''
|
||||
if (token.nesting === 1) {
|
||||
return options.before?.(info, tokens, index) || `<div class="custom-container ${type}">`
|
||||
return before?.(info, tokens, index, options, env) || `<div class="custom-container ${type}">`
|
||||
}
|
||||
else {
|
||||
return options.after?.(info, tokens, index) || '</div>'
|
||||
return after?.(info, tokens, index, options, env) || '</div>'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { isPlainObject } from '@vuepress/helper'
|
||||
import { alignPlugin } from './align.js'
|
||||
import { cardPlugin } from './card.js'
|
||||
import { codeTabs } from './codeTabs.js'
|
||||
import { collapsePlugin } from './collapse.js'
|
||||
import { demoWrapperPlugin } from './demoWrapper.js'
|
||||
import { fileTreePlugin } from './fileTree.js'
|
||||
import { langReplPlugin } from './langRepl.js'
|
||||
@ -50,4 +51,7 @@ export async function containerPlugin(
|
||||
|
||||
if (options.timeline)
|
||||
timelinePlugin(md)
|
||||
|
||||
if (options.collapse)
|
||||
collapsePlugin(md)
|
||||
}
|
||||
|
||||
@ -109,6 +109,13 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
enhances.add(`app.component('VPTimelineItem', VPTimelineItem)`)
|
||||
}
|
||||
|
||||
if (options.collapse) {
|
||||
imports.add(`import VPCollapse from '${CLIENT_FOLDER}components/VPCollapse.vue'`)
|
||||
imports.add(`import VPCollapseItem from '${CLIENT_FOLDER}components/VPCollapseItem.vue'`)
|
||||
enhances.add(`app.component('VPCollapse', VPCollapse)`)
|
||||
enhances.add(`app.component('VPCollapseItem', VPCollapseItem)`)
|
||||
}
|
||||
|
||||
return app.writeTemp(
|
||||
'md-power/config.js',
|
||||
`\
|
||||
|
||||
@ -61,7 +61,9 @@ export interface MarkdownPowerPluginOptions {
|
||||
*
|
||||
* ```md
|
||||
* ::: timeline
|
||||
* - title time="Q1" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
|
||||
* - title
|
||||
* time="Q1" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
|
||||
*
|
||||
* xxx
|
||||
* :::
|
||||
* ```
|
||||
@ -70,6 +72,25 @@ export interface MarkdownPowerPluginOptions {
|
||||
*/
|
||||
timeline?: boolean
|
||||
|
||||
/**
|
||||
* 是否启用 collapse 折叠面板 语法
|
||||
*
|
||||
* ```md
|
||||
* ::: collapse accordion
|
||||
* - + title
|
||||
*
|
||||
* content
|
||||
*
|
||||
* - - title
|
||||
*
|
||||
* content
|
||||
* :::
|
||||
* ```
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
collapse?: boolean
|
||||
|
||||
// video embed
|
||||
/**
|
||||
* 是否启用 bilibili 视频嵌入
|
||||
|
||||
@ -58,6 +58,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
|
||||
'repl',
|
||||
'replit',
|
||||
'timeline',
|
||||
'collapse',
|
||||
'youtube',
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user