Merge pull request #162 from pengzhanbo/file-tree

feat(plugin-md-power): add support file-tree container
This commit is contained in:
pengzhanbo 2024-09-02 00:11:21 +08:00 committed by GitHub
commit 3316ee5ebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1534 additions and 116 deletions

View File

@ -35,7 +35,7 @@ export async function run(mode: Mode, root?: string) {
}
const cdCommand = mode === Mode.create ? colors.green(`cd ${data.root}`) : ''
const runCommand = colors.green(pm === 'yarn' ? 'yarn dev' : `${pm} run dev`)
const runCommand = colors.green(pm === 'yarn' ? 'yarn docs:dev' : `${pm} run docs:dev`)
const installCommand = colors.green(pm === 'yarn' ? 'yarn' : `${pm} install`)
progress.stop(t('spinner.stop'))

View File

@ -166,6 +166,73 @@ tags:
4. 结束
::::
## 文件树
在 Markdown 中,你可以使用 `file-tree` 容器 来显示带有文件图标和可折叠子目录的目录结构。
`::: file-tree` 容器,使用内置的 **Markdown 无序列表语法** 指定文件和目录的组织结构。
使用嵌套的列表项创建子目录;若希望某个目录不显示具体内容,只需在列表项末尾添加斜杠 `/` 即可。
以下语法可用于自定义文件树的外观:
- 通过加粗文件名或目录名来突出显示,例如 `**README.md**`
- 通过在名称后添加更多文本来为文件或目录添加注释
- 使用 `...``…` 作为名称来添加占位符文件和目录。
**输入:**
```md
::: file-tree
- docs
- .vuepress
- config.ts
- page1.md
- README.md
- theme 一个 **主题** 目录
- client
- components
- **Navbar.vue**
- composables
- useNavbar.ts
- styles
- navbar.css
- config.ts
- node/
- package.json
- pnpm-lock.yaml
- .gitignore
- README.md
- …
:::
```
**输出:**
::: file-tree
- docs
- .vuepress
- config.ts
- page1.md
- README.md
- theme 一个 **主题** 目录
- client
- components
- **Navbar.vue**
- composables
- useNavbar.ts
- styles
- navbar.css
- config.ts
- node/
- package.json
- pnpm-lock.yaml
- .gitignore
- README.md
- …
:::
## 选项组
在 Markdown 中支持选项卡。

View File

