mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
* feat(plugin-md-power): add code-tree container and embed syntax, close #567 * chore: tweak
This commit is contained in:
parent
3b214f1b58
commit
31e3b41a27
@ -42,6 +42,7 @@ export const themeGuide = defineNoteConfig({
|
||||
'card',
|
||||
'steps',
|
||||
'file-tree',
|
||||
'code-tree',
|
||||
'field',
|
||||
'tabs',
|
||||
'timeline',
|
||||
|
||||
@ -31,6 +31,7 @@ export const theme: Theme = plumeTheme({
|
||||
timeline: true,
|
||||
collapse: true,
|
||||
chat: true,
|
||||
codeTree: true,
|
||||
field: true,
|
||||
imageSize: 'all',
|
||||
pdf: true,
|
||||
|
||||
170
docs/notes/theme/guide/markdown/code-tree.md
Normal file
170
docs/notes/theme/guide/markdown/code-tree.md
Normal file
@ -0,0 +1,170 @@
|
||||
---
|
||||
title: 代码树
|
||||
icon: stash:side-peek
|
||||
createTime: 2025/05/02 05:59:44
|
||||
permalink: /guide/6smvgtbx/
|
||||
badge: 新
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
在 markdown 中,使用 `::: code-tress` 容器,或者使用 `@[code-tree](dir_path)`,
|
||||
可以显示一个带有文件树的代码块区域。
|
||||
|
||||
相比于 代码块分组,代码树 可以更加清晰地展示代码文件的组织结构,以及文件的依赖关系。
|
||||
|
||||
## 启用
|
||||
|
||||
该功能默认不启用,你需要在 `theme` 配置中启用。
|
||||
|
||||
```ts title=".vuepress/config.ts"
|
||||
export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
markdown: {
|
||||
codeTree: true, // [!code ++]
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
主题提供了 两种使用方式:
|
||||
|
||||
### code-tree 容器
|
||||
|
||||
````md
|
||||
::: code-tree title="Project Name" height="400px" entry="filepath"
|
||||
```lang title="filepath" :active
|
||||
<!-- code content-->
|
||||
```
|
||||
|
||||
```lang title="filepath"
|
||||
<!-- code content-->
|
||||
```
|
||||
<!-- 更多代码块 -->
|
||||
:::
|
||||
````
|
||||
|
||||
使用 `::: code-tree` 容器包裹多个代码块。
|
||||
|
||||
- 在 `::: code-tree` 后使用 `title="Project Name"` 声明代码树的标题
|
||||
- 在 `::: code-tree` 后使用 `height="400px"` 声明代码树的高度
|
||||
- 在 `::: code-tree` 后使用 `entry="filepath"` 声明默认展开的文件路径
|
||||
- 在代码块 <code>\`\`\` lang</code> 后使用 `title="filepath"` 声明当前代码块的文件路径
|
||||
- 如果在 `::: code-tree` 未声明 `entry="filepath"`,可以在代码块 <code>\`\`\` lang</code> 后使用 `:active` 声明当前代码块为展开状态
|
||||
- 如果未指定展开的文件路径,默认展开第一个文件
|
||||
|
||||
::: details 代码块上为什么是 `title="filepath"` 而不是 `filepath="filepath"` ?
|
||||
因为主题已经在 [代码块上提供了标题语法的支持](../code/features.md#代码块标题) ,沿用已有的语法支持
|
||||
可以减少学习成本。
|
||||
:::
|
||||
|
||||
**输入:**
|
||||
|
||||
````md :collapsed-lines
|
||||
::: code-tree title="Vue App" height="400px" entry="src/main.ts"
|
||||
```vue title="src/components/HelloWorld.vue"
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>Hello World</h1>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
```vue title="src/App.vue"
|
||||
<template>
|
||||
<div id="app">
|
||||
<h3>vuepress-theme-plume</h3>
|
||||
<HelloWorld />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts title="src/main.ts"
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
```
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "Vue App",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
::: code-tree title="Vue App" height="400px" entry="src/main.ts"
|
||||
|
||||
```vue title="src/components/HelloWorld.vue"
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>Hello World</h1>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
```vue title="src/App.vue"
|
||||
<template>
|
||||
<div id="app">
|
||||
<h3>vuepress-theme-plume</h3>
|
||||
<HelloWorld />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts title="src/main.ts"
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
```
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "Vue App",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 从目录导入 code-tree
|
||||
|
||||
主题支持通过以下语法从目录导入 `code-tree`:
|
||||
|
||||
```md
|
||||
<!-- 简单导入 -->
|
||||
@[code-tree](dir_path)
|
||||
|
||||
<!-- 添加的配置 -->
|
||||
@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)
|
||||
```
|
||||
|
||||
- **dir_path**:
|
||||
当传入绝对路径,即以 `/` 开头时,从文档站点的 源目录 开始查找。
|
||||
当传入相对路径时,即以 `.` 开头时,表示相对于当前 markdown 文件。
|
||||
|
||||
- **title**: 代码树标题,可选,默认为空
|
||||
- **height**: 代码树高度,可选,默认为空
|
||||
- **entry**: 默认展开的文件路径,可选,默认为第一个文件
|
||||
|
||||
**输入:**
|
||||
|
||||
```md
|
||||
<!-- 此目录为主题仓库 `docs/.vuepress/notes/` -->
|
||||
@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes)
|
||||
```
|
||||
|
||||
**输出:**
|
||||
|
||||
@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes)
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveAttrs } from '../src/node/utils/resolveAttrs.js'
|
||||
import { resolveAttr, resolveAttrs } from '../src/node/utils/resolveAttrs.js'
|
||||
|
||||
describe('resolveAttrs(info)', () => {
|
||||
it('should resolve attrs', () => {
|
||||
@ -10,6 +10,16 @@ describe('resolveAttrs(info)', () => {
|
||||
attrs: { a: '1' },
|
||||
})
|
||||
|
||||
expect(resolveAttrs('a=1 b=2 c')).toEqual({
|
||||
rawAttrs: 'a=1 b=2 c',
|
||||
attrs: { a: '1', b: '2', c: true },
|
||||
})
|
||||
|
||||
expect(resolveAttrs('a=1 b=true c=false')).toEqual({
|
||||
rawAttrs: 'a=1 b=true c=false',
|
||||
attrs: { a: '1', b: true, c: false },
|
||||
})
|
||||
|
||||
expect(resolveAttrs('a="1" b="2"')).toMatchObject({
|
||||
rawAttrs: 'a="1" b="2"',
|
||||
attrs: { a: '1', b: '2' },
|
||||
@ -33,3 +43,13 @@ describe('resolveAttrs(info)', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveAttr(info, key)', () => {
|
||||
it('should resolve attr', () => {
|
||||
expect(resolveAttr('a="1"', 'a')).toEqual('1')
|
||||
expect(resolveAttr('a="1"', 'b')).toEqual(undefined)
|
||||
expect(resolveAttr('a=1', 'a')).toEqual('1')
|
||||
expect(resolveAttr('a=\'1\'', 'a')).toEqual('1')
|
||||
expect(resolveAttr('a', 'a')).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
"markdown-it-container": "catalog:prod",
|
||||
"nanoid": "catalog:prod",
|
||||
"shiki": "catalog:prod",
|
||||
"tinyglobby": "catalog:prod",
|
||||
"tm-grammars": "catalog:prod",
|
||||
"tm-themes": "catalog:prod",
|
||||
"vue": "catalog:prod"
|
||||
|
||||
@ -9,6 +9,7 @@ const props = defineProps<{
|
||||
diff?: 'add' | 'remove'
|
||||
expanded?: boolean
|
||||
focus?: boolean
|
||||
filepath?: string
|
||||
}>()
|
||||
|
||||
const activeFileTreeNode = inject<Ref<string>>('active-file-tree-node', ref(''))
|
||||
@ -23,7 +24,7 @@ function nodeClick() {
|
||||
if (props.filename === '…' || props.filename === '...')
|
||||
return
|
||||
|
||||
onNodeClick(props.filename, props.type)
|
||||
onNodeClick(props.filepath || props.filename, props.type)
|
||||
}
|
||||
|
||||
function toggle(ev: MouseEvent) {
|
||||
@ -47,7 +48,7 @@ function toggle(ev: MouseEvent) {
|
||||
[type]: true,
|
||||
focus,
|
||||
expanded: type === 'folder' ? active : false,
|
||||
active: type === 'file' ? activeFileTreeNode === filename : false,
|
||||
active: type === 'file' ? activeFileTreeNode === filepath : false,
|
||||
diff,
|
||||
add: diff === 'add',
|
||||
remove: diff === 'remove',
|
||||
|
||||
159
plugins/plugin-md-power/src/client/components/VPCodeTree.vue
Normal file
159
plugins/plugin-md-power/src/client/components/VPCodeTree.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
height?: string
|
||||
entryFile?: string
|
||||
}>(), { height: '320px' })
|
||||
|
||||
const activeNode = ref(props.entryFile || '')
|
||||
const isEmpty = ref(true)
|
||||
const codePanel = useTemplateRef<HTMLDivElement>('codePanel')
|
||||
|
||||
provide('active-file-tree-node', activeNode)
|
||||
provide('on-file-tree-node-click', (filepath: string, type: 'file' | 'folder') => {
|
||||
if (type === 'file') {
|
||||
activeNode.value = filepath
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
() => activeNode.value,
|
||||
() => {
|
||||
if (codePanel.value) {
|
||||
const items = Array.from(codePanel.value.querySelectorAll('.code-block-title'))
|
||||
let hasActive = false
|
||||
items.forEach((item) => {
|
||||
if (item.getAttribute('data-title') === activeNode.value) {
|
||||
item.classList.add('active')
|
||||
hasActive = true
|
||||
}
|
||||
else {
|
||||
item.classList.remove('active')
|
||||
}
|
||||
})
|
||||
isEmpty.value = !hasActive
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-code-tree">
|
||||
<div class="code-tree-panel" :style="{ 'max-height': props.height }">
|
||||
<div v-if="title" class="code-tree-title" :title="title">
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="vp-file-tree">
|
||||
<slot name="file-tree" />
|
||||
</div>
|
||||
</div>
|
||||
<div ref="codePanel" class="code-panel" :style="{ height: props.height }">
|
||||
<slot />
|
||||
<div v-if="isEmpty" class="code-tree-empty">
|
||||
<span class="vpi-code-tree-empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-code-tree {
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
overflow: hidden;
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.vp-code-tree {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.vp-code-tree .code-tree-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: solid 1px var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.vp-code-tree .code-tree-panel {
|
||||
border-right: solid 1px var(--vp-c-divider);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vp-code-tree .code-tree-panel .code-tree-title {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-bottom: solid 1px var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.vp-code-tree .code-tree-panel .vp-file-tree {
|
||||
flex: 1 2;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-tree-panel .vp-file-tree .vp-file-tree-info.file {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel [class*="language-"] {
|
||||
flex: 1 2;
|
||||
margin: 16px 0 0;
|
||||
overflow: auto;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel .code-block-title {
|
||||
display: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel .code-block-title.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel .code-block-title .code-block-title-bar {
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel .code-tree-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vp-code-tree .code-panel .code-tree-empty .vpi-code-tree-empty {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256' viewBox='0 0 256 256'%3E%3Cpath fill='%23000' d='m198.24 62.63l15.68-17.25a8 8 0 0 0-11.84-10.76L186.4 51.86A95.95 95.95 0 0 0 57.76 193.37l-15.68 17.25a8 8 0 1 0 11.84 10.76l15.68-17.24A95.95 95.95 0 0 0 198.24 62.63M48 128a80 80 0 0 1 127.6-64.25l-107 117.73A79.63 79.63 0 0 1 48 128m80 80a79.55 79.55 0 0 1-47.6-15.75l107-117.73A79.95 79.95 0 0 1 128 208'/%3E%3C/svg%3E");
|
||||
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
color: var(--vp-c-default-soft);
|
||||
}
|
||||
</style>
|
||||
227
plugins/plugin-md-power/src/node/container/codeTree.ts
Normal file
227
plugins/plugin-md-power/src/node/container/codeTree.ts
Normal file
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @module CodeTree
|
||||
*
|
||||
* code-tree 容器
|
||||
* ````md
|
||||
* ::: code-tree title="Project Name" height="400px" entry="filepath"
|
||||
* ``` lang :active title="filepath"
|
||||
* ```
|
||||
* <!-- more code block -->
|
||||
* :::
|
||||
* ````
|
||||
*
|
||||
* embed syntax
|
||||
*
|
||||
* `@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)`
|
||||
*/
|
||||
|
||||
import type { App, Page } from 'vuepress/core'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import type { CodeTreeOptions } from '../../shared/codeTree.js'
|
||||
import type { FileTreeIconMode } from '../../shared/fileTree.js'
|
||||
import type { FileTreeNodeProps } from './fileTree.js'
|
||||
import path from 'node:path'
|
||||
import { globSync } from 'tinyglobby'
|
||||
import { removeLeadingSlash } from 'vuepress/shared'
|
||||
import { findFile, readFileSync } from '../demo/supports/file.js'
|
||||
import { createEmbedRuleBlock } from '../embed/createEmbedRuleBlock.js'
|
||||
import { defaultFile, defaultFolder, getFileIcon } from '../fileIcons/index.js'
|
||||
import { parseRect } from '../utils/parseRect.js'
|
||||
import { resolveAttr, resolveAttrs } from '../utils/resolveAttrs.js'
|
||||
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
const UNSUPPORTED_FILE_TYPES = [
|
||||
/* image */
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'avif',
|
||||
'webp',
|
||||
/* media */
|
||||
'mp3',
|
||||
'mp4',
|
||||
'ogg',
|
||||
'm3u8',
|
||||
'm3u',
|
||||
'flv',
|
||||
'webm',
|
||||
'wav',
|
||||
'flac',
|
||||
'aac',
|
||||
/* document */
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xls',
|
||||
'xlsx',
|
||||
]
|
||||
|
||||
interface CodeTreeMeta {
|
||||
title?: string
|
||||
/**
|
||||
* 文件图标类型
|
||||
*/
|
||||
icon?: FileTreeIconMode
|
||||
/**
|
||||
* 代码树容器高度
|
||||
*/
|
||||
height?: string
|
||||
|
||||
/**
|
||||
* 入口文件,默认打开
|
||||
*/
|
||||
entry?: string
|
||||
}
|
||||
|
||||
interface FileTreeNode {
|
||||
level: number
|
||||
children?: FileTreeNode[]
|
||||
filename: string
|
||||
filepath?: string
|
||||
}
|
||||
|
||||
function parseFileNodes(files: string[]): FileTreeNode[] {
|
||||
const nodes: FileTreeNode[] = []
|
||||
for (const file of files) {
|
||||
const parts = removeLeadingSlash(file).split('/')
|
||||
let node = nodes
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const isFile = i === parts.length - 1
|
||||
let child = node.find(n => n.filename === part)
|
||||
if (!child) {
|
||||
child = {
|
||||
level: i + 1,
|
||||
filename: part,
|
||||
filepath: isFile ? file : undefined,
|
||||
children: isFile ? undefined : [],
|
||||
}
|
||||
node.push(child)
|
||||
}
|
||||
if (!isFile && child.children)
|
||||
node = child.children
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions = {}) {
|
||||
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
|
||||
mode ||= options.icon || 'colored'
|
||||
if (mode === 'simple')
|
||||
return type === 'folder' ? defaultFolder : defaultFile
|
||||
return getFileIcon(filename, type)
|
||||
}
|
||||
|
||||
function renderFileTree(nodes: FileTreeNode[], mode?: FileTreeIconMode): string {
|
||||
return nodes.map((node) => {
|
||||
const props: FileTreeNodeProps & { filepath?: string } = {
|
||||
filename: node.filename,
|
||||
level: node.level,
|
||||
type: node.children?.length ? 'folder' : 'file',
|
||||
expanded: true,
|
||||
filepath: node.filepath,
|
||||
}
|
||||
return `<FileTreeNode${stringifyAttrs(props)}>
|
||||
<template #icon><VPIcon name="${getIcon(node.filename, props.type, mode)}" /></template>
|
||||
${node.children?.length ? renderFileTree(node.children, mode) : ''}
|
||||
</FileTreeNode>`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
createContainerPlugin(md, 'code-tree', {
|
||||
before: (info, tokens, index) => {
|
||||
const files: string[] = []
|
||||
let activeFile: string | undefined
|
||||
for (
|
||||
let i = index + 1;
|
||||
!(
|
||||
tokens[i].nesting === -1
|
||||
&& tokens[i].type === 'container_code-tree_close'
|
||||
);
|
||||
i++
|
||||
) {
|
||||
const token = tokens[i]
|
||||
if (token.type === 'fence' && token.tag === 'code') {
|
||||
const fenceInfo = md.utils.unescapeAll(token.info)
|
||||
const title = resolveAttr(fenceInfo, 'title')
|
||||
if (title) {
|
||||
files.push(title)
|
||||
if (fenceInfo.includes(':active'))
|
||||
activeFile = title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { attrs } = resolveAttrs<CodeTreeMeta>(info)
|
||||
const { title, icon, height, entry } = attrs
|
||||
const fileTreeNodes = parseFileNodes(files)
|
||||
const entryFile = activeFile || entry || files[0]
|
||||
const h = height || String(options.height)
|
||||
return `<VPCodeTree${stringifyAttrs({ title, entryFile, height: h ? parseRect(h) : undefined })}><template #file-tree>${
|
||||
renderFileTree(fileTreeNodes, icon)
|
||||
}</template>`
|
||||
},
|
||||
after: () => '</VPCodeTree>',
|
||||
})
|
||||
|
||||
createEmbedRuleBlock(md, {
|
||||
type: 'code-tree',
|
||||
syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/,
|
||||
meta: ([, info, dir]) => {
|
||||
const { attrs } = resolveAttrs<CodeTreeMeta>(info)
|
||||
const h = attrs.height || String(options.height)
|
||||
return {
|
||||
title: attrs.title,
|
||||
entryFile: attrs.entry,
|
||||
icon: attrs.icon,
|
||||
height: h ? parseRect(h) : undefined,
|
||||
dir,
|
||||
}
|
||||
},
|
||||
content: ({ dir, icon, ...props }, _, env) => {
|
||||
const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[]
|
||||
const root = findFile(app, env, dir)
|
||||
const files = globSync('**/*', {
|
||||
cwd: root,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
ignore: ['**/node_modules/**', '**/.DS_Store', '**/.gitkeep'],
|
||||
}).sort((a, b) => {
|
||||
const al = a.split('/').length
|
||||
const bl = b.split('/').length
|
||||
return bl - al
|
||||
})
|
||||
props.entryFile ||= files[0]
|
||||
|
||||
const codeContent = files.map((file) => {
|
||||
const ext = path.extname(file).slice(1)
|
||||
if (UNSUPPORTED_FILE_TYPES.includes(ext)) {
|
||||
return ''
|
||||
}
|
||||
const filepath = path.join(root, file)
|
||||
codeTreeFiles.push(filepath)
|
||||
const content = readFileSync(filepath)
|
||||
return `\`\`\`${ext || 'txt'} title="${file}"\n${content}\n\`\`\``
|
||||
}).filter(Boolean).join('\n')
|
||||
|
||||
const fileTreeNodes = parseFileNodes(files)
|
||||
return `<VPCodeTree${stringifyAttrs(props)}><template #file-tree>${
|
||||
renderFileTree(fileTreeNodes, icon)
|
||||
}</template>${md.render(codeContent)}</VPCodeTree>`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function extendsPageWithCodeTree(page: Page): void {
|
||||
const markdownEnv = page.markdownEnv
|
||||
const codeTreeFiles = (markdownEnv.codeTreeFiles ?? []) as string[]
|
||||
if (codeTreeFiles.length)
|
||||
page.deps.push(...codeTreeFiles)
|
||||
}
|
||||
@ -16,7 +16,7 @@ interface FileTreeAttrs {
|
||||
icon?: FileTreeIconMode
|
||||
}
|
||||
|
||||
interface FileTreeNodeProps {
|
||||
export interface FileTreeNodeProps {
|
||||
filename: string
|
||||
comment?: string
|
||||
focus?: boolean
|
||||
|
||||
@ -6,6 +6,7 @@ import { alignPlugin } from './align.js'
|
||||
import { cardPlugin } from './card.js'
|
||||
import { chatPlugin } from './chat.js'
|
||||
import { codeTabs } from './codeTabs.js'
|
||||
import { codeTreePlugin } from './codeTree.js'
|
||||
import { collapsePlugin } from './collapse.js'
|
||||
import { demoWrapperPlugin } from './demoWrapper.js'
|
||||
import { fieldPlugin } from './field.js'
|
||||
@ -51,6 +52,10 @@ export async function containerPlugin(
|
||||
fileTreePlugin(md, isPlainObject(options.fileTree) ? options.fileTree : {})
|
||||
}
|
||||
|
||||
if (options.codeTree) {
|
||||
codeTreePlugin(md, app, isPlainObject(options.codeTree) ? options.codeTree : {})
|
||||
}
|
||||
|
||||
if (options.timeline)
|
||||
timelinePlugin(md)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Plugin } from 'vuepress/core'
|
||||
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
|
||||
import { addViteOptimizeDepsInclude } from '@vuepress/helper'
|
||||
import { isPackageExists } from 'local-pkg'
|
||||
import { extendsPageWithCodeTree } from './container/codeTree.js'
|
||||
import { containerPlugin } from './container/index.js'
|
||||
import { demoPlugin, demoWatcher, extendsPageWithDemo, waitDemoRender } from './demo/index.js'
|
||||
import { embedSyntaxPlugin } from './embed/index.js'
|
||||
@ -68,6 +69,9 @@ export function markdownPowerPlugin(
|
||||
extendsPage: (page) => {
|
||||
if (options.demo)
|
||||
extendsPageWithDemo(page)
|
||||
|
||||
if (options.codeTree)
|
||||
extendsPageWithCodeTree(page)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,11 +70,16 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
enhances.add(`app.component('CanIUseViewer', CanIUse)`)
|
||||
}
|
||||
|
||||
if (options.fileTree) {
|
||||
if (options.fileTree || options.codeTree) {
|
||||
imports.add(`import FileTreeNode from '${CLIENT_FOLDER}components/FileTreeNode.vue'`)
|
||||
enhances.add(`app.component('FileTreeNode', FileTreeNode)`)
|
||||
}
|
||||
|
||||
if (options.codeTree) {
|
||||
imports.add(`import VPCodeTree from '${CLIENT_FOLDER}components/VPCodeTree.vue'`)
|
||||
enhances.add(`app.component('VPCodeTree', VPCodeTree)`)
|
||||
}
|
||||
|
||||
if (options.artPlayer) {
|
||||
imports.add(`import ArtPlayer from '${CLIENT_FOLDER}components/ArtPlayer.vue'`)
|
||||
enhances.add(`app.component('ArtPlayer', ArtPlayer)`)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { camelCase } from '@pengzhanbo/utils'
|
||||
|
||||
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=\s*(?<quote>['"])(?<value>.+?)\k<quote>)?(?:\s+|$)/
|
||||
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuote>.+?)\k<quote>|=(?<valueWithoutQuote>\S+))?(?:\s+|$)/
|
||||
|
||||
export function resolveAttrs<T extends Record<string, any> = Record<string, any>>(info: string): {
|
||||
attrs: T
|
||||
@ -18,7 +18,8 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (matched = info.match(RE_ATTR_VALUE)) {
|
||||
const { attr, value = true } = matched.groups!
|
||||
const { attr, valueWithQuote, valueWithoutQuote } = matched.groups!
|
||||
const value = valueWithQuote || valueWithoutQuote || true
|
||||
let v = typeof value === 'string' ? value.trim() : value
|
||||
if (v === 'true')
|
||||
v = true
|
||||
@ -31,3 +32,9 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
|
||||
|
||||
return { attrs: attrs as T, rawAttrs }
|
||||
}
|
||||
|
||||
export function resolveAttr(info: string, key: string): string | undefined {
|
||||
const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`)
|
||||
const groups = info.match(pattern)?.groups
|
||||
return groups?.valueWithQuote || groups?.valueWithoutQuote
|
||||
}
|
||||
|
||||
6
plugins/plugin-md-power/src/shared/codeTree.ts
Normal file
6
plugins/plugin-md-power/src/shared/codeTree.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { FileTreeIconMode } from './fileTree'
|
||||
|
||||
export interface CodeTreeOptions {
|
||||
icon?: FileTreeIconMode
|
||||
height?: string | number
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import type { CanIUseOptions } from './caniuse.js'
|
||||
import type { CodeTabsOptions } from './codeTabs.js'
|
||||
import type { CodeTreeOptions } from './codeTree.js'
|
||||
import type { FileTreeOptions } from './fileTree.js'
|
||||
import type { IconsOptions } from './icons.js'
|
||||
import type { NpmToOptions } from './npmTo.js'
|
||||
@ -191,6 +192,21 @@ export interface MarkdownPowerPluginOptions {
|
||||
*/
|
||||
fileTree?: boolean | FileTreeOptions
|
||||
|
||||
/**
|
||||
* 是否启用 代码树 容器语法 和 嵌入语法
|
||||
*
|
||||
* ```md
|
||||
* ::: code-tree
|
||||
* :::
|
||||
* ```
|
||||
*
|
||||
* `@[code-tree](file_path)`
|
||||
*
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
codeTree?: boolean | CodeTreeOptions
|
||||
|
||||
/**
|
||||
* 是否启用 demo 语法
|
||||
*/
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@ -278,6 +278,9 @@ catalogs:
|
||||
shiki:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
tinyglobby:
|
||||
specifier: 0.2.13
|
||||
version: 0.2.13
|
||||
tm-grammars:
|
||||
specifier: ^1.23.16
|
||||
version: 1.23.16
|
||||
@ -645,6 +648,9 @@ importers:
|
||||
stylus:
|
||||
specifier: catalog:dev
|
||||
version: 0.64.0
|
||||
tinyglobby:
|
||||
specifier: catalog:prod
|
||||
version: 0.2.13
|
||||
tm-grammars:
|
||||
specifier: catalog:prod
|
||||
version: 1.23.16
|
||||
@ -4217,14 +4223,6 @@ packages:
|
||||
fault@2.0.1:
|
||||
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
||||
|
||||
fdir@6.4.3:
|
||||
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fdir@6.4.4:
|
||||
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
|
||||
peerDependencies:
|
||||
@ -6579,10 +6577,6 @@ packages:
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyglobby@0.2.12:
|
||||
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyglobby@0.2.13:
|
||||
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -9698,7 +9692,7 @@ snapshots:
|
||||
package-manager-detector: 1.1.0
|
||||
semver: 7.7.1
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.12
|
||||
tinyglobby: 0.2.13
|
||||
yaml: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
@ -10671,7 +10665,7 @@ snapshots:
|
||||
jsonc-eslint-parser: 2.4.0
|
||||
pathe: 2.0.3
|
||||
pnpm-workspace-yaml: 0.3.1
|
||||
tinyglobby: 0.2.12
|
||||
tinyglobby: 0.2.13
|
||||
yaml-eslint-parser: 1.3.0
|
||||
|
||||
eslint-plugin-regexp@2.7.0(eslint@9.25.1(jiti@2.4.2)):
|
||||
@ -10927,10 +10921,6 @@ snapshots:
|
||||
dependencies:
|
||||
format: 0.2.2
|
||||
|
||||
fdir@6.4.3(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.4.4(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
@ -13515,11 +13505,6 @@ snapshots:
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyglobby@0.2.12:
|
||||
dependencies:
|
||||
fdir: 6.4.3(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
|
||||
tinyglobby@0.2.13:
|
||||
dependencies:
|
||||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
@ -13593,7 +13578,7 @@ snapshots:
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.35.0
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.12
|
||||
tinyglobby: 0.2.13
|
||||
tree-kill: 1.2.2
|
||||
optionalDependencies:
|
||||
postcss: 8.5.3
|
||||
|
||||
@ -110,6 +110,7 @@ catalogs:
|
||||
package-manager-detector: ^1.2.0
|
||||
picocolors: ^1.1.1
|
||||
shiki: ^3.3.0
|
||||
tinyglobby: 0.2.13
|
||||
tm-grammars: ^1.23.16
|
||||
tm-themes: ^1.10.5
|
||||
unplugin: ^2.3.2
|
||||
|
||||
@ -115,13 +115,15 @@ html:not([data-theme="dark"]) .vp-code span {
|
||||
.vp-doc div[class*="language-"].line-numbers-mode .line-numbers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
/* rtl:ignore */
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: 32px;
|
||||
height: fit-content;
|
||||
min-height: 100%;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: var(--vp-code-font-size);
|
||||
line-height: var(--vp-code-line-height);
|
||||
|
||||
@ -47,6 +47,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
|
||||
'caniuse',
|
||||
'codeSandbox',
|
||||
'codeTabs',
|
||||
'codeTree',
|
||||
'codepen',
|
||||
'demo',
|
||||
'fileTree',
|
||||
|
||||
@ -43,7 +43,7 @@ export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig {
|
||||
langs: uniq([...twoslash ? ['ts', 'js', 'vue', 'json', 'bash', 'sh'] : [], ...langs]),
|
||||
codeBlockTitle: (title, code) => {
|
||||
const icon = getIcon(title)
|
||||
return `<div class="code-block-title"><div class="code-block-title-bar"><span class="title">${icon ? `<VPIcon name="${icon}"/>` : ''}${title}</span></div>${code}</div>`
|
||||
return `<div class="code-block-title" data-title="${title}"><div class="code-block-title-bar"><span class="title">${icon ? `<VPIcon name="${icon}"/>` : ''}${title}</span></div>${code}</div>`
|
||||
},
|
||||
twoslash: isPlainObject(twoslashOptions)
|
||||
? {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user