mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-05-01 12:38:12 +08:00
Compare commits
64 Commits
v1.0.0-rc.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2d2b3dc1 | ||
|
|
475d7f2db1 | ||
|
|
5d5b5399ff | ||
|
|
26c588ab23 | ||
|
|
a9e7ebd6ba | ||
|
|
32fb93bf35 | ||
|
|
4614041bbf | ||
|
|
a9ddb04acd | ||
|
|
3265be84a9 | ||
|
|
2bfdec82d7 | ||
|
|
ac63654151 | ||
|
|
6ed5a5c552 | ||
|
|
d69e0b9765 | ||
|
|
02038f2df0 | ||
|
|
e5126663ef | ||
|
|
402f259086 | ||
|
|
58ea2fc8cb | ||
|
|
6ebb1bda6e | ||
|
|
68f39695c4 | ||
|
|
76787f6530 | ||
|
|
e2b47da532 | ||
|
|
035d521e96 | ||
|
|
bfd0c8409c | ||
|
|
e11c7a8fcd | ||
|
|
1329051536 | ||
|
|
0677f6749e | ||
|
|
28963eb419 | ||
|
|
cfc89adab8 | ||
|
|
e0ba59a6f9 | ||
|
|
352874b29a | ||
|
|
c824ad85f4 | ||
|
|
db2eda82f3 | ||
|
|
e9fe35bc4f | ||
|
|
709ade741c | ||
|
|
d8b79e89e8 | ||
|
|
dbc6f0be0f | ||
|
|
9fe294b9dd | ||
|
|
ecf100cfc6 | ||
|
|
b7ee45642e | ||
|
|
54c05c8cea | ||
|
|
86cb872ce6 | ||
|
|
a6cb3820b1 | ||
|
|
184d1aee76 | ||
|
|
cbc5c55891 | ||
|
|
4f40f8441d | ||
|
|
fe0d4bbc92 | ||
|
|
39a76a35d7 | ||
|
|
a01bc13c66 | ||
|
|
1b213d4c28 | ||
|
|
aede6f5d87 | ||
|
|
7febfbf237 | ||
|
|
7ce4e40521 | ||
|
|
12c4f5b39e | ||
|
|
aa54090b5d | ||
|
|
192b260d2b | ||
|
|
75df783295 | ||
|
|
97a5ba20c3 | ||
|
|
896c7e22df | ||
|
|
77856e36c5 | ||
|
|
552f0f5c32 | ||
|
|
7751e4c798 | ||
|
|
17646708b1 | ||
|
|
f14d663bb5 | ||
|
|
50fa747ec1 |
3
.github/workflows/docs-deploy.yaml
vendored
3
.github/workflows/docs-deploy.yaml
vendored
@ -13,6 +13,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@ -6,6 +6,9 @@ on:
|
|||||||
- v*
|
- v*
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/lint.yaml
vendored
3
.github/workflows/lint.yaml
vendored
@ -8,6 +8,9 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
7
.github/workflows/release.yaml
vendored
7
.github/workflows/release.yaml
vendored
@ -5,6 +5,10 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yaml
|
uses: ./.github/workflows/lint.yaml
|
||||||
@ -16,9 +20,6 @@ jobs:
|
|||||||
if: github.repository == 'pengzhanbo/vuepress-theme-plume'
|
if: github.repository == 'pengzhanbo/vuepress-theme-plume'
|
||||||
needs: [test, lint]
|
needs: [test, lint]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.github/workflows/test.yaml
vendored
3
.github/workflows/test.yaml
vendored
@ -8,6 +8,9 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit-test:
|
unit-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,3 +15,6 @@ dist/
|
|||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
!.claude/skills/
|
||||||
|
|||||||
542
CHANGELOG.md
542
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal 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`
|
||||||
@ -19,7 +19,7 @@ In the `plugins` directory:
|
|||||||
|
|
||||||
Development requirements:
|
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+
|
- [pnpm](https://pnpm.io/zh/) version 9+
|
||||||
|
|
||||||
Clone the repository and install dependencies:
|
Clone the repository and install dependencies:
|
||||||
|
|||||||
@ -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+
|
- [pnpm](https://pnpm.io/zh/) version 9+
|
||||||
|
|
||||||
克隆代码仓库,并安装依赖:
|
克隆代码仓库,并安装依赖:
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------------- | ------------------ |
|
| ---------------- | ------------------ |
|
||||||
| >= 1.0.0-rc.170 | :white_check_mark: |
|
| >= 1.0.0-rc.190 | :white_check_mark: |
|
||||||
| < 1.0.0-rc.170 | :x: |
|
| < 1.0.0-rc.190 | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "create-vuepress-theme-plume",
|
"name": "create-vuepress-theme-plume",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0-rc.192",
|
"version": "1.0.0-rc.198",
|
||||||
"description": "The cli for create vuepress-theme-plume's project",
|
"description": "The cli for create vuepress-theme-plume's project",
|
||||||
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -27,7 +27,7 @@
|
|||||||
"templates"
|
"templates"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown"
|
"build": "tsdown --config-loader unrun"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "catalog:prod",
|
"@clack/prompts": "catalog:prod",
|
||||||
@ -40,8 +40,8 @@
|
|||||||
"sort-package-json": "catalog:prod"
|
"sort-package-json": "catalog:prod"
|
||||||
},
|
},
|
||||||
"plume-deps": {
|
"plume-deps": {
|
||||||
"vuepress": "2.0.0-rc.26",
|
"vuepress": "2.0.0-rc.28",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.32",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 cac from 'cac'
|
||||||
import { version } from '../package.json'
|
import { version } from '../package.json'
|
||||||
import { Mode } from './constants.js'
|
import { Mode } from './constants.js'
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import type { Locale } from '../types.js'
|
import type { Locale } from '../types.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* English locale configuration for CLI prompts and messages.
|
||||||
|
*
|
||||||
|
* CLI 提示和消息的英语本地化配置。
|
||||||
|
*/
|
||||||
export const en: Locale = {
|
export const en: Locale = {
|
||||||
'question.root': 'Where would you want to initialize VuePress?',
|
'question.root': 'Where would you want to initialize VuePress?',
|
||||||
'question.site.name': 'Site Name:',
|
'question.site.name': 'Site Name:',
|
||||||
|
|||||||
@ -2,6 +2,15 @@ import type { Langs, Locale } from '../types.js'
|
|||||||
import { en } from './en.js'
|
import { en } from './en.js'
|
||||||
import { zh } from './zh.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> = {
|
export const locales: Record<Langs, Locale> = {
|
||||||
'zh-CN': zh,
|
'zh-CN': zh,
|
||||||
'en-US': en,
|
'en-US': en,
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import type { Locale } from '../types.js'
|
import type { Locale } from '../types.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chinese (Simplified) locale configuration for CLI prompts and messages.
|
||||||
|
*
|
||||||
|
* CLI 提示和消息的简体中文本地化配置。
|
||||||
|
*/
|
||||||
export const zh: Locale = {
|
export const zh: Locale = {
|
||||||
'question.root': '您想在哪里初始化 VuePress?',
|
'question.root': '您想在哪里初始化 VuePress?',
|
||||||
'question.site.name': '站点名称:',
|
'question.site.name': '站点名称:',
|
||||||
|
|||||||
@ -5,6 +5,14 @@ import _sortPackageJson from 'sort-package-json'
|
|||||||
import { Mode } from './constants.js'
|
import { Mode } from './constants.js'
|
||||||
import { readJsonFile, resolve } from './utils/index.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>) {
|
function sortPackageJson(json: Record<any, any>) {
|
||||||
return _sortPackageJson(json, {
|
return _sortPackageJson(json, {
|
||||||
sortOrder: ['name', 'type', 'version', 'private', 'description', 'packageManager', 'author', 'license', 'scripts', 'devDependencies', 'dependencies', 'pnpm'],
|
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() {
|
async function getUserInfo() {
|
||||||
const { output: username } = await spawn('git', ['config', '--global', 'user.name'])
|
const { output: username } = await spawn('git', ['config', '--global', 'user.name'])
|
||||||
const { output: email } = await spawn('git', ['config', '--global', 'user.email'])
|
const { output: email } = await spawn('git', ['config', '--global', 'user.email'])
|
||||||
return { username, 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) {
|
async function getPackageManagerVersion(pkg: string) {
|
||||||
const { output } = await spawn(pkg, ['--version'])
|
const { output } = await spawn(pkg, ['--version'])
|
||||||
return output
|
return output
|
||||||
|
|||||||
@ -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 {
|
function resolveData(result: PromptResult, mode: Mode): ResolvedData {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
|||||||
@ -1,6 +1,19 @@
|
|||||||
import type { PackageManager } from '../types.js'
|
import type { PackageManager } from '../types.js'
|
||||||
import process from 'node:process'
|
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 {
|
export function getPackageManager(): PackageManager {
|
||||||
const name = process.env?.npm_config_user_agent || 'npm'
|
const name = process.env?.npm_config_user_agent || 'npm'
|
||||||
return name.split('/')[0] as PackageManager
|
return name.split('/')[0] as PackageManager
|
||||||
|
|||||||
@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
|
|||||||
'chat',
|
'chat',
|
||||||
'include',
|
'include',
|
||||||
'env',
|
'env',
|
||||||
|
'obsidian',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
|
|||||||
'chat',
|
'chat',
|
||||||
'include',
|
'include',
|
||||||
'env',
|
'env',
|
||||||
|
'obsidian',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export const enNavbar: ThemeNavItem[] = defineNavbarConfig([
|
|||||||
{
|
{
|
||||||
text: `${version}`,
|
text: `${version}`,
|
||||||
icon: 'codicon:versions',
|
icon: 'codicon:versions',
|
||||||
badge: '新',
|
badge: 'New',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Changelog', link: '/en/changelog/' },
|
{ text: 'Changelog', link: '/en/changelog/' },
|
||||||
{ text: 'Contributing', link: '/en/contributing/' },
|
{ text: 'Contributing', link: '/en/contributing/' },
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
BIN
docs/.vuepress/public/images/demos/plume.webp
Normal file
BIN
docs/.vuepress/public/images/demos/plume.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@ -58,6 +58,7 @@ export const theme: Theme = plumeTheme({
|
|||||||
jsfiddle: true,
|
jsfiddle: true,
|
||||||
demo: true,
|
demo: true,
|
||||||
encrypt: true,
|
encrypt: true,
|
||||||
|
obsidian: true,
|
||||||
npmTo: ['pnpm', 'yarn', 'npm'],
|
npmTo: ['pnpm', 'yarn', 'npm'],
|
||||||
repl: {
|
repl: {
|
||||||
go: true,
|
go: true,
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export default defineUserConfig({
|
|||||||
|
|
||||||
主题提供了 `plume.config.ts` 配置文件,==对该文件的修改支持热更新,无需重启服务=={.tip} ::twemoji:confetti-ball::。
|
主题提供了 `plume.config.ts` 配置文件,==对该文件的修改支持热更新,无需重启服务=={.tip} ::twemoji:confetti-ball::。
|
||||||
|
|
||||||
你可以在其中配置支持热更新的字段,如 `navbar`、`profile` 等。
|
您可以在其中配置支持热更新的字段,如 `navbar`、`profile` 等。
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
这些字段仍可在 VuePress 配置文件的 `theme` 中配置,但主题配置文件的设置最终会合并到主配置中。
|
这些字段仍可在 VuePress 配置文件的 `theme` 中配置,但主题配置文件的设置最终会合并到主配置中。
|
||||||
|
|||||||
@ -6,7 +6,7 @@ permalink: /config/locales/
|
|||||||
|
|
||||||
这些选项用于配置与语言相关的文本。
|
这些选项用于配置与语言相关的文本。
|
||||||
|
|
||||||
如果你的站点是以非内置语言支持以外的其他语言提供服务的,你应该为每个语言设置这些选项来提供翻译。
|
如果您的站点是以非内置语言支持以外的其他语言提供服务的,您应该为每个语言设置这些选项来提供翻译。
|
||||||
|
|
||||||
## 内置语言支持
|
## 内置语言支持
|
||||||
|
|
||||||
|
|||||||
@ -149,7 +149,7 @@ export default defineUserConfig({
|
|||||||
- **默认值**: `{ provider: 'iconify' }`
|
- **默认值**: `{ provider: 'iconify' }`
|
||||||
- **详情**: 图标配置
|
- **详情**: 图标配置
|
||||||
|
|
||||||
[查看 **icon** 使用说明](../../theme/guide/features/icon.md){.read-more}
|
[查看 **icon** 使用说明](../guide/features/icon.md){.read-more}
|
||||||
|
|
||||||
### plot
|
### plot
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ permalink: /config/navigation/
|
|||||||
|
|
||||||
主题默认会自动生成最简单的导航栏配置,仅包括 **首页** 和 **文章列表页** 。
|
主题默认会自动生成最简单的导航栏配置,仅包括 **首页** 和 **文章列表页** 。
|
||||||
|
|
||||||
你也可以自己配置导航栏,覆盖默认的的导航栏配置。
|
您也可以自己配置导航栏,覆盖默认的导航栏配置。
|
||||||
|
|
||||||
默认配置如下:
|
默认配置如下:
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@ interface SearchBoxLocale {
|
|||||||
### 启用
|
### 启用
|
||||||
|
|
||||||
```ts title=".vuepress/config.ts" twoslash
|
```ts title=".vuepress/config.ts" twoslash
|
||||||
|
// @errors: 2353
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
import { plumeTheme } from 'vuepress-theme-plume'
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ permalink: /config/watermark/
|
|||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
```ts title=".vuepress/config.ts" twoslash
|
```ts title=".vuepress/config.ts" twoslash
|
||||||
|
// @errors: 7006
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
import { plumeTheme } from 'vuepress-theme-plume'
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,7 @@ permalink: /config/theme/
|
|||||||
::: warning 该字段不支持在 [主题配置文件 `plume.config.js`](./intro.md#主题配置文件) 中进行配置。
|
::: warning 该字段不支持在 [主题配置文件 `plume.config.js`](./intro.md#主题配置文件) 中进行配置。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
无以上声明的字段,你可以在 `.vuepress/config.ts` 或者 `.vuepress/plume.config.ts` 的任意一个文件中
|
无以上声明的字段,您可以在 `.vuepress/config.ts` 或者 `.vuepress/plume.config.ts` 的任意一个文件中进行配置,一般情况下建议在 `.vuepress/plume.config.ts` 中进行配置。
|
||||||
进行配置,一般情况下建议在 `.vuepress/plume.config.ts` 中进行配置。
|
|
||||||
|
|
||||||
::: warning 已经在一个配置文件中进行配置的字段,尽量不要在另一个配置文件中重复配置
|
::: 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)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ docs:
|
|||||||
logo: /plume.png
|
logo: /plume.png
|
||||||
url: https://theme-plume.vuejs.press
|
url: https://theme-plume.vuejs.press
|
||||||
repo: https://github.com/pengzhanbo/vuepress-theme-plume
|
repo: https://github.com/pengzhanbo/vuepress-theme-plume
|
||||||
preview: /images/demos/plume.jpg
|
preview: /images/demos/plume.webp
|
||||||
-
|
-
|
||||||
name: city walk 城市漫步
|
name: city walk 城市漫步
|
||||||
desc: 致力于汇聚全国350多个城市的户外活动地点与文化场馆的开放数据平台。
|
desc: 致力于汇聚全国350多个城市的户外活动地点与文化场馆的开放数据平台。
|
||||||
@ -25,12 +25,6 @@ docs:
|
|||||||
url: https://shenzhen.citywalk.group/
|
url: https://shenzhen.citywalk.group/
|
||||||
repo: https://github.com/sunshang-hl/CityWalk
|
repo: https://github.com/sunshang-hl/CityWalk
|
||||||
preview: https://pub-187e90a3327b41ccb8869558b6b8bbc0.r2.dev/city-shenzhen/2024/12/ed251c4438f722dffd6cb95db86c0d56.jpg
|
preview: https://pub-187e90a3327b41ccb8869558b6b8bbc0.r2.dev/city-shenzhen/2024/12/ed251c4438f722dffd6cb95db86c0d56.jpg
|
||||||
-
|
|
||||||
name: 哦麦 MC
|
|
||||||
desc: 我的世界教学文档。
|
|
||||||
logo: https://static.ohmymc.com/img/minecraft-154749_1280.png?max_width=1920&max_height=1920
|
|
||||||
url: https://ohmymc.com/
|
|
||||||
preview: https://static.ohmymc.com/img/20241228225159139.png?max_width=1920&max_height=1920
|
|
||||||
-
|
-
|
||||||
name: NcatBotDocs
|
name: NcatBotDocs
|
||||||
desc: NcatBot,一个 QQ 机器人框架项目的使用文档。
|
desc: NcatBot,一个 QQ 机器人框架项目的使用文档。
|
||||||
@ -93,6 +87,12 @@ docs:
|
|||||||
logo: https://official.skycraft.cn/i/3.jpg
|
logo: https://official.skycraft.cn/i/3.jpg
|
||||||
url: https://docs.skycraft.cn/
|
url: https://docs.skycraft.cn/
|
||||||
preview: https://bbsimage.skycraft.cn/docs-preview.jpg
|
preview: https://bbsimage.skycraft.cn/docs-preview.jpg
|
||||||
|
-
|
||||||
|
name: mcenahle Docs
|
||||||
|
desc: mcenahle 的文档网站。
|
||||||
|
logo: https://d.mcenahle.com/images/logo.png
|
||||||
|
url: https://d.mcenahle.com/
|
||||||
|
preview: https://mcenahle.cn/resources/docs-site-preview.jpg
|
||||||
|
|
||||||
blog:
|
blog:
|
||||||
-
|
-
|
||||||
@ -186,13 +186,6 @@ blog:
|
|||||||
url: https://ar0m.com
|
url: https://ar0m.com
|
||||||
repo: https://github.com/jindongjie/blog-vuepress-2025
|
repo: https://github.com/jindongjie/blog-vuepress-2025
|
||||||
preview: /images/demos/jindongjie.jpg
|
preview: /images/demos/jindongjie.jpg
|
||||||
-
|
|
||||||
name: 艺述论
|
|
||||||
desc: 一枚喜受艺术的程序员's blog
|
|
||||||
logo: https://yishulun.com/avatar.png
|
|
||||||
url: https://yishulun.com
|
|
||||||
repo: https://github.com/rixingyike/rixingyike.github.io
|
|
||||||
preview: /images/demos/yishulun.com.jpg
|
|
||||||
-
|
-
|
||||||
name: 菲兹克斯喵
|
name: 菲兹克斯喵
|
||||||
desc: 一名物理系学生的笔记和生活
|
desc: 一名物理系学生的笔记和生活
|
||||||
@ -269,6 +262,13 @@ blog:
|
|||||||
logo: https://raw.githubusercontent.com/Konata9/pic-base/main/pics/20260126223726455.png
|
logo: https://raw.githubusercontent.com/Konata9/pic-base/main/pics/20260126223726455.png
|
||||||
url: https://konata9.cc/
|
url: https://konata9.cc/
|
||||||
preview: https://raw.githubusercontent.com/Konata9/pic-base/main/pics/20260125225910673.webp
|
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
|
:::important
|
||||||
@ -281,6 +281,12 @@ blog:
|
|||||||
|
|
||||||
[前往 **Github Pull Request** 提交站点](https://github.com/pengzhanbo/vuepress-theme-plume/edit/main/docs/demos.md){.read-more}
|
[前往 **Github Pull Request** 提交站点](https://github.com/pengzhanbo/vuepress-theme-plume/edit/main/docs/demos.md){.read-more}
|
||||||
|
|
||||||
|
::: info 案例每半年检查一次,以下情况的站点将会被移除
|
||||||
|
|
||||||
|
- 站点链接无法访问
|
||||||
|
- 已不再使用 vuepress-theme-plume 主题
|
||||||
|
:::
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
<Demos :list="$frontmatter.docs" />
|
<Demos :list="$frontmatter.docs" />
|
||||||
|
|||||||
@ -124,8 +124,8 @@ export default defineUserConfig({
|
|||||||
'/': {
|
'/': {
|
||||||
// Chinese collection configuration // [!code focus:4]
|
// Chinese collection configuration // [!code focus:4]
|
||||||
collections: [
|
collections: [
|
||||||
{ type: 'post', dir: 'blog', title: '博客' },
|
{ type: 'post', dir: 'blog', title: 'Blog' },
|
||||||
{ type: 'doc', dir: 'typescript', title: 'TypeScript笔记', sidebar: 'auto' }
|
{ type: 'doc', dir: 'typescript', title: 'TypeScript Notes', sidebar: 'auto' }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'/en/': {
|
'/en/': {
|
||||||
@ -150,8 +150,8 @@ export default defineThemeConfig({
|
|||||||
'/': {
|
'/': {
|
||||||
// Chinese collection configuration // [!code focus:4]
|
// Chinese collection configuration // [!code focus:4]
|
||||||
collections: [
|
collections: [
|
||||||
{ type: 'post', dir: 'blog', title: '博客' },
|
{ type: 'post', dir: 'blog', title: 'Blog' },
|
||||||
{ type: 'doc', dir: 'typescript', title: 'TypeScript笔记', sidebar: 'auto' }
|
{ type: 'doc', dir: 'typescript', title: 'TypeScript Notes', sidebar: 'auto' }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'/en/': {
|
'/en/': {
|
||||||
|
|||||||
@ -159,7 +159,7 @@ The `include` configuration is implemented by the
|
|||||||
- **Default:** `{ provider: 'iconify' }`
|
- **Default:** `{ provider: 'iconify' }`
|
||||||
- **Details:** Icon configuration.
|
- **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
|
### plot
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@ Refer to [Algolia DocSearch Reference](/guide/features/content-search/#algolia-d
|
|||||||
### Enable
|
### Enable
|
||||||
|
|
||||||
```ts title=".vuepress/config.ts" twoslash
|
```ts title=".vuepress/config.ts" twoslash
|
||||||
|
// @errors: 2353
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
import { plumeTheme } from 'vuepress-theme-plume'
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ Related plugin: [@vuepress/plugin-watermark](https://ecosystem.vuejs.press/zh/pl
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```ts title=".vuepress/config.ts" twoslash
|
```ts title=".vuepress/config.ts" twoslash
|
||||||
|
// @errors: 7006
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
import { plumeTheme } from 'vuepress-theme-plume'
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
|||||||
@ -77,12 +77,12 @@ list:
|
|||||||
name: pengzhanbo
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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:
|
socials:
|
||||||
-
|
-
|
||||||
icon: github
|
icon: github
|
||||||
@ -96,7 +96,7 @@ list:
|
|||||||
avatar: https://github.com/pengzhanbo.png
|
avatar: https://github.com/pengzhanbo.png
|
||||||
location: GuangZhou
|
location: GuangZhou
|
||||||
organization: PengZhanBo
|
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:
|
socials:
|
||||||
-
|
-
|
||||||
icon: github
|
icon: github
|
||||||
@ -110,17 +110,17 @@ list:
|
|||||||
avatar: https://github.com/pengzhanbo.png
|
avatar: https://github.com/pengzhanbo.png
|
||||||
location: GuangZhou
|
location: GuangZhou
|
||||||
organization: PengZhanBo
|
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:
|
groups:
|
||||||
-
|
-
|
||||||
title: 分组 1
|
title: Group 1
|
||||||
desc: 自定义颜色
|
desc: Custom colors
|
||||||
list:
|
list:
|
||||||
-
|
-
|
||||||
name: pengzhanbo
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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)
|
backgroundColor: rgb(255,153,0)
|
||||||
color: rgb(255,255,153)
|
color: rgb(255,255,153)
|
||||||
nameColor: rgb(255,255,170)
|
nameColor: rgb(255,255,170)
|
||||||
@ -135,7 +135,7 @@ groups:
|
|||||||
name: pengzhanbo
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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)
|
backgroundColor: rgb(255,102,102)
|
||||||
color: rgb(255,204,204)
|
color: rgb(255,204,204)
|
||||||
nameColor: rgb(255,238,238)
|
nameColor: rgb(255,238,238)
|
||||||
@ -143,22 +143,22 @@ groups:
|
|||||||
name: pengzhanbo
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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)
|
backgroundColor: rgb(0,153,204)
|
||||||
color: rgb(153,238,255)
|
color: rgb(153,238,255)
|
||||||
nameColor: rgb(153,255,255)
|
nameColor: rgb(153,255,255)
|
||||||
-
|
-
|
||||||
title: 分组 2
|
title: Group 2
|
||||||
desc: 这里是分组 2 的描述文字
|
desc: Description for Group 2
|
||||||
list:
|
list:
|
||||||
-
|
-
|
||||||
name: pengzhanbo
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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
|
name: pengzhanbo
|
||||||
link: https://github.com/pengzhanbo
|
link: https://github.com/pengzhanbo
|
||||||
avatar: https://github.com/pengzhanbo.png
|
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.
|
||||||
---
|
---
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Refer to the [ECharts documentation](https://echarts.apache.org/handbook/zh/get-
|
|||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
You can import and use `defineEchartsConfig` in the
|
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
|
```ts
|
||||||
import { defineEchartsConfig } from '@vuepress/plugin-markdown-chart/client'
|
import { defineEchartsConfig } from '@vuepress/plugin-markdown-chart/client'
|
||||||
|
|||||||
@ -132,4 +132,4 @@ It can also be placed within a `<CardGrid>` component.
|
|||||||
/>
|
/>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
||||||
[View Photography Works Example](../../../../../blog/1.示例/照片类作品示例.md)
|
[View Photography Works Example](/en/demos/)
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export default defineUserConfig({
|
|||||||
encrypt: {
|
encrypt: {
|
||||||
rules: {
|
rules: {
|
||||||
// Can be the relative path of an MD file to encrypt that file
|
// 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
|
// Can be a directory path to encrypt all articles under that directory
|
||||||
'/notes/vuepress-theme-plume/': '123456',
|
'/notes/vuepress-theme-plume/': '123456',
|
||||||
// Can be a request path to encrypt all articles under that path
|
// Can be a request path to encrypt all articles under that path
|
||||||
|
|||||||
@ -16,7 +16,7 @@ The theme supports icons from the following sources:
|
|||||||
Icons are used in the same way across the following theme features:
|
Icons are used in the same way across the following theme features:
|
||||||
|
|
||||||
- [Navbar Icons](../../config/navbar.md#configuration)
|
- [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)
|
- [File Tree Icons](../../guide/markdown/file-tree.md)
|
||||||
- [Code Group Title Icons](../code/code-tabs.md#group-title-icons)
|
- [Code Group Title Icons](../code/code-tabs.md#group-title-icons)
|
||||||
|
|
||||||
|
|||||||
@ -159,13 +159,13 @@ Right-aligned content
|
|||||||
**Input:**
|
**Input:**
|
||||||
|
|
||||||
````md
|
````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**
|
[^footnote1]: From India.Rabindranath Tagore **The Farthest Distance in the World**
|
||||||
````
|
````
|
||||||
|
|
||||||
**Output:**
|
**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**
|
[^footnote1]: From India.Rabindranath Tagore **The Farthest Distance in the World**
|
||||||
|
|||||||
@ -3,6 +3,9 @@ title: File Tree
|
|||||||
createTime: 2025/10/08 14:41:57
|
createTime: 2025/10/08 14:41:57
|
||||||
icon: mdi:file-tree
|
icon: mdi:file-tree
|
||||||
permalink: /en/guide/markdown/file-tree/
|
permalink: /en/guide/markdown/file-tree/
|
||||||
|
badge:
|
||||||
|
text: Change
|
||||||
|
type: warning
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## 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:
|
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**`
|
- 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 `--`
|
- 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.
|
- 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.
|
- 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
|
- ++ config.ts
|
||||||
- -- page1.md
|
- -- page1.md
|
||||||
- README.md
|
- README.md
|
||||||
- theme A **theme** directory
|
- theme # A **theme** directory
|
||||||
- client
|
- client
|
||||||
- components
|
- components
|
||||||
- **Navbar.vue**
|
- **Navbar.vue**
|
||||||
@ -61,7 +64,7 @@ The following syntax can be used to customize the appearance of the file tree:
|
|||||||
- ++ config.ts
|
- ++ config.ts
|
||||||
- -- page1.md
|
- -- page1.md
|
||||||
- README.md
|
- README.md
|
||||||
- theme A **theme** directory
|
- theme # A **theme** directory
|
||||||
- client
|
- client
|
||||||
- components
|
- components
|
||||||
- **Navbar.vue**
|
- **Navbar.vue**
|
||||||
|
|||||||
461
docs/en/guide/markdown/obsidian.md
Normal file
461
docs/en/guide/markdown/obsidian.md
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
- [Callout](#callout) - Highlight important information with styled containers
|
||||||
|
- [Comments](#comments) - Add comments visible only during editing
|
||||||
|
|
||||||
|
::: warning No plans to support extension syntax provided by Obsidian's third-party community plugins
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 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
|
||||||
|
callout: true, // Callout
|
||||||
|
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](#wiki-links) syntax.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="embedLink" type="boolean" default="true" optional
|
||||||
|
Enable [Embeds](#embeds) syntax.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="callout" type="boolean" default="true" optional
|
||||||
|
Enable [Callout](#callout) syntax.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="comment" type="boolean" default="true" optional
|
||||||
|
Enable [Comments](#comments) syntax.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## 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}
|
||||||
|
|
||||||
|
## Callout
|
||||||
|
|
||||||
|
Callout is a syntax for highlighting important information, similar to VuePress's `::: hint` container syntax.
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!note]
|
||||||
|
> Content
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optional Title:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!tip] Custom Title
|
||||||
|
> Content
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
Callout supports the following types, with aliases automatically mapped to their corresponding primary types:
|
||||||
|
|
||||||
|
| Type | Aliases | Description |
|
||||||
|
| ---- | ------- | ----------- |
|
||||||
|
| `note` | `quote`, `cite` | Notes, quotes |
|
||||||
|
| `tip` | `hint` | Tips, hints |
|
||||||
|
| `info` | `todo` | Information, todos |
|
||||||
|
| `success` | `check`, `done` | Success, done |
|
||||||
|
| `warning` | `question`, `help`, `faq` | Warnings, questions, help |
|
||||||
|
| `caution` | `attention`, `failure`, `fail`, `missing`, `danger`, `error`, `bug` | Caution, failure, danger |
|
||||||
|
| `important` | `example` | Important, examples |
|
||||||
|
| `details` | `abstract`, `summary`, `tldr` | Details, summary |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
**Basic Usage:**
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!NOTE]
|
||||||
|
> This is a note callout.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This is a note callout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**With Title:**
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!TIP] Useful Tip
|
||||||
|
> Using `pnpm` can significantly speed up dependency installation.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
> [!TIP] Useful Tip
|
||||||
|
> Using `pnpm` can significantly speed up dependency installation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Multiple Types:**
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!success]
|
||||||
|
> Operation completed successfully!
|
||||||
|
>
|
||||||
|
> [!warning]
|
||||||
|
> This is a warning message.
|
||||||
|
>
|
||||||
|
> [!caution]
|
||||||
|
> Please proceed with caution, this action cannot be undone.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
> [!success]
|
||||||
|
> Operation completed successfully!
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> This is a warning message.
|
||||||
|
|
||||||
|
> [!caution]
|
||||||
|
> Please proceed with caution, this action cannot be undone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Details Type:**
|
||||||
|
|
||||||
|
The `details` type renders as an HTML `<details>` element, supporting collapse/expand:
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!details]
|
||||||
|
> Click to expand more content
|
||||||
|
>
|
||||||
|
> This is hidden content.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
> [!details]
|
||||||
|
> Click to expand more content
|
||||||
|
>
|
||||||
|
> This is hidden content.
|
||||||
|
|
||||||
|
[Obsidian Official - **Callout**](https://obsidian.md/en/help/callouts){.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}
|
||||||
|
|
||||||
|
## 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
|
||||||
@ -6,7 +6,7 @@ permalink: /en/guide/markdown/qrcode/
|
|||||||
badge: New
|
badge: New
|
||||||
---
|
---
|
||||||
|
|
||||||
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ Inline syntax is suitable for shorter `text`, such as links.
|
|||||||
<!-- Basic Syntax -->
|
<!-- Basic Syntax -->
|
||||||
@[qrcode](text)
|
@[qrcode](text)
|
||||||
<!-- With Attributes -->
|
<!-- With Attributes -->
|
||||||
@[qrcode card svg title="xxx" align="center"](text)
|
@[qrcode card title="xxx" align="center" logo="/plume.png"](text)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Container Syntax
|
### Container Syntax
|
||||||
@ -50,7 +50,7 @@ Inline syntax is suitable for shorter `text`, such as links.
|
|||||||
Container syntax is suitable for longer `text`, such as paragraphs or multi-line text.
|
Container syntax is suitable for longer `text`, such as paragraphs or multi-line text.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
::: qrcode card svg title="xxx" align="center"
|
::: qrcode card title="xxx" align="center"
|
||||||
text
|
text
|
||||||
:::
|
:::
|
||||||
```
|
```
|
||||||
@ -64,8 +64,13 @@ text
|
|||||||
::: field name="card" type="boolean" optional default="false"
|
::: field name="card" type="boolean" optional default="false"
|
||||||
Whether to enable the card style.
|
Whether to enable the card style.
|
||||||
:::
|
:::
|
||||||
::: field name="svg" type="boolean" optional default="false"
|
::: field name="logo" type="string" optional
|
||||||
Whether to render the QR code in SVG format. The default format is PNG.
|
The path to the logo image displayed at the center of the QR code.
|
||||||
|
|
||||||
|
Only absolute paths are supported.
|
||||||
|
:::
|
||||||
|
::: field name="logoSize" type="number" optional default="0.2"
|
||||||
|
The size ratio of the logo relative to the QR code.
|
||||||
:::
|
:::
|
||||||
::: field name="title" type="string" optional
|
::: field name="title" type="string" optional
|
||||||
The title of the QR code.
|
The title of the QR code.
|
||||||
@ -100,6 +105,8 @@ Four levels are available depending on the operating environment.
|
|||||||
Higher levels provide better error resistance but reduce the data capacity of the symbol.
|
Higher levels provide better error resistance but reduce the data capacity of the symbol.
|
||||||
|
|
||||||
If the QR code symbol is unlikely to be damaged, lower error correction levels like Low or Medium can be safely used.
|
If the QR code symbol is unlikely to be damaged, lower error correction levels like Low or Medium can be safely used.
|
||||||
|
|
||||||
|
When the QR code contains a logo, the default value is `H`.
|
||||||
:::
|
:::
|
||||||
::: field name="version" type="number" optional
|
::: field name="version" type="number" optional
|
||||||
**QR Code Version**
|
**QR Code Version**
|
||||||
@ -129,8 +136,23 @@ If not specified, a more suitable value will be automatically calculated.
|
|||||||
|
|
||||||
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
|
### QR Code with Logo
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
### Internal Page Path
|
### Internal Page Path
|
||||||
|
|
||||||
|
::: tip Internal page links will automatically have a logo added
|
||||||
|
:::
|
||||||
|
|
||||||
**Input:**
|
**Input:**
|
||||||
|
|
||||||
```md
|
```md
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export default defineUserConfig({
|
|||||||
{
|
{
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
dir: 'guide',
|
dir: 'guide',
|
||||||
title: '指南',
|
title: 'Guide',
|
||||||
// autoFrontmatter: true, // Theme built-in configuration
|
// autoFrontmatter: true, // Theme built-in configuration
|
||||||
autoFrontmatter: {
|
autoFrontmatter: {
|
||||||
title: true, // Auto-generate title
|
title: true, // Auto-generate title
|
||||||
@ -117,7 +117,7 @@ export default defineThemeConfig({
|
|||||||
{
|
{
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
dir: 'guide',
|
dir: 'guide',
|
||||||
title: '指南',
|
title: 'Guide',
|
||||||
// autoFrontmatter: true, // Theme built-in configuration
|
// autoFrontmatter: true, // Theme built-in configuration
|
||||||
autoFrontmatter: {
|
autoFrontmatter: {
|
||||||
title: true, // Auto-generate title
|
title: true, // Auto-generate title
|
||||||
@ -297,23 +297,23 @@ Example:
|
|||||||
|
|
||||||
::: code-tree
|
::: code-tree
|
||||||
|
|
||||||
```md title="docs/blog/服务.md"
|
```md title="docs/blog/service.md"
|
||||||
---
|
---
|
||||||
title: 服务
|
title: Service
|
||||||
permalink: /blog/wu-fu/
|
permalink: /blog/service/
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
```md title="docs/blog/都城.md"
|
```md title="docs/blog/capital.md"
|
||||||
---
|
---
|
||||||
title: 都城
|
title: Capital
|
||||||
permalink: /blog/dou-cheng/
|
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
|
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`,
|
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.
|
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: 都城
|
title: Capital
|
||||||
permalink: /blog/du-cheng/
|
permalink: /blog/capital/
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|||||||
@ -760,8 +760,6 @@ Automatically switches to `top` layout on narrow-screen devices to ensure displa
|
|||||||
|
|
||||||
## Article Metadata
|
## Article Metadata
|
||||||
|
|
||||||
## 文章元数据
|
|
||||||
|
|
||||||
In the collection, the `meta` option allows you to set the display method of 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**:
|
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',
|
type: 'post',
|
||||||
dir: 'blog',
|
dir: 'blog',
|
||||||
title: '博客',
|
title: 'Blog',
|
||||||
// [!code hl:11]
|
// [!code hl:11]
|
||||||
meta: {
|
meta: {
|
||||||
tags: true, // Whether to display labels
|
tags: true, // Whether to display labels
|
||||||
@ -808,7 +806,7 @@ export default defineThemeConfig({
|
|||||||
{
|
{
|
||||||
type: 'post',
|
type: 'post',
|
||||||
dir: 'blog',
|
dir: 'blog',
|
||||||
title: '博客',
|
title: 'Blog',
|
||||||
// [!code hl:11]
|
// [!code hl:11]
|
||||||
meta: {
|
meta: {
|
||||||
tags: true, // Whether to display labels
|
tags: true, // Whether to display labels
|
||||||
|
|||||||
@ -28,10 +28,10 @@ A typical VuePress static site has the following file structure:
|
|||||||
:::file-tree
|
:::file-tree
|
||||||
|
|
||||||
- my-site
|
- my-site
|
||||||
- docs \# Source directory
|
- docs # Source directory
|
||||||
- .vuepress/
|
- .vuepress/
|
||||||
- …
|
- …
|
||||||
- README.md \# Homepage
|
- README.md # Homepage
|
||||||
- package.json
|
- package.json
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|||||||
@ -13,7 +13,7 @@ whether you are creating a **technical blog**, **personal journal**, **product d
|
|||||||
**knowledge base**, or **tutorial series**.
|
**knowledge base**, or **tutorial series**.
|
||||||
|
|
||||||
Deep optimizations have been applied to typography and image display,
|
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**.
|
create professional content that is **aesthetically pleasing, highly readable, and expressive**.
|
||||||
|
|
||||||
::: details New to VuePress?
|
::: details New to VuePress?
|
||||||
@ -42,7 +42,7 @@ a more beautiful, clean, and user-friendly reading experience.
|
|||||||
==content encryption==, and ==article watermarking==.
|
==content encryption==, and ==article watermarking==.
|
||||||
- **Code Presentation**: Support for code block ==grouping==, ==collapsing==, ==focusing==,
|
- **Code Presentation**: Support for code block ==grouping==, ==collapsing==, ==focusing==,
|
||||||
==line highlighting==, ==diff comparison==, and embedding ==code demos== from platforms like CodePen, JSFiddle, and CodeSandbox.
|
==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.
|
**200,000+** ==icons==, with optional support for `iconfont` / `fontawesome` icon libraries.
|
||||||
- **Media Embedding**: Support for ==PDF embedding==, and ==Bilibili/Youtube/Local Video== embedding.
|
- **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.
|
- **Chart Rendering**: Integration with multiple ==chart engines== including chart.js, Echarts, Mermaid, Flowchart, Markmap, and PlantUML.
|
||||||
|
|||||||
@ -13,22 +13,22 @@ For projects created via the [command-line tool](./usage.md#command-line-install
|
|||||||
::: file-tree
|
::: file-tree
|
||||||
|
|
||||||
- .git/
|
- .git/
|
||||||
- **docs** \# Documentation source directory
|
- **docs** # Documentation source directory
|
||||||
- .vuepress \# VuePress configuration directory
|
- .vuepress/ # VuePress configuration directory
|
||||||
- public/ \# Static assets
|
- public/ # Static assets
|
||||||
- client.ts \# Client configuration (optional)
|
- client.ts # Client configuration (optional)
|
||||||
- collections.ts \# Collections configuration (optional)
|
- collections.ts # Collections configuration (optional)
|
||||||
- config.ts \# VuePress main configuration
|
- config.ts # VuePress main configuration
|
||||||
- navbar.ts \# Navbar configuration (optional)
|
- navbar.ts # Navbar configuration (optional)
|
||||||
- plume.config.ts \# Theme configuration file (optional)
|
- plume.config.ts # Theme configuration file (optional)
|
||||||
- demo \# `doc` type collection
|
- demo # `doc` type collection
|
||||||
- foo.md
|
- foo.md
|
||||||
- bar.md
|
- bar.md
|
||||||
- blog \# `post` type collection
|
- blog # `post` type collection
|
||||||
- preview \# Blog category
|
- preview # Blog category
|
||||||
- markdown.md \# Category article
|
- markdown.md # Category article
|
||||||
- article.md \# Blog article
|
- article.md # Blog article
|
||||||
- README.md \# Site homepage
|
- README.md # Site homepage
|
||||||
- …
|
- …
|
||||||
- package.json
|
- package.json
|
||||||
- pnpm-lock.yaml
|
- pnpm-lock.yaml
|
||||||
|
|||||||
@ -33,7 +33,7 @@ A typical project structure might look like:
|
|||||||
- rust # Rust Programming Notes
|
- rust # Rust Programming Notes
|
||||||
- tuple.md
|
- tuple.md
|
||||||
- struct.md
|
- struct.md
|
||||||
- README.md # Site Homepage
|
- README.md # Site Homepage
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Configuration via `sidebar`
|
### Configuration via `sidebar`
|
||||||
|
|||||||
@ -35,7 +35,7 @@ Page content starts after the second `---`.
|
|||||||
Frontmatter is a configuration block using
|
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 `---`.
|
[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
|
## Automatic Frontmatter Generation
|
||||||
@ -139,14 +139,14 @@ The numeric part serves as the **sorting basis**. Directories without numbers ar
|
|||||||
::: file-tree
|
::: file-tree
|
||||||
|
|
||||||
- docs
|
- docs
|
||||||
- blog \# post type collection
|
- blog # post type collection
|
||||||
- 1.Frontend
|
- 1.Frontend
|
||||||
- 1.html/
|
- 1.html/
|
||||||
- 2.css/
|
- 2.css/
|
||||||
- 3.javascript/
|
- 3.javascript/
|
||||||
- 2.Backend/
|
- 2.Backend/
|
||||||
- DevOps/
|
- DevOps/
|
||||||
- typescript \# doc type collection
|
- typescript # doc type collection
|
||||||
- 1.Basics
|
- 1.Basics
|
||||||
- 1.Variables.md
|
- 1.Variables.md
|
||||||
- 2.Types.md
|
- 2.Types.md
|
||||||
|
|||||||
@ -5,19 +5,19 @@ createTime: 2024/04/22 09:44:37
|
|||||||
permalink: /en/guide/repl/kotlin/
|
permalink: /en/guide/repl/kotlin/
|
||||||
---
|
---
|
||||||
|
|
||||||
## 概述
|
## Overview
|
||||||
|
|
||||||
主题提供了 Kotlin 代码演示,支持 在线运行 Kotlin 代码。
|
The theme provides Kotlin code demonstrations, supporting online execution of Kotlin code.
|
||||||
|
|
||||||
::: important
|
::: 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"
|
```ts title=".vuepress/config.ts"
|
||||||
export default defineUserConfig({
|
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
|
````md
|
||||||
::: kotlin-repl title="自定义标题"
|
::: kotlin-repl title="Custom Title"
|
||||||
```kotlin
|
```kotlin
|
||||||
// your kotlin code
|
// 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
|
````md
|
||||||
::: kotlin-repl editable title="自定义标题"
|
::: kotlin-repl editable title="Custom Title"
|
||||||
```kotlin
|
```kotlin
|
||||||
// your kotlin code
|
// your kotlin code
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
````
|
````
|
||||||
|
|
||||||
## 示例
|
## Examples
|
||||||
|
|
||||||
### 打印内容
|
### Print Content
|
||||||
|
|
||||||
**输入:**
|
**Input:**
|
||||||
|
|
||||||
````md
|
````md
|
||||||
::: kotlin-repl
|
::: kotlin-repl
|
||||||
@ -78,7 +78,7 @@ fun main(args: Array<String>) {
|
|||||||
:::
|
:::
|
||||||
````
|
````
|
||||||
|
|
||||||
**输出:**
|
**Output:**
|
||||||
|
|
||||||
::: kotlin-repl
|
::: kotlin-repl
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 运算
|
### Computation
|
||||||
|
|
||||||
::: kotlin-repl
|
::: kotlin-repl
|
||||||
|
|
||||||
@ -109,9 +109,9 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 可编辑代码演示
|
### Editable Code Demo
|
||||||
|
|
||||||
**输入:**
|
**Input:**
|
||||||
|
|
||||||
````md
|
````md
|
||||||
::: kotlin-repl editable
|
::: kotlin-repl editable
|
||||||
@ -126,7 +126,7 @@ fun main(args: Array<String>) {
|
|||||||
:::
|
:::
|
||||||
````
|
````
|
||||||
|
|
||||||
**输出:**
|
**Output:**
|
||||||
|
|
||||||
::: kotlin-repl editable
|
::: kotlin-repl editable
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export default defineUserConfig({
|
|||||||
:::
|
:::
|
||||||
````
|
````
|
||||||
|
|
||||||
图标配置请查看 [chart.js] 文档 。
|
图表配置请查看 [chart.js] 文档。
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ permalink: /guide/code/group/
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
代码组(Code Tabs)是 主题 中用于并排展示多个相关代码片段的强大功能。
|
代码组(Code Tabs)是主题中用于并排展示多个相关代码片段的强大功能。
|
||||||
通过标签页形式组织代码,您可以清晰对比不同技术栈、配置方案或语言版本的实现差异。
|
通过标签页形式组织代码,您可以清晰对比不同技术栈、配置方案或语言版本的实现差异。
|
||||||
|
|
||||||
## 基础语法
|
## 基础语法
|
||||||
|
|||||||
@ -641,7 +641,7 @@ str = 'Hello'
|
|||||||
你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 `description` 中找到对应的错误码。
|
你应该能够在控制台中查看到相关的错误信息,然后在错误信息的 `description` 中找到对应的错误码。
|
||||||
然后再将错误码添加到 `@errors` 中。
|
然后再将错误码添加到 `@errors` 中。
|
||||||
|
|
||||||
不用担心变异失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。
|
不用担心编译失败会终止进程,主题会在编译失败时显示错误信息,同时在代码块中输出未编译的代码。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### `@noErrors`
|
### `@noErrors`
|
||||||
|
|||||||
@ -7,7 +7,7 @@ permalink: /guide/components/card-masonry/
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
瀑布流容器是一个 通用的容器组件,你可以把任何内容放到 `<CardMasonry>` 里面,容器会自动计算每一个 **项** 的高度,
|
瀑布流容器是一个通用的容器组件,你可以把任何内容放到 `<CardMasonry>` 里面,容器会自动计算每一个 **项** 的高度,
|
||||||
然后将它们按照瀑布流的方式进行排列。
|
然后将它们按照瀑布流的方式进行排列。
|
||||||
|
|
||||||
::: details 什么是项 ?
|
::: details 什么是项 ?
|
||||||
|
|||||||
@ -7,7 +7,7 @@ permalink: /guide/components/home-box/
|
|||||||
|
|
||||||
## 首页布局容器
|
## 首页布局容器
|
||||||
|
|
||||||
自定义首页时,使用 `<HomeBox>` 提供给 区域 的 包装容器。
|
自定义首页时,使用 `<HomeBox>` 提供给区域的包装容器。
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ permalink: /guide/components/image-card/
|
|||||||
|
|
||||||
使用 `<ImageCard>` 组件在页面中显示图片卡片。
|
使用 `<ImageCard>` 组件在页面中显示图片卡片。
|
||||||
|
|
||||||
图片卡片 有别于 markdown 的 普通插入图片方式,它展示与图片相关的更多信息,包括标题、描述、作者、链接等。
|
图片卡片有别于 markdown 的普通插入图片方式,它展示与图片相关的更多信息,包括标题、描述、作者、链接等。
|
||||||
适用于如 摄影作品、设计作品、宣传海报 等场景。
|
适用于如 摄影作品、设计作品、宣传海报 等场景。
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import NpmBadgeGroup from 'vuepress-theme-plume/features/NpmBadgeGroup.vue'
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
Npm 徽章组件 用于显示 npm 包信息,并提供相关的链接。
|
Npm 徽章组件用于显示 npm 包信息,并提供相关的链接。
|
||||||
|
|
||||||
徽章由 <https://shields.io> 提供支持。
|
徽章由 <https://shields.io> 提供支持。
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: “隐秘”文本
|
title: “隐秘” 文本
|
||||||
icon: lets-icons:hide-eye
|
icon: lets-icons:hide-eye
|
||||||
createTime: 2024/08/18 23:02:39
|
createTime: 2024/08/18 23:02:39
|
||||||
permalink: /guide/components/plot/
|
permalink: /guide/components/plot/
|
||||||
@ -7,7 +7,7 @@ permalink: /guide/components/plot/
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
使用 `<Plot>` 组件显示 [“隐秘”文本](../markdown/plot.md) ,能够更灵活的控制行为。
|
使用 `<Plot>` 组件显示 ["隐秘"文本](../markdown/plot.md),能够更灵活地控制行为。
|
||||||
|
|
||||||
该组件默认不启用,你需要在 theme 配置中启用。
|
该组件默认不启用,你需要在 theme 配置中启用。
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue'
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
Repo 卡片组件 用于显示 GitHub / Gitee 仓库信息。
|
Repo 卡片组件用于显示 GitHub / Gitee 仓库信息。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@ badge: 新
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
对于大多数的站点而言,一个 **炫酷好看** 首页首屏,能够更容易的吸引用户停留下来。
|
对于大多数的站点而言,一个 **炫酷好看** 的首页首屏,能够更容易地吸引用户停留下来。
|
||||||
但实现 **炫酷好看** 往往需要比较复杂的技术成本,以及一些不错的灵感。
|
但实现 **炫酷好看** 的效果往往需要比较复杂的技术成本,以及一些不错的灵感。
|
||||||
|
|
||||||
主题对 **首页** 的 **Hero** 部分,内置了一系列 **炫酷好看** 的背景效果,
|
主题对 **首页** 的 **Hero** 部分,内置了一系列 **炫酷好看** 的背景效果,
|
||||||
通过简单的配置即可应用到你的站点首页中:
|
通过简单的配置即可应用到你的站点首页中:
|
||||||
|
|||||||
@ -320,7 +320,7 @@ const {
|
|||||||
|
|
||||||
公告板的唯一标识由 `bulletin.id` 配置。
|
公告板的唯一标识由 `bulletin.id` 配置。
|
||||||
|
|
||||||
唯一标识是用于区分公告板,并根据 该表示 决定 `bulletin.lifetime` 的有效期。
|
唯一标识是用于区分公告板,并根据该标识决定 `bulletin.lifetime` 的有效期。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default defineUserConfig({
|
export default defineUserConfig({
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export default defineUserConfig({
|
|||||||
|
|
||||||
贡献者信息列表。
|
贡献者信息列表。
|
||||||
|
|
||||||
用户在本地 git 服务中配置的 用户名和邮箱 可能与 git 托管服务(如 github、gitlab、gitee)的用户信息不一致。
|
用户在本地 git 服务中配置的用户名和邮箱可能与 git 托管服务(如 github、gitlab、gitee)的用户信息不一致。
|
||||||
可以在此预先配置贡献者信息。
|
可以在此预先配置贡献者信息。
|
||||||
|
|
||||||
(对于非 github 的其他 git 托管服务,诸如 gitlab、gitee,由于不能通过用户名直接获取头像和用户地址,请在此
|
(对于非 github 的其他 git 托管服务,诸如 gitlab、gitee,由于不能通过用户名直接获取头像和用户地址,请在此
|
||||||
|
|||||||
@ -16,7 +16,7 @@ permalink: /guide/features/icon/
|
|||||||
在主题的以下功能中以相同的方式使用图标:
|
在主题的以下功能中以相同的方式使用图标:
|
||||||
|
|
||||||
- [导航栏图标](../../config/navbar.md#配置)
|
- [导航栏图标](../../config/navbar.md#配置)
|
||||||
- [侧边栏图标](../../guide/document.md#侧边栏图标)
|
- [侧边栏图标](../quick-start/sidebar.md#视觉增强功能)
|
||||||
- [文件树图标](../../guide/markdown/file-tree.md)
|
- [文件树图标](../../guide/markdown/file-tree.md)
|
||||||
- [代码分组标题图标](../code/code-tabs.md#分组标题图标)
|
- [代码分组标题图标](../code/code-tabs.md#分组标题图标)
|
||||||
|
|
||||||
|
|||||||
@ -26,16 +26,15 @@ tags:
|
|||||||
|
|
||||||
内部和外部链接都会被特殊处理。
|
内部和外部链接都会被特殊处理。
|
||||||
|
|
||||||
主题默认对每个 md 文件自动生成一个新的 链接,并保存在对应的 md 文件的 frontmatter 的 `permalink` 中。
|
主题默认对每个 Markdown 文件自动生成一个新的链接,并保存在对应的 Markdown 文件的 frontmatter 的 `permalink` 中。您可以随时修改它们。您也可以通过 `theme.autoFrontmatter` 选项来禁用这个功能,这时会恢复为 VuePress 的默认行为。
|
||||||
你可以随时修改它们。你也可以通过 `theme.autoFrontmatter` 选项来禁用这个功能,这时会恢复为 VuePress 的默认行为。
|
|
||||||
|
|
||||||
### 内部链接
|
### 内部链接
|
||||||
|
|
||||||
有三种方式来使用内部链接:
|
有三种方式来使用内部链接:
|
||||||
|
|
||||||
- 使用 生成的 `permalink` 作为内部链接的目标。
|
- 使用生成的 `permalink` 作为内部链接的目标。
|
||||||
- 使用 md 文件的相对路径作为内部链接的目标。
|
- 使用 Markdown 文件的相对路径作为内部链接的目标。
|
||||||
- 使用 md 文件的绝对路径作为内部链接的目标, 绝对路径 `/` 表示从 `${sourceDir}` 目录开始。
|
- 使用 Markdown 文件的绝对路径作为内部链接的目标,绝对路径 `/` 表示从 `${sourceDir}` 目录开始。
|
||||||
|
|
||||||
```md
|
```md
|
||||||
[Markdown](/guide/markdown/)
|
[Markdown](/guide/markdown/)
|
||||||
|
|||||||
@ -3,6 +3,9 @@ title: 文件树
|
|||||||
createTime: 2024/09/30 14:41:57
|
createTime: 2024/09/30 14:41:57
|
||||||
icon: mdi:file-tree
|
icon: mdi:file-tree
|
||||||
permalink: /guide/markdown/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.md # 这是一个 README 文件`
|
||||||
- 通过在名称前添加 `++` 或 `--` 来标记文件或目录为 **新增** 或 **删除**
|
- 通过在名称前添加 `++` 或 `--` 来标记文件或目录为 **新增** 或 **删除**
|
||||||
- 使用 `...` 或 `…` 作为名称来添加占位符文件和目录。
|
- 使用 `...` 或 `…` 作为名称来添加占位符文件和目录。
|
||||||
- 在 `:::file-tree` 后添加 `icon="simple"` 或 添加 `icon="colored"` 可以切换为简单图标或彩色图标,默认为彩色图标。
|
- 在 `:::file-tree` 后添加 `icon="simple"` 或 添加 `icon="colored"` 可以切换为简单图标或彩色图标,默认为彩色图标。
|
||||||
- 在 `:::file-tree` 后添加 `title="xxxx"` 可以为文件树添加标题。
|
- 在 `:::file-tree` 后添加 `title="xxxx"` 可以为文件树添加标题。
|
||||||
|
|
||||||
|
::: important `rc.193` 主题更新说明
|
||||||
|
过去 `file-tree` 使用 **空格** 来区分文件名和注释,这在某些情况下会导致问题,例如文件名中包含空格时。
|
||||||
|
为了解决这个问题,我们引入了 **# 号注释** 语法,您可以在文件名后添加以 `#` 开头的注释,例如 `README.md # 这是一个 README 文件`。
|
||||||
|
|
||||||
|
**此修改为 ==破坏性更新=={.danger} 更新。**
|
||||||
|
:::
|
||||||
|
|
||||||
**输入:**
|
**输入:**
|
||||||
|
|
||||||
```md /++/ /--/
|
```md /++/ /--/
|
||||||
@ -33,7 +43,7 @@ permalink: /guide/markdown/file-tree/
|
|||||||
- ++ config.ts
|
- ++ config.ts
|
||||||
- -- page1.md
|
- -- page1.md
|
||||||
- README.md
|
- README.md
|
||||||
- theme 一个 **主题** 目录
|
- theme # 一个 **主题** 目录
|
||||||
- client
|
- client
|
||||||
- components
|
- components
|
||||||
- **Navbar.vue**
|
- **Navbar.vue**
|
||||||
@ -60,7 +70,7 @@ permalink: /guide/markdown/file-tree/
|
|||||||
- ++ config.ts
|
- ++ config.ts
|
||||||
- -- page1.md
|
- -- page1.md
|
||||||
- README.md
|
- README.md
|
||||||
- theme 一个 **主题** 目录
|
- theme # 一个 **主题** 目录
|
||||||
- client
|
- client
|
||||||
- components
|
- components
|
||||||
- **Navbar.vue**
|
- **Navbar.vue**
|
||||||
|
|||||||
460
docs/guide/markdown/obsidian.md
Normal file
460
docs/guide/markdown/obsidian.md
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
---
|
||||||
|
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-链接) - 页面间相互链接的语法
|
||||||
|
- [嵌入内容](#嵌入内容) - 将其他文件内容嵌入到当前页面
|
||||||
|
- [Callout](#callout) - 使用样式容器突出显示重要信息
|
||||||
|
- [注释](#注释) - 添加仅在编辑时可见的注释
|
||||||
|
|
||||||
|
::: warning 不计划支持 Obsidian 社区第三方插件提供的扩展语法
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
Obsidian 兼容功能默认全部启用,你可以通过配置选择性地启用或禁用:
|
||||||
|
|
||||||
|
```ts title=".vuepress/config.ts"
|
||||||
|
export default defineUserConfig({
|
||||||
|
theme: plumeTheme({
|
||||||
|
plugins: {
|
||||||
|
mdPower: {
|
||||||
|
obsidian: {
|
||||||
|
wikiLink: true, // Wiki 链接
|
||||||
|
embedLink: true, // 嵌入内容
|
||||||
|
callout: true, // Callout
|
||||||
|
comment: true, // 注释
|
||||||
|
},
|
||||||
|
pdf: true, // PDF 嵌入功能
|
||||||
|
artPlayer: true, // 视频嵌入功能
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项
|
||||||
|
|
||||||
|
:::: field-group
|
||||||
|
|
||||||
|
::: field name="wikiLink" type="boolean" default="true" optional
|
||||||
|
启用 [Wiki 链接](#wiki-链接) 语法。
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="embedLink" type="boolean" default="true" optional
|
||||||
|
启用 [嵌入内容](#嵌入内容) 语法。
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="callout" type="boolean" default="true" optional
|
||||||
|
启用 [Callout](#callout) 语法。
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: field name="comment" type="boolean" default="true" optional
|
||||||
|
启用 [注释](#注释) 语法。
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## 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}
|
||||||
|
|
||||||
|
## Callout
|
||||||
|
|
||||||
|
Callout 是一种用于突出显示重要信息的语法,类似于 VuePress 的 `::: hint` 提示框语法。
|
||||||
|
|
||||||
|
### 语法
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!note]
|
||||||
|
> 内容
|
||||||
|
```
|
||||||
|
|
||||||
|
**可选标题:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!tip] 自定义标题
|
||||||
|
> 内容
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型
|
||||||
|
|
||||||
|
Callout 支持以下类型,别名会自动映射到对应的主要类型:
|
||||||
|
|
||||||
|
| 类型 | 别名 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `note` | `quote`, `cite` | 笔记、引用 |
|
||||||
|
| `tip` | `hint` | 技巧、提示 |
|
||||||
|
| `info` | `todo` | 信息、待办 |
|
||||||
|
| `success` | `check`, `done` | 成功、完成 |
|
||||||
|
| `warning` | `question`, `help`, `faq` | 警告、问题、帮助 |
|
||||||
|
| `caution` | `attention`, `failure`, `fail`, `missing`, `danger`, `error`, `bug` | 注意、失败、危险 |
|
||||||
|
| `important` | `example` | 重要、示例 |
|
||||||
|
| `details` | `abstract`, `summary`, `tldr` | 详情、摘要 |
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**基础用法:**
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!NOTE]
|
||||||
|
> 这是一个笔记提示框。
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 这是一个笔记提示框。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**带标题:**
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!TIP] 实用技巧
|
||||||
|
> 使用 `pnpm` 可以显著加快依赖安装速度。
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
> [!TIP] 实用技巧
|
||||||
|
> 使用 `pnpm` 可以显著加快依赖安装速度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**多种类型:**
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!success]
|
||||||
|
> 操作成功完成!
|
||||||
|
>
|
||||||
|
> [!warning]
|
||||||
|
> 这是一个警告信息。
|
||||||
|
>
|
||||||
|
> [!caution]
|
||||||
|
> 请谨慎操作,此操作不可撤销。
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
> [!success]
|
||||||
|
> 操作成功完成!
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> 这是一个警告信息。
|
||||||
|
|
||||||
|
> [!caution]
|
||||||
|
> 请谨慎操作,此操作不可撤销。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Details 类型:**
|
||||||
|
|
||||||
|
`details` 类型会渲染为 HTML `<details>` 元素,支持折叠展开:
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
> [!details]
|
||||||
|
> 点我展开更多内容
|
||||||
|
>
|
||||||
|
> 这是一段隐藏的内容。
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
> [!details]
|
||||||
|
> 点我展开更多内容
|
||||||
|
>
|
||||||
|
> 这是一段隐藏的内容。
|
||||||
|
|
||||||
|
[Obsidian 官方 - Callout](https://obsidian.md/zh/help/callouts){.readmore}
|
||||||
|
|
||||||
|
## 注释
|
||||||
|
|
||||||
|
使用 `%%` 包裹的内容会被当作注释,不会渲染到页面中。
|
||||||
|
|
||||||
|
### 语法
|
||||||
|
|
||||||
|
**行内注释:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
这是一个 %%行内注释%% 示例。
|
||||||
|
```
|
||||||
|
|
||||||
|
**块级注释:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
%%
|
||||||
|
这是一个块级注释。
|
||||||
|
可以跨越多行。
|
||||||
|
%%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**行内注释:**
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
这是一个 %%行内注释%% 示例。
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
这是一个 %%行内注释%% 示例。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**块级注释:**
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
注释之前的内容
|
||||||
|
|
||||||
|
%%
|
||||||
|
这是一个块级注释。
|
||||||
|
|
||||||
|
可以跨越多行。
|
||||||
|
%%
|
||||||
|
|
||||||
|
注释之后的内容
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
注释之前的内容
|
||||||
|
|
||||||
|
%%
|
||||||
|
这是一个块级注释。
|
||||||
|
%%
|
||||||
|
|
||||||
|
可以跨越多行。
|
||||||
|
|
||||||
|
[Obsidian 官方 - 注释](https://obsidian.md/zh/help/syntax#%E6%B3%A8%E9%87%8A){.readmore}
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 这些插件提供的是 **兼容性支持**,并非完全实现 Obsidian 的全部功能
|
||||||
|
- 部分 Obsidian 特有的功能(如内部链接的图谱视图、双向链接等)不在支持范围内
|
||||||
|
- 嵌入内容时,被嵌入的页面也会参与主题的构建过程
|
||||||
|
- PDF 嵌入需要同时启用 `markdown.pdf` 插件
|
||||||
|
- 视频嵌入需要同时启用 `markdown.artPlayer` 插件
|
||||||
|
- 以 `/` 开头或使用 `./` 形式的嵌入资源会从 `public` 目录加载
|
||||||
@ -6,7 +6,7 @@ permalink: /guide/markdown/qrcode/
|
|||||||
badge: 新
|
badge: 新
|
||||||
---
|
---
|
||||||
|
|
||||||
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ export default defineUserConfig({
|
|||||||
<!-- 基础语法-->
|
<!-- 基础语法-->
|
||||||
@[qrcode](text)
|
@[qrcode](text)
|
||||||
<!-- 添加属性 -->
|
<!-- 添加属性 -->
|
||||||
@[qrcode card svg title="xxx" align="center"](text)
|
@[qrcode card title="xxx" align="center" logo="/plume.png"](text)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 容器语法
|
### 容器语法
|
||||||
@ -50,7 +50,7 @@ export default defineUserConfig({
|
|||||||
容器语法适用于 `text` 文本较长时,比如 段落,多行文本 等。
|
容器语法适用于 `text` 文本较长时,比如 段落,多行文本 等。
|
||||||
|
|
||||||
```md
|
```md
|
||||||
::: qrcode card svg title="xxx" align="center"
|
::: qrcode card title="xxx" align="center"
|
||||||
text
|
text
|
||||||
:::
|
:::
|
||||||
```
|
```
|
||||||
@ -64,8 +64,13 @@ text
|
|||||||
::: field name="card" type="boolean" optional default="false"
|
::: field name="card" type="boolean" optional default="false"
|
||||||
是否启用卡片样式。
|
是否启用卡片样式。
|
||||||
:::
|
:::
|
||||||
::: field name="svg" type="boolean" optional default="false"
|
::: field name="logo" type="string" optional
|
||||||
是否将二维码渲染为 SVG 格式。默认渲染为 PNG 格式。
|
二维码 logo 图片路径。显示于二维码中心。
|
||||||
|
|
||||||
|
仅支持 绝对路径。
|
||||||
|
:::
|
||||||
|
::: field name="logoSize" type="number" optional default="0.2"
|
||||||
|
logo 相对于二维码 大小比例
|
||||||
:::
|
:::
|
||||||
::: field name="title" type="string" optional
|
::: field name="title" type="string" optional
|
||||||
二维码标题。
|
二维码标题。
|
||||||
@ -98,6 +103,8 @@ text
|
|||||||
更高级别提供更好的抗错能力,但会降低符号的容量。
|
更高级别提供更好的抗错能力,但会降低符号的容量。
|
||||||
|
|
||||||
如果二维码符号可能被损坏的几率较低,则可以安全使用低纠错级别,如低或中。
|
如果二维码符号可能被损坏的几率较低,则可以安全使用低纠错级别,如低或中。
|
||||||
|
|
||||||
|
当二维码中包含 logo 时,默认值为 `H`。
|
||||||
:::
|
:::
|
||||||
::: field name="version" type="number" optional
|
::: field name="version" type="number" optional
|
||||||
**二维码版本**
|
**二维码版本**
|
||||||
@ -127,8 +134,23 @@ text
|
|||||||
|
|
||||||
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
@[qrcode](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
|
### 带 logo 的二维码
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
|
||||||
|
```md
|
||||||
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
|
||||||
|
@[qrcode logo="/plume.png" logo-size=0.25](https://github.com/pengzhanbo/vuepress-theme-plume)
|
||||||
|
|
||||||
### 站内的页面路径
|
### 站内的页面路径
|
||||||
|
|
||||||
|
::: tip 站内页面链接自动添加 logo 图片
|
||||||
|
:::
|
||||||
|
|
||||||
**输入:**
|
**输入:**
|
||||||
|
|
||||||
```md
|
```md
|
||||||
|
|||||||
@ -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)。
|
更多自定义配置,请参考 [自定义首页](../custom/home.md)。
|
||||||
|
|
||||||
当使用以上两种方式 将首页配置为 文章列表页后,由于主题默认依然会生成 文章列表页,
|
当使用以上两种方式将首页配置为文章列表页后,由于主题默认依然会生成文章列表页,这导致存在了重复功能的页面。为此,您可能需要在集合配置中**关闭自动生成博客文章列表页**:
|
||||||
这导致存在了重复功能的页面。为此,你可能需要在 集合配置中,
|
|
||||||
**关闭自动生成博客文章列表页**:
|
|
||||||
|
|
||||||
(还可以重新修改 分类页/标签页/归档页的链接地址)
|
(还可以重新修改 分类页/标签页/归档页的链接地址)
|
||||||
|
|
||||||
|
|||||||
@ -27,10 +27,10 @@ permalink: /guide/collection/
|
|||||||
:::file-tree
|
:::file-tree
|
||||||
|
|
||||||
- my-site
|
- my-site
|
||||||
- docs \# 源目录
|
- docs # 源目录
|
||||||
- .vuepress/
|
- .vuepress/
|
||||||
- …
|
- …
|
||||||
- README.md \# 首页
|
- README.md # 首页
|
||||||
- package.json
|
- package.json
|
||||||
|
|
||||||
:::
|
:::
|
||||||
@ -87,7 +87,7 @@ permalink: /guide/collection/
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
`blog` 目录下的 markdown 文章,在 post 集合中读取为文章列表,并生成列表页、分类页、标签页等页面。
|
`blog` 目录下的 Markdown 文章将被读取为文章列表,并自动生成列表页、分类页、标签页等页面。
|
||||||
|
|
||||||
- **完成**
|
- **完成**
|
||||||
::::
|
::::
|
||||||
|
|||||||
@ -12,22 +12,22 @@ permalink: /guide/project-structure/
|
|||||||
::: file-tree
|
::: file-tree
|
||||||
|
|
||||||
- .git/
|
- .git/
|
||||||
- **docs** \# 文档源目录
|
- **docs** # 文档源目录
|
||||||
- .vuepress \# VuePress 配置目录
|
- .vuepress/ # VuePress 配置目录
|
||||||
- public/ \# 静态资源
|
- public/ # 静态资源
|
||||||
- client.ts \# 客户端配置(可选)
|
- client.ts # 客户端配置(可选)
|
||||||
- collections.ts \# Collections 配置(可选)
|
- collections.ts # Collections 配置(可选)
|
||||||
- config.ts \# VuePress 主配置
|
- config.ts # VuePress 主配置
|
||||||
- navbar.ts \# 导航栏配置(可选)
|
- navbar.ts # 导航栏配置(可选)
|
||||||
- plume.config.ts \# 主题配置文件(可选)
|
- plume.config.ts # 主题配置文件(可选)
|
||||||
- demo \# `doc` 类型 collection
|
- demo # `doc` 类型 collection
|
||||||
- foo.md
|
- foo.md
|
||||||
- bar.md
|
- bar.md
|
||||||
- blog \# `post` 类型 collection
|
- blog # `post` 类型 collection
|
||||||
- preview \# 博客分类
|
- preview # 博客分类
|
||||||
- markdown.md \# 分类文章
|
- markdown.md # 分类文章
|
||||||
- article.md \# 博客文章
|
- article.md # 博客文章
|
||||||
- README.md \# 站点首页
|
- README.md # 站点首页
|
||||||
- …
|
- …
|
||||||
- package.json
|
- package.json
|
||||||
- pnpm-lock.yaml
|
- pnpm-lock.yaml
|
||||||
|
|||||||
@ -13,7 +13,7 @@ tags:
|
|||||||
|
|
||||||
侧边栏是文档常见的页面导航方式,可以快速定位到文档内容。
|
侧边栏是文档常见的页面导航方式,可以快速定位到文档内容。
|
||||||
|
|
||||||
主题提供了两种方式配置侧边栏,包括:
|
主题提供了两种方式配置侧边栏:
|
||||||
|
|
||||||
- 通过主题配置的 `sidebar` 选项配置侧边栏
|
- 通过主题配置的 `sidebar` 选项配置侧边栏
|
||||||
- 在 [类型为 `doc` 的集合](./collection-doc.md) 中配置侧边栏
|
- 在 [类型为 `doc` 的集合](./collection-doc.md) 中配置侧边栏
|
||||||
@ -33,7 +33,7 @@ tags:
|
|||||||
- rust # Rust 编程笔记
|
- rust # Rust 编程笔记
|
||||||
- tuple.md
|
- tuple.md
|
||||||
- struct.md
|
- struct.md
|
||||||
- README.md # 站点首页
|
- README.md # 站点首页
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 通过`sidebar` 配置
|
### 通过`sidebar` 配置
|
||||||
|
|||||||
@ -8,11 +8,9 @@ tags:
|
|||||||
- 快速开始
|
- 快速开始
|
||||||
---
|
---
|
||||||
|
|
||||||
VuePress 完整支持 [标准 Markdown 语法](../markdown/basic.md),同时允许通过
|
VuePress 完整支持 [标准 Markdown 语法](../markdown/basic.md),同时允许通过 [YAML](https://dev.to/paulasantamaria/introduction-to-yaml-125f) 格式的 Frontmatter 定义页面元数据(如标题、创建时间等)。
|
||||||
[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 页面配置
|
## Frontmatter 页面配置
|
||||||
|
|
||||||
@ -132,14 +130,14 @@ const dir = /\d+\.[\s\S]+/
|
|||||||
::: file-tree
|
::: file-tree
|
||||||
|
|
||||||
- docs
|
- docs
|
||||||
- blog \# post 类型 collection
|
- blog # post 类型 collection
|
||||||
- 1.前端
|
- 1.前端
|
||||||
- 1.html/
|
- 1.html/
|
||||||
- 2.css/
|
- 2.css/
|
||||||
- 3.javascript/
|
- 3.javascript/
|
||||||
- 2.后端/
|
- 2.后端/
|
||||||
- 运维/
|
- 运维/
|
||||||
- typescript \# doc 类型 collection
|
- typescript # doc 类型 collection
|
||||||
- 1.基础
|
- 1.基础
|
||||||
- 1.变量.md
|
- 1.变量.md
|
||||||
- 2.类型.md
|
- 2.类型.md
|
||||||
|
|||||||
@ -369,8 +369,8 @@ const count = ref(0)
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
:::::: warning
|
:::::: warning
|
||||||
vue demo 容器语法虽然也支持 使用 `.js/ts + css` 的方式来嵌入演示代码,
|
vue demo 容器语法虽然也支持使用 `.js/ts + css` 的方式来嵌入演示代码,
|
||||||
但主题不推荐这样做。因为 样式无法被隔离,这可能导致样式污染。
|
但主题不推荐这样做。因为样式无法被隔离,这可能导致样式污染。
|
||||||
|
|
||||||
::::: details 参考示例
|
::::: details 参考示例
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,8 @@ search: false
|
|||||||
| M*e | 2020-12-26 | 10.00 | 最近使用主题弄了个博客,简洁好用。作者回复也快,因为还是学生也就只能微博的支持了 |
|
| M*e | 2020-12-26 | 10.00 | 最近使用主题弄了个博客,简洁好用。作者回复也快,因为还是学生也就只能微博的支持了 |
|
||||||
| *纪 | 2026-01-03 | 9.90 | 新年快乐(,,>‿<,,),感谢佬 |
|
| *纪 | 2026-01-03 | 9.90 | 新年快乐(,,>‿<,,),感谢佬 |
|
||||||
| J*n | 2026-01-22 | 10.00 | 用本开源主题搭了好几个网站了,作者耐心解答,添加合理功能需求,必须支持一下,辛苦了❤️ |
|
| J*n | 2026-01-22 | 10.00 | 用本开源主题搭了好几个网站了,作者耐心解答,添加合理功能需求,必须支持一下,辛苦了❤️ |
|
||||||
|
| *燧 | 2026-03-14 | 8.88 | 智齿主播,大佬加油 <br>(作者回复:啊?主播?我不是啊) |
|
||||||
|
| *飞 | 2026-04-25 | 9.90 | - |
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.base.json",
|
"extends": "../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/themes/*": ["./.vuepress/themes/*"],
|
"~/themes/*": ["./.vuepress/themes/*"],
|
||||||
"~/components/*": ["./.vuepress/themes/components/*"],
|
"~/components/*": ["./.vuepress/themes/components/*"],
|
||||||
|
|||||||
@ -7,8 +7,11 @@ export default config({
|
|||||||
},
|
},
|
||||||
ignores: [
|
ignores: [
|
||||||
'lib',
|
'lib',
|
||||||
|
'skills',
|
||||||
'docs/snippet/code-block.snippet.md',
|
'docs/snippet/code-block.snippet.md',
|
||||||
'docs/snippet/whitespace.snippet.md',
|
'docs/snippet/whitespace.snippet.md',
|
||||||
|
'docs/en/guide/markdown/obsidian.md',
|
||||||
|
'docs/guide/markdown/obsidian.md',
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
__VUEPRESS_VERSION__: 'readonly',
|
__VUEPRESS_VERSION__: 'readonly',
|
||||||
|
|||||||
26
package.json
26
package.json
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "vuepress-theme-plume-monorepo",
|
"name": "vuepress-theme-plume-monorepo",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0-rc.192",
|
"version": "1.0.0-rc.198",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.30.3",
|
"packageManager": "pnpm@10.33.2",
|
||||||
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@ -18,9 +18,9 @@
|
|||||||
"pnpm": ">=9"
|
"pnpm": ">=9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm clean && pnpm build:package",
|
"build": "pnpm run clean && pnpm build:package",
|
||||||
"build:package": "pnpm -r --stream build",
|
"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": "pnpm --stream '/(dev:package|docs:dev)/'",
|
||||||
"dev:package": "pnpm --parallel dev",
|
"dev:package": "pnpm --parallel dev",
|
||||||
"docs:dev": "wait-on -d 100 theme/lib/node/index.js && pnpm -F=docs docs: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}",
|
"lint:css": "stylelint **/*.{css,vue}",
|
||||||
"test": "cross-env TZ=Etc/UTC vitest --coverage",
|
"test": "cross-env TZ=Etc/UTC vitest --coverage",
|
||||||
"prepare": "husky",
|
"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:check": "pnpm lint && pnpm build",
|
||||||
"release:sync": "node scripts/mirror-sync.mjs",
|
"release:sync": "node scripts/mirror-sync.mjs",
|
||||||
"release:publish": "pnpm -r publish --tag latest",
|
"release:publish": "pnpm -r publish --tag latest",
|
||||||
@ -57,7 +57,8 @@
|
|||||||
"@vitest/coverage-v8": "catalog:dev",
|
"@vitest/coverage-v8": "catalog:dev",
|
||||||
"bumpp": "catalog:dev",
|
"bumpp": "catalog:dev",
|
||||||
"commitizen": "catalog:dev",
|
"commitizen": "catalog:dev",
|
||||||
"conventional-changelog-cli": "catalog:dev",
|
"conventional-changelog": "catalog:dev",
|
||||||
|
"conventional-changelog-angular": "catalog:dev",
|
||||||
"cpx2": "catalog:dev",
|
"cpx2": "catalog:dev",
|
||||||
"cross-env": "catalog:dev",
|
"cross-env": "catalog:dev",
|
||||||
"cz-conventional-changelog": "catalog:dev",
|
"cz-conventional-changelog": "catalog:dev",
|
||||||
@ -85,14 +86,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@bufbuild/protobuf": "^2.11.0",
|
"@bufbuild/protobuf": "^2.12.0",
|
||||||
"@eslint-community/eslint-utils": "catalog:peer",
|
"@eslint-community/eslint-utils": "catalog:peer",
|
||||||
|
"@shikijs/core": "^4.0.2",
|
||||||
|
"@shikijs/twoslash": "^4.0.2",
|
||||||
"@typescript-eslint/types": "catalog:peer",
|
"@typescript-eslint/types": "catalog:peer",
|
||||||
"@typescript-eslint/utils": "catalog:peer",
|
"@typescript-eslint/utils": "catalog:peer",
|
||||||
"baseline-browser-mapping": "^2.10.0",
|
"@xmldom/xmldom": ">=0.9.10",
|
||||||
|
"baseline-browser-mapping": "^2.10.22",
|
||||||
"chokidar": "catalog:prod",
|
"chokidar": "catalog:prod",
|
||||||
|
"dompurify": ">=3.4.1",
|
||||||
"esbuild": "catalog:prod",
|
"esbuild": "catalog:prod",
|
||||||
|
"follow-redirects": ">=1.16.0",
|
||||||
|
"lodash": ">=4.18.1",
|
||||||
|
"lodash-es": ">=4.18.1",
|
||||||
"sass-embedded": "catalog:peer",
|
"sass-embedded": "catalog:peer",
|
||||||
|
"shiki": "^4.0.2",
|
||||||
|
"tmp": ">=0.2.5",
|
||||||
"vite": "catalog:dev",
|
"vite": "catalog:dev",
|
||||||
"vue-router": "catalog:prod"
|
"vue-router": "catalog:prod"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@vuepress-plume/plugin-fonts",
|
"name": "@vuepress-plume/plugin-fonts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0-rc.192",
|
"version": "1.0.0-rc.198",
|
||||||
"description": "The Plugin for VuePress 2 - fonts",
|
"description": "The Plugin for VuePress 2 - fonts",
|
||||||
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"build": "pnpm run tsdown && pnpm run copy",
|
"build": "pnpm run tsdown && pnpm run copy",
|
||||||
"clean": "rimraf --glob ./lib",
|
"clean": "rimraf --glob ./lib",
|
||||||
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png,woff2}\" lib",
|
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png,woff2}\" lib",
|
||||||
"tsdown": "tsdown"
|
"tsdown": "tsdown --config-loader unrun"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vuepress": "catalog:vuepress"
|
"vuepress": "catalog:vuepress"
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
import { defineConfig, type UserConfig } from 'tsdown'
|
||||||
import { argv } from '../../scripts/tsdown-args.mjs'
|
import { argv } from '../../scripts/tsdown-args'
|
||||||
|
|
||||||
/** @import {Options} from 'tsdown' */
|
|
||||||
|
|
||||||
const clientExternal = [
|
const clientExternal = [
|
||||||
/.*\.vue$/,
|
/.*\.vue$/,
|
||||||
@ -9,15 +7,13 @@ const clientExternal = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(() => {
|
||||||
/** @type {Options} */
|
const DEFAULT_OPTIONS: UserConfig = {
|
||||||
const DEFAULT_OPTIONS = {
|
|
||||||
dts: true,
|
dts: true,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
fixedExtension: false,
|
fixedExtension: false,
|
||||||
}
|
}
|
||||||
/** @type {Options[]} */
|
const options: UserConfig[] = []
|
||||||
const options = []
|
|
||||||
|
|
||||||
if (argv.node) {
|
if (argv.node) {
|
||||||
options.push({
|
options.push({
|
||||||
@ -36,7 +32,7 @@ export default defineConfig(() => {
|
|||||||
entry: ['./src/client/config.ts'],
|
entry: ['./src/client/config.ts'],
|
||||||
outDir: './lib/client',
|
outDir: './lib/client',
|
||||||
dts: false,
|
dts: false,
|
||||||
external: clientExternal,
|
deps: { neverBundle: clientExternal },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ describe('fileTreePlugin', () => {
|
|||||||
- client
|
- client
|
||||||
- components
|
- components
|
||||||
- **Navbar.vue**
|
- **Navbar.vue**
|
||||||
- index.ts \# comment
|
- index.ts # comment
|
||||||
- node
|
- node
|
||||||
- index.ts
|
- index.ts
|
||||||
- .gitignore
|
- .gitignore
|
||||||
|
|||||||
917
plugins/plugin-md-power/__test__/obsidianCallouts.spec.ts
Normal file
917
plugins/plugin-md-power/__test__/obsidianCallouts.spec.ts
Normal file
@ -0,0 +1,917 @@
|
|||||||
|
import type { MarkdownEnv } from 'vuepress/markdown'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { calloutPlugin } from '../src/node/obsidian/callouts.js'
|
||||||
|
|
||||||
|
function createMockEnv(filePathRelative = 'test.md'): MarkdownEnv {
|
||||||
|
return {
|
||||||
|
filePathRelative,
|
||||||
|
base: '/',
|
||||||
|
links: [],
|
||||||
|
importedFiles: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMarkdown() {
|
||||||
|
return new MarkdownIt({ html: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('calloutPlugin', () => {
|
||||||
|
// ==================== Primary Callout Types ====================
|
||||||
|
|
||||||
|
describe('primary callout types', () => {
|
||||||
|
const types = ['note', 'tip', 'info', 'success', 'warning', 'caution', 'important', 'details']
|
||||||
|
|
||||||
|
types.forEach((type) => {
|
||||||
|
it(`should render ${type} callout`, () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
// Callout format: >[!type] title on same line, content on continuation lines with >
|
||||||
|
const result = md.render(`>[!${type}]\n>\n> Content here.`)
|
||||||
|
|
||||||
|
expect(result).toContain(`hint-container ${type}`)
|
||||||
|
expect(result).toContain('Content here')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render note with quote and cite aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
|
||||||
|
const quoteResult = md.render('>[!quote]\n>\n> Content.')
|
||||||
|
expect(quoteResult).toContain('hint-container note')
|
||||||
|
|
||||||
|
const citeResult = md.render('>[!cite]\n>\n> Content.')
|
||||||
|
expect(citeResult).toContain('hint-container note')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render tip with hint alias', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!hint]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render info with todo alias', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!todo]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container info')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render success with check and done aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
|
||||||
|
const checkResult = md.render('>[!check]\n>\n> Content.')
|
||||||
|
expect(checkResult).toContain('hint-container success')
|
||||||
|
|
||||||
|
const doneResult = md.render('>[!done]\n>\n> Content.')
|
||||||
|
expect(doneResult).toContain('hint-container success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render warning with question, help, and faq aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
|
||||||
|
const questionResult = md.render('>[!question]\n>\n> Content.')
|
||||||
|
expect(questionResult).toContain('hint-container warning')
|
||||||
|
|
||||||
|
const helpResult = md.render('>[!help]\n>\n> Content.')
|
||||||
|
expect(helpResult).toContain('hint-container warning')
|
||||||
|
|
||||||
|
const faqResult = md.render('>[!faq]\n>\n> Content.')
|
||||||
|
expect(faqResult).toContain('hint-container warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render caution with multiple aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const aliases = ['attention', 'failure', 'fail', 'missing', 'danger', 'error', 'bug']
|
||||||
|
|
||||||
|
aliases.forEach((alias) => {
|
||||||
|
const result = md.render(`>[!${alias}]\n>\n> Content.`)
|
||||||
|
expect(result).toContain('hint-container caution')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render important with example alias', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!example]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container important')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render details with abstract, summary, and tldr aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
|
||||||
|
const abstractResult = md.render('>[!abstract]\n>\n> Content.')
|
||||||
|
expect(abstractResult).toContain('hint-container details')
|
||||||
|
|
||||||
|
const summaryResult = md.render('>[!summary]\n>\n> Content.')
|
||||||
|
expect(summaryResult).toContain('hint-container details')
|
||||||
|
|
||||||
|
const tldrResult = md.render('>[!tldr]\n>\n> Content.')
|
||||||
|
expect(tldrResult).toContain('hint-container details')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Case Insensitivity ====================
|
||||||
|
|
||||||
|
describe('case insensitivity', () => {
|
||||||
|
it('should handle uppercase type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!TIP]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mixed case type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!Tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle lowercase type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Title Handling ====================
|
||||||
|
|
||||||
|
describe('title handling', () => {
|
||||||
|
it('should render custom title text', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] Custom Title\n>\n> Content.')
|
||||||
|
expect(result).toContain('Custom Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render title with + prefix', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] + Custom Title\n>\n> Content.')
|
||||||
|
expect(result).toContain('Custom Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render title with - prefix', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] - Custom Title\n>\n> Content.')
|
||||||
|
expect(result).toContain('Custom Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default capitalized type when no title', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('Tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render empty title with default', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] \n>\n> Content.')
|
||||||
|
expect(result).toContain('Tip')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Content Rendering ====================
|
||||||
|
|
||||||
|
describe('content rendering', () => {
|
||||||
|
it('should render single line content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Single line content.')
|
||||||
|
expect(result).toContain('Single line content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render multi-line content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> First paragraph.
|
||||||
|
>
|
||||||
|
> Second paragraph.`)
|
||||||
|
expect(result).toContain('First paragraph')
|
||||||
|
expect(result).toContain('Second paragraph')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render nested list within callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> - Item 1
|
||||||
|
> - Item 2
|
||||||
|
> - Item 3`)
|
||||||
|
expect(result).toContain('Item 1')
|
||||||
|
expect(result).toContain('Item 2')
|
||||||
|
expect(result).toContain('Item 3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render heading within callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> ### Nested Heading
|
||||||
|
>
|
||||||
|
> Content after heading.`)
|
||||||
|
expect(result).toContain('Nested Heading')
|
||||||
|
expect(result).toContain('Content after heading')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render code block within callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> \`\`\`js
|
||||||
|
> const x = 1;
|
||||||
|
> \`\`\``)
|
||||||
|
expect(result).toContain('const x = 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse content after callout correctly', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Callout content.
|
||||||
|
>
|
||||||
|
After callout paragraph.
|
||||||
|
|
||||||
|
## Heading after
|
||||||
|
|
||||||
|
More content.`)
|
||||||
|
|
||||||
|
expect(result).toContain('Callout content')
|
||||||
|
expect(result).toContain('After callout paragraph')
|
||||||
|
expect(result).toContain('Heading after')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Syntax Variations ====================
|
||||||
|
|
||||||
|
describe('syntax variations', () => {
|
||||||
|
it('should parse without space after >', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse with space after >', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('> [!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse with multiple spaces', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('> [!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse with tab after >', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>\t[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle tab with space alignment', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(' >\t [!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Block Parsing Edge Cases ====================
|
||||||
|
|
||||||
|
describe('block parsing edge cases', () => {
|
||||||
|
it('should terminate on empty line outside callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
>
|
||||||
|
> More content.
|
||||||
|
|
||||||
|
After callout.`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('More content')
|
||||||
|
expect(result).toContain('After callout')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate on outdented content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
outdented line`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('outdented line')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle outdented line as block terminator (line 265)', () => {
|
||||||
|
// When the content line is outdented (sCount < blkIndent), the callout ends
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. List item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
|
||||||
|
2. Next item`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('Next item')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate on horizontal rule', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
// Using *** instead of - - - to ensure it's recognized as horizontal rule
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
>
|
||||||
|
> ***`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate when terminator rule matches (lines 280-281)', () => {
|
||||||
|
// The terminator rule for blockquote will match when the callout is properly terminated
|
||||||
|
// by another blockquote-like structure
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
// After the horizontal rule, the content below should be separate
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
After horizontal rule.`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('After horizontal rule')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle list terminator correctly (lines 280-281)', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Callout content.
|
||||||
|
>
|
||||||
|
> 1. Ordered list inside`)
|
||||||
|
|
||||||
|
expect(result).toContain('Callout content')
|
||||||
|
expect(result).toContain('Ordered list inside')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate on list item', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content.
|
||||||
|
>
|
||||||
|
> 1. Ordered item`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle continuation after blockquote in list', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. List item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Content in callout.
|
||||||
|
>
|
||||||
|
> More content.
|
||||||
|
|
||||||
|
2. Next list item`)
|
||||||
|
|
||||||
|
expect(result).toContain('Content in callout')
|
||||||
|
expect(result).toContain('More content')
|
||||||
|
expect(result).toContain('Next list item')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should restore state correctly when blkIndent !== 0 (lines 290-304)', () => {
|
||||||
|
// When callout is in a list item (non-zero blkIndent) and is terminated
|
||||||
|
// by another block, the blkIndent adjustment should be restored
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. List item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Callout content.
|
||||||
|
>
|
||||||
|
> ---`)
|
||||||
|
|
||||||
|
expect(result).toContain('Callout content')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle nested blockquote in callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> > Nested quote
|
||||||
|
>
|
||||||
|
> Content after nested.`)
|
||||||
|
expect(result).toContain('Nested quote')
|
||||||
|
expect(result).toContain('Content after nested')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle callout with proper terminator restoration', () => {
|
||||||
|
// Test for lines 290-304: blkIndent restoration when terminated by other block
|
||||||
|
// This requires the callout to be inside a list with non-zero blkIndent
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. List item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Callout content.
|
||||||
|
>
|
||||||
|
> More content.
|
||||||
|
>
|
||||||
|
> - - -
|
||||||
|
|
||||||
|
2. Next item`)
|
||||||
|
|
||||||
|
expect(result).toContain('Callout content')
|
||||||
|
expect(result).toContain('More content')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Next item')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Invalid Syntax ====================
|
||||||
|
|
||||||
|
describe('invalid syntax', () => {
|
||||||
|
it('should not parse unknown type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!unknown]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse empty type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse incomplete syntax without closing bracket', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse without opening bracket', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>!tip]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse without > marker', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('[!tip]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse when indented more than 3 spaces (becomes code)', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(' >[!tip]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
expect(result).toContain('<code')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when sCount - blkIndent >= 4 (line 44)', () => {
|
||||||
|
// Line 44: if sCount - blkIndent >= 4, return false
|
||||||
|
// This would happen when a line is deeply indented beyond the block indent
|
||||||
|
// In practice, blkIndent tracks sCount in list contexts, making this hard to trigger
|
||||||
|
// We test with a deeply indented block that exceeds normal block processing
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
// This scenario exercises the code path even if the exact condition is hard to isolate
|
||||||
|
const result = md.render('> [!tip]\n>\n> Content.')
|
||||||
|
// With 5 spaces after >, offset-initial=4 triggers line 96 first
|
||||||
|
// But the overall block parsing exercises related code paths
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse type only without brackets', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>tip]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse empty callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse callout with only empty continuation lines', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
>`)
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when offset - initial >= 4 (line 96)', () => {
|
||||||
|
// Line 96: offset - initial >= 4 means 4+ spaces after > before the callout type
|
||||||
|
// > [!tip] has 5 spaces after >, so offset - initial = 4 >= 4, returns false
|
||||||
|
// This causes it to be treated as a code block within blockquote
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('> [!tip]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
// It should be treated as blockquote with code-like content
|
||||||
|
expect(result).toContain('<code')
|
||||||
|
expect(result).toContain('[!tip]')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Special Type: details ====================
|
||||||
|
|
||||||
|
describe('details type rendering', () => {
|
||||||
|
it('should render details as details element', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!details]\n>\n> Content.')
|
||||||
|
expect(result).toContain('<details')
|
||||||
|
expect(result).toContain('</details>')
|
||||||
|
expect(result).toContain('<summary')
|
||||||
|
expect(result).toContain('</summary>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not use div for details type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!details]\n>\n> Content.')
|
||||||
|
expect(result).not.toContain('<div class="hint-container details"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render summary tag for details title', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!details] Summary Title\n>\n> Content.')
|
||||||
|
expect(result).toContain('<summary')
|
||||||
|
expect(result).toContain('Summary Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render details with other aliases', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!abstract]\n>\n> Content.')
|
||||||
|
expect(result).toContain('<details')
|
||||||
|
expect(result).toContain('<summary')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Default Rendering Structure ====================
|
||||||
|
|
||||||
|
describe('rendering structure', () => {
|
||||||
|
it('should render alert_open with correct class', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render alert_title with correct class', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] Title\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container-title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render opening and closing tags', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('<div')
|
||||||
|
expect(result).toContain('</div>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render hint-container class', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('hint-container')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Locale Support ====================
|
||||||
|
|
||||||
|
describe('locale support', () => {
|
||||||
|
it('should use default type name when no locale match', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin, {
|
||||||
|
locales: {},
|
||||||
|
})
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
expect(result).toContain('Tip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use custom locale title when provided', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin, {
|
||||||
|
locales: {
|
||||||
|
'/': {
|
||||||
|
tip: 'Custom Tip Title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.', createMockEnv('/'))
|
||||||
|
expect(result).toContain('Custom Tip Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use locale for specific path', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin, {
|
||||||
|
locales: {
|
||||||
|
'/zh/': {
|
||||||
|
tip: '提示',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.', createMockEnv('zh/guide.md'))
|
||||||
|
expect(result).toContain('提示')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefer custom locale over default', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin, {
|
||||||
|
locales: {
|
||||||
|
'/': {
|
||||||
|
tip: 'Default Tip',
|
||||||
|
},
|
||||||
|
'/zh/': {
|
||||||
|
tip: '中文提示',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultResult = md.render('>[!tip]\n>\n> Content.', createMockEnv('guide.md'))
|
||||||
|
expect(defaultResult).toContain('Default Tip')
|
||||||
|
|
||||||
|
const zhResult = md.render('>[!tip]\n>\n> Content.', createMockEnv('zh/guide.md'))
|
||||||
|
expect(zhResult).toContain('中文提示')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle locale without matching type', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin, {
|
||||||
|
locales: {
|
||||||
|
'/': {
|
||||||
|
note: 'Note Title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = md.render('>[!tip]\n>\n> Content.')
|
||||||
|
// Should still use capitalized type as fallback
|
||||||
|
expect(result).toContain('Tip')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Edge Cases ====================
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle callout at beginning of document', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> First content.
|
||||||
|
|
||||||
|
Second paragraph.`)
|
||||||
|
expect(result).toContain('First content')
|
||||||
|
expect(result).toContain('Second paragraph')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle multiple callouts in sequence', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Tip content.
|
||||||
|
|
||||||
|
>[!warning]
|
||||||
|
>
|
||||||
|
> Warning content.
|
||||||
|
|
||||||
|
>[!note]
|
||||||
|
>
|
||||||
|
> Note content.`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Tip content')
|
||||||
|
expect(result).toContain('hint-container warning')
|
||||||
|
expect(result).toContain('Warning content')
|
||||||
|
expect(result).toContain('hint-container note')
|
||||||
|
expect(result).toContain('Note content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty lines within callout', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Line 1.
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> Line 2.`)
|
||||||
|
|
||||||
|
expect(result).toContain('Line 1')
|
||||||
|
expect(result).toContain('Line 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle adjacent callouts without blank line', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> First.
|
||||||
|
>[!note]
|
||||||
|
>
|
||||||
|
> Second.`)
|
||||||
|
|
||||||
|
expect(result).toContain('First')
|
||||||
|
expect(result).toContain('Second')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not interfere with regular blockquotes', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`> Regular blockquote
|
||||||
|
>
|
||||||
|
> Another line.`)
|
||||||
|
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
expect(result).toContain('Regular blockquote')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle indented callout in list', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`- List item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Indented callout.`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Indented callout')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle unicode in title', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] 中文标题\n>\n> Content.')
|
||||||
|
expect(result).toContain('中文标题')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle emoji in title', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip] 🚀 Launch\n>\n> Content.')
|
||||||
|
expect(result).toContain('🚀')
|
||||||
|
expect(result).toContain('Launch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Inline Content Rendering ====================
|
||||||
|
|
||||||
|
describe('inline content rendering', () => {
|
||||||
|
it('should render inline code in content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Use `code` inline.')
|
||||||
|
expect(result).toContain('<code>code</code>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render links in content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> Check [this](https://example.com).')
|
||||||
|
expect(result).toContain('<a href="https://example.com"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render emphasis in content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> This is *italic* and **bold**.')
|
||||||
|
expect(result).toContain('<em>italic</em>')
|
||||||
|
expect(result).toContain('<strong>bold</strong>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render strikethrough in content', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render('>[!tip]\n>\n> ~~Deleted~~ text.')
|
||||||
|
expect(result).toContain('<s>Deleted</s>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Code Block Type Detection ====================
|
||||||
|
|
||||||
|
describe('code block type detection', () => {
|
||||||
|
it('should detect as code block when indented 4+ spaces', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(` >[!tip] Title
|
||||||
|
>
|
||||||
|
> Content.`)
|
||||||
|
|
||||||
|
expect(result).toContain('<code')
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sCount - blkIndent >= 4', () => {
|
||||||
|
it('should return false when deeply indented and code rule is disabled', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
md.block.ruler.disable('code')
|
||||||
|
const result = md.render(' >[!tip]\n>\n> Content.')
|
||||||
|
|
||||||
|
expect(result).not.toContain('hint-container')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when deeply indented inside list with code rule disabled', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
md.block.ruler.disable('code')
|
||||||
|
const result = md.render(`- Item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Content`)
|
||||||
|
|
||||||
|
expect(result).not.toContain('hint-container tip')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isOutdented break inside list', () => {
|
||||||
|
it('should break when body line is outdented from list context', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`- Item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
Outdented line`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('Outdented line')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should break when callout body reaches line outside list indent', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. Item
|
||||||
|
>[!warning]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
Outside list`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container warning')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('Outside list')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('terminator rule matches', () => {
|
||||||
|
it('should terminate callout when horizontal rule follows without blank line', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
---`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('<hr')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate callout when ATX heading follows without blank line', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
## Heading`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('Heading')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should terminate callout when fence block follows without blank line', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`>[!tip]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
\`\`\`js
|
||||||
|
code
|
||||||
|
\`\`\``)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
expect(result).toContain('<code')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('blkIndent !== 0 when terminated', () => {
|
||||||
|
it('should adjust sCount when callout in list is terminated by hr', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`- Item
|
||||||
|
>[!tip]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
---`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container tip')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should adjust sCount when callout in ordered list is terminated by hr', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`1. Item
|
||||||
|
>[!caution]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
---`)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container caution')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle callout in list terminated by fence', () => {
|
||||||
|
const md = createMarkdown().use(calloutPlugin)
|
||||||
|
const result = md.render(`- Item
|
||||||
|
>[!note]
|
||||||
|
>
|
||||||
|
> Content
|
||||||
|
\`\`\`
|
||||||
|
code
|
||||||
|
\`\`\``)
|
||||||
|
|
||||||
|
expect(result).toContain('hint-container note')
|
||||||
|
expect(result).toContain('Content')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
752
plugins/plugin-md-power/__test__/obsidianEmbedLink.spec.ts
Normal file
752
plugins/plugin-md-power/__test__/obsidianEmbedLink.spec.ts
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
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(/^\//, '')),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@vuepress/shared', () => ({
|
||||||
|
ensureLeadingSlash: vi.fn((p: string) => (p[0] === '/' ? p : `/${p}`)),
|
||||||
|
isLinkHttp: vi.fn((p: string) => p.startsWith('http://') || p.startsWith('https://')),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../src/node/enhance/links.js', () => ({
|
||||||
|
resolvePaths: vi.fn((rawPath: string) => ({
|
||||||
|
absolutePath: `/${rawPath}`,
|
||||||
|
relativePath: rawPath,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../src/node/utils/slugify.js', () => ({
|
||||||
|
slugify: vi.fn((s: string) => s.toLowerCase().replace(/\s+/g, '-')),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../src/node/utils/cleanMarkdownEnv.js', () => ({
|
||||||
|
cleanMarkdownEnv: vi.fn((env: MarkdownEnv) => env),
|
||||||
|
}))
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return empty string when file has only frontmatter', () => {
|
||||||
|
// gray-matter will extract empty content when file has only frontmatter
|
||||||
|
mockGlobSync.mockReturnValue(['empty.md'])
|
||||||
|
mockReadFileSync.mockReturnValue(`---
|
||||||
|
title: Empty
|
||||||
|
---
|
||||||
|
`)
|
||||||
|
|
||||||
|
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('![[empty]]', env)
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('is empty'))
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset search when encountering same-level heading with different text', () => {
|
||||||
|
const content = `# A
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
B content.
|
||||||
|
|
||||||
|
## C
|
||||||
|
|
||||||
|
C content.`
|
||||||
|
|
||||||
|
mockGlobSync.mockReturnValue(['guide.md'])
|
||||||
|
mockReadFileSync.mockReturnValue(content)
|
||||||
|
|
||||||
|
const app = createMockApp()
|
||||||
|
initPagePaths(app)
|
||||||
|
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
// Searching for A > B > C, but C is at same level as B, not nested under it
|
||||||
|
const result = md.render('![[guide#A#B#C]]', env)
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset headingPointer when first heading reappears at shallower level', () => {
|
||||||
|
// Structure: # A, ## B, ## A
|
||||||
|
// When searching for A > B > A, after finding A at level 1 and B at level 2,
|
||||||
|
// we encounter A again at level 2 which is <= currentLevel and matches headings[0]
|
||||||
|
const content = `# A
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
B content.
|
||||||
|
|
||||||
|
## A
|
||||||
|
|
||||||
|
A content again.`
|
||||||
|
|
||||||
|
mockGlobSync.mockReturnValue(['guide.md'])
|
||||||
|
mockReadFileSync.mockReturnValue(content)
|
||||||
|
|
||||||
|
const app = createMockApp()
|
||||||
|
initPagePaths(app)
|
||||||
|
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
// Searching for A > B > A
|
||||||
|
// After finding A (level 1) and B (level 2), we find A at level 2
|
||||||
|
// level 2 <= currentLevel 2 is true, and A === headings[0], so we reset
|
||||||
|
const result = md.render('![[guide#A#B#A]]', env)
|
||||||
|
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 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('![[]]')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Inline Embed Link ====================
|
||||||
|
|
||||||
|
describe('inline embed link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGlobSync.mockReturnValue([])
|
||||||
|
const app = createMockApp()
|
||||||
|
initPagePaths(app)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline image embed within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Here is an image ![[photo.png]] in text.')
|
||||||
|
expect(result).toContain('<img')
|
||||||
|
expect(result).toContain('src="/photo.png"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline audio embed within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Listen to ![[music.mp3]] this.')
|
||||||
|
expect(result).toContain('<audio')
|
||||||
|
expect(result).toContain('<source')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline video embed within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Watch ![[clip.mp4]] this video.')
|
||||||
|
expect(result).toContain('<ArtPlayer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline pdf embed within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('See ![[doc.pdf]] for details.')
|
||||||
|
expect(result).toContain('<PDFViewer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline external http link within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Check ![[https://example.com/link]] out.')
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
expect(result).toContain('href="https://example.com/link"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse inline embed with settings within text', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Image ![[photo.png|400x300]] here.')
|
||||||
|
expect(result).toContain('<img')
|
||||||
|
expect(result).toContain('width: 400px')
|
||||||
|
expect(result).toContain('height: 300px')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not parse inline embed without closing', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Here ![[image.png] is text.')
|
||||||
|
expect(result).toContain('![[image.png]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle inline embed with empty content', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('Here ![[]] is text.')
|
||||||
|
// Empty embed is parsed and rendered as an external link
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
expect(result).toContain('href="/"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Inline Markdown Page Embed ====================
|
||||||
|
|
||||||
|
describe('inline markdown page embed (VPLink)', () => {
|
||||||
|
const guideContent = `---
|
||||||
|
title: Guide
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
This is intro content.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Steps for getting started.
|
||||||
|
`
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGlobSync.mockReturnValue(['guide.md'])
|
||||||
|
mockReadFileSync.mockReturnValue(guideContent)
|
||||||
|
|
||||||
|
const app = createMockApp()
|
||||||
|
initPagePaths(app)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline markdown page embed as VPLink', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('See ![[guide]] for details.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<VPLink')
|
||||||
|
expect(result).toContain('href="/guide.md"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline markdown page embed with anchor as VPLink', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('See ![[guide#Introduction]] for details.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<VPLink')
|
||||||
|
expect(result).toContain('href="/guide.md#introduction"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline markdown page embed with settings as VPLink', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('See ![[guide|Custom Text]] for details.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<VPLink')
|
||||||
|
expect(result).toContain('Custom Text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline markdown page embed with hashes as VPLink using after-text template', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('See ![[guide#Introduction#Getting Started]] for details.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<VPLink')
|
||||||
|
// Anchor slug is appended to href, after-text shows the full path hierarchy
|
||||||
|
expect(result).toContain('href="/guide.md#getting-started"')
|
||||||
|
expect(result).toContain('template #after-text')
|
||||||
|
expect(result).toContain('> Introduction > Getting Started')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should track links in env when rendering inline page embed', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
md.render('See ![[guide]] for details.', env)
|
||||||
|
|
||||||
|
expect(env.links).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
raw: 'guide.md',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline embed with relative path as external link when not found', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv('docs/page.md')
|
||||||
|
|
||||||
|
const result = md.render('See ![[./guide]] for details.', env)
|
||||||
|
|
||||||
|
// ./guide doesn't resolve to a page in findFirstPage, so renders as external link
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
expect(result).toContain('href="/docs/./guide"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline nonexistent page embed as external link', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('See ![[nonexistent]] for details.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
expect(result).toContain('href="/nonexistent"')
|
||||||
|
expect(result).toContain('target="_blank"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Inline External Resource with Anchor ====================
|
||||||
|
|
||||||
|
describe('inline external resource with anchor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGlobSync.mockReturnValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render inline external link with anchor', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('Check ![[https://example.com/page#section]] out.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
// The #section anchor is parsed but since the URL doesn't have a recognized extension,
|
||||||
|
// it's treated as an external link without proper anchor handling
|
||||||
|
expect(result).toContain('href="https://example.com/page"')
|
||||||
|
expect(result).toContain('target="_blank"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render external markdown file with heading anchor', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const env = createMockEnv()
|
||||||
|
|
||||||
|
const result = md.render('Check ![[https://example.com/doc.md#intro]] out.', env)
|
||||||
|
|
||||||
|
expect(result).toContain('<a')
|
||||||
|
// URLs with anchors are not properly handled in current implementation
|
||||||
|
expect(result).toContain('href="https://example.com/doc.md"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== Block Embed with Inline-like Content ====================
|
||||||
|
|
||||||
|
describe('block embed with various content', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGlobSync.mockReturnValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle block embed of image with pipe settings', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('![[banner.jpg|800x200]]')
|
||||||
|
expect(result).toContain('<img')
|
||||||
|
expect(result).toContain('width: 800px')
|
||||||
|
expect(result).toContain('height: 200px')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle block embed of audio with controls', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('![[narration.mp3]]')
|
||||||
|
expect(result).toContain('<audio')
|
||||||
|
expect(result).toContain('controls="true"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle block embed of video with all attributes', () => {
|
||||||
|
const md = createMarkdownWithMockRules().use(embedLinkPlugin, createMockApp())
|
||||||
|
const result = md.render('![[presentation.mp4]]')
|
||||||
|
expect(result).toContain('<ArtPlayer')
|
||||||
|
expect(result).toContain(':fullscreen="true"')
|
||||||
|
expect(result).toContain(':flip="true"')
|
||||||
|
expect(result).toContain(':playback-rate="true"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
297
plugins/plugin-md-power/__test__/obsidianExtractContent.spec.ts
Normal file
297
plugins/plugin-md-power/__test__/obsidianExtractContent.spec.ts
Normal 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.')
|
||||||
|
})
|
||||||
|
})
|
||||||
156
plugins/plugin-md-power/__test__/obsidianFindFirstPage.spec.ts
Normal file
156
plugins/plugin-md-power/__test__/obsidianFindFirstPage.spec.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
85
plugins/plugin-md-power/__test__/obsidianPlugin.spec.ts
Normal file
85
plugins/plugin-md-power/__test__/obsidianPlugin.spec.ts
Normal 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%%')
|
||||||
|
})
|
||||||
|
})
|
||||||
268
plugins/plugin-md-power/__test__/obsidianWikiLink.spec.ts
Normal file
268
plugins/plugin-md-power/__test__/obsidianWikiLink.spec.ts
Normal 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 > 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('> anchor1 > 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<')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vuepress-plugin-md-power",
|
"name": "vuepress-plugin-md-power",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0-rc.192",
|
"version": "1.0.0-rc.198",
|
||||||
"description": "The Plugin for VuePress 2 - markdown power",
|
"description": "The Plugin for VuePress 2 - markdown power",
|
||||||
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -36,8 +36,8 @@
|
|||||||
"clean": "rimraf --glob ./lib",
|
"clean": "rimraf --glob ./lib",
|
||||||
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" 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",
|
"copy:watch": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib -w",
|
||||||
"tsdown": "tsdown",
|
"tsdown": "tsdown --config-loader unrun",
|
||||||
"tsdown:watch": "tsdown --watch -- -c"
|
"tsdown:watch": "tsdown --config-loader unrun --watch -- -c"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"artplayer": "catalog:peer",
|
"artplayer": "catalog:peer",
|
||||||
@ -97,6 +97,7 @@
|
|||||||
"@vuepress/helper": "catalog:vuepress",
|
"@vuepress/helper": "catalog:vuepress",
|
||||||
"@vueuse/core": "catalog:prod",
|
"@vueuse/core": "catalog:prod",
|
||||||
"chokidar": "catalog:prod",
|
"chokidar": "catalog:prod",
|
||||||
|
"gray-matter": "catalog:prod",
|
||||||
"image-size": "catalog:prod",
|
"image-size": "catalog:prod",
|
||||||
"local-pkg": "catalog:prod",
|
"local-pkg": "catalog:prod",
|
||||||
"lru-cache": "catalog:prod",
|
"lru-cache": "catalog:prod",
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { QRCodeToDataURLOptions, QRCodeToStringOptions } from 'qrcode'
|
|
||||||
import type { QRCodeProps } from '../../shared/index.js'
|
import type { QRCodeProps } from '../../shared/index.js'
|
||||||
import { isLinkWithProtocol } from '@vuepress/helper/client'
|
import { isLinkWithProtocol } from '@vuepress/helper/client'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { resolveRoute, usePage, withBase } from 'vuepress/client'
|
import { resolveRoute, usePage, withBase } from 'vuepress/client'
|
||||||
|
import { attemptLoadLogo, generateQRCode } from '../composables/qrcode.js'
|
||||||
|
|
||||||
const { title, text, mode, align = 'left', reverse = false, svg = false, width, level, version, mask, margin = 2, scale = 4, light, dark } = defineProps<QRCodeProps>()
|
const { title, text, mode, align = 'left', reverse = false, width, level, version, mask, margin = 2, scale = 4, light, dark, logo, logoSize = '0.2' } = defineProps<QRCodeProps>()
|
||||||
|
|
||||||
const page = usePage()
|
const page = usePage()
|
||||||
|
|
||||||
let qr: typeof import('qrcode') | null = null
|
|
||||||
|
|
||||||
const qrcode = ref('')
|
const qrcode = ref('')
|
||||||
const parsedText = ref('')
|
const parsedText = ref('')
|
||||||
|
const imgWidth = ref(300)
|
||||||
const isLink = ref(false)
|
const isLink = ref(false)
|
||||||
|
const isInternalLink = ref(false)
|
||||||
|
|
||||||
const styles = computed(() => {
|
const styles = computed(() => {
|
||||||
const size = typeof width === 'number' ? width : width ? Number.parseInt(width) : undefined
|
const size = typeof width === 'number' ? width : width ? Number.parseInt(width) : undefined
|
||||||
@ -26,6 +26,7 @@ function parseText(): string | void {
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
if (text === '.') {
|
if (text === '.') {
|
||||||
|
isInternalLink.value = true
|
||||||
isLink.value = true
|
isLink.value = true
|
||||||
return location.href.split(/[?#]/)[0]
|
return location.href.split(/[?#]/)[0]
|
||||||
}
|
}
|
||||||
@ -42,6 +43,7 @@ function parseText(): string | void {
|
|||||||
if (notFound) {
|
if (notFound) {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
isInternalLink.value = true
|
||||||
isLink.value = true
|
isLink.value = true
|
||||||
return new URL(`${withBase(path)}${rest.join('')}`, location.href).toString()
|
return new URL(`${withBase(path)}${rest.join('')}`, location.href).toString()
|
||||||
}
|
}
|
||||||
@ -50,10 +52,8 @@ function parseText(): string | void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const callback = (_: any, url: string) => qrcode.value = url
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [text, svg, level, version, mask, margin, scale, light, dark],
|
() => [text, level, version, mask, margin, scale, light, dark, logo, logoSize],
|
||||||
async () => {
|
async () => {
|
||||||
const text = parseText()
|
const text = parseText()
|
||||||
parsedText.value = text || ''
|
parsedText.value = text || ''
|
||||||
@ -62,36 +62,39 @@ onMounted(async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
qr ??= (await import(/* webpackChunkName: "qrcode" */ 'qrcode')).default
|
imgWidth.value = 300 * Math.round(window.devicePixelRatio || 1)
|
||||||
const opts: QRCodeToDataURLOptions & QRCodeToStringOptions = {
|
qrcode.value = await generateQRCode(
|
||||||
version,
|
{
|
||||||
maskPattern: mask,
|
text,
|
||||||
errorCorrectionLevel: (level ? level.toUpperCase() : 'M') as any,
|
logo: await attemptLoadLogo(text, logo, isInternalLink.value),
|
||||||
width: 300 * Math.round(window.devicePixelRatio || 1),
|
logoSize,
|
||||||
margin,
|
},
|
||||||
scale,
|
{
|
||||||
color: { dark, light },
|
version,
|
||||||
}
|
maskPattern: mask,
|
||||||
|
width: imgWidth.value,
|
||||||
if (svg)
|
margin,
|
||||||
qr.toString(text, { type: 'svg', ...opts }, callback)
|
scale,
|
||||||
else
|
color: { dark, light },
|
||||||
qr.toDataURL(text, { type: 'image/png', ...opts }, callback)
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
qr = null
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="qrcode" class="vp-qrcode" :class="{ card: mode === 'card', reverse, [align]: true }">
|
<div v-if="qrcode" class="vp-qrcode" :class="{ card: mode === 'card', reverse, [align]: true }">
|
||||||
<div class="qrcode-content">
|
<div class="qrcode-content">
|
||||||
<div v-if="svg" class="qrcode-svg" :style="styles" :title="parsedText" v-html="qrcode" />
|
<img
|
||||||
<img v-else class="qrcode-img" :src="qrcode" :alt="parsedText" :title="parsedText" :style="styles">
|
class="qrcode-img"
|
||||||
|
:src="qrcode"
|
||||||
|
:alt="parsedText"
|
||||||
|
:title="parsedText"
|
||||||
|
:style="styles"
|
||||||
|
:width="imgWidth" :height="imgWidth"
|
||||||
|
>
|
||||||
<div v-if="title && mode !== 'card'" class="qrcode-label">
|
<div v-if="title && mode !== 'card'" class="qrcode-label">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
@ -166,7 +169,6 @@ onUnmounted(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vp-qrcode .qrcode-svg,
|
|
||||||
.vp-qrcode .qrcode-img {
|
.vp-qrcode .qrcode-img {
|
||||||
width: var(--vp-qrcode-size);
|
width: var(--vp-qrcode-size);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -174,11 +176,6 @@ onUnmounted(() => {
|
|||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vp-qrcode .qrcode-svg :deep(svg) {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vp-qrcode .qrcode-info {
|
.vp-qrcode .qrcode-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -110,6 +110,7 @@ function onCopy(type: 'html' | 'md') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vp-table .table-container table {
|
.vp-table .table-container table {
|
||||||
|
display: table;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,18 +5,63 @@ import { http } from '../utils/http.js'
|
|||||||
import { sleep } from '../utils/sleep.js'
|
import { sleep } from '../utils/sleep.js'
|
||||||
import { rustExecute } from './rustRepl.js'
|
import { rustExecute } from './rustRepl.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSS selectors for nodes to ignore when extracting code.
|
||||||
|
*
|
||||||
|
* 提取代码时要忽略的节点的 CSS 选择器。
|
||||||
|
*/
|
||||||
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
|
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching language class.
|
||||||
|
*
|
||||||
|
* 匹配语言类的正则表达式。
|
||||||
|
*/
|
||||||
const RE_LANGUAGE = /language-(\w+)/
|
const RE_LANGUAGE = /language-(\w+)/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API endpoints for code execution backends.
|
||||||
|
*
|
||||||
|
* 代码执行后端的 API 端点。
|
||||||
|
*/
|
||||||
const api = {
|
const api = {
|
||||||
go: 'https://api.pengzhanbo.cn/repl/golang/run',
|
go: 'https://api.pengzhanbo.cn/repl/golang/run',
|
||||||
kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run',
|
kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pyodide instance for Python execution.
|
||||||
|
*
|
||||||
|
* 用于 Python 执行的 Pyodide 实例。
|
||||||
|
*/
|
||||||
let pyodide: PyodideInterface | null = null
|
let pyodide: PyodideInterface | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported languages for code execution.
|
||||||
|
*
|
||||||
|
* 支持代码执行的语言。
|
||||||
|
*/
|
||||||
type Lang = 'kotlin' | 'go' | 'rust' | 'python'
|
type Lang = 'kotlin' | 'go' | 'rust' | 'python'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function type for code execution.
|
||||||
|
*
|
||||||
|
* 代码执行的函数类型。
|
||||||
|
*/
|
||||||
type ExecuteFn = (code: string) => Promise<any>
|
type ExecuteFn = (code: string) => Promise<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of language to execution function.
|
||||||
|
*
|
||||||
|
* 语言到执行函数的映射。
|
||||||
|
*/
|
||||||
type ExecuteMap = Record<Lang, ExecuteFn>
|
type ExecuteMap = Record<Lang, ExecuteFn>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language alias mapping.
|
||||||
|
*
|
||||||
|
* 语言别名映射。
|
||||||
|
*/
|
||||||
const langAlias: Record<string, string> = {
|
const langAlias: Record<string, string> = {
|
||||||
kt: 'kotlin',
|
kt: 'kotlin',
|
||||||
kotlin: 'kotlin',
|
kotlin: 'kotlin',
|
||||||
@ -27,12 +72,33 @@ const langAlias: Record<string, string> = {
|
|||||||
python: 'python',
|
python: 'python',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of supported languages.
|
||||||
|
*
|
||||||
|
* 支持的语言列表。
|
||||||
|
*/
|
||||||
const supportLang: Lang[] = ['kotlin', 'go', 'rust', 'python']
|
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) {
|
function resolveLang(lang?: string) {
|
||||||
return lang ? langAlias[lang] || lang : ''
|
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 {
|
export function resolveCode(el: HTMLElement): string {
|
||||||
const clone = el.cloneNode(true) as HTMLElement
|
const clone = el.cloneNode(true) as HTMLElement
|
||||||
clone
|
clone
|
||||||
@ -42,6 +108,14 @@ export function resolveCode(el: HTMLElement): string {
|
|||||||
return clone.textContent || ''
|
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): {
|
export function resolveCodeInfo(el: HTMLDivElement): {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
code: string
|
code: string
|
||||||
@ -57,19 +131,55 @@ export function resolveCodeInfo(el: HTMLDivElement): {
|
|||||||
return { lang: resolveLang(lang) as Lang, code }
|
return { lang: resolveLang(lang) as Lang, code }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result interface for useCodeRepl composable.
|
||||||
|
*
|
||||||
|
* useCodeRepl 组合式函数的结果接口。
|
||||||
|
*/
|
||||||
interface UseCodeReplResult {
|
interface UseCodeReplResult {
|
||||||
|
/** Current language / 当前语言 */
|
||||||
lang: Ref<Lang | undefined>
|
lang: Ref<Lang | undefined>
|
||||||
|
/** Whether the code is loaded / 代码是否已加载 */
|
||||||
loaded: Ref<boolean>
|
loaded: Ref<boolean>
|
||||||
|
/** Whether this is the first run / 是否为首次运行 */
|
||||||
firstRun: Ref<boolean>
|
firstRun: Ref<boolean>
|
||||||
|
/** Whether execution is finished / 执行是否完成 */
|
||||||
finished: Ref<boolean>
|
finished: Ref<boolean>
|
||||||
|
/** Standard output lines / 标准输出行 */
|
||||||
stdout: Ref<string[]>
|
stdout: Ref<string[]>
|
||||||
|
/** Standard error lines / 标准错误行 */
|
||||||
stderr: Ref<string[]>
|
stderr: Ref<string[]>
|
||||||
|
/** Error message / 错误信息 */
|
||||||
error: Ref<string>
|
error: Ref<string>
|
||||||
|
/** Backend version / 后端版本 */
|
||||||
backendVersion: Ref<string>
|
backendVersion: Ref<string>
|
||||||
|
/** Clean run state / 清理运行状态 */
|
||||||
onCleanRun: () => void
|
onCleanRun: () => void
|
||||||
|
/** Run code execution / 运行代码执行 */
|
||||||
onRunCode: () => Promise<void>
|
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.
|
||||||
|
*
|
||||||
|
* 该组合式函数提供在各种语言(Kotlin、Go、Rust、Python)中执行代码和管理执行状态的功能。
|
||||||
|
*
|
||||||
|
* @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 {
|
export function useCodeRepl(el: Ref<HTMLDivElement | null>): UseCodeReplResult {
|
||||||
const lang = ref<Lang>()
|
const lang = ref<Lang>()
|
||||||
const loaded = ref(true)
|
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 {
|
interface GolangRequest {
|
||||||
|
/** Code to execute / 要执行的代码 */
|
||||||
code: string
|
code: string
|
||||||
|
/** Go version / Go 版本 */
|
||||||
version?: '' | 'goprev' | 'gotip'
|
version?: '' | 'goprev' | 'gotip'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response interface for Golang execution API.
|
||||||
|
*
|
||||||
|
* Golang 执行 API 的响应接口。
|
||||||
|
*/
|
||||||
interface GolangResponse {
|
interface GolangResponse {
|
||||||
|
/** Execution events / 执行事件 */
|
||||||
events?: {
|
events?: {
|
||||||
|
/** Event message / 事件消息 */
|
||||||
message: ''
|
message: ''
|
||||||
|
/** Event kind / 事件类型 */
|
||||||
kind: 'stdout' | 'stderr'
|
kind: 'stdout' | 'stderr'
|
||||||
|
/** Event delay / 事件延迟 */
|
||||||
delay: number
|
delay: number
|
||||||
}[]
|
}[]
|
||||||
|
/** Error message / 错误信息 */
|
||||||
error?: string
|
error?: string
|
||||||
|
/** Go version / Go 版本 */
|
||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request interface for Kotlin execution API.
|
||||||
|
*
|
||||||
|
* Kotlin 执行 API 的请求接口。
|
||||||
|
*/
|
||||||
interface KotlinRequest {
|
interface KotlinRequest {
|
||||||
|
/** Command line arguments / 命令行参数 */
|
||||||
args?: string
|
args?: string
|
||||||
|
/** Files to compile / 要编译的文件 */
|
||||||
files: {
|
files: {
|
||||||
|
/** File name / 文件名 */
|
||||||
name: string
|
name: string
|
||||||
|
/** Public ID / 公共 ID */
|
||||||
publicId: string
|
publicId: string
|
||||||
|
/** File content / 文件内容 */
|
||||||
text: string
|
text: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response interface for Kotlin execution API.
|
||||||
|
*
|
||||||
|
* Kotlin 执行 API 的响应接口。
|
||||||
|
*/
|
||||||
interface KotlinResponse {
|
interface KotlinResponse {
|
||||||
|
/** Execution output / 执行输出 */
|
||||||
text: string
|
text: string
|
||||||
|
/** Kotlin version / Kotlin 版本 */
|
||||||
version: string
|
version: string
|
||||||
|
/** Compilation errors / 编译错误 */
|
||||||
errors: {
|
errors: {
|
||||||
[filename: string]: {
|
[filename: string]: {
|
||||||
|
/** Error message / 错误信息 */
|
||||||
message: string
|
message: string
|
||||||
|
/** Error severity / 错误严重程度 */
|
||||||
severity: 'ERROR' | 'WARNING'
|
severity: 'ERROR' | 'WARNING'
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,48 @@
|
|||||||
import type { ComputedRef } from 'vue'
|
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(
|
export function useDecrypt(
|
||||||
config: ComputedRef<{ salt: number[], iv: number[] }>,
|
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)
|
const toUnit8Array = (raw: number[]) => Uint8Array.from(raw)
|
||||||
|
|
||||||
return {
|
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) => {
|
decrypt: async (password: string, text: string) => {
|
||||||
if (!password)
|
if (!password)
|
||||||
return
|
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) {
|
function getKeyMaterial(password: string) {
|
||||||
const enc = new TextEncoder()
|
const enc = new TextEncoder()
|
||||||
return window.crypto.subtle.importKey(
|
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) {
|
function getCryptoDeriveKey(keyMaterial: CryptoKey, salt: BufferSource) {
|
||||||
return window.crypto.subtle.deriveKey(
|
return window.crypto.subtle.deriveKey(
|
||||||
|
|||||||
@ -1,12 +1,61 @@
|
|||||||
import { onContentUpdated } from 'vuepress/client'
|
import { onContentUpdated } from 'vuepress/client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute name for mark mode.
|
||||||
|
*
|
||||||
|
* 标记模式属性名。
|
||||||
|
*/
|
||||||
const MARK_MODE_ATTR = 'data-mark-mode'
|
const MARK_MODE_ATTR = 'data-mark-mode'
|
||||||
|
/**
|
||||||
|
* Lazy mode constant.
|
||||||
|
*
|
||||||
|
* 懒加载模式常量。
|
||||||
|
*/
|
||||||
const MARK_MODE_LAZY = 'lazy'
|
const MARK_MODE_LAZY = 'lazy'
|
||||||
|
/**
|
||||||
|
* CSS class for visible marks.
|
||||||
|
*
|
||||||
|
* 可见标记的 CSS 类名。
|
||||||
|
*/
|
||||||
const MARK_VISIBLE_CLASS = 'vp-mark-visible'
|
const MARK_VISIBLE_CLASS = 'vp-mark-visible'
|
||||||
|
/**
|
||||||
|
* Attribute name for mark boundary.
|
||||||
|
*
|
||||||
|
* 标记边界属性名。
|
||||||
|
*/
|
||||||
const MARK_BOUND_ATTR = 'data-vp-mark-bound'
|
const MARK_BOUND_ATTR = 'data-vp-mark-bound'
|
||||||
|
/**
|
||||||
|
* CSS selector for mark elements.
|
||||||
|
*
|
||||||
|
* 标记元素的 CSS 选择器。
|
||||||
|
*/
|
||||||
const MARK_SELECTOR = 'mark'
|
const MARK_SELECTOR = 'mark'
|
||||||
|
/**
|
||||||
|
* CSS selector for bounded mark elements.
|
||||||
|
*
|
||||||
|
* 已绑定标记元素的 CSS 选择器。
|
||||||
|
*/
|
||||||
const BOUND_SELECTOR = `${MARK_SELECTOR}[${MARK_BOUND_ATTR}="1"]`
|
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 {
|
export function setupMarkHighlight(mode: 'lazy' | 'eager'): void {
|
||||||
if (typeof window === 'undefined' || __VUEPRESS_SSR__)
|
if (typeof window === 'undefined' || __VUEPRESS_SSR__)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -17,6 +17,14 @@ import { withBase } from 'vuepress/client'
|
|||||||
import { ensureEndingSlash, isLinkHttp } from 'vuepress/shared'
|
import { ensureEndingSlash, isLinkHttp } from 'vuepress/shared'
|
||||||
import { pluginOptions } from '../options.js'
|
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 {
|
function queryStringify(options: PDFTokenMeta): string {
|
||||||
const { page, noToolbar, zoom } = options
|
const { page, noToolbar, zoom } = options
|
||||||
const params = [
|
const params = [
|
||||||
@ -32,6 +40,16 @@ function queryStringify(options: PDFTokenMeta): string {
|
|||||||
return queryString
|
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(
|
export function renderPDF(
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
url: string,
|
url: string,
|
||||||
@ -72,6 +90,20 @@ export function renderPDF(
|
|||||||
el.appendChild(pdf)
|
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).
|
||||||
|
*
|
||||||
|
* 该函数检测浏览器能力并选择适当的嵌入方法来显示 PDF(PDF.js、iframe 或 embed)。
|
||||||
|
*
|
||||||
|
* @param el - Container element / 容器元素
|
||||||
|
* @param url - PDF URL / PDF URL
|
||||||
|
* @param options - PDF token metadata / PDF 令牌元数据
|
||||||
|
*/
|
||||||
export function usePDF(
|
export function usePDF(
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
url: string,
|
url: string,
|
||||||
@ -86,10 +118,10 @@ export function usePDF(
|
|||||||
const isModernBrowser = typeof window.Promise === 'function'
|
const isModernBrowser = typeof window.Promise === 'function'
|
||||||
|
|
||||||
// Quick test for mobile devices.
|
// Quick test for mobile devices.
|
||||||
const isMobileDevice = isiPad(userAgent) || isMobile(userAgent)
|
const isMobileDevice = isiPad() || isMobile()
|
||||||
|
|
||||||
// Safari desktop requires special handling
|
// Safari desktop requires special handling
|
||||||
const isSafariDesktop = !isMobileDevice && isSafari(userAgent)
|
const isSafariDesktop = !isMobileDevice && isSafari()
|
||||||
|
|
||||||
const isFirefoxWithPDFJS
|
const isFirefoxWithPDFJS
|
||||||
= !isMobileDevice
|
= !isMobileDevice
|
||||||
|
|||||||
83
plugins/plugin-md-power/src/client/composables/qrcode.ts
Normal file
83
plugins/plugin-md-power/src/client/composables/qrcode.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import type { Prettify } from '@pengzhanbo/utils'
|
||||||
|
import type { QRCodeByteSegment, QRCodeErrorCorrectionLevel, QRCodeRenderersOptions, QRCodeToDataURLOptions, QRCodeToStringOptions } from 'qrcode'
|
||||||
|
|
||||||
|
export interface GenerateQRCodeConfig {
|
||||||
|
text: string
|
||||||
|
logo?: string
|
||||||
|
logoSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QRCodeInstance {
|
||||||
|
toCanvas: (str: string | QRCodeByteSegment[], options: QRCodeRenderersOptions) => Promise<HTMLCanvasElement>
|
||||||
|
toPNG: (str: string | QRCodeByteSegment[], options: QRCodeToDataURLOptions) => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
let qr: QRCodeInstance | null = null
|
||||||
|
|
||||||
|
async function initQRCodeInstance() {
|
||||||
|
if (qr)
|
||||||
|
return qr
|
||||||
|
|
||||||
|
const qrcode = (await import(/* webpackChunkName: "qrcode" */ 'qrcode')).default
|
||||||
|
qr = {
|
||||||
|
toCanvas: (text: string | QRCodeByteSegment[], options: QRCodeRenderersOptions) => {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
qrcode.toCanvas(text, options, (error, canvas) => error ? reject(error) : resolve(canvas)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
toPNG: (text, options) => qrcode.toDataURL(text, { type: 'image/png', ...options }),
|
||||||
|
}
|
||||||
|
return qr
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateQRCode(
|
||||||
|
{ text, logo, logoSize = '0.2' }: GenerateQRCodeConfig,
|
||||||
|
options: Prettify<QRCodeToDataURLOptions & QRCodeToStringOptions & QRCodeRenderersOptions>,
|
||||||
|
): Promise<string> {
|
||||||
|
const { toCanvas, toPNG } = await initQRCodeInstance()
|
||||||
|
const segments: QRCodeByteSegment[] = [{ data: new TextEncoder().encode(text), mode: 'byte' }]
|
||||||
|
|
||||||
|
const qrWidth = options.width!
|
||||||
|
if (logo) {
|
||||||
|
// 有 logo 时,需要设置 errorCorrectionLevel 为 H
|
||||||
|
// 因为 logo 会占用二维码的一部分空间,导致二维码的纠错能力下降
|
||||||
|
// 所以需要增加纠错能力
|
||||||
|
const level = options.errorCorrectionLevel ?? 'H'
|
||||||
|
options.errorCorrectionLevel = (level.length === 1 ? level.toUpperCase() : `${level[0].toUpperCase()}${level[1].toLowerCase()}`) as unknown as QRCodeErrorCorrectionLevel
|
||||||
|
const logoImg = await loadImage(logo)
|
||||||
|
const actualWith = Number.parseFloat(logoSize) * qrWidth
|
||||||
|
const actualHeight = actualWith / logoImg.width * logoImg.height
|
||||||
|
const dx = (qrWidth - actualWith) / 2
|
||||||
|
const dy = (qrWidth - actualHeight) / 2
|
||||||
|
const canvas = await toCanvas(segments, options)
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
// 绘制 logo 背景
|
||||||
|
ctx.fillStyle = options.color?.light || '#fff'
|
||||||
|
ctx.roundRect(dx, dy, actualWith, actualHeight, actualWith / 20)
|
||||||
|
ctx.fill()
|
||||||
|
// 绘制 logo 图片
|
||||||
|
ctx.drawImage(logoImg, dx, dy, actualWith, actualHeight)
|
||||||
|
return canvas.toDataURL()
|
||||||
|
}
|
||||||
|
const level = options.errorCorrectionLevel ?? 'M'
|
||||||
|
options.errorCorrectionLevel = (level.length === 1 ? level.toUpperCase() : `${level[0].toUpperCase()}${level[1].toLowerCase()}`) as unknown as QRCodeErrorCorrectionLevel
|
||||||
|
return await toPNG(segments, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadImage(url: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.src = url
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = () => reject(new Error('Failed to load image'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attemptLoadLogo(text: string, logo: string | undefined, isInternalLink: boolean): Promise<string> {
|
||||||
|
if (logo)
|
||||||
|
return logo
|
||||||
|
if (isInternalLink)
|
||||||
|
return (document.querySelector('link[rel="icon"]') as HTMLLinkElement)?.href
|
||||||
|
return ''
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user