Compare commits

...

52 Commits

Author SHA1 Message Date
pengzhanbo
d69e0b9765 ci: update workflow permissions 2026-04-22 17:07:34 +08:00
pengzhanbo
02038f2df0 build: publish v1.0.0-rc.196 2026-04-19 14:37:52 +08:00
pengzhanbo
e5126663ef fix: fix security 2026-04-19 14:34:47 +08:00
pengzhanbo
402f259086
refactor(plugin-md-power): refactor obsidian plugins (#893) 2026-04-19 14:10:54 +08:00
pengzhanbo
58ea2fc8cb
fix(theme): remove cwd options from picomatch (#892) 2026-04-19 14:10:40 +08:00
pengzhanbo
6ebb1bda6e
fix(plugin-md-power): fix cell display issue caused by colspan in table (#891) 2026-04-19 14:10:22 +08:00
pengzhanbo
68f39695c4 chore: update tsconfig 2026-04-19 14:09:52 +08:00
pengzhanbo
76787f6530 build: publish v1.0.0-rc.195 2026-04-18 17:13:48 +08:00
pengzhanbo
e2b47da532 chore: tweak 2026-04-18 17:09:26 +08:00
pengzhanbo
035d521e96 chore: update deps to latest 2026-04-18 17:07:12 +08:00
pengzhanbo
bfd0c8409c
feat(plugin-md-power): compat obsidian official markdown syntax (#890)
* feat(plugin-md-power): compat obsidian official markdown syntax

* chore: tweak

* chore: tweak

* chore: tweak

* chore: tweak
2026-04-18 17:01:41 +08:00
pengzhanbo
e11c7a8fcd build: publish v1.0.0-rc.194 2026-04-14 15:37:37 +08:00
pengzhanbo
1329051536 chore: tweak 2026-04-14 15:36:15 +08:00
pengzhanbo
0677f6749e chore: update deps to latest 2026-04-14 15:31:38 +08:00
pengzhanbo
28963eb419
fix(plugin-search): fix search index race condition on pageUpdated, close #888 (#889) 2026-04-14 15:29:58 +08:00
pengzhanbo
cfc89adab8 chore: update security deps 2026-04-04 16:35:48 +08:00
pengzhanbo
e0ba59a6f9 build: update changelog 2026-04-03 02:56:28 +08:00
pengzhanbo
352874b29a build: publish v1.0.0-rc.193 2026-04-03 02:25:46 +08:00
pengzhanbo
c824ad85f4 chore: update gitignore 2026-04-03 02:18:23 +08:00
pengzhanbo
db2eda82f3 build: update clean scripts 2026-04-03 02:18:01 +08:00
pengzhanbo
e9fe35bc4f
fix(theme): fix sidebar items prefix not handled correctly, close #876 (#885) 2026-04-03 02:13:18 +08:00
pengzhanbo
709ade741c chore: improve comment 2026-04-03 02:06:32 +08:00
pengzhanbo
d8b79e89e8
refactor(plugin-search): improve search index update (#884) 2026-04-03 01:58:25 +08:00
pengzhanbo
dbc6f0be0f
fix(theme): fix auto-sidebar group icon error inherit, close #873 (#883) 2026-04-02 22:05:54 +08:00
pengzhanbo
9fe294b9dd fix(theme): fix MarkdownOptions types 2026-04-02 21:15:33 +08:00
pengzhanbo
ecf100cfc6 docs: update security.md 2026-04-02 21:15:08 +08:00
pengzhanbo
b7ee45642e docs: update contributing.md 2026-04-02 21:14:51 +08:00
pengzhanbo
54c05c8cea docs: add claude.md 2026-04-02 21:14:34 +08:00
pengzhanbo
86cb872ce6 refactor: migrate onWatched to onPageUpdated 2026-04-02 21:14:16 +08:00
pengzhanbo
a6cb3820b1 refactor: remove deprecated enhancement 2026-04-02 21:12:59 +08:00
pengzhanbo
184d1aee76 build: improve tsdown bundle config 2026-04-02 20:59:23 +08:00
pengzhanbo
cbc5c55891 perf: update deps to latest 2026-04-02 20:57:51 +08:00
mcenahle
4f40f8441d
docs: add "mcenahle Docs" to demo page (#882)
* Update demos.md

* chore: fix URLs for 哦麦 MC logo and preview images

Updated logo and preview image URLs for 哦麦 MC in demos.md.

---------

Co-authored-by: pengzhanbo <volodymyr@foxmail.com>
2026-04-02 20:54:47 +08:00
pengzhanbo
fe0d4bbc92
feat: improve accessibility features (#869) 2026-04-02 20:49:20 +08:00
pengzhanbo
39a76a35d7
feat(plugin-md-power)!: use # as the comment delimiter (#870) 2026-04-02 20:48:55 +08:00
pengzhanbo
a01bc13c66
fix(plugin-md-power): fix tsdown icon (#878) 2026-04-02 20:48:34 +08:00
pengzhanbo
1b213d4c28
fix(theme): add bulletin to outline ignores (#879) 2026-04-02 20:48:10 +08:00
pengzhanbo
aede6f5d87
fix(theme): twoslash comment error (#881)
* fix(theme): fix incorrect auto-sidebar-link parse

* fix(theme): twoslash comment error
2026-04-02 16:38:33 +08:00
pengzhanbo
7febfbf237
fix(theme): fix incorrect auto-sidebar-link parse (#880) 2026-04-02 16:37:58 +08:00
suixinio
7ce4e40521
docs: update ohmymc img (#877)
docs: update ohmymc img
2026-04-02 14:34:49 +08:00
zhenghaoyang24
12c4f5b39e
docs: fix icon documentation links (#874)
- Update relative paths in markdown config documentation to point
  to correct guide location
- Change sidebar icons link from document.md to quick-start/sidebar.md
  for proper navigation structure
2026-04-02 14:34:14 +08:00
pengzhanbo
aa54090b5d docs: update sponsor 2026-03-19 02:09:31 +08:00
逸燧Esyka
192b260d2b
docs: update repository url for Esyka's blog (#872) 2026-03-15 16:41:38 +08:00
逸燧Esyka
75df783295
docs: add Esyka's Blog to demos (#871)
Added Esyka's Blog with relevant details.
2026-03-14 23:39:00 +08:00
pengzhanbo
97a5ba20c3 docs: fix typo 2026-03-08 21:55:27 +08:00
pengzhanbo
896c7e22df chore: improve theme code comments 2026-03-08 16:59:50 +08:00
pengzhanbo
77856e36c5 chore: improve plugin-md-power code comments 2026-03-08 16:35:52 +08:00
pengzhanbo
552f0f5c32 chore: improve plugin-search code comments 2026-03-08 16:16:04 +08:00
pengzhanbo
7751e4c798 chore: improve cli code comments 2026-03-08 16:15:26 +08:00
pengzhanbo
17646708b1 docs: update skills usage doc 2026-03-06 16:40:02 +08:00
pengzhanbo
f14d663bb5 docs: update skills 2026-03-06 15:54:30 +08:00
pengzhanbo
50fa747ec1 docs: update agent skills 2026-03-06 00:44:21 +08:00
198 changed files with 10945 additions and 5386 deletions

View File

@ -13,6 +13,9 @@ on:
workflow_dispatch:
workflow_call:
permissions:
contents: write
jobs:
deploy-docs:
runs-on: ubuntu-latest

View File

@ -6,6 +6,9 @@ on:
- v*
workflow_dispatch:
permissions:
contents: write
jobs:
deploy-docs:
runs-on: ubuntu-latest

View File

@ -8,6 +8,9 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest

View File

@ -5,6 +5,10 @@ on:
tags:
- v*
permissions:
contents: write
id-token: write
jobs:
lint:
uses: ./.github/workflows/lint.yaml
@ -16,9 +20,6 @@ jobs:
if: github.repository == 'pengzhanbo/vuepress-theme-plume'
needs: [test, lint]
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v6
with:

View File

@ -8,6 +8,9 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
jobs:
unit-test:
runs-on: ubuntu-latest

3
.gitignore vendored
View File

@ -15,3 +15,6 @@ dist/
coverage/
.idea
.claude/
!.claude/skills/

File diff suppressed because it is too large Load Diff

111
CLAUDE.md Normal file
View File

@ -0,0 +1,111 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
vuepress-theme-plume is a VuePress 2 theme monorepo for building blogs, documentation, and knowledge bases.
It includes a main theme, several plugins, a CLI tool, and example implementations.
## Commands
```bash
# Install dependencies
pnpm install
# Build all packages (required after clone, outputs to lib/)
pnpm build
# Development - runs theme + docs dev servers concurrently
pnpm dev
# Lint (eslint + stylelint)
pnpm lint
pnpm lint:fix # auto-fix
# Run tests (vitest)
pnpm test
# Run a single test file
pnpm test src/path/to/file.spec.ts
# Run tests related to changed files (for pre-commit)
cross-env TZ=Etc/UTC vitest related --run
# Build docs only
pnpm docs:build
# Serve docs locally
pnpm docs:serve
# Release workflow
pnpm release # runs lint + build + version bump + changelog + git commit
```
## Monorepo Structure
```txt
├── theme/ # Main VuePress theme (vuepress-theme-plume)
├── plugins/ # VuePress plugins
│ ├── plugin-search/ # Full-text fuzzy search
│ ├── plugin-md-power/ # Markdown enhancements
│ └── plugin-fonts/ # Special character font support
├── cli/ # CLI tool (create project scaffolding)
├── docs/ # Documentation site
└── examples/ # Example implementations
├── pure-blog/
└── layout-slots/
```
## Theme Architecture
The theme is organized into three layers:
- **`src/node/`** - Build-time code (runs during `vuepress build/dev`)
- `prepare/` - Content preparation (frontmatter parsing, collection resolution)
- `plugins/` - VuePress plugin registration
- `config/` - Theme configuration handling
- `autoFrontmatter/` - Automatic frontmatter generation
- **`src/client/`** - Client-side code (runs in browser)
- `components/` - Vue components
- `composables/` - Vue composables (outline, search, etc.)
- `styles/` - CSS/SCSS styles
- `features/` - Feature-specific components and logic
- **`src/shared/`** - Shared code (used by both node and client)
- `frontmatter/` - Frontmatter schemas and utilities
- `locale/` - i18n translations
- `options.ts` - Theme options types
- `features/` - Feature flags and shared feature logic
## Build Output
Each package uses [tsdown](https://tsdown.dev/) to compile TypeScript. Build output goes to `lib/`:
- `lib/node/` - Node-side exports
- `lib/client/` - Client-side exports
- `lib/shared/` - Shared exports
The `lib/` directory is gitignored and must be built with `pnpm build`.
## Testing
Tests use Vitest with coverage enabled. Test files are located at `**/__test__/**/*.spec.ts` and are excluded from coverage reports. Run tests with timezone fixed to UTC to ensure consistent results.
## Key Dependencies
- **VuePress**: v2.0.0-rc.28 with @vuepress/bundler-vite
- **Vue**: ^3.5.30
- **Shiki**: ^4.x for syntax highlighting
- **VueUse**: ^14.x for composables
- **markdown-it**: ^14.x for Markdown processing
## Development Notes
- Node.js 20.19.0+ required
- pnpm catalogs are used for dependency management (`dev`, `peer`, `prod`, `vuepress`)
- The theme depends on `vuepress-plugin-md-power` and `@vuepress-plume/plugin-search` as workspace dependencies
- Some peer dependencies are optional (e.g., artplayer, dashjs, three.js)
- Plugins (`plugins/*`) do not have dev commands — changes require `pnpm build` to take effect
- The `lib/` directory is gitignored and must be rebuilt after `pnpm install`

View File

@ -19,7 +19,7 @@ In the `plugins` directory:
Development requirements:
- [Node.js](http://nodejs.org/) version 20.6.0+
- [Node.js](http://nodejs.org/) version 20.19.0+
- [pnpm](https://pnpm.io/zh/) version 9+
Clone the repository and install dependencies:

View File

@ -19,7 +19,7 @@
开发要求:
- [Node.js](http://nodejs.org/) version 20.6.0+
- [Node.js](http://nodejs.org/) version 20.19.0+
- [pnpm](https://pnpm.io/zh/) version 9+
克隆代码仓库,并安装依赖:

View File

@ -4,8 +4,8 @@
| Version | Supported |
| ---------------- | ------------------ |
| >= 1.0.0-rc.170 | :white_check_mark: |
| < 1.0.0-rc.170 | :x: |
| >= 1.0.0-rc.190 | :white_check_mark: |
| < 1.0.0-rc.190 | :x: |
## Reporting a Vulnerability

View File

@ -1,7 +1,7 @@
{
"name": "create-vuepress-theme-plume",
"type": "module",
"version": "1.0.0-rc.192",
"version": "1.0.0-rc.196",
"description": "The cli for create vuepress-theme-plume's project",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
@ -27,7 +27,7 @@
"templates"
],
"scripts": {
"build": "tsdown"
"build": "tsdown --config-loader unrun"
},
"dependencies": {
"@clack/prompts": "catalog:prod",
@ -40,8 +40,8 @@
"sort-package-json": "catalog:prod"
},
"plume-deps": {
"vuepress": "2.0.0-rc.26",
"vue": "^3.5.26",
"vuepress": "2.0.0-rc.28",
"vue": "^3.5.32",
"http-server": "^14.1.1",
"typescript": "^5.9.3"
},

View File

@ -1,3 +1,15 @@
/**
* VuePress Theme Plume CLI Entry Point
*
* VuePress Theme Plume CLI
*
* This module provides command-line interface for creating and initializing
* VuePress projects with vuepress-theme-plume.
*
* VuePress
*
* @module cli
*/
import cac from 'cac'
import { version } from '../package.json'
import { Mode } from './constants.js'

View File

@ -1,5 +1,10 @@
import type { Locale } from '../types.js'
/**
* English locale configuration for CLI prompts and messages.
*
* CLI
*/
export const en: Locale = {
'question.root': 'Where would you want to initialize VuePress?',
'question.site.name': 'Site Name:',

View File

@ -2,6 +2,15 @@ import type { Langs, Locale } from '../types.js'
import { en } from './en.js'
import { zh } from './zh.js'
/**
* Locale configurations for different languages.
*
*
*
* Maps language codes to their respective locale strings.
*
*
*/
export const locales: Record<Langs, Locale> = {
'zh-CN': zh,
'en-US': en,

View File

@ -1,5 +1,10 @@
import type { Locale } from '../types.js'
/**
* Chinese (Simplified) locale configuration for CLI prompts and messages.
*
* CLI
*/
export const zh: Locale = {
'question.root': '您想在哪里初始化 VuePress',
'question.site.name': '站点名称:',

View File

@ -5,6 +5,14 @@ import _sortPackageJson from 'sort-package-json'
import { Mode } from './constants.js'
import { readJsonFile, resolve } from './utils/index.js'
/**
* Sort package.json fields in a consistent order.
*
* package.json
*
* @param json - Package.json object to sort / package.json
* @returns Sorted package.json object / package.json
*/
function sortPackageJson(json: Record<any, any>) {
return _sortPackageJson(json, {
sortOrder: ['name', 'type', 'version', 'private', 'description', 'packageManager', 'author', 'license', 'scripts', 'devDependencies', 'dependencies', 'pnpm'],
@ -111,12 +119,29 @@ export async function createPackageJson(
}
}
/**
* Get user information from git global configuration.
*
* git
*
* @returns User information object with username and email /
* @throws Error if git command fails / git
*/
async function getUserInfo() {
const { output: username } = await spawn('git', ['config', '--global', 'user.name'])
const { output: email } = await spawn('git', ['config', '--global', 'user.email'])
return { username, email }
}
/**
* Get the version of a package manager.
*
*
*
* @param pkg - Package manager name (npm, yarn, pnpm) /
* @returns Version string of the package manager /
* @throws Error if package manager command fails /
*/
async function getPackageManagerVersion(pkg: string) {
const { output } = await spawn(pkg, ['--version'])
return output

View File

@ -78,6 +78,15 @@ export async function run(mode: Mode, root?: string): Promise<void> {
}
}
/**
* Resolve prompt result into final configuration data.
*
*
*
* @param result - Prompt result from user input /
* @param mode - Operation mode (init or create) /
* @returns Resolved configuration data /
*/
function resolveData(result: PromptResult, mode: Mode): ResolvedData {
return {
...result,

View File

@ -1,6 +1,19 @@
import type { PackageManager } from '../types.js'
import process from 'node:process'
/**
* Detect the current package manager from environment variables.
*
* 使
*
* @returns The detected package manager name /
* @example
* // When using pnpm
* const pm = getPackageManager() // returns 'pnpm'
*
* // When using npm
* const pm = getPackageManager() // returns 'npm'
*/
export function getPackageManager(): PackageManager {
const name = process.env?.npm_config_user_agent || 'npm'
return name.split('/')[0] as PackageManager

View File

@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
'chat',
'include',
'env',
'obsidian',
],
},
{

View File

@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
'chat',
'include',
'env',
'obsidian',
],
},
{

View File

@ -94,7 +94,7 @@ export const enNavbar: ThemeNavItem[] = defineNavbarConfig([
{
text: `${version}`,
icon: 'codicon:versions',
badge: '',
badge: 'New',
items: [
{ text: 'Changelog', link: '/en/changelog/' },
{ text: 'Contributing', link: '/en/contributing/' },

View File

@ -58,6 +58,7 @@ export const theme: Theme = plumeTheme({
jsfiddle: true,
demo: true,
encrypt: true,
obsidian: true,
npmTo: ['pnpm', 'yarn', 'npm'],
repl: {
go: true,

View File

@ -82,7 +82,7 @@ export default defineUserConfig({
主题提供了 `plume.config.ts` 配置文件,==对该文件的修改支持热更新,无需重启服务=={.tip} ::twemoji:confetti-ball::。
可以在其中配置支持热更新的字段,如 `navbar``profile` 等。
可以在其中配置支持热更新的字段,如 `navbar``profile` 等。
::: tip
这些字段仍可在 VuePress 配置文件的 `theme` 中配置,但主题配置文件的设置最终会合并到主配置中。

View File

@ -6,7 +6,7 @@ permalink: /config/locales/
这些选项用于配置与语言相关的文本。
如果你的站点是以非内置语言支持以外的其他语言提供服务的,你应该为每个语言设置这些选项来提供翻译。
如果您的站点是以非内置语言支持以外的其他语言提供服务的,您应该为每个语言设置这些选项来提供翻译。
## 内置语言支持

View File

@ -149,7 +149,7 @@ export default defineUserConfig({
- **默认值**: `{ provider: 'iconify' }`
- **详情**: 图标配置
[查看 **icon** 使用说明](../../theme/guide/features/icon.md){.read-more}
[查看 **icon** 使用说明](../guide/features/icon.md){.read-more}
### plot

View File

@ -11,7 +11,7 @@ permalink: /config/navigation/
主题默认会自动生成最简单的导航栏配置,仅包括 **首页****文章列表页**
你也可以自己配置导航栏,覆盖默认的的导航栏配置。
您也可以自己配置导航栏,覆盖默认的导航栏配置。
默认配置如下:

View File

@ -82,6 +82,7 @@ interface SearchBoxLocale {
### 启用
```ts title=".vuepress/config.ts" twoslash
// @errors: 2353
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'

View File

@ -13,6 +13,7 @@ permalink: /config/watermark/
## 使用
```ts title=".vuepress/config.ts" twoslash
// @errors: 7006
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'

View File

@ -14,8 +14,7 @@ permalink: /config/theme/
::: warning 该字段不支持在 [主题配置文件 `plume.config.js`](./intro.md#主题配置文件) 中进行配置。
:::
无以上声明的字段,你可以在 `.vuepress/config.ts` 或者 `.vuepress/plume.config.ts` 的任意一个文件中
进行配置,一般情况下建议在 `.vuepress/plume.config.ts` 中进行配置。
无以上声明的字段,您可以在 `.vuepress/config.ts` 或者 `.vuepress/plume.config.ts` 的任意一个文件中进行配置,一般情况下建议在 `.vuepress/plume.config.ts` 中进行配置。
::: warning 已经在一个配置文件中进行配置的字段,尽量不要在另一个配置文件中重复配置
:::
@ -441,9 +440,9 @@ export default defineUserConfig({
:::
[可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
[可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
如果 **Iconify** 无法满足你的需求,可以传入 `{ svg: string, name?: string }`的格式,使用自定义图标,传入 svg 源码字符串,可选 `name` 字段,用于配置 [`navbarSocialInclude`](#navbarsocialinclude)
如果 **Iconify** 无法满足您的需求,可以传入 `{ svg: string, name?: string }` 格式使用自定义图标,传入 SVG 源码字符串,可选 `name` 字段用于配置 [`navbarSocialInclude`](#navbarsocialinclude)
示例:

View File

@ -28,9 +28,9 @@ docs:
-
name: 哦麦 MC
desc: 我的世界教学文档。
logo: https://static.ohmymc.com/img/minecraft-154749_1280.png?max_width=1920&max_height=1920
logo: https://s.xc.life/img/img/minecraft-154749_1280.png
url: https://ohmymc.com/
preview: https://static.ohmymc.com/img/20241228225159139.png?max_width=1920&max_height=1920
preview: https://s.xc.life/img/img/20241228225159139.png
-
name: NcatBotDocs
desc: NcatBot一个 QQ 机器人框架项目的使用文档。
@ -93,6 +93,12 @@ docs:
logo: https://official.skycraft.cn/i/3.jpg
url: https://docs.skycraft.cn/
preview: https://bbsimage.skycraft.cn/docs-preview.jpg
-
name: mcenahle Docs
desc: mcenahle 的文档网站。
logo: https://d.mcenahle.cn/images/logo.png
url: https://d.mcenahle.cn/
preview: https://mcenahle.cn/resources/docs-site-preview.jpg
blog:
-
@ -269,6 +275,13 @@ blog:
logo: https://raw.githubusercontent.com/Konata9/pic-base/main/pics/20260126223726455.png
url: https://konata9.cc/
preview: https://raw.githubusercontent.com/Konata9/pic-base/main/pics/20260125225910673.webp
-
name: Esyka
desc: Esyka's Blog
logo: https://www.esyka.top/images/logo.png
url: https://www.esyka.top/
repo: https://github.com/esyka114514
preview: https://www.esyka.top/images/preview.png
---
:::important

View File

@ -124,8 +124,8 @@ export default defineUserConfig({
'/': {
// Chinese collection configuration // [!code focus:4]
collections: [
{ type: 'post', dir: 'blog', title: '博客' },
{ type: 'doc', dir: 'typescript', title: 'TypeScript笔记', sidebar: 'auto' }
{ type: 'post', dir: 'blog', title: 'Blog' },
{ type: 'doc', dir: 'typescript', title: 'TypeScript Notes', sidebar: 'auto' }
],
},
'/en/': {
@ -150,8 +150,8 @@ export default defineThemeConfig({
'/': {
// Chinese collection configuration // [!code focus:4]
collections: [
{ type: 'post', dir: 'blog', title: '博客' },
{ type: 'doc', dir: 'typescript', title: 'TypeScript笔记', sidebar: 'auto' }
{ type: 'post', dir: 'blog', title: 'Blog' },
{ type: 'doc', dir: 'typescript', title: 'TypeScript Notes', sidebar: 'auto' }
],
},
'/en/': {

View File

@ -159,7 +159,7 @@ The `include` configuration is implemented by the
- **Default:** `{ provider: 'iconify' }`
- **Details:** Icon configuration.
[View **icon** usage instructions](../../theme/guide/features/icon.md){.read-more}
[View **icon** usage instructions](../guide/features/icon.md){.read-more}
### plot

View File

@ -82,6 +82,7 @@ Refer to [Algolia DocSearch Reference](/guide/features/content-search/#algolia-d
### Enable
```ts title=".vuepress/config.ts" twoslash
// @errors: 2353
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'

View File

@ -13,6 +13,7 @@ Related plugin: [@vuepress/plugin-watermark](https://ecosystem.vuejs.press/zh/pl
## Usage
```ts title=".vuepress/config.ts" twoslash
// @errors: 7006
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'

View File

@ -77,12 +77,12 @@ list:
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
-
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
socials:
-
icon: github
@ -96,7 +96,7 @@ list:
avatar: https://github.com/pengzhanbo.png
location: GuangZhou
organization: PengZhanBo
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
socials:
-
icon: github
@ -110,17 +110,17 @@ list:
avatar: https://github.com/pengzhanbo.png
location: GuangZhou
organization: PengZhanBo
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
groups:
-
title: 分组 1
desc: 自定义颜色
title: Group 1
desc: Custom colors
list:
-
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
backgroundColor: rgb(255,153,0)
color: rgb(255,255,153)
nameColor: rgb(255,255,170)
@ -135,7 +135,7 @@ groups:
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
backgroundColor: rgb(255,102,102)
color: rgb(255,204,204)
nameColor: rgb(255,238,238)
@ -143,22 +143,22 @@ groups:
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
backgroundColor: rgb(0,153,204)
color: rgb(153,238,255)
nameColor: rgb(153,255,255)
-
title: 分组 2
desc: 这里是分组 2 的描述文字
title: Group 2
desc: Description for Group 2
list:
-
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
-
name: pengzhanbo
link: https://github.com/pengzhanbo
avatar: https://github.com/pengzhanbo.png
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
desc: Even if slow, persist without stop; even if falling behind, even if failing, one must be able to reach the goal they are heading towards.
---

View File

@ -86,7 +86,7 @@ Refer to the [ECharts documentation](https://echarts.apache.org/handbook/zh/get-
## Advanced
You can import and use `defineEchartsConfig` in the
[client configuration file](https://vuejs.press/zh/guide/configuration.html##使用脚本) to customize ECharts:
[client configuration file](https://vuejs.press/guide/configuration.html#client-config-file) to customize ECharts:
```ts
import { defineEchartsConfig } from '@vuepress/plugin-markdown-chart/client'

View File

@ -132,4 +132,4 @@ It can also be placed within a `<CardGrid>` component.
/>
</CardGrid>
[View Photography Works Example](../../../../../blog/1.示例/照片类作品示例.md)
[View Photography Works Example](/en/demos/)

View File

@ -64,7 +64,7 @@ export default defineUserConfig({
encrypt: {
rules: {
// Can be the relative path of an MD file to encrypt that file
'前端/基础.md': '123456',
'frontend/basics.md': '123456',
// Can be a directory path to encrypt all articles under that directory
'/notes/vuepress-theme-plume/': '123456',
// Can be a request path to encrypt all articles under that path

View File

@ -16,7 +16,7 @@ The theme supports icons from the following sources:
Icons are used in the same way across the following theme features:
- [Navbar Icons](../../config/navbar.md#configuration)
- [Sidebar Icons](../../guide/document.md#sidebar-icons)
- [Sidebar Icons](../../guide/quick-start/sidebar.md#visual-enhancement-features)
- [File Tree Icons](../../guide/markdown/file-tree.md)
- [Code Group Title Icons](../code/code-tabs.md#group-title-icons)

View File

@ -159,13 +159,13 @@ Right-aligned content
**Input:**
````md
The farthest distance in the world Is not the distance between life and death But you don't know I love you when I stand in front of you.[^footnote1]
The farthest distance in the world Is not the distance between life and death But you don't know I love you when I stand in front of you.[^footnote1].
[^footnote1]: From India.Rabindranath Tagore **The Farthest Distance in the World**
````
**Output:**
The farthest distance in the world Is not the distance between life and death But you don't know I love you when I stand in front of you.[^footnote1]
The farthest distance in the world Is not the distance between life and death But you don't know I love you when I stand in front of you.[^footnote1].
[^footnote1]: From India.Rabindranath Tagore **The Farthest Distance in the World**

View File

@ -3,6 +3,9 @@ title: File Tree
createTime: 2025/10/08 14:41:57
icon: mdi:file-tree
permalink: /en/guide/markdown/file-tree/
badge:
text: Change
type: warning
---
## Overview
@ -18,7 +21,7 @@ displayed, simply add a slash `/` at the end of the list item.
The following syntax can be used to customize the appearance of the file tree:
- Emphasize file or directory names by making them bold, e.g., `**README.md**`
- Add comments to files or directories by adding additional text after the name
- Add comments to files or directories by appending a comment starting with `#` after the name, for example, `README.md This is a README file`
- Mark files or directories as **added** or **deleted** by prefixing the name with `++` or `--`
- Use `...` or `…` as the name to add placeholder files and directories.
- Add `icon="simple"` or `icon="colored"` after `:::file-tree` to switch to simple icons or colored icons. The default is colored icons.
@ -34,7 +37,7 @@ The following syntax can be used to customize the appearance of the file tree:
- ++ config.ts
- -- page1.md
- README.md
- theme A **theme** directory
- theme # A **theme** directory
- client
- components
- **Navbar.vue**
@ -61,7 +64,7 @@ The following syntax can be used to customize the appearance of the file tree:
- ++ config.ts
- -- page1.md
- README.md
- theme A **theme** directory
- theme # A **theme** directory
- client
- components
- **Navbar.vue**

View File

@ -0,0 +1,337 @@
---
title: Obsidian Compatibility
icon: simple-icons:obsidian
createTime: 2026/04/17 21:56:55
permalink: /en/guide/markdown/obsidian/
---
## Overview
The theme provides compatibility support for Obsidian's official Markdown extension syntax through the `vuepress-plugin-md-power` plugin, enabling Obsidian users to write documentation using familiar syntax.
Currently supported Obsidian extension syntax includes:
- [Wiki Links](#wiki-links) - Syntax for inter-page linking
- [Embeds](#embeds) - Embed content from other files into the current page
- [Comments](#comments) - Add comments visible only during editing
::: warning No plans to support extension syntax provided by Obsidian's third-party community plugins
:::
## Wiki Links
Wiki Links are syntax used in Obsidian for linking to other notes. Use double brackets `[[]]` to wrap content to create internal links.
### Syntax
```md
[[filename]]
[[filename#heading]]
[[filename#heading#subheading]]
[[filename|alias]]
[[filename#heading|alias]]
[[https://example.com|External Link]]
```
### Filename Search Rules
When using Wiki Links, filenames are matched according to the following rules:
**Match Priority:**
1. **Full Path** - Exact match against file paths
2. **Fuzzy Match** - Match filenames at the end of paths, prioritizing the shortest path
**Path Resolution Rules:**
- **Relative paths** (starting with `.`): Resolved relative to the current file's directory
- **Absolute paths** (not starting with `.`): Searched throughout the document tree, prioritizing the shortest path
- **Directory form** (ending with `/`): Matches `README.md` in that directory
**Example:**
Assuming the following document structure:
```txt
docs/
├── README.md
├── guide/
│ ├── README.md
│ └── markdown/
│ └── obsidian.md
```
In `docs/guide/markdown/obsidian.md`:
| Syntax | Match Result |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `[[obsidian]]` | Matches `docs/guide/markdown/obsidian.md` (matched via filename) |
| `[[./]]` | Matches `docs/guide/markdown/README.md` (relative path) |
| `[[../]]` | Matches `docs/guide/README.md` (parent directory) |
| `[[guide/]]` | Matches `docs/guide/README.md` (directory form) |
### Examples
**External Links:**
**Input:**
```md
[[https://example.com|External Link]]
```
**Output:**
[[https://example.com|External Link]]
**Internal Anchor Links:**
**Input:**
```md
[[npm-to]] <!-- Search by filename -->
[[guide/markdown/math]] <!-- Search by file path -->
[[#Wiki Links]] <!-- Heading on current page -->
[[file-tree#Configuration]] <!-- Search by filename, link to heading -->
```
**Output:**
[[npm-to]]
[[guide/markdown/math]]
[[#Wiki Links]]
[[file-tree#Configuration]]
[Obsidian Official - **Wiki Links**](https://obsidian.md/en/help/links){.readmore}
## Embeds
The embed syntax allows you to insert other file resources into the current page.
### Syntax
```md
![[filename]]
![[filename#heading]]
![[filename#heading#subheading]]
```
Filename search rules are the same as [Wiki Links](#filename-search-rules).
::: info Resources starting with `/` or having no path prefix like `./` are loaded from the `public` directory
:::
### Image Embeds
**Syntax:**
```md
![[image]]
![[image|300]]
![[image|300x200]]
```
Supported formats: `jpg`, `jpeg`, `png`, `gif`, `avif`, `webp`, `svg`, `bmp`, `ico`, `tiff`, `apng`, `jfif`, `pjpeg`, `pjp`, `xbm`
**Example:**
::: demo markdown title="Basic Image" expanded
```md
![[images/custom-hero.jpg]]
```
:::
::: demo markdown title="Set Width" expanded
```md
![[images/custom-hero.jpg|300]]
```
:::
::: demo markdown title="Set Width and Height" expanded
```md
![[images/custom-hero.jpg|300x200]]
```
:::
### PDF Embeds
> [!NOTE]
> PDF embeds require the `markdown.pdf` plugin to be enabled for proper functionality.
**Syntax:**
```md
![[document.pdf]]
![[document.pdf#page=1]] <!-- #page=1 means first page -->
![[document.pdf#page=1#height=300]] <!-- #page=page number #height=height -->
```
Supported formats: `pdf`
---
### Audio Embeds
**Syntax:**
```md
![[audio file]]
```
Supported formats: `mp3`, `flac`, `wav`, `ogg`, `opus`, `webm`, `acc`
---
### Video Embeds
> [!NOTE]
> Video embeds require the `markdown.artPlayer` plugin to be enabled for proper functionality.
**Syntax:**
```md
![[video file]]
![[video file#height=400]] <!-- Set video height -->
```
Supported formats: `mp4`, `webm`, `mov`, etc.
---
### Content Fragment Embeds
Content fragments under a specified heading can be embedded using `#heading`:
**Input:**
```md
![[my-note]]
![[my-note#Heading One]]
![[my-note#Heading One#Subheading]]
```
[Obsidian Official - **Insert Files**](https://obsidian.md/en/help/embeds){.readmore}
[Obsidian Official - **File Formats**](https://obsidian.md/en/help/file-formats){.readmore}
## Comments
Content wrapped in `%%` is treated as a comment and will not be rendered on the page.
### Syntax
**Inline Comments:**
```md
This is an %%inline comment%% example.
```
**Block Comments:**
```md
%%
This is a block comment.
It can span multiple lines.
%%
```
### Examples
**Inline Comments:**
**Input:**
```md
This is an %%inline comment%% example.
```
**Output:**
This is an %%inline comment%% example.
---
**Block Comments:**
**Input:**
```md
Content before the comment
%%
This is a block comment.
It can span multiple lines.
%%
Content after the comment
```
**Output:**
Content before the comment
%%
This is a block comment.
%%
It can span multiple lines.
[Obsidian Official - **Comments**](https://obsidian.md/en/help/syntax#%E6%B3%A8%E9%87%8B){.readmore}
## Configuration
Obsidian compatibility features are all enabled by default. You can selectively enable or disable them through configuration:
```ts title=".vuepress/config.ts"
export default defineUserConfig({
theme: plumeTheme({
plugins: {
mdPower: {
obsidian: {
wikiLink: true, // Wiki Links
embedLink: true, // Embeds
comment: true, // Comments
},
pdf: true, // PDF embed functionality
artPlayer: true, // Video embed functionality
}
}
})
})
```
### Configuration Options
:::: field-group
::: field name="wikiLink" type="boolean" default="true" optional
Enable Wiki Links syntax.
:::
::: field name="embedLink" type="boolean" default="true" optional
Enable embed content syntax.
:::
::: field name="comment" type="boolean" default="true" optional
Enable comment syntax.
:::
::::
## Notes
- These plugins provide **compatibility support** and do not fully implement all of Obsidian's functionality
- Some Obsidian-specific features (such as internal link graph views, bidirectional links, etc.) are outside the scope of this support
- When embedding content, the embedded page also participates in the theme's build process
- PDF embeds require the `markdown.pdf` plugin to be enabled simultaneously
- Video embeds require the `markdown.artPlayer` plugin to be enabled simultaneously
- Embed resources starting with `/` or using `./` form are loaded from the `public` directory

View File

@ -94,7 +94,7 @@ export default defineUserConfig({
{
type: 'doc',
dir: 'guide',
title: '指南',
title: 'Guide',
// autoFrontmatter: true, // Theme built-in configuration
autoFrontmatter: {
title: true, // Auto-generate title
@ -117,7 +117,7 @@ export default defineThemeConfig({
{
type: 'doc',
dir: 'guide',
title: '指南',
title: 'Guide',
// autoFrontmatter: true, // Theme built-in configuration
autoFrontmatter: {
title: true, // Auto-generate title
@ -297,23 +297,23 @@ Example:
::: code-tree
```md title="docs/blog/服务.md"
```md title="docs/blog/service.md"
---
title: 服务
permalink: /blog/wu-fu/
title: Service
permalink: /blog/service/
---
```
```md title="docs/blog/都城.md"
```md title="docs/blog/capital.md"
---
title: 都城
permalink: /blog/dou-cheng/
title: Capital
permalink: /blog/capital/
---
```
:::
You probably noticed that in the example, the permalink for the `都城.md` file is `/blog/dou-cheng/`,
You probably noticed that in the example, the permalink for the `capital.md` file is `/blog/capital/`,
which is incorrect. This is because `pinyin-pro`'s default dictionary cannot accurately identify polyphonic
characters. If you need a more precise conversion result,you can manually install `@pinyin-pro/data`,
and the theme will automatically load this dictionary to improve accuracy.
@ -326,9 +326,9 @@ npm i @pinyin-pro/data
:::
```md title="docs/blog/都城.md"
```md title="docs/blog/capital.md"
---
title: 都城
permalink: /blog/du-cheng/
title: Capital
permalink: /blog/capital/
---
```

View File

@ -760,8 +760,6 @@ Automatically switches to `top` layout on narrow-screen devices to ensure displa
## Article Metadata
## 文章元数据
In the collection, the `meta` option allows you to set the display method of article metadata,
This setting will directly affect the display of metadata on both the **article list page** and the **article content page**:
@ -779,7 +777,7 @@ export default defineUserConfig({
{
type: 'post',
dir: 'blog',
title: '博客',
title: 'Blog',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels
@ -808,7 +806,7 @@ export default defineThemeConfig({
{
type: 'post',
dir: 'blog',
title: '博客',
title: 'Blog',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels

View File

@ -28,10 +28,10 @@ A typical VuePress static site has the following file structure:
:::file-tree
- my-site
- docs \# Source directory
- docs # Source directory
- .vuepress/
- …
- README.md \# Homepage
- README.md # Homepage
- package.json
:::

View File

@ -13,7 +13,7 @@ whether you are creating a **technical blog**, **personal journal**, **product d
**knowledge base**, or **tutorial series**.
Deep optimizations have been applied to typography and image display,
with extensive enhancements specifically for Markdown syntax.This allows you to effortlessly
with extensive enhancements specifically for Markdown syntax. This allows you to effortlessly
create professional content that is **aesthetically pleasing, highly readable, and expressive**.
::: details New to VuePress?
@ -42,7 +42,7 @@ a more beautiful, clean, and user-friendly reading experience.
==content encryption==, and ==article watermarking==.
- **Code Presentation**: Support for code block ==grouping==, ==collapsing==, ==focusing==,
==line highlighting==, ==diff comparison==, and embedding ==code demos== from platforms like CodePen, JSFiddle, and CodeSandbox.
- **Icon System**: Integration with [iconify](https://icon-sets.iconify.d/) providing access to
- **Icon System**: Integration with [iconify](https://icon-sets.iconify.design/) providing access to
**200,000+** ==icons==, with optional support for `iconfont` / `fontawesome` icon libraries.
- **Media Embedding**: Support for ==PDF embedding==, and ==Bilibili/Youtube/Local Video== embedding.
- **Chart Rendering**: Integration with multiple ==chart engines== including chart.js, Echarts, Mermaid, Flowchart, Markmap, and PlantUML.

View File

@ -13,22 +13,22 @@ For projects created via the [command-line tool](./usage.md#command-line-install
::: file-tree
- .git/
- **docs** \# Documentation source directory
- .vuepress \# VuePress configuration directory
- public/ \# Static assets
- client.ts \# Client configuration (optional)
- collections.ts \# Collections configuration (optional)
- config.ts \# VuePress main configuration
- navbar.ts \# Navbar configuration (optional)
- plume.config.ts \# Theme configuration file (optional)
- demo \# `doc` type collection
- **docs** # Documentation source directory
- .vuepress/ # VuePress configuration directory
- public/ # Static assets
- client.ts # Client configuration (optional)
- collections.ts # Collections configuration (optional)
- config.ts # VuePress main configuration
- navbar.ts # Navbar configuration (optional)
- plume.config.ts # Theme configuration file (optional)
- demo # `doc` type collection
- foo.md
- bar.md
- blog \# `post` type collection
- preview \# Blog category
- markdown.md \# Category article
- article.md \# Blog article
- README.md \# Site homepage
- blog # `post` type collection
- preview # Blog category
- markdown.md # Category article
- article.md # Blog article
- README.md # Site homepage
- …
- package.json
- pnpm-lock.yaml

View File

@ -33,7 +33,7 @@ A typical project structure might look like:
- rust # Rust Programming Notes
- tuple.md
- struct.md
- README.md # Site Homepage
- README.md # Site Homepage
:::
### Configuration via `sidebar`

View File

@ -35,7 +35,7 @@ Page content starts after the second `---`.
Frontmatter is a configuration block using
[YAML](https://dev.to/paulasantamaria/introduction-to-yaml-125f) format, located at the top of a Markdown file and delimited by `---`.
It is recommended to read the [Frontmatter Detailed Guide](../../../../4.教程/frontmatter.md) for the complete syntax specification.
It is recommended to read the [Frontmatter Detailed Guide](../auto-frontmatter.md) for the complete syntax specification.
:::
## Automatic Frontmatter Generation
@ -139,14 +139,14 @@ The numeric part serves as the **sorting basis**. Directories without numbers ar
::: file-tree
- docs
- blog \# post type collection
- blog # post type collection
- 1.Frontend
- 1.html/
- 2.css/
- 3.javascript/
- 2.Backend/
- DevOps/
- typescript \# doc type collection
- typescript # doc type collection
- 1.Basics
- 1.Variables.md
- 2.Types.md

View File

@ -5,19 +5,19 @@ createTime: 2024/04/22 09:44:37
permalink: /en/guide/repl/kotlin/
---
## 概述
## Overview
主题提供了 Kotlin 代码演示,支持 在线运行 Kotlin 代码。
The theme provides Kotlin code demonstrations, supporting online execution of Kotlin code.
::: important
该功能通过将 代码提交到 服务器 进行 编译并执行,且一次只能提交单个代码文件。
This feature works by submitting code to a server for compilation and execution, and only a single code file can be submitted at a time.
因此,请不要使用此功能 执行 过于复杂的代码,也不要过于频繁的进行执行请求。
Therefore, please do not use this feature to execute overly complex code, and avoid making execution requests too frequently.
:::
## 配置
## Configuration
该功能默认不启用,你可以通过配置来启用它。
This feature is disabled by default. You can enable it through configuration.
```ts title=".vuepress/config.ts"
export default defineUserConfig({
@ -31,39 +31,39 @@ export default defineUserConfig({
})
```
## 使用
## Usage
使用 `::: kotlin-repl` 容器语法 将 kotlin 代码块包裹起来。主题会检查代码块并添加执行按钮。
Use the `::: kotlin-repl` container syntax to wrap Kotlin code blocks. The theme will inspect the code block and add an execution button.
### 只读代码演示
### Read-Only Code Demo
kotlin 代码演示默认是只读的,不可编辑。
Kotlin code demos are read-only by default and cannot be edited.
````md
::: kotlin-repl title="自定义标题"
::: kotlin-repl title="Custom Title"
```kotlin
// your kotlin code
```
:::
````
### 可编辑代码演示
### Editable Code Demo
如果需要在线编辑并执行,需要将代码块包裹在 `::: kotlin-repl editable` 容器语法中
If you need online editing and execution, wrap the code block in the `::: kotlin-repl editable` container syntax.
````md
::: kotlin-repl editable title="自定义标题"
::: kotlin-repl editable title="Custom Title"
```kotlin
// your kotlin code
```
:::
````
## 示例
## Examples
### 打印内容
### Print Content
**输入:**
**Input:**
````md
::: kotlin-repl
@ -78,7 +78,7 @@ fun main(args: Array<String>) {
:::
````
**输出:**
**Output:**
::: kotlin-repl
@ -93,7 +93,7 @@ fun main(args: Array<String>) {
:::
### 运算
### Computation
::: kotlin-repl
@ -109,9 +109,9 @@ fun main(args: Array<String>) {
:::
### 可编辑代码演示
### Editable Code Demo
**输入:**
**Input:**
````md
::: kotlin-repl editable
@ -126,7 +126,7 @@ fun main(args: Array<String>) {
:::
````
**输出:**
**Output:**
::: kotlin-repl editable

View File

@ -51,7 +51,7 @@ export default defineUserConfig({
:::
````
标配置请查看 [chart.js] 文档
表配置请查看 [chart.js] 文档
## 示例

View File

@ -7,7 +7,7 @@ permalink: /guide/code/group/
## 概述
代码组Code Tabs 主题 中用于并排展示多个相关代码片段的强大功能。
代码组Code Tabs是主题中用于并排展示多个相关代码片段的强大功能。
通过标签页形式组织代码,您可以清晰对比不同技术栈、配置方案或语言版本的实现差异。
## 基础语法

View File

@ -641,7 +641,7 @@ str = 'Hello'
你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 `description` 中找到对应的错误码。
然后再将错误码添加到 `@errors` 中。
不用担心变异失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。
不用担心编译失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。
:::
### `@noErrors`

View File

@ -7,7 +7,7 @@ permalink: /guide/components/card-masonry/
## 概述
瀑布流容器是一个 通用的容器组件,你可以把任何内容放到 `<CardMasonry>` 里面,容器会自动计算每一个 **项** 的高度,
瀑布流容器是一个通用的容器组件,你可以把任何内容放到 `<CardMasonry>` 里面,容器会自动计算每一个 **项** 的高度,
然后将它们按照瀑布流的方式进行排列。
::: details 什么是项

View File

@ -7,7 +7,7 @@ permalink: /guide/components/home-box/
## 首页布局容器
自定义首页时,使用 `<HomeBox>` 提供给 区域 包装容器。
自定义首页时,使用 `<HomeBox>` 提供给区域的包装容器。
## Props

View File

@ -9,7 +9,7 @@ permalink: /guide/components/image-card/
使用 `<ImageCard>` 组件在页面中显示图片卡片。
图片卡片 有别于 markdown 的 普通插入图片方式,它展示与图片相关的更多信息,包括标题、描述、作者、链接等。
图片卡片有别于 markdown 的普通插入图片方式,它展示与图片相关的更多信息,包括标题、描述、作者、链接等。
适用于如 摄影作品、设计作品、宣传海报 等场景。
## Props

View File

@ -12,7 +12,7 @@ import NpmBadgeGroup from 'vuepress-theme-plume/features/NpmBadgeGroup.vue'
## 概述
Npm 徽章组件 用于显示 npm 包信息,并提供相关的链接。
Npm 徽章组件用于显示 npm 包信息,并提供相关的链接。
徽章由 <https://shields.io> 提供支持。

View File

@ -1,5 +1,5 @@
---
title: “隐秘”文本
title: “隐秘” 文本
icon: lets-icons:hide-eye
createTime: 2024/08/18 23:02:39
permalink: /guide/components/plot/
@ -7,7 +7,7 @@ permalink: /guide/components/plot/
## 概述
使用 `<Plot>` 组件显示 [“隐秘”文本](../markdown/plot.md) ,能够更灵活的控制行为。
使用 `<Plot>` 组件显示 ["隐秘"文本](../markdown/plot.md),能够更灵活地控制行为。
该组件默认不启用,你需要在 theme 配置中启用。

View File

@ -11,7 +11,7 @@ import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue'
## 概述
Repo 卡片组件 用于显示 GitHub / Gitee 仓库信息。
Repo 卡片组件用于显示 GitHub / Gitee 仓库信息。
## 使用

View File

@ -8,8 +8,8 @@ badge: 新
## 概述
对于大多数的站点而言,一个 **炫酷好看** 首页首屏,能够更容易的吸引用户停留下来。
但实现 **炫酷好看** 往往需要比较复杂的技术成本,以及一些不错的灵感。
对于大多数的站点而言,一个 **炫酷好看** 的首页首屏,能够更容易地吸引用户停留下来。
但实现 **炫酷好看** 的效果往往需要比较复杂的技术成本,以及一些不错的灵感。
主题对 **首页****Hero** 部分,内置了一系列 **炫酷好看** 的背景效果,
通过简单的配置即可应用到你的站点首页中:

View File

@ -320,7 +320,7 @@ const {
公告板的唯一标识由 `bulletin.id` 配置。
唯一标识是用于区分公告板,并根据 该表示 决定 `bulletin.lifetime` 的有效期。
唯一标识是用于区分公告板,并根据该标识决定 `bulletin.lifetime` 的有效期。
```ts
export default defineUserConfig({

View File

@ -122,7 +122,7 @@ export default defineUserConfig({
贡献者信息列表。
用户在本地 git 服务中配置的 用户名和邮箱 可能与 git 托管服务(如 github、gitlab、gitee的用户信息不一致。
用户在本地 git 服务中配置的用户名和邮箱可能与 git 托管服务(如 github、gitlab、gitee的用户信息不一致。
可以在此预先配置贡献者信息。
(对于非 github 的其他 git 托管服务,诸如 gitlab、gitee由于不能通过用户名直接获取头像和用户地址请在此

View File

@ -16,7 +16,7 @@ permalink: /guide/features/icon/
在主题的以下功能中以相同的方式使用图标:
- [导航栏图标](../../config/navbar.md#配置)
- [侧边栏图标](../../guide/document.md#侧边栏图标)
- [侧边栏图标](../quick-start/sidebar.md#视觉增强功能)
- [文件树图标](../../guide/markdown/file-tree.md)
- [代码分组标题图标](../code/code-tabs.md#分组标题图标)

View File

@ -26,16 +26,15 @@ tags:
内部和外部链接都会被特殊处理。
主题默认对每个 md 文件自动生成一个新的 链接,并保存在对应的 md 文件的 frontmatter 的 `permalink` 中。
你可以随时修改它们。你也可以通过 `theme.autoFrontmatter` 选项来禁用这个功能,这时会恢复为 VuePress 的默认行为。
主题默认对每个 Markdown 文件自动生成一个新的链接,并保存在对应的 Markdown 文件的 frontmatter 的 `permalink` 中。您可以随时修改它们。您也可以通过 `theme.autoFrontmatter` 选项来禁用这个功能,这时会恢复为 VuePress 的默认行为。
### 内部链接
有三种方式来使用内部链接:
- 使用 生成的 `permalink` 作为内部链接的目标。
- 使用 md 文件的相对路径作为内部链接的目标。
- 使用 md 文件的绝对路径作为内部链接的目标, 绝对路径 `/` 表示从 `${sourceDir}` 目录开始。
- 使用生成的 `permalink` 作为内部链接的目标。
- 使用 Markdown 文件的相对路径作为内部链接的目标。
- 使用 Markdown 文件的绝对路径作为内部链接的目标,绝对路径 `/` 表示从 `${sourceDir}` 目录开始。
```md
[Markdown](/guide/markdown/)

View File

@ -3,6 +3,9 @@ title: 文件树
createTime: 2024/09/30 14:41:57
icon: mdi:file-tree
permalink: /guide/markdown/file-tree/
badge:
text: 变更
type: warning
---
## 概述
@ -17,12 +20,19 @@ permalink: /guide/markdown/file-tree/
以下语法可用于自定义文件树的外观:
- 通过加粗文件名或目录名来突出显示,例如 `**README.md**`
- 通过在名称后添加更多文本来为文件或目录添加注释
- 通过在名称后添加`#` 开头的注释来为文件或目录添加注释,例如 `README.md # 这是一个 README 文件`
- 通过在名称前添加 `++``--` 来标记文件或目录为 **新增** 或 **删除**
- 使用 `...``…` 作为名称来添加占位符文件和目录。
- 在 `:::file-tree` 后添加 `icon="simple"` 或 添加 `icon="colored"` 可以切换为简单图标或彩色图标,默认为彩色图标。
- 在 `:::file-tree` 后添加 `title="xxxx"` 可以为文件树添加标题。
::: important `rc.193` 主题更新说明
过去 `file-tree` 使用 **空格** 来区分文件名和注释,这在某些情况下会导致问题,例如文件名中包含空格时。
为了解决这个问题,我们引入了 **# 号注释** 语法,您可以在文件名后添加以 `#` 开头的注释,例如 `README.md # 这是一个 README 文件`
**此修改为 ==破坏性更新=={.danger} 更新。**
:::
**输入:**
```md /++/ /--/
@ -33,7 +43,7 @@ permalink: /guide/markdown/file-tree/
- ++ config.ts
- -- page1.md
- README.md
- theme 一个 **主题** 目录
- theme # 一个 **主题** 目录
- client
- components
- **Navbar.vue**
@ -60,7 +70,7 @@ permalink: /guide/markdown/file-tree/
- ++ config.ts
- -- page1.md
- README.md
- theme 一个 **主题** 目录
- theme # 一个 **主题** 目录
- client
- components
- **Navbar.vue**

View File

@ -0,0 +1,337 @@
---
title: Obsidian 兼容
icon: simple-icons:obsidian
createTime: 2026/04/17 21:56:55
permalink: /guide/markdown/obsidian/
---
## 概述
主题通过 `vuepress-plugin-md-power` 插件提供对 Obsidian 官方 Markdown 扩展语法的兼容性支持,使 Obsidian 用户能够以熟悉的语法撰写文档。
当前已支持的 Obsidian 扩展语法包括:
- [Wiki 链接](#wiki-链接) - 页面间相互链接的语法
- [嵌入内容](#嵌入内容) - 将其他文件内容嵌入到当前页面
- [注释](#注释) - 添加仅在编辑时可见的注释
::: warning 不计划支持 Obsidian 社区第三方插件提供的扩展语法
:::
## Wiki 链接
Wiki 链接是 Obsidian 中用于链接到其他笔记的语法。使用双括号 `[[]]` 包裹内容来创建内部链接。
### 语法
```md
[[文件名]]
[[文件名#标题]]
[[文件名#标题#子标题]]
[[文件名|别名]]
[[文件名#标题|别名]]
[[https://example.com|外部链接]]
```
### 文件名搜索规则
当使用 Wiki 链接时,文件名会按照以下规则进行搜索匹配:
**匹配优先级:**
1. **完整路径** - 精确匹配文件路径
2. **模糊匹配** - 匹配路径结尾的文件名,优先匹配最短路径
**路径解析规则:**
- **相对路径**(以 `.` 开头):相对于当前文件所在目录解析
- **绝对路径**(不以 `.` 开头):在整个文档树中搜索,优先匹配最短路径
- **目录形式**(以 `/` 结尾):匹配该目录下的 `README.md`
**示例:**
假设文档结构如下:
```txt
docs/
├── README.md
├── guide/
│ ├── README.md
│ └── markdown/
│ └── obsidian.md
```
`docs/guide/markdown/obsidian.md` 中:
| 语法 | 匹配结果 |
| -------------- | -------------------------------------------------------- |
| `[[obsidian]]` | 匹配 `docs/guide/markdown/obsidian.md`(通过文件名检索) |
| `[[./]]` | 匹配 `docs/guide/markdown/README.md`(相对路径) |
| `[[../]]` | 匹配 `docs/guide/README.md`(上级目录) |
| `[[guide/]]` | 匹配 `docs/guide/README.md`(目录形式) |
### 示例
**外部链接:**
**输入:**
```md
[[https://example.com|外部链接]]
```
**输出:**
[[https://example.com|外部链接]]
**内部锚点链接:**
**输入:**
```md
[[npm-to]] <!-- 通过文件名检索 -->
[[guide/markdown/math]] <!-- 通过文件路径检索-->
[[#Wiki 链接]] <!-- 当前页面使用 heading -->
[[file-tree#配置]] <!-- 通过文件名检索,并链接到 heading -->
```
**输出:**
[[npm-to]]
[[guide/markdown/math]]
[[#Wiki 链接]]
[[file-tree#配置]]
[Obsidian 官方 - **Wiki Links**](https://obsidian.md/zh/help/links){.readmore}
## 嵌入内容
嵌入语法允许你将其他文件资源插入到当前页面中。
### 语法
```md
![[文件名]]
![[文件名#标题]]
![[文件名#标题#子标题]]
```
文件名搜索规则与 [Wiki 链接](#文件名搜索规则) 相同。
::: info 以 `/` 开头或 无路径前缀如 `./` 形式的,从 `public` 目录中加载资源
:::
### 图片嵌入
**语法:**
```md
![[图片]]
![[图片|宽度]]
![[图片|宽度x高度]]
```
支持格式:`jpg``jpeg``png``gif``avif``webp``svg``bmp``ico``tiff``apng``jfif``pjpeg``pjp``xbm`
**示例:**
::: demo markdown title="基础图片" expanded
```md
![[images/custom-hero.jpg]]
```
:::
::: demo markdown title="设置宽度" expanded
```md
![[images/custom-hero.jpg|300]]
```
:::
::: demo markdown title="设置宽度和高度" expanded
```md
![[images/custom-hero.jpg|300x200]]
```
:::
### PDF 嵌入
> [!NOTE]
> PDF 嵌入需要启用 `markdown.pdf` 插件才能正常工作。
**语法:**
```md
![[文档.pdf]]
![[文档.pdf#page=1]] <!-- #page=1 表示第一页 -->
![[文档.pdf#page=1#height=300]] <!-- #page=页码 #height=高度 -->
```
支持格式:`pdf`
---
### 音频嵌入
**语法:**
```md
![[音频文件]]
```
支持格式:`mp3``flac``wav``ogg``opus``webm``acc`
---
### 视频嵌入
> [!NOTE]
> 视频嵌入需要启用 `markdown.artPlayer` 插件才能正常工作。
**语法:**
```md
![[视频文件]]
![[视频文件#height=400]] <!-- 设置视频高度 -->
```
支持格式:`mp4``webm``mov`
---
### 内容片段嵌入
通过 `#标题` 可以嵌入指定标题下的内容片段:
**输入:**
```md
![[我的笔记]]
![[我的笔记#标题一]]
![[我的笔记#标题一#子标题]]
```
[Obsidian 官方 - 插入文件](https://obsidian.md/zh/help/embeds){.readmore}
[Obsidian 官方 - 文件格式](https://obsidian.md/zh/help/file-formats){.readmore}
## 注释
使用 `%%` 包裹的内容会被当作注释,不会渲染到页面中。
### 语法
**行内注释:**
```md
这是一个 %%行内注释%% 示例。
```
**块级注释:**
```md
%%
这是一个块级注释。
可以跨越多行。
%%
```
### 示例
**行内注释:**
**输入:**
```md
这是一个 %%行内注释%% 示例。
```
**输出:**
这是一个 %%行内注释%% 示例。
---
**块级注释:**
**输入:**
```md
注释之前的内容
%%
这是一个块级注释。
可以跨越多行。
%%
注释之后的内容
```
**输出:**
注释之前的内容
%%
这是一个块级注释。
%%
可以跨越多行。
[Obsidian 官方 - 注释](https://obsidian.md/zh/help/syntax#%E6%B3%A8%E9%87%8A){.readmore}
## 配置
Obsidian 兼容功能默认全部启用,你可以通过配置选择性地启用或禁用:
```ts title=".vuepress/config.ts"
export default defineUserConfig({
theme: plumeTheme({
plugins: {
mdPower: {
obsidian: {
wikiLink: true, // Wiki 链接
embedLink: true, // 嵌入内容
comment: true, // 注释
},
pdf: true, // PDF 嵌入功能
artPlayer: true, // 视频嵌入功能
}
}
})
})
```
### 配置项
:::: field-group
::: field name="wikiLink" type="boolean" default="true" optional
启用 Wiki 链接语法。
:::
::: field name="embedLink" type="boolean" default="true" optional
启用嵌入内容语法。
:::
::: field name="comment" type="boolean" default="true" optional
启用注释语法。
:::
::::
## 注意事项
- 这些插件提供的是 **兼容性支持**,并非完全实现 Obsidian 的全部功能
- 部分 Obsidian 特有的功能(如内部链接的图谱视图、双向链接等)不在支持范围内
- 嵌入内容时,被嵌入的页面也会参与主题的构建过程
- PDF 嵌入需要同时启用 `markdown.pdf` 插件
- 视频嵌入需要同时启用 `markdown.artPlayer` 插件
- 以 `/` 开头或使用 `./` 形式的嵌入资源会从 `public` 目录加载

View File

@ -536,9 +536,9 @@ export default defineThemeConfig({
:::
[可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
[可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
如果 **Iconify** 无法满足你的需求,可以传入 `{ svg: string, name?: string }`的格式,使用自定义图标,传入 svg 源码字符串。
如果 **Iconify** 无法满足您的需求,可以传入 `{ svg: string, name?: string }` 格式使用自定义图标,传入 SVG 源码字符串。
## 文章封面配置
@ -938,9 +938,7 @@ config:
更多自定义配置,请参考 [自定义首页](../custom/home.md)。
当使用以上两种方式 将首页配置为 文章列表页后,由于主题默认依然会生成 文章列表页,
这导致存在了重复功能的页面。为此,你可能需要在 集合配置中,
**关闭自动生成博客文章列表页**
当使用以上两种方式将首页配置为文章列表页后,由于主题默认依然会生成文章列表页,这导致存在了重复功能的页面。为此,您可能需要在集合配置中**关闭自动生成博客文章列表页**
(还可以重新修改 分类页/标签页/归档页的链接地址)

View File

@ -27,10 +27,10 @@ permalink: /guide/collection/
:::file-tree
- my-site
- docs \# 源目录
- docs # 源目录
- .vuepress/
- …
- README.md \# 首页
- README.md # 首页
- package.json
:::
@ -87,7 +87,7 @@ permalink: /guide/collection/
:::
`blog` 目录下的 markdown 文章,在 post 集合中读取为文章列表,并生成列表页、分类页、标签页等页面。
`blog` 目录下的 Markdown 文章将被读取为文章列表,并自动生成列表页、分类页、标签页等页面。
- **完成**
::::

View File

@ -12,22 +12,22 @@ permalink: /guide/project-structure/
::: file-tree
- .git/
- **docs** \# 文档源目录
- .vuepress \# VuePress 配置目录
- public/ \# 静态资源
- client.ts \# 客户端配置(可选)
- collections.ts \# Collections 配置(可选)
- config.ts \# VuePress 主配置
- navbar.ts \# 导航栏配置(可选)
- plume.config.ts \# 主题配置文件(可选)
- demo \# `doc` 类型 collection
- **docs** # 文档源目录
- .vuepress/ # VuePress 配置目录
- public/ # 静态资源
- client.ts # 客户端配置(可选)
- collections.ts # Collections 配置(可选)
- config.ts # VuePress 主配置
- navbar.ts # 导航栏配置(可选)
- plume.config.ts # 主题配置文件(可选)
- demo # `doc` 类型 collection
- foo.md
- bar.md
- blog \# `post` 类型 collection
- preview \# 博客分类
- markdown.md \# 分类文章
- article.md \# 博客文章
- README.md \# 站点首页
- blog # `post` 类型 collection
- preview # 博客分类
- markdown.md # 分类文章
- article.md # 博客文章
- README.md # 站点首页
- …
- package.json
- pnpm-lock.yaml

View File

@ -13,7 +13,7 @@ tags:
侧边栏是文档常见的页面导航方式,可以快速定位到文档内容。
主题提供了两种方式配置侧边栏,包括:
主题提供了两种方式配置侧边栏
- 通过主题配置的 `sidebar` 选项配置侧边栏
- 在 [类型为 `doc` 的集合](./collection-doc.md) 中配置侧边栏
@ -33,7 +33,7 @@ tags:
- rust # Rust 编程笔记
- tuple.md
- struct.md
- README.md # 站点首页
- README.md # 站点首页
:::
### 通过`sidebar` 配置

View File

@ -8,11 +8,9 @@ tags:
- 快速开始
---
VuePress 完整支持 [标准 Markdown 语法](../markdown/basic.md),同时允许通过
[YAML](https://dev.to/paulasantamaria/introduction-to-yaml-125f)
格式的 Frontmatter 定义页面元数据(如标题、创建时间等)。
VuePress 完整支持 [标准 Markdown 语法](../markdown/basic.md),同时允许通过 [YAML](https://dev.to/paulasantamaria/introduction-to-yaml-125f) 格式的 Frontmatter 定义页面元数据(如标题、创建时间等)。
此外,主题还提供了丰富的 [Markdown 扩展语法](../markdown/extensions.md)您不仅可以在 Markdown 中直接编写 HTML还能使用 Vue 组件来增强内容表现力。
此外,主题还提供了丰富的 [Markdown 扩展语法](../markdown/extensions.md),您不仅可以在 Markdown 中直接编写 HTML还能使用 Vue 组件来增强内容表现力。
## Frontmatter 页面配置
@ -132,14 +130,14 @@ const dir = /\d+\.[\s\S]+/
::: file-tree
- docs
- blog \# post 类型 collection
- blog # post 类型 collection
- 1.前端
- 1.html/
- 2.css/
- 3.javascript/
- 2.后端/
- 运维/
- typescript \# doc 类型 collection
- typescript # doc 类型 collection
- 1.基础
- 1.变量.md
- 2.类型.md

View File

@ -369,8 +369,8 @@ const count = ref(0)
:::
:::::: warning
vue demo 容器语法虽然也支持 使用 `.js/ts + css` 的方式来嵌入演示代码,
但主题不推荐这样做。因为 样式无法被隔离,这可能导致样式污染。
vue demo 容器语法虽然也支持使用 `.js/ts + css` 的方式来嵌入演示代码,
但主题不推荐这样做。因为样式无法被隔离,这可能导致样式污染。
::::: details 参考示例

View File

@ -98,6 +98,7 @@ search: false
| M*e | 2020-12-26 | 10.00 | 最近使用主题弄了个博客,简洁好用。作者回复也快,因为还是学生也就只能微博的支持了 |
| *纪 | 2026-01-03 | 9.90 | 新年快乐(,,>‿<,,),感谢佬 |
| J*n | 2026-01-22 | 10.00 | 用本开源主题搭了好几个网站了,作者耐心解答,添加合理功能需求,必须支持一下,辛苦了❤️ |
| *燧 | 2026-03-14 | 8.88 | 智齿主播,大佬加油 <br>(作者回复:啊?主播?我不是啊) |
</div>

View File

@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/themes/*": ["./.vuepress/themes/*"],
"~/components/*": ["./.vuepress/themes/components/*"],

View File

@ -7,8 +7,11 @@ export default config({
},
ignores: [
'lib',
'skills',
'docs/snippet/code-block.snippet.md',
'docs/snippet/whitespace.snippet.md',
'docs/en/guide/markdown/obsidian.md',
'docs/guide/markdown/obsidian.md',
],
globals: {
__VUEPRESS_VERSION__: 'readonly',

View File

@ -1,9 +1,9 @@
{
"name": "vuepress-theme-plume-monorepo",
"type": "module",
"version": "1.0.0-rc.192",
"version": "1.0.0-rc.196",
"private": true,
"packageManager": "pnpm@10.30.3",
"packageManager": "pnpm@10.33.0",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"keywords": [
@ -18,9 +18,9 @@
"pnpm": ">=9"
},
"scripts": {
"build": "pnpm clean && pnpm build:package",
"build": "pnpm run clean && pnpm build:package",
"build:package": "pnpm -r --stream build",
"clean": "pnpm -r --stream clean",
"clean": "pnpm -r --stream run clean",
"dev": "pnpm --stream '/(dev:package|docs:dev)/'",
"dev:package": "pnpm --parallel dev",
"docs:dev": "wait-on -d 100 theme/lib/node/index.js && pnpm -F=docs docs:dev",
@ -33,7 +33,7 @@
"lint:css": "stylelint **/*.{css,vue}",
"test": "cross-env TZ=Etc/UTC vitest --coverage",
"prepare": "husky",
"release:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release:changelog": "conventional-changelog -p angular",
"release:check": "pnpm lint && pnpm build",
"release:sync": "node scripts/mirror-sync.mjs",
"release:publish": "pnpm -r publish --tag latest",
@ -57,7 +57,8 @@
"@vitest/coverage-v8": "catalog:dev",
"bumpp": "catalog:dev",
"commitizen": "catalog:dev",
"conventional-changelog-cli": "catalog:dev",
"conventional-changelog": "catalog:dev",
"conventional-changelog-angular": "catalog:dev",
"cpx2": "catalog:dev",
"cross-env": "catalog:dev",
"cz-conventional-changelog": "catalog:dev",
@ -87,12 +88,21 @@
"resolutions": {
"@bufbuild/protobuf": "^2.11.0",
"@eslint-community/eslint-utils": "catalog:peer",
"@shikijs/core": "^4.0.2",
"@shikijs/twoslash": "^4.0.2",
"@typescript-eslint/types": "catalog:peer",
"@typescript-eslint/utils": "catalog:peer",
"baseline-browser-mapping": "^2.10.0",
"@xmldom/xmldom": ">=0.9.10",
"baseline-browser-mapping": "^2.10.20",
"chokidar": "catalog:prod",
"dompurify": ">=3.4.0",
"esbuild": "catalog:prod",
"follow-redirects": ">=1.16.0",
"lodash": ">=4.18.1",
"lodash-es": ">=4.18.1",
"sass-embedded": "catalog:peer",
"shiki": "^4.0.2",
"tmp": ">=0.2.5",
"vite": "catalog:dev",
"vue-router": "catalog:prod"
},

View File

@ -1,7 +1,7 @@
{
"name": "@vuepress-plume/plugin-fonts",
"type": "module",
"version": "1.0.0-rc.192",
"version": "1.0.0-rc.196",
"description": "The Plugin for VuePress 2 - fonts",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
@ -30,7 +30,7 @@
"build": "pnpm run tsdown && pnpm run copy",
"clean": "rimraf --glob ./lib",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png,woff2}\" lib",
"tsdown": "tsdown"
"tsdown": "tsdown --config-loader unrun"
},
"peerDependencies": {
"vuepress": "catalog:vuepress"

View File

@ -1,7 +1,5 @@
import { defineConfig } from 'tsdown'
import { argv } from '../../scripts/tsdown-args.mjs'
/** @import {Options} from 'tsdown' */
import { defineConfig, type UserConfig } from 'tsdown'
import { argv } from '../../scripts/tsdown-args'
const clientExternal = [
/.*\.vue$/,
@ -9,15 +7,13 @@ const clientExternal = [
]
export default defineConfig(() => {
/** @type {Options} */
const DEFAULT_OPTIONS = {
const DEFAULT_OPTIONS: UserConfig = {
dts: true,
sourcemap: false,
format: 'esm',
fixedExtension: false,
}
/** @type {Options[]} */
const options = []
const options: UserConfig[] = []
if (argv.node) {
options.push({
@ -36,7 +32,7 @@ export default defineConfig(() => {
entry: ['./src/client/config.ts'],
outDir: './lib/client',
dts: false,
external: clientExternal,
deps: { neverBundle: clientExternal },
},
])
}

View File

@ -72,7 +72,7 @@ describe('fileTreePlugin', () => {
- client
- components
- **Navbar.vue**
- index.ts \# comment
- index.ts # comment
- node
- index.ts
- .gitignore

View File

@ -0,0 +1,72 @@
import MarkdownIt from 'markdown-it'
import { describe, expect, it } from 'vitest'
import { commentPlugin } from '../src/node/obsidian/comment.js'
describe('commentPlugin', () => {
const md = new MarkdownIt().use(commentPlugin)
it('should ignore inline comment', () => {
const result = md.render('This is %%inline comment%% text.')
expect(result).not.toContain('inline comment')
expect(result).toContain('This is text.')
})
it('should ignore block comment', () => {
const result = md.render(`%% block comment %%
more text`)
expect(result).not.toContain('block comment')
expect(result).toContain('more text')
})
it('should handle multi-line block comment', () => {
const result = md.render(`%%
This is a block comment
spanning multiple lines
%%
This is after.`)
expect(result).not.toContain('block comment')
expect(result).not.toContain('spanning multiple lines')
expect(result).toContain('This is after.')
})
it('should handle comment at start of line', () => {
const result = md.render('%%comment%% start')
expect(result).toContain('start')
expect(result).not.toContain('comment')
})
it('should handle empty comment', () => {
const result = md.render('%%%%')
expect(result).toBeDefined()
})
it('should not treat single % as comment', () => {
const result = md.render('50% off')
expect(result).toContain('50%')
expect(result).not.toContain('%%')
})
it('should handle nested content after comment', () => {
const result = md.render(`%%
block comment
%%
## Heading
paragraph`)
expect(result).toContain('<h2')
expect(result).toContain('Heading')
expect(result).not.toContain('block comment')
})
it('should not parse incomplete comment without closing', () => {
const result = md.render('%%incomplete')
expect(result).toContain('%%incomplete')
})
it('should not parse single opening percent', () => {
const result = md.render('% test')
expect(result).toContain('% test')
})
})

View File

@ -0,0 +1,421 @@
import type { App } from 'vuepress'
import type { MarkdownEnv } from 'vuepress/markdown'
import MarkdownIt from 'markdown-it'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { embedLinkPlugin } from '../src/node/obsidian/embedLink.js'
import { initPagePaths } from '../src/node/obsidian/findFirstPage.js'
const mockGlobSync = vi.fn()
const mockReadFileSync = vi.fn()
vi.mock('vuepress/utils', () => ({
tinyglobby: {
globSync: (...args: unknown[]) => mockGlobSync(...args),
},
fs: {
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
},
path: {
dirname: vi.fn((p: string) => p.split('/').slice(0, -1).join('/') || '.'),
extname: vi.fn((p: string) => {
const i = p.lastIndexOf('.')
return i > 0 ? p.slice(i) : ''
}),
join: vi.fn((...args: string[]) => args.join('/')),
},
hash: vi.fn((s: string) => `hash_${s.length}`),
}))
vi.mock('gray-matter', () => ({
default: vi.fn((content: string) => ({
content: content.replace(/^---[\s\S]*?---\n?/, ''),
data: {},
})),
}))
vi.mock('@vuepress/helper', () => ({
removeLeadingSlash: vi.fn((p: string) => p.replace(/^\//, '')),
}))
function createMockApp(pages: App['pages'] = []): App {
return {
pages,
options: {
pagePatterns: ['**/*.md'],
},
dir: {
source: () => '/source',
},
} as unknown as App
}
function createMockEnv(filePathRelative = 'test.md'): MarkdownEnv {
return {
filePathRelative,
base: '/',
links: [],
importedFiles: [],
}
}
function createMarkdownWithMockRules() {
return MarkdownIt({ html: true }).use((md) => {
md.block.ruler.before('code', 'import_code', () => false)
md.renderer.rules.import_code = () => ''
})
}
describe('embedLinkPlugin', () => {
beforeEach(() => {
mockGlobSync.mockReset()
mockReadFileSync.mockReset()
})
// ==================== Asset Embedding ====================
describe('asset embedding', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([])
})
it('should render image embed', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[image.png]]')
expect(result).toContain('<img')
expect(result).toContain('src="/image.png"')
expect(result).toContain('alt="image.png"')
})
it('should render image with width setting', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[image.png|300]]')
expect(result).toContain('<img')
expect(result).toContain('width: 300px')
})
it('should render image with width x height setting', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[image.png|300x200]]')
expect(result).toContain('<img')
expect(result).toContain('width: 300px')
expect(result).toContain('height: 200px')
})
it('should render audio embed', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[audio.mp3]]')
expect(result).toContain('<audio')
expect(result).toContain('<source')
expect(result).toContain('src="/audio.mp3"')
})
it('should render video embed with artPlayer', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[video.mp4]]')
expect(result).toContain('<ArtPlayer')
expect(result).toContain('src="/video.mp4"')
expect(result).toContain('type="mp4"')
})
it('should render pdf embed', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[document.pdf]]')
expect(result).toContain('<PDFViewer')
expect(result).toContain('src="/document.pdf"')
})
it('should render pdf with page hash', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[document.pdf#page=1]]')
expect(result).toContain('page="1"')
})
})
// ==================== External Links ====================
describe('external links', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([])
})
it('should render external http link as anchor', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[https://example.com/file]]')
expect(result).toContain('<a')
expect(result).toContain('href="https://example.com/file"')
expect(result).toContain('target="_blank"')
})
it('should return http links as-is for assets', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[https://example.com/image.png]]')
expect(result).toContain('src="https://example.com/image.png"')
})
})
// ==================== Path Resolution ====================
describe('path resolution', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([])
})
it('should return relative paths starting with dot as-is', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[./image.png]]', createMockEnv('docs/page.md'))
expect(result).toContain('src="./image.png"')
})
it('should return absolute paths starting with slash as-is', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[/images/cover.jpg]]')
expect(result).toContain('src="/images/cover.jpg"')
})
it('should prepend slash to relative paths without dot', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[image.png]]')
expect(result).toContain('src="/image.png"')
})
it('should ignore non-image with unsupported extension as link', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[file.unknown]]')
expect(result).toContain('<a')
expect(result).toContain('href="file.unknown"')
})
})
// ==================== Markdown Embedding ====================
describe('markdown file embedding', () => {
const guideContent = `---
title: Guide
---
# Introduction
This is intro content.
## Getting Started
Steps for getting started.
## Advanced
Advanced content.
`
beforeEach(() => {
mockGlobSync.mockReturnValue(['guide.md'])
mockReadFileSync.mockReturnValue(guideContent)
const app = createMockApp()
initPagePaths(app)
})
it('should embed entire markdown file when no heading specified', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
const result = md.render('![[guide]]', env)
expect(result).toContain('Introduction')
expect(result).toContain('intro content')
expect(result).toContain('Getting Started')
expect(result).toContain('Steps for getting started')
})
it('should embed content under specific heading', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
const result = md.render('![[guide#Getting Started]]', env)
expect(result).toContain('Steps for getting started')
expect(result).not.toContain('Advanced content')
expect(result).not.toContain('#') // no heading markers
})
it('should embed nested heading content', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
const result = md.render('![[guide#Introduction#Getting Started]]', env)
expect(result).toContain('Steps for getting started')
expect(result).not.toContain('Advanced content')
})
it('should track imported files in env', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
md.render('![[guide]]', env)
expect(env.importedFiles).toContain('guide.md')
})
})
// ==================== Markdown Not Found ====================
describe('when page does not exist', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([])
})
it('should render markdown file embed as link', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[nonexistent.md]]')
expect(result).toContain('<a')
expect(result).toContain('href="/nonexistent.md"')
})
it('should render with heading anchor', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[nonexistent#section]]')
expect(result).toContain('<a')
expect(result).toContain('href="/nonexistent#section"')
expect(result).toContain('target="_blank"')
})
})
// ==================== Container Syntax ====================
describe('container syntax preservation', () => {
const contentWithContainers = `---
title: Test
---
# Section
::: info
This is a container
:::
Regular content.
`
beforeEach(() => {
mockGlobSync.mockReturnValue(['test.md'])
mockReadFileSync.mockReturnValue(contentWithContainers)
const app = createMockApp()
initPagePaths(app)
})
it('should preserve container syntax when embedding', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
const result = md.render('![[test#Section]]', env)
expect(result).toContain('::: info')
expect(result).toContain('This is a container')
expect(result).toContain('Regular content')
})
})
// ==================== Error Handling ====================
describe('error handling', () => {
it('should return empty string when file read fails', () => {
mockGlobSync.mockReturnValue(['guide.md'])
mockReadFileSync.mockImplementation(() => {
throw new Error('ENOENT')
})
const app = createMockApp()
initPagePaths(app)
const md = createMarkdownWithMockRules().use(embedLinkPlugin, app)
const env = createMockEnv()
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = md.render('![[guide]]', env)
expect(result).toBe('')
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('can not read file'))
warnSpy.mockRestore()
})
})
// ==================== Heading Search Edge Cases ====================
describe('heading search edge cases', () => {
it('should find heading when same text appears at different nesting levels', () => {
const content = `# Title
## Summary
Summary content.
## Details
### Summary
Nested summary under details.
## Conclusion
Conclusion content.`
mockGlobSync.mockReturnValue(['guide.md'])
mockReadFileSync.mockReturnValue(content)
const app = createMockApp()
initPagePaths(app)
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
// Should match first "Summary" at level 2
const result = md.render('![[guide#Summary]]', env)
expect(result).toContain('Summary content.')
expect(result).not.toContain('Nested summary')
})
it('should return empty string when heading not found', () => {
const content = `# Title
## Section
Content.`
mockGlobSync.mockReturnValue(['guide.md'])
mockReadFileSync.mockReturnValue(content)
const app = createMockApp()
initPagePaths(app)
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const env = createMockEnv()
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = md.render('![[guide#Nonexistent]]', env)
expect(result).toBe('')
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No heading found'))
warnSpy.mockRestore()
})
})
// ==================== Edge Cases ====================
describe('edge cases', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([])
})
it('should not parse embed not ending with ]]', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[image.png]')
expect(result).toContain('![[image.png]')
})
it('should not parse empty embed link', () => {
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
const result = md.render('![[]]')
expect(result).toContain('![[]]')
})
})
})

View File

@ -0,0 +1,297 @@
import { describe, expect, it } from 'vitest'
// Replicate the extractContentByHeadings logic for isolated testing
const HEADING_HASH_REG = /^#+/
const HEADING_ATTRS_REG = /(?:\{[^}]*\})?$/
interface ParsedHeading {
lineIndex: number
level: number
text: string
}
function extractContentByHeadings(content: string, headings: string[]): string {
if (!headings.length)
return content
const containers: Record<string, string> = {}
content = content.replaceAll(/(?<mark>:{3,})[\s\S]*?\k<mark>/g, (matched) => {
const key = `CONTAINER_${Object.keys(containers).length}`
containers[key] = matched
return `<!--container:${key}-->`
})
const lines = content.split(/\r?\n/)
const allHeadings: ParsedHeading[] = []
for (let i = 0; i < lines.length; i++) {
let text = lines[i].trimEnd()
let level = 0
text = text.replace(HEADING_HASH_REG, (matched) => {
level = matched.length
return ''
})
if (level) {
text = text.replace(HEADING_ATTRS_REG, '').trim()
allHeadings.push({ lineIndex: i, level, text })
}
}
let targetHeadingIndex = -1
let currentLevel = 0
let headingPointer = 0
for (let i = 0; i < allHeadings.length; i++) {
const heading = allHeadings[i]
if (headingPointer === 0) {
if (heading.text === headings[0]) {
headingPointer++
currentLevel = heading.level
if (headingPointer === headings.length) {
targetHeadingIndex = i
break
}
}
}
else {
if (heading.level > currentLevel && heading.text === headings[headingPointer]) {
headingPointer++
currentLevel = heading.level
if (headingPointer === headings.length) {
targetHeadingIndex = i
break
}
}
else if (heading.level <= currentLevel) {
if (heading.text === headings[0]) {
headingPointer = 1
currentLevel = heading.level
}
else {
headingPointer = 0
currentLevel = 0
}
}
}
}
if (targetHeadingIndex === -1) {
return ''
}
const targetHeading = allHeadings[targetHeadingIndex]
const startLine = targetHeading.lineIndex + 1
const targetLevel = targetHeading.level
let endLine = lines.length
for (let i = targetHeadingIndex + 1; i < allHeadings.length; i++) {
if (allHeadings[i].level <= targetLevel) {
endLine = allHeadings[i].lineIndex
break
}
}
const result = lines.slice(startLine, endLine).join('\n').trim()
return result.replaceAll(/<!--container:(.*?)-->/g, (_, key) => containers[key] ?? '')
}
describe('extractContentByHeadings', () => {
it('should return full content when no headings specified', () => {
const content = '# Title\n\nSome content here.'
expect(extractContentByHeadings(content, [])).toBe(content)
})
it('should extract content under single heading', () => {
const content = `# Title
Intro content.
## Section 1
Section 1 content.
## Section 2
Section 2 content.`
expect(extractContentByHeadings(content, ['Section 1'])).toBe('Section 1 content.')
})
it('should extract content under nested heading', () => {
const content = `# Title
## Level 2
### Level 3
Deep content.
## Back to Level 2
Other content.`
expect(extractContentByHeadings(content, ['Level 2', 'Level 3'])).toBe('Deep content.')
})
it('should stop at sibling heading of same level', () => {
const content = `# Title
## Section A
Content A.
## Section B
Content B.
### Nested in B
Nested content.`
expect(extractContentByHeadings(content, ['Section A'])).toBe('Content A.')
expect(extractContentByHeadings(content, ['Section B'])).toBe('Content B.\n\n### Nested in B\n\nNested content.')
})
it('should handle heading with attributes', () => {
const content = `# Title
## Section {#id .class data=value}
Section content with attributes.`
expect(extractContentByHeadings(content, ['Section'])).toBe('Section content with attributes.')
})
it('should preserve container syntax that appears within the extracted content', () => {
const content = `## Section
::: info
Container content
:::
Content after container.`
const result = extractContentByHeadings(content, ['Section'])
expect(result).toContain('::: info')
expect(result).toContain('Container content')
expect(result).toContain('Content after container')
})
it('should handle multiple containers within extracted content', () => {
const content = `## Section
::: info
First container
:::
::: warning
Second container
:::
Content.`
const result = extractContentByHeadings(content, ['Section'])
expect(result).toContain('::: info')
expect(result).toContain('First container')
expect(result).toContain('::: warning')
expect(result).toContain('Second container')
})
it('should return empty string when heading not found', () => {
const content = `# Title
## Section
Content.`
expect(extractContentByHeadings(content, ['Nonexistent'])).toBe('')
})
it('should handle deeply nested structure', () => {
const content = `# H1
## H2a
### H3a
H3a content.
### H3b
H3b content.
## H2b
H2b content.`
expect(extractContentByHeadings(content, ['H2a', 'H3b'])).toBe('H3b content.')
expect(extractContentByHeadings(content, ['H2a'])).toContain('H3a content')
expect(extractContentByHeadings(content, ['H2a'])).toContain('H3b content')
expect(extractContentByHeadings(content, ['H2a'])).not.toContain('H2b content')
})
it('should handle content with code blocks', () => {
const content = `# Title
## Section
\`\`\`js
const x = 1;
\`\`\`
More content.`
const result = extractContentByHeadings(content, ['Section'])
expect(result).toContain('```js')
expect(result).toContain('const x = 1;')
expect(result).toContain('More content.')
})
it('should handle content with blockquotes', () => {
const content = `# Title
## Section
> Blockquote text
Paragraph after.`
const result = extractContentByHeadings(content, ['Section'])
expect(result).toContain('> Blockquote text')
expect(result).toContain('Paragraph after.')
})
it('should handle headings at different levels with same text', () => {
const content = `# Title
## Summary
Summary content.
## Details
### Summary
Nested summary under details.
## Conclusion
Conclusion content.`
// Should match first "Summary" at level 2
expect(extractContentByHeadings(content, ['Summary'])).toBe('Summary content.')
})
it('should handle heading with trailing spaces', () => {
const content = `# Title
## Section
Section content.`
expect(extractContentByHeadings(content, ['Section'])).toBe('Section content.')
})
})

View File

@ -0,0 +1,156 @@
import type { App } from 'vuepress'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { findFirstPage, initPagePaths, updatePagePaths } from '../src/node/obsidian/findFirstPage.js'
const mockGlobSync = vi.fn()
vi.mock('vuepress/utils', () => ({
tinyglobby: {
globSync: (...args: unknown[]) => mockGlobSync(...args),
},
path: {
dirname: vi.fn((p: string) => p.split('/').slice(0, -1).join('/') || '.'),
extname: vi.fn((p: string) => {
const i = p.lastIndexOf('.')
return i > 0 ? p.slice(i) : ''
}),
join: vi.fn((...args: string[]) => args.join('/')),
},
}))
vi.mock('@vuepress/helper', () => ({
removeLeadingSlash: vi.fn((p: string) => p.replace(/^\//, '')),
}))
function createMockApp(pagePatterns = ['**/*.md']): App {
return {
pages: [],
options: {
pagePatterns,
},
dir: {
source: () => '/source',
},
} as unknown as App
}
describe('findFirstPage', () => {
beforeEach(() => {
mockGlobSync.mockReset()
})
describe('initPagePaths', () => {
it('should initialize page paths from glob pattern', () => {
mockGlobSync.mockReturnValue([
'README.md',
'guide.md',
'docs/api.md',
'docs/guide/intro.md',
])
const app = createMockApp()
initPagePaths(app)
expect(mockGlobSync).toHaveBeenCalledWith(['**/*.md'], {
cwd: '/source',
ignore: ['**/node_modules/**', '**/.vuepress/**'],
})
})
it('should sort page paths by directory depth', () => {
mockGlobSync.mockReturnValue([
'docs/a/b/c.md',
'a.md',
'docs/a.md',
])
const app = createMockApp()
initPagePaths(app)
// Should find a.md first because it's shortest
expect(findFirstPage('a', 'any/path.md')).toBe('a.md')
})
})
describe('updatePagePaths', () => {
it('should add new page path on create', () => {
mockGlobSync.mockReturnValue(['existing.md'])
const app = createMockApp()
initPagePaths(app)
updatePagePaths('new-page.md', 'create')
expect(findFirstPage('new-page', 'any/path.md')).toBe('new-page.md')
})
it('should remove page path on delete', () => {
mockGlobSync.mockReturnValue(['existing.md', 'to-delete.md'])
const app = createMockApp()
initPagePaths(app)
updatePagePaths('to-delete.md', 'delete')
expect(findFirstPage('to-delete', 'any/path.md')).toBeUndefined()
expect(findFirstPage('existing', 'any/path.md')).toBe('existing.md')
})
it('should not add empty filepath', () => {
mockGlobSync.mockReturnValue(['existing.md'])
const app = createMockApp()
initPagePaths(app)
const beforeUpdate = findFirstPage('existing', 'any/path.md')
updatePagePaths('', 'create')
expect(findFirstPage('existing', 'any/path.md')).toBe(beforeUpdate)
})
})
describe('findFirstPage matching logic', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([
'README.md',
'guide.md',
'docs/api.md',
'docs/guide/intro.md',
'docs/guide/advanced.md',
'page.md',
])
const app = createMockApp()
initPagePaths(app)
})
it('should return exact match', () => {
expect(findFirstPage('guide', 'any/path.md')).toBe('guide.md')
expect(findFirstPage('api', 'any/path.md')).toBe('docs/api.md')
})
it('should return path that ends with the filename', () => {
expect(findFirstPage('intro', 'any/path.md')).toBe('docs/guide/intro.md')
})
it('should add .md extension if no extension provided', () => {
expect(findFirstPage('page', 'any/path.md')).toBe('page.md')
})
it('should not add .md if extension already present', () => {
expect(findFirstPage('page.md', 'any/path.md')).toBe('page.md')
})
it('should find page via endsWith matching when given partial path', () => {
// When searching for 'guide/advanced', it should find 'docs/guide/advanced.md'
// because the pagePath ends with 'guide/advanced.md'
expect(findFirstPage('guide/advanced', 'any/path.md')).toBe('docs/guide/advanced.md')
})
it('should return undefined when page not found', () => {
expect(findFirstPage('nonexistent', 'any/path.md')).toBeUndefined()
expect(findFirstPage('does-not-exist', 'any/path.md')).toBeUndefined()
})
})
})

View File

@ -0,0 +1,85 @@
import type { App } from 'vuepress'
import MarkdownIt from 'markdown-it'
import { describe, expect, it, vi } from 'vitest'
import { obsidianPlugin } from '../src/node/obsidian/index.js'
vi.mock('vuepress/utils', async () => {
const actual = await vi.importActual('vuepress/utils')
return {
...actual,
tinyglobby: {
globSync: vi.fn(() => []),
},
}
})
function createMockApp(pages: App['pages'] = []): App {
return {
pages,
options: {
pagePatterns: ['**/*.md'],
},
dir: {
source: () => '/source',
},
} as unknown as App
}
function createMarkdownWithMockRules() {
return MarkdownIt({ html: true }).use((md) => {
md.block.ruler.before('code', 'import_code', () => false)
md.renderer.rules.import_code = () => ''
})
}
describe('obsidianPlugin', () => {
it('should enable all plugins by default', () => {
const md = createMarkdownWithMockRules()
const mockApp = createMockApp([{ path: '/', filePathRelative: 'README.md', title: 'Home' }] as unknown as App['pages'])
obsidianPlugin(mockApp, md, {})
// Wiki link should not work since findFirstPage returns undefined when pagePaths is empty
const wikiResult = md.render('[[Home]]')
expect(wikiResult).not.toContain('<VPLink')
const commentResult = md.render('%%comment%%')
expect(commentResult).not.toContain('comment')
})
it('should allow disabling specific plugins', () => {
const md = createMarkdownWithMockRules()
const mockApp = createMockApp()
obsidianPlugin(mockApp, md, { obsidian: { wikiLink: false } })
const wikiResult = md.render('[[Page]]')
expect(wikiResult).not.toContain('<VPLink')
expect(wikiResult).toContain('[[Page]]')
})
it('should disable all plugins when obsidian is false', () => {
const md = createMarkdownWithMockRules()
const mockApp = createMockApp()
obsidianPlugin(mockApp, md, { obsidian: false })
const result = md.render('![[image.png]]')
expect(result).toContain('![[image.png]]')
})
it('should disable embedLink when explicitly set to false', () => {
const md = createMarkdownWithMockRules()
const mockApp = createMockApp()
obsidianPlugin(mockApp, md, { obsidian: { embedLink: false } })
const result = md.render('![[image.png]]')
expect(result).not.toContain('<img')
})
it('should disable comment when explicitly set to false', () => {
const md = createMarkdownWithMockRules()
const mockApp = createMockApp()
obsidianPlugin(mockApp, md, { obsidian: { comment: false } })
const commentResult = md.render('%%comment%%')
expect(commentResult).toContain('%%comment%%')
})
})

View File

@ -0,0 +1,268 @@
import type { App } from 'vuepress'
import type { MarkdownEnv } from 'vuepress/markdown'
import MarkdownIt from 'markdown-it'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { initPagePaths } from '../src/node/obsidian/findFirstPage.js'
import { wikiLinkPlugin } from '../src/node/obsidian/wikiLink.js'
const mockGlobSync = vi.fn()
vi.mock('vuepress/utils', () => ({
tinyglobby: {
globSync: (...args: unknown[]) => mockGlobSync(...args),
},
path: {
dirname: vi.fn((p: string) => p.split('/').slice(0, -1).join('/') || '.'),
extname: vi.fn((p: string) => {
const i = p.lastIndexOf('.')
return i > 0 ? p.slice(i) : ''
}),
join: vi.fn((...args: string[]) => args.join('/')),
},
}))
vi.mock('@vuepress/helper', () => ({
removeLeadingSlash: vi.fn((p: string) => p.replace(/^\//, '')),
}))
function createMockApp(pagePatterns = ['**/*.md']): App {
return {
pages: [],
options: {
pagePatterns,
},
dir: {
source: () => '/source',
},
} as unknown as App
}
function createMockEnv(filePathRelative = 'test.md'): MarkdownEnv {
return {
filePathRelative,
base: '/',
links: [],
}
}
describe('wikiLinkPlugin', () => {
beforeEach(() => {
mockGlobSync.mockReset()
})
// ==================== External Links ====================
describe('external links', () => {
it('should render external http link', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[https://example.com]]')
expect(result).toContain('<a')
expect(result).toContain('href="https://example.com"')
expect(result).toContain('target="_blank"')
})
it('should render external link with alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[https://example.com|Example Site]]')
expect(result).toContain('>Example Site<')
expect(result).toContain('href="https://example.com"')
})
it('should render external link with heading and alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[https://example.com/page#section|Go to Section]]')
expect(result).toContain('>Go to Section<')
expect(result).toContain('href="https://example.com/page#section"')
})
it('should render external link with heading but no alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[https://example.com/page#section]]')
expect(result).toContain('href="https://example.com/page#section"')
expect(result).toContain('https://example.com/page &gt; section</a>')
})
})
// ==================== Internal Hash Links ====================
describe('internal hash links', () => {
it('should render internal hash link for empty filename', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv('docs/page.md')
const result = md.render('[[#anchor]]', env)
expect(result).toContain('<VPLink')
expect(result).toContain('href="#anchor"')
})
it('should render internal hash link with alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv('docs/page.md')
const result = md.render('[[#anchor|Back to Top]]', env)
expect(result).toContain('>Back to Top<')
expect(result).toContain('href="#anchor"')
})
it('should render internal hash link with titles but no alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv('docs/page.md')
const result = md.render('[[#anchor1#anchor2]]', env)
expect(result).toContain('href="#anchor2"')
expect(result).toContain('&gt; anchor1 &gt; anchor2</template>')
})
})
// ==================== Internal Page Resolution ====================
describe('internal page resolution', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue([
'README.md',
'guide.md',
'docs/api.md',
'docs/guide/intro.md',
])
const app = createMockApp()
initPagePaths(app)
})
it('should render internal wiki link with VPLink', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[guide]]', env)
expect(result).toContain('<VPLink')
expect(result).toContain('href="/guide.md"')
})
it('should render wiki link with heading anchor', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[guide#Getting Started]]', env)
expect(result).toContain('<VPLink')
expect(result).toContain('href="/guide.md#getting-started"')
})
it('should render wiki link with alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[guide|Guide Page]]', env)
expect(result).toContain('Guide Page')
expect(result).toContain('<VPLink')
expect(result).toContain('href="/guide.md"')
})
it('should render wiki link with heading and alias', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[guide#Getting Started|Getting Started]]', env)
expect(result).toContain('Getting Started')
expect(result).toContain('href="/guide.md#getting-started"')
})
it('should track links in env', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
md.render('[[guide]]', env)
expect(env.links).toBeDefined()
expect(env.links!.length).toBeGreaterThan(0)
})
it('should find page by partial filename', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
// Should match docs/guide/intro.md when searching for "intro"
const result = md.render('[[intro]]', env)
expect(result).toContain('<VPLink')
expect(result).toContain('href="/docs/guide/intro.md"')
})
})
// ==================== Page Not Found ====================
describe('when page does not exist', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue(['existing.md'])
const app = createMockApp()
initPagePaths(app)
})
it('should render as external anchor link', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[nonexistent]]')
expect(result).toContain('<a')
expect(result).toContain('href="/nonexistent"')
expect(result).toContain('target="_blank"')
})
it('should render with heading anchor for nonexistent page', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[nonexistent#section]]')
expect(result).toContain('href="/nonexistent#section"')
})
})
// ==================== Edge Cases ====================
describe('edge cases', () => {
beforeEach(() => {
mockGlobSync.mockReturnValue(['docs/page.md'])
const app = createMockApp()
initPagePaths(app)
})
it('should not parse wiki link without closing bracket', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[Page')
expect(result).toContain('[[Page')
})
it('should not parse empty wiki link', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const result = md.render('[[]]')
expect(result).toContain('[[]]')
})
it('should handle wiki link with extra whitespace', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[ page ]]', env)
expect(result).toContain('<VPLink')
})
it('should handle wiki link with multiple hashes', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
const result = md.render('[[page#h1#h2#h3]]', env)
expect(result).toContain('href="/docs/page.md#h3"')
})
it('should handle wiki link with pipe in filename', () => {
const md = new MarkdownIt({ html: true }).use(wikiLinkPlugin)
const env = createMockEnv()
// Filename with pipe character should be treated as alias separator
const result = md.render('[[page|alias]]', env)
expect(result).toContain('>alias<')
})
})
})

View File

@ -1,7 +1,7 @@
{
"name": "vuepress-plugin-md-power",
"type": "module",
"version": "1.0.0-rc.192",
"version": "1.0.0-rc.196",
"description": "The Plugin for VuePress 2 - markdown power",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
@ -36,8 +36,8 @@
"clean": "rimraf --glob ./lib",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"copy:watch": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib -w",
"tsdown": "tsdown",
"tsdown:watch": "tsdown --watch -- -c"
"tsdown": "tsdown --config-loader unrun",
"tsdown:watch": "tsdown --config-loader unrun --watch -- -c"
},
"peerDependencies": {
"artplayer": "catalog:peer",
@ -97,6 +97,7 @@
"@vuepress/helper": "catalog:vuepress",
"@vueuse/core": "catalog:prod",
"chokidar": "catalog:prod",
"gray-matter": "catalog:prod",
"image-size": "catalog:prod",
"local-pkg": "catalog:prod",
"lru-cache": "catalog:prod",

View File

@ -5,18 +5,63 @@ import { http } from '../utils/http.js'
import { sleep } from '../utils/sleep.js'
import { rustExecute } from './rustRepl.js'
/**
* CSS selectors for nodes to ignore when extracting code.
*
* CSS
*/
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
/**
* Regular expression for matching language class.
*
*
*/
const RE_LANGUAGE = /language-(\w+)/
/**
* API endpoints for code execution backends.
*
* API
*/
const api = {
go: 'https://api.pengzhanbo.cn/repl/golang/run',
kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run',
}
/**
* Pyodide instance for Python execution.
*
* Python Pyodide
*/
let pyodide: PyodideInterface | null = null
/**
* Supported languages for code execution.
*
*
*/
type Lang = 'kotlin' | 'go' | 'rust' | 'python'
/**
* Function type for code execution.
*
*
*/
type ExecuteFn = (code: string) => Promise<any>
/**
* Map of language to execution function.
*
*
*/
type ExecuteMap = Record<Lang, ExecuteFn>
/**
* Language alias mapping.
*
*
*/
const langAlias: Record<string, string> = {
kt: 'kotlin',
kotlin: 'kotlin',
@ -27,12 +72,33 @@ const langAlias: Record<string, string> = {
python: 'python',
}
/**
* List of supported languages.
*
*
*/
const supportLang: Lang[] = ['kotlin', 'go', 'rust', 'python']
/**
* Resolve language name from alias.
*
*
*
* @param lang - Language or alias /
* @returns Resolved language name /
*/
function resolveLang(lang?: string) {
return lang ? langAlias[lang] || lang : ''
}
/**
* Resolve code content from HTML element, ignoring specified nodes.
*
* HTML
*
* @param el - HTML element / HTML
* @returns Code content /
*/
export function resolveCode(el: HTMLElement): string {
const clone = el.cloneNode(true) as HTMLElement
clone
@ -42,6 +108,14 @@ export function resolveCode(el: HTMLElement): string {
return clone.textContent || ''
}
/**
* Resolve code information from HTML element.
*
* HTML
*
* @param el - HTML element / HTML
* @returns Object with language and code /
*/
export function resolveCodeInfo(el: HTMLDivElement): {
lang: Lang
code: string
@ -57,19 +131,55 @@ export function resolveCodeInfo(el: HTMLDivElement): {
return { lang: resolveLang(lang) as Lang, code }
}
/**
* Result interface for useCodeRepl composable.
*
* useCodeRepl
*/
interface UseCodeReplResult {
/** Current language / 当前语言 */
lang: Ref<Lang | undefined>
/** Whether the code is loaded / 代码是否已加载 */
loaded: Ref<boolean>
/** Whether this is the first run / 是否为首次运行 */
firstRun: Ref<boolean>
/** Whether execution is finished / 执行是否完成 */
finished: Ref<boolean>
/** Standard output lines / 标准输出行 */
stdout: Ref<string[]>
/** Standard error lines / 标准错误行 */
stderr: Ref<string[]>
/** Error message / 错误信息 */
error: Ref<string>
/** Backend version / 后端版本 */
backendVersion: Ref<string>
/** Clean run state / 清理运行状态 */
onCleanRun: () => void
/** Run code execution / 运行代码执行 */
onRunCode: () => Promise<void>
}
/**
* Composable for code REPL functionality.
*
* REPL
*
* This composable provides functionality to execute code in various languages
* (Kotlin, Go, Rust, Python) and manage the execution state.
*
* KotlinGoRustPython
*
* @param el - Reference to the code element /
* @returns REPL state and methods / REPL
*
* @example
* ```vue
* <script setup>
* const codeEl = ref(null)
* const { onRunCode, stdout, stderr, loaded } = useCodeRepl(codeEl)
* </script>
* ```
*/
export function useCodeRepl(el: Ref<HTMLDivElement | null>): UseCodeReplResult {
const lang = ref<Lang>()
const loaded = ref(true)
@ -227,36 +337,74 @@ export function useCodeRepl(el: Ref<HTMLDivElement | null>): UseCodeReplResult {
}
}
/**
* Request interface for Golang execution API.
*
* Golang API
*/
interface GolangRequest {
/** Code to execute / 要执行的代码 */
code: string
/** Go version / Go 版本 */
version?: '' | 'goprev' | 'gotip'
}
/**
* Response interface for Golang execution API.
*
* Golang API
*/
interface GolangResponse {
/** Execution events / 执行事件 */
events?: {
/** Event message / 事件消息 */
message: ''
/** Event kind / 事件类型 */
kind: 'stdout' | 'stderr'
/** Event delay / 事件延迟 */
delay: number
}[]
/** Error message / 错误信息 */
error?: string
/** Go version / Go 版本 */
version: string
}
/**
* Request interface for Kotlin execution API.
*
* Kotlin API
*/
interface KotlinRequest {
/** Command line arguments / 命令行参数 */
args?: string
/** Files to compile / 要编译的文件 */
files: {
/** File name / 文件名 */
name: string
/** Public ID / 公共 ID */
publicId: string
/** File content / 文件内容 */
text: string
}[]
}
/**
* Response interface for Kotlin execution API.
*
* Kotlin API
*/
interface KotlinResponse {
/** Execution output / 执行输出 */
text: string
/** Kotlin version / Kotlin 版本 */
version: string
/** Compilation errors / 编译错误 */
errors: {
[filename: string]: {
/** Error message / 错误信息 */
message: string
/** Error severity / 错误严重程度 */
severity: 'ERROR' | 'WARNING'
}[]
}

View File

@ -1,11 +1,48 @@
import type { ComputedRef } from 'vue'
/**
* Composable for decrypting encrypted content.
*
*
*
* This composable provides a decrypt function that uses the Web Crypto API
* to decrypt content encrypted with AES-CBC algorithm.
*
* 使 Web Crypto API 使 AES-CBC
*
* @param config - Configuration containing salt and IV / IV
* @returns Object with decrypt function /
*
* @example
* ```ts
* const config = computed(() => ({ salt: [...], iv: [...] }))
* const { decrypt } = useDecrypt(config)
* const content = await decrypt('password', 'encrypted-content')
* ```
*/
export function useDecrypt(
config: ComputedRef<{ salt: number[], iv: number[] }>,
) {
/**
* Convert number array to Uint8Array.
*
* Uint8Array
*
* @param raw - Number array /
* @returns Uint8Array / Uint8Array
*/
const toUnit8Array = (raw: number[]) => Uint8Array.from(raw)
return {
/**
* Decrypt encrypted content using password.
*
* 使
*
* @param password - Decryption password /
* @param text - Encrypted content /
* @returns Decrypted content or undefined / undefined
*/
decrypt: async (password: string, text: string) => {
if (!password)
return
@ -29,6 +66,14 @@ export function useDecrypt(
}
}
/**
* Get key material from password using PBKDF2.
*
* 使 PBKDF2
*
* @param password - Password string /
* @returns CryptoKey for key derivation / CryptoKey
*/
function getKeyMaterial(password: string) {
const enc = new TextEncoder()
return window.crypto.subtle.importKey(
@ -41,7 +86,13 @@ function getKeyMaterial(password: string) {
}
/**
* crypto
* Derive encryption key from key material using PBKDF2.
*
* 使 PBKDF2
*
* @param keyMaterial - Key material from password /
* @param salt - Salt for key derivation /
* @returns Derived CryptoKey for AES-CBC / AES-CBC CryptoKey
*/
function getCryptoDeriveKey(keyMaterial: CryptoKey, salt: BufferSource) {
return window.crypto.subtle.deriveKey(

View File

@ -1,12 +1,61 @@
import { onContentUpdated } from 'vuepress/client'
/**
* Attribute name for mark mode.
*
*
*/
const MARK_MODE_ATTR = 'data-mark-mode'
/**
* Lazy mode constant.
*
*
*/
const MARK_MODE_LAZY = 'lazy'
/**
* CSS class for visible marks.
*
* CSS
*/
const MARK_VISIBLE_CLASS = 'vp-mark-visible'
/**
* Attribute name for mark boundary.
*
*
*/
const MARK_BOUND_ATTR = 'data-vp-mark-bound'
/**
* CSS selector for mark elements.
*
* CSS
*/
const MARK_SELECTOR = 'mark'
/**
* CSS selector for bounded mark elements.
*
* CSS
*/
const BOUND_SELECTOR = `${MARK_SELECTOR}[${MARK_BOUND_ATTR}="1"]`
/**
* Setup mark highlight animation for lazy mode.
*
*
*
* When mode is 'lazy', marks will animate into view using IntersectionObserver.
* When mode is 'eager', marks are immediately visible without animation.
*
* 'lazy' 使 IntersectionObserver
* 'eager'
*
* @param mode - Animation mode: 'lazy' or 'eager' / 'lazy' 'eager'
*
* @example
* ```ts
* // In client config setup
* setupMarkHighlight('lazy')
* ```
*/
export function setupMarkHighlight(mode: 'lazy' | 'eager'): void {
if (typeof window === 'undefined' || __VUEPRESS_SSR__)
return

View File

@ -17,6 +17,14 @@ import { withBase } from 'vuepress/client'
import { ensureEndingSlash, isLinkHttp } from 'vuepress/shared'
import { pluginOptions } from '../options.js'
/**
* Build query string from PDF options.
*
* PDF
*
* @param options - PDF token metadata / PDF
* @returns Query string /
*/
function queryStringify(options: PDFTokenMeta): string {
const { page, noToolbar, zoom } = options
const params = [
@ -32,6 +40,16 @@ function queryStringify(options: PDFTokenMeta): string {
return queryString
}
/**
* Render PDF viewer in the specified element.
*
* PDF
*
* @param el - Container element /
* @param url - PDF URL / PDF URL
* @param embedType - Embed type: 'pdfjs', 'iframe', or 'embed' /
* @param options - PDF token metadata / PDF
*/
export function renderPDF(
el: HTMLElement,
url: string,
@ -72,6 +90,20 @@ export function renderPDF(
el.appendChild(pdf)
}
/**
* Composable for PDF viewer functionality.
*
* PDF
*
* This function detects browser capabilities and chooses the appropriate
* embedding method for PDF display (PDF.js, iframe, or embed).
*
* PDFPDF.jsiframe embed
*
* @param el - Container element /
* @param url - PDF URL / PDF URL
* @param options - PDF token metadata / PDF
*/
export function usePDF(
el: HTMLElement,
url: string,
@ -86,10 +118,10 @@ export function usePDF(
const isModernBrowser = typeof window.Promise === 'function'
// Quick test for mobile devices.
const isMobileDevice = isiPad(userAgent) || isMobile(userAgent)
const isMobileDevice = isiPad() || isMobile()
// Safari desktop requires special handling
const isSafariDesktop = !isMobileDevice && isSafari(userAgent)
const isSafariDesktop = !isMobileDevice && isSafari()
const isFirefoxWithPDFJS
= !isMobileDevice

View File

@ -8,8 +8,18 @@ declare const __MD_POWER_HLSJS_INSTALLED__: boolean
declare const __MD_POWER_MPEGTSJS_INSTALLED__: boolean
declare const __MD_POWER_ENCRYPT_LOCALES__: LocaleConfig<EncryptSnippetLocale>
/**
* Plugin options injected at build time.
*
*
*/
export const pluginOptions: MarkdownPowerPluginOptions = __MD_POWER_INJECT_OPTIONS__
/**
* Package installation status for video streaming libraries.
*
*
*/
export const installed: {
dashjs: boolean
hlsjs: boolean
@ -20,6 +30,11 @@ export const installed: {
mpegtsjs: __MD_POWER_MPEGTSJS_INSTALLED__,
}
/**
* Supported video types for ArtPlayer.
*
* ArtPlayer
*/
export const ART_PLAYER_SUPPORTED_VIDEO_TYPES: string[] = ['mp4', 'mp3', 'webm', 'ogg']
if (installed.dashjs) {
@ -34,12 +49,27 @@ if (installed.mpegtsjs) {
ART_PLAYER_SUPPORTED_VIDEO_TYPES.push('ts', 'flv')
}
/**
* Injection key for timeline component communication.
*
* 线
*/
export const INJECT_TIMELINE_KEY: symbol = Symbol(
__VUEPRESS_DEV__ ? 'timeline' : '',
)
/**
* Injection key for collapse component communication.
*
*
*/
export const INJECT_COLLAPSE_KEY: symbol = Symbol(
__VUEPRESS_DEV__ ? 'collapse' : '',
)
/**
* Encrypt snippet locale data.
*
*
*/
export const ENCRYPT_LOCALES = __MD_POWER_ENCRYPT_LOCALES__

View File

@ -119,9 +119,9 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
// Extract filename and comment
if (filename === '' && !focus) {
const spaceIndex = info.indexOf(' ')
filename = info.slice(0, spaceIndex === -1 ? info.length : spaceIndex)
info = spaceIndex === -1 ? '' : info.slice(spaceIndex)
const sharpIndex = info.indexOf('#')
filename = info.slice(0, sharpIndex === -1 ? info.length : sharpIndex).trim()
info = sharpIndex === -1 ? '' : info.slice(sharpIndex)
}
comment = info.trim()

View File

@ -88,6 +88,7 @@ export function tablePlugin(md: Markdown, options: TableContainerOptions = {}):
let isTable = false
let colIndex = 0
let rowIndex = 0
let skipCells = 0
for (const token of tableTokens) {
if (token.type === 'table_open')
isTable = true
@ -100,13 +101,39 @@ export function tablePlugin(md: Markdown, options: TableContainerOptions = {}):
if (token.type === 'tr_open') {
rowIndex++
colIndex = 0
// 当 th 设置了 colspan 时,需要跳过空单元格
skipCells = 0
}
// cell (rowIndex, colIndex)
if (token.type === 'th_open' || token.type === 'td_open') {
if (skipCells > 0) {
// Skip this empty cell`th` element only
if (token.type === 'th_open')
token.hidden = true
skipCells--
continue
}
colIndex++
const classes = cells[rowIndex]?.[colIndex] || rows[rowIndex] || cols[colIndex]
if (classes)
token.attrJoin('class', classes)
// Check for colspan attribute
const colspanIndex = token.attrIndex('colspan')
if (colspanIndex >= 0) {
const colspanValue = Number.parseInt(token.attrs![colspanIndex][1])
if (!Number.isNaN(colspanValue) && colspanValue > 1) {
skipCells = colspanValue - 1
}
}
}
// Skip the content and close tokens for skipped cells
if (skipCells > 0 && (token.type === 'text' || token.type === 'th_close' || token.type === 'td_close')) {
if (token.type === 'th_close')
// Skip this empty cell`th` element only
token.hidden = true
continue
}
}

View File

@ -3,6 +3,19 @@ import type { Markdown } from 'vuepress/markdown'
import { demoContainer, demoEmbed } from './demo.js'
import { createDemoRender } from './watcher.js'
/**
* Register demo plugin for markdown-it.
*
* markdown-it demo
*
* This plugin enables demo syntax in markdown files, allowing users to
* create interactive code demonstrations with live preview.
*
* markdown demo
*
* @param app - VuePress app instance / VuePress
* @param md - Markdown-it instance / Markdown-it
*/
export function demoPlugin(app: App, md: Markdown): void {
createDemoRender()
demoEmbed(app, md)

View File

@ -1,6 +1,6 @@
import type { DemoFile, MarkdownDemoEnv } from '../../../shared/demo.js'
const SCRIPT_RE = /<script.*?>/
const SCRIPT_RE = /<script\b[^>]*>/
export function insertSetupScript({ export: name, path }: DemoFile, env: MarkdownDemoEnv): void {
const imports = `import ${name ? `${name} from ` : ''}'${path}';`

View File

@ -18,7 +18,7 @@ const installed = {
mpegtsjs: isPackageExists('mpegts.js'),
}
const SUPPORTED_VIDEO_TYPES = ['mp4', 'mp3', 'webm', 'ogg', 'mpd', 'dash', 'm3u8', 'hls', 'ts', 'flv']
export const SUPPORTED_VIDEO_TYPES = ['mp4', 'mp3', 'webm', 'ogg', 'mpd', 'dash', 'm3u8', 'hls', 'ts', 'flv', 'mkv', 'mov', 'ogv']
export const artPlayerPlugin: PluginWithOptions<never> = (md) => {
createEmbedRuleBlock<ArtPlayerTokenMeta>(md, {
@ -51,7 +51,7 @@ export const artPlayerPlugin: PluginWithOptions<never> = (md) => {
})
}
function checkSupportType(type?: string) {
export function checkSupportType(type?: string) {
if (!type)
return

View File

@ -559,11 +559,11 @@ export const definitions: Definitions = {
'rolldown.config.prod.cjs': 'vscode-icons:file-type-light-rolldown',
'rolldown.config.prod.mjs': 'vscode-icons:file-type-light-rolldown',
'rolldown.config.prod.ts': 'vscode-icons:file-type-light-rolldown',
'tsdown.config.js': 'vscode-icons:file-type-light-tsdown',
'tsdown.config.cjs': 'vscode-icons:file-type-light-tsdown',
'tsdown.config.mjs': 'vscode-icons:file-type-light-tsdown',
'tsdown.config.ts': 'vscode-icons:file-type-light-tsdown',
'tsdown.config.json': 'vscode-icons:file-type-light-tsdown',
'tsdown.config.js': 'vscode-icons:file-type-tsdown',
'tsdown.config.cjs': 'vscode-icons:file-type-tsdown',
'tsdown.config.mjs': 'vscode-icons:file-type-tsdown',
'tsdown.config.ts': 'vscode-icons:file-type-tsdown',
'tsdown.config.json': 'vscode-icons:file-type-tsdown',
'.oxlintignore': 'vscode-icons:file-type-oxc',
'.oxlintrc.json': 'vscode-icons:file-type-oxc',

View File

@ -9,6 +9,17 @@ import { ruLocale } from './ru'
import { zhLocale } from './zh'
import { zhTWLocale } from './zh-tw'
/**
* Default locale options for the plugin.
*
*
*
* This constant defines the default locale configurations for all supported languages.
* Each locale entry maps language codes to their respective locale data.
*
*
*
*/
export const LOCALE_OPTIONS: DefaultLocaleInfo<MDPowerLocaleData> = [
[['en', 'en-US'], enLocale],
[['zh', 'zh-CN', 'zh-Hans', 'zh-Hant'], zhLocale],

Some files were not shown because too many files have changed in this diff Show More