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')