diff --git a/plugins/plugin-md-power/__test__/alignPlugin.spec.ts b/plugins/plugin-md-power/__test__/alignPlugin.spec.ts
index 0b4ae114..7b927880 100644
--- a/plugins/plugin-md-power/__test__/alignPlugin.spec.ts
+++ b/plugins/plugin-md-power/__test__/alignPlugin.spec.ts
@@ -4,10 +4,21 @@ import { alignPlugin } from '../src/node/container/align.js'
describe('alignPlugin', () => {
const md = new MarkdownIt().use(alignPlugin)
- it('should work', () => {
+ it('should work with align', () => {
expect(md.render(':::left\n:::')).toContain('style="text-align:left"')
expect(md.render(':::center\n:::')).toContain('style="text-align:center"')
expect(md.render(':::right\n:::')).toContain('style="text-align:right"')
expect(md.render(':::justify\n:::')).toContain('style="text-align:justify"')
})
+
+ it('should work with flex', () => {
+ expect(md.render(':::flex\n:::')).toContain('display:flex')
+ expect(md.render(':::flex start\n:::')).toContain('align-items:flex-start')
+ expect(md.render(':::flex end\n:::')).toContain('align-items:flex-end')
+ expect(md.render(':::flex center\n:::')).toContain('align-items:center')
+ expect(md.render(':::flex between\n:::')).toContain('justify-content:space-between')
+ expect(md.render(':::flex around\n:::')).toContain('justify-content:space-around')
+ expect(md.render(':::flex wrap\n:::')).toContain('flex-wrap:wrap')
+ expect(md.render(':::flex column\n:::')).toContain('flex-direction:column')
+ })
})
diff --git a/plugins/plugin-md-power/src/node/container/align.ts b/plugins/plugin-md-power/src/node/container/align.ts
index 9c32eb59..205b0a6d 100644
--- a/plugins/plugin-md-power/src/node/container/align.ts
+++ b/plugins/plugin-md-power/src/node/container/align.ts
@@ -3,9 +3,9 @@ import { parseRect } from '../utils/parseRect.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { createContainerPlugin } from './createContainer.js'
-const alignList = ['left', 'center', 'right', 'justify']
-
export function alignPlugin(md: Markdown): void {
+ const alignList = ['left', 'center', 'right', 'justify']
+
for (const name of alignList) {
createContainerPlugin(md, name, {
before: () => `
`,
diff --git a/plugins/plugin-md-power/src/node/container/codeTree.ts b/plugins/plugin-md-power/src/node/container/codeTree.ts
index 008f33a0..0599ddcd 100644
--- a/plugins/plugin-md-power/src/node/container/codeTree.ts
+++ b/plugins/plugin-md-power/src/node/container/codeTree.ts
@@ -61,6 +61,9 @@ const UNSUPPORTED_FILE_TYPES = [
'xlsx',
]
+/**
+ * code-tree 容器元信息
+ */
interface CodeTreeMeta {
title?: string
/**
@@ -78,6 +81,9 @@ interface CodeTreeMeta {
entry?: string
}
+/**
+ * 文件树节点类型
+ */
interface FileTreeNode {
level: number
children?: FileTreeNode[]
@@ -85,6 +91,11 @@ interface FileTreeNode {
filepath?: string
}
+/**
+ * 将文件路径数组解析为文件树节点结构
+ * @param files 文件路径数组
+ * @returns 文件树节点数组
+ */
function parseFileNodes(files: string[]): FileTreeNode[] {
const nodes: FileTreeNode[] = []
for (const file of files) {
@@ -111,7 +122,16 @@ function parseFileNodes(files: string[]): FileTreeNode[] {
return nodes
}
+/**
+ * 注册 code-tree 容器和嵌入语法的 markdown 插件
+ * @param md markdown-it 实例
+ * @param app vuepress app 实例
+ * @param options code-tree 配置项
+ */
export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions = {}): void {
+ /**
+ * 获取文件或文件夹的图标
+ */
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
mode ||= options.icon || 'colored'
if (mode === 'simple')
@@ -119,6 +139,9 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
return getFileIcon(filename, type)
}
+ /**
+ * 渲染文件树节点为组件字符串
+ */
function renderFileTree(nodes: FileTreeNode[], mode?: FileTreeIconMode): string {
return nodes.map((node) => {
const props: FileTreeNodeProps & { filepath?: string } = {
@@ -136,8 +159,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
.join('\n')
}
+ // 注册 ::: code-tree 容器
createContainerPlugin(md, 'code-tree', {
before: (info, tokens, index) => {
+ // 收集 code-tree 容器内的文件名和激活文件
const files: string[] = []
let activeFile: string | undefined
for (
@@ -172,6 +197,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
after: () => '',
})
+ // 注册 @[code-tree](dir) 语法
createEmbedRuleBlock(md, {
type: 'code-tree',
syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/,
@@ -187,8 +213,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
}
},
content: ({ dir, icon, ...props }, _, env) => {
+ // codeTreeFiles 用于页面依赖收集
const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[]
const root = findFile(app, env, dir)
+ // 获取目录下所有文件
const files = globSync('**/*', {
cwd: root,
onlyFiles: true,
@@ -201,6 +229,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
})
props.entryFile ||= files[0]
+ // 生成所有文件的代码块内容
const codeContent = files.map((file) => {
const ext = path.extname(file).slice(1)
if (UNSUPPORTED_FILE_TYPES.includes(ext)) {
@@ -220,6 +249,10 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
})
}
+/**
+ * 扩展页面依赖,将 codeTreeFiles 添加到页面依赖中
+ * @param page vuepress 页面对象
+ */
export function extendsPageWithCodeTree(page: Page): void {
const markdownEnv = page.markdownEnv
const codeTreeFiles = (markdownEnv.codeTreeFiles ?? []) as string[]
diff --git a/plugins/plugin-md-power/src/node/container/createContainer.ts b/plugins/plugin-md-power/src/node/container/createContainer.ts
index 04dc578f..008466ee 100644
--- a/plugins/plugin-md-power/src/node/container/createContainer.ts
+++ b/plugins/plugin-md-power/src/node/container/createContainer.ts
@@ -4,29 +4,51 @@ import type { Markdown } from 'vuepress/markdown'
import container from 'markdown-it-container'
import { resolveAttrs } from '../utils/resolveAttrs.js'
+/**
+ * RenderRuleParams 类型用于获取 RenderRule 的参数类型。
+ */
type RenderRuleParams = Parameters
extends [...infer Args, infer _] ? Args : never
+/**
+ * 自定义容器的配置项。
+ * - before: 渲染容器起始标签时的回调
+ * - after: 渲染容器结束标签时的回调
+ */
export interface ContainerOptions {
before?: (info: string, ...args: RenderRuleParams) => string
after?: (info: string, ...args: RenderRuleParams) => string
}
+/**
+ * 创建 markdown-it 的自定义容器插件。
+ *
+ * @param md markdown-it 实例
+ * @param type 容器类型(如 'tip', 'warning' 等)
+ * @param options 可选的 before/after 渲染钩子
+ * @param options.before 渲染容器起始标签时的回调函数
+ * @param options.after 渲染容器结束标签时的回调函数
+ */
export function createContainerPlugin(
md: Markdown,
type: string,
{ before, after }: ContainerOptions = {},
): void {
+ // 自定义渲染规则
const render: RenderRule = (tokens, index, options, env): string => {
const token = tokens[index]
+ // 提取 ::: 后的 info 信息
const info = token.info.trim().slice(type.length).trim() || ''
if (token.nesting === 1) {
+ // 容器起始标签
return before?.(info, tokens, index, options, env) ?? ``
}
else {
+ // 容器结束标签
return after?.(info, tokens, index, options, env) ?? '
'
}
}
+ // 注册 markdown-it-container 插件
md.use(container, type, { render })
}
@@ -55,17 +77,27 @@ export function createContainerSyntaxPlugin(
const maker = ':'
const markerMinLen = 3
+ /**
+ * 自定义容器的 block 规则定义。
+ * @param state 当前 block 状态
+ * @param startLine 起始行
+ * @param endLine 结束行
+ * @param silent 是否为静默模式
+ * @returns 是否匹配到自定义容器
+ */
function defineContainer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
const start = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
let pos = start
// check marker
+ // 检查是否以指定的 maker(:)开头
if (state.src[pos] !== maker)
return false
pos += markerMinLen
+ // 检查 marker 长度是否满足要求
for (pos = start + 1; pos <= max; pos++) {
if (state.src[pos] !== maker)
break
@@ -78,6 +110,7 @@ export function createContainerSyntaxPlugin(
const info = state.src.slice(pos, max).trim()
// ::: type
+ // 检查 info 是否以 type 开头
if (!info.startsWith(type))
return false
@@ -87,6 +120,7 @@ export function createContainerSyntaxPlugin(
let line = startLine
let content = ''
+ // 收集容器内容,直到遇到结束的 marker
while (++line < endLine) {
if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === markup) {
break
@@ -95,6 +129,7 @@ export function createContainerSyntaxPlugin(
content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n`
}
+ // 创建 token,保存内容和属性
const token = state.push(`${type}_container`, '', 0)
token.meta = resolveAttrs(info.slice(type.length)).attrs
token.content = content
@@ -106,11 +141,13 @@ export function createContainerSyntaxPlugin(
return true
}
+ // 默认渲染函数
const defaultRender: RenderRule = (tokens, index) => {
const { content } = tokens[index]
return `${content}
`
}
+ // 注册 block 规则和渲染规则
md.block.ruler.before('fence', `${type}_definition`, defineContainer)
md.renderer.rules[`${type}_container`] = render ?? defaultRender
}
diff --git a/plugins/plugin-md-power/src/node/container/fileTree.ts b/plugins/plugin-md-power/src/node/container/fileTree.ts
index ab6cd24c..7b31aa64 100644
--- a/plugins/plugin-md-power/src/node/container/fileTree.ts
+++ b/plugins/plugin-md-power/src/node/container/fileTree.ts
@@ -5,17 +5,26 @@ import { defaultFile, defaultFolder, getFileIcon } from '../fileIcons/index.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerSyntaxPlugin } from './createContainer.js'
+/**
+ * 文件树节点结构
+ */
interface FileTreeNode {
info: string
level: number
children: FileTreeNode[]
}
+/**
+ * 文件树容器属性
+ */
interface FileTreeAttrs {
title?: string
icon?: FileTreeIconMode
}
+/**
+ * 文件树节点属性(用于渲染组件)
+ */
export interface FileTreeNodeProps {
filename: string
comment?: string
@@ -26,6 +35,11 @@ export interface FileTreeNodeProps {
level?: number
}
+/**
+ * 解析原始文件树内容为节点树结构
+ * @param content 文件树的原始文本内容
+ * @returns 文件树节点数组
+ */
export function parseFileTreeRawContent(content: string): FileTreeNode[] {
const root: FileTreeNode = { info: '', level: -1, children: [] }
const stack: FileTreeNode[] = [root]
@@ -54,6 +68,11 @@ export function parseFileTreeRawContent(content: string): FileTreeNode[] {
const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/
+/**
+ * 解析单个节点的 info 字符串,提取文件名、注释、类型等属性
+ * @param info 节点描述字符串
+ * @returns 文件树节点属性
+ */
export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let filename = ''
let comment = ''
@@ -62,6 +81,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let type: 'folder' | 'file' = 'file'
let diff: 'add' | 'remove' | undefined
+ // 处理 diff 标记
if (info.startsWith('++')) {
info = info.slice(2).trim()
diff = 'add'
@@ -71,12 +91,14 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
diff = 'remove'
}
+ // 处理高亮(focus)标记
info = info.replace(RE_FOCUS, (_, matched) => {
filename = matched
focus = true
return ''
})
+ // 提取文件名和注释
if (filename === '' && !focus) {
const spaceIndex = info.indexOf(' ')
filename = info.slice(0, spaceIndex === -1 ? info.length : spaceIndex)
@@ -85,6 +107,7 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
comment = info.trim()
+ // 判断是否为文件夹
if (filename.endsWith('/')) {
type = 'folder'
expanded = false
@@ -94,7 +117,15 @@ export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
return { filename, comment, focus, expanded, type, diff }
}
+/**
+ * 文件树 markdown 插件主函数
+ * @param md markdown 实例
+ * @param options 文件树渲染选项
+ */
export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}): void {
+ /**
+ * 获取文件或文件夹的图标
+ */
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
mode ||= options.icon || 'colored'
if (mode === 'simple')
@@ -102,12 +133,16 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}): voi
return getFileIcon(filename, type)
}
+ /**
+ * 递归渲染文件树节点
+ */
const renderFileTree = (nodes: FileTreeNode[], meta: FileTreeAttrs): string =>
nodes.map((node) => {
const { info, level, children } = node
const { filename, comment, focus, expanded, type, diff } = parseFileTreeNodeInfo(info)
const isOmit = filename === '…' || filename === '...' /* fallback */
+ // 文件夹无子节点时补充省略号
if (children.length === 0 && type === 'folder') {
children.push({ info: '…', level: level + 1, children: [] })
}
@@ -132,6 +167,7 @@ ${renderedIcon}${renderedComment}${children.length > 0 ? renderFileTree(children
`
}).join('\n')
+ // 注册自定义容器语法插件
return createContainerSyntaxPlugin(
md,
'file-tree',
diff --git a/plugins/plugin-md-power/src/node/container/npmTo.ts b/plugins/plugin-md-power/src/node/container/npmTo.ts
index 0d1bb3d3..0a9772e0 100644
--- a/plugins/plugin-md-power/src/node/container/npmTo.ts
+++ b/plugins/plugin-md-power/src/node/container/npmTo.ts
@@ -28,10 +28,14 @@ import type { CommandConfig, CommandConfigItem } from './npmToPreset.js'
import { isArray } from '@vuepress/helper'
import { colors } from 'vuepress/utils'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
+import { logger } from '../utils/logger.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { createContainerPlugin } from './createContainer.js'
import { ALLOW_LIST, BOOL_FLAGS, DEFAULT_TABS, MANAGERS_CONFIG } from './npmToPreset.js'
+/**
+ * 注册 npm-to 容器插件,将 npm 代码块自动转换为多包管理器命令分组
+ */
export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
const opt = isArray(options) ? { tabs: options } : options
const defaultTabs = opt.tabs?.length ? opt.tabs : DEFAULT_TABS
@@ -46,19 +50,28 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
token.hidden = true
token.type = 'text'
token.content = ''
+ // 拆分命令行内容,转换为多包管理器命令
const lines = content.split(/(\n|\s*&&\s*)/)
return md.render(
resolveNpmTo(lines, token.info.trim(), idx, tabs),
cleanMarkdownEnv(env),
)
}
- console.warn(`${colors.yellow('[vuepress-plugin-md-power]')} Invalid npm-to container in ${colors.gray(env.filePathRelative || env.filePath)}`)
+ // 非法容器警告
+ logger.warn('npm-to', `Invalid npm-to container in ${colors.gray(env.filePathRelative || env.filePath)}`)
return ''
},
after: () => '',
})
}
+/**
+ * 将 npm 命令转换为各包管理器命令分组
+ * @param lines 命令行数组
+ * @param info 代码块类型
+ * @param idx token 索引
+ * @param tabs 需要支持的包管理器
+ */
function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string {
tabs = validateTabs(tabs)
const res: string[] = []
@@ -68,6 +81,7 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
for (const line of lines) {
const config = findConfig(line)
if (config && config[tab]) {
+ // 解析并替换命令参数
const parsed = (map[line] ??= parseLine(line)) as LineParsed
const { cli, flags } = config[tab] as CommandConfigItem
@@ -91,11 +105,15 @@ function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPac
newLines.push(line)
}
}
+ // 拼接为 code-tabs 格式
res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\n\`\`\``)
}
return `:::code-tabs#npm-to-${tabs.join('-')}\n${res.join('\n')}\n:::`
}
+/**
+ * 根据命令行内容查找对应的包管理器配置
+ */
function findConfig(line: string): CommandConfig | undefined {
for (const { pattern, ...config } of Object.values(MANAGERS_CONFIG)) {
if (pattern.test(line)) {
@@ -105,6 +123,9 @@ function findConfig(line: string): CommandConfig | undefined {
return undefined
}
+/**
+ * 校验 tabs 合法性,返回允许的包管理器列表
+ */
function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
tabs = tabs.filter(tab => ALLOW_LIST.includes(tab))
if (tabs.length === 0) {
@@ -113,16 +134,22 @@ function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
return tabs
}
+/**
+ * 命令行解析结果类型
+ */
interface LineParsed {
- env: string
- cli: string
- cmd: string
- args?: string
- scriptArgs?: string
+ env: string // 环境变量前缀
+ cli: string // 命令行工具(npm/npx ...)
+ cmd: string // 命令/脚本名
+ args?: string // 参数
+ scriptArgs?: string // 脚本参数
}
const LINE_REG = /(.*)(npm|npx)\s+(.*)/
+/**
+ * 解析一行 npm/npx 命令,拆分出环境变量、命令、参数等
+ */
export function parseLine(line: string): false | LineParsed {
const match = line.match(LINE_REG)
if (!match)
@@ -149,6 +176,9 @@ export function parseLine(line: string): false | LineParsed {
return { env, cli: `${cli} ${rest.slice(0, idx)}`, ...parseArgs(rest.slice(idx + 1)) }
}
+/**
+ * 解析 npm 命令参数,区分命令、参数、脚本参数
+ */
function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } {
line = line?.trim()
@@ -156,6 +186,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
let cmd = ''
let args = ''
if (npmArgs[0] !== '-') {
+ // 处理命令和参数
if (npmArgs[0] === '"' || npmArgs[0] === '\'') {
const idx = npmArgs.slice(1).indexOf(npmArgs[0])
cmd = npmArgs.slice(0, idx + 2)
@@ -173,6 +204,7 @@ function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: str
}
}
else {
+ // 处理仅有参数的情况
let newLine = ''
let value = ''
let isQuote = false
diff --git a/plugins/plugin-md-power/src/node/demo/demo.ts b/plugins/plugin-md-power/src/node/demo/demo.ts
index 0d868e19..a124ea76 100644
--- a/plugins/plugin-md-power/src/node/demo/demo.ts
+++ b/plugins/plugin-md-power/src/node/demo/demo.ts
@@ -4,13 +4,28 @@ import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import container from 'markdown-it-container'
+import { colors } from 'vuepress/utils'
import { createEmbedRuleBlock } from '../embed/createEmbedRuleBlock.js'
+import { logger } from '../utils/logger.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { markdownContainerRender, markdownEmbed } from './markdown.js'
import { normalContainerRender, normalEmbed } from './normal.js'
import { normalizeAlias } from './supports/alias.js'
import { vueContainerRender, vueEmbed } from './vue.js'
+const embedMap: Record<
+ DemoMeta['type'],
+ (app: App, md: Markdown, env: MarkdownDemoEnv, meta: DemoMeta) => string
+> = {
+ vue: vueEmbed,
+ normal: normalEmbed,
+ markdown: markdownEmbed,
+}
+
+/**
+ * 嵌入语法
+ * @[demo type info](url)
+ */
export function demoEmbed(app: App, md: Markdown): void {
createEmbedRuleBlock(md, {
type: 'demo',
@@ -23,21 +38,13 @@ export function demoEmbed(app: App, md: Markdown): void {
content: (meta, content, env: MarkdownDemoEnv) => {
const { url, type } = meta
if (!url) {
- console.warn('[vuepress-plugin-md-power] Invalid demo url: ', url)
+ logger.warn('demo-vue', `Invalid filepath: ${colors.gray(url)}`)
return content
}
- if (type === 'vue') {
- return vueEmbed(app, md, env, meta)
- }
- if (type === 'normal') {
- return normalEmbed(app, md, env, meta)
+ if (embedMap[type]) {
+ return embedMap[type](app, md, env, meta)
}
-
- if (type === 'markdown') {
- return markdownEmbed(app, md, env, meta)
- }
-
return content
},
})
diff --git a/plugins/plugin-md-power/src/node/demo/markdown.ts b/plugins/plugin-md-power/src/node/demo/markdown.ts
index 117989e3..2ce85dc9 100644
--- a/plugins/plugin-md-power/src/node/demo/markdown.ts
+++ b/plugins/plugin-md-power/src/node/demo/markdown.ts
@@ -1,6 +1,8 @@
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
+import { colors } from 'vuepress/utils'
+import { logger } from '../utils/logger.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { findFile, readFileSync } from './supports/file.js'
@@ -13,7 +15,7 @@ export function markdownEmbed(
const filepath = findFile(app, env, url)
const code = readFileSync(filepath)
if (code === false) {
- console.warn('[vuepress-plugin-md-power] Cannot read markdown file:', filepath)
+ logger.warn('demo-markdown', `Cannot read markdown file: ${colors.gray(filepath)}\n at: ${colors.gray(env.filePathRelative || '')}`)
return ''
}
const demo: DemoFile = { type: 'markdown', path: filepath }
diff --git a/plugins/plugin-md-power/src/node/demo/normal.ts b/plugins/plugin-md-power/src/node/demo/normal.ts
index 70d1d1d3..9b91cc79 100644
--- a/plugins/plugin-md-power/src/node/demo/normal.ts
+++ b/plugins/plugin-md-power/src/node/demo/normal.ts
@@ -3,6 +3,8 @@ import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import fs from 'node:fs'
import path from 'node:path'
+import { colors } from 'vuepress/utils'
+import { logger } from '../utils/logger.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { compileScript, compileStyle } from './supports/compiler.js'
import { findFile, readFileSync, writeFileSync } from './supports/file.js'
@@ -89,7 +91,7 @@ export async function compileCode(code: NormalCode, output: string): Promise {
export const audioReaderPlugin: PluginWithOptions = (md) => {
md.renderer.rules.audio_reader = (tokens, idx) => {
- const meta = (tokens[idx].meta ?? {}) as AudioReaderTokenMeta
+ const meta = tokens[idx].meta as AudioReaderTokenMeta
if (meta.startTime)
meta.startTime = Number(meta.startTime)
diff --git a/plugins/plugin-md-power/src/node/embed/video/artPlayer.ts b/plugins/plugin-md-power/src/node/embed/video/artPlayer.ts
index f01df181..91389d13 100644
--- a/plugins/plugin-md-power/src/node/embed/video/artPlayer.ts
+++ b/plugins/plugin-md-power/src/node/embed/video/artPlayer.ts
@@ -6,6 +6,7 @@ import type { PluginWithOptions } from 'markdown-it'
import type { ArtPlayerTokenMeta } from '../../../shared/index.js'
import { isPackageExists } from 'local-pkg'
import { colors } from 'vuepress/utils'
+import { logger } from '../../utils/logger.js'
import { parseRect } from '../../utils/parseRect.js'
import { resolveAttrs } from '../../utils/resolveAttrs.js'
import { stringifyAttrs } from '../../utils/stringifyAttrs.js'
@@ -75,11 +76,11 @@ function checkSupportType(type?: string) {
}
/* istanbul ignore if -- @preserve */
if (name) {
- console.warn(`${colors.yellow('[vuepress-plugin-md-power] artPlayer: ')} ${colors.cyan(name)} is not installed, please install it via npm or yarn or pnpm`)
+ logger.warn('artPlayer', `${colors.cyan(name)} is not installed, please install it via npm or yarn or pnpm`)
}
}
else {
/* istanbul ignore next -- @preserve */
- console.warn(`${colors.yellow('[vuepress-plugin-md-power] artPlayer: ')} unsupported video type: ${colors.cyan(type)}`)
+ logger.warn('artPlayer', `unsupported video type: ${colors.cyan(type)}`)
}
}
diff --git a/plugins/plugin-md-power/src/node/icon/icon.ts b/plugins/plugin-md-power/src/node/icon/icon.ts
index a32fb7b1..62be8ca3 100644
--- a/plugins/plugin-md-power/src/node/icon/icon.ts
+++ b/plugins/plugin-md-power/src/node/icon/icon.ts
@@ -2,6 +2,7 @@ import type { PluginWithOptions } from 'markdown-it'
import type { MarkdownEnv } from 'vuepress/markdown'
import type { IconOptions } from '../../shared/index.js'
import { colors } from 'vuepress/utils'
+import { logger } from '../utils/logger.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createIconRule } from './createIconRule.js'
import { resolveIcon } from './resolveIcon.js'
@@ -42,7 +43,7 @@ export const iconPlugin: PluginWithOptions = (md, options = {}) =>
const [size, color] = opt.trim().split('/')
icon = `${name}${size ? ` =${size}` : ''}${color ? ` /${color}` : ''}`
- console.warn(`The icon syntax of \`${colors.yellow(`:[${content}]:`)}\` is deprecated, please use \`${colors.green(`::${icon}::`)}\` instead. (${colors.gray(env.filePathRelative || env.filePath)})`)
+ logger.warn('icon', `The icon syntax of \`${colors.yellow(`:[${content}]:`)}\` is deprecated, please use \`${colors.green(`::${icon}::`)}\` instead. (${colors.gray(env.filePathRelative || env.filePath)})`)
}
return iconRender(icon, options)
diff --git a/plugins/plugin-md-power/src/node/icon/prepareIcon.ts b/plugins/plugin-md-power/src/node/icon/prepareIcon.ts
index 91dce054..f7cdc8b6 100644
--- a/plugins/plugin-md-power/src/node/icon/prepareIcon.ts
+++ b/plugins/plugin-md-power/src/node/icon/prepareIcon.ts
@@ -2,6 +2,7 @@ import type { IconOptions } from '../../shared/index.js'
import { notNullish, toArray, uniqueBy } from '@pengzhanbo/utils'
import { isLinkAbsolute } from '@vuepress/helper'
import { isLinkHttp } from 'vuepress/shared'
+import { logger } from '../utils/logger.js'
interface AssetInfo {
type: 'style' | 'script'
@@ -76,7 +77,7 @@ function normalizeAsset(asset: string, provide?: string): AssetInfo | null {
if (asset.endsWith('.css')) {
return { type: 'style', link, provide }
}
- console.error(`[vuepress:icon] Can not recognize icon link: "${asset}"`)
+ logger.error('icon', `Can not recognize icon link: "${asset}"`)
return null
}
diff --git a/plugins/plugin-md-power/src/node/utils/logger.ts b/plugins/plugin-md-power/src/node/utils/logger.ts
new file mode 100644
index 00000000..ece2b416
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/utils/logger.ts
@@ -0,0 +1,78 @@
+/* istanbul ignore file -- @preserve */
+/* eslint-disable no-console */
+import { colors, ora } from 'vuepress/utils'
+
+type Ora = ReturnType
+
+/**
+ * Logger utils
+ */
+export class Logger {
+ public constructor(
+ /**
+ * Plugin/Theme name
+ */
+ private readonly name = '',
+ ) {}
+
+ private init(subname: string, text: string): Ora {
+ return ora({
+ prefixText: colors.blue(`${this.name}${subname ? `:${subname}` : ''}: `),
+ text,
+ })
+ }
+
+ /**
+ * Create a loading spinner with text
+ */
+ public load(subname: string, msg: string): {
+ succeed: (text?: string) => void
+ fail: (text?: string) => void
+ } {
+ const instance = this.init(subname, msg)
+
+ return {
+ succeed: (text?: string) => instance.succeed(text),
+ fail: (text?: string) => instance.succeed(text),
+ }
+ }
+
+ public info(subname: string, text = '', ...args: unknown[]): void {
+ this.init(subname, colors.blue(text)).info()
+
+ if (args.length)
+ console.info(...args)
+ }
+
+ /**
+ * Log success msg
+ */
+ public succeed(subname: string, text = '', ...args: unknown[]): void {
+ this.init(subname, colors.green(text)).succeed()
+
+ if (args.length)
+ console.log(...args)
+ }
+
+ /**
+ * Log warning msg
+ */
+ public warn(subname: string, text = '', ...args: unknown[]): void {
+ this.init(subname, colors.yellow(text)).warn()
+
+ if (args.length)
+ console.warn(...args)
+ }
+
+ /**
+ * Log error msg
+ */
+ public error(subname: string, text = '', ...args: unknown[]): void {
+ this.init(subname, colors.red(text)).fail()
+
+ if (args.length)
+ console.error(...args)
+ }
+}
+
+export const logger: Logger = new Logger('vuepress-plugin-md-power')