feat(plugin-md-power): improve logger (#607)
This commit is contained in:
parent
3c384a4362
commit
d6a47419e4
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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: () => `<div style="text-align:${name}">`,
|
||||
|
||||
@ -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: () => '</VPCodeTree>',
|
||||
})
|
||||
|
||||
// 注册 @[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[]
|
||||
|
||||
@ -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<RenderRule> 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) ?? `<div class="custom-container ${type}">`
|
||||
}
|
||||
else {
|
||||
// 容器结束标签
|
||||
return after?.(info, tokens, index, options, env) ?? '</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 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 `<div class="custom-container ${type}">${content}</div>`
|
||||
}
|
||||
|
||||
// 注册 block 规则和渲染规则
|
||||
md.block.ruler.before('fence', `${type}_definition`, defineContainer)
|
||||
md.renderer.rules[`${type}_container`] = render ?? defaultRender
|
||||
}
|
||||
|
||||
@ -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
|
||||
</FileTreeNode>`
|
||||
}).join('\n')
|
||||
|
||||
// 注册自定义容器语法插件
|
||||
return createContainerSyntaxPlugin(
|
||||
md,
|
||||
'file-tree',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<DemoMeta>(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
|
||||
},
|
||||
})
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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<voi
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[vuepress-plugin-md-power] demo parse error: \n', e)
|
||||
logger.error('demo-normal', 'demo parse error: \n', e)
|
||||
}
|
||||
|
||||
writeFileSync(output, `import { ref } from "vue"\nexport default ref(${JSON.stringify(res, null, 2)})`)
|
||||
@ -106,7 +108,7 @@ export function normalEmbed(
|
||||
const code = readFileSync(filepath)
|
||||
|
||||
if (code === false) {
|
||||
console.warn('[vuepress-plugin-md-power] Cannot read demo file:', filepath)
|
||||
logger.warn('demo-normal', `Cannot read demo file: ${colors.gray(filepath)}\n at: ${colors.gray(env.filePathRelative || '')}`)
|
||||
return ''
|
||||
}
|
||||
const source = parseEmbedCode(code)
|
||||
|
||||
@ -2,6 +2,8 @@ import type { App } from 'vuepress'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
|
||||
import path from 'node:path'
|
||||
import { colors } from 'vuepress/utils'
|
||||
import { logger } from '../utils/logger.js'
|
||||
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
|
||||
import { findFile, readFileSync, writeFileSync } from './supports/file.js'
|
||||
import { insertSetupScript } from './supports/insertScript.js'
|
||||
@ -15,7 +17,7 @@ export function vueEmbed(
|
||||
const filepath = findFile(app, env, url)
|
||||
const code = readFileSync(filepath)
|
||||
if (code === false) {
|
||||
console.warn('[vuepress-plugin-md-power] Cannot read vue file:', filepath)
|
||||
logger.warn('demo-vue', `Cannot read vue demo file: ${colors.gray(filepath)}\n at: ${colors.gray(env.filePathRelative || '')}`)
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ const audioReader: RuleInline = (state, silent) => {
|
||||
|
||||
export const audioReaderPlugin: PluginWithOptions<never> = (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)
|
||||
|
||||
|
||||
@ -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)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<IconOptions> = (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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
78
plugins/plugin-md-power/src/node/utils/logger.ts
Normal file
78
plugins/plugin-md-power/src/node/utils/logger.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
/* eslint-disable no-console */
|
||||
import { colors, ora } from 'vuepress/utils'
|
||||
|
||||
type Ora = ReturnType<typeof ora>
|
||||
|
||||
/**
|
||||
* 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')
|
||||
Loading…
x
Reference in New Issue
Block a user