docs: improve jsdoc (#852)

This commit is contained in:
pengzhanbo 2026-02-14 14:53:41 +08:00 committed by GitHub
parent 77da8a3470
commit 5c201e3ed0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
151 changed files with 4411 additions and 225 deletions

View File

@ -1,27 +1,86 @@
import type { Bundler, Langs, Options } from './types.js' import type { Bundler, Langs, Options } from './types.js'
/**
* Language options for VuePress configuration
*
* VuePress
*/
export const languageOptions: Options<Langs> = [ export const languageOptions: Options<Langs> = [
{ label: 'English', value: 'en-US' }, { label: 'English', value: 'en-US' },
{ label: '简体中文', value: 'zh-CN' }, { label: '简体中文', value: 'zh-CN' },
] ]
/**
* Bundler options for VuePress build tool
*
* VuePress
*/
export const bundlerOptions: Options<Bundler> = [ export const bundlerOptions: Options<Bundler> = [
{ label: 'Vite', value: 'vite' }, { label: 'Vite', value: 'vite' },
{ label: 'Webpack', value: 'webpack' }, { label: 'Webpack', value: 'webpack' },
] ]
/**
* Operation mode for VuePress CLI
*
* VuePress CLI
* @readonly
* @enum {number}
*/
export enum Mode { export enum Mode {
/**
* Initialize existing directory
*
*
*/
init, init,
/**
* Create new project
*
*
*/
create, create,
} }
/**
* Deployment type for VuePress site
*
* VuePress
* @readonly
* @enum {string}
*/
export enum DeployType { export enum DeployType {
/**
* GitHub Pages deployment
*
* GitHub Pages
*/
github = 'github', github = 'github',
/**
* Vercel deployment
*
* Vercel
*/
vercel = 'vercel', vercel = 'vercel',
/**
* Netlify deployment
*
* Netlify
*/
netlify = 'netlify', netlify = 'netlify',
/**
* Custom deployment
*
*
*/
custom = 'custom', custom = 'custom',
} }
/**
* Deployment options for hosting platforms
*
*
*/
export const deployOptions: Options<DeployType> = [ export const deployOptions: Options<DeployType> = [
{ label: 'Custom', value: DeployType.custom }, { label: 'Custom', value: DeployType.custom },
{ label: 'GitHub Pages', value: DeployType.github }, { label: 'GitHub Pages', value: DeployType.github },

View File

@ -8,6 +8,15 @@ import { createPackageJson } from './packageJson.js'
import { createRender } from './render.js' import { createRender } from './render.js'
import { getTemplate, readFiles, readJsonFile, writeFiles } from './utils/index.js' import { getTemplate, readFiles, readJsonFile, writeFiles } from './utils/index.js'
/**
* Generate VuePress project files
*
* VuePress
*
* @param mode - Operation mode (init or create) /
* @param data - Resolved configuration data /
* @param cwd - Current working directory /
*/
export async function generate( export async function generate(
mode: Mode, mode: Mode,
data: ResolvedData, data: ResolvedData,
@ -106,6 +115,14 @@ export async function generate(
}) })
} }
/**
* Create documentation files based on configuration
*
*
*
* @param data - Resolved configuration data /
* @returns Array of file objects /
*/
async function createDocsFiles(data: ResolvedData): Promise<File[]> { async function createDocsFiles(data: ResolvedData): Promise<File[]> {
const fileList: File[] = [] const fileList: File[] = []
if (data.multiLanguage) { if (data.multiLanguage) {
@ -131,6 +148,15 @@ async function createDocsFiles(data: ResolvedData): Promise<File[]> {
return updateFileListTarget(fileList, data.docsDir) return updateFileListTarget(fileList, data.docsDir)
} }
/**
* Update file list target path
*
*
*
* @param fileList - Array of files /
* @param target - Target directory path /
* @returns Updated file array /
*/
function updateFileListTarget(fileList: File[], target: string): File[] { function updateFileListTarget(fileList: File[], target: string): File[] {
return fileList.map(({ filepath, content }) => ({ return fileList.map(({ filepath, content }) => ({
filepath: path.join(target, filepath), filepath: path.join(target, filepath),

View File

@ -11,6 +11,23 @@ function sortPackageJson(json: Record<any, any>) {
}) })
} }
/**
* Create package.json file for VuePress project
*
* VuePress package.json
*
* @param mode - Operation mode (init or create) /
* @param pkg - Existing package.json data / package.json
* @param data - Resolved configuration data /
* @param data.packageManager - Package manager to use / 使
* @param data.siteName - Site name /
* @param data.siteDescription - Site description /
* @param data.docsDir - Documentation directory path /
* @param data.bundler - Bundler to use / 使
* @param data.injectNpmScripts - Whether to inject npm scripts / npm
*
* @returns File object with package.json content / package.json
*/
export async function createPackageJson( export async function createPackageJson(
mode: Mode, mode: Mode,
pkg: Record<string, any>, pkg: Record<string, any>,

View File

@ -2,16 +2,33 @@ import type { ResolvedData } from './types.js'
import { kebabCase } from '@pengzhanbo/utils' import { kebabCase } from '@pengzhanbo/utils'
import handlebars from 'handlebars' import handlebars from 'handlebars'
/**
* Extended resolved data with additional rendering information
*
*
*/
export interface RenderData extends ResolvedData { export interface RenderData extends ResolvedData {
/** Project name in kebab-case / 项目名称kebab-case 格式) */
name: string name: string
/** Site name / 网站名称 */
siteName: string siteName: string
/** Locale configuration array / 语言配置数组 */
locales: { path: string, lang: string, isEn: boolean, prefix: string }[] locales: { path: string, lang: string, isEn: boolean, prefix: string }[]
/** Whether default language is English / 默认语言是否为英语 */
isEN: boolean isEN: boolean
} }
handlebars.registerHelper('removeLeadingSlash', (path: string) => path.replace(/^\//, '')) handlebars.registerHelper('removeLeadingSlash', (path: string) => path.replace(/^\//, ''))
handlebars.registerHelper('equal', (a: string, b: string) => a === b) handlebars.registerHelper('equal', (a: string, b: string) => a === b)
/**
* Create render function with Handlebars template engine
*
* 使 Handlebars
*
* @param result - Resolved configuration data /
* @returns Render function that processes Handlebars templates / Handlebars
*/
export function createRender(result: ResolvedData) { export function createRender(result: ResolvedData) {
const data: RenderData = { const data: RenderData = {
...result, ...result,

View File

@ -11,6 +11,14 @@ import { prompt } from './prompt.js'
import { t } from './translate.js' import { t } from './translate.js'
import { getPackageManager } from './utils/index.js' import { getPackageManager } from './utils/index.js'
/**
* Run the CLI workflow for VuePress project initialization or creation
*
* VuePress CLI
*
* @param mode - Operation mode (init or create) /
* @param root - Root directory path /
*/
export async function run(mode: Mode, root?: string): Promise<void> { export async function run(mode: Mode, root?: string): Promise<void> {
intro(colors.cyan('Welcome to VuePress and vuepress-theme-plume !')) intro(colors.cyan('Welcome to VuePress and vuepress-theme-plume !'))

View File

@ -6,6 +6,14 @@ interface Translate {
t: (key: keyof Locale) => string t: (key: keyof Locale) => string
} }
/**
* Create a translate instance with specified language
*
*
*
* @param lang - Language code /
* @returns Translate interface /
*/
function createTranslate(lang?: Langs): Translate { function createTranslate(lang?: Langs): Translate {
let current: Langs = lang || 'en-US' let current: Langs = lang || 'en-US'
@ -19,5 +27,21 @@ function createTranslate(lang?: Langs): Translate {
const translate = createTranslate() const translate = createTranslate()
/**
* Get translated string by key
*
*
*
* @param key - Locale key /
* @returns Translated string /
*/
export const t: Translate['t'] = translate.t export const t: Translate['t'] = translate.t
/**
* Set current language
*
*
*
* @param lang - Language code to set /
*/
export const setLang: Translate['setLang'] = translate.setLang export const setLang: Translate['setLang'] = translate.setLang

View File

@ -1,57 +1,276 @@
import type { DeployType } from './constants.js' import type { DeployType } from './constants.js'
/**
* Supported language codes for VuePress site
*
* VuePress
*/
export type Langs = 'zh-CN' | 'en-US' export type Langs = 'zh-CN' | 'en-US'
/**
* Locale configuration for CLI prompts and messages
*
* CLI
*/
export interface Locale { export interface Locale {
/**
* Question: Project root directory name
*
*
*/
'question.root': string 'question.root': string
/**
* Question: Site name
*
*
*/
'question.site.name': string 'question.site.name': string
/**
* Question: Site description
*
*
*/
'question.site.description': string 'question.site.description': string
/**
* Question: Enable multi-language support
*
*
*/
'question.multiLanguage': string 'question.multiLanguage': string
/**
* Question: Default language
*
*
*/
'question.defaultLanguage': string 'question.defaultLanguage': string
/**
* Question: Build tool bundler
*
*
*/
'question.bundler': string 'question.bundler': string
/**
* Question: Use TypeScript
*
* 使 TypeScript
*/
'question.useTs': string 'question.useTs': string
/**
* Question: Inject npm scripts
*
* npm
*/
'question.injectNpmScripts': string 'question.injectNpmScripts': string
/**
* Question: Initialize git repository
*
* git
*/
'question.git': string 'question.git': string
/**
* Question: Deployment type
*
*
*/
'question.deploy': string 'question.deploy': string
/**
* Question: Install dependencies
*
*
*/
'question.installDeps': string 'question.installDeps': string
/**
* Spinner: Start message
*
*
*/
'spinner.start': string 'spinner.start': string
/**
* Spinner: Stop message
*
*
*/
'spinner.stop': string 'spinner.stop': string
/**
* Spinner: Git init message
*
* Git
*/
'spinner.git': string 'spinner.git': string
/**
* Spinner: Install message
*
*
*/
'spinner.install': string 'spinner.install': string
/**
* Spinner: Command hint message
*
*
*/
'spinner.command': string 'spinner.command': string
/**
* Hint: Cancel operation
*
*
*/
'hint.cancel': string 'hint.cancel': string
/**
* Hint: Root directory
*
*
*/
'hint.root': string 'hint.root': string
/**
* Hint: Illegal root directory name
*
*
*/
'hint.root.illegal': string 'hint.root.illegal': string
} }
/**
* Package manager types
*
*
*/
export type PackageManager = 'npm' | 'yarn' | 'pnpm' export type PackageManager = 'npm' | 'yarn' | 'pnpm'
/**
* Build tool bundler types
*
*
*/
export type Bundler = 'vite' | 'webpack' export type Bundler = 'vite' | 'webpack'
/**
* Generic options type for CLI prompts
*
* CLI
*
* @template Value - The value type for options
* @template Label - The label type for options
*/
export type Options<Value = string, Label = string> = { label: Label, value: Value }[] export type Options<Value = string, Label = string> = { label: Label, value: Value }[]
/**
* File structure for generated project
*
*
*/
export interface File { export interface File {
/**
* File path relative to project root
*
*
*/
filepath: string filepath: string
/**
* File content
*
*
*/
content: string content: string
} }
/**
* Result from CLI prompts
*
* CLI
*/
export interface PromptResult { export interface PromptResult {
displayLang: string // cli display language /**
* CLI display language
*
* CLI
*/
displayLang: string
/**
* Project root directory name
*
*
*/
root: string root: string
/**
* Site name
*
*
*/
siteName: string siteName: string
/**
* Site description
*
*
*/
siteDescription: string siteDescription: string
/**
* Build tool bundler
*
*
*/
bundler: Bundler bundler: Bundler
/**
* Enable multi-language support
*
*
*/
multiLanguage: boolean multiLanguage: boolean
/**
* Default language
*
*
*/
defaultLanguage: Langs defaultLanguage: Langs
/**
* Use TypeScript
*
* 使 TypeScript
*/
useTs: boolean useTs: boolean
/**
* Inject npm scripts
*
* npm
*/
injectNpmScripts: boolean injectNpmScripts: boolean
/**
* Deployment type
*
*
*/
deploy: DeployType deploy: DeployType
/**
* Initialize git repository
*
* git
*/
git: boolean git: boolean
/**
* Install dependencies
*
*
*/
install: boolean install: boolean
} }
/**
* Resolved data after processing prompts
*
*
*/
export interface ResolvedData extends PromptResult { export interface ResolvedData extends PromptResult {
/**
* Selected package manager
*
*
*/
packageManager: PackageManager packageManager: PackageManager
/**
* Documentation directory name
*
*
*/
docsDir: string docsDir: string
} }

View File

@ -2,6 +2,14 @@ import type { File } from '../types.js'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
/**
* Read all files from a directory recursively
*
*
*
* @param root - Root directory path to read from /
* @returns Array of file objects /
*/
export async function readFiles(root: string): Promise<File[]> { export async function readFiles(root: string): Promise<File[]> {
const filepaths = await fs.readdir(root, { recursive: true }) const filepaths = await fs.readdir(root, { recursive: true })
const files: File[] = [] const files: File[] = []
@ -18,6 +26,15 @@ export async function readFiles(root: string): Promise<File[]> {
return files return files
} }
/**
* Write files to target directory
*
*
*
* @param files - Array of file objects to write /
* @param target - Target directory path /
* @param rewrite - Optional function to rewrite file paths /
*/
export async function writeFiles( export async function writeFiles(
files: File[], files: File[],
target: string, target: string,
@ -32,6 +49,14 @@ export async function writeFiles(
} }
} }
/**
* Read and parse JSON file
*
* JSON
*
* @param filepath - Path to JSON file / JSON
* @returns Parsed JSON object or null if parsing fails / JSON null
*/
export async function readJsonFile<T extends Record<string, any> = Record<string, any>>(filepath: string): Promise<T | null> { export async function readJsonFile<T extends Record<string, any> = Record<string, any>>(filepath: string): Promise<T | null> {
try { try {
const content = await fs.readFile(filepath, 'utf-8') const content = await fs.readFile(filepath, 'utf-8')

View File

@ -3,8 +3,24 @@ import { fileURLToPath } from 'node:url'
export const __dirname: string = path.dirname(fileURLToPath(import.meta.url)) export const __dirname: string = path.dirname(fileURLToPath(import.meta.url))
/**
* Resolve path relative to the project root
*
*
*
* @param args - Path segments to resolve /
* @returns Resolved absolute path /
*/
export const resolve = (...args: string[]): string => path.resolve(__dirname, '../', ...args) export const resolve = (...args: string[]): string => path.resolve(__dirname, '../', ...args)
/**
* Get template directory path
*
*
*
* @param dir - Subdirectory name within templates / templates
* @returns Resolved template directory path /
*/
export const getTemplate = (dir: string): string => resolve('templates', dir) export const getTemplate = (dir: string): string => resolve('templates', dir)
export * from './fs.js' export * from './fs.js'

View File

@ -3,6 +3,36 @@ import { parseRect } from '../utils/parseRect.js'
import { resolveAttrs } from '../utils/resolveAttrs.js' import { resolveAttrs } from '../utils/resolveAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Flex container attributes
*
* Flex
*/
interface FlexContainerAttrs {
center?: boolean
wrap?: boolean
between?: boolean
around?: boolean
start?: boolean
end?: boolean
gap?: string
column?: boolean
}
/**
* Align plugin - Enable text alignment containers
*
* -
*
* Syntax:
* - ::: left
* - ::: center
* - ::: right
* - ::: justify
* - ::: flex [options]
*
* @param md - Markdown instance / Markdown
*/
export function alignPlugin(md: Markdown): void { export function alignPlugin(md: Markdown): void {
const alignList = ['left', 'center', 'right', 'justify'] const alignList = ['left', 'center', 'right', 'justify']
@ -38,14 +68,3 @@ export function alignPlugin(md: Markdown): void {
}, },
}) })
} }
interface FlexContainerAttrs {
center?: boolean
wrap?: boolean
between?: boolean
around?: boolean
start?: boolean
end?: boolean
gap?: string
column?: boolean
}

View File

@ -3,16 +3,38 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Card container attributes
*
*
*/
interface CardAttrs { interface CardAttrs {
title?: string title?: string
icon?: string icon?: string
} }
/**
* Card masonry container attributes
*
*
*/
interface CardMasonryAttrs { interface CardMasonryAttrs {
cols?: number cols?: number
gap?: number gap?: number
} }
/**
* Card plugin - Enable card containers
*
* -
*
* Syntax:
* - ::: card title="xxx" icon="xxx"
* - ::: card-grid
* - ::: card-masonry cols="2" gap="10"
*
* @param md - Markdown instance / Markdown
*/
export function cardPlugin(md: Markdown): void { export function cardPlugin(md: Markdown): void {
/** /**
* ::: card title="xxx" icon="xxx" * ::: card title="xxx" icon="xxx"

View File

@ -1,4 +1,10 @@
/** /**
* Chat container plugin
*
*
*
* Syntax:
* ```md
* ::: chat * ::: chat
* {:time} * {:time}
* *
@ -8,12 +14,18 @@
* {.} * {.}
* xxxxxx * xxxxxx
* ::: * :::
* ```
*/ */
import type { PluginSimple } from 'markdown-it' import type { PluginSimple } from 'markdown-it'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown' import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
import { createContainerSyntaxPlugin } from './createContainer.js' import { createContainerSyntaxPlugin } from './createContainer.js'
/**
* Chat message structure
*
*
*/
interface ChatMessage { interface ChatMessage {
sender: 'user' | 'self' sender: 'user' | 'self'
date: string date: string
@ -21,6 +33,16 @@ interface ChatMessage {
content: string[] content: string[]
} }
/**
* Render chat messages to HTML
*
* HTML
*
* @param md - Markdown instance / Markdown
* @param env - Markdown environment / Markdown
* @param messages - Chat messages /
* @returns Rendered HTML / HTML
*/
function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessage[]): string { function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessage[]): string {
let currentDate = '' let currentDate = ''
return messages.map(({ sender, username, date, content }) => { return messages.map(({ sender, username, date, content }) => {
@ -43,7 +65,14 @@ function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessag
.join('\n') .join('\n')
} }
// 解析聊天内容的核心逻辑 /**
* Parse chat content to messages
*
*
*
* @param content - Raw chat content /
* @returns Parsed chat messages /
*/
function parseChatContent(content: string): ChatMessage[] { function parseChatContent(content: string): ChatMessage[] {
const lines = content.split('\n') const lines = content.split('\n')
const messages: ChatMessage[] = [] const messages: ChatMessage[] = []
@ -53,13 +82,13 @@ function parseChatContent(content: string): ChatMessage[] {
for (const line of lines) { for (const line of lines) {
const lineStr = line.trim() const lineStr = line.trim()
// 解析时间标记 // Parse time marker
if (lineStr.startsWith('{:') && lineStr.endsWith('}')) { if (lineStr.startsWith('{:') && lineStr.endsWith('}')) {
currentDate = lineStr.slice(2, -1).trim() currentDate = lineStr.slice(2, -1).trim()
continue continue
} }
// 解析用户 / 本人标记 // Parse user/self marker
if (lineStr.startsWith('{') && lineStr.endsWith('}')) { if (lineStr.startsWith('{') && lineStr.endsWith('}')) {
const username = lineStr.slice(1, -1).trim() const username = lineStr.slice(1, -1).trim()
message = { message = {
@ -72,7 +101,7 @@ function parseChatContent(content: string): ChatMessage[] {
continue continue
} }
// 收集消息内容 // Collect message content
if (message?.sender) { if (message?.sender) {
message.content.push(line) message.content.push(line)
} }
@ -80,6 +109,13 @@ function parseChatContent(content: string): ChatMessage[] {
return messages return messages
} }
/**
* Chat plugin - Enable chat container
*
* -
*
* @param md - Markdown-it instance / Markdown-it
*/
export const chatPlugin: PluginSimple = md => createContainerSyntaxPlugin( export const chatPlugin: PluginSimple = md => createContainerSyntaxPlugin(
md, md,
'chat', 'chat',

View File

@ -6,6 +6,14 @@ import { definitions, getFileIconName, getFileIconTypeFromExtension } from '../f
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
import { stringifyProp } from '../utils/stringifyProp.js' import { stringifyProp } from '../utils/stringifyProp.js'
/**
* Create code tab icon getter function
*
*
*
* @param options - Code tabs options /
* @returns Icon getter function /
*/
export function createCodeTabIconGetter( export function createCodeTabIconGetter(
options: CodeTabsOptions = {}, options: CodeTabsOptions = {},
): (filename: string) => string | void { ): (filename: string) => string | void {
@ -35,6 +43,14 @@ export function createCodeTabIconGetter(
} }
} }
/**
* Code tabs plugin - Enable code tabs container
*
* -
*
* @param md - Markdown-it instance / Markdown-it
* @param options - Code tabs options /
*/
export const codeTabs: PluginWithOptions<CodeTabsOptions> = (md, options: CodeTabsOptions = {}) => { export const codeTabs: PluginWithOptions<CodeTabsOptions> = (md, options: CodeTabsOptions = {}) => {
const getIcon = createCodeTabIconGetter(options) const getIcon = createCodeTabIconGetter(options)

View File

@ -1,5 +1,5 @@
/** /**
* @module CodeTree * Code tree container plugin
* *
* code-tree * code-tree
* ````md * ````md
@ -10,7 +10,7 @@
* ::: * :::
* ```` * ````
* *
* embed syntax * Embed syntax
* *
* `@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)` * `@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)`
*/ */
@ -32,6 +32,11 @@ import { resolveAttr, resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Unsupported file types for code tree
*
*
*/
const UNSUPPORTED_FILE_TYPES = [ const UNSUPPORTED_FILE_TYPES = [
/* image */ /* image */
'jpg', 'jpg',
@ -62,26 +67,36 @@ const UNSUPPORTED_FILE_TYPES = [
] ]
/** /**
* Code tree metadata
*
* code-tree * code-tree
*/ */
interface CodeTreeMeta { interface CodeTreeMeta {
title?: string title?: string
/** /**
* File icon type
*
* *
*/ */
icon?: FileTreeIconMode icon?: FileTreeIconMode
/** /**
* Code tree container height
*
* *
*/ */
height?: string height?: string
/** /**
* Entry file, opened by default
*
* *
*/ */
entry?: string entry?: string
} }
/** /**
* File tree node type
*
* *
*/ */
interface FileTreeNode { interface FileTreeNode {
@ -92,9 +107,12 @@ interface FileTreeNode {
} }
/** /**
* Parse file paths to file tree node structure
*
* *
* @param files *
* @returns * @param files - File path array /
* @returns File tree node array /
*/ */
function parseFileNodes(files: string[]): FileTreeNode[] { function parseFileNodes(files: string[]): FileTreeNode[] {
const nodes: FileTreeNode[] = [] const nodes: FileTreeNode[] = []
@ -123,13 +141,18 @@ function parseFileNodes(files: string[]): FileTreeNode[] {
} }
/** /**
* Code tree plugin - Register code-tree container and embed syntax
*
* code-tree markdown * code-tree markdown
* @param md markdown-it *
* @param app vuepress app * @param md - Markdown-it instance / markdown-it
* @param options code-tree * @param app - VuePress app instance / vuepress app
* @param options - Code tree options / code-tree
*/ */
export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions = {}): void { export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions = {}): void {
/** /**
* Get file or folder icon
*
* *
*/ */
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => { const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
@ -140,6 +163,8 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
} }
/** /**
* Render file tree nodes to component string
*
* *
*/ */
function renderFileTree(nodes: FileTreeNode[], mode?: FileTreeIconMode): string { function renderFileTree(nodes: FileTreeNode[], mode?: FileTreeIconMode): string {
@ -159,10 +184,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
.join('\n') .join('\n')
} }
// 注册 ::: code-tree 容器 // Register ::: code-tree container
createContainerPlugin(md, 'code-tree', { createContainerPlugin(md, 'code-tree', {
before: (info, tokens, index) => { before: (info, tokens, index) => {
// 收集 code-tree 容器内的文件名和激活文件 // Collect filenames and active file in code-tree container
const files: string[] = [] const files: string[] = []
let activeFile: string | undefined let activeFile: string | undefined
for ( for (
@ -197,7 +222,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
after: () => '</VPCodeTree>', after: () => '</VPCodeTree>',
}) })
// 注册 @[code-tree](dir) 语法 // Register @[code-tree](dir) syntax
createEmbedRuleBlock(md, { createEmbedRuleBlock(md, {
type: 'code-tree', type: 'code-tree',
syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/, syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/,
@ -213,10 +238,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
} }
}, },
content: ({ dir, icon, ...props }, _, env) => { content: ({ dir, icon, ...props }, _, env) => {
// codeTreeFiles 用于页面依赖收集 // codeTreeFiles for page dependency collection
const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[] const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[]
const root = findFile(app, env, dir) const root = findFile(app, env, dir)
// 获取目录下所有文件 // Get all files in directory
const files = tinyglobby.globSync('**/*', { const files = tinyglobby.globSync('**/*', {
cwd: root, cwd: root,
onlyFiles: true, onlyFiles: true,
@ -229,7 +254,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
}) })
props.entryFile ||= files[0] props.entryFile ||= files[0]
// 生成所有文件的代码块内容 // Generate code block content for all files
const codeContent = files.map((file) => { const codeContent = files.map((file) => {
const ext = path.extname(file).slice(1) const ext = path.extname(file).slice(1)
if (UNSUPPORTED_FILE_TYPES.includes(ext)) { if (UNSUPPORTED_FILE_TYPES.includes(ext)) {
@ -250,8 +275,11 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
} }
/** /**
* Extend page dependencies with codeTreeFiles
*
* codeTreeFiles * codeTreeFiles
* @param page vuepress *
* @param page - VuePress page object / vuepress
*/ */
export function extendsPageWithCodeTree(page: Page): void { export function extendsPageWithCodeTree(page: Page): void {
const markdownEnv = page.markdownEnv const markdownEnv = page.markdownEnv

View File

@ -1,10 +1,17 @@
/** /**
* Collapse container plugin
*
*
*
* Syntax:
* ```md
* ::: collapse accordion * ::: collapse accordion
* - + * - +
* *
* - - * - -
* *
* ::: * :::
* ```
*/ */
import type Token from 'markdown-it/lib/token.mjs' import type Token from 'markdown-it/lib/token.mjs'
import type { Markdown } from 'vuepress/markdown' import type { Markdown } from 'vuepress/markdown'
@ -12,16 +19,33 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Collapse metadata
*
*
*/
interface CollapseMeta { interface CollapseMeta {
accordion?: boolean accordion?: boolean
expand?: boolean expand?: boolean
} }
/**
* Collapse item metadata
*
*
*/
interface CollapseItemMeta { interface CollapseItemMeta {
expand?: boolean expand?: boolean
index?: number index?: number
} }
/**
* Collapse plugin - Enable collapse container
*
* -
*
* @param md - Markdown instance / Markdown
*/
export function collapsePlugin(md: Markdown): void { export function collapsePlugin(md: Markdown): void {
createContainerPlugin(md, 'collapse', { createContainerPlugin(md, 'collapse', {
before: (info, tokens, index) => { before: (info, tokens, index) => {
@ -43,9 +67,19 @@ export function collapsePlugin(md: Markdown): void {
md.renderer.rules.collapse_item_title_close = () => '</template>' md.renderer.rules.collapse_item_title_close = () => '</template>'
} }
/**
* Parse collapse tokens
*
*
*
* @param tokens - Token array /
* @param index - Start index /
* @param attrs - Collapse metadata /
* @returns Default expanded index /
*/
function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): number | void { function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): number | void {
const listStack: number[] = [] // 记录列表嵌套深度 const listStack: number[] = [] // Track list nesting depth
let idx = -1 // 记录当前列表项下标 let idx = -1 // Current list item index
let defaultIndex: number | undefined let defaultIndex: number | undefined
let hashExpand = false let hashExpand = false
for (let i = index + 1; i < tokens.length; i++) { for (let i = index + 1; i < tokens.length; i++) {
@ -53,9 +87,9 @@ function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): num
if (token.type === 'container_collapse_close') { if (token.type === 'container_collapse_close') {
break break
} }
// 列表层级追踪 // Track list level
if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') { if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
listStack.push(0) // 每个新列表初始层级为0 listStack.push(0) // Each new list starts at level 0
if (listStack.length === 1) if (listStack.length === 1)
token.hidden = true token.hidden = true
} }
@ -66,7 +100,7 @@ function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): num
} }
else if (token.type === 'list_item_open') { else if (token.type === 'list_item_open') {
const currentLevel = listStack.length const currentLevel = listStack.length
// 仅处理根级列表项层级1 // Only process root level list items (level 1)
if (currentLevel === 1) { if (currentLevel === 1) {
token.type = 'collapse_item_open' token.type = 'collapse_item_open'
tokens[i + 1].type = 'collapse_item_title_open' tokens[i + 1].type = 'collapse_item_title_open'

View File

@ -5,59 +5,73 @@ import container from 'markdown-it-container'
import { resolveAttrs } from '../utils/resolveAttrs.js' import { resolveAttrs } from '../utils/resolveAttrs.js'
/** /**
* RenderRuleParams RenderRule * Type for getting RenderRule parameters
*
* RenderRule
*/ */
type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never
/** /**
* * Container options
* - before: 渲染容器起始标签时的回调 *
* - after: 渲染容器结束标签时的回调 *
*/ */
export interface ContainerOptions { export interface ContainerOptions {
/**
* Callback for rendering container opening tag
*
*
*/
before?: (info: string, ...args: RenderRuleParams) => string before?: (info: string, ...args: RenderRuleParams) => string
/**
* Callback for rendering container closing tag
*
*
*/
after?: (info: string, ...args: RenderRuleParams) => string after?: (info: string, ...args: RenderRuleParams) => string
} }
/** /**
* markdown-it * Create markdown-it custom container plugin
* *
* @param md markdown-it * markdown-it
* @param type 'tip', 'warning' *
* @param options before/after * @param md - Markdown-it instance / Markdown-it
* @param options.before * @param type - Container type (e.g., 'tip', 'warning') / 'tip', 'warning'
* @param options.after * @param options - Optional before/after render hooks / before/after
* @param options.before - Callback for rendering container opening tag /
* @param options.after - Callback for rendering container closing tag /
*/ */
export function createContainerPlugin( export function createContainerPlugin(
md: Markdown, md: Markdown,
type: string, type: string,
{ before, after }: ContainerOptions = {}, { before, after }: ContainerOptions = {},
): void { ): void {
// 自定义渲染规则 // Custom render rule
const render: RenderRule = (tokens, index, options, env): string => { const render: RenderRule = (tokens, index, options, env): string => {
const token = tokens[index] const token = tokens[index]
// 提取 ::: 后的 info 信息 // Extract info after :::
const info = token.info.trim().slice(type.length).trim() || '' const info = token.info.trim().slice(type.length).trim() || ''
if (token.nesting === 1) { if (token.nesting === 1) {
// 容器起始标签 // Container opening tag
return before?.(info, tokens, index, options, env) ?? `<div class="custom-container ${type}">` return before?.(info, tokens, index, options, env) ?? `<div class="custom-container ${type}">`
} }
else { else {
// 容器结束标签 // Container closing tag
return after?.(info, tokens, index, options, env) ?? '</div>' return after?.(info, tokens, index, options, env) ?? '</div>'
} }
} }
// 注册 markdown-it-container 插件 // Register markdown-it-container plugin
md.use(container, type, { render }) md.use(container, type, { render })
} }
/** /**
* markdown-it * Create a custom container rule where content is not processed by markdown-it
* content * Requires custom content processing logic
* ```md * ```md
* ::: type * ::: type
* xxxx <-- content: 这部分的内容不会交给 markdown-it * xxxx <-- content: this part will not be processed by markdown-it
* ::: * :::
* ``` * ```
* *
@ -68,6 +82,10 @@ export function createContainerPlugin(
* return `<div class="example">${meta.title} | ${content}</div>` * return `<div class="example">${meta.title} | ${content}</div>`
* }) * })
* ``` * ```
*
* @param md - Markdown-it instance / Markdown-it
* @param type - Container type /
* @param render - Custom render rule /
*/ */
export function createContainerSyntaxPlugin( export function createContainerSyntaxPlugin(
md: Markdown, md: Markdown,
@ -78,12 +96,15 @@ export function createContainerSyntaxPlugin(
const markerMinLen = 3 const markerMinLen = 3
/** /**
* block * Custom container block rule definition
* @param state block *
* @param startLine * block
* @param endLine *
* @param silent * @param state - Current block state / block
* @returns * @param startLine - Start line /
* @param endLine - End line /
* @param silent - Silent mode /
* @returns Whether matched /
*/ */
function defineContainer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { function defineContainer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
const start = state.bMarks[startLine] + state.tShift[startLine] const start = state.bMarks[startLine] + state.tShift[startLine]
@ -91,11 +112,11 @@ export function createContainerSyntaxPlugin(
let pos = start let pos = start
// check marker // check marker
// 检查是否以指定的 maker:)开头 // Check if starts with specified maker (:)
if (state.src[pos] !== maker) if (state.src[pos] !== maker)
return false return false
// 检查 marker 长度是否满足要求 // Check if marker length meets requirements
for (pos = start + 1; pos <= max; pos++) { for (pos = start + 1; pos <= max; pos++) {
if (state.src[pos] !== maker) if (state.src[pos] !== maker)
break break
@ -108,7 +129,7 @@ export function createContainerSyntaxPlugin(
const info = state.src.slice(pos, max).trim() const info = state.src.slice(pos, max).trim()
// ::: type // ::: type
// 检查 info 是否以 type 开头 // Check if info starts with type
if (!info.startsWith(type)) if (!info.startsWith(type))
return false return false
@ -118,7 +139,7 @@ export function createContainerSyntaxPlugin(
let line = startLine let line = startLine
let content = '' let content = ''
// 收集容器内容,直到遇到结束的 marker // Collect container content until end marker
while (++line < endLine) { while (++line < endLine) {
if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === markup) { if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === markup) {
break break
@ -127,7 +148,7 @@ export function createContainerSyntaxPlugin(
content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n` content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n`
} }
// 创建 token保存内容和属性 // Create token, save content and attributes
const token = state.push(`${type}_container`, '', 0) const token = state.push(`${type}_container`, '', 0)
token.meta = resolveAttrs(info.slice(type.length)).attrs token.meta = resolveAttrs(info.slice(type.length)).attrs
token.content = content token.content = content
@ -139,13 +160,13 @@ export function createContainerSyntaxPlugin(
return true return true
} }
// 默认渲染函数 // Default render function
const defaultRender: RenderRule = (tokens, index) => { const defaultRender: RenderRule = (tokens, index) => {
const { content } = tokens[index] const { content } = tokens[index]
return `<div class="custom-container ${type}">${content}</div>` return `<div class="custom-container ${type}">${content}</div>`
} }
// 注册 block 规则和渲染规则 // Register block rule and render rule
md.block.ruler.before('fence', `${type}_definition`, defineContainer) md.block.ruler.before('fence', `${type}_definition`, defineContainer)
md.renderer.rules[`${type}_container`] = render ?? defaultRender md.renderer.rules[`${type}_container`] = render ?? defaultRender
} }

View File

@ -2,6 +2,11 @@ import type { Markdown } from 'vuepress/markdown'
import { resolveAttrs } from '.././utils/resolveAttrs.js' import { resolveAttrs } from '.././utils/resolveAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Demo wrapper attributes
*
*
*/
interface DemoWrapperAttrs { interface DemoWrapperAttrs {
title?: string title?: string
img?: string img?: string
@ -10,8 +15,14 @@ interface DemoWrapperAttrs {
} }
/** /**
* :::demo-wrapper img no-padding title="xxx" height="100px" * Demo wrapper plugin - Enable demo wrapper container
* ::: *
* -
*
* Syntax: :::demo-wrapper img no-padding title="xxx" height="100px"
* :::demo-wrapper img no-padding title="xxx" height="100px"
*
* @param md - Markdown instance / Markdown
*/ */
export function demoWrapperPlugin(md: Markdown): void { export function demoWrapperPlugin(md: Markdown): void {
createContainerPlugin(md, 'demo-wrapper', { createContainerPlugin(md, 'demo-wrapper', {

View File

@ -10,16 +10,35 @@ import { encryptContent } from '../utils/encryptContent'
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
import { createContainerSyntaxPlugin } from './createContainer' import { createContainerSyntaxPlugin } from './createContainer'
/**
* Encryption options
*
*
*/
interface EncryptOptions { interface EncryptOptions {
password: string password: string
salt: Uint8Array salt: Uint8Array
iv: Uint8Array iv: Uint8Array
} }
/**
* Encrypt plugin - Enable encrypted content container
*
* -
*
* @param app - VuePress app / VuePress
* @param md - Markdown instance / Markdown
* @param options - Encrypt snippet options /
*/
export function encryptPlugin(app: App, md: Markdown, options: EncryptSnippetOptions): void { export function encryptPlugin(app: App, md: Markdown, options: EncryptSnippetOptions): void {
const encrypted: Set<string> = new Set() const encrypted: Set<string> = new Set()
const entryFile = 'internal/encrypt-snippets/index.js' const entryFile = 'internal/encrypt-snippets/index.js'
/**
* Write encrypted content to temp file
*
*
*/
const writeTemp = async ( const writeTemp = async (
hash: string, hash: string,
content: string, content: string,
@ -29,6 +48,11 @@ export function encryptPlugin(app: App, md: Markdown, options: EncryptSnippetOpt
await app.writeTemp(`internal/encrypt-snippets/${hash}.js`, `export default ${JSON.stringify(encrypted)}`) await app.writeTemp(`internal/encrypt-snippets/${hash}.js`, `export default ${JSON.stringify(encrypted)}`)
} }
/**
* Write entry file with all encrypted snippets
*
*
*/
const writeEntry = debounce(150, async () => { const writeEntry = debounce(150, async () => {
let content = `export default {\n` let content = `export default {\n`
for (const hash of encrypted) { for (const hash of encrypted) {
@ -39,11 +63,17 @@ export function encryptPlugin(app: App, md: Markdown, options: EncryptSnippetOpt
}) })
if (!fs.existsSync(app.dir.temp(entryFile))) { if (!fs.existsSync(app.dir.temp(entryFile))) {
// 初始化 // Initialize
app.writeTemp(entryFile, 'export default {}\n') app.writeTemp(entryFile, 'export default {}\n')
} }
const localKeys = Object.keys(app.options.locales || {}).filter(key => key !== '/') const localKeys = Object.keys(app.options.locales || {}).filter(key => key !== '/')
/**
* Get locale from relative path
*
*
*/
const getLocale = (relativePath: string) => { const getLocale = (relativePath: string) => {
const relative = ensureLeadingSlash(relativePath) const relative = ensureLeadingSlash(relativePath)
return localKeys.find(key => relative.startsWith(key)) || '/' return localKeys.find(key => relative.startsWith(key)) || '/'

View File

@ -4,6 +4,11 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Field attributes
*
*
*/
interface FieldAttrs { interface FieldAttrs {
name: string name: string
type?: string type?: string
@ -13,6 +18,16 @@ interface FieldAttrs {
default?: string default?: string
} }
/**
* Field plugin - Enable field container for API documentation
*
* - API
*
* Syntax: ::: field name="xxx" type="string" required
* ::: field name="xxx" type="string" required
*
* @param md - Markdown instance / Markdown
*/
export function fieldPlugin(md: Markdown): void { export function fieldPlugin(md: Markdown): void {
createContainerPlugin(md, 'field', { createContainerPlugin(md, 'field', {
before: (info) => { before: (info) => {

View File

@ -7,6 +7,8 @@ import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerSyntaxPlugin } from './createContainer.js' import { createContainerSyntaxPlugin } from './createContainer.js'
/** /**
* File tree node structure
*
* *
*/ */
interface FileTreeNode extends FileTreeNodeProps { interface FileTreeNode extends FileTreeNodeProps {
@ -15,6 +17,8 @@ interface FileTreeNode extends FileTreeNodeProps {
} }
/** /**
* File tree container attributes
*
* *
*/ */
interface FileTreeAttrs { interface FileTreeAttrs {
@ -23,6 +27,8 @@ interface FileTreeAttrs {
} }
/** /**
* File tree node props (for rendering component)
*
* *
*/ */
export interface FileTreeNodeProps { export interface FileTreeNodeProps {
@ -36,25 +42,28 @@ export interface FileTreeNodeProps {
} }
/** /**
* Parse raw file tree content to node tree structure
*
* *
* @param content *
* @returns * @param content - Raw file tree text content /
* @returns File tree node array /
*/ */
export function parseFileTreeRawContent(content: string): FileTreeNode[] { export function parseFileTreeRawContent(content: string): FileTreeNode[] {
const root: FileTreeNode = { level: -1, children: [] } as unknown as FileTreeNode const root: FileTreeNode = { level: -1, children: [] } as unknown as FileTreeNode
const stack: FileTreeNode[] = [root] const stack: FileTreeNode[] = [root]
const lines = content.trimEnd().split('\n') const lines = content.trimEnd().split('\n')
const spaceLength = lines[0].match(/^\s*/)?.[0].length ?? 0 // 去除行首空格/) const spaceLength = lines[0].match(/^\s*/)?.[0].length ?? 0 // Remove leading spaces
for (const line of lines) { for (const line of lines) {
const match = line.match(/^(\s*)-(.*)$/) const match = line.match(/^(\s*)-(.*)$/)
if (!match) if (!match)
continue continue
const level = Math.floor((match[1].length - spaceLength) / 2) // 每两个空格为一个层级 const level = Math.floor((match[1].length - spaceLength) / 2) // Two spaces per level
const info = match[2].trim() const info = match[2].trim()
// 检索当前层级的父节点 // Find parent node at current level
while (stack.length > 0 && stack[stack.length - 1].level >= level) { while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop() stack.pop()
} }
@ -68,12 +77,20 @@ export function parseFileTreeRawContent(content: string): FileTreeNode[] {
return root.children return root.children
} }
/**
* Regex for focus marker
*
*
*/
const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/ const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/
/** /**
* Parse single node info string, extract filename, comment, type, etc.
*
* info * info
* @param info *
* @returns * @param info - Node description string /
* @returns File tree node props /
*/ */
export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps { export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let filename = '' let filename = ''
@ -83,7 +100,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let type: 'folder' | 'file' = 'file' let type: 'folder' | 'file' = 'file'
let diff: 'add' | 'remove' | undefined let diff: 'add' | 'remove' | undefined
// 处理 diff 标记 // Process diff marker
if (info.startsWith('++')) { if (info.startsWith('++')) {
info = info.slice(2).trim() info = info.slice(2).trim()
diff = 'add' diff = 'add'
@ -93,14 +110,14 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
diff = 'remove' diff = 'remove'
} }
// 处理高亮focus标记 // Process focus marker
info = info.replace(RE_FOCUS, (_, matched) => { info = info.replace(RE_FOCUS, (_, matched) => {
filename = matched filename = matched
focus = true focus = true
return '' return ''
}) })
// 提取文件名和注释 // Extract filename and comment
if (filename === '' && !focus) { if (filename === '' && !focus) {
const spaceIndex = info.indexOf(' ') const spaceIndex = info.indexOf(' ')
filename = info.slice(0, spaceIndex === -1 ? info.length : spaceIndex) filename = info.slice(0, spaceIndex === -1 ? info.length : spaceIndex)
@ -109,7 +126,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
comment = info.trim() comment = info.trim()
// 判断是否为文件夹 // Determine if folder
if (filename.endsWith('/')) { if (filename.endsWith('/')) {
type = 'folder' type = 'folder'
expanded = false expanded = false
@ -120,9 +137,13 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
} }
/** /**
* File tree markdown plugin main function
*
* markdown * markdown
* @param md markdown *
* @param options * @param md - Markdown instance / markdown
* @param options - File tree render options /
* @param locales - Locale data /
*/ */
export function fileTreePlugin( export function fileTreePlugin(
md: Markdown, md: Markdown,
@ -130,6 +151,8 @@ export function fileTreePlugin(
locales: Record<string, CommonLocaleData>, locales: Record<string, CommonLocaleData>,
): void { ): void {
/** /**
* Get file or folder icon
*
* *
*/ */
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => { const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
@ -140,6 +163,8 @@ export function fileTreePlugin(
} }
/** /**
* Recursively render file tree nodes
*
* *
*/ */
const renderFileTree = (nodes: FileTreeNode[], meta: FileTreeAttrs): string => const renderFileTree = (nodes: FileTreeNode[], meta: FileTreeAttrs): string =>
@ -147,7 +172,7 @@ export function fileTreePlugin(
const { level, children, filename, comment, focus, expanded, type, diff } = node const { level, children, filename, comment, focus, expanded, type, diff } = node
const isOmit = filename === '…' || filename === '...' /* fallback */ const isOmit = filename === '…' || filename === '...' /* fallback */
// 文件夹无子节点时补充省略号 // Add ellipsis for folder without children
if (children.length === 0 && type === 'folder') { if (children.length === 0 && type === 'folder') {
children.push({ level: level + 1, children: [], filename: '…', type: 'file' } as unknown as FileTreeNode) children.push({ level: level + 1, children: [], filename: '…', type: 'file' } as unknown as FileTreeNode)
} }
@ -172,7 +197,7 @@ ${renderedIcon}${renderedComment}${children.length > 0 ? renderFileTree(children
</FileTreeNode>` </FileTreeNode>`
}).join('\n') }).join('\n')
// 注册自定义容器语法插件 // Register custom container syntax plugin
return createContainerSyntaxPlugin( return createContainerSyntaxPlugin(
md, md,
'file-tree', 'file-tree',
@ -192,6 +217,15 @@ ${renderedIcon}${renderedComment}${children.length > 0 ? renderFileTree(children
) )
} }
/**
* Convert file tree to command line text format
*
*
*
* @param nodes - File tree nodes /
* @param prefix - Line prefix /
* @returns CMD text / CMD
*/
function fileTreeToCMDText(nodes: FileTreeNode[], prefix = ''): string { function fileTreeToCMDText(nodes: FileTreeNode[], prefix = ''): string {
let content = prefix ? '' : '.\n' let content = prefix ? '' : '.\n'
for (let i = 0, l = nodes.length; i < l; i++) { for (let i = 0, l = nodes.length; i < l; i++) {

View File

@ -21,6 +21,16 @@ import { tablePlugin } from './table.js'
import { tabs } from './tabs.js' import { tabs } from './tabs.js'
import { timelinePlugin } from './timeline.js' import { timelinePlugin } from './timeline.js'
/**
* Container plugin - Register all container plugins
*
* -
*
* @param app - VuePress app / VuePress
* @param md - Markdown instance / Markdown
* @param options - Plugin options /
* @param locales - Locale configuration /
*/
export async function containerPlugin( export async function containerPlugin(
app: App, app: App,
md: Markdown, md: Markdown,

View File

@ -8,11 +8,32 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Code REPL metadata
*
* REPL
*/
interface CodeReplMeta { interface CodeReplMeta {
editable?: boolean editable?: boolean
title?: string title?: string
} }
/**
* Language REPL plugin - Enable interactive code playgrounds
*
* REPL -
*
* Supports: kotlin, go, rust, python
*
* @param app - VuePress app / VuePress
* @param md - Markdown-it instance / Markdown-it
* @param options - REPL options / REPL
* @param options.theme - Editor theme /
* @param options.go - Enable Go playground / Go
* @param options.kotlin - Enable Kotlin playground / Kotlin
* @param options.rust - Enable Rust playground / Rust
* @param options.python - Enable Python playground / Python
*/
export async function langReplPlugin(app: App, md: markdownIt, { export async function langReplPlugin(app: App, md: markdownIt, {
theme, theme,
go = false, go = false,
@ -20,6 +41,11 @@ export async function langReplPlugin(app: App, md: markdownIt, {
rust = false, rust = false,
python = false, python = false,
}: ReplOptions): Promise<void> { }: ReplOptions): Promise<void> {
/**
* Create container for specific language
*
*
*/
const container = (lang: string): void => createContainerPlugin(md, `${lang}-repl`, { const container = (lang: string): void => createContainerPlugin(md, `${lang}-repl`, {
before(info) { before(info) {
const { attrs } = resolveAttrs<CodeReplMeta>(info) const { attrs } = resolveAttrs<CodeReplMeta>(info)
@ -85,6 +111,14 @@ export async function langReplPlugin(app: App, md: markdownIt, {
) )
} }
/**
* Read JSON file
*
* JSON
*
* @param file - File path /
* @returns Parsed JSON / JSON
*/
async function read(file: string): Promise<any> { async function read(file: string): Promise<any> {
try { try {
const content = await fs.readFile(file, 'utf-8') const content = await fs.readFile(file, 'utf-8')

View File

@ -1,4 +1,6 @@
/** /**
* npm-to container plugin
*
* npm [npm, pnpm, yarn, bun, deno] * npm [npm, pnpm, yarn, bun, deno]
* *
* ::: npm-to * ::: npm-to
@ -34,7 +36,12 @@ import { createContainerPlugin } from './createContainer.js'
import { ALLOW_LIST, BOOL_FLAGS, DEFAULT_TABS, MANAGERS_CONFIG } from './npmToPreset.js' import { ALLOW_LIST, BOOL_FLAGS, DEFAULT_TABS, MANAGERS_CONFIG } from './npmToPreset.js'
/** /**
* npm-to plugin - Convert npm commands to multiple package manager commands
*
* npm-to npm * npm-to npm
*
* @param md - Markdown instance / Markdown
* @param options - npm-to options / npm-to
*/ */
export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void { export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
const opt = isArray(options) ? { tabs: options } : options const opt = isArray(options) ? { tabs: options } : options
@ -50,14 +57,14 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
token.hidden = true token.hidden = true
token.type = 'text' token.type = 'text'
token.content = '' token.content = ''
// 拆分命令行内容,转换为多包管理器命令 // Split command line content, convert to multiple package manager commands
const lines = content.split(/(\n|\s*&&\s*)/) const lines = content.split(/(\n|\s*&&\s*)/)
return md.render( return md.render(
resolveNpmTo(lines, token.info.trim(), idx, tabs), resolveNpmTo(lines, token.info.trim(), idx, tabs),
cleanMarkdownEnv(env), cleanMarkdownEnv(env),
) )
} }
// 非法容器警告 // Invalid container warning
logger.warn('npm-to', `Invalid npm-to container in ${colors.gray(env.filePathRelative || env.filePath)}`) logger.warn('npm-to', `Invalid npm-to container in ${colors.gray(env.filePathRelative || env.filePath)}`)
return '' return ''
}, },
@ -66,11 +73,15 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
} }
/** /**
* Convert npm commands to package manager command groups
*
* npm * npm
* @param lines *
* @param info * @param lines - Command line array /
* @param idx token * @param info - Code block type /
* @param tabs * @param idx - Token index / token
* @param tabs - Package managers to support /
* @returns code-tabs formatted string / code-tabs
*/ */
function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string { function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string {
tabs = validateTabs(tabs) tabs = validateTabs(tabs)
@ -81,7 +92,7 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
for (const line of lines) { for (const line of lines) {
const config = findConfig(line) const config = findConfig(line)
if (config && config[tab]) { if (config && config[tab]) {
// 解析并替换命令参数 // Parse and replace command arguments
const parsed = (map[line] ??= parseLine(line)) as LineParsed const parsed = (map[line] ??= parseLine(line)) as LineParsed
const { cli, flags } = config[tab] as CommandConfigItem const { cli, flags } = config[tab] as CommandConfigItem
@ -105,14 +116,19 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
newLines.push(line) newLines.push(line)
} }
} }
// 拼接为 code-tabs 格式 // Concatenate as code-tabs format
res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\n\`\`\``) res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\n\`\`\``)
} }
return `:::code-tabs#npm-to-${tabs.join('-')}\n${res.join('\n')}\n:::` return `:::code-tabs#npm-to-${tabs.join('-')}\n${res.join('\n')}\n:::`
} }
/** /**
* Find package manager config by command line content
*
* *
*
* @param line - Command line /
* @returns Command config /
*/ */
function findConfig(line: string): CommandConfig | undefined { function findConfig(line: string): CommandConfig | undefined {
for (const { pattern, ...config } of Object.values(MANAGERS_CONFIG)) { for (const { pattern, ...config } of Object.values(MANAGERS_CONFIG)) {
@ -124,7 +140,12 @@ function findConfig(line: string): CommandConfig | undefined {
} }
/** /**
* Validate tabs, return allowed package manager list
*
* tabs * tabs
*
* @param tabs - Package managers /
* @returns Validated tabs /
*/ */
function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] { function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
tabs = tabs.filter(tab => ALLOW_LIST.includes(tab)) tabs = tabs.filter(tab => ALLOW_LIST.includes(tab))
@ -135,20 +156,32 @@ function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
} }
/** /**
* Command line parse result type
*
* *
*/ */
interface LineParsed { interface LineParsed {
env: string // 环境变量前缀 env: string // Environment variable prefix / 环境变量前缀
cli: string // 命令行工具npm/npx ... cli: string // CLI tool (npm/npx ...) / 命令行工具
cmd: string // 命令/脚本名 cmd: string // Command/script name / 命令/脚本名
args?: string // 参数 args?: string // Arguments / 参数
scriptArgs?: string // 脚本参数 scriptArgs?: string // Script arguments / 脚本参数
} }
/**
* Regex for parsing npm/npx commands
*
* npm/npx
*/
const LINE_REG = /(.*)(npm|npx)\s+(.*)/ const LINE_REG = /(.*)(npm|npx)\s+(.*)/
/** /**
* Parse a line of npm/npx command, split env, command, args, etc.
*
* npm/npx * npm/npx
*
* @param line - Command line /
* @returns Parsed result /
*/ */
export function parseLine(line: string): false | LineParsed { export function parseLine(line: string): false | LineParsed {
const match = line.match(LINE_REG) const match = line.match(LINE_REG)
@ -177,7 +210,12 @@ export function parseLine(line: string): false | LineParsed {
} }
/** /**
* Parse npm command arguments, distinguish command, args, script args
*
* npm * npm
*
* @param line - Arguments line /
* @returns Parsed args /
*/ */
function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } { function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } {
line = line?.trim() line = line?.trim()
@ -186,7 +224,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
let cmd = '' let cmd = ''
let args = '' let args = ''
if (npmArgs[0] !== '-') { if (npmArgs[0] !== '-') {
// 处理命令和参数 // Process command and args
if (npmArgs[0] === '"' || npmArgs[0] === '\'') { if (npmArgs[0] === '"' || npmArgs[0] === '\'') {
const idx = npmArgs.slice(1).indexOf(npmArgs[0]) const idx = npmArgs.slice(1).indexOf(npmArgs[0])
cmd = npmArgs.slice(0, idx + 2) cmd = npmArgs.slice(0, idx + 2)
@ -204,7 +242,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
} }
} }
else { else {
// 处理仅有参数的情况 // Process args only
let newLine = '' let newLine = ''
let value = '' let value = ''
let isQuote = false let isQuote = false

View File

@ -1,22 +1,62 @@
import type { NpmToPackageManager } from '../../shared/index.js' import type { NpmToPackageManager } from '../../shared/index.js'
/**
* Package command types
*
*
*/
export type PackageCommand = 'install' | 'add' | 'remove' | 'run' | 'create' | 'init' | 'npx' | 'ci' export type PackageCommand = 'install' | 'add' | 'remove' | 'run' | 'create' | 'init' | 'npx' | 'ci'
/**
* Command config item
*
*
*/
export interface CommandConfigItem { export interface CommandConfigItem {
cli: string cli: string
flags?: Record<string, string> flags?: Record<string, string>
} }
/**
* Command config for package managers (excluding npm)
*
* npm
*/
export type CommandConfig = Record<Exclude<NpmToPackageManager, 'npm'>, CommandConfigItem | false> export type CommandConfig = Record<Exclude<NpmToPackageManager, 'npm'>, CommandConfigItem | false>
/**
* Command configs for all package commands
*
*
*/
export type CommandConfigs = Record<PackageCommand, { pattern: RegExp } & CommandConfig> export type CommandConfigs = Record<PackageCommand, { pattern: RegExp } & CommandConfig>
/**
* Allowed package managers list
*
*
*/
export const ALLOW_LIST = ['npm', 'pnpm', 'yarn', 'bun', 'deno'] as const export const ALLOW_LIST = ['npm', 'pnpm', 'yarn', 'bun', 'deno'] as const
/**
* Boolean flags for npm commands
*
* npm
*/
export const BOOL_FLAGS: string[] = ['--no-save', '-B', '--save-bundle', '--save-dev', '-D', '--save-prod', '-P', '--save-peer', '-O', '--save-optional', '-E', '--save-exact', '-y', '--yes', '-g', '--global'] export const BOOL_FLAGS: string[] = ['--no-save', '-B', '--save-bundle', '--save-dev', '-D', '--save-prod', '-P', '--save-peer', '-O', '--save-optional', '-E', '--save-exact', '-y', '--yes', '-g', '--global']
/**
* Default tabs to display
*
*
*/
export const DEFAULT_TABS: NpmToPackageManager[] = ['npm', 'pnpm', 'yarn'] export const DEFAULT_TABS: NpmToPackageManager[] = ['npm', 'pnpm', 'yarn']
/**
* Package manager configurations
*
*
*/
export const MANAGERS_CONFIG: CommandConfigs = { export const MANAGERS_CONFIG: CommandConfigs = {
install: { install: {
pattern: /(?:^|\s)npm\s+(?:install|i)$/, pattern: /(?:^|\s)npm\s+(?:install|i)$/,

View File

@ -2,6 +2,12 @@ import type { Markdown } from 'vuepress/markdown'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/** /**
* Steps container plugin
*
*
*
* Syntax:
* ```md
* :::steps * :::steps
* 1. 1 * 1. 1
* xxx * xxx
@ -9,6 +15,9 @@ import { createContainerPlugin } from './createContainer.js'
* xxx * xxx
* 3. ... * 3. ...
* ::: * :::
* ```
*
* @param md - Markdown instance / Markdown
*/ */
export function stepsPlugin(md: Markdown): void { export function stepsPlugin(md: Markdown): void {
createContainerPlugin(md, 'steps', { createContainerPlugin(md, 'steps', {

View File

@ -4,19 +4,30 @@ import { encodeData } from '@vuepress/helper'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerSyntaxPlugin } from './createContainer.js' import { createContainerSyntaxPlugin } from './createContainer.js'
/**
* Table container attributes
*
*
*/
export interface TableContainerAttrs extends TableContainerOptions { export interface TableContainerAttrs extends TableContainerOptions {
/** /**
* Table title
*
* *
*/ */
title?: string title?: string
/** /**
* Highlighted rows
*
* *
* *
* @example hl-rows="warning:1,2,3;error:4,5,6" * @example hl-rows="warning:1,2,3;error:4,5,6"
*/ */
hlRows?: string hlRows?: string
/** /**
* Highlighted columns
*
* *
* *
* @example hl-cols="warning:1;error:2,3" * @example hl-cols="warning:1;error:2,3"
@ -24,6 +35,8 @@ export interface TableContainerAttrs extends TableContainerOptions {
hlCols?: string hlCols?: string
/** /**
* Highlighted cells
*
* *
* *
* @example hl-cells="warning:(1,2)(2,3);error:(3,4)(4,5)" * @example hl-cells="warning:(1,2)(2,3);error:(3,4)(4,5)"
@ -32,7 +45,9 @@ export interface TableContainerAttrs extends TableContainerOptions {
} }
/** /**
* * Table plugin - Wrap table with container for enhanced features
*
* -
* *
* @example * @example
* ```md * ```md
@ -43,6 +58,9 @@ export interface TableContainerAttrs extends TableContainerOptions {
* | xx | xx | xx | * | xx | xx | xx |
* ::: * :::
* ``` * ```
*
* @param md - Markdown instance / Markdown
* @param options - Table container options /
*/ */
export function tablePlugin(md: Markdown, options: TableContainerOptions = {}): void { export function tablePlugin(md: Markdown, options: TableContainerOptions = {}): void {
createContainerSyntaxPlugin(md, 'table', (tokens, index, opt, env) => { createContainerSyntaxPlugin(md, 'table', (tokens, index, opt, env) => {
@ -96,6 +114,14 @@ export function tablePlugin(md: Markdown, options: TableContainerOptions = {}):
}) })
} }
/**
* Parse highlight string
*
*
*
* @param hl - Highlight string /
* @returns Parsed highlight map /
*/
function parseHl(hl: string) { function parseHl(hl: string) {
const res: Record<number, string> = {} const res: Record<number, string> = {}
if (!hl) if (!hl)
@ -110,6 +136,14 @@ function parseHl(hl: string) {
return res return res
} }
/**
* Parse highlight cells string
*
*
*
* @param hl - Highlight cells string /
* @returns Parsed highlight cells map /
*/
function parseHlCells(hl: string) { function parseHlCells(hl: string) {
const res: Record<string, Record<number, string>> = {} const res: Record<string, Record<number, string>> = {}
if (!hl) if (!hl)

View File

@ -3,6 +3,23 @@ import { tab } from '@mdit/plugin-tab'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
import { stringifyProp } from '../utils/stringifyProp.js' import { stringifyProp } from '../utils/stringifyProp.js'
/**
* Tabs plugin - Enable tabs container
*
* -
*
* Syntax:
* ```md
* :::tabs
* @tab Tab 1
* Content 1
* @tab Tab 2
* Content 2
* :::
* ```
*
* @param md - Markdown-it instance / Markdown-it
*/
export const tabs: PluginSimple = (md) => { export const tabs: PluginSimple = (md) => {
tab(md, { tab(md, {
name: 'tabs', name: 'tabs',

View File

@ -1,4 +1,10 @@
/** /**
* Timeline container plugin
*
* 线
*
* Syntax:
* ```md
* ::: timeline * ::: timeline
* *
* - title * - title
@ -11,6 +17,7 @@
* *
* content * content
* ::: * :::
* ```
*/ */
import type Token from 'markdown-it/lib/token.mjs' import type Token from 'markdown-it/lib/token.mjs'
import type { Markdown } from 'vuepress/markdown' import type { Markdown } from 'vuepress/markdown'
@ -19,6 +26,11 @@ import { resolveAttrs } from '.././utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js' import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js' import { createContainerPlugin } from './createContainer.js'
/**
* Timeline attributes
*
* 线
*/
export interface TimelineAttrs { export interface TimelineAttrs {
horizontal?: boolean horizontal?: boolean
card?: boolean card?: boolean
@ -26,6 +38,11 @@ export interface TimelineAttrs {
line?: string line?: string
} }
/**
* Timeline item metadata
*
* 线
*/
export interface TimelineItemMeta { export interface TimelineItemMeta {
time?: string time?: string
type?: string type?: string
@ -36,10 +53,34 @@ export interface TimelineItemMeta {
placement?: string placement?: string
} }
/**
* Regex for matching attribute keys
*
*
*/
const RE_KEY = /(\w+)=\s*/ const RE_KEY = /(\w+)=\s*/
/**
* Regex for searching next attribute key
*
*
*/
const RE_SEARCH_KEY = /\s+\w+=\s*|$/ const RE_SEARCH_KEY = /\s+\w+=\s*|$/
/**
* Regex for cleaning quote values
*
*
*/
const RE_CLEAN_VALUE = /(?<quote>["'])(.*?)(\k<quote>)/ const RE_CLEAN_VALUE = /(?<quote>["'])(.*?)(\k<quote>)/
/**
* Timeline plugin - Enable timeline container
*
* 线 - 线
*
* @param md - Markdown instance / Markdown
*/
export function timelinePlugin(md: Markdown): void { export function timelinePlugin(md: Markdown): void {
createContainerPlugin(md, 'timeline', { createContainerPlugin(md, 'timeline', {
before(info, tokens, index) { before(info, tokens, index) {
@ -65,17 +106,25 @@ export function timelinePlugin(md: Markdown): void {
md.renderer.rules.timeline_item_title_close = () => '</template>' md.renderer.rules.timeline_item_title_close = () => '</template>'
} }
/**
* Parse timeline tokens
*
* 线
*
* @param tokens - Token array /
* @param index - Start index /
*/
function parseTimeline(tokens: Token[], index: number) { function parseTimeline(tokens: Token[], index: number) {
const listStack: number[] = [] // 记录列表嵌套深度 const listStack: number[] = [] // Track list nesting depth
for (let i = index + 1; i < tokens.length; i++) { for (let i = index + 1; i < tokens.length; i++) {
const token = tokens[i] const token = tokens[i]
if (token.type === 'container_timeline_close') { if (token.type === 'container_timeline_close') {
break break
} }
// 列表层级追踪 // Track list level
if (token.type === 'bullet_list_open') { if (token.type === 'bullet_list_open') {
listStack.push(0) // 每个新列表初始层级为0 listStack.push(0) // Each new list starts at level 0
if (listStack.length === 1) if (listStack.length === 1)
token.hidden = true token.hidden = true
} }
@ -86,7 +135,7 @@ function parseTimeline(tokens: Token[], index: number) {
} }
else if (token.type === 'list_item_open') { else if (token.type === 'list_item_open') {
const currentLevel = listStack.length const currentLevel = listStack.length
// 仅处理根级列表项层级1 // Only process root level list items (level 1)
if (currentLevel === 1) { if (currentLevel === 1) {
token.type = 'timeline_item_open' token.type = 'timeline_item_open'
tokens[i + 1].type = 'timeline_item_title_open' tokens[i + 1].type = 'timeline_item_title_open'
@ -94,9 +143,9 @@ function parseTimeline(tokens: Token[], index: number) {
// - title // - title
// attrs // attrs
// 列表项 `-` 后面包括紧跟随的后续行均在 type=inline 的 token 中, 并作为 children // List item `-` followed by subsequent lines are in type=inline token as children
const inlineToken = tokens[i + 2] const inlineToken = tokens[i + 2]
// 找到最后一个 softbreak最后一行作为 attrs 进行解析 // Find last softbreak, last line as attrs
const softbreakIndex = inlineToken.children!.findLastIndex( const softbreakIndex = inlineToken.children!.findLastIndex(
token => token.type === 'softbreak', token => token.type === 'softbreak',
) )
@ -121,39 +170,47 @@ function parseTimeline(tokens: Token[], index: number) {
} }
} }
/**
* Extract timeline attributes from raw text
*
* 线
*
* @param rawText - Raw attribute text /
* @returns Timeline item metadata / 线
*/
export function extractTimelineAttributes(rawText: string): TimelineItemMeta { export function extractTimelineAttributes(rawText: string): TimelineItemMeta {
const attrKeys = ['time', 'type', 'icon', 'line', 'color', 'card', 'placement'] as const const attrKeys = ['time', 'type', 'icon', 'line', 'color', 'card', 'placement'] as const
const attrs: TimelineItemMeta = {} const attrs: TimelineItemMeta = {}
let buffer = rawText.trim() let buffer = rawText.trim()
while (buffer.length) { while (buffer.length) {
// 匹配属性键 (支持大小写) // Match attribute key (case insensitive)
const keyMatch = buffer.match(RE_KEY) const keyMatch = buffer.match(RE_KEY)
if (!keyMatch) { if (!keyMatch) {
break break
} }
// 提取可能的关键字 // Extract possible keyword
const matchedKey = keyMatch[1].toLowerCase() const matchedKey = keyMatch[1].toLowerCase()
if (!attrKeys.includes(matchedKey as any)) { if (!attrKeys.includes(matchedKey as any)) {
break break
} }
const keyStart = keyMatch.index! const keyStart = keyMatch.index!
// 跳过已匹配的 key: // Skip matched key:
const keyEnd = keyStart + keyMatch[0].length const keyEnd = keyStart + keyMatch[0].length
buffer = buffer.slice(keyEnd) buffer = buffer.slice(keyEnd)
// 提取属性值 (到下一个属性或行尾) // Extract attribute value (to next attribute or end of line)
let valueEnd = buffer.search(RE_SEARCH_KEY) let valueEnd = buffer.search(RE_SEARCH_KEY)
/* istanbul ignore if -- @preserve */ /* istanbul ignore if -- @preserve */
if (valueEnd === -1) if (valueEnd === -1)
valueEnd = buffer.length valueEnd = buffer.length
const value = buffer.slice(0, valueEnd).trim() const value = buffer.slice(0, valueEnd).trim()
// 存储属性 // Store attribute
attrs[matchedKey as keyof TimelineItemMeta] = value.replace(RE_CLEAN_VALUE, '$2') attrs[matchedKey as keyof TimelineItemMeta] = value.replace(RE_CLEAN_VALUE, '$2')
// 跳过已处理的值 // Skip processed value
buffer = buffer.slice(valueEnd) buffer = buffer.slice(valueEnd)
} }

View File

@ -1,23 +1,78 @@
import type { RuleOptions } from 'markdown-it/lib/ruler.mjs' import type { RuleOptions } from 'markdown-it/lib/ruler.mjs'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown' import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
/**
* Embed rule block options
*
*
*
* @typeParam Meta - Metadata type /
*/
export interface EmbedRuleBlockOptions<Meta extends Record<string, any>> { export interface EmbedRuleBlockOptions<Meta extends Record<string, any>> {
/** /**
* @[type]() * Embed type syntax: @[type]()
*
* @[type]()
*/ */
type: string type: string
/** /**
* token name * Token name
*
*
*/ */
name?: string name?: string
/**
* Name of the rule to insert before
*
*
*/
beforeName?: string beforeName?: string
/**
* Syntax pattern regular expression
*
*
*/
syntaxPattern: RegExp syntaxPattern: RegExp
/**
* Rule options
*
*
*/
ruleOptions?: RuleOptions ruleOptions?: RuleOptions
/**
* Extract metadata from match
*
*
*
* @param match - RegExp match array /
* @returns Metadata object /
*/
meta: (match: RegExpMatchArray) => Meta meta: (match: RegExpMatchArray) => Meta
/**
* Generate content from metadata
*
*
*
* @param meta - Metadata /
* @param content - Original content /
* @param env - Markdown environment / Markdown
* @returns Generated content /
*/
content: (meta: Meta, content: string, env: MarkdownEnv) => string content: (meta: Meta, content: string, env: MarkdownEnv) => string
} }
// @[name]() /**
* Create embed rule block
*
*
*
* Syntax: @[name]()
* @[name]()
*
* @param md - Markdown instance / Markdown
* @param options - Embed rule block options /
* @typeParam Meta - Metadata type /
*/
export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<string, any>>( export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<string, any>>(
md: Markdown, md: Markdown,
{ {
@ -41,22 +96,26 @@ export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<s
const max = state.eMarks[startLine] const max = state.eMarks[startLine]
// return false if the length is shorter than min length // return false if the length is shorter than min length
// 如果长度小于最小长度,返回 false
if (pos + MIN_LENGTH > max) if (pos + MIN_LENGTH > max)
return false return false
// check if it's matched the start // check if it's matched the start
// 检查是否匹配开始
for (let i = 0; i < START_CODES.length; i += 1) { for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i]) if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false return false
} }
// check if it's matched the syntax // check if it's matched the syntax
// 检查是否匹配语法
const content = state.src.slice(pos, max) const content = state.src.slice(pos, max)
const match = content.match(syntaxPattern) const match = content.match(syntaxPattern)
if (!match) if (!match)
return false return false
// return true as we have matched the syntax // return true as we have matched the syntax
// 返回 true 表示已匹配语法
/* istanbul ignore if -- @preserve */ /* istanbul ignore if -- @preserve */
if (silent) if (silent)
return true return true

View File

@ -13,6 +13,14 @@ import { artPlayerPlugin } from './video/artPlayer.js'
import { bilibiliPlugin } from './video/bilibili.js' import { bilibiliPlugin } from './video/bilibili.js'
import { youtubePlugin } from './video/youtube.js' import { youtubePlugin } from './video/youtube.js'
/**
* Embed syntax plugin - Register all embed syntax plugins
*
* -
*
* @param md - Markdown instance / Markdown
* @param options - Plugin options /
*/
export function embedSyntaxPlugin(md: Markdown, options: MarkdownPowerPluginOptions): void { export function embedSyntaxPlugin(md: Markdown, options: MarkdownPowerPluginOptions): void {
if (options.caniuse) { if (options.caniuse) {
const caniuse = options.caniuse === true ? {} : options.caniuse const caniuse = options.caniuse === true ? {} : options.caniuse

View File

@ -1,10 +1,24 @@
import type { Markdown, MarkdownEnv } from 'vuepress/markdown' import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
/**
* Regular expression for matching h1 heading
*
* h1
*/
const REG_HEADING = /^#\s*?([^#\s].*)?\n/ const REG_HEADING = /^#\s*?([^#\s].*)?\n/
/** /**
* markdown h1 frontmatter * Docs title plugin - Extract h1 title from markdown to frontmatter
*
* - markdown h1 frontmatter
*
* Adapts to theme's document page title by extracting h1 title from markdown to frontmatter
* and removing it from content to avoid duplicate display.
*
* markdown h1 frontmatter
* *
*
* @param md - Markdown instance / Markdown
*/ */
export function docsTitlePlugin(md: Markdown): void { export function docsTitlePlugin(md: Markdown): void {
const render = md.render const render = md.render
@ -28,6 +42,14 @@ export function docsTitlePlugin(md: Markdown): void {
} }
} }
/**
* Parse markdown source to separate frontmatter and content
*
* markdown frontmatter
*
* @param source - Markdown source / Markdown
* @returns Object with matter and content / matter content
*/
function parseSource(source: string) { function parseSource(source: string) {
const char = '---' const char = '---'

View File

@ -10,14 +10,49 @@ import imageSize from 'image-size'
import { fs, logger, path } from 'vuepress/utils' import { fs, logger, path } from 'vuepress/utils'
import { resolveAttrs } from '../utils/resolveAttrs.js' import { resolveAttrs } from '../utils/resolveAttrs.js'
/**
* Image size interface
*
*
*/
interface ImgSize { interface ImgSize {
/**
* Image width
*
*
*/
width: number width: number
/**
* Image height
*
*
*/
height: number height: number
} }
/**
* Regular expression for matching markdown image syntax
*
* markdown
*/
const REG_IMG = /!\[.*?\]\(.*?\)/g const REG_IMG = /!\[.*?\]\(.*?\)/g
/**
* Regular expression for matching HTML img tag
*
* HTML img
*/
const REG_IMG_TAG = /<img(.*?)>/g const REG_IMG_TAG = /<img(.*?)>/g
/**
* Regular expression for matching src/srcset attribute
*
* src/srcset
*/
const REG_IMG_TAG_SRC = /src(?:set)?=(['"])(.+?)\1/g const REG_IMG_TAG_SRC = /src(?:set)?=(['"])(.+?)\1/g
/**
* List of badge URLs to exclude
*
* URL
*/
const BADGE_LIST = [ const BADGE_LIST = [
'https://img.shields.io', 'https://img.shields.io',
'https://badge.fury.io', 'https://badge.fury.io',
@ -26,8 +61,22 @@ const BADGE_LIST = [
'https://vercel.com/button', 'https://vercel.com/button',
] ]
/**
* Cache for image sizes
*
*
*/
const cache = new Map<string, ImgSize>() const cache = new Map<string, ImgSize>()
/**
* Image size plugin - Add width and height attributes to images
*
* -
*
* @param app - VuePress app / VuePress
* @param md - Markdown instance / Markdown
* @param type - Image size type: 'local', 'all', or false / 'local''all' false
*/
export async function imageSizePlugin( export async function imageSizePlugin(
app: App, app: App,
md: Markdown, md: Markdown,
@ -71,6 +120,14 @@ export async function imageSizePlugin(
md.renderer.rules.html_block = createHtmlRule(rawHtmlBlockRule) md.renderer.rules.html_block = createHtmlRule(rawHtmlBlockRule)
md.renderer.rules.html_inline = createHtmlRule(rawHtmlInlineRule) md.renderer.rules.html_inline = createHtmlRule(rawHtmlInlineRule)
/**
* Create HTML rule for processing img tags
*
* img HTML
*
* @param rawHtmlRule - Original HTML rule / HTML
* @returns New HTML rule / HTML
*/
function createHtmlRule(rawHtmlRule: RenderRule): RenderRule { function createHtmlRule(rawHtmlRule: RenderRule): RenderRule {
return (tokens, idx, options, env, self) => { return (tokens, idx, options, env, self) => {
const token = tokens[idx] const token = tokens[idx]
@ -95,6 +152,17 @@ export async function imageSizePlugin(
} }
} }
/**
* Resolve image size from source
*
*
*
* @param src - Image source /
* @param width - Existing width /
* @param height - Existing height /
* @param env - Markdown environment / Markdown
* @returns Image size or false / false
*/
function resolveSize( function resolveSize(
src: string | null | undefined, src: string | null | undefined,
width: string | null | undefined, width: string | null | undefined,
@ -150,6 +218,16 @@ export async function imageSizePlugin(
} }
} }
/**
* Resolve image URL from source
*
* URL
*
* @param src - Image source /
* @param env - Markdown environment / Markdown
* @param app - VuePress app / VuePress
* @returns Resolved image URL / URL
*/
function resolveImageUrl(src: string, env: MarkdownEnv, app: App): string { function resolveImageUrl(src: string, env: MarkdownEnv, app: App): string {
if (src[0] === '/') if (src[0] === '/')
return app.dir.public(src.slice(1)) return app.dir.public(src.slice(1))
@ -164,6 +242,13 @@ function resolveImageUrl(src: string, env: MarkdownEnv, app: App): string {
return path.resolve(src) return path.resolve(src)
} }
/**
* Scan remote image sizes in markdown files
*
* markdown
*
* @param app - VuePress app / VuePress
*/
export async function scanRemoteImageSize(app: App): Promise<void> { export async function scanRemoteImageSize(app: App): Promise<void> {
if (!app.env.isBuild) if (!app.env.isBuild)
return return
@ -194,6 +279,13 @@ export async function scanRemoteImageSize(app: App): Promise<void> {
} }
} }
/**
* Add source to image list
*
*
*
* @param src - Image source /
*/
function addList(src: string) { function addList(src: string) {
if (src && isLinkHttp(src) if (src && isLinkHttp(src)
&& !imgList.includes(src) && !imgList.includes(src)
@ -211,6 +303,14 @@ export async function scanRemoteImageSize(app: App): Promise<void> {
})) }))
} }
/**
* Fetch image size from remote URL
*
* URL
*
* @param src - Image URL / URL
* @returns Image size /
*/
function fetchImageSize(src: string): Promise<ImgSize> { function fetchImageSize(src: string): Promise<ImgSize> {
const link = new URL(src) const link = new URL(src)
@ -248,6 +348,16 @@ function fetchImageSize(src: string): Promise<ImgSize> {
} }
} }
/**
* Resolve image size from URL
*
* URL
*
* @param app - VuePress app / VuePress
* @param url - Image URL / URL
* @param remote - Whether to fetch remote images /
* @returns Image size /
*/
export async function resolveImageSize(app: App, url: string, remote = false): Promise<ImgSize> { export async function resolveImageSize(app: App, url: string, remote = false): Promise<ImgSize> {
if (cache.has(url)) if (cache.has(url))
return cache.get(url)! return cache.get(url)!

View File

@ -4,8 +4,16 @@ import { removeLeadingSlash } from '@vuepress/shared'
import { path } from '@vuepress/utils' import { path } from '@vuepress/utils'
import { isLinkWithProtocol } from 'vuepress/shared' import { isLinkWithProtocol } from 'vuepress/shared'
/**
* Links plugin - Process internal and external links
*
* -
*
* @param md - Markdown instance / Markdown
*/
export function linksPlugin(md: Markdown): void { export function linksPlugin(md: Markdown): void {
// attrs that going to be added to external links // attrs that going to be added to external links
// 要添加到外部链接的属性
const externalAttrs = { const externalAttrs = {
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
@ -14,24 +22,37 @@ export function linksPlugin(md: Markdown): void {
let hasOpenInternalLink = false let hasOpenInternalLink = false
const internalTag = 'VPLink' const internalTag = 'VPLink'
/**
* Handle link open token
*
*
*
* @param tokens - Token array /
* @param idx - Token index /
* @param env - Markdown environment / Markdown
*/
function handleLinkOpen(tokens: Token[], idx: number, env: MarkdownEnv) { function handleLinkOpen(tokens: Token[], idx: number, env: MarkdownEnv) {
hasOpenInternalLink = false hasOpenInternalLink = false
const token = tokens[idx] const token = tokens[idx]
// get `href` attr index // get `href` attr index
// 获取 `href` 属性索引
const hrefIndex = token.attrIndex('href') const hrefIndex = token.attrIndex('href')
// if `href` attr does not exist, skip // if `href` attr does not exist, skip
// 如果 `href` 属性不存在,跳过
/* istanbul ignore if -- @preserve */ /* istanbul ignore if -- @preserve */
if (hrefIndex < 0) { if (hrefIndex < 0) {
return return
} }
// if `href` attr exists, `token.attrs` is not `null` // if `href` attr exists, `token.attrs` is not `null`
// 如果 `href` 属性存在,`token.attrs` 不为 `null`
const hrefAttr = token.attrs![hrefIndex] const hrefAttr = token.attrs![hrefIndex]
const hrefLink: string = hrefAttr[1] const hrefLink: string = hrefAttr[1]
if (isLinkWithProtocol(hrefLink)) { if (isLinkWithProtocol(hrefLink)) {
// set `externalAttrs` to current token // set `externalAttrs` to current token
// 将 `externalAttrs` 设置到当前令牌
Object.entries(externalAttrs).forEach(([key, val]) => { Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val) token.attrSet(key, val)
}) })
@ -42,6 +63,7 @@ export function linksPlugin(md: Markdown): void {
return return
// convert starting tag of internal link // convert starting tag of internal link
// 转换内部链接的开始标签
hasOpenInternalLink = true hasOpenInternalLink = true
token.tag = internalTag token.tag = internalTag
@ -72,6 +94,7 @@ export function linksPlugin(md: Markdown): void {
md.renderer.rules.link_close = (tokens, idx, opts, _env, self) => { md.renderer.rules.link_close = (tokens, idx, opts, _env, self) => {
// convert ending tag of internal link // convert ending tag of internal link
// 转换内部链接的结束标签
if (hasOpenInternalLink) { if (hasOpenInternalLink) {
hasOpenInternalLink = false hasOpenInternalLink = false
tokens[idx].tag = internalTag tokens[idx].tag = internalTag
@ -82,6 +105,13 @@ export function linksPlugin(md: Markdown): void {
/** /**
* Resolve relative and absolute paths according to the `base` and `filePathRelative` * Resolve relative and absolute paths according to the `base` and `filePathRelative`
*
* `base` `filePathRelative`
*
* @param rawPath - Raw path /
* @param base - Base URL / URL
* @param filePathRelative - Relative file path /
* @returns Object with absolutePath and relativePath / absolutePath relativePath
*/ */
export function resolvePaths(rawPath: string, base: string, filePathRelative: string | null): { export function resolvePaths(rawPath: string, base: string, filePathRelative: string | null): {
absolutePath: string | null absolutePath: string | null
@ -91,37 +121,50 @@ export function resolvePaths(rawPath: string, base: string, filePathRelative: st
let relativePath: string let relativePath: string
// if raw path is absolute // if raw path is absolute
// 如果原始路径是绝对路径
if (rawPath.startsWith('/')) { if (rawPath.startsWith('/')) {
// if raw path is a link to markdown file // if raw path is a link to markdown file
// 如果原始路径是 markdown 文件链接
if (rawPath.endsWith('.md')) { if (rawPath.endsWith('.md')) {
// prepend `base` to the link // prepend `base` to the link
// 将 `base` 添加到链接前面
absolutePath = path.join(base, rawPath) absolutePath = path.join(base, rawPath)
relativePath = removeLeadingSlash(rawPath) relativePath = removeLeadingSlash(rawPath)
} }
// if raw path is a link to other kind of file // if raw path is a link to other kind of file
// 如果原始路径是其他类型文件链接
else { else {
// keep the link as is // keep the link as is
// 保持链接不变
absolutePath = rawPath absolutePath = rawPath
relativePath = path.relative(base, absolutePath) relativePath = path.relative(base, absolutePath)
} }
} }
// if raw path is relative // if raw path is relative
// 如果原始路径是相对路径
// if `filePathRelative` is available // if `filePathRelative` is available
// 如果 `filePathRelative` 可用
else if (filePathRelative) { else if (filePathRelative) {
// resolve relative path according to `filePathRelative` // resolve relative path according to `filePathRelative`
// 根据 `filePathRelative` 解析相对路径
relativePath = path.join( relativePath = path.join(
// file path may contain non-ASCII characters // file path may contain non-ASCII characters
// 文件路径可能包含非 ASCII 字符
path.dirname(encodeURI(filePathRelative)), path.dirname(encodeURI(filePathRelative)),
rawPath, rawPath,
) )
// resolve absolute path according to `base` // resolve absolute path according to `base`
// 根据 `base` 解析绝对路径
absolutePath = path.join(base, relativePath) absolutePath = path.join(base, relativePath)
} }
// if `filePathRelative` is not available // if `filePathRelative` is not available
// 如果 `filePathRelative` 不可用
else { else {
// remove leading './' // remove leading './'
// 移除开头的 './'
relativePath = rawPath.replace(/^(?:\.\/)?(.*)$/, '$1') relativePath = rawPath.replace(/^(?:\.\/)?(.*)$/, '$1')
// just take relative link as absolute link // just take relative link as absolute link
// 将相对链接视为绝对链接
absolutePath = null absolutePath = null
} }

View File

@ -1,5 +1,7 @@
/** /**
* Forked and modified from https://github.com/markdown-it/markdown-it-abbr/blob/master/index.mjs * Forked and modified from https://github.com/markdown-it/markdown-it-abbr/blob/master/index.mjs
*
* https://github.com/markdown-it/markdown-it-abbr/blob/master/index.mjs 分叉并修改
*/ */
import type { PluginWithOptions } from 'markdown-it' import type { PluginWithOptions } from 'markdown-it'
@ -11,18 +13,59 @@ import type Token from 'markdown-it/lib/token.mjs'
import { isEmptyObject, objectMap } from '@pengzhanbo/utils' import { isEmptyObject, objectMap } from '@pengzhanbo/utils'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
/**
* Abbreviation state block
*
*
*/
interface AbbrStateBlock extends StateBlock { interface AbbrStateBlock extends StateBlock {
/**
* Environment
*
*
*/
env: { env: {
/**
* Abbreviations record
*
*
*/
abbreviations?: Record<string, string> abbreviations?: Record<string, string>
} }
} }
/**
* Abbreviation state core
*
*
*/
interface AbbrStateCore extends StateCore { interface AbbrStateCore extends StateCore {
/**
* Environment
*
*
*/
env: { env: {
/**
* Abbreviations record
*
*
*/
abbreviations?: Record<string, string> abbreviations?: Record<string, string>
} }
} }
/**
* Abbreviation plugin - Enable abbreviation syntax
*
* -
*
* Definition syntax: *[ABBREV]: Full description
* *[]:
*
* @param md - Markdown-it instance / Markdown-it
* @param globalAbbreviations - Global abbreviations preset /
*/
export const abbrPlugin: PluginWithOptions<Record<string, string>> = (md, globalAbbreviations = {}) => { export const abbrPlugin: PluginWithOptions<Record<string, string>> = (md, globalAbbreviations = {}) => {
const { arrayReplaceAt, escapeRE, lib } = md.utils const { arrayReplaceAt, escapeRE, lib } = md.utils
globalAbbreviations = objectMap( globalAbbreviations = objectMap(
@ -37,6 +80,17 @@ export const abbrPlugin: PluginWithOptions<Record<string, string>> = (md, global
const UNICODE_SPACE_REGEXP = (lib.ucmicro.Z as RegExp).source const UNICODE_SPACE_REGEXP = (lib.ucmicro.Z as RegExp).source
const WORDING_REGEXP_TEXT = `${UNICODE_PUNCTUATION_REGEXP}|${UNICODE_SPACE_REGEXP}|[${OTHER_CHARS.split('').map(escapeRE).join('')}]` const WORDING_REGEXP_TEXT = `${UNICODE_PUNCTUATION_REGEXP}|${UNICODE_SPACE_REGEXP}|[${OTHER_CHARS.split('').map(escapeRE).join('')}]`
/**
* Abbreviation definition rule
*
*
*
* @param state - State block /
* @param startLine - Start line number /
* @param _endLine - End line number /
* @param silent - Silent mode /
* @returns Whether matched /
*/
const abbrDefinition: RuleBlock = ( const abbrDefinition: RuleBlock = (
state: AbbrStateBlock, state: AbbrStateBlock,
startLine, startLine,
@ -90,6 +144,13 @@ export const abbrPlugin: PluginWithOptions<Record<string, string>> = (md, global
return true return true
} }
/**
* Abbreviation replace rule
*
*
*
* @param state - State core /
*/
const abbrReplace: RuleCore = (state: AbbrStateCore) => { const abbrReplace: RuleCore = (state: AbbrStateCore) => {
const tokens = state.tokens const tokens = state.tokens
const { abbreviations: localAbbreviations } = state.env const { abbreviations: localAbbreviations } = state.env

View File

@ -7,29 +7,105 @@ import type Token from 'markdown-it/lib/token.mjs'
import { objectMap, toArray } from '@pengzhanbo/utils' import { objectMap, toArray } from '@pengzhanbo/utils'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv' import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv'
/**
* Annotation token with meta information
*
*
*/
interface AnnotationToken extends Token { interface AnnotationToken extends Token {
/**
* Token meta information
*
*
*/
meta: { meta: {
/**
* Annotation label
*
*
*/
label: string label: string
} }
} }
/**
* Annotation environment
*
*
*/
interface AnnotationEnv extends Record<string, unknown> { interface AnnotationEnv extends Record<string, unknown> {
/**
* Annotations record
*
*
*/
annotations: Record<string, { annotations: Record<string, {
/**
* Source texts
*
*
*/
sources: string[] sources: string[]
/**
* Rendered contents
*
*
*/
rendered: string[] rendered: string[]
}> }>
} }
/**
* Annotation state block
*
*
*/
interface AnnotationStateBlock extends StateBlock { interface AnnotationStateBlock extends StateBlock {
/**
* Tokens array
*
*
*/
tokens: AnnotationToken[] tokens: AnnotationToken[]
/**
* Environment
*
*
*/
env: AnnotationEnv env: AnnotationEnv
} }
/**
* Annotation state inline
*
*
*/
interface AnnotationStateInline extends StateInline { interface AnnotationStateInline extends StateInline {
/**
* Tokens array
*
*
*/
tokens: AnnotationToken[] tokens: AnnotationToken[]
/**
* Environment
*
*
*/
env: AnnotationEnv env: AnnotationEnv
} }
/**
* Annotation definition rule
*
*
*
* @param state - State block /
* @param startLine - Start line number /
* @param endLine - End line number /
* @param silent - Silent mode /
* @returns Whether matched /
*/
const annotationDef: RuleBlock = ( const annotationDef: RuleBlock = (
state: AnnotationStateBlock, state: AnnotationStateBlock,
startLine: number, startLine: number,
@ -100,6 +176,15 @@ const annotationDef: RuleBlock = (
return true return true
} }
/**
* Annotation reference rule
*
*
*
* @param state - State inline /
* @param silent - Silent mode /
* @returns Whether matched /
*/
const annotationRef: RuleInline = ( const annotationRef: RuleInline = (
state: AnnotationStateInline, state: AnnotationStateInline,
silent: boolean, silent: boolean,
@ -155,6 +240,20 @@ const annotationRef: RuleInline = (
return true return true
} }
/**
* Annotation plugin - Enable annotation syntax
*
* -
*
* Definition syntax: [+label]: annotation content
* Reference syntax: [+label]
*
* [+label]:
* [+label]
*
* @param md - Markdown-it instance / Markdown-it
* @param globalAnnotations - Global annotations preset /
*/
export const annotationPlugin: PluginWithOptions<Record<string, string | string[]>> = ( export const annotationPlugin: PluginWithOptions<Record<string, string | string[]>> = (
md, md,
globalAnnotations = {}, globalAnnotations = {},

View File

@ -3,7 +3,12 @@ import type { MarkdownEnvPreset } from '../../shared/index.js'
import { isEmptyObject, isString, objectMap } from '@pengzhanbo/utils' import { isEmptyObject, isString, objectMap } from '@pengzhanbo/utils'
/** /**
* inject preset to markdown env * Environment preset plugin - Inject preset references to markdown env
*
* - Markdown
*
* @param md - Markdown-it instance / Markdown-it
* @param env - Environment preset configuration /
*/ */
export const envPresetPlugin: PluginWithOptions<MarkdownEnvPreset> = (md, env = {}) => { export const envPresetPlugin: PluginWithOptions<MarkdownEnvPreset> = (md, env = {}) => {
if (isEmptyObject(env)) if (isEmptyObject(env))

View File

@ -13,6 +13,14 @@ import { annotationPlugin } from './annotation.js'
import { envPresetPlugin } from './env-preset.js' import { envPresetPlugin } from './env-preset.js'
import { plotPlugin } from './plot.js' import { plotPlugin } from './plot.js'
/**
* Inline syntax plugin - Register all inline markdown syntax plugins
*
* - Markdown
*
* @param md - Markdown instance / Markdown
* @param options - Plugin options /
*/
export function inlineSyntaxPlugin( export function inlineSyntaxPlugin(
md: Markdown, md: Markdown,
options: MarkdownPowerPluginOptions, options: MarkdownPowerPluginOptions,

View File

@ -1,9 +1,20 @@
/** /**
* !! hover !! * !! hover !!
*
* !!The text here will be hidden by a spoiler, and can only be revealed by clicking or hovering!!
*/ */
import type { PluginWithOptions } from 'markdown-it' import type { PluginWithOptions } from 'markdown-it'
import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs' import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
/**
* Plot inline rule definition
*
*
*
* @param state - Markdown-it state / Markdown-it
* @param silent - Silent mode /
* @returns Whether matched /
*/
const plotDef: RuleInline = (state, silent) => { const plotDef: RuleInline = (state, silent) => {
let found = false let found = false
const max = state.posMax const max = state.posMax
@ -76,6 +87,16 @@ const plotDef: RuleInline = (state, silent) => {
return true return true
} }
/**
* Plot plugin - Hide text with spoiler effect
*
* - 使
*
* Syntax: !!hidden text!!
* !!!!
*
* @param md - Markdown-it instance / Markdown-it
*/
export const plotPlugin: PluginWithOptions<never> = (md) => { export const plotPlugin: PluginWithOptions<never> = (md) => {
md.inline.ruler.before('emphasis', 'plot', plotDef) md.inline.ruler.before('emphasis', 'plot', plotDef)
} }

View File

@ -1,15 +1,49 @@
import type { MarkdownEnv } from 'vuepress/markdown' import type { MarkdownEnv } from 'vuepress/markdown'
/**
* Clean Markdown Environment
*
* Markdown
*/
export interface CleanMarkdownEnv extends MarkdownEnv { export interface CleanMarkdownEnv extends MarkdownEnv {
/**
* References
*
*
*/
references?: unknown references?: unknown
/**
* Abbreviations
*
*
*/
abbreviations?: unknown abbreviations?: unknown
/**
* Annotations
*
*
*/
annotations?: unknown annotations?: unknown
} }
/**
* Whitelist of environment keys to preserve
*
*
*/
const WHITE_LIST = ['base', 'filePath', 'filePathRelative', 'references', 'abbreviations', 'annotations'] as const const WHITE_LIST = ['base', 'filePath', 'filePathRelative', 'references', 'abbreviations', 'annotations'] as const
type WhiteListUnion = (typeof WHITE_LIST)[number] type WhiteListUnion = (typeof WHITE_LIST)[number]
/**
* Clean markdown environment, keeping only whitelisted keys
*
* Markdown
*
* @param env - Markdown environment / Markdown
* @param excludes - Keys to exclude /
* @returns Cleaned environment /
*/
export function cleanMarkdownEnv(env: CleanMarkdownEnv, excludes: WhiteListUnion[] = []): CleanMarkdownEnv { export function cleanMarkdownEnv(env: CleanMarkdownEnv, excludes: WhiteListUnion[] = []): CleanMarkdownEnv {
const result: CleanMarkdownEnv = {} const result: CleanMarkdownEnv = {}
for (const key of WHITE_LIST) { for (const key of WHITE_LIST) {

View File

@ -1,8 +1,13 @@
import { webcrypto } from 'node:crypto' import { webcrypto } from 'node:crypto'
/** /**
* Get key material from password
*
*
*
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/deriveKey#pbkdf2_2 * @see https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/deriveKey#pbkdf2_2
* @param password * @param password - Password string /
* @returns CryptoKey /
*/ */
function getKeyMaterial(password: string) { function getKeyMaterial(password: string) {
const enc = new TextEncoder() const enc = new TextEncoder()
@ -15,6 +20,15 @@ function getKeyMaterial(password: string) {
) )
} }
/**
* Get derived key from key material
*
*
*
* @param keyMaterial - Key material /
* @param salt - Salt for key derivation /
* @returns Derived CryptoKey /
*/
function getCryptoDeriveKey(keyMaterial: CryptoKey | webcrypto.CryptoKey, salt: Uint8Array) { function getCryptoDeriveKey(keyMaterial: CryptoKey | webcrypto.CryptoKey, salt: Uint8Array) {
return webcrypto.subtle.deriveKey( return webcrypto.subtle.deriveKey(
{ {
@ -34,8 +48,17 @@ function getCryptoDeriveKey(keyMaterial: CryptoKey | webcrypto.CryptoKey, salt:
} }
/** /**
* Encrypt content using AES-CBC
*
* 使 AES-CBC
*
* @see https://github.com/mdn/dom-examples/blob/main/web-crypto/encrypt-decrypt/aes-cbc.js * @see https://github.com/mdn/dom-examples/blob/main/web-crypto/encrypt-decrypt/aes-cbc.js
* @param content * @param content - Content to encrypt /
* @param options - Encryption options /
* @param options.password - Password for encryption /
* @param options.iv - Initialization vector /
* @param options.salt - Salt for key derivation /
* @returns Encrypted content /
*/ */
export async function encryptContent(content: string, options: { export async function encryptContent(content: string, options: {
password: string password: string

View File

@ -1,6 +1,15 @@
import type { LocaleConfig } from 'vuepress' import type { LocaleConfig } from 'vuepress'
import type { MDPowerLocaleData } from '../../shared/index.js' import type { MDPowerLocaleData } from '../../shared/index.js'
/**
* Find locale configurations for a specific key
*
*
*
* @param locales - Locale configuration /
* @param key - Key to find /
* @returns Record of locale paths to locale data /
*/
export function findLocales< export function findLocales<
T extends MDPowerLocaleData, T extends MDPowerLocaleData,
K extends keyof T, K extends keyof T,

View File

@ -5,16 +5,31 @@ import { colors, ora } from 'vuepress/utils'
type Ora = ReturnType<typeof ora> type Ora = ReturnType<typeof ora>
/** /**
* Logger utils * Logger utility class for plugin
*
*
*/ */
export class Logger { export class Logger {
/**
* Create a logger instance
*
*
*
* @param name - Plugin/Theme name / /
*/
public constructor( public constructor(
/**
* Plugin/Theme name
*/
private readonly name = '', private readonly name = '',
) {} ) {}
/**
* Initialize spinner
*
*
*
* @param subname - Subname /
* @param text - Loading text /
* @returns Ora spinner instance / Ora
*/
private init(subname: string, text: string): Ora { private init(subname: string, text: string): Ora {
return ora({ return ora({
prefixText: colors.blue(`${this.name}${subname ? `:${subname}` : ''}: `), prefixText: colors.blue(`${this.name}${subname ? `:${subname}` : ''}: `),
@ -24,6 +39,12 @@ export class Logger {
/** /**
* Create a loading spinner with text * Create a loading spinner with text
*
*
*
* @param subname - Subname /
* @param msg - Message /
* @returns Object with succeed and fail methods / succeed fail
*/ */
public load(subname: string, msg: string): { public load(subname: string, msg: string): {
succeed: (text?: string) => void succeed: (text?: string) => void
@ -37,6 +58,15 @@ export class Logger {
} }
} }
/**
* Log info message
*
*
*
* @param subname - Subname /
* @param text - Message text /
* @param args - Additional arguments /
*/
public info(subname: string, text = '', ...args: unknown[]): void { public info(subname: string, text = '', ...args: unknown[]): void {
this.init(subname, colors.blue(text)).info() this.init(subname, colors.blue(text)).info()
@ -45,7 +75,13 @@ export class Logger {
} }
/** /**
* Log success msg * Log success message
*
*
*
* @param subname - Subname /
* @param text - Message text /
* @param args - Additional arguments /
*/ */
public succeed(subname: string, text = '', ...args: unknown[]): void { public succeed(subname: string, text = '', ...args: unknown[]): void {
this.init(subname, colors.green(text)).succeed() this.init(subname, colors.green(text)).succeed()
@ -55,7 +91,13 @@ export class Logger {
} }
/** /**
* Log warning msg * Log warning message
*
*
*
* @param subname - Subname /
* @param text - Message text /
* @param args - Additional arguments /
*/ */
public warn(subname: string, text = '', ...args: unknown[]): void { public warn(subname: string, text = '', ...args: unknown[]): void {
this.init(subname, colors.yellow(text)).warn() this.init(subname, colors.yellow(text)).warn()
@ -65,7 +107,13 @@ export class Logger {
} }
/** /**
* Log error msg * Log error message
*
*
*
* @param subname - Subname /
* @param text - Message text /
* @param args - Additional arguments /
*/ */
public error(subname: string, text = '', ...args: unknown[]): void { public error(subname: string, text = '', ...args: unknown[]): void {
this.init(subname, colors.red(text)).fail() this.init(subname, colors.red(text)).fail()
@ -75,4 +123,9 @@ export class Logger {
} }
} }
/**
* Default logger instance for vuepress-plugin-md-power
*
* vuepress-plugin-md-power
*/
export const logger: Logger = new Logger('vuepress-plugin-md-power') export const logger: Logger = new Logger('vuepress-plugin-md-power')

View File

@ -1,3 +1,11 @@
import { customAlphabet } from 'nanoid' import { customAlphabet } from 'nanoid'
/**
* Generate a nanoid with custom alphabet
*
* 使 nanoid
*
* @param size - ID length / ID
* @returns Nanoid string / Nanoid
*/
export const nanoid: (size?: number) => string = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5) export const nanoid: (size?: number) => string = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5)

View File

@ -1,5 +1,21 @@
/**
* Type for values that can be either a value or a Promise of that value
*
* Promise
*
* @typeParam T - The type of the value /
*/
export type Awaitable<T> = T | Promise<T> export type Awaitable<T> = T | Promise<T>
/**
* Get the default export from a module, or the module itself if no default export
*
*
*
* @param m - Module or Promise of module / Promise
* @returns Default export or module itself /
* @typeParam T - Module type /
*/
export async function interopDefault<T>(m: Awaitable<T>): Promise<T extends { default: infer U } ? U : T> { export async function interopDefault<T>(m: Awaitable<T>): Promise<T extends { default: infer U } ? U : T> {
const resolved = await m const resolved = await m
return (resolved as any).default || resolved return (resolved as any).default || resolved

View File

@ -1,3 +1,12 @@
/**
* Parse rect size string, add unit if it's a number
*
*
*
* @param str - Size string /
* @param unit - Unit to append (default: 'px') / 'px'
* @returns Size string with unit /
*/
export function parseRect(str: string, unit = 'px'): string { export function parseRect(str: string, unit = 'px'): string {
if (Number.parseFloat(str) === Number(str)) if (Number.parseFloat(str) === Number(str))
return `${str}${unit}` return `${str}${unit}`

View File

@ -1,7 +1,21 @@
import { camelCase } from '@pengzhanbo/utils' import { camelCase } from '@pengzhanbo/utils'
/**
* Regular expression for matching attribute values
*
*
*/
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuote>.+?)\k<quote>|=(?<valueWithoutQuote>\S+))?(?:\s+|$)/ const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuote>.+?)\k<quote>|=(?<valueWithoutQuote>\S+))?(?:\s+|$)/
/**
* Resolve attribute string to object
*
*
*
* @param info - Attribute string /
* @returns Object with attrs and rawAttrs / attrs rawAttrs
* @typeParam T - Attribute type /
*/
export function resolveAttrs<T extends Record<string, any> = Record<string, any>>(info: string): { export function resolveAttrs<T extends Record<string, any> = Record<string, any>>(info: string): {
attrs: T attrs: T
rawAttrs: string rawAttrs: string
@ -35,6 +49,15 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
return { attrs: attrs as T, rawAttrs } return { attrs: attrs as T, rawAttrs }
} }
/**
* Resolve single attribute value from info string
*
*
*
* @param info - Info string /
* @param key - Attribute key /
* @returns Attribute value or undefined / undefined
*/
export function resolveAttr(info: string, key: string): string | undefined { export function resolveAttr(info: string, key: string): string | undefined {
const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`) const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`)
const groups = info.match(pattern)?.groups const groups = info.match(pattern)?.groups

View File

@ -1,5 +1,16 @@
import { isBoolean, isNull, isNumber, isString, isUndefined, kebabCase } from '@pengzhanbo/utils' import { isBoolean, isNull, isNumber, isString, isUndefined, kebabCase } from '@pengzhanbo/utils'
/**
* Stringify attributes object to HTML attribute string
*
* HTML
*
* @param attrs - Attributes object /
* @param withUndefinedOrNull - Whether to include undefined/null values / undefined/null
* @param forceStringify - Keys to force stringify /
* @returns HTML attribute string / HTML
* @typeParam T - Attribute type /
*/
export function stringifyAttrs<T extends object = object>( export function stringifyAttrs<T extends object = object>(
attrs: T, attrs: T,
withUndefinedOrNull = false, withUndefinedOrNull = false,

View File

@ -1,4 +1,13 @@
// Single quote will break @vue/compiler-sfc /**
* Stringify a property value for use in Vue templates
*
* Vue 使
*
* @param data - Data to stringify /
* @returns Stringified data with single quotes escaped /
*/
export function stringifyProp(data: unknown): string { export function stringifyProp(data: unknown): string {
// Single quote will break @vue/compiler-sfc
// 单引号会破坏 @vue/compiler-sfc
return JSON.stringify(data).replace(/'/g, '&#39') return JSON.stringify(data).replace(/'/g, '&#39')
} }

View File

@ -1,17 +1,53 @@
/**
* CanIUse Display Mode
*
* CanIUse
*/
export type CanIUseMode export type CanIUseMode
= | 'embed' = | 'embed'
| 'baseline' | 'baseline'
/** @deprecated */ /** @deprecated */
| 'image' | 'image'
/**
* CanIUse Token Metadata
*
* CanIUse
*/
export interface CanIUseTokenMeta { export interface CanIUseTokenMeta {
/**
* Feature name
*
*
*/
feature: string feature: string
/**
* Display mode
*
*
*/
mode: CanIUseMode mode: CanIUseMode
/**
* Browser versions to display
*
*
*/
versions: string versions: string
} }
/**
* CanIUse Options
*
* CanIUse
*/
export interface CanIUseOptions { export interface CanIUseOptions {
/** /**
* Embed mode
*
* embed - embed via iframe, providing interactive view
*
* image - embed via image, static
*
* *
* *
* embed iframe嵌入 * embed iframe嵌入

View File

@ -1,12 +1,57 @@
import type { SizeOptions } from './size.js' import type { SizeOptions } from './size.js'
/**
* CodeSandbox Token Metadata
*
* CodeSandbox
*/
export interface CodeSandboxTokenMeta extends SizeOptions { export interface CodeSandboxTokenMeta extends SizeOptions {
/**
* User name
*
*
*/
user?: string user?: string
/**
* Sandbox ID
*
* ID
*/
id?: string id?: string
/**
* Layout
*
*
*/
layout?: string layout?: string
/**
* Embed type
*
*
*/
type?: 'button' | 'embed' type?: 'button' | 'embed'
/**
* Title
*
*
*/
title?: string title?: string
/**
* File path
*
*
*/
filepath?: string filepath?: string
/**
* Whether to show navbar
*
*
*/
navbar?: boolean navbar?: boolean
/**
* Whether to show console
*
*
*/
console?: boolean console?: boolean
} }

View File

@ -1,3 +1,18 @@
/**
* Code tabs options
*
*
*/
export interface CodeTabsOptions { export interface CodeTabsOptions {
/**
* Icon configuration for code tabs
*
*
*
* - `boolean`: Whether to enable icons /
* - `object`: Detailed icon configuration /
* - `named`: Named icons to use / 使
* - `extensions`: File extensions to show icons for /
*/
icon?: boolean | { named?: false | string[], extensions?: false | string[] } icon?: boolean | { named?: false | string[], extensions?: false | string[] }
} }

View File

@ -1,11 +1,51 @@
import type { SizeOptions } from './size' import type { SizeOptions } from './size'
/**
* CodePen Token Metadata
*
* CodePen
*/
export interface CodepenTokenMeta extends SizeOptions { export interface CodepenTokenMeta extends SizeOptions {
/**
* Pen title
*
* Pen
*/
title?: string title?: string
/**
* User name
*
*
*/
user?: string user?: string
/**
* Pen slug
*
* Pen
*/
slash?: string slash?: string
/**
* Display tabs
*
*
*/
tab?: string tab?: string
/**
* Theme
*
*
*/
theme?: string theme?: string
/**
* Whether to show preview
*
*
*/
preview?: boolean preview?: boolean
/**
* Whether editable
*
*
*/
editable?: boolean editable?: boolean
} }

View File

@ -2,27 +2,107 @@ import type Token from 'markdown-it/lib/token.mjs'
import type { App } from 'vuepress' import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown' import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
/**
* Demo File Type
*
*
*/
export interface DemoFile { export interface DemoFile {
/**
* File type
*
*
*/
type: 'vue' | 'normal' | 'css' | 'markdown' type: 'vue' | 'normal' | 'css' | 'markdown'
/**
* Export name
*
*
*/
export?: string export?: string
/**
* File path
*
*
*/
path: string path: string
/**
* Whether to add to gitignore
*
* gitignore
*/
gitignore?: boolean gitignore?: boolean
} }
/**
* Markdown Demo Environment
*
* Markdown
*/
export interface MarkdownDemoEnv extends MarkdownEnv { export interface MarkdownDemoEnv extends MarkdownEnv {
/**
* Demo files
*
*
*/
demoFiles?: DemoFile[] demoFiles?: DemoFile[]
} }
/**
* Demo Metadata
*
*
*/
export interface DemoMeta { export interface DemoMeta {
/**
* Demo type
*
*
*/
type: 'vue' | 'normal' | 'markdown' type: 'vue' | 'normal' | 'markdown'
/**
* URL
*
*
*/
url: string url: string
/**
* Title
*
*
*/
title?: string title?: string
/**
* Description
*
*
*/
desc?: string desc?: string
/**
* Code settings
*
*
*/
codeSetting?: string codeSetting?: string
/**
* Whether expanded
*
*
*/
expanded?: boolean expanded?: boolean
} }
/**
* Demo Container Render
*
*
*/
export interface DemoContainerRender { export interface DemoContainerRender {
/**
* Before render
*
*
*/
before: ( before: (
app: App, app: App,
md: Markdown, md: Markdown,
@ -30,6 +110,16 @@ export interface DemoContainerRender {
meta: DemoMeta, meta: DemoMeta,
codeMap: Record<string, string>, codeMap: Record<string, string>,
) => string ) => string
/**
* After render
*
*
*/
after: () => string after: () => string
/**
* Token processor
*
*
*/
token?: (token: Token, tokens: Token[], index: number) => void token?: (token: Token, tokens: Token[], index: number) => void
} }

View File

@ -1,36 +1,66 @@
import type { LocaleData } from 'vuepress' import type { LocaleData } from 'vuepress'
/**
* Encrypt Snippet Locale
*
*
*/
export interface EncryptSnippetLocale extends LocaleData { export interface EncryptSnippetLocale extends LocaleData {
/** /**
* @default 'The content is encrypted, please unlock to view.'' * Hint message
*
*
* @default 'The content is encrypted, please unlock to view.'
*/ */
hint?: string hint?: string
/** /**
* Password placeholder
*
*
* @default 'Enter password' * @default 'Enter password'
*/ */
placeholder?: string placeholder?: string
/** /**
* Incorrect password message
*
*
* @default 'Incorrect password' * @default 'Incorrect password'
*/ */
incPwd?: string incPwd?: string
/** /**
* No content message
*
*
* @default 'Unlocked, but content failed to load, please try again later.' * @default 'Unlocked, but content failed to load, please try again later.'
*/ */
noContent?: string noContent?: string
/** /**
* Security warning title
*
*
* @default '🚨 Security Warning:' * @default '🚨 Security Warning:'
*/ */
warningTitle?: string warningTitle?: string
/** /**
* Security warning text
*
*
* @default 'Your connection is not encrypted with HTTPS, posing a risk of content leakage and preventing access to encrypted content.' * @default 'Your connection is not encrypted with HTTPS, posing a risk of content leakage and preventing access to encrypted content.'
*/ */
warningText?: string warningText?: string
} }
/**
* Encrypt Snippet Options
*
*
*/
export interface EncryptSnippetOptions { export interface EncryptSnippetOptions {
/** /**
* default password * Default password
*
*
*/ */
password?: string password?: string
} }

View File

@ -1,4 +1,10 @@
/* eslint-disable jsdoc/no-multi-asterisks */ /* eslint-disable jsdoc/no-multi-asterisks */
/**
* Markdown Environment Preset Configuration
*
* Markdown
*/
export interface MarkdownEnvPreset { export interface MarkdownEnvPreset {
/** /**
* markdown reference preset, use in any markdown file * markdown reference preset, use in any markdown file
@ -20,7 +26,7 @@ export interface MarkdownEnvPreset {
* [link][label-1] * [link][label-1]
* [link][label-2] * [link][label-2]
* ``` * ```
* same as * same as /
* ```markdown * ```markdown
* [label-1]: http://example.com/ * [label-1]: http://example.com/
* [label-2]: http://example.com/ "title" * [label-2]: http://example.com/ "title"
@ -47,7 +53,7 @@ export interface MarkdownEnvPreset {
* ```markdown * ```markdown
* The HTML specification is maintained by the W3C. * The HTML specification is maintained by the W3C.
* ``` * ```
* same as * same as /
* ```markdown * ```markdown
* *[HTML]: Hyper Text Markup Language * *[HTML]: Hyper Text Markup Language
* *[W3C]: World Wide Web Consortium * *[W3C]: World Wide Web Consortium
@ -73,7 +79,7 @@ export interface MarkdownEnvPreset {
* ```markdown * ```markdown
* [+vuepress-theme-plume] is a theme for [+vuepress] * [+vuepress-theme-plume] is a theme for [+vuepress]
* ``` * ```
* same as * same as /
* ```markdown * ```markdown
* [+vuepress]: vuepress is a Vue.js based documentation generator * [+vuepress]: vuepress is a Vue.js based documentation generator
* [+vuepress-theme-plume]: vuepress-theme-plume is a theme for vuepress * [+vuepress-theme-plume]: vuepress-theme-plume is a theme for vuepress

View File

@ -1,5 +1,23 @@
/**
* File tree icon mode
*
*
*/
export type FileTreeIconMode = 'simple' | 'colored' export type FileTreeIconMode = 'simple' | 'colored'
/**
* File tree options
*
*
*/
export interface FileTreeOptions { export interface FileTreeOptions {
/**
* Icon mode for file tree
*
*
*
* - `simple`: Simple icons /
* - `colored`: Colored icons /
*/
icon?: FileTreeIconMode icon?: FileTreeIconMode
} }

View File

@ -1,8 +1,33 @@
import type { SizeOptions } from './size' import type { SizeOptions } from './size'
/**
* JSFiddle token metadata
*
* JSFiddle
*/
export interface JSFiddleTokenMeta extends SizeOptions { export interface JSFiddleTokenMeta extends SizeOptions {
/**
* Source URL
*
* URL
*/
source: string source: string
/**
* Fiddle title
*
* Fiddle
*/
title?: string title?: string
/**
* Theme
*
*
*/
theme?: string theme?: string
/**
* Display tabs
*
*
*/
tab?: string tab?: string
} }

View File

@ -1,12 +1,42 @@
import type { LocaleData } from 'vuepress' import type { LocaleData } from 'vuepress'
import type { EncryptSnippetLocale } from './encrypt' import type { EncryptSnippetLocale } from './encrypt'
/**
* Markdown Power Plugin Locale Data
*
* Markdown Power
*/
export interface MDPowerLocaleData extends LocaleData { export interface MDPowerLocaleData extends LocaleData {
/**
* Common locale data
*
*
*/
common?: CommonLocaleData common?: CommonLocaleData
/**
* Encrypt snippet locale data
*
*
*/
encrypt?: EncryptSnippetLocale encrypt?: EncryptSnippetLocale
} }
/**
* Common Locale Data
*
*
*/
export interface CommonLocaleData extends LocaleData { export interface CommonLocaleData extends LocaleData {
/**
* Copy button text
*
*
*/
copy?: string copy?: string
/**
* Copied button text
*
*
*/
copied?: string copied?: string
} }

View File

@ -1,5 +1,20 @@
/**
* Supported package managers
*
*
*/
export type NpmToPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' export type NpmToPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'
/**
* npm-to options
*
* npm-to
*/
export type NpmToOptions = NpmToPackageManager[] | { export type NpmToOptions = NpmToPackageManager[] | {
/**
* Tabs to display
*
*
*/
tabs?: NpmToPackageManager[] tabs?: NpmToPackageManager[]
} }

View File

@ -1,18 +1,60 @@
import type { SizeOptions } from './size' import type { SizeOptions } from './size'
/**
* PDF embed type
*
* PDF
*/
export type PDFEmbedType = 'iframe' | 'embed' | 'pdfjs' export type PDFEmbedType = 'iframe' | 'embed' | 'pdfjs'
/**
* PDF token metadata
*
* PDF
*/
export interface PDFTokenMeta extends SizeOptions { export interface PDFTokenMeta extends SizeOptions {
/**
* Page number to display
*
*
*/
page?: number page?: number
/**
* Whether to hide toolbar
*
*
*/
noToolbar?: boolean noToolbar?: boolean
/**
* Zoom level
*
*
*/
zoom?: number zoom?: number
/**
* PDF source URL
*
* PDF URL
*/
src?: string src?: string
/**
* Title of the PDF
*
* PDF
*/
title?: string title?: string
} }
/**
* PDF options
*
* PDF
*/
export interface PDFOptions { export interface PDFOptions {
/** /**
* pdfjs url * PDF.js library URL
*
* PDF.js URL
*/ */
pdfjsUrl?: string pdfjsUrl?: string
} }

View File

@ -14,11 +14,20 @@ import type { PlotOptions } from './plot.js'
import type { ReplOptions } from './repl.js' import type { ReplOptions } from './repl.js'
import type { TableContainerOptions } from './table.js' import type { TableContainerOptions } from './table.js'
/**
* Markdown Power Plugin Options
*
* Markdown Power
*/
export interface MarkdownPowerPluginOptions { export interface MarkdownPowerPluginOptions {
/** /**
* Whether to preset markdown env, such as preset link references, abbreviations, content annotations, etc.
*
* markdown env * markdown env
* *
* Presets can be used in any markdown file
*
* markdown 使 * markdown 使
* *
* @example * @example
@ -43,78 +52,98 @@ export interface MarkdownPowerPluginOptions {
*/ */
env?: MarkdownEnvPreset env?: MarkdownEnvPreset
/** /**
* Whether to enable annotation, or preset content annotations
*
* *
* @default false * @default false
*/ */
annotation?: boolean | MarkdownEnvPreset['annotations'] annotation?: boolean | MarkdownEnvPreset['annotations']
/** /**
* Whether to enable abbr syntax, or preset abbreviations
*
* abbr * abbr
* @default false * @default false
*/ */
abbr?: boolean | MarkdownEnvPreset['abbreviations'] abbr?: boolean | MarkdownEnvPreset['abbreviations']
/** /**
* Mark pen animation mode
*
* *
* @default 'eager' * @default 'eager'
*/ */
mark?: MarkOptions mark?: MarkOptions
/** /**
* Whether to enable content snippet encryption container
*
* *
* *
* @default false * @default false
*/ */
encrypt?: boolean | EncryptSnippetOptions encrypt?: boolean | EncryptSnippetOptions
/** /**
* Configure code block grouping
*
* *
*/ */
codeTabs?: CodeTabsOptions codeTabs?: CodeTabsOptions
/** /**
* Whether to enable npm-to container
*
* npm-to * npm-to
*/ */
npmTo?: boolean | NpmToOptions npmTo?: boolean | NpmToOptions
/** /**
* PDF * Whether to enable PDF embed syntax
* *
* `@[pdf](pdf_url)` * `@[pdf](pdf_url)`
* *
* PDF
*
* @default false * @default false
*/ */
pdf?: boolean | PDFOptions pdf?: boolean | PDFOptions
// new syntax // new syntax
/** /**
* * Whether to enable icon support
* - iconify - `::collect:icon_name::` => `<VPIcon name="collect:icon_name" />` * - iconify - `::collect:icon_name::` => `<VPIcon name="collect:icon_name" />`
* - iconfont - `::name::` => `<i class="iconfont icon-name"></i>` * - iconfont - `::name::` => `<i class="iconfont icon-name"></i>`
* - fontawesome - `::fas:name::` => `<i class="fa-solid fa-name"></i>` * - fontawesome - `::fas:name::` => `<i class="fa-solid fa-name"></i>`
* *
*
*
* @default false * @default false
*/ */
icon?: IconOptions icon?: IconOptions
/** /**
* iconify * Whether to enable iconify icon embed syntax
* *
* `::collect:icon_name::` * `::collect:icon_name::`
* *
* iconify
*
* @default false * @default false
* @deprecated use `icon` instead 使 `icon` * @deprecated use `icon` instead / 使 `icon`
*/ */
icons?: boolean | IconOptions icons?: boolean | IconOptions
/** /**
* * Whether to enable hidden text syntax
* *
* `!!plot_content!!` * `!!plot_content!!`
* *
*
*
* @default false * @default false
*/ */
plot?: boolean | PlotOptions plot?: boolean | PlotOptions
/** /**
* timeline * Whether to enable timeline syntax
* *
* ```md * ```md
* ::: timeline * ::: timeline
@ -125,12 +154,14 @@ export interface MarkdownPowerPluginOptions {
* ::: * :::
* ``` * ```
* *
* timeline
*
* @default false * @default false
*/ */
timeline?: boolean timeline?: boolean
/** /**
* collapse * Whether to enable collapse folding panel syntax
* *
* ```md * ```md
* ::: collapse accordion * ::: collapse accordion
@ -144,12 +175,14 @@ export interface MarkdownPowerPluginOptions {
* ::: * :::
* ``` * ```
* *
* collapse
*
* @default false * @default false
*/ */
collapse?: boolean collapse?: boolean
/** /**
* chat * Whether to enable chat container syntax
* *
* ```md * ```md
* ::: chat * ::: chat
@ -162,11 +195,16 @@ export interface MarkdownPowerPluginOptions {
* message * message
* ::: * :::
* ``` * ```
*
* chat
*
* @default false * @default false
*/ */
chat?: boolean chat?: boolean
/** /**
* Whether to enable field / field-group container
*
* field / field-group * field / field-group
* *
* @default false * @default false
@ -174,50 +212,62 @@ export interface MarkdownPowerPluginOptions {
field?: boolean field?: boolean
// video embed // video embed
/** /**
* acfun * Whether to enable acfun video embed
* *
* `@[acfun](acid)` * `@[acfun](acid)`
* *
* acfun
*
* @default false * @default false
*/ */
acfun?: boolean acfun?: boolean
/** /**
* bilibili * Whether to enable bilibili video embed
* *
* `@[bilibili](bid)` * `@[bilibili](bid)`
* *
* bilibili
*
* @default false * @default false
*/ */
bilibili?: boolean bilibili?: boolean
/** /**
* youtube * Whether to enable youtube video embed
* *
* `@[youtube](video_id)` * `@[youtube](video_id)`
* *
* youtube
*
* @default false * @default false
*/ */
youtube?: boolean youtube?: boolean
/** /**
* artPlayer * Whether to enable artPlayer video embed
* *
* `@[artPlayer](url)` * `@[artPlayer](url)`
*
* artPlayer
*/ */
artPlayer?: boolean artPlayer?: boolean
/** /**
* audioReader * Whether to enable audioReader audio embed
* *
* `@[audioReader](url)` * `@[audioReader](url)`
*
* audioReader
*/ */
audioReader?: boolean audioReader?: boolean
// code embed // code embed
/** /**
* codepen * Whether to enable codepen embed
* *
* `@[codepen](pen_id)` * `@[codepen](pen_id)`
* *
* codepen
*
* @default false * @default false
*/ */
codepen?: boolean codepen?: boolean
@ -226,30 +276,38 @@ export interface MarkdownPowerPluginOptions {
*/ */
replit?: boolean replit?: boolean
/** /**
* codeSandbox * Whether to enable codeSandbox embed
* *
* `@[codesandbox](codesandbox_id)` * `@[codesandbox](codesandbox_id)`
* *
* codeSandbox
*
* @default false * @default false
*/ */
codeSandbox?: boolean codeSandbox?: boolean
/** /**
* jsfiddle * Whether to enable jsfiddle embed
* *
* `@[jsfiddle](jsfiddle_id)` * `@[jsfiddle](jsfiddle_id)`
* *
* jsfiddle
*
* @default false * @default false
*/ */
jsfiddle?: boolean jsfiddle?: boolean
// container // container
/** /**
* Whether to enable REPL container syntax
*
* REPL * REPL
* *
* @default false * @default false
*/ */
repl?: false | ReplOptions repl?: false | ReplOptions
/** /**
* Whether to enable file tree container syntax
*
* *
* *
* @default false * @default false
@ -257,7 +315,7 @@ export interface MarkdownPowerPluginOptions {
fileTree?: boolean | FileTreeOptions fileTree?: boolean | FileTreeOptions
/** /**
* * Whether to enable code tree container syntax and embed syntax
* *
* ```md * ```md
* ::: code-tree * ::: code-tree
@ -267,25 +325,35 @@ export interface MarkdownPowerPluginOptions {
* `@[code-tree](file_path)` * `@[code-tree](file_path)`
* *
* *
*
*
* @default false * @default false
*/ */
codeTree?: boolean | CodeTreeOptions codeTree?: boolean | CodeTreeOptions
/** /**
* Whether to enable demo syntax
*
* demo * demo
*/ */
demo?: boolean demo?: boolean
/** /**
* caniuse * Whether to enable caniuse embed syntax
* *
* `@[caniuse](feature_name)` * `@[caniuse](feature_name)`
* *
* caniuse
*
* @default false * @default false
*/ */
caniuse?: boolean | CanIUseOptions caniuse?: boolean | CanIUseOptions
/** /**
* Whether to enable table container syntax, providing enhanced functionality for tables
*
* - `copy`: Whether to enable copy functionality, supports copying as html format and markdown format
*
* table * table
* *
* - `copy`: html markdown * - `copy`: html markdown
@ -295,6 +363,8 @@ export interface MarkdownPowerPluginOptions {
table?: boolean | TableContainerOptions table?: boolean | TableContainerOptions
/** /**
* Whether to enable QR code embed syntax
*
* *
* *
* @default false * @default false
@ -303,6 +373,21 @@ export interface MarkdownPowerPluginOptions {
// enhance // enhance
/** /**
* Whether to enable automatic filling of image width and height attributes
*
* __Please note that regardless of whether it is enabled, this feature only takes effect when building production packages__
*
* - If `true`, equivalent to `'local'`
* - If `local`, only add width and height for local images
* - If `all`, add width and height for all images (including local and remote)
*
* If images load slowly, the process from loading to completion can cause unstable page layout and content flickering.
* This feature solves this problem by adding `width` and `height` attributes to images.
*
* Please use the `all` option with caution. This option will initiate network requests during the build phase,
* attempting to load remote images to obtain image size information,
* which may cause build times to become longer (fortunately, obtaining size information only requires loading a few KB of image data packets, so the time consumption will not be too long)
*
* *
* *
* __请注意__ * __请注意__

View File

@ -1,27 +1,51 @@
/**
* QR code metadata
*
*
*/
export interface QRCodeMeta extends QRCodeProps { export interface QRCodeMeta extends QRCodeProps {
/** /**
* Alias for mode: 'card'
*
* mode: 'card' * mode: 'card'
*/ */
card?: boolean card?: boolean
} }
/**
* QR code props
*
*
*/
export interface QRCodeProps { export interface QRCodeProps {
/** /**
* QR code title
* Used as HTML `title` and `alt` attributes
*
* *
* HTML `title` `alt` * HTML `title` `alt`
*/ */
title?: string title?: string
/** /**
* QR code content
*
* *
*/ */
text?: string text?: string
/** /**
* QR code width
*
* *
*/ */
width?: number | string width?: number | string
/** /**
* Display mode
* - img: Display QR code as image
* - card: Display as card with left-right layout, QR code on left, title + content on right
* @default 'img'
*
* *
* - img: 以图片的形式显示二维码 * - img: 以图片的形式显示二维码
* - card: 以卡片的形式显示 + * - card: 以卡片的形式显示 +
@ -30,50 +54,79 @@ export interface QRCodeProps {
mode?: 'img' | 'card' mode?: 'img' | 'card'
/** /**
* Whether to reverse layout in card mode
*
* card * card
*/ */
reverse?: boolean reverse?: boolean
/** /**
* QR code alignment
* @default 'left'
*
* *
* @default 'left' * @default 'left'
*/ */
align?: 'left' | 'center' | 'right' align?: 'left' | 'center' | 'right'
/** /**
* Whether to render as SVG format
* Default output is PNG format dataURL
* @default false
*
* SVG * SVG
* PNG dataURL * PNG dataURL
* @default false * @default false
*/ */
svg?: boolean svg?: boolean
/** /**
* Error correction level.
* Possible values: Low, Medium, Quartile, High, corresponding to L, M, Q, H.
* @default 'M'
*
* *
* LMQH * LMQH
* @default 'M' * @default 'M'
*/ */
level?: 'L' | 'M' | 'Q' | 'H' | 'l' | 'm' | 'q' | 'h' level?: 'L' | 'M' | 'Q' | 'H' | 'l' | 'm' | 'q' | 'h'
/** /**
* QR code version. If not specified, will automatically calculate more suitable value.
* Range: 1-40
*
* *
* 1-40 * 1-40
*/ */
version?: number version?: number
/** /**
* Mask pattern used to mask symbols.
* Possible values: 0, 1, 2, 3, 4, 5, 6, 7.
* If not specified, system will automatically calculate more suitable value.
*
* *
* 01234567 * 01234567
* *
*/ */
mask?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 mask?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
/** /**
* Define how wide the quiet zone should be.
* @default 4
*
* *
* @default 4 * @default 4
*/ */
margin?: number margin?: number
/** /**
* Scale factor. Value of 1 means 1 pixel per module (black dot).
*
* 11 * 11
*/ */
scale?: number scale?: number
/** /**
* Color of dark modules. Value must be in hexadecimal format (RGBA).
* Note: Dark should always be darker than light module color.
* @default '#000000ff'
*
* RGBA * RGBA
* *
* @default '#000000ff' * @default '#000000ff'
@ -81,6 +134,10 @@ export interface QRCodeProps {
light?: string light?: string
/** /**
* Color of light modules. Value must be in hexadecimal format (RGBA).
* Note: Light should always be lighter than dark module color.
* @default '#ffffffff'
*
* RGBA * RGBA
* *
* @default '#ffffffff' * @default '#ffffffff'

View File

@ -1,28 +1,88 @@
import type { BuiltinTheme, ThemeRegistration } from 'shiki' import type { BuiltinTheme, ThemeRegistration } from 'shiki'
/**
* Theme options for REPL
*
* REPL
*/
export type ThemeOptions export type ThemeOptions
= | BuiltinTheme = | BuiltinTheme
| { | {
/**
* Light theme
*
*
*/
light: BuiltinTheme light: BuiltinTheme
/**
* Dark theme
*
*
*/
dark: BuiltinTheme dark: BuiltinTheme
} }
/**
* REPL options
*
* REPL
*/
export interface ReplOptions { export interface ReplOptions {
/**
* Theme for code editor
*
*
*/
theme?: ThemeOptions theme?: ThemeOptions
/**
* Whether to enable Go language support
*
* Go
*/
go?: boolean go?: boolean
/**
* Whether to enable Kotlin language support
*
* Kotlin
*/
kotlin?: boolean kotlin?: boolean
/**
* Whether to enable Rust language support
*
* Rust
*/
rust?: boolean rust?: boolean
/**
* Whether to enable Python language support
*
* Python
*/
python?: boolean python?: boolean
} }
/**
* REPL editor data
*
* REPL
*/
export interface ReplEditorData { export interface ReplEditorData {
/**
* Grammar definitions for languages
*
*
*/
grammars: { grammars: {
go?: any go?: any
kotlin?: any kotlin?: any
rust?: any rust?: any
python?: any python?: any
} }
/**
* Theme registration
*
*
*/
theme: ThemeRegistration | { theme: ThemeRegistration | {
light: ThemeRegistration light: ThemeRegistration
dark: ThemeRegistration dark: ThemeRegistration

View File

@ -1,5 +1,25 @@
/**
* Size Options
*
*
*/
export interface SizeOptions { export interface SizeOptions {
/**
* Width
*
*
*/
width?: string width?: string
/**
* Height
*
*
*/
height?: string height?: string
/**
* Aspect ratio
*
*
*/
ratio?: number | string ratio?: number | string
} }

View File

@ -1,41 +1,191 @@
import type { SizeOptions } from './size' import type { SizeOptions } from './size'
/**
* Video options
*
*
*/
export interface VideoOptions { export interface VideoOptions {
/**
* Whether to enable Bilibili video embed
*
* Bilibili
*/
bilibili?: boolean bilibili?: boolean
/**
* Whether to enable YouTube video embed
*
* YouTube
*/
youtube?: boolean youtube?: boolean
} }
/**
* AcFun token metadata
*
* AcFun
*/
export interface AcFunTokenMeta extends SizeOptions { export interface AcFunTokenMeta extends SizeOptions {
/**
* Video title
*
*
*/
title?: string title?: string
/**
* Video ID
*
* ID
*/
id: string id: string
} }
/**
* Bilibili token metadata
*
* Bilibili
*/
export interface BilibiliTokenMeta extends SizeOptions { export interface BilibiliTokenMeta extends SizeOptions {
/**
* Video title
*
*
*/
title?: string title?: string
/**
* BV ID
*
* BV ID
*/
bvid?: string bvid?: string
/**
* AV ID
*
* AV ID
*/
aid?: string aid?: string
/**
* CID
*
* CID
*/
cid?: string cid?: string
/**
* Whether to autoplay
*
*
*/
autoplay?: boolean autoplay?: boolean
/**
* Start time
*
*
*/
time?: string | number time?: string | number
/**
* Page number
*
*
*/
page?: number page?: number
} }
/**
* YouTube token metadata
*
* YouTube
*/
export interface YoutubeTokenMeta extends SizeOptions { export interface YoutubeTokenMeta extends SizeOptions {
/**
* Video title
*
*
*/
title?: string title?: string
/**
* Video ID
*
* ID
*/
id: string id: string
/**
* Whether to autoplay
*
*
*/
autoplay?: boolean autoplay?: boolean
/**
* Whether to loop
*
*
*/
loop?: boolean loop?: boolean
/**
* Start time
*
*
*/
start?: string | number start?: string | number
/**
* End time
*
*
*/
end?: string | number end?: string | number
} }
/**
* ArtPlayer token metadata
*
* ArtPlayer
*/
export interface ArtPlayerTokenMeta extends SizeOptions { export interface ArtPlayerTokenMeta extends SizeOptions {
/**
* Whether muted
*
*
*/
muted?: boolean muted?: boolean
/**
* Whether to autoplay
*
*
*/
autoplay?: boolean autoplay?: boolean
/**
* Whether auto mini mode
*
*
*/
autoMini?: boolean autoMini?: boolean
/**
* Whether to loop
*
*
*/
loop?: boolean loop?: boolean
volume?: number // 0-1 /**
* Volume level (0-1)
*
* (0-1)
*/
volume?: number
/**
* Poster image URL
*
* URL
*/
poster?: string poster?: string
/**
* Video URL
*
* URL
*/
url: string url: string
/**
* Video type
*
*
*/
type?: string type?: string
} }

View File

@ -3,12 +3,31 @@ import { useDark, useEventListener } from '@vueuse/core'
import { inject, ref } from 'vue' import { inject, ref } from 'vue'
import { useThemeData } from './theme-data.js' import { useThemeData } from './theme-data.js'
/**
* Dark mode reference type
*
*
*/
type DarkModeRef = Ref<boolean> type DarkModeRef = Ref<boolean>
/**
* Injection key for dark mode
*
*
*/
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol( export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
__VUEPRESS_DEV__ ? 'darkMode' : '', __VUEPRESS_DEV__ ? 'darkMode' : '',
) )
/**
* Check if view transitions are enabled and supported
* Considers prefers-reduced-motion preference
*
*
* prefers-reduced-motion
*
* @returns Whether transitions are enabled /
*/
export function enableTransitions(): boolean { export function enableTransitions(): boolean {
if (typeof document === 'undefined') if (typeof document === 'undefined')
return false return false
@ -16,6 +35,15 @@ export function enableTransitions(): boolean {
&& window.matchMedia('(prefers-reduced-motion: no-preference)').matches && window.matchMedia('(prefers-reduced-motion: no-preference)').matches
} }
/**
* Setup dark mode for the Vue application
* Configures dark mode based on theme settings and user preferences
*
* Vue
*
*
* @param app - Vue application instance / Vue
*/
export function setupDarkMode(app: App): void { export function setupDarkMode(app: App): void {
const theme = useThemeData() const theme = useThemeData()
@ -51,6 +79,7 @@ export function setupDarkMode(app: App): void {
get: () => isDark, get: () => isDark,
}) })
// Handle print events - switch to light mode for printing
useEventListener('beforeprint', () => { useEventListener('beforeprint', () => {
if (isDark.value) if (isDark.value)
document.documentElement.dataset.theme = 'light' document.documentElement.dataset.theme = 'light'
@ -63,7 +92,13 @@ export function setupDarkMode(app: App): void {
} }
/** /**
* Inject dark mode global computed * Use dark mode
* Returns the dark mode reactive reference
*
*
*
* @returns Dark mode reference /
* @throws Error if called without provider /
*/ */
export function useDarkMode(): DarkModeRef { export function useDarkMode(): DarkModeRef {
const isDarkMode = inject(darkModeSymbol) const isDarkMode = inject(darkModeSymbol)

View File

@ -45,6 +45,11 @@ export interface Data<T extends FrontmatterType = 'page', C extends FrontmatterC
collection: CollectionItemRef<C extends 'doc' ? ThemeDocCollection : ThemePostCollection> collection: CollectionItemRef<C extends 'doc' ? ThemeDocCollection : ThemePostCollection>
} }
/**
* Use data
*
* frontmatter
*/
export function useData<T extends FrontmatterType = 'page', C extends FrontmatterCollectionType = 'doc'>(): Data<T, C> { export function useData<T extends FrontmatterType = 'page', C extends FrontmatterCollectionType = 'doc'>(): Data<T, C> {
const theme = useThemeLocaleData() const theme = useThemeLocaleData()
const page = usePageData<ThemePageData>() const page = usePageData<ThemePageData>()

View File

@ -3,34 +3,94 @@ import { encrypt as rawEncrypt } from '@internal/encrypt'
import { decodeData } from '@vuepress/helper/client' import { decodeData } from '@vuepress/helper/client'
import { ref } from 'vue' import { ref } from 'vue'
/**
* Encrypt configuration tuple type
* Contains keys, rules, global flag, and admin passwords
*
*
*
*/
export type EncryptConfig = readonly [ export type EncryptConfig = readonly [
keys: string, // keys keys: string, // keys / 密钥
rules: string, // rules rules: string, // rules / 规则
global: number, // global global: number, // global / 全局标志
admin: string, // admin admin: string, // admin / 管理员密码
] ]
/**
* Encrypt data rule interface
* Defines a single encryption rule with matching pattern and passwords
*
*
*
*/
export interface EncryptDataRule { export interface EncryptDataRule {
/** Unique key for the rule / 规则的唯一键 */
key: string key: string
/** Match pattern for the rule / 规则的匹配模式 */
match: string match: string
/** Array of valid passwords / 有效密码数组 */
rules: string[] rules: string[]
} }
/**
* Encrypt data interface
* Contains all encryption configuration and rules
*
*
*
*/
export interface EncryptData { export interface EncryptData {
/** Whether global encryption is enabled / 是否启用全局加密 */
global: boolean global: boolean
/** Array of admin password hashes / 管理员密码哈希数组 */
admins: string[] admins: string[]
/** Array of match patterns / 匹配模式数组 */
matches: string[] matches: string[]
/** Array of encryption rules / 加密规则数组 */
ruleList: EncryptDataRule[] ruleList: EncryptDataRule[]
} }
/**
* Encrypt data reference type
*
*
*/
export type EncryptRef = Ref<EncryptData> export type EncryptRef = Ref<EncryptData>
/**
* Global encrypt data reference
*
*
*/
export const encrypt: EncryptRef = ref(resolveEncryptData(rawEncrypt)) export const encrypt: EncryptRef = ref(resolveEncryptData(rawEncrypt))
/**
* Use encrypt data
* Returns the global encrypt data reference
*
*
*
*
* @returns Encrypt data reference /
*/
export function useEncryptData(): EncryptRef { export function useEncryptData(): EncryptRef {
return encrypt as EncryptRef return encrypt as EncryptRef
} }
/**
* Resolve encrypt data from raw configuration
* Decodes and parses the raw encrypt configuration
*
*
*
* @param data - Raw encrypt configuration /
* @param data."0" rawKeys - Encoded keys string /
* @param data."1" rawRules - Encoded rules string /
* @param data."2" global - Global encryption flag /
* @param data."3" admin - Admin passwords string /
* @returns Parsed encrypt data /
*/
function resolveEncryptData( function resolveEncryptData(
[rawKeys, rawRules, global, admin]: EncryptConfig, [rawKeys, rawRules, global, admin]: EncryptConfig,
): EncryptData { ): EncryptData {
@ -49,6 +109,14 @@ function resolveEncryptData(
} }
} }
/**
* Unwrap and decode data from raw string
*
*
*
* @param raw - Raw encoded string /
* @returns Parsed data /
*/
function unwrapData<T>(raw: string): T { function unwrapData<T>(raw: string): T {
return JSON.parse(decodeData(raw)) as T return JSON.parse(decodeData(raw)) as T
} }

View File

@ -8,17 +8,40 @@ import { removeLeadingSlash } from 'vuepress/shared'
import { useData } from './data.js' import { useData } from './data.js'
import { useEncryptData } from './encrypt-data.js' import { useEncryptData } from './encrypt-data.js'
/**
* Encrypt interface
* Provides encryption-related reactive states and properties
*
*
*
*/
export interface Encrypt { export interface Encrypt {
/** Whether the current page has encryption / 当前页面是否有加密 */
hasPageEncrypt: Ref<boolean> hasPageEncrypt: Ref<boolean>
/** Whether global encryption is decrypted / 全局加密是否已解密 */
isGlobalDecrypted: Ref<boolean> isGlobalDecrypted: Ref<boolean>
/** Whether page encryption is decrypted / 页面加密是否已解密 */
isPageDecrypted: Ref<boolean> isPageDecrypted: Ref<boolean>
/** List of encryption rules for the current page / 当前页面的加密规则列表 */
hashList: Ref<EncryptDataRule[]> hashList: Ref<EncryptDataRule[]>
} }
/**
* Injection key for encrypt functionality
*
*
*/
export const EncryptSymbol: InjectionKey<Encrypt> = Symbol( export const EncryptSymbol: InjectionKey<Encrypt> = Symbol(
__VUEPRESS_DEV__ ? 'Encrypt' : '', __VUEPRESS_DEV__ ? 'Encrypt' : '',
) )
/**
* Session storage for encryption state
* Stores global and page decryption states
*
*
*
*/
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => { const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
if (__VUEPRESS_SSR__) { if (__VUEPRESS_SSR__) {
return { g: '', p: [] as string[] } return { g: '', p: [] as string[] }
@ -29,8 +52,27 @@ const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
} }
}) })
/**
* Cache for password comparison results
* Improves performance by caching bcrypt verification results
*
*
* bcrypt
*/
const compareCache = new Map<string, boolean>() const compareCache = new Map<string, boolean>()
const separator = ':' const separator = ':'
/**
* Compare password with hash using bcrypt
* Caches results to avoid repeated computations
*
* 使 bcrypt
*
*
* @param content - Password to verify /
* @param hash - Bcrypt hash to compare against / bcrypt
* @returns Whether the password matches /
*/
async function compareDecrypt(content: string, hash: string): Promise<boolean> { async function compareDecrypt(content: string, hash: string): Promise<boolean> {
const key = [content, hash].join(separator) const key = [content, hash].join(separator)
if (compareCache.has(key)) if (compareCache.has(key))
@ -47,7 +89,23 @@ async function compareDecrypt(content: string, hash: string): Promise<boolean> {
} }
} }
/**
* Cache for regex patterns
* Improves performance by caching compiled regexes
*
*
*
*/
const matchCache = new Map<string, RegExp>() const matchCache = new Map<string, RegExp>()
/**
* Create or retrieve cached regex pattern
*
*
*
* @param match - Pattern string /
* @returns Compiled regex /
*/
function createMatchRegex(match: string) { function createMatchRegex(match: string) {
if (matchCache.has(match)) if (matchCache.has(match))
return matchCache.get(match)! return matchCache.get(match)!
@ -57,6 +115,18 @@ function createMatchRegex(match: string) {
return regex return regex
} }
/**
* Check if a match pattern applies to the current page
* Supports regex patterns (starting with ^) and path patterns
*
*
* ^
*
* @param match - Match pattern /
* @param pagePath - Current page path /
* @param filePathRelative - Relative file path /
* @returns Whether the pattern matches /
*/
function toMatch(match: string, pagePath: string, filePathRelative?: string | null) { function toMatch(match: string, pagePath: string, filePathRelative?: string | null) {
const relativePath = filePathRelative || '' const relativePath = filePathRelative || ''
if (match[0] === '^') { if (match[0] === '^') {
@ -69,11 +139,22 @@ function toMatch(match: string, pagePath: string, filePathRelative?: string | nu
return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match)) return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
} }
/**
* Setup encrypt functionality for the application
* Initializes encryption state and provides it to child components
*
*
*
*/
export function setupEncrypt(): void { export function setupEncrypt(): void {
const { page } = useData() const { page } = useData()
const route = useRoute() const route = useRoute()
const encrypt = useEncryptData() const encrypt = useEncryptData()
/**
* Whether the current page has encryption enabled
* Checks page-specific encryption and rule-based encryption
*/
const hasPageEncrypt = computed(() => { const hasPageEncrypt = computed(() => {
const pagePath = route.path const pagePath = route.path
const filePathRelative = page.value.filePathRelative const filePathRelative = page.value.filePathRelative
@ -85,6 +166,10 @@ export function setupEncrypt(): void {
: false : false
}) })
/**
* Whether global encryption is decrypted
* Checks if any admin password hash matches the stored hash
*/
const isGlobalDecrypted = computedAsync(async () => { const isGlobalDecrypted = computedAsync(async () => {
const hash = storage.value.g const hash = storage.value.g
if (!encrypt.value.global) if (!encrypt.value.global)
@ -97,6 +182,10 @@ export function setupEncrypt(): void {
return false return false
}, !encrypt.value.global) }, !encrypt.value.global)
/**
* List of encryption rules applicable to the current page
* Includes page-specific rules and matching pattern rules
*/
const hashList = computed(() => { const hashList = computed(() => {
const pagePath = route.path const pagePath = route.path
const filePathRelative = page.value.filePathRelative const filePathRelative = page.value.filePathRelative
@ -112,6 +201,10 @@ export function setupEncrypt(): void {
return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[] return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[]
}) })
/**
* Whether the current page is decrypted
* Checks admin passwords and page-specific passwords
*/
const isPageDecrypted = computedAsync(async () => { const isPageDecrypted = computedAsync(async () => {
if (!hasPageEncrypt.value) if (!hasPageEncrypt.value)
return true return true
@ -142,6 +235,15 @@ export function setupEncrypt(): void {
}) })
} }
/**
* Use encrypt
* Returns the encryption state and properties
*
*
*
* @returns Encrypt state and properties /
* @throws Error if called without setup /
*/
export function useEncrypt(): Encrypt { export function useEncrypt(): Encrypt {
const result = inject(EncryptSymbol) const result = inject(EncryptSymbol)
@ -151,6 +253,14 @@ export function useEncrypt(): Encrypt {
return result return result
} }
/**
* Use encrypt compare
* Provides password verification functions for global and page encryption
*
*
*
* @returns Object with compareGlobal and comparePage functions / compareGlobal comparePage
*/
export function useEncryptCompare(): { export function useEncryptCompare(): {
compareGlobal: (password: string) => Promise<boolean> compareGlobal: (password: string) => Promise<boolean>
comparePage: (password: string) => Promise<boolean> comparePage: (password: string) => Promise<boolean>
@ -160,6 +270,13 @@ export function useEncryptCompare(): {
const route = useRoute() const route = useRoute()
const { hashList } = useEncrypt() const { hashList } = useEncrypt()
/**
* Compare global password
* Verifies against admin passwords
*
* @param password - Password to verify /
* @returns Whether the password is valid /
*/
async function compareGlobal(password: string): Promise<boolean> { async function compareGlobal(password: string): Promise<boolean> {
if (!password) if (!password)
return false return false
@ -174,6 +291,13 @@ export function useEncryptCompare(): {
return false return false
} }
/**
* Compare page password
* Verifies against page-specific rules and falls back to global password
*
* @param password - Password to verify /
* @returns Whether the password is valid /
*/
async function comparePage(password: string): Promise<boolean> { async function comparePage(password: string): Promise<boolean> {
if (!password) if (!password)
return false return false

View File

@ -10,6 +10,11 @@ import { useCloseSidebarOnEscape, useSidebarControl } from './sidebar.js'
const is960 = shallowRef(false) const is960 = shallowRef(false)
const is1280 = shallowRef(false) const is1280 = shallowRef(false)
/**
* Use layout
*
*
*/
export function useLayout() { export function useLayout() {
const { frontmatter, theme } = useData() const { frontmatter, theme } = useData()
const { isPageDecrypted } = useEncrypt() const { isPageDecrypted } = useEncrypt()

View File

@ -4,19 +4,44 @@ import { computed, toValue } from 'vue'
import { resolveRoute, resolveRouteFullPath, useRoute } from 'vuepress/client' import { resolveRoute, resolveRouteFullPath, useRoute } from 'vuepress/client'
import { useData } from './data.js' import { useData } from './data.js'
/**
* Link resolution result interface
* Provides information about the resolved link
*
*
*
*/
interface UseLinkResult { interface UseLinkResult {
/** /**
* * Whether the link is external
*
*/ */
isExternal: ComputedRef<boolean> isExternal: ComputedRef<boolean>
/** /**
* * Whether the link uses an external protocol
* Does not include target="_blank" cases
* 使
* target="_blank" * target="_blank"
*/ */
isExternalProtocol: ComputedRef<boolean> isExternalProtocol: ComputedRef<boolean>
/**
* The resolved link URL
* URL
*/
link: ComputedRef<string | undefined> link: ComputedRef<string | undefined>
} }
/**
* Use link
* Resolves and processes a link URL with smart handling of internal/external links
*
* 使
* URL/
*
* @param href - Link URL or reference / URL
* @param target - Link target or reference /
* @returns Link resolution result /
*/
export function useLink( export function useLink(
href: MaybeRefOrGetter<string | undefined>, href: MaybeRefOrGetter<string | undefined>,
target?: MaybeRefOrGetter<string | undefined>, target?: MaybeRefOrGetter<string | undefined>,
@ -24,6 +49,8 @@ export function useLink(
const route = useRoute() const route = useRoute()
const { page } = useData() const { page } = useData()
// Pre-determine if it can be considered an external link
// At this point, it cannot be fully confirmed if it must be an internal link
// 预判断是否可以直接认为是外部链接 // 预判断是否可以直接认为是外部链接
// 在此时并不能完全确认是否一定是内部链接 // 在此时并不能完全确认是否一定是内部链接
const maybeIsExternal = computed(() => { const maybeIsExternal = computed(() => {
@ -36,6 +63,7 @@ export function useLink(
return false return false
}) })
// Pre-process link, try to convert to internal link
// 预处理链接,尝试转为内部的链接 // 预处理链接,尝试转为内部的链接
const preProcessLink = computed(() => { const preProcessLink = computed(() => {
const link = toValue(href) const link = toValue(href)
@ -45,6 +73,8 @@ export function useLink(
const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined
const path = resolveRouteFullPath(link, currentPath) const path = resolveRouteFullPath(link, currentPath)
if (path.includes('#')) { if (path.includes('#')) {
// Compare path + anchor with current route path
// Convert to anchor link to avoid page refresh
// 将路径 + 锚点 与 当前路由路径进行比较 // 将路径 + 锚点 与 当前路由路径进行比较
// 转为锚点链接,避免页面发生刷新 // 转为锚点链接,避免页面发生刷新
if (path.slice(0, path.indexOf('#')) === route.path) { if (path.slice(0, path.indexOf('#')) === route.path) {
@ -62,6 +92,7 @@ export function useLink(
if (!link || link[0] === '#') if (!link || link[0] === '#')
return false return false
// Check if it's a non-existent route
// 判断是否为不存在的路由 // 判断是否为不存在的路由
const routePath = link.split(/[?#]/)[0] const routePath = link.split(/[?#]/)[0]
const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined
@ -74,6 +105,7 @@ export function useLink(
}) })
const link = computed(() => { const link = computed(() => {
// Keep external links as-is
// 外部链接保持原样 // 外部链接保持原样
if (isExternal.value) { if (isExternal.value) {
return toValue(href) return toValue(href)

View File

@ -10,12 +10,32 @@ import { useRoute } from 'vuepress/client'
import { normalizeLink, resolveNavLink } from '../utils/index.js' import { normalizeLink, resolveNavLink } from '../utils/index.js'
import { useData } from './data.js' import { useData } from './data.js'
/**
* Use navbar data
* Returns the resolved navbar items based on theme configuration
*
*
*
*
* @returns Reactive reference to resolved navbar items /
*/
export function useNavbarData(): Ref<ResolvedNavItem[]> { export function useNavbarData(): Ref<ResolvedNavItem[]> {
const { theme } = useData() const { theme } = useData()
return computed(() => resolveNavbar(theme.value.navbar || [])) return computed(() => resolveNavbar(theme.value.navbar || []))
} }
/**
* Resolve navbar configuration to resolved items
* Recursively processes navbar items and resolves links
*
*
*
*
* @param navbar - Raw navbar configuration /
* @param _prefix - URL prefix for nested items / URL
* @returns Array of resolved navbar items /
*/
function resolveNavbar(navbar: ThemeNavItem[], _prefix = ''): ResolvedNavItem[] { function resolveNavbar(navbar: ThemeNavItem[], _prefix = ''): ResolvedNavItem[] {
const resolved: ResolvedNavItem[] = [] const resolved: ResolvedNavItem[] = []
navbar.forEach((item) => { navbar.forEach((item) => {
@ -40,26 +60,56 @@ function resolveNavbar(navbar: ThemeNavItem[], _prefix = ''): ResolvedNavItem[]
return resolved return resolved
} }
/**
* Navigation control return type
* Provides state and methods for mobile navigation menu
*
*
*
*/
export interface UseNavReturn { export interface UseNavReturn {
/** Whether the mobile screen menu is open / 移动端屏幕菜单是否打开 */
isScreenOpen: Ref<boolean> isScreenOpen: Ref<boolean>
/** Open the mobile screen menu / 打开移动端屏幕菜单 */
openScreen: () => void openScreen: () => void
/** Close the mobile screen menu / 关闭移动端屏幕菜单 */
closeScreen: () => void closeScreen: () => void
/** Toggle the mobile screen menu / 切换移动端屏幕菜单 */
toggleScreen: () => void toggleScreen: () => void
} }
/**
* Use nav
* Provides mobile navigation menu control functionality
*
*
*
* @returns Navigation control state and methods /
*/
export function useNav(): UseNavReturn { export function useNav(): UseNavReturn {
const isScreenOpen = ref(false) const isScreenOpen = ref(false)
/**
* Open the mobile navigation screen
* Adds resize listener to auto-close on larger screens
*/
function openScreen(): void { function openScreen(): void {
isScreenOpen.value = true isScreenOpen.value = true
window.addEventListener('resize', closeScreenOnTabletWindow) window.addEventListener('resize', closeScreenOnTabletWindow)
} }
/**
* Close the mobile navigation screen
* Removes resize listener
*/
function closeScreen(): void { function closeScreen(): void {
isScreenOpen.value = false isScreenOpen.value = false
window.removeEventListener('resize', closeScreenOnTabletWindow) window.removeEventListener('resize', closeScreenOnTabletWindow)
} }
/**
* Toggle the mobile navigation screen
*/
function toggleScreen(): void { function toggleScreen(): void {
if (isScreenOpen.value) { if (isScreenOpen.value) {
closeScreen() closeScreen()
@ -71,6 +121,7 @@ export function useNav(): UseNavReturn {
/** /**
* Close screen when the user resizes the window wider than tablet size. * Close screen when the user resizes the window wider than tablet size.
* Automatically closes the mobile menu on larger screens.
*/ */
function closeScreenOnTabletWindow(): void { function closeScreenOnTabletWindow(): void {
if (window.outerWidth >= 768) { if (window.outerWidth >= 768) {

View File

@ -7,46 +7,81 @@ import { onContentUpdated, useRouter } from 'vuepress/client'
import { useData } from './data.js' import { useData } from './data.js'
import { useLayout } from './layout.js' import { useLayout } from './layout.js'
/**
* Header interface representing a page heading
*
* Header
*/
export interface Header { export interface Header {
/** /**
* The level of the header * The level of the header
*
* `1` to `6` for `<h1>` to `<h6>` * `1` to `6` for `<h1>` to `<h6>`
*
*
* `1` `6` `<h1>` `<h6>`
*/ */
level: number level: number
/** /**
* The title of the header * The title of the header
*
*
*/ */
title: string title: string
/** /**
* The slug of the header * The slug of the header
*
* Typically the `id` attr of the header anchor * Typically the `id` attr of the header anchor
*
* slug
* `id`
*/ */
slug: string slug: string
/** /**
* Link of the header * Link of the header
*
* Typically using `#${slug}` as the anchor hash * Typically using `#${slug}` as the anchor hash
*
*
* 使 `#${slug}`
*/ */
link: string link: string
/** /**
* The children of the header * The children of the header
*
*
*/ */
children: Header[] children: Header[]
} }
// cached list of anchor elements from resolveHeaders // cached list of anchor elements from resolveHeaders
// 从 resolveHeaders 缓存的锚点元素列表
const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = [] const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
/**
* Menu item type for outline navigation
* Extends Header with element reference and additional properties
*
*
* Header
*/
export type MenuItem = Omit<Header, 'slug' | 'children'> & { export type MenuItem = Omit<Header, 'slug' | 'children'> & {
/** Reference to the DOM element / DOM 元素引用 */
element: HTMLHeadElement element: HTMLHeadElement
/** Child menu items / 子菜单项 */
children?: MenuItem[] children?: MenuItem[]
/** Lowest level for outline display / 目录显示的最低级别 */
lowLevel?: number lowLevel?: number
} }
const headers = ref<MenuItem[]>([]) const headers = ref<MenuItem[]>([])
/**
* Setup headers for the current page
* Initializes header extraction based on frontmatter and theme configuration
*
*
* frontmatter
*
* @returns Reference to the headers array /
*/
export function setupHeaders(): Ref<MenuItem[]> { export function setupHeaders(): Ref<MenuItem[]> {
const { frontmatter, theme } = useData() const { frontmatter, theme } = useData()
@ -57,10 +92,28 @@ export function setupHeaders(): Ref<MenuItem[]> {
return headers return headers
} }
/**
* Use headers
* Returns the reactive headers reference for the current page
*
*
*
* @returns Reactive reference to menu items /
*/
export function useHeaders(): Ref<MenuItem[]> { export function useHeaders(): Ref<MenuItem[]> {
return headers return headers
} }
/**
* Get headers from the page content
* Extracts and filters headings based on the outline configuration
*
*
*
*
* @param range - Outline configuration for header levels /
* @returns Array of menu items representing headers /
*/
export function getHeaders(range?: ThemeOutline): MenuItem[] { export function getHeaders(range?: ThemeOutline): MenuItem[] {
const heading = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] const heading = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const ignores = Array.from(document.querySelectorAll( const ignores = Array.from(document.querySelectorAll(
@ -87,6 +140,14 @@ export function getHeaders(range?: ThemeOutline): MenuItem[] {
return resolveSubRangeHeader(resolveHeaders(headers, high), low) return resolveSubRangeHeader(resolveHeaders(headers, high), low)
} }
/**
* Get the header level range from outline configuration
*
*
*
* @param range - Outline configuration /
* @returns Tuple of [high, low] levels / [, ]
*/
function getRange(range?: Exclude<ThemeOutline, boolean>): readonly [number, number] { function getRange(range?: Exclude<ThemeOutline, boolean>): readonly [number, number] {
const levelsRange = range || 2 const levelsRange = range || 2
// [high, low] // [high, low]
@ -97,6 +158,17 @@ function getRange(range?: Exclude<ThemeOutline, boolean>): readonly [number, num
: levelsRange : levelsRange
} }
/**
* Get the lowest outline level for a specific header
* Checks for data-outline or outline attributes
*
*
* data-outline outline
*
* @param el - Header element /
* @param level - Current header level /
* @returns Lowest level or undefined /
*/
function getLowLevel(el: HTMLHeadElement, level: number): number | undefined { function getLowLevel(el: HTMLHeadElement, level: number): number | undefined {
if (!el.hasAttribute('data-outline') && !el.hasAttribute('outline')) if (!el.hasAttribute('data-outline') && !el.hasAttribute('outline'))
return return
@ -114,6 +186,16 @@ function getLowLevel(el: HTMLHeadElement, level: number): number | undefined {
return undefined return undefined
} }
/**
* Serialize a header element to text
* Extracts text content while ignoring badges and ignored elements
*
*
*
*
* @param h - Header element /
* @returns Serialized header text /
*/
function serializeHeader(h: Element): string { function serializeHeader(h: Element): string {
// <hx><a href="#"><span>title</span></a></hx> // <hx><a href="#"><span>title</span></a></hx>
const anchor = h.firstChild const anchor = h.firstChild
@ -146,6 +228,15 @@ function serializeHeader(h: Element): string {
return ret.trim() return ret.trim()
} }
/**
* Clear ignored nodes from a list of child nodes
* Recursively removes elements with 'ignore-header' class
*
*
* 'ignore-header'
*
* @param list - Array of child nodes /
*/
function clearHeaderNodeList(list?: ChildNode[]) { function clearHeaderNodeList(list?: ChildNode[]) {
if (list?.length) { if (list?.length) {
for (const node of list) { for (const node of list) {
@ -161,6 +252,17 @@ function clearHeaderNodeList(list?: ChildNode[]) {
} }
} }
/**
* Resolve headers into a hierarchical structure
* Organizes flat headers into nested menu items based on levels
*
*
*
*
* @param headers - Flat array of menu items /
* @param high - Minimum header level to include /
* @returns Hierarchical array of menu items /
*/
export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] { export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] {
headers = headers.filter(h => h.level >= high) headers = headers.filter(h => h.level >= high)
// clear previous caches // clear previous caches
@ -175,6 +277,7 @@ export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] {
const cur = headers[i] const cur = headers[i]
if (i === 0) { if (i === 0) {
ret.push(cur) ret.push(cur)
continue
} }
else { else {
for (let j = i - 1; j >= 0; j--) { for (let j = i - 1; j >= 0; j--) {
@ -192,6 +295,17 @@ export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] {
return ret return ret
} }
/**
* Resolve sub range header
* Filters children headers based on the lowest level
*
*
*
*
* @param headers - Array of menu items /
* @param low - Lowest level to include /
* @returns Filtered menu items /
*/
function resolveSubRangeHeader(headers: MenuItem[], low: number): MenuItem[] { function resolveSubRangeHeader(headers: MenuItem[], low: number): MenuItem[] {
return headers.map((header) => { return headers.map((header) => {
if (header.children?.length) { if (header.children?.length) {
@ -203,6 +317,15 @@ function resolveSubRangeHeader(headers: MenuItem[], low: number): MenuItem[] {
}) })
} }
/**
* Use active anchor
* Tracks scroll position and updates the active outline item
*
*
*
* @param container - Reference to the outline container /
* @param marker - Reference to the active marker element /
*/
export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<HTMLElement | null>): void { export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<HTMLElement | null>): void {
const { isAsideEnabled } = useLayout() const { isAsideEnabled } = useLayout()
const router = useRouter() const router = useRouter()
@ -210,6 +333,10 @@ export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<
let prevActiveLink: HTMLAnchorElement | null = null let prevActiveLink: HTMLAnchorElement | null = null
/**
* Set the active link based on scroll position
* Determines which header is currently in view
*/
const setActiveLink = (): void => { const setActiveLink = (): void => {
if (!isAsideEnabled.value) if (!isAsideEnabled.value)
return return
@ -257,6 +384,12 @@ export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<
activateLink(activeLink) activateLink(activeLink)
} }
/**
* Activate a specific link in the outline
* Updates visual indicators and marker position
*
* @param hash - Hash of the link to activate /
*/
function activateLink(hash: string | null): void { function activateLink(hash: string | null): void {
routeHash.value = hash || '' routeHash.value = hash || ''
if (prevActiveLink) if (prevActiveLink)
@ -311,6 +444,16 @@ export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<
}) })
} }
/**
* Get the absolute top position of an element
* Accounts for fixed positioned ancestors
*
*
*
*
* @param element - Element to measure /
* @returns Absolute top position or NaN / NaN
*/
function getAbsoluteTop(element: HTMLElement): number { function getAbsoluteTop(element: HTMLElement): number {
let offsetTop = 0 let offsetTop = 0
while (element && element !== document.body) { while (element && element !== document.body) {
@ -324,7 +467,14 @@ function getAbsoluteTop(element: HTMLElement): number {
} }
/** /**
* Update current hash and do not trigger `scrollBehavior` * Update current hash without triggering scrollBehavior
* Temporarily disables scroll behavior during hash update
*
* scrollBehavior
*
*
* @param router - Vue Router instance / Vue Router
* @param hash - New hash value /
*/ */
async function updateHash(router: Router, hash: string): Promise<void> { async function updateHash(router: Router, hash: string): Promise<void> {
const { path, query } = router.currentRoute.value const { path, query } = router.currentRoute.value

View File

@ -2,6 +2,11 @@ import type { ComputedRef } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from './data.js' import { useData } from './data.js'
/**
* Use posts page data
*
*
*/
export function usePostsPageData(): { export function usePostsPageData(): {
isPosts: ComputedRef<boolean> isPosts: ComputedRef<boolean>
isPostsLayout: ComputedRef<boolean> isPostsLayout: ComputedRef<boolean>

View File

@ -9,12 +9,27 @@ import { useCollection } from './collections.js'
export type PostsDataRef = Ref<Record<string, ThemePosts>> export type PostsDataRef = Ref<Record<string, ThemePosts>>
/**
* Posts data ref
*
*
*/
export const postsData: PostsDataRef = ref(postsDataRaw) export const postsData: PostsDataRef = ref(postsDataRaw)
/**
* Use posts data
*
*
*/
export function usePostsData(): PostsDataRef { export function usePostsData(): PostsDataRef {
return postsData as PostsDataRef return postsData as PostsDataRef
} }
/**
* Use locale post list
*
*
*/
export function useLocalePostList(): ComputedRef<ThemePosts> { export function useLocalePostList(): ComputedRef<ThemePosts> {
const collection = useCollection() const collection = useCollection()
const routeLocale = useRouteLocale() const routeLocale = useRouteLocale()

View File

@ -8,6 +8,11 @@ import { useRouteQuery } from './route-query.js'
const DEFAULT_PER_PAGE = 15 const DEFAULT_PER_PAGE = 15
/**
* Use post list control result
*
*
*/
interface UsePostListControlResult { interface UsePostListControlResult {
postList: ComputedRef<ThemePostsItem[]> postList: ComputedRef<ThemePostsItem[]>
page: Ref<number> page: Ref<number>
@ -22,6 +27,11 @@ interface UsePostListControlResult {
changePage: (page: number) => void changePage: (page: number) => void
} }
/**
* Use post list control
*
*
*/
export function usePostListControl(homePage: Ref<boolean>): UsePostListControlResult { export function usePostListControl(homePage: Ref<boolean>): UsePostListControlResult {
const { collection } = useData<'page', 'post'>() const { collection } = useData<'page', 'post'>()

View File

@ -9,12 +9,22 @@ import { useTagColors } from './tag-colors.js'
type ShortPostItem = Pick<ThemePostsItem, 'title' | 'path' | 'createTime'> type ShortPostItem = Pick<ThemePostsItem, 'title' | 'path' | 'createTime'>
/**
* Posts tag item
*
*
*/
interface PostsTagItem { interface PostsTagItem {
name: string name: string
count: string | number count: string | number
className: string className: string
} }
/**
* Use tags result
*
*
*/
interface UseTagsResult { interface UseTagsResult {
tags: ComputedRef<PostsTagItem[]> tags: ComputedRef<PostsTagItem[]>
currentTag: Ref<string> currentTag: Ref<string>
@ -22,6 +32,11 @@ interface UseTagsResult {
handleTagClick: (tag: string) => void handleTagClick: (tag: string) => void
} }
/**
* Use tags
*
*
*/
export function useTags(): UseTagsResult { export function useTags(): UseTagsResult {
const { collection } = useData<'page', 'post'>() const { collection } = useData<'page', 'post'>()
const list = useLocalePostList() const list = useLocalePostList()

View File

@ -9,6 +9,11 @@ import { usePostsPageData } from './page.js'
import { useLocalePostList } from './posts-data.js' import { useLocalePostList } from './posts-data.js'
import { useSidebar } from './sidebar.js' import { useSidebar } from './sidebar.js'
/**
* Use prev next result
*
* /
*/
interface UsePrevNextResult { interface UsePrevNextResult {
prev: ComputedRef<NavItemWithLink | null> prev: ComputedRef<NavItemWithLink | null>
next: ComputedRef<NavItemWithLink | null> next: ComputedRef<NavItemWithLink | null>
@ -16,6 +21,11 @@ interface UsePrevNextResult {
const SEPARATOR_RE = /^-{3,}$/ const SEPARATOR_RE = /^-{3,}$/
/**
* Use prev next
*
*
*/
export function usePrevNext(): UsePrevNextResult { export function usePrevNext(): UsePrevNextResult {
const route = useRoute() const route = useRoute()
const { frontmatter, theme } = useData() const { frontmatter, theme } = useData()

View File

@ -66,6 +66,11 @@ export function setupSidebar(): void {
}, { immediate: true }) }, { immediate: true })
} }
/**
* Use sidebar data
*
*
*/
export function useSidebarData(): Ref<ResolvedSidebarItem[]> { export function useSidebarData(): Ref<ResolvedSidebarItem[]> {
return sidebar return sidebar
} }

View File

@ -10,6 +10,8 @@ import { getSidebarGroups, sidebarData, useSidebarData } from './sidebar-data.js
/** /**
* Check if the given sidebar item contains any active link. * Check if the given sidebar item contains any active link.
*
*
*/ */
export function hasActiveLink(path: string, items: ResolvedSidebarItem | ResolvedSidebarItem[]): boolean { export function hasActiveLink(path: string, items: ResolvedSidebarItem | ResolvedSidebarItem[]): boolean {
if (Array.isArray(items)) { if (Array.isArray(items)) {
@ -31,6 +33,11 @@ const containsActiveLink = hasActiveLink
const isSidebarEnabled = ref(false) const isSidebarEnabled = ref(false)
const isSidebarCollapsed = ref(false) const isSidebarCollapsed = ref(false)
/**
* Use sidebar control
*
* //
*/
export function useSidebarControl() { export function useSidebarControl() {
const enableSidebar = (): void => { const enableSidebar = (): void => {
isSidebarEnabled.value = true isSidebarEnabled.value = true
@ -63,6 +70,11 @@ export function useSidebarControl() {
} }
} }
/**
* Use sidebar
*
*
*/
export function useSidebar(): { export function useSidebar(): {
sidebar: Ref<ResolvedSidebarItem[]> sidebar: Ref<ResolvedSidebarItem[]>
sidebarKey: ComputedRef<string> sidebarKey: ComputedRef<string>
@ -93,8 +105,9 @@ export function useSidebar(): {
} }
/** /**
* a11y: cache the element that opened the Sidebar (the menu button) then * Use close sidebar on escape
* focus that button again when Menu is closed with Escape key. *
* a11y: 缓存打开侧边栏的元素使 Escape
*/ */
export function useCloseSidebarOnEscape(): void { export function useCloseSidebarOnEscape(): void {
const { disableSidebar } = useSidebarControl() const { disableSidebar } = useSidebarControl()
@ -122,6 +135,11 @@ export function useCloseSidebarOnEscape(): void {
} }
} }
/**
* Use sidebar item control
*
*
*/
export function useSidebarItemControl(item: ComputedRef<ResolvedSidebarItem>): SidebarItemControl { export function useSidebarItemControl(item: ComputedRef<ResolvedSidebarItem>): SidebarItemControl {
const { page } = useData() const { page } = useData()
const route = useRoute() const route = useRoute()

View File

@ -7,16 +7,47 @@ import { clientDataSymbol } from 'vuepress/client'
declare const __VUE_HMR_RUNTIME__: Record<string, any> declare const __VUE_HMR_RUNTIME__: Record<string, any>
/**
* Theme data reference type
*
*
*/
export type ThemeDataRef<T extends ThemeData = ThemeData> = Ref<T> export type ThemeDataRef<T extends ThemeData = ThemeData> = Ref<T>
/**
* Theme locale data reference type
*
*
*/
export type ThemeLocaleDataRef<T extends ThemeData = ThemeData> = ComputedRef<T> export type ThemeLocaleDataRef<T extends ThemeData = ThemeData> = ComputedRef<T>
/**
* Injection key for theme locale data
*
*
*/
export const themeLocaleDataSymbol: InjectionKey<ThemeLocaleDataRef> = Symbol( export const themeLocaleDataSymbol: InjectionKey<ThemeLocaleDataRef> = Symbol(
__VUEPRESS_DEV__ ? 'themeLocaleData' : '', __VUEPRESS_DEV__ ? 'themeLocaleData' : '',
) )
/**
* Theme data ref
* Global reference to the theme configuration data
*
*
*
*/
export const themeData: ThemeDataRef = ref(themeDataRaw) export const themeData: ThemeDataRef = ref(themeDataRaw)
/**
* Use theme data
* Returns the global theme data reference
*
*
*
*
* @returns Theme data reference /
*/
export function useThemeData< export function useThemeData<
T extends ThemeData = ThemeData, T extends ThemeData = ThemeData,
>(): ThemeDataRef<T> { >(): ThemeDataRef<T> {
@ -29,6 +60,15 @@ if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
} }
} }
/**
* Use theme locale data
* Returns the theme data for the current route's locale
*
*
*
* @returns Theme locale data computed reference /
* @throws Error if called without provider /
*/
export function useThemeLocaleData< export function useThemeLocaleData<
T extends ThemeData = ThemeData, T extends ThemeData = ThemeData,
>(): ThemeLocaleDataRef<T> { >(): ThemeLocaleDataRef<T> {
@ -40,8 +80,15 @@ export function useThemeLocaleData<
} }
/** /**
* Merge the locales fields to the root fields * Merge the locales fields to the root fields according to the route path
* according to the route path * Combines base theme options with locale-specific options
*
*
*
*
* @param theme - Base theme data /
* @param routeLocale - Current route locale /
* @returns Merged theme data for the locale /
*/ */
function resolveThemeLocaleData(theme: ThemeData, routeLocale: RouteLocale): ThemeData { function resolveThemeLocaleData(theme: ThemeData, routeLocale: RouteLocale): ThemeData {
const { locales, ...baseOptions } = theme const { locales, ...baseOptions } = theme
@ -52,6 +99,15 @@ function resolveThemeLocaleData(theme: ThemeData, routeLocale: RouteLocale): The
} }
} }
/**
* Setup theme data for the Vue app
* Provides theme data and theme locale data to the application
*
* Vue
*
*
* @param app - Vue application instance / Vue
*/
export function setupThemeData(app: App): void { export function setupThemeData(app: App): void {
// provide theme data & theme locale data // provide theme data & theme locale data
const themeData = useThemeData() const themeData = useThemeData()

View File

@ -1,14 +1,33 @@
/** /**
* @method * Tweening algorithm for smooth animation
* t: current time * Implements cubic easing function for natural motion
* b: beginning value *
* c: change in value *
* d: duration *
*
* @param t - Current time (progress from 0 to d) / 0 d
* @param b - Beginning value (initial value) /
* @param c - Change in value (target - initial) / -
* @param d - Duration (total time) /
* @returns The current value at time t / t
*/ */
export function tween(t: number, b: number, c: number, d: number): number { export function tween(t: number, b: number, c: number, d: number): number {
return c * (t /= d) * t * t + b return c * (t /= d) * t * t + b
} }
/**
* Linear interpolation for animation
* Provides constant speed motion without easing
*
* 线
*
*
* @param t - Current time (progress from 0 to d) / 0 d
* @param b - Beginning value (initial value) /
* @param c - Change in value (target - initial) / -
* @param d - Duration (total time) /
* @returns The current value at time t / t
*/
export function linear(t: number, b: number, c: number, d: number): number { export function linear(t: number, b: number, c: number, d: number): number {
return (c * t) / d + b return (c * t) / d + b
} }

View File

@ -1,5 +1,14 @@
import { tween } from './animate.js' import { tween } from './animate.js'
/**
* Get the computed CSS value of an element as a number
*
* CSS
*
* @param el - Target element /
* @param property - CSS property name / CSS
* @returns The numeric value of the CSS property, 0 if not found or invalid / CSS 0
*/
export function getCssValue(el: HTMLElement | null, property: string): number { export function getCssValue(el: HTMLElement | null, property: string): number {
const val = el?.ownerDocument?.defaultView?.getComputedStyle(el, null)?.[ const val = el?.ownerDocument?.defaultView?.getComputedStyle(el, null)?.[
property as any property as any
@ -8,6 +17,14 @@ export function getCssValue(el: HTMLElement | null, property: string): number {
return Number.isNaN(num) ? 0 : num return Number.isNaN(num) ? 0 : num
} }
/**
* Get the scrollTop value of a target element or document
*
* scrollTop
*
* @param target - Target element or document, defaults to document / document
* @returns Current scrollTop value / scrollTop
*/
export function getScrollTop( export function getScrollTop(
target: Document | HTMLElement = document, target: Document | HTMLElement = document,
): number { ): number {
@ -24,6 +41,14 @@ export function getScrollTop(
} }
} }
/**
* Set the scrollTop value of a target element or document
*
* scrollTop
*
* @param target - Target element or document, defaults to document / document
* @param scrollTop - ScrollTop value to set / scrollTop
*/
export function setScrollTop( export function setScrollTop(
target: Document | HTMLElement = document, target: Document | HTMLElement = document,
scrollTop = 0, scrollTop = 0,
@ -45,6 +70,15 @@ export function setScrollTop(
} }
} }
/**
* Smoothly scroll to a specific position
*
*
*
* @param target - Target element or document /
* @param top - Target scrollTop position / scrollTop
* @param time - Animation duration in milliseconds, defaults to 300ms / 300ms
*/
export function scrollTo( export function scrollTo(
target: Document | HTMLElement, target: Document | HTMLElement,
top: number, top: number,
@ -68,6 +102,14 @@ export function scrollTo(
} }
} }
/**
* Get the offset top of an element relative to the document
*
* offsetTop
*
* @param target - Target element /
* @returns The total offsetTop value / offsetTop
*/
export function getOffsetTop<T extends HTMLElement = HTMLElement>(target: T | null): number { export function getOffsetTop<T extends HTMLElement = HTMLElement>(target: T | null): number {
if (!target) if (!target)
return 0 return 0

View File

@ -6,6 +6,13 @@ import {
} from 'vuepress/shared' } from 'vuepress/shared'
import { resolveRepoType } from './resolveRepoType.js' import { resolveRepoType } from './resolveRepoType.js'
/**
* Edit link patterns for different repository platforms
* Maps repository types to their respective edit URL patterns
*
*
* URL
*/
export const editLinkPatterns: Record<Exclude<RepoType, null>, string> = { export const editLinkPatterns: Record<Exclude<RepoType, null>, string> = {
GitHub: ':repo/edit/:branch/:path', GitHub: ':repo/edit/:branch/:path',
GitLab: ':repo/-/edit/:branch/:path', GitLab: ':repo/-/edit/:branch/:path',
@ -14,6 +21,16 @@ export const editLinkPatterns: Record<Exclude<RepoType, null>, string> = {
':repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default', ':repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default',
} }
/**
* Resolve the edit link pattern based on repository configuration
*
*
*
* @param params - Parameters object /
* @param params.docsRepo - Repository URL / URL
* @param params.editLinkPattern - Custom edit link pattern /
* @returns The resolved edit link pattern or null / null
*/
function resolveEditLinkPatterns({ function resolveEditLinkPatterns({
docsRepo, docsRepo,
editLinkPattern, editLinkPattern,
@ -31,6 +48,21 @@ function resolveEditLinkPatterns({
return null return null
} }
/**
* Resolve the complete edit link URL for a file
* Generates an edit link based on repository configuration and file path
*
* URL
*
*
* @param params - Parameters object /
* @param params.docsRepo - Repository URL / URL
* @param params.docsBranch - Branch name /
* @param params.docsDir - Documentation directory /
* @param params.filePathRelative - Relative file path /
* @param params.editLinkPattern - Custom edit link pattern /
* @returns The complete edit link URL or null / URL null
*/
export function resolveEditLink({ export function resolveEditLink({
docsRepo, docsRepo,
docsBranch, docsBranch,

View File

@ -9,7 +9,13 @@ import { resolveRoute } from 'vuepress/client'
/** /**
* Resolve NavLink props from string * Resolve NavLink props from string
* Converts a link string to a resolved navigation item with metadata
* *
* NavLink
*
*
* @param link - The link string to resolve /
* @returns Resolved navigation item with link, text, icon and badge /
* @example * @example
* - Input: '/README.md' * - Input: '/README.md'
* - Output: { text: 'Home', link: '/' } * - Output: { text: 'Home', link: '/' }
@ -31,17 +37,47 @@ export function resolveNavLink(link: string): ResolvedNavItemWithLink {
} }
} }
/**
* Normalize a path to extract a readable title
* Removes index.html, .html extension and trailing slash
*
*
* index.html.html
*
* @param path - The path to normalize /
* @returns The extracted title from path /
*/
function normalizeTitleWithPath(path: string): string { function normalizeTitleWithPath(path: string): string {
path = path.replace(/index\.html?$/i, '').replace(/\.html?$/i, '').replace(/\/$/, '') path = path.replace(/index\.html?$/i, '').replace(/\.html?$/i, '').replace(/\/$/, '')
return decodeURIComponent(path.slice(path.lastIndexOf('/') + 1)) return decodeURIComponent(path.slice(path.lastIndexOf('/') + 1))
} }
/**
* Normalize a link by combining base and link
* Handles absolute links and protocol links correctly
*
* base link
*
*
* @param base - Base URL / URL
* @param link - Link to normalize /
* @returns Normalized link /
*/
export function normalizeLink(base = '', link = ''): string { export function normalizeLink(base = '', link = ''): string {
return isLinkAbsolute(link) || isLinkWithProtocol(link) return isLinkAbsolute(link) || isLinkWithProtocol(link)
? link ? link
: ensureLeadingSlash(`${base}/${link}`.replace(/\/+/g, '/')) : ensureLeadingSlash(`${base}/${link}`.replace(/\/+/g, '/'))
} }
/**
* Normalize a prefix by ensuring it ends with a slash
*
*
*
* @param base - Base URL / URL
* @param link - Link to normalize /
* @returns Normalized prefix with trailing slash /
*/
export function normalizePrefix(base: string, link = ''): string { export function normalizePrefix(base: string, link = ''): string {
return ensureEndingSlash(normalizeLink(base, link)) return ensureEndingSlash(normalizeLink(base, link))
} }

View File

@ -1,7 +1,24 @@
import { isLinkHttp } from 'vuepress/shared' import { isLinkHttp } from 'vuepress/shared'
/**
* Supported repository types
* Represents the type of code hosting platform
*
*
*
*/
export type RepoType = 'GitHub' | 'GitLab' | 'Gitee' | 'Bitbucket' | null export type RepoType = 'GitHub' | 'GitLab' | 'Gitee' | 'Bitbucket' | null
/**
* Resolve the repository type from a repository URL
* Detects the platform based on URL patterns
*
* URL
* URL
*
* @param repo - Repository URL or path / URL
* @returns The detected repository type or null if unknown / null
*/
export function resolveRepoType(repo: string): RepoType { export function resolveRepoType(repo: string): RepoType {
if (!isLinkHttp(repo) || /github\.com/.test(repo)) if (!isLinkHttp(repo) || /github\.com/.test(repo))
return 'GitHub' return 'GitHub'

View File

@ -1,14 +1,62 @@
/**
* Regular expression to match external URLs
*
* URL
*/
export const EXTERNAL_URL_RE: RegExp = /^[a-z]+:/i export const EXTERNAL_URL_RE: RegExp = /^[a-z]+:/i
/**
* Regular expression to match pathname protocol
*
* pathname
*/
export const PATHNAME_PROTOCOL_RE: RegExp = /^pathname:\/\// export const PATHNAME_PROTOCOL_RE: RegExp = /^pathname:\/\//
export const HASH_RE: RegExp = /#.*$/
/**
* Regular expression to match hash
*
*
*/
export const HASH_RE: RegExp = /#.*/
/**
* Regular expression to match file extension
*
*
*/
export const EXT_RE: RegExp = /(index|README)?\.(md|html)$/ export const EXT_RE: RegExp = /(index|README)?\.(md|html)$/
/**
* Whether running in browser
*
*
*/
export const inBrowser: boolean = typeof document !== 'undefined' export const inBrowser: boolean = typeof document !== 'undefined'
/**
* Convert value to array
*
*
*
* @param value - Value to convert, can be single value or array /
* @returns Array containing the value(s) /
*/
export function toArray<T>(value: T | T[]): T[] { export function toArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
} }
/**
* Check if the current path matches the given match path
* Supports both exact matching and regex matching
*
*
*
*
* @param currentPath - Current path to check /
* @param matchPath - Path pattern to match against /
* @param asRegex - Whether to treat matchPath as regex / matchPath
* @returns True if paths match / true
*/
export function isActive( export function isActive(
currentPath: string, currentPath: string,
matchPath?: string, matchPath?: string,
@ -33,10 +81,28 @@ export function isActive(
return true return true
} }
/**
* Normalize a path by removing hash and file extension
*
*
*
* @param path - Path to normalize /
* @returns Normalized path /
*/
export function normalize(path: string): string { export function normalize(path: string): string {
return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '') return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
} }
/**
* Convert a numeric value to CSS unit string
* Adds 'px' suffix if the value is a plain number
*
* CSS
* 'px'
*
* @param value - Value to convert, can be number or string with unit /
* @returns CSS unit string / CSS
*/
export function numToUnit(value?: string | number): string { export function numToUnit(value?: string | number): string {
if (typeof value === 'undefined') if (typeof value === 'undefined')
return '' return ''
@ -48,6 +114,14 @@ export function numToUnit(value?: string | number): string {
const gradient: string[] = ['linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'conic-gradient'] const gradient: string[] = ['linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'conic-gradient']
/**
* Check if a value is a CSS gradient
*
* CSS
*
* @param value - Value to check /
* @returns True if value is a gradient / true
*/
export function isGradient(value: string): boolean { export function isGradient(value: string): boolean {
return gradient.some(v => value.startsWith(v)) return gradient.some(v => value.startsWith(v))
} }

View File

@ -1,6 +1,8 @@
import type { ThemeCollectionItem, ThemeCollections, ThemeConfig, ThemeNavItem } from '../shared/index.js' import type { ThemeCollectionItem, ThemeCollections, ThemeConfig, ThemeNavItem } from '../shared/index.js'
/** /**
* Theme configuration helper function, used in separate `plume.config.ts`
*
* `plume.config.ts` 使 * `plume.config.ts` 使
*/ */
export function defineThemeConfig(config: ThemeConfig): ThemeConfig { export function defineThemeConfig(config: ThemeConfig): ThemeConfig {
@ -8,6 +10,8 @@ export function defineThemeConfig(config: ThemeConfig): ThemeConfig {
} }
/** /**
* Theme navbar configuration helper function
*
* *
*/ */
export function defineNavbarConfig(navbar: ThemeNavItem[]): ThemeNavItem[] { export function defineNavbarConfig(navbar: ThemeNavItem[]): ThemeNavItem[] {
@ -15,6 +19,8 @@ export function defineNavbarConfig(navbar: ThemeNavItem[]): ThemeNavItem[] {
} }
/** /**
* Theme notes configuration helper function
*
* notes * notes
* @deprecated 使 `defineCollections` * @deprecated 使 `defineCollections`
*/ */
@ -23,6 +29,8 @@ export function defineNotesConfig(notes: unknown): unknown {
} }
/** /**
* Theme note item configuration helper function
*
* notes item * notes item
* @deprecated 使 `defineCollection` * @deprecated 使 `defineCollection`
*/ */
@ -31,6 +39,8 @@ export function defineNoteConfig(note: unknown): unknown {
} }
/** /**
* Theme collections configuration helper function
*
* collections * collections
*/ */
export function defineCollections(collections: ThemeCollections): ThemeCollections { export function defineCollections(collections: ThemeCollections): ThemeCollections {
@ -38,7 +48,9 @@ export function defineCollections(collections: ThemeCollections): ThemeCollectio
} }
/** /**
* collections item * Theme collection item configuration helper function
*
* collection item
*/ */
export function defineCollection(collection: ThemeCollectionItem): ThemeCollectionItem { export function defineCollection(collection: ThemeCollectionItem): ThemeCollectionItem {
return collection return collection

View File

@ -5,6 +5,8 @@ import { detectMarkdown } from './markdown.js'
import { detectPlugins } from './plugins.js' import { detectPlugins } from './plugins.js'
/** /**
* Detect theme options
*
* *
*/ */
export function detectThemeOptions({ export function detectThemeOptions({

View File

@ -16,6 +16,11 @@ const t = createTranslate({
}, },
}) })
/**
* Detect version compatibility
*
*
*/
export function detectVersions(app: App): void { export function detectVersions(app: App): void {
detectVuepressVersion() detectVuepressVersion()
detectThemeVersion(app) detectThemeVersion(app)

View File

@ -7,5 +7,7 @@ export { plumeTheme }
/** /**
* @deprecated 使 * @deprecated 使
*
* @deprecated Please use named exports instead of default export
*/ */
export default plumeTheme export default plumeTheme

View File

@ -1,4 +1,8 @@
/** /**
* Multilingual presets
* Except for /zh/ and /en/, other language presets are generated by AI and are not guaranteed to be accurate
* If there are any errors, welcome to submit an issue
*
* *
* /zh/ /en/ AI * /zh/ /en/ AI
* issue * issue

View File

@ -10,6 +10,11 @@ const cache: Record<string, number> = {}
const RE_CATEGORY = /^(?:(\d+)\.)?([\s\S]+)$/ const RE_CATEGORY = /^(?:(\d+)\.)?([\s\S]+)$/
/**
* Auto category for page
*
*
*/
export function autoCategory(page: Page<ThemePageData>): void { export function autoCategory(page: Page<ThemePageData>): void {
const collection = findCollection(page) const collection = findCollection(page)

View File

@ -13,6 +13,11 @@ function getRootLang(app: App): string {
return app.siteData.lang return app.siteData.lang
} }
/**
* Create additional pages
*
*
*/
export async function createPages(app: App): Promise<void> { export async function createPages(app: App): Promise<void> {
const options = getThemeConfig() const options = getThemeConfig()

View File

@ -4,6 +4,11 @@ import { toArray } from '@pengzhanbo/utils'
import pMap from 'p-map' import pMap from 'p-map'
import { genEncrypt } from '../utils/index.js' import { genEncrypt } from '../utils/index.js'
/**
* Encrypt page
*
*
*/
export async function encryptPage( export async function encryptPage(
page: Page<ThemePageData>, page: Page<ThemePageData>,
): Promise<void> { ): Promise<void> {

View File

@ -5,6 +5,11 @@ import { autoCategory } from './autoCategory.js'
import { encryptPage } from './encryptPage.js' import { encryptPage } from './encryptPage.js'
import { enableBulletin } from './pageBulletin.js' import { enableBulletin } from './pageBulletin.js'
/**
* Extend page data
*
*
*/
export async function extendsPageData( export async function extendsPageData(
page: Page<ThemePageData>, page: Page<ThemePageData>,
): Promise<void> { ): Promise<void> {

View File

@ -3,6 +3,11 @@ import type { ThemePageData } from '../../shared/index.js'
import { isFunction, isPlainObject } from '@vuepress/helper' import { isFunction, isPlainObject } from '@vuepress/helper'
import { getThemeConfig } from '../loadConfig/index.js' import { getThemeConfig } from '../loadConfig/index.js'
/**
* Enable bulletin for page
*
*
*/
export function enableBulletin( export function enableBulletin(
page: Page<ThemePageData>, page: Page<ThemePageData>,
): void { ): void {

View File

@ -7,6 +7,11 @@ import { shikiPlugin } from '@vuepress/plugin-shiki'
import { createCodeTabIconGetter } from 'vuepress-plugin-md-power' import { createCodeTabIconGetter } from 'vuepress-plugin-md-power'
import { getThemeConfig } from '../loadConfig/index.js' import { getThemeConfig } from '../loadConfig/index.js'
/**
* Setup code-related plugins
*
*
*/
export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig { export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig {
const options = getThemeConfig() const options = getThemeConfig()
const plugins: PluginConfig = [] const plugins: PluginConfig = []

View File

@ -4,6 +4,11 @@ import { isPlainObject } from '@vuepress/helper'
import { gitPlugin as rawGitPlugin } from '@vuepress/plugin-git' import { gitPlugin as rawGitPlugin } from '@vuepress/plugin-git'
import { getThemeConfig } from '../loadConfig/index.js' import { getThemeConfig } from '../loadConfig/index.js'
/**
* Setup git plugin
*
* Git
*/
export function gitPlugin(app: App, pluginOptions: ThemeBuiltinPlugins): PluginConfig { export function gitPlugin(app: App, pluginOptions: ThemeBuiltinPlugins): PluginConfig {
const options = getThemeConfig() const options = getThemeConfig()

View File

@ -11,6 +11,11 @@ const CODE_BLOCK_RE = /(?:^|\n)(?<marker>\s*`{3,})([\s\w])[\s\S]*?\n\k<marker>(?
const ENCRYPT_CONTAINER_RE = /(?:^|\n)(?<marker>\s*:{3,})\s*encrypt\b[\s\S]*?\n\k<marker>(?:\n|$)/g const ENCRYPT_CONTAINER_RE = /(?:^|\n)(?<marker>\s*:{3,})\s*encrypt\b[\s\S]*?\n\k<marker>(?:\n|$)/g
const RESTORE_RE = /<!-- llms-code-block:(\w+) -->/g const RESTORE_RE = /<!-- llms-code-block:(\w+) -->/g
/**
* Setup LLMs plugin
*
* LLM LLM Markdown
*/
export function llmsPlugin(app: App, userOptions: true | LlmsPluginOptions): PluginConfig { export function llmsPlugin(app: App, userOptions: true | LlmsPluginOptions): PluginConfig {
if (!app.env.isBuild) if (!app.env.isBuild)
return [] return []

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