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
This commit is contained in:
pengzhanbo 2024-10-08 18:57:59 +08:00 committed by GitHub
parent 08eeac7cb8
commit b721bf08f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 619 additions and 133 deletions

View File

@ -40,6 +40,7 @@ export const themeGuide = defineNoteConfig({
'选项组',
'隐秘文本',
'示例容器',
'npm-to',
'caniuse',
'导入文件',
],

View File

@ -30,6 +30,7 @@ export const theme: Theme = plumeTheme({
replit: true,
codeSandbox: true,
jsfiddle: true,
npmTo: ['pnpm', 'yarn', 'npm'],
repl: {
go: true,
rust: true,

View File

@ -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 <!-- [!code hl] -->
``` sh
npm install -D vuepress vuepress-theme-plume
```
::: <!-- [!code hl] -->
````
将 包含 `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" <!-- [!code hl] -->
``` sh
npm install -D vuepress vuepress-theme-plume
```
::: <!-- [!code hl] -->
````
**输出:**
::: 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
```
:::

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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<string, string>
}
type CommandConfig = Record<Exclude<NpmToPackageManager, 'npm'>, CommandConfigItem | false>
type CommandConfigs = Record<PackageCommand, { pattern: RegExp } & CommandConfig>
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<string, LineParsed | false> = {}
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 }
}

View File

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

View File

@ -0,0 +1,5 @@
export type NpmToPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'
export type NpmToOptions = NpmToPackageManager[] | {
tabs?: NpmToPackageManager[]
}

View File

@ -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
*