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

View File

@ -8,6 +8,15 @@ import { createPackageJson } from './packageJson.js'
import { createRender } from './render.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(
mode: Mode,
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[]> {
const fileList: File[] = []
if (data.multiLanguage) {
@ -131,6 +148,15 @@ async function createDocsFiles(data: ResolvedData): Promise<File[]> {
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[] {
return fileList.map(({ filepath, content }) => ({
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(
mode: Mode,
pkg: Record<string, any>,

View File

@ -2,16 +2,33 @@ import type { ResolvedData } from './types.js'
import { kebabCase } from '@pengzhanbo/utils'
import handlebars from 'handlebars'
/**
* Extended resolved data with additional rendering information
*
*
*/
export interface RenderData extends ResolvedData {
/** Project name in kebab-case / 项目名称kebab-case 格式) */
name: string
/** Site name / 网站名称 */
siteName: string
/** Locale configuration array / 语言配置数组 */
locales: { path: string, lang: string, isEn: boolean, prefix: string }[]
/** Whether default language is English / 默认语言是否为英语 */
isEN: boolean
}
handlebars.registerHelper('removeLeadingSlash', (path: string) => path.replace(/^\//, ''))
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) {
const data: RenderData = {
...result,

View File

@ -11,6 +11,14 @@ import { prompt } from './prompt.js'
import { t } from './translate.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> {
intro(colors.cyan('Welcome to VuePress and vuepress-theme-plume !'))

View File

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

View File

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

View File

@ -2,6 +2,14 @@ import type { File } from '../types.js'
import fs from 'node:fs/promises'
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[]> {
const filepaths = await fs.readdir(root, { recursive: true })
const files: File[] = []
@ -18,6 +26,15 @@ export async function readFiles(root: string): Promise<File[]> {
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(
files: File[],
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> {
try {
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))
/**
* 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)
/**
* 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 * from './fs.js'

View File

@ -3,6 +3,36 @@ import { parseRect } from '../utils/parseRect.js'
import { resolveAttrs } from '../utils/resolveAttrs.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 {
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 { createContainerPlugin } from './createContainer.js'
/**
* Card container attributes
*
*
*/
interface CardAttrs {
title?: string
icon?: string
}
/**
* Card masonry container attributes
*
*
*/
interface CardMasonryAttrs {
cols?: 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 {
/**
* ::: card title="xxx" icon="xxx"

View File

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

View File

@ -6,6 +6,14 @@ import { definitions, getFileIconName, getFileIconTypeFromExtension } from '../f
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.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(
options: CodeTabsOptions = {},
): (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 = {}) => {
const getIcon = createCodeTabIconGetter(options)

View File

@ -1,5 +1,5 @@
/**
* @module CodeTree
* Code tree container plugin
*
* code-tree
* ````md
@ -10,7 +10,7 @@
* :::
* ````
*
* embed syntax
* Embed syntax
*
* `@[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 { createContainerPlugin } from './createContainer.js'
/**
* Unsupported file types for code tree
*
*
*/
const UNSUPPORTED_FILE_TYPES = [
/* image */
'jpg',
@ -62,26 +67,36 @@ const UNSUPPORTED_FILE_TYPES = [
]
/**
* Code tree metadata
*
* code-tree
*/
interface CodeTreeMeta {
title?: string
/**
* File icon type
*
*
*/
icon?: FileTreeIconMode
/**
* Code tree container height
*
*
*/
height?: string
/**
* Entry file, opened by default
*
*
*/
entry?: string
}
/**
* File tree node type
*
*
*/
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[] {
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
* @param md markdown-it
* @param app vuepress app
* @param options code-tree
*
* @param md - Markdown-it instance / markdown-it
* @param app - VuePress app instance / vuepress app
* @param options - Code tree options / code-tree
*/
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 => {
@ -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 {
@ -159,10 +184,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
.join('\n')
}
// 注册 ::: code-tree 容器
// Register ::: code-tree container
createContainerPlugin(md, 'code-tree', {
before: (info, tokens, index) => {
// 收集 code-tree 容器内的文件名和激活文件
// Collect filenames and active file in code-tree container
const files: string[] = []
let activeFile: string | undefined
for (
@ -197,7 +222,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
after: () => '</VPCodeTree>',
})
// 注册 @[code-tree](dir) 语法
// Register @[code-tree](dir) syntax
createEmbedRuleBlock(md, {
type: 'code-tree',
syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/,
@ -213,10 +238,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
}
},
content: ({ dir, icon, ...props }, _, env) => {
// codeTreeFiles 用于页面依赖收集
// codeTreeFiles for page dependency collection
const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[]
const root = findFile(app, env, dir)
// 获取目录下所有文件
// Get all files in directory
const files = tinyglobby.globSync('**/*', {
cwd: root,
onlyFiles: true,
@ -229,7 +254,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
})
props.entryFile ||= files[0]
// 生成所有文件的代码块内容
// Generate code block content for all files
const codeContent = files.map((file) => {
const ext = path.extname(file).slice(1)
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
* @param page vuepress
*
* @param page - VuePress page object / vuepress
*/
export function extendsPageWithCodeTree(page: Page): void {
const markdownEnv = page.markdownEnv

View File

@ -1,10 +1,17 @@
/**
* Collapse container plugin
*
*
*
* Syntax:
* ```md
* ::: collapse accordion
* - +
*
* - -
*
* :::
* ```
*/
import type Token from 'markdown-it/lib/token.mjs'
import type { Markdown } from 'vuepress/markdown'
@ -12,16 +19,33 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js'
/**
* Collapse metadata
*
*
*/
interface CollapseMeta {
accordion?: boolean
expand?: boolean
}
/**
* Collapse item metadata
*
*
*/
interface CollapseItemMeta {
expand?: boolean
index?: number
}
/**
* Collapse plugin - Enable collapse container
*
* -
*
* @param md - Markdown instance / Markdown
*/
export function collapsePlugin(md: Markdown): void {
createContainerPlugin(md, 'collapse', {
before: (info, tokens, index) => {
@ -43,9 +67,19 @@ export function collapsePlugin(md: Markdown): void {
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 {
const listStack: number[] = [] // 记录列表嵌套深度
let idx = -1 // 记录当前列表项下标
const listStack: number[] = [] // Track list nesting depth
let idx = -1 // Current list item index
let defaultIndex: number | undefined
let hashExpand = false
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') {
break
}
// 列表层级追踪
// Track list level
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)
token.hidden = true
}
@ -66,7 +100,7 @@ function parseCollapse(tokens: Token[], index: number, attrs: CollapseMeta): num
}
else if (token.type === 'list_item_open') {
const currentLevel = listStack.length
// 仅处理根级列表项层级1
// Only process root level list items (level 1)
if (currentLevel === 1) {
token.type = 'collapse_item_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'
/**
* RenderRuleParams RenderRule
* Type for getting RenderRule parameters
*
* RenderRule
*/
type RenderRuleParams = Parameters<RenderRule> extends [...infer Args, infer _] ? Args : never
/**
*
* - before: 渲染容器起始标签时的回调
* - after: 渲染容器结束标签时的回调
* Container options
*
*
*/
export interface ContainerOptions {
/**
* Callback for rendering container opening tag
*
*
*/
before?: (info: string, ...args: RenderRuleParams) => string
/**
* Callback for rendering container closing tag
*
*
*/
after?: (info: string, ...args: RenderRuleParams) => string
}
/**
* markdown-it
* Create markdown-it custom container plugin
*
* @param md markdown-it
* @param type 'tip', 'warning'
* @param options before/after
* @param options.before
* @param options.after
* markdown-it
*
* @param md - Markdown-it instance / Markdown-it
* @param type - Container type (e.g., 'tip', 'warning') / 'tip', 'warning'
* @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(
md: Markdown,
type: string,
{ before, after }: ContainerOptions = {},
): void {
// 自定义渲染规则
// Custom render rule
const render: RenderRule = (tokens, index, options, env): string => {
const token = tokens[index]
// 提取 ::: 后的 info 信息
// Extract info after :::
const info = token.info.trim().slice(type.length).trim() || ''
if (token.nesting === 1) {
// 容器起始标签
// Container opening tag
return before?.(info, tokens, index, options, env) ?? `<div class="custom-container ${type}">`
}
else {
// 容器结束标签
// Container closing tag
return after?.(info, tokens, index, options, env) ?? '</div>'
}
}
// 注册 markdown-it-container 插件
// Register markdown-it-container plugin
md.use(container, type, { render })
}
/**
* markdown-it
* content
* Create a custom container rule where content is not processed by markdown-it
* Requires custom content processing logic
* ```md
* ::: 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>`
* })
* ```
*
* @param md - Markdown-it instance / Markdown-it
* @param type - Container type /
* @param render - Custom render rule /
*/
export function createContainerSyntaxPlugin(
md: Markdown,
@ -78,12 +96,15 @@ export function createContainerSyntaxPlugin(
const markerMinLen = 3
/**
* block
* @param state block
* @param startLine
* @param endLine
* @param silent
* @returns
* Custom container block rule definition
*
* block
*
* @param state - Current block state / block
* @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 {
const start = state.bMarks[startLine] + state.tShift[startLine]
@ -91,11 +112,11 @@ export function createContainerSyntaxPlugin(
let pos = start
// check marker
// 检查是否以指定的 maker:)开头
// Check if starts with specified maker (:)
if (state.src[pos] !== maker)
return false
// 检查 marker 长度是否满足要求
// Check if marker length meets requirements
for (pos = start + 1; pos <= max; pos++) {
if (state.src[pos] !== maker)
break
@ -108,7 +129,7 @@ export function createContainerSyntaxPlugin(
const info = state.src.slice(pos, max).trim()
// ::: type
// 检查 info 是否以 type 开头
// Check if info starts with type
if (!info.startsWith(type))
return false
@ -118,7 +139,7 @@ export function createContainerSyntaxPlugin(
let line = startLine
let content = ''
// 收集容器内容,直到遇到结束的 marker
// Collect container content until end marker
while (++line < endLine) {
if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === markup) {
break
@ -127,7 +148,7 @@ export function createContainerSyntaxPlugin(
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)
token.meta = resolveAttrs(info.slice(type.length)).attrs
token.content = content
@ -139,13 +160,13 @@ export function createContainerSyntaxPlugin(
return true
}
// 默认渲染函数
// Default render function
const defaultRender: RenderRule = (tokens, index) => {
const { content } = tokens[index]
return `<div class="custom-container ${type}">${content}</div>`
}
// 注册 block 规则和渲染规则
// Register block rule and render rule
md.block.ruler.before('fence', `${type}_definition`, defineContainer)
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 { createContainerPlugin } from './createContainer.js'
/**
* Demo wrapper attributes
*
*
*/
interface DemoWrapperAttrs {
title?: 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 {
createContainerPlugin(md, 'demo-wrapper', {

View File

@ -10,16 +10,35 @@ import { encryptContent } from '../utils/encryptContent'
import { logger } from '../utils/logger'
import { createContainerSyntaxPlugin } from './createContainer'
/**
* Encryption options
*
*
*/
interface EncryptOptions {
password: string
salt: 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 {
const encrypted: Set<string> = new Set()
const entryFile = 'internal/encrypt-snippets/index.js'
/**
* Write encrypted content to temp file
*
*
*/
const writeTemp = async (
hash: 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)}`)
}
/**
* Write entry file with all encrypted snippets
*
*
*/
const writeEntry = debounce(150, async () => {
let content = `export default {\n`
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))) {
// 初始化
// Initialize
app.writeTemp(entryFile, 'export default {}\n')
}
const localKeys = Object.keys(app.options.locales || {}).filter(key => key !== '/')
/**
* Get locale from relative path
*
*
*/
const getLocale = (relativePath: string) => {
const relative = ensureLeadingSlash(relativePath)
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 { createContainerPlugin } from './createContainer.js'
/**
* Field attributes
*
*
*/
interface FieldAttrs {
name: string
type?: string
@ -13,6 +18,16 @@ interface FieldAttrs {
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 {
createContainerPlugin(md, 'field', {
before: (info) => {

View File

@ -7,6 +7,8 @@ import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerSyntaxPlugin } from './createContainer.js'
/**
* File tree node structure
*
*
*/
interface FileTreeNode extends FileTreeNodeProps {
@ -15,6 +17,8 @@ interface FileTreeNode extends FileTreeNodeProps {
}
/**
* File tree container attributes
*
*
*/
interface FileTreeAttrs {
@ -23,6 +27,8 @@ interface FileTreeAttrs {
}
/**
* File tree node props (for rendering component)
*
*
*/
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[] {
const root: FileTreeNode = { level: -1, children: [] } as unknown as FileTreeNode
const stack: FileTreeNode[] = [root]
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) {
const match = line.match(/^(\s*)-(.*)$/)
if (!match)
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()
// 检索当前层级的父节点
// Find parent node at current level
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop()
}
@ -68,12 +77,20 @@ export function parseFileTreeRawContent(content: string): FileTreeNode[] {
return root.children
}
/**
* Regex for focus marker
*
*
*/
const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/
/**
* Parse single node info string, extract filename, comment, type, etc.
*
* info
* @param info
* @returns
*
* @param info - Node description string /
* @returns File tree node props /
*/
export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let filename = ''
@ -83,7 +100,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let type: 'folder' | 'file' = 'file'
let diff: 'add' | 'remove' | undefined
// 处理 diff 标记
// Process diff marker
if (info.startsWith('++')) {
info = info.slice(2).trim()
diff = 'add'
@ -93,14 +110,14 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
diff = 'remove'
}
// 处理高亮focus标记
// Process focus marker
info = info.replace(RE_FOCUS, (_, matched) => {
filename = matched
focus = true
return ''
})
// 提取文件名和注释
// Extract filename and comment
if (filename === '' && !focus) {
const spaceIndex = info.indexOf(' ')
filename = info.slice(0, spaceIndex === -1 ? info.length : spaceIndex)
@ -109,7 +126,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
comment = info.trim()
// 判断是否为文件夹
// Determine if folder
if (filename.endsWith('/')) {
type = 'folder'
expanded = false
@ -120,9 +137,13 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
}
/**
* File tree markdown plugin main function
*
* markdown
* @param md markdown
* @param options
*
* @param md - Markdown instance / markdown
* @param options - File tree render options /
* @param locales - Locale data /
*/
export function fileTreePlugin(
md: Markdown,
@ -130,6 +151,8 @@ export function fileTreePlugin(
locales: Record<string, CommonLocaleData>,
): void {
/**
* Get file or folder icon
*
*
*/
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 =>
@ -147,7 +172,7 @@ export function fileTreePlugin(
const { level, children, filename, comment, focus, expanded, type, diff } = node
const isOmit = filename === '…' || filename === '...' /* fallback */
// 文件夹无子节点时补充省略号
// Add ellipsis for folder without children
if (children.length === 0 && type === 'folder') {
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>`
}).join('\n')
// 注册自定义容器语法插件
// Register custom container syntax plugin
return createContainerSyntaxPlugin(
md,
'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 {
let content = prefix ? '' : '.\n'
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 { 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(
app: App,
md: Markdown,

View File

@ -8,11 +8,32 @@ import { resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js'
/**
* Code REPL metadata
*
* REPL
*/
interface CodeReplMeta {
editable?: boolean
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, {
theme,
go = false,
@ -20,6 +41,11 @@ export async function langReplPlugin(app: App, md: markdownIt, {
rust = false,
python = false,
}: ReplOptions): Promise<void> {
/**
* Create container for specific language
*
*
*/
const container = (lang: string): void => createContainerPlugin(md, `${lang}-repl`, {
before(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> {
try {
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-to
@ -34,7 +36,12 @@ import { createContainerPlugin } from './createContainer.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
*
* @param md - Markdown instance / Markdown
* @param options - npm-to options / npm-to
*/
export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
const opt = isArray(options) ? { tabs: options } : options
@ -50,14 +57,14 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
token.hidden = true
token.type = 'text'
token.content = ''
// 拆分命令行内容,转换为多包管理器命令
// Split command line content, convert to multiple package manager commands
const lines = content.split(/(\n|\s*&&\s*)/)
return md.render(
resolveNpmTo(lines, token.info.trim(), idx, tabs),
cleanMarkdownEnv(env),
)
}
// 非法容器警告
// Invalid container warning
logger.warn('npm-to', `Invalid npm-to container in ${colors.gray(env.filePathRelative || env.filePath)}`)
return ''
},
@ -66,11 +73,15 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
}
/**
* Convert npm commands to package manager command groups
*
* npm
* @param lines
* @param info
* @param idx token
* @param tabs
*
* @param lines - Command line array /
* @param info - Code block type /
* @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 {
tabs = validateTabs(tabs)
@ -81,7 +92,7 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
for (const line of lines) {
const config = findConfig(line)
if (config && config[tab]) {
// 解析并替换命令参数
// Parse and replace command arguments
const parsed = (map[line] ??= parseLine(line)) as LineParsed
const { cli, flags } = config[tab] as CommandConfigItem
@ -105,14 +116,19 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
newLines.push(line)
}
}
// 拼接为 code-tabs 格式
// Concatenate as code-tabs format
res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\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 {
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
*
* @param tabs - Package managers /
* @returns Validated tabs /
*/
function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
tabs = tabs.filter(tab => ALLOW_LIST.includes(tab))
@ -135,20 +156,32 @@ function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
}
/**
* Command line parse result type
*
*
*/
interface LineParsed {
env: string // 环境变量前缀
cli: string // 命令行工具npm/npx ...
cmd: string // 命令/脚本名
args?: string // 参数
scriptArgs?: string // 脚本参数
env: string // Environment variable prefix / 环境变量前缀
cli: string // CLI tool (npm/npx ...) / 命令行工具
cmd: string // Command/script name / 命令/脚本名
args?: string // Arguments / 参数
scriptArgs?: string // Script arguments / 脚本参数
}
/**
* Regex for parsing npm/npx commands
*
* npm/npx
*/
const LINE_REG = /(.*)(npm|npx)\s+(.*)/
/**
* Parse a line of npm/npx command, split env, command, args, etc.
*
* npm/npx
*
* @param line - Command line /
* @returns Parsed result /
*/
export function parseLine(line: string): false | LineParsed {
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
*
* @param line - Arguments line /
* @returns Parsed args /
*/
function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } {
line = line?.trim()
@ -186,7 +224,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
let cmd = ''
let args = ''
if (npmArgs[0] !== '-') {
// 处理命令和参数
// Process command and args
if (npmArgs[0] === '"' || npmArgs[0] === '\'') {
const idx = npmArgs.slice(1).indexOf(npmArgs[0])
cmd = npmArgs.slice(0, idx + 2)
@ -204,7 +242,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
}
}
else {
// 处理仅有参数的情况
// Process args only
let newLine = ''
let value = ''
let isQuote = false

View File

@ -1,22 +1,62 @@
import type { NpmToPackageManager } from '../../shared/index.js'
/**
* Package command types
*
*
*/
export type PackageCommand = 'install' | 'add' | 'remove' | 'run' | 'create' | 'init' | 'npx' | 'ci'
/**
* Command config item
*
*
*/
export interface CommandConfigItem {
cli: string
flags?: Record<string, string>
}
/**
* Command config for package managers (excluding npm)
*
* npm
*/
export type CommandConfig = Record<Exclude<NpmToPackageManager, 'npm'>, CommandConfigItem | false>
/**
* Command configs for all package commands
*
*
*/
export type CommandConfigs = Record<PackageCommand, { pattern: RegExp } & CommandConfig>
/**
* Allowed package managers list
*
*
*/
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']
/**
* Default tabs to display
*
*
*/
export const DEFAULT_TABS: NpmToPackageManager[] = ['npm', 'pnpm', 'yarn']
/**
* Package manager configurations
*
*
*/
export const MANAGERS_CONFIG: CommandConfigs = {
install: {
pattern: /(?:^|\s)npm\s+(?:install|i)$/,

View File

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

View File

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

View File

@ -3,6 +3,23 @@ import { tab } from '@mdit/plugin-tab'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.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) => {
tab(md, {
name: 'tabs',

View File

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

View File

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

View File

@ -13,6 +13,14 @@ import { artPlayerPlugin } from './video/artPlayer.js'
import { bilibiliPlugin } from './video/bilibili.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 {
if (options.caniuse) {
const caniuse = options.caniuse === true ? {} : options.caniuse

View File

@ -1,10 +1,24 @@
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
/**
* Regular expression for matching h1 heading
*
* h1
*/
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 {
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) {
const char = '---'

View File

@ -10,14 +10,49 @@ import imageSize from 'image-size'
import { fs, logger, path } from 'vuepress/utils'
import { resolveAttrs } from '../utils/resolveAttrs.js'
/**
* Image size interface
*
*
*/
interface ImgSize {
/**
* Image width
*
*
*/
width: number
/**
* Image height
*
*
*/
height: number
}
/**
* Regular expression for matching markdown image syntax
*
* markdown
*/
const REG_IMG = /!\[.*?\]\(.*?\)/g
/**
* Regular expression for matching HTML img tag
*
* HTML img
*/
const REG_IMG_TAG = /<img(.*?)>/g
/**
* Regular expression for matching src/srcset attribute
*
* src/srcset
*/
const REG_IMG_TAG_SRC = /src(?:set)?=(['"])(.+?)\1/g
/**
* List of badge URLs to exclude
*
* URL
*/
const BADGE_LIST = [
'https://img.shields.io',
'https://badge.fury.io',
@ -26,8 +61,22 @@ const BADGE_LIST = [
'https://vercel.com/button',
]
/**
* Cache for image sizes
*
*
*/
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(
app: App,
md: Markdown,
@ -71,6 +120,14 @@ export async function imageSizePlugin(
md.renderer.rules.html_block = createHtmlRule(rawHtmlBlockRule)
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 {
return (tokens, idx, options, env, self) => {
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(
src: 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 {
if (src[0] === '/')
return app.dir.public(src.slice(1))
@ -164,6 +242,13 @@ function resolveImageUrl(src: string, env: MarkdownEnv, app: App): string {
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> {
if (!app.env.isBuild)
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) {
if (src && isLinkHttp(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> {
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> {
if (cache.has(url))
return cache.get(url)!

View File

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

View File

@ -1,5 +1,7 @@
/**
* 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'
@ -11,18 +13,59 @@ import type Token from 'markdown-it/lib/token.mjs'
import { isEmptyObject, objectMap } from '@pengzhanbo/utils'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
/**
* Abbreviation state block
*
*
*/
interface AbbrStateBlock extends StateBlock {
/**
* Environment
*
*
*/
env: {
/**
* Abbreviations record
*
*
*/
abbreviations?: Record<string, string>
}
}
/**
* Abbreviation state core
*
*
*/
interface AbbrStateCore extends StateCore {
/**
* Environment
*
*
*/
env: {
/**
* Abbreviations record
*
*
*/
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 = {}) => {
const { arrayReplaceAt, escapeRE, lib } = md.utils
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 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 = (
state: AbbrStateBlock,
startLine,
@ -90,6 +144,13 @@ export const abbrPlugin: PluginWithOptions<Record<string, string>> = (md, global
return true
}
/**
* Abbreviation replace rule
*
*
*
* @param state - State core /
*/
const abbrReplace: RuleCore = (state: AbbrStateCore) => {
const tokens = state.tokens
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 { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv'
/**
* Annotation token with meta information
*
*
*/
interface AnnotationToken extends Token {
/**
* Token meta information
*
*
*/
meta: {
/**
* Annotation label
*
*
*/
label: string
}
}
/**
* Annotation environment
*
*
*/
interface AnnotationEnv extends Record<string, unknown> {
/**
* Annotations record
*
*
*/
annotations: Record<string, {
/**
* Source texts
*
*
*/
sources: string[]
/**
* Rendered contents
*
*
*/
rendered: string[]
}>
}
/**
* Annotation state block
*
*
*/
interface AnnotationStateBlock extends StateBlock {
/**
* Tokens array
*
*
*/
tokens: AnnotationToken[]
/**
* Environment
*
*
*/
env: AnnotationEnv
}
/**
* Annotation state inline
*
*
*/
interface AnnotationStateInline extends StateInline {
/**
* Tokens array
*
*
*/
tokens: AnnotationToken[]
/**
* Environment
*
*
*/
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 = (
state: AnnotationStateBlock,
startLine: number,
@ -100,6 +176,15 @@ const annotationDef: RuleBlock = (
return true
}
/**
* Annotation reference rule
*
*
*
* @param state - State inline /
* @param silent - Silent mode /
* @returns Whether matched /
*/
const annotationRef: RuleInline = (
state: AnnotationStateInline,
silent: boolean,
@ -155,6 +240,20 @@ const annotationRef: RuleInline = (
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[]>> = (
md,
globalAnnotations = {},

View File

@ -3,7 +3,12 @@ import type { MarkdownEnvPreset } from '../../shared/index.js'
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 = {}) => {
if (isEmptyObject(env))

View File

@ -13,6 +13,14 @@ import { annotationPlugin } from './annotation.js'
import { envPresetPlugin } from './env-preset.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(
md: Markdown,
options: MarkdownPowerPluginOptions,

View File

@ -1,9 +1,20 @@
/**
* !! 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 { 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) => {
let found = false
const max = state.posMax
@ -76,6 +87,16 @@ const plotDef: RuleInline = (state, silent) => {
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) => {
md.inline.ruler.before('emphasis', 'plot', plotDef)
}

View File

@ -1,15 +1,49 @@
import type { MarkdownEnv } from 'vuepress/markdown'
/**
* Clean Markdown Environment
*
* Markdown
*/
export interface CleanMarkdownEnv extends MarkdownEnv {
/**
* References
*
*
*/
references?: unknown
/**
* Abbreviations
*
*
*/
abbreviations?: unknown
/**
* Annotations
*
*
*/
annotations?: unknown
}
/**
* Whitelist of environment keys to preserve
*
*
*/
const WHITE_LIST = ['base', 'filePath', 'filePathRelative', 'references', 'abbreviations', 'annotations'] as const
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 {
const result: CleanMarkdownEnv = {}
for (const key of WHITE_LIST) {

View File

@ -1,8 +1,13 @@
import { webcrypto } from 'node:crypto'
/**
* Get key material from password
*
*
*
* @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) {
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) {
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
* @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: {
password: string

View File

@ -1,6 +1,15 @@
import type { LocaleConfig } from 'vuepress'
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<
T extends MDPowerLocaleData,
K extends keyof T,

View File

@ -5,16 +5,31 @@ import { colors, ora } from 'vuepress/utils'
type Ora = ReturnType<typeof ora>
/**
* Logger utils
* Logger utility class for plugin
*
*
*/
export class Logger {
/**
* Create a logger instance
*
*
*
* @param name - Plugin/Theme name / /
*/
public constructor(
/**
* Plugin/Theme 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 {
return ora({
prefixText: colors.blue(`${this.name}${subname ? `:${subname}` : ''}: `),
@ -24,6 +39,12 @@ export class Logger {
/**
* 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): {
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 {
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 {
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 {
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 {
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')

View File

@ -1,3 +1,11 @@
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)

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>
/**
* 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> {
const resolved = await m
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 {
if (Number.parseFloat(str) === Number(str))
return `${str}${unit}`

View File

@ -1,7 +1,21 @@
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+|$)/
/**
* 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): {
attrs: T
rawAttrs: string
@ -35,6 +49,15 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
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 {
const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`)
const groups = info.match(pattern)?.groups

View File

@ -1,5 +1,16 @@
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>(
attrs: T,
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 {
// Single quote will break @vue/compiler-sfc
// 单引号会破坏 @vue/compiler-sfc
return JSON.stringify(data).replace(/'/g, '&#39')
}

View File

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

View File

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

View File

@ -1,3 +1,18 @@
/**
* Code tabs options
*
*
*/
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[] }
}

View File

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

View File

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

View File

@ -1,36 +1,66 @@
import type { LocaleData } from 'vuepress'
/**
* Encrypt Snippet Locale
*
*
*/
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
/**
* Password placeholder
*
*
* @default 'Enter password'
*/
placeholder?: string
/**
* Incorrect password message
*
*
* @default 'Incorrect password'
*/
incPwd?: string
/**
* No content message
*
*
* @default 'Unlocked, but content failed to load, please try again later.'
*/
noContent?: string
/**
* Security warning title
*
*
* @default '🚨 Security Warning:'
*/
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.'
*/
warningText?: string
}
/**
* Encrypt Snippet Options
*
*
*/
export interface EncryptSnippetOptions {
/**
* default password
* Default password
*
*
*/
password?: string
}

View File

@ -1,4 +1,10 @@
/* eslint-disable jsdoc/no-multi-asterisks */
/**
* Markdown Environment Preset Configuration
*
* Markdown
*/
export interface MarkdownEnvPreset {
/**
* markdown reference preset, use in any markdown file
@ -20,7 +26,7 @@ export interface MarkdownEnvPreset {
* [link][label-1]
* [link][label-2]
* ```
* same as
* same as /
* ```markdown
* [label-1]: http://example.com/
* [label-2]: http://example.com/ "title"
@ -47,7 +53,7 @@ export interface MarkdownEnvPreset {
* ```markdown
* The HTML specification is maintained by the W3C.
* ```
* same as
* same as /
* ```markdown
* *[HTML]: Hyper Text Markup Language
* *[W3C]: World Wide Web Consortium
@ -73,7 +79,7 @@ export interface MarkdownEnvPreset {
* ```markdown
* [+vuepress-theme-plume] is a theme for [+vuepress]
* ```
* same as
* same as /
* ```markdown
* [+vuepress]: vuepress is a Vue.js based documentation generator
* [+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'
/**
* File tree options
*
*
*/
export interface FileTreeOptions {
/**
* Icon mode for file tree
*
*
*
* - `simple`: Simple icons /
* - `colored`: Colored icons /
*/
icon?: FileTreeIconMode
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,11 +14,20 @@ import type { PlotOptions } from './plot.js'
import type { ReplOptions } from './repl.js'
import type { TableContainerOptions } from './table.js'
/**
* Markdown Power Plugin Options
*
* Markdown Power
*/
export interface MarkdownPowerPluginOptions {
/**
* Whether to preset markdown env, such as preset link references, abbreviations, content annotations, etc.
*
* markdown env
*
* Presets can be used in any markdown file
*
* markdown 使
*
* @example
@ -43,78 +52,98 @@ export interface MarkdownPowerPluginOptions {
*/
env?: MarkdownEnvPreset
/**
* Whether to enable annotation, or preset content annotations
*
*
* @default false
*/
annotation?: boolean | MarkdownEnvPreset['annotations']
/**
* Whether to enable abbr syntax, or preset abbreviations
*
* abbr
* @default false
*/
abbr?: boolean | MarkdownEnvPreset['abbreviations']
/**
* Mark pen animation mode
*
*
* @default 'eager'
*/
mark?: MarkOptions
/**
* Whether to enable content snippet encryption container
*
*
*
* @default false
*/
encrypt?: boolean | EncryptSnippetOptions
/**
* Configure code block grouping
*
*
*/
codeTabs?: CodeTabsOptions
/**
* Whether to enable npm-to container
*
* npm-to
*/
npmTo?: boolean | NpmToOptions
/**
* PDF
* Whether to enable PDF embed syntax
*
* `@[pdf](pdf_url)`
*
* PDF
*
* @default false
*/
pdf?: boolean | PDFOptions
// new syntax
/**
*
* Whether to enable icon support
* - iconify - `::collect:icon_name::` => `<VPIcon name="collect:icon_name" />`
* - iconfont - `::name::` => `<i class="iconfont icon-name"></i>`
* - fontawesome - `::fas:name::` => `<i class="fa-solid fa-name"></i>`
*
*
*
* @default false
*/
icon?: IconOptions
/**
* iconify
* Whether to enable iconify icon embed syntax
*
* `::collect:icon_name::`
*
* iconify
*
* @default false
* @deprecated use `icon` instead 使 `icon`
* @deprecated use `icon` instead / 使 `icon`
*/
icons?: boolean | IconOptions
/**
*
* Whether to enable hidden text syntax
*
* `!!plot_content!!`
*
*
*
* @default false
*/
plot?: boolean | PlotOptions
/**
* timeline
* Whether to enable timeline syntax
*
* ```md
* ::: timeline
@ -125,12 +154,14 @@ export interface MarkdownPowerPluginOptions {
* :::
* ```
*
* timeline
*
* @default false
*/
timeline?: boolean
/**
* collapse
* Whether to enable collapse folding panel syntax
*
* ```md
* ::: collapse accordion
@ -144,12 +175,14 @@ export interface MarkdownPowerPluginOptions {
* :::
* ```
*
* collapse
*
* @default false
*/
collapse?: boolean
/**
* chat
* Whether to enable chat container syntax
*
* ```md
* ::: chat
@ -162,11 +195,16 @@ export interface MarkdownPowerPluginOptions {
* message
* :::
* ```
*
* chat
*
* @default false
*/
chat?: boolean
/**
* Whether to enable field / field-group container
*
* field / field-group
*
* @default false
@ -174,50 +212,62 @@ export interface MarkdownPowerPluginOptions {
field?: boolean
// video embed
/**
* acfun
* Whether to enable acfun video embed
*
* `@[acfun](acid)`
*
* acfun
*
* @default false
*/
acfun?: boolean
/**
* bilibili
* Whether to enable bilibili video embed
*
* `@[bilibili](bid)`
*
* bilibili
*
* @default false
*/
bilibili?: boolean
/**
* youtube
* Whether to enable youtube video embed
*
* `@[youtube](video_id)`
*
* youtube
*
* @default false
*/
youtube?: boolean
/**
* artPlayer
* Whether to enable artPlayer video embed
*
* `@[artPlayer](url)`
*
* artPlayer
*/
artPlayer?: boolean
/**
* audioReader
* Whether to enable audioReader audio embed
*
* `@[audioReader](url)`
*
* audioReader
*/
audioReader?: boolean
// code embed
/**
* codepen
* Whether to enable codepen embed
*
* `@[codepen](pen_id)`
*
* codepen
*
* @default false
*/
codepen?: boolean
@ -226,30 +276,38 @@ export interface MarkdownPowerPluginOptions {
*/
replit?: boolean
/**
* codeSandbox
* Whether to enable codeSandbox embed
*
* `@[codesandbox](codesandbox_id)`
*
* codeSandbox
*
* @default false
*/
codeSandbox?: boolean
/**
* jsfiddle
* Whether to enable jsfiddle embed
*
* `@[jsfiddle](jsfiddle_id)`
*
* jsfiddle
*
* @default false
*/
jsfiddle?: boolean
// container
/**
* Whether to enable REPL container syntax
*
* REPL
*
* @default false
*/
repl?: false | ReplOptions
/**
* Whether to enable file tree container syntax
*
*
*
* @default false
@ -257,7 +315,7 @@ export interface MarkdownPowerPluginOptions {
fileTree?: boolean | FileTreeOptions
/**
*
* Whether to enable code tree container syntax and embed syntax
*
* ```md
* ::: code-tree
@ -267,25 +325,35 @@ export interface MarkdownPowerPluginOptions {
* `@[code-tree](file_path)`
*
*
*
*
* @default false
*/
codeTree?: boolean | CodeTreeOptions
/**
* Whether to enable demo syntax
*
* demo
*/
demo?: boolean
/**
* caniuse
* Whether to enable caniuse embed syntax
*
* `@[caniuse](feature_name)`
*
* caniuse
*
* @default false
*/
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
*
* - `copy`: html markdown
@ -295,6 +363,8 @@ export interface MarkdownPowerPluginOptions {
table?: boolean | TableContainerOptions
/**
* Whether to enable QR code embed syntax
*
*
*
* @default false
@ -303,6 +373,21 @@ export interface MarkdownPowerPluginOptions {
// 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 {
/**
* Alias for mode: 'card'
*
* mode: 'card'
*/
card?: boolean
}
/**
* QR code props
*
*
*/
export interface QRCodeProps {
/**
* QR code title
* Used as HTML `title` and `alt` attributes
*
*
* HTML `title` `alt`
*/
title?: string
/**
* QR code content
*
*
*/
text?: string
/**
* QR code width
*
*
*/
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: 以图片的形式显示二维码
* - card: 以卡片的形式显示 +
@ -30,50 +54,79 @@ export interface QRCodeProps {
mode?: 'img' | 'card'
/**
* Whether to reverse layout in card mode
*
* card
*/
reverse?: boolean
/**
* QR code alignment
* @default 'left'
*
*
* @default 'left'
*/
align?: 'left' | 'center' | 'right'
/**
* Whether to render as SVG format
* Default output is PNG format dataURL
* @default false
*
* SVG
* PNG dataURL
* @default false
*/
svg?: boolean
/**
* Error correction level.
* Possible values: Low, Medium, Quartile, High, corresponding to L, M, Q, H.
* @default 'M'
*
*
* LMQH
* @default 'M'
*/
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
*/
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
*
*/
mask?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
/**
* Define how wide the quiet zone should be.
* @default 4
*
*
* @default 4
*/
margin?: number
/**
* Scale factor. Value of 1 means 1 pixel per module (black dot).
*
* 11
*/
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
*
* @default '#000000ff'
@ -81,6 +134,10 @@ export interface QRCodeProps {
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
*
* @default '#ffffffff'

View File

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

View File

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

View File

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

View File

@ -3,12 +3,31 @@ import { useDark, useEventListener } from '@vueuse/core'
import { inject, ref } from 'vue'
import { useThemeData } from './theme-data.js'
/**
* Dark mode reference type
*
*
*/
type DarkModeRef = Ref<boolean>
/**
* Injection key for dark mode
*
*
*/
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
__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 {
if (typeof document === 'undefined')
return false
@ -16,6 +35,15 @@ export function enableTransitions(): boolean {
&& 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 {
const theme = useThemeData()
@ -51,6 +79,7 @@ export function setupDarkMode(app: App): void {
get: () => isDark,
})
// Handle print events - switch to light mode for printing
useEventListener('beforeprint', () => {
if (isDark.value)
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 {
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>
}
/**
* Use data
*
* frontmatter
*/
export function useData<T extends FrontmatterType = 'page', C extends FrontmatterCollectionType = 'doc'>(): Data<T, C> {
const theme = useThemeLocaleData()
const page = usePageData<ThemePageData>()

View File

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

View File

@ -8,17 +8,40 @@ import { removeLeadingSlash } from 'vuepress/shared'
import { useData } from './data.js'
import { useEncryptData } from './encrypt-data.js'
/**
* Encrypt interface
* Provides encryption-related reactive states and properties
*
*
*
*/
export interface Encrypt {
/** Whether the current page has encryption / 当前页面是否有加密 */
hasPageEncrypt: Ref<boolean>
/** Whether global encryption is decrypted / 全局加密是否已解密 */
isGlobalDecrypted: Ref<boolean>
/** Whether page encryption is decrypted / 页面加密是否已解密 */
isPageDecrypted: Ref<boolean>
/** List of encryption rules for the current page / 当前页面的加密规则列表 */
hashList: Ref<EncryptDataRule[]>
}
/**
* Injection key for encrypt functionality
*
*
*/
export const EncryptSymbol: InjectionKey<Encrypt> = Symbol(
__VUEPRESS_DEV__ ? 'Encrypt' : '',
)
/**
* Session storage for encryption state
* Stores global and page decryption states
*
*
*
*/
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
if (__VUEPRESS_SSR__) {
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 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> {
const key = [content, hash].join(separator)
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>()
/**
* Create or retrieve cached regex pattern
*
*
*
* @param match - Pattern string /
* @returns Compiled regex /
*/
function createMatchRegex(match: string) {
if (matchCache.has(match))
return matchCache.get(match)!
@ -57,6 +115,18 @@ function createMatchRegex(match: string) {
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) {
const relativePath = filePathRelative || ''
if (match[0] === '^') {
@ -69,11 +139,22 @@ function toMatch(match: string, pagePath: string, filePathRelative?: string | nu
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 {
const { page } = useData()
const route = useRoute()
const encrypt = useEncryptData()
/**
* Whether the current page has encryption enabled
* Checks page-specific encryption and rule-based encryption
*/
const hasPageEncrypt = computed(() => {
const pagePath = route.path
const filePathRelative = page.value.filePathRelative
@ -85,6 +166,10 @@ export function setupEncrypt(): void {
: false
})
/**
* Whether global encryption is decrypted
* Checks if any admin password hash matches the stored hash
*/
const isGlobalDecrypted = computedAsync(async () => {
const hash = storage.value.g
if (!encrypt.value.global)
@ -97,6 +182,10 @@ export function setupEncrypt(): void {
return false
}, !encrypt.value.global)
/**
* List of encryption rules applicable to the current page
* Includes page-specific rules and matching pattern rules
*/
const hashList = computed(() => {
const pagePath = route.path
const filePathRelative = page.value.filePathRelative
@ -112,6 +201,10 @@ export function setupEncrypt(): void {
return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[]
})
/**
* Whether the current page is decrypted
* Checks admin passwords and page-specific passwords
*/
const isPageDecrypted = computedAsync(async () => {
if (!hasPageEncrypt.value)
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 {
const result = inject(EncryptSymbol)
@ -151,6 +253,14 @@ export function useEncrypt(): Encrypt {
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(): {
compareGlobal: (password: string) => Promise<boolean>
comparePage: (password: string) => Promise<boolean>
@ -160,6 +270,13 @@ export function useEncryptCompare(): {
const route = useRoute()
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> {
if (!password)
return false
@ -174,6 +291,13 @@ export function useEncryptCompare(): {
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> {
if (!password)
return false

View File

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

View File

@ -4,19 +4,44 @@ import { computed, toValue } from 'vue'
import { resolveRoute, resolveRouteFullPath, useRoute } from 'vuepress/client'
import { useData } from './data.js'
/**
* Link resolution result interface
* Provides information about the resolved link
*
*
*
*/
interface UseLinkResult {
/**
*
* Whether the link is external
*
*/
isExternal: ComputedRef<boolean>
/**
*
* Whether the link uses an external protocol
* Does not include target="_blank" cases
* 使
* target="_blank"
*/
isExternalProtocol: ComputedRef<boolean>
/**
* The resolved link URL
* URL
*/
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(
href: MaybeRefOrGetter<string | undefined>,
target?: MaybeRefOrGetter<string | undefined>,
@ -24,6 +49,8 @@ export function useLink(
const route = useRoute()
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(() => {
@ -36,6 +63,7 @@ export function useLink(
return false
})
// Pre-process link, try to convert to internal link
// 预处理链接,尝试转为内部的链接
const preProcessLink = computed(() => {
const link = toValue(href)
@ -45,6 +73,8 @@ export function useLink(
const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined
const path = resolveRouteFullPath(link, currentPath)
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) {
@ -62,6 +92,7 @@ export function useLink(
if (!link || link[0] === '#')
return false
// Check if it's a non-existent route
// 判断是否为不存在的路由
const routePath = link.split(/[?#]/)[0]
const currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : undefined
@ -74,6 +105,7 @@ export function useLink(
})
const link = computed(() => {
// Keep external links as-is
// 外部链接保持原样
if (isExternal.value) {
return toValue(href)

View File

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

View File

@ -7,46 +7,81 @@ import { onContentUpdated, useRouter } from 'vuepress/client'
import { useData } from './data.js'
import { useLayout } from './layout.js'
/**
* Header interface representing a page heading
*
* Header
*/
export interface Header {
/**
* The level of the header
*
* `1` to `6` for `<h1>` to `<h6>`
*
*
* `1` `6` `<h1>` `<h6>`
*/
level: number
/**
* The title of the header
*
*
*/
title: string
/**
* The slug of the header
*
* Typically the `id` attr of the header anchor
*
* slug
* `id`
*/
slug: string
/**
* Link of the header
*
* Typically using `#${slug}` as the anchor hash
*
*
* 使 `#${slug}`
*/
link: string
/**
* The children of the header
*
*
*/
children: Header[]
}
// cached list of anchor elements from resolveHeaders
// 从 resolveHeaders 缓存的锚点元素列表
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'> & {
/** Reference to the DOM element / DOM 元素引用 */
element: HTMLHeadElement
/** Child menu items / 子菜单项 */
children?: MenuItem[]
/** Lowest level for outline display / 目录显示的最低级别 */
lowLevel?: number
}
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[]> {
const { frontmatter, theme } = useData()
@ -57,10 +92,28 @@ export function setupHeaders(): Ref<MenuItem[]> {
return headers
}
/**
* Use headers
* Returns the reactive headers reference for the current page
*
*
*
* @returns Reactive reference to menu items /
*/
export function useHeaders(): Ref<MenuItem[]> {
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[] {
const heading = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const ignores = Array.from(document.querySelectorAll(
@ -87,6 +140,14 @@ export function getHeaders(range?: ThemeOutline): MenuItem[] {
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] {
const levelsRange = range || 2
// [high, low]
@ -97,6 +158,17 @@ function getRange(range?: Exclude<ThemeOutline, boolean>): readonly [number, num
: 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 {
if (!el.hasAttribute('data-outline') && !el.hasAttribute('outline'))
return
@ -114,6 +186,16 @@ function getLowLevel(el: HTMLHeadElement, level: number): number | 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 {
// <hx><a href="#"><span>title</span></a></hx>
const anchor = h.firstChild
@ -146,6 +228,15 @@ function serializeHeader(h: Element): string {
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[]) {
if (list?.length) {
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[] {
headers = headers.filter(h => h.level >= high)
// clear previous caches
@ -175,6 +277,7 @@ export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] {
const cur = headers[i]
if (i === 0) {
ret.push(cur)
continue
}
else {
for (let j = i - 1; j >= 0; j--) {
@ -192,6 +295,17 @@ export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] {
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[] {
return headers.map((header) => {
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 {
const { isAsideEnabled } = useLayout()
const router = useRouter()
@ -210,6 +333,10 @@ export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<
let prevActiveLink: HTMLAnchorElement | null = null
/**
* Set the active link based on scroll position
* Determines which header is currently in view
*/
const setActiveLink = (): void => {
if (!isAsideEnabled.value)
return
@ -257,6 +384,12 @@ export function useActiveAnchor(container: Ref<HTMLElement | null>, marker: Ref<
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 {
routeHash.value = hash || ''
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 {
let offsetTop = 0
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> {
const { path, query } = router.currentRoute.value

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,16 +7,47 @@ import { clientDataSymbol } from 'vuepress/client'
declare const __VUE_HMR_RUNTIME__: Record<string, any>
/**
* Theme data reference type
*
*
*/
export type ThemeDataRef<T extends ThemeData = ThemeData> = Ref<T>
/**
* Theme locale data reference type
*
*
*/
export type ThemeLocaleDataRef<T extends ThemeData = ThemeData> = ComputedRef<T>
/**
* Injection key for theme locale data
*
*
*/
export const themeLocaleDataSymbol: InjectionKey<ThemeLocaleDataRef> = Symbol(
__VUEPRESS_DEV__ ? 'themeLocaleData' : '',
)
/**
* Theme data ref
* Global reference to the theme configuration data
*
*
*
*/
export const themeData: ThemeDataRef = ref(themeDataRaw)
/**
* Use theme data
* Returns the global theme data reference
*
*
*
*
* @returns Theme data reference /
*/
export function useThemeData<
T extends ThemeData = ThemeData,
>(): 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<
T extends ThemeData = ThemeData,
>(): ThemeLocaleDataRef<T> {
@ -40,8 +80,15 @@ export function useThemeLocaleData<
}
/**
* Merge the locales fields to the root fields
* according to the route path
* Merge the locales fields to the root fields 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 {
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 {
// provide theme data & theme locale data
const themeData = useThemeData()

View File

@ -1,14 +1,33 @@
/**
* @method
* t: current time
* b: beginning value
* c: change in value
* d: duration
* Tweening algorithm for smooth animation
* Implements cubic easing function for natural motion
*
*
*
*
* @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 {
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 {
return (c * t) / d + b
}

View File

@ -1,5 +1,14 @@
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 {
const val = el?.ownerDocument?.defaultView?.getComputedStyle(el, null)?.[
property as any
@ -8,6 +17,14 @@ export function getCssValue(el: HTMLElement | null, property: string): number {
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(
target: Document | HTMLElement = document,
): 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(
target: Document | HTMLElement = document,
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(
target: Document | HTMLElement,
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 {
if (!target)
return 0

View File

@ -6,6 +6,13 @@ import {
} from 'vuepress/shared'
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> = {
GitHub: ':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',
}
/**
* 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({
docsRepo,
editLinkPattern,
@ -31,6 +48,21 @@ function resolveEditLinkPatterns({
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({
docsRepo,
docsBranch,

View File

@ -9,7 +9,13 @@ import { resolveRoute } from 'vuepress/client'
/**
* 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
* - Input: '/README.md'
* - 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 {
path = path.replace(/index\.html?$/i, '').replace(/\.html?$/i, '').replace(/\/$/, '')
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 {
return isLinkAbsolute(link) || isLinkWithProtocol(link)
? link
: 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 {
return ensureEndingSlash(normalizeLink(base, link))
}

View File

@ -1,7 +1,24 @@
import { isLinkHttp } from 'vuepress/shared'
/**
* Supported repository types
* Represents the type of code hosting platform
*
*
*
*/
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 {
if (!isLinkHttp(repo) || /github\.com/.test(repo))
return 'GitHub'

View File

@ -1,14 +1,62 @@
/**
* Regular expression to match external URLs
*
* URL
*/
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 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)$/
/**
* Whether running in browser
*
*
*/
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[] {
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(
currentPath: string,
matchPath?: string,
@ -33,10 +81,28 @@ export function isActive(
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 {
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 {
if (typeof value === 'undefined')
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']
/**
* 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 {
return gradient.some(v => value.startsWith(v))
}

View File

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

View File

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

View File

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

View File

@ -7,5 +7,7 @@ export { plumeTheme }
/**
* @deprecated 使
*
* @deprecated Please use named exports instead of default export
*/
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
* issue

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,11 @@ import { isPlainObject } from '@vuepress/helper'
import { gitPlugin as rawGitPlugin } from '@vuepress/plugin-git'
import { getThemeConfig } from '../loadConfig/index.js'
/**
* Setup git plugin
*
* Git
*/
export function gitPlugin(app: App, pluginOptions: ThemeBuiltinPlugins): PluginConfig {
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 RESTORE_RE = /<!-- llms-code-block:(\w+) -->/g
/**
* Setup LLMs plugin
*
* LLM LLM Markdown
*/
export function llmsPlugin(app: App, userOptions: true | LlmsPluginOptions): PluginConfig {
if (!app.env.isBuild)
return []

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