From b721bf08f9ecf5c71a8982f479a2909cfc7bd04d Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Tue, 8 Oct 2024 18:57:59 +0800 Subject: [PATCH] feat(plugin-md-power): add support `npm-to` container (#256) * feat(plugin-md-power): add support `npm-to` container * chore: tweak * chore: tweak * chore: tweak --- docs/.vuepress/notes/zh/theme-guide.ts | 1 + docs/.vuepress/theme.ts | 1 + docs/notes/theme/guide/markdown/npm-to.md | 206 ++++++++++ docs/notes/theme/guide/markdown/图标.md | 15 +- docs/notes/theme/guide/功能/图标.md | 15 +- docs/notes/theme/guide/安装与使用.md | 94 +---- docs/questions.md | 29 +- .../src/node/container/index.ts | 5 + .../src/node/container/npmTo.ts | 373 ++++++++++++++++++ plugins/plugin-md-power/src/shared/index.ts | 1 + plugins/plugin-md-power/src/shared/npmTo.ts | 5 + plugins/plugin-md-power/src/shared/plugin.ts | 7 + 12 files changed, 619 insertions(+), 133 deletions(-) create mode 100644 docs/notes/theme/guide/markdown/npm-to.md create mode 100644 plugins/plugin-md-power/src/node/container/npmTo.ts create mode 100644 plugins/plugin-md-power/src/shared/npmTo.ts diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts index 8fd87dcd..62acb5b0 100644 --- a/docs/.vuepress/notes/zh/theme-guide.ts +++ b/docs/.vuepress/notes/zh/theme-guide.ts @@ -40,6 +40,7 @@ export const themeGuide = defineNoteConfig({ '选项组', '隐秘文本', '示例容器', + 'npm-to', 'caniuse', '导入文件', ], diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 4d727611..2afa6f49 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -30,6 +30,7 @@ export const theme: Theme = plumeTheme({ replit: true, codeSandbox: true, jsfiddle: true, + npmTo: ['pnpm', 'yarn', 'npm'], repl: { go: true, rust: true, diff --git a/docs/notes/theme/guide/markdown/npm-to.md b/docs/notes/theme/guide/markdown/npm-to.md new file mode 100644 index 00000000..6b70ddc8 --- /dev/null +++ b/docs/notes/theme/guide/markdown/npm-to.md @@ -0,0 +1,206 @@ +--- +title: npmTo 容器 +icon: gg:npm +createTime: 2024/10/08 15:54:12 +permalink: /guide/markdown/npm-to/ +--- + +## 概述 + +npmTo 容器用于将 npm 命令行转换为 `pnpm / yarn / deno / bun` 的命令行,并它们作为 代码块组呈现在页面。 + +在 `::: npm-to` 容器中,只需要写 一次 npm 命令 代码块即可。 + +::: details 为什么需要 npmTo 容器 ? +在我编写文档时,常常需要提供 `pnpm / yarn / npm` 等不同运行环境的命令,需要写多个代码块并包装在 `::: code-tabs` +容器中。它占据了我不少的工作量,而且还占据了 markdown 内容中的很长一部分空间,体验并不友好。 +因此我编写了这个 `::: npm-to` 容器以解决这个问题。 +::: + +## 用法 + +````md +::: npm-to +``` sh +npm install -D vuepress vuepress-theme-plume +``` +::: +```` + +将 包含 `npm` 命令行的代码块,包裹在 `::: npm-to` 容器中即可。 + +::: warning npm-to 容器仅支持存在单个代码块,不能包含其他内容 +::: + +上述代码在内部会被转换为: + +````md +::: code-tabs +@tab pnpm +``` sh +pnpm add -D vuepress vuepress-theme-plume +``` +@tab yarn +``` sh +yarn add -D vuepress vuepress-theme-plume +``` +@tab npm +``` sh +npm install -D vuepress vuepress-theme-plume +``` +::: +```` + +最终会在页面中呈现为: + +::: npm-to + +``` sh +npm install -D vuepress vuepress-theme-plume +``` + +::: + +还可以控制 代码块组中的代码块的显示顺序,如下所示: + +**输入:** + +````md +::: npm-to tabs="npm,yarn,pnpm,bun,deno" +``` sh +npm install -D vuepress vuepress-theme-plume +``` +::: +```` + +**输出:** + +::: npm-to tabs="npm,yarn,pnpm,bun,deno" + +``` sh +npm install -D vuepress vuepress-theme-plume +``` + +::: + +## 配置 + +该功能默认不启用,您需要在 `theme` 配置中启用它。 + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + markdownPower: { + // npmTo: true, // 启用,并使用默认配置 + npmTo: { + tabs: ['npm', 'yarn', 'pnpm'], // 代码块组默认显示顺序 + } + }, + } + }) +}) +``` + +`npm-to` 支持将 `npm` 命令行转换为 `pnpm / yarn / deno / bun` 的命令行。可以根据需求进行配置 `tabs` 。 + +## 命令行支持 + +`npmTo` 并不对 `npm` 的所有命令行提供支持,以下是支持的列表: + +- `npm install` / `npm i` +- `npm run` / `npm run-script` +- `npm init` +- `npm create` +- `npm uninstall` / `npm rm` / `npm remove` / `npm un` / `npm unlink` +- `npm ci` +- `npx` + +::: info +对于不支持的命令行,`npmTo` 不会处理它们,仅将它们复制到其他的代码块中。 +::: + +## 示例 + +**输入:** + +````md +::: npm-to +```sh +npm install && npm run docs:dev +``` +::: +```` + +**输出:** + +::: npm-to + +```sh +npm install && npm run docs:dev +``` + +::: + +**输入:** + +````md +::: npm-to +```sh +npm i -D vue +npm i --save-peer vuepress +npm i typescript +``` +::: +```` + +**输出:** +::: npm-to + +```sh +npm i -D vue +npm i --save-peer vuepress +npm i typescript +``` + +::: + +**输入:** + +````md +::: npm-to +```sh +npm run docs:dev -- --clean-cache +``` +::: +```` + +**输出:** + +::: npm-to + +```sh +npm run docs:dev -- --clean-cache +``` + +::: + +**输入:** + +````md +::: npm-to tabs="pnpm,yarn,npm,bun,deno" +```sh +npm ci +``` +::: +```` + +**输出:** + +::: npm-to tabs="pnpm,yarn,npm,bun,deno" + +```sh +npm ci +``` + +::: diff --git a/docs/notes/theme/guide/markdown/图标.md b/docs/notes/theme/guide/markdown/图标.md index 68b1a8e7..3202c6f4 100644 --- a/docs/notes/theme/guide/markdown/图标.md +++ b/docs/notes/theme/guide/markdown/图标.md @@ -15,20 +15,7 @@ permalink: /guide/markdown/iconify/ 为了更好的使用该功能,主题推荐你安装 `@iconify/json` 依赖。主题会自动解析 `@iconify/json` 中的图标数据, 将有使用的图标打包为本地资源,以获得更好的访问体验。 -::: code-tabs -@tab pnpm - -```sh -pnpm add @iconify/json -``` - -@tab yarn - -```sh -yarn add @iconify/json -``` - -@tab npm +::: npm-to ```sh npm install @iconify/json diff --git a/docs/notes/theme/guide/功能/图标.md b/docs/notes/theme/guide/功能/图标.md index da745411..d1ae8048 100644 --- a/docs/notes/theme/guide/功能/图标.md +++ b/docs/notes/theme/guide/功能/图标.md @@ -67,20 +67,7 @@ permalink: /guide/features/icon/ 由于 `@iconify/json` 包比较大,需要手动进行安装: -::: code-tabs -@tab pnpm - -```sh -pnpm add @iconify/json -``` - -@tab yarn - -```sh -yarn add @iconify/json -``` - -@tab npm +::: npm-to ```sh npm install @iconify/json diff --git a/docs/notes/theme/guide/安装与使用.md b/docs/notes/theme/guide/安装与使用.md index 03465f6c..adf6f56d 100644 --- a/docs/notes/theme/guide/安装与使用.md +++ b/docs/notes/theme/guide/安装与使用.md @@ -44,24 +44,10 @@ const vuepressVersion = __VUEPRESS_VERSION__ 主题提供了一个 命令行工具,帮助您构建一个基本项目。您可以通过运行以下命令,启动 安装向导。 -::: code-tabs - -@tab pnpm +::: npm-to ```sh -pnpm create vuepress-theme-plume@latest -``` - -@tab yarn - -```sh -yarn create vuepress-theme-plume@latest -``` - -@tab npm - -```sh -npm init vuepress-theme-plume@latest +npm create vuepress-theme-plume@latest ``` ::: @@ -110,24 +96,9 @@ cd open-source # 进入 D: 分区下的 open-source 目录 - ### 初始化项目 - ::: code-tabs - @tab pnpm + ::: npm-to - ``` sh :no-line-numbers - git init - pnpm init - ``` - - @tab yarn - - ``` sh :no-line-numbers - git init - yarn init - ``` - - @tab npm - - ``` sh :no-line-numbers + ``` sh git init npm init ``` @@ -138,30 +109,11 @@ cd open-source # 进入 D: 分区下的 open-source 目录 安装 `vuepress@next` 和 `vuepress-theme-plume` 作为本地依赖。 - ::: code-tabs - @tab pnpm + ::: npm-to - ```sh :no-line-numbers + ```sh # 安装 vuepress - pnpm add -D vuepress@next vue - # 安装 主题和打包工具 - pnpm add -D vuepress-theme-plume @vuepress/bundler-vite@next - ``` - - @tab yarn - - ``` sh :no-line-numbers - # 安装 vuepress - yarn add -D vuepress@next - # 安装 主题和打包工具 - yarn add -D vuepress-theme-plume @vuepress/bundler-vite@next - ``` - - @tab npm - - ``` sh :no-line-numbers - # 安装 vuepress - npm i -D vuepress@next + npm i -D vuepress@next vue # 安装 主题和打包工具 npm i -D vuepress-theme-plume @vuepress/bundler-vite@next ``` @@ -255,22 +207,9 @@ cd open-source # 进入 D: 分区下的 open-source 目录 - ### 在本地服务器启动你的文档站点 - ::: code-tabs - @tab pnpm + ::: npm-to - ```sh :no-line-numbers - pnpm docs:dev - ``` - - @tab yarn - - ``` sh :no-line-numbers - yarn docs:dev - ``` - - @tab npm - - ``` sh :no-line-numbers + ``` sh npm run docs:dev ``` @@ -286,20 +225,7 @@ cd open-source # 进入 D: 分区下的 open-source 目录 您可以直接在项目中运行以下命令检查是否有可用的更新: -::: code-tabs -@tab pnpm - -```sh -pnpm dlx vp-update -``` - -@tab yarn - -``` sh -yarn dlx vp-update -``` - -@tab npm +::: npm-to ``` sh npx vp-update diff --git a/docs/questions.md b/docs/questions.md index ac116851..205cc53d 100644 --- a/docs/questions.md +++ b/docs/questions.md @@ -34,20 +34,7 @@ draft: true 复制以下命令到你的项目中运行: -::: code-tabs -@tab pnpm - -```sh -pnpm dlx vp-update -``` - -@tab yarn - -```sh -yarn dlx vp-update -``` - -@tab npm +::: npm-to ```sh npx vp-update @@ -58,13 +45,13 @@ npx vp-update ## 为什么更新主题版本后新的功能没有生效? 由于 VuePress 在启动开发服务时,全量编译源目录中的的 `markdown` 文件耗时较长,主题对 `markdown` 的编译进行了 -缓存,以提高启动速度。主题功能并重启开发服务时,由于源目录中的 `markdown` 文件没有变化,跳过了编译直接使用缓存, +缓存,以提高启动速度。主题更新后重启开发服务时,由于源目录中的 `markdown` 文件没有变化,跳过了编译直接使用缓存, 这会导致与 markdown 有关的新功能没有生效。 -**只需要删除缓存文件,并重启即可**。 +**只需要删除缓存文件,并重启即可**: -1. 直接删除 `.vuepress/.cache` 目录。 -2. 在启动开发服务命令后面,添加 `--clean-cache` 参数: +1. 方法一:直接删除 `.vuepress/.cache` 目录。 +2. 方法二:在启动开发服务命令后面,添加 `--clean-cache` 参数: ```sh vuepress dev docs --clean-cache @@ -76,10 +63,10 @@ npx vp-update `plugins.markdownMath` 的配置。它与 [为什么更新主题版本后新的功能没有生效?](#为什么更新主题版本后新的功能没有生效) 的原因相同。因此 -**只需要删除缓存文件,并重启即可**。 +**只需要删除缓存文件,并重启即可**: -1. 直接删除 `.vuepress/.cache` 目录。 -2. 在启动开发服务命令后面,添加 `--clean-cache` 参数: +1. 方法一:直接删除 `.vuepress/.cache` 目录。 +2. 方法二:在启动开发服务命令后面,添加 `--clean-cache` 参数: ```sh vuepress dev docs --clean-cache diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts index f76d98f4..63df661d 100644 --- a/plugins/plugin-md-power/src/node/container/index.ts +++ b/plugins/plugin-md-power/src/node/container/index.ts @@ -6,6 +6,7 @@ import { alignPlugin } from './align.js' import { codeTabs } from './codeTabs.js' import { fileTreePlugin } from './fileTree.js' import { langReplPlugin } from './langRepl.js' +import { npmToPlugins } from './npmTo.js' import { tabs } from './tabs.js' export async function containerPlugin( @@ -20,6 +21,10 @@ export async function containerPlugin( // ::: code-tabs codeTabs(md, options.codeTabs) + if (options.npmTo) { + npmToPlugins(md, typeof options.npmTo === 'boolean' ? {} : options.npmTo) + } + if (options.repl) await langReplPlugin(app, md, options.repl) diff --git a/plugins/plugin-md-power/src/node/container/npmTo.ts b/plugins/plugin-md-power/src/node/container/npmTo.ts new file mode 100644 index 00000000..2f791aeb --- /dev/null +++ b/plugins/plugin-md-power/src/node/container/npmTo.ts @@ -0,0 +1,373 @@ +/** + * 只写一个 npm 代码块,自动转为 代码块分组 [npm, pnpm, yarn, bun, deno] + * + * ::: npm-to + * ``` sh + * npm i -D vuepress-theme-plume + * ``` + * ::: + * ↓ ↓ ↓ ↓ ↓ + * ::: code-tabs + * @tab npm + * ```sh + * npm i -D vuepress-theme-plume + * ``` + * @tab pnpm + * ```sh + * pnpm add -D vuepress-theme-plume + * ``` + * @tab yarn + * ```sh + * yarn add -D vuepress-theme-plume + * ``` + * ::: + */ +import type Token from 'markdown-it/lib/token.mjs' +import type { Markdown } from 'vuepress/markdown' +import type { NpmToOptions, NpmToPackageManager } from '../../shared/index.js' +import { isArray } from '@vuepress/helper' +import container from 'markdown-it-container' +import { resolveAttrs } from '../utils/resolveAttrs.js' + +type PackageCommand = 'install' | 'add' | 'remove' | 'run' | 'create' | 'init' | 'npx' | 'ci' +interface CommandConfigItem { + cli: string + flags?: Record +} +type CommandConfig = Record, CommandConfigItem | false> +type CommandConfigs = Record + +const ALLOW_LIST = ['npm', 'pnpm', 'yarn', 'bun', 'deno'] as const +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'] + +const DEFAULT_TABS: NpmToPackageManager[] = ['npm', 'pnpm', 'yarn'] + +const MANAGERS_CONFIG: CommandConfigs = { + install: { + pattern: /(?:^|\s)npm\s+(?:install|i)$/, + pnpm: { cli: 'pnpm install' }, + yarn: { cli: 'yarn' }, + bun: { cli: 'bun install' }, + deno: { cli: 'deno install' }, + }, + add: { + pattern: /(?:^|\s)npm\s+(?:install|i|add)(?:\s|$)/, + pnpm: { + cli: 'pnpm add', + flags: { + '--no-save': '', // unsupported + '-B': '', // unsupported + '--save-bundle': '', // unsupported + }, + }, + yarn: { + cli: 'yarn add', + flags: { + '--save-dev': '--dev', + '--save-prod': '--prod', + '-P': '', // in npm, `-P` same as `--save-prod`. but in yarn, `-P` same as `--peer` + '--save-peer': '--peer', + '--save-optional': '--optional', + '--no-save': '', // unsupported + '--save-exact': '--exact', + '-B': '', // unsupported + '--save-bundle': '', // unsupported + }, + }, + bun: { + cli: 'bun add', + flags: { + '--save-dev': '--development', + '-P': '', // it's default + '--save-prod': '', // it's default + '--save-peer': '', // unsupported + '-O': '--optional', + '--save-optional': '--optional', + '--no-save': '', // unsupported + '--save-exact': '--exact', + '-B': '', // unsupported + '--save-bundle': '', // unsupported + }, + }, + deno: { + cli: 'deno add', + flags: { + '-g': '', // unsupported + '--global': '', // unsupported + '--save-dev': '--dev', + '-P': '', // unsupported + '--save-prod': '', // unsupported + '--save-peer': '', // unsupported + '-O': '', // unsupported + '--save-optional': '', // unsupported + '--no-save': '', // unsupported + '-E': '', // unsupported + '--save-exact': '', // unsupported + '-B': '', // unsupported + '--save-bundle': '', // unsupported + }, + }, + }, + run: { + pattern: /(?:^|\s)npm\s+(?:run|run-script|rum|urn)(?:\s|$)/, + pnpm: { + cli: 'pnpm', + flags: { + '-w': '-F', // same as `--workspace` + '--workspace': '--filter', // filter by workspaces + '--': '', // scripts flags + }, + }, + yarn: { + cli: 'yarn', + flags: { + '-w': '', // unsupported + '--workspace': '', // unsupported + }, + }, + bun: { + cli: 'bun run', + flags: { + '-w': '--filter', // same as `--workspace` + '--workspace': '--filter', // filter by workspaces + }, + }, + deno: { + cli: 'deno run', + flags: { + '-w': '', // unsupported + '--workspace': '', // unsupported + }, + }, + }, + create: { + pattern: /(?:^|\s)npm\s+create\s/, + pnpm: { cli: 'pnpm create', flags: { '-y': '', '--yes': '' } }, + yarn: { cli: 'yarn create', flags: { '-y': '', '--yes': '' } }, + bun: { cli: 'bun create', flags: { '-y': '', '--yes': '' } }, + deno: { cli: 'deno run -A ', flags: { '-y': '', '--yes': '' } }, + }, + init: { + pattern: /(?:^|\s)npm\s+init/, + pnpm: { cli: 'pnpm init', flags: { '-y': '', '--yes': '' } }, + yarn: { cli: 'yarn init', flags: { '-y': '', '--yes': '' } }, + bun: { cli: 'bun init', flags: { '-y': '', '--yes': '' } }, + deno: { cli: 'deno init', flags: { '-y': '', '--yes': '' } }, + }, + npx: { + pattern: /(?:^|\s)npx\s+/, + pnpm: { cli: 'pnpm dlx' }, + yarn: { cli: 'yarn dlx' }, + bun: { cli: 'bunx' }, + deno: { cli: 'deno run -A' }, + }, + remove: { + pattern: /(?:^|\s)npm\s+(?:uninstall|r|rm|remove|unlink|un)(?:\s|$)/, + pnpm: { + cli: 'pnpm remove', + flags: { '--no-save': '', '--save': '', '-S': '' }, + }, + yarn: { + cli: 'yarn remove', + flags: { '--save-dev': '--dev', '--save': '', '-S': '', '-g': '', '--global': '' }, + }, + bun: { + cli: 'bun remove', + flags: { '--save-dev': '--development', '--save': '', '-S': '', '-g': '', '--global': '' }, + }, + deno: { + cli: 'deno uninstall', + flags: { '--save-dev': '--dev', '--save': '', '-S': '' }, + }, + }, + ci: { + pattern: /(?:^|\s)npm\s+ci$/, + pnpm: { cli: 'pnpm install --frozen-lockfile' }, + yarn: { cli: 'yarn install --immutable' }, + bun: { cli: 'bun install --frozen-lockfile' }, + deno: { cli: 'deno install --frozen' }, + }, +} + +export function npmToPlugins(md: Markdown, options: NpmToOptions): void { + const type = 'npm-to' + const validate = (info: string): boolean => info.trim().startsWith(type) + + const opt = isArray(options) ? { tabs: options } : options + const defaultTabs = opt.tabs?.length ? opt.tabs : DEFAULT_TABS + + const render = (tokens: Token[], idx: number): string => { + const { attrs } = resolveAttrs(tokens[idx].info.slice(type.length - 1)) + const tabs = (attrs.tabs ? attrs.tabs.split(/,\s*/) : defaultTabs) as NpmToPackageManager[] + if (tokens[idx].nesting === 1) { + const token = tokens[idx + 1] + const info = token.info.trim() + if ( + token.type === 'fence' + && (info.startsWith('sh') || info.startsWith('bash') || info.startsWith('shell')) + ) { + const content = token.content + token.hidden = true + token.type = 'text' + token.content = '' + const lines = content.split(/(\n|\s*&&\s*)/) + return md.render(resolveNpmTo(lines, info, idx, tabs), {}) + } + } + return '' + } + + md.use(container, type, { validate, render }) +} + +function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string { + tabs = validateTabs(tabs) + const res: string[] = [] + const map: Record = {} + for (const tab of tabs) { + const newLines: string[] = [] + for (const line of lines) { + const config = findConfig(line) + if (config && config[tab]) { + const parsed = (map[line] ??= parseLine(line)) + const { cli, flags } = config[tab] as CommandConfigItem + if (parsed) { + let newLine = `${parsed.env ? `${parsed.env} ` : ''}${cli}` + if (parsed.args && flags) { + let args = parsed.args + for (const [key, value] of Object.entries(flags)) { + args = args.replaceAll(key, value) + } + newLine += ` ${args.replace(/\s+-/g, ' -').trim()}` + } + + if (parsed.cmd) + newLine += ` ${parsed.cmd}` + + if (parsed.scriptArgs) + newLine += ` ${parsed.scriptArgs}` + newLines.push(newLine.trim()) + } + } + else { + newLines.push(line) + } + } + res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\n\`\`\``) + } + return `:::code-tabs#npm-to-${idx}\n${res.join('\n')}\n:::` +} + +function findConfig(line: string): CommandConfig | undefined { + for (const { pattern, ...config } of Object.values(MANAGERS_CONFIG)) { + if (pattern.test(line)) { + return config + } + } + return undefined +} + +function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] { + if (tabs.length === 0) { + return DEFAULT_TABS + } + return tabs.filter(tab => ALLOW_LIST.includes(tab)) +} + +interface LineParsed { + env: string + cli: string + cmd: string + args?: string + scriptArgs?: string +} + +const LINE_REG = /(.*)(npm|npx)\s+(.*)/ +export function parseLine(line: string): false | LineParsed { + const match = line.match(LINE_REG) + if (!match) + return false + + const [, env, cli, rest] = match + if (cli === 'npx') + return { env, cli, cmd: '', scriptArgs: rest?.trim() } + + const idx = rest.indexOf(' ') + if (idx === -1) + return { env, cli: `${cli} ${rest.trim()}`, cmd: '' } + + return { env, cli: `${cli} ${rest.slice(0, idx)}`, ...parseArgs(rest.slice(idx + 1)) } +} + +function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } { + line = line?.trim() + if (!line) + return { cmd: '' } + + const [npmArgs, scriptArgs] = line.split(/\s+--\s+/) + 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) + args = npmArgs.slice(idx + 1) + } + else { + const idx = npmArgs.indexOf(' ') + if (idx === -1) { + cmd = npmArgs + } + else { + cmd = npmArgs.slice(0, idx) + args = npmArgs.slice(idx + 1) + } + } + } + else { + let newLine = '' + let value = '' + let isQuote = false + let isBool = false + let isNextValue = false + let quote = '' + for (let i = 0; i < npmArgs.length; i++) { + const v = npmArgs[i] + if (!isQuote && (v === '"' || v === '\'')) { + quote = v + isQuote = true + value += v + } + else if (isQuote && v === quote) { + isQuote = false + value += v + } + else if ((v === ' ' || v === '=' || i === npmArgs.length - 1) && !isQuote && value) { + if (i === npmArgs.length - 1) { + value += v + } + const isKey = value[0] === '-' + if (isKey) { + isBool = BOOL_FLAGS.includes(value) + isNextValue = !isBool + } + if (!isKey && !isNextValue) { + cmd += `${value} ` + } + else { + newLine += `${value}${v || ''}` + if (!isKey && isNextValue) { + isNextValue = false + } + } + value = '' + } + else { + value += v + } + } + args = newLine + } + + return { cmd, args: args.trim(), scriptArgs } +} diff --git a/plugins/plugin-md-power/src/shared/index.ts b/plugins/plugin-md-power/src/shared/index.ts index beb2af52..2ee48eb7 100644 --- a/plugins/plugin-md-power/src/shared/index.ts +++ b/plugins/plugin-md-power/src/shared/index.ts @@ -5,6 +5,7 @@ export * from './codeTabs.js' export * from './fileTree.js' export * from './icons.js' export * from './jsfiddle.js' +export * from './npmTo.js' export * from './pdf.js' export * from './plot.js' export * from './plugin.js' diff --git a/plugins/plugin-md-power/src/shared/npmTo.ts b/plugins/plugin-md-power/src/shared/npmTo.ts new file mode 100644 index 00000000..b07a90ed --- /dev/null +++ b/plugins/plugin-md-power/src/shared/npmTo.ts @@ -0,0 +1,5 @@ +export type NpmToPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' + +export type NpmToOptions = NpmToPackageManager[] | { + tabs?: NpmToPackageManager[] +} diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts index 3fcf28e9..6168af82 100644 --- a/plugins/plugin-md-power/src/shared/plugin.ts +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -2,6 +2,7 @@ import type { CanIUseOptions } from './caniuse.js' import type { CodeTabsOptions } from './codeTabs.js' import type { FileTreeOptions } from './fileTree.js' import type { IconsOptions } from './icons.js' +import type { NpmToOptions } from './npmTo.js' import type { PDFOptions } from './pdf.js' import type { PlotOptions } from './plot.js' import type { ReplOptions } from './repl.js' @@ -11,6 +12,12 @@ export interface MarkdownPowerPluginOptions { * 配置代码块分组 */ codeTabs?: CodeTabsOptions + + /** + * 是否启用 npm-to 容器 + */ + npmTo?: boolean | NpmToOptions + /** * 是否启用 PDF 嵌入语法 *