@ -9,36 +9,39 @@ tags:
- 快速开始
---
## 介绍
==vuepress-theme-plume== 是一个基于 VuePress 的主题,适用于 博客、文档 和 知识笔记 。
主题针对 文本内容、图片内容 的表现做了大量的优化,特别是针对 Markdown 内容的语法 做了 丰富的扩展,
你可以很轻松的利用这些特性编写 漂亮、美观、易读、表现力丰富 的内容。
::: details 不了解 VuePress
VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。
专为构建快速、以内容为中心的站点而设计。
简而言之VuePress 获取用 Markdown 编写的内容,对其应用主题,并生成可以轻松部署到任何地方的静态 HTML 页面。
::: tip
本主题 基于 [vuepress-next](https://github.com/vuepress/vuepress-next), 目前处于 RC 阶段。
当前主题 功能和 API 趋于稳定,但在未来的更新中仍有小概率出现破坏更改。
如果您在使用过程中遇到问题,或者有新的想法,
请在 [Issues](https://github.com/pengzhanbo/vuepress-theme-plume/issues) 里提出,
也欢迎 通过 [PR](https://github.com/pengzhanbo/vuepress-theme-plume/pulls) 帮助完善主题。
:::
## 优势
与 vuepress 默认主题相比:
- 大幅度优化了界面、交互,更具美观度,更好的用户体验。
- 同时,还添加了大量的丰富实用的功能,如 代码分组、提示容器、任务列表、数学公式、代码演示、
内容搜索、文章评论、加密 等。
- 新增编译缓存,加快启动速度。
- 支持使用单独的主题配置文件,避免修改配置导致频繁重启 VuePress 服务。
- 大幅度简化了配置,更易于使用,同时还保留了丰富灵活的配置项,满足个性化的需求。
- **更好的用户体验**
大幅度优化了界面、交互,更为美观、简洁,易用。
- **更多的功能**
代码分组、提示容器、任务列表、数学公式、代码演示、内容搜索、文章评论、加密、文件树 等。
- **更好的开发体验**
增加编译缓存,缓存 markdown 文件编译、缓存 复杂 代码块 内容解析结果。
- **更好的配置**
支持使用单独的主题配置文件,避免修改配置导致频繁重启 VuePress 服务。
大幅度简化了配置,更易于使用,同时还保留了丰富灵活的配置项,满足个性化的需求。
==plume 主题== 尽可能的内置你可能需要的功能,以及搭建站点所需要的一般性配置,您无需关注这些细节。
目的是,让您更专注于 内容的创作,更好的表达你的想法,享受 Markdown 增强语法带来的便利。
@ -52,7 +55,19 @@ VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_
- 🔑 支持 全站加密、部分加密
- 👀 支持 搜索、文章评论
- 👨‍💻‍ 支持 浅色/深色 主题 (包括代码高亮)
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示 等
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示、文件树
- 📚 代码演示,支持 CodePen, Replit, JSFiddle, CodeSandbox
- 📊 嵌入图标,支持 chart.jsEchartsMermaidflowchart 等
- 🎛 资源嵌入,支持 PDF, bilibili视频youtube视频等
::: tip
本主题 基于 [vuepress-next](https://github.com/vuepress/vuepress-next), 目前处于 RC 阶段。
当前主题 功能和 API 趋于稳定,但在未来的更新中仍有小概率出现破坏更改。
如果您在使用过程中遇到问题,或者有新的想法,
请在 [Issues](https://github.com/pengzhanbo/vuepress-theme-plume/issues) 里提出,
也欢迎 通过 [PR](https://github.com/pengzhanbo/vuepress-theme-plume/pulls) 帮助完善主题。
:::

View File

@ -13,14 +13,15 @@ tags:
要使用内置的 i18n (国际化) 功能,需要创建类似于下面的目录结构:
```
docs/
├─ es/
│ ├─ foo.md
├─ fr/
│ ├─ foo.md
├─ foo.md
```
::: file-tree
- docs
- en
- foo.md
- fr
- foo.md
- foo.md
:::
## vuepress 配置

View File

@ -18,14 +18,63 @@ const vuepressVersion = __VUEPRESS_VERSION__
- [Node.js v18.20.0+](https://nodejs.org/)
- [pnpm 8+](https://pnpm.io/zh/) 或 [Yarn 2+](https://yarnpkg.com/)
## 命令行安装
主题提供了一个 命令行工具,帮助您构建一个基本项目。您可以通过运行以下命令,启动 安装向导。
::: code-tabs
@tab pnpm
```sh
pnpm create vuepress-theme-plume@latest
```
@tab yarn
```sh
yarn create vuepress-theme-plume@latest
```
@tab npm
```sh
npm init vuepress-theme-plume@latest
```
:::
启动向导后,您只需要回答几个简单的问题:
<!-- @include: ../snippet/create.snippet.md ---->
::: details 怎么使用命令行工具?
以 Windows 系统为例,你可以使用以下方法来启动 CMD 命令行工具:
1. 按下 `Win + R` 键打开 “运行” 对话框。
2. 输入 `cmd` 并按下 Enter 键。 (也可以输入 `powershell` 来打开 PowerShell
注意此时 `cmd` 可能不在你期望的目录位置,你可以使用如下命令来切换到正确的目录:
```sh
D: # 此命令将切换到 D: 分区,进入其他分区请按照实际情况修改
cd open-source # 进入 D: 分区下的 open-source 目录
```
此时,你就可以在这里输入 `pnpm create vuepress-theme-plume@latest` 来创建一个基本的项目了。
创建的项目将位于 `D:\open-source\my-project` 目录下。
:::
## 手动安装
::: info 提示
- 使用 [pnpm](https://pnpm.io/zh/) 时,你需要安装 `vue` 作为 peer-dependencies 。
- 使用 [Yarn 2+](https://yarnpkg.com/) 时,你需要在 `.yarnrc.yml` 文件中设置 `nodeLinker: 'node-modules'`
:::
## 安装
使用本主题,你需要首先新建一个项目,并安装`vuepress@next`以及本主题
:::: steps
@ -110,8 +159,8 @@ const vuepressVersion = __VUEPRESS_VERSION__
``` json :no-line-numbers
{
"scripts": {
"dev": "vuepress dev docs",
"build": "vuepress build docs"
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
}
```
@ -188,19 +237,19 @@ const vuepressVersion = __VUEPRESS_VERSION__
@tab pnpm
```sh :no-line-numbers
pnpm dev
pnpm docs:dev
```
@tab yarn
``` sh :no-line-numbers
yarn dev
yarn docs:dev
```
@tab npm
``` sh :no-line-numbers
npm run dev
npm docs:run dev
```
:::
@ -210,3 +259,34 @@ const vuepressVersion = __VUEPRESS_VERSION__
- ### 完成
::::
## 文件结构
使用命令行工具创建的项目,它的文件结构是这样的。(如果你是手动创建的,也可以参考此文件结构管理您的项目)
::: file-tree
- .git/
- docs \# 文档源目录
- .vuepress
- public/ \# 静态资源目录
- client.ts
- config.ts \# vuepress 配值文件
- navbar.ts \# 导航栏配置
- notes.ts \# notes 配置
- plume.config.ts \# 主题配置文件
- notes
- demo
- foo.md
- bar.md
- preview
- markdown.md
- README.md \# 首页
- package.json
- package-lock.json
- .gitignore
- README.md
:::
`docs` 目录中, 除 `.vuepress` 目录外,目录中的 所有 markdown 文件都会被识别为文档。
其中,除 `notes` 目录外的 `markdown` 文件会被识别为 博客文章,而 `notes` 目录下 `markdown` 文件会被识别为 文档笔记。

View File

@ -22,14 +22,16 @@ tags:
示例:
``` :no-line-numbers
{sourceDir}/
├─ notes/
│ ├─ typescript/
│ │ └─ foo.md
│ └─ rust/
│ └─ foo.md
```
::: file-tree
- \{sourceDir\}
- notes
- typescript
- foo.md
- rust
- foo.md
:::
其中,`typescript``rust` 为目录名,各自独立保存与之相关的 markdown 文件。

View File

@ -40,15 +40,16 @@ const dir = /\d+\.[\s\S]+/
__example:__
``` txt :no-line-numbers
.{sourceDir}
- 1.前端
- 1.html
- 2.css
- 3.javascript
- 2.后端
- 运维
```
::: file-tree
- \{sourceDir\}
- 1.前端/
- 1.html/
- 2.css/
- 3.javascript/
- 2.后端/
- 运维/
:::
## 文章写作

View File

@ -10,8 +10,7 @@ tags:
---
::: tip
此文档 fork 自 [vuepress official doc](https://v2.vuepress.vuejs.org/zh/guide/deployment.html)
仅为了方便阅读。
此文档 fork 自 [vuepress official doc](https://v2.vuepress.vuejs.org/zh/guide/deployment.html)。
:::
下述的指南基于以下条件:
@ -64,7 +63,7 @@ jobs:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
# 选择要使用的 pnpm 版本
version: 8

View File

@ -0,0 +1,42 @@
```ansi :no-line-numbers :collapsed-lines
┌ Welcome to VuePress and vuepress-theme-plume !
│
◇ Select a language to display / 选择显示语言
│ 简体中文
│
◇ 您想在哪里初始化 VuePress
│ ./my-project
│
◇ 站点名称:
│ My Vuepress Site
│
◇ 站点描述信息:
│ My Vuepress Site Description
│
◇ 是否使用多语言?
│ No
│
◇ 请选择站点默认语言
│ 简体中文
│
◇ 是否使用 TypeScript
│ Yes
│
◇ 请选择打包工具
│ Vite
│
◇ 部署方式:
│ Custom
│
◇ 是否初始化 git 仓库?
│ Yes
│
◇ 是否安装依赖?
│ Yes
│
◇ 🎉 创建成功!
│
└ 🔨 执行以下命令即可启动:
cd ./my-project
pnpm run docs:dev
```

View File

@ -12,7 +12,7 @@
"vuepress": "2.0.0-rc.15"
},
"dependencies": {
"@iconify/json": "^2.2.242",
"@iconify/json": "^2.2.243",
"@simonwep/pickr": "^1.9.1",
"@vuepress/bundler-vite": "2.0.0-rc.15",
"chart.js": "^4.4.4",

View File

@ -30,3 +30,4 @@ readingTime: false
| ---- | ---------- | ------ | ------------------ |
| **锋 | 2024-04-18 | 6.88 | 支持下作者,加油! |
| *杰 | 2024-05-25 | 6.00 | 请你喝杯茶,加油 |
| **党 | 2024-08-22 | 8.80 | 感谢开源,加油 |

View File

@ -52,7 +52,7 @@
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.9.1",
"husky": "^9.1.5",
"lint-staged": "^15.2.9",
"lint-staged": "^15.2.10",
"rimraf": "^6.0.1",
"stylelint": "^16.9.0",
"tsconfig-vuepress": "^5.0.0",

View File

@ -44,8 +44,8 @@
"@vueuse/core": "^11.0.3",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.7",
"shiki": "^1.14.1",
"tm-grammars": "^1.17.8",
"shiki": "^1.15.2",
"tm-grammars": "^1.17.11",
"tm-themes": "^1.8.1",
"vue": "^3.4.38"
},

View File

@ -0,0 +1,151 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const props = defineProps<{
type: 'file' | 'folder'
expanded: boolean
empty: boolean
}>()
const active = ref(!!props.expanded)
const el = ref<HTMLElement>()
onMounted(() => {
if (!el.value || props.type !== 'folder')
return
el.value.querySelector('.tree-node.folder')?.addEventListener('click', () => {
active.value = !active.value
})
})
</script>
<template>
<li ref="el" class="file-tree-item" :class="{ expanded: active }">
<slot />
<ul v-if="props.type === 'folder' && props.empty">
<li class="file-tree-item">
<span class="tree-node file">
<span class="name"></span>
</span>
</li>
</ul>
</li>
</template>
<style>
.vp-file-tree {
width: fit-content;
max-width: 100%;
padding: 16px;
font-size: 14px;
background-color: var(--vp-c-bg-safe);
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
transition: border var(--t-color), background-color var(--t-color);
}
.vp-file-tree ul {
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
}
.file-tree-item {
margin-left: 14px;
}
.vp-file-tree .file-tree-item {
margin-top: 0;
}
.file-tree-item .tree-node {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
margin: 4px 0;
}
.file-tree-item .tree-node .name {
font-family: var(--vp-font-family-mono);
}
.file-tree-item .tree-node.folder {
position: relative;
}
.file-tree-item .tree-node.folder > .name {
color: var(--vp-c-text-1);
cursor: pointer;
transition: color var(--t-color);
}
.file-tree-item .tree-node.folder > .name:hover {
color: var(--vp-c-brand-1);
}
.file-tree-item .tree-node.folder::before {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M5.536 21.886a1 1 0 0 0 1.033-.064l13-9a1 1 0 0 0 0-1.644l-13-9A1 1 0 0 0 5 3v18a1 1 0 0 0 .536.886'/%3E%3C/svg%3E");
position: absolute;
top: 7px;
left: -14px;
display: block;
width: 10px;
height: 10px;
color: var(--vp-c-text-3);
content: "";
background-color: currentcolor;
-webkit-mask: var(--icon) no-repeat;
mask: var(--icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
transition: color var(--t-color);
}
.file-tree-item .tree-node.file .name.focus {
font-weight: bold;
color: var(--vp-c-brand-1);
transition: color var(--t-color);
}
.file-tree-item .tree-node .comment {
margin-left: 20px;
overflow: hidden;
color: var(--vp-c-text-3);
transition: color var(--t-color);
}
.file-tree-item .tree-node [class*="vp-fti-"] {
display: inline-block;
width: 0.9em;
height: 0.9em;
color: var(--vp-c-text-2);
background-color: currentcolor;
-webkit-mask: var(--icon) no-repeat;
mask: var(--icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.file-tree-item .tree-node.folder [class*="vp-fti-"] {
cursor: pointer;
}
.vp-file-tree .file-tree-item > ul {
padding-left: 8px !important;
margin: 0 0 0 6px !important;
border-left: solid 1px var(--vp-c-divider);
transition: border-color var(--t-color);
}
.file-tree-item:not(.expanded) > ul {
display: none;
}
.file-tree-item.expanded > .tree-node.folder::before {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,74 @@
import { FileIcons, definitions } from './icons.js'
export interface FileIcon {
name: string
svg: string
}
export const defaultFileIcon: FileIcon = {
name: 'default',
svg: makeSVGIcon(FileIcons['seti:default']),
}
export const folderIcon: FileIcon = {
name: 'folder',
svg: makeSVGIcon(FileIcons['seti:folder']),
}
export function getFileIcon(fileName: string): FileIcon {
const name = getFileIconName(fileName)
if (!name)
return defaultFileIcon
if (name in FileIcons) {
const path = FileIcons[name as keyof typeof FileIcons]
return {
name: name.includes(':') ? name.split(':')[1] : name,
svg: makeSVGIcon(path),
}
}
return defaultFileIcon
}
function makeSVGIcon(svg: string): string {
svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">${svg}</svg>`
.replace(/"/g, '\'')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/\{/g, '%7B')
.replace(/\}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
return `url("data:image/svg+xml,${svg}")`
}
function getFileIconName(fileName: string) {
let icon: string | undefined = definitions.files[fileName]
if (icon)
return icon
icon = getFileIconTypeFromExtension(fileName)
if (icon)
return icon
for (const [partial, partialIcon] of Object.entries(definitions.partials)) {
if (fileName.includes(partial))
return partialIcon
}
return icon
}
function getFileIconTypeFromExtension(fileName: string): string | undefined {
const firstDotIndex = fileName.indexOf('.')
if (firstDotIndex === -1)
return
let extension = fileName.slice(firstDotIndex)
while (extension !== '') {
const icon = definitions.extensions[extension]
if (icon)
return icon
const nextDotIndex = extension.indexOf('.', 1)
if (nextDotIndex === -1)
return
extension = extension.slice(nextDotIndex)
}
return undefined
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,77 @@
import fs from 'node:fs'
import container from 'markdown-it-container'
import type { Markdown } from 'vuepress/markdown'
import type Token from 'markdown-it/lib/token.mjs'
import type { App } from 'vuepress/core'
import { resolveTreeNodeInfo, updateInlineToken } from './resolveTreeNodeInfo.js'
import { type FileIcon, folderIcon, getFileIcon } from './findIcon.js'
const type = 'file-tree'
const closeType = `container_${type}_close`
const componentName = 'FileTreeItem'
const itemOpen = 'file_tree_item_open'
const itemClose = 'file_tree_item_close'
const classPrefix = 'vp-fti-'
const styleFilepath = 'internal/md-power/file-tree.css'
export async function fileTreePlugin(app: App, md: Markdown) {
const validate = (info: string): boolean => info.trim().startsWith(type)
const render = (tokens: Token[], idx: number): string => {
if (tokens[idx].nesting === 1) {
for (
let i = idx + 1;
!(tokens[i].nesting === -1
&& tokens[i].type === closeType);
++i
) {
const token = tokens[i]
if (token.type === 'list_item_open') {
const result = resolveTreeNodeInfo(tokens, token, i)
if (result) {
const [info, inline] = result
const { filename, type, expanded, empty } = info
const icon = type === 'file' ? getFileIcon(filename) : folderIcon
token.type = itemOpen
token.tag = componentName
token.attrSet('type', type)
token.attrSet(':expanded', expanded ? 'true' : 'false')
token.attrSet(':empty', empty ? 'true' : 'false')
updateInlineToken(inline, info, `${classPrefix}${icon.name}`)
addIcon(icon)
}
}
else if (token.type === 'list_item_close') {
token.type = itemClose
token.tag = componentName
}
}
return '<div class="vp-file-tree">'
}
else {
return '</div>'
}
}
let timer: NodeJS.Timeout | null = null
const icons: Record<string, FileIcon> = {}
function addIcon(icon: FileIcon) {
icons[icon.name] = icon
if (timer)
clearTimeout(timer)
timer = setTimeout(async () => {
let content = ''
for (const icon of Object.values(icons)) {
content += `.${classPrefix}${icon.name} { --icon: ${icon.svg}; }\n`
}
await app.writeTemp(styleFilepath, content)
}, 150)
}
md.use(container, type, { validate, render })
if (!fs.existsSync(app.dir.temp(styleFilepath)))
await app.writeTemp(styleFilepath, '')
}

View File

@ -0,0 +1,125 @@
import { removeLeadingSlash } from 'vuepress/shared'
import Token from 'markdown-it/lib/token.mjs'
interface FileTreeNode {
filename: string
type: 'folder' | 'file'
expanded: boolean
focus: boolean
empty: boolean
}
export function resolveTreeNodeInfo(
tokens: Token[],
current: Token,
idx: number,
): [FileTreeNode, Token] | undefined {
let hasInline = false
let hasChildren = false
let inline!: Token
for (
let i = idx + 1;
!(tokens[i].level === current.level && tokens[i].type === 'list_item_close');
++i
) {
if (tokens[i].type === 'inline' && !hasInline) {
inline = tokens[i]
hasInline = true
}
else if (tokens[i].tag === 'ul') {
hasChildren = true
}
if (hasInline && hasChildren)
break
}
if (!hasInline)
return undefined
const children = inline.children?.filter(token => (token.type === 'text' && token.content) || token.tag === 'strong') || []
const filename = children.filter(token => token.type === 'text').map(token => token.content).join(' ').split(/\s+/)[0] ?? ''
const focus = children[0]?.tag === 'strong'
const type = hasChildren || filename.endsWith('/') ? 'folder' : 'file'
const info: FileTreeNode = {
filename: removeLeadingSlash(filename),
type,
focus,
empty: !hasChildren,
expanded: type === 'folder' && !filename.endsWith('/'),
}
return [info, inline] as const
}
export function updateInlineToken(inline: Token, info: FileTreeNode, icon: string) {
const children = inline.children
if (!children)
return
const tokens: Token[] = []
const wrapperOpen = new Token('span_open', 'span', 1)
const wrapperClose = new Token('span_close', 'span', -1)
wrapperOpen.attrSet('class', `tree-node ${info.type}`)
tokens.push(wrapperOpen)
if (info.filename !== '...' && info.filename !== '…') {
const iconOpen = new Token('span_open', 'span', 1)
iconOpen.attrSet('class', icon)
const iconClose = new Token('span_close', 'span', -1)
tokens.push(iconOpen, iconClose)
}
const fileOpen = new Token('span_open', 'span', 1)
fileOpen.attrSet('class', `name${info.focus ? ' focus' : ''}`)
tokens.push(fileOpen)
let isStrongTag = false
while (children.length) {
const token = children.shift()!
if (token.type === 'text' && token.content) {
if (token.content.includes(' ')) {
const [first, ...other] = token.content.split(' ')
const text = new Token('text', '', 0)
text.content = first
tokens.push(text)
const comment = new Token('text', '', 0)
comment.content = other.join(' ')
children.unshift(comment)
}
else {
tokens.push(token)
}
if (!isStrongTag)
break
}
else if (token.tag === 'strong') {
tokens.push(token)
if (token.nesting === 1) {
isStrongTag = true
}
else {
break
}
}
else {
tokens.push(token)
}
}
const fileClose = new Token('span_close', 'span', -1)
tokens.push(fileClose)
if (children.filter(token => token.type === 'text' && token.content.trim()).length) {
const commentOpen = new Token('span_open', 'span', 1)
commentOpen.attrSet('class', 'comment')
const commentClose = new Token('span_close', 'span', -1)
tokens.push(commentOpen, ...children, commentClose)
}
tokens.push(wrapperClose)
inline.children = tokens
}

View File

@ -14,6 +14,7 @@ import { jsfiddlePlugin } from './features/jsfiddle.js'
import { plotPlugin } from './features/plot.js'
import { langReplPlugin } from './features/langRepl.js'
import { prepareConfigFile } from './prepareConfigFile.js'
import { fileTreePlugin } from './features/fileTree/index.js'
export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin {
return (app) => {
@ -89,12 +90,17 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
options.plot === true
|| (typeof options.plot === 'object' && options.plot.tag !== false)
) {
// =|plot|=
// !!plot!!
md.use(plotPlugin)
}
if (options.repl)
await langReplPlugin(app, md, options.repl)
if (options.fileTree) {
// ::: file-tree
await fileTreePlugin(app, md)
}
},
}
}

View File

@ -54,6 +54,12 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('CanIUseViewer', CanIUse)`)
}
if (options.fileTree) {
imports.add(`import FileTreeItem from '${CLIENT_FOLDER}components/FileTreeItem.vue'`)
imports.add(`import '@internal/md-power/file-tree.css'`)
enhances.add(`app.component('FileTreeItem', FileTreeItem)`)
}
return app.writeTemp(
'md-power/config.js',
`\

View File

@ -26,6 +26,7 @@ export interface MarkdownPowerPluginOptions {
// container
repl?: false | ReplOptions
fileTree?: boolean
caniuse?: boolean | CanIUseOptions
}

View File

@ -33,6 +33,7 @@ export default defineConfig(() => {
entry: ['./src/node/index.ts'],
outDir: './lib/node',
target: 'node18',
external: ['markdown-it'],
},
// client
...config.map(({ dir, files }) => ({

View File

@ -36,8 +36,8 @@
"vuepress": "2.0.0-rc.15"
},
"dependencies": {
"@shikijs/transformers": "^1.14.1",
"@shikijs/twoslash": "^1.14.1",
"@shikijs/transformers": "^1.15.2",
"@shikijs/twoslash": "^1.15.2",
"@types/hast": "^3.0.4",
"@vuepress/helper": "2.0.0-rc.42",
"@vueuse/core": "^11.0.3",
@ -46,7 +46,7 @@
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-hast": "^13.2.0",
"nanoid": "^5.0.7",
"shiki": "^1.14.1",
"shiki": "^1.15.2",
"twoslash": "^0.2.9",
"twoslash-vue": "^0.2.9"
},

91
pnpm-lock.yaml generated
View File

@ -51,8 +51,8 @@ importers:
specifier: ^9.1.5
version: 9.1.5
lint-staged:
specifier: ^15.2.9
version: 15.2.9
specifier: ^15.2.10
version: 15.2.10
rimraf:
specifier: ^6.0.1
version: 6.0.1
@ -96,8 +96,8 @@ importers:
docs:
dependencies:
'@iconify/json':
specifier: ^2.2.242
version: 2.2.242
specifier: ^2.2.243
version: 2.2.243
'@simonwep/pickr':
specifier: ^1.9.1
version: 1.9.1
@ -163,11 +163,11 @@ importers:
specifier: ^5.0.7
version: 5.0.7
shiki:
specifier: ^1.14.1
version: 1.14.1
specifier: ^1.15.2
version: 1.15.2
tm-grammars:
specifier: ^1.17.8
version: 1.17.8
specifier: ^1.17.11
version: 1.17.11
tm-themes:
specifier: ^1.8.1
version: 1.8.1
@ -218,11 +218,11 @@ importers:
plugins/plugin-shikiji:
dependencies:
'@shikijs/transformers':
specifier: ^1.14.1
version: 1.14.1
specifier: ^1.15.2
version: 1.15.2
'@shikijs/twoslash':
specifier: ^1.14.1
version: 1.14.1(typescript@5.5.4)
specifier: ^1.15.2
version: 1.15.2(typescript@5.5.4)
'@types/hast':
specifier: ^3.0.4
version: 3.0.4
@ -248,8 +248,8 @@ importers:
specifier: ^5.0.7
version: 5.0.7
shiki:
specifier: ^1.14.1
version: 1.14.1
specifier: ^1.15.2
version: 1.15.2
twoslash:
specifier: ^0.2.9
version: 0.2.9(typescript@5.5.4)
@ -375,8 +375,8 @@ importers:
version: link:../plugins/plugin-md-power
devDependencies:
'@iconify/json':
specifier: ^2.2.242
version: 2.2.242
specifier: ^2.2.243
version: 2.2.243
packages:
@ -1010,8 +1010,8 @@ packages:
resolution: {integrity: sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==}
engines: {node: '>=10.13.0'}
'@iconify/json@2.2.242':
resolution: {integrity: sha512-cS6eYdx1C1GhqaZm25ztH5yoghCaTXGJBeseUkS259GxxX9obtGLLk0yy+twxpNCD5/F9gjbgxh46BjNWsHtwg==}
'@iconify/json@2.2.243':
resolution: {integrity: sha512-92pXEKdjDmH8sV3fZwJTxifKpJtp3ie8KD7oCqHRJh/Z+CoGm3bzDcK5UnWs5G5EDbu00kYVcSGM4jRnY9HgGQ==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@ -1551,14 +1551,14 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@shikijs/core@1.14.1':
resolution: {integrity: sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==}
'@shikijs/core@1.15.2':
resolution: {integrity: sha512-hi6XZuwHYn6bU4wtXZxST8ynM55aiU2+rVU9aPIrSxqKmEKl4d65puwGsggwcZWTET+7zGXKe7AUj46iQ8Aq8w==}
'@shikijs/transformers@1.14.1':
resolution: {integrity: sha512-JJqL8QBVCJh3L61jqqEXgFq1cTycwjcGj7aSmqOEsbxnETM9hRlaB74QuXvY/fVJNjbNt8nvWo0VwAXKvMSLRg==}
'@shikijs/transformers@1.15.2':
resolution: {integrity: sha512-J+3kFFXb4hN3esMzdDBGb2GhBsMPX8UC3o/U9G4Jognb8k0ADQAzZkShTARwS76O0g2VFoMu4vnIchiVE6x/uw==}
'@shikijs/twoslash@1.14.1':
resolution: {integrity: sha512-b0krVIqVCpdh9Gji+gTSJp0n2KyepPmnjKEDs+dUb765MUcyfN9qK/vRr7fA/YdAJxab8IDpz1GbLl0GuzAyFQ==}
'@shikijs/twoslash@1.15.2':
resolution: {integrity: sha512-xGMsTd5QYL1cP7bjAbEl2YckyC7D8EOhsRy/ie13izjszjPPTSWl0fJs6cb5bEOLxa8ivFwUtwepr8RwWlE6yw==}
'@sideway/address@4.1.5':
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
@ -4005,8 +4005,8 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lint-staged@15.2.9:
resolution: {integrity: sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==}
lint-staged@15.2.10:
resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==}
engines: {node: '>=18.12.0'}
hasBin: true
@ -4363,10 +4363,6 @@ packages:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
micromatch@4.0.7:
resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
engines: {node: '>=8.6'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@ -5080,8 +5076,8 @@ packages:
shell-quote@1.8.1:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
shiki@1.14.1:
resolution: {integrity: sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==}
shiki@1.15.2:
resolution: {integrity: sha512-M+7QZQZiZw/cZeizrC/yryG3eeG8pTUhu7ZaHxVyzPNFIRIlN46YBciquoNPCiXiwLnx6JB62f3lSuSYQrus1w==}
side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
@ -5366,8 +5362,8 @@ packages:
tinyexec@0.2.0:
resolution: {integrity: sha512-au8dwv4xKSDR+Fw52csDo3wcDztPdne2oM1o/7LFro4h6bdFmvyUAeAfX40pwDtzHgRFqz1XWaUqgKS2G83/ig==}
tm-grammars@1.17.8:
resolution: {integrity: sha512-Qw67JNutL9LCt8FFw5RfsogeQ40iSeqrTHDSp0ecnY/b+ZweK8izlw6y/ZMje2+I6DMtTiBOCgmXEf+2oH11jQ==}
tm-grammars@1.17.11:
resolution: {integrity: sha512-qxTMAxbmY9NqBxTSaeFkI7ZCofhp+SVtrCLdf5OkcX6iiazUTL5LTLpzw0ZSFUW0RRr0Z2kMepIup/WDBha4sQ==}
tm-themes@1.8.1:
resolution: {integrity: sha512-jTUfDRn5TysYhkxxEWBQDo1C1n4yoHcnfNNqXkVxIMGQCgal/9poGuMBsfbnZCPEmFVcN2rtrUwaOJ8s2hVQXg==}
@ -6440,7 +6436,7 @@ snapshots:
'@hutson/parse-repository-url@5.0.0': {}
'@iconify/json@2.2.242':
'@iconify/json@2.2.243':
dependencies:
'@iconify/types': 2.0.0
pathe: 1.1.2
@ -6903,17 +6899,17 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@shikijs/core@1.14.1':
'@shikijs/core@1.15.2':
dependencies:
'@types/hast': 3.0.4
'@shikijs/transformers@1.14.1':
'@shikijs/transformers@1.15.2':
dependencies:
shiki: 1.14.1
shiki: 1.15.2
'@shikijs/twoslash@1.14.1(typescript@5.5.4)':
'@shikijs/twoslash@1.15.2(typescript@5.5.4)':
dependencies:
'@shikijs/core': 1.14.1
'@shikijs/core': 1.15.2
twoslash: 0.2.9(typescript@5.5.4)
transitivePeerDependencies:
- supports-color
@ -9774,7 +9770,7 @@ snapshots:
dependencies:
uc.micro: 2.1.0
lint-staged@15.2.9:
lint-staged@15.2.10:
dependencies:
chalk: 5.3.0
commander: 12.1.0
@ -9782,7 +9778,7 @@ snapshots:
execa: 8.0.1
lilconfig: 3.1.2
listr2: 8.2.4
micromatch: 4.0.7
micromatch: 4.0.8
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.5.0
@ -10432,11 +10428,6 @@ snapshots:
braces: 3.0.2
picomatch: 2.3.1
micromatch@4.0.7:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@ -11145,9 +11136,9 @@ snapshots:
shell-quote@1.8.1: {}
shiki@1.14.1:
shiki@1.15.2:
dependencies:
'@shikijs/core': 1.14.1
'@shikijs/core': 1.15.2
'@types/hast': 3.0.4
side-channel@1.0.6:
@ -11458,7 +11449,7 @@ snapshots:
tinyexec@0.2.0: {}
tm-grammars@1.17.8: {}
tm-grammars@1.17.11: {}
tm-themes@1.8.1: {}

View File

@ -109,6 +109,6 @@
"vuepress-plugin-md-power": "workspace:*"
},
"devDependencies": {
"@iconify/json": "^2.2.242"
"@iconify/json": "^2.2.243"
}
}

View File

@ -131,6 +131,9 @@ export function getPlugins({
if (pluginOptions.markdownPower !== false) {
plugins.push(markdownPowerPlugin({
caniuse: pluginOptions.caniuse,
fileTree: true,
plot: true,
icons: true,
...pluginOptions.markdownPower || {},
repl: pluginOptions.markdownPower?.repl
? { theme: shikiTheme, ...pluginOptions.markdownPower?.repl }

View File

@ -1,7 +1,7 @@
import type { Plugin } from 'vuepress/core'
import type { MarkdownEnv } from 'vuepress/markdown'
const REG_HEADING = /^#\s*?(\S.*)?\n/
const REG_HEADING = /^#\s*?([^#\s].*)?\n/
export function markdownTitlePlugin(): Plugin {
return {