diff --git a/cli/templates/.vuepress/config.ts.handlebars b/cli/templates/.vuepress/config.ts.handlebars index f041388f..10dc23de 100644 --- a/cli/templates/.vuepress/config.ts.handlebars +++ b/cli/templates/.vuepress/config.ts.handlebars @@ -140,6 +140,7 @@ export default defineUserConfig({ // go: true, // ::: go-repl // rust: true, // ::: rust-repl // kotlin: true, // ::: kotlin-repl + // python: true, // ::: python-repl // }, // math: { // 启用数学公式 // type: 'katex', diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts index 3964e3b9..a4c49708 100644 --- a/docs/.vuepress/notes/zh/theme-guide.ts +++ b/docs/.vuepress/notes/zh/theme-guide.ts @@ -80,6 +80,7 @@ export const themeGuide: ThemeNote = defineNoteConfig({ 'rust', 'golang', 'kotlin', + 'python', 'codepen', 'jsFiddle', 'codeSandbox', diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 38a893ce..9650315f 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -50,6 +50,7 @@ export const theme: Theme = plumeTheme({ go: true, rust: true, kotlin: true, + python: true, }, }, diff --git a/docs/notes/theme/config/plugins/markdown-power.md b/docs/notes/theme/config/plugins/markdown-power.md index 3f674c10..614da741 100644 --- a/docs/notes/theme/config/plugins/markdown-power.md +++ b/docs/notes/theme/config/plugins/markdown-power.md @@ -190,6 +190,12 @@ __语法:__ // kotlin code ``` ::: + +::: python-repl +```python +# python code +``` +::: ```` 请查看完整使用文档: @@ -197,6 +203,7 @@ __语法:__ - [代码演示 > Rust](../../guide/repl/rust.md) - [代码演示 > Golang](../../guide/repl/golang.md) - [代码演示 > Kotlin](../../guide/repl/kotlin.md) +- [代码演示 > Python](../../guide/repl/python.md) ### Plot 隐秘文本 diff --git a/docs/notes/theme/guide/repl/python.md b/docs/notes/theme/guide/repl/python.md new file mode 100644 index 00000000..d368e585 --- /dev/null +++ b/docs/notes/theme/guide/repl/python.md @@ -0,0 +1,153 @@ +--- +title: Python +icon: devicon-plain:python +createTime: 2025/05/03 21:53:58 +permalink: /guide/repl/python/ +--- + +## 概述 + +主题提供了 Python 代码演示,支持在线运行 Python 代码。 + +## 安装 + +python 在线执行由 [pyodide](https://pyodide.org/en/latest/) 提供,使用前请确保有 `pyodide` 可用 + +::: npm-to + +```sh +npm install pyodide +``` + +::: + +## 配置 + +该功能默认不启用,你可以通过配置来启用它。 + +```ts title=".vuepress/config.ts" +export default defineUserConfig({ + theme: plumeTheme({ + markdown: { + repl: { + python: true, + }, + }, + }) +}) +``` + +## 使用 + +使用 `::: python-repl` 容器语法 将 python 代码块包裹起来。主题会检查代码块并添加执行按钮。 + +::: warning python-repl 的支持是有限的,目前只支持: + +- 基本 Python 语法的执行(不依赖后端) +- 可导入 Python 基本库 +- 标准输出流(stdout)捕获输出 +- 最后一条语句是一个表达式(且代码不以分号结尾),则会返回该表达式的值。 +- 异常信息输出 + +::: + +### 只读代码演示 + +Python 代码演示默认是只读的,不可编辑。 + +````md +::: python-repl title="自定义标题" +```python +// your python code +``` +::: +```` + +### 可编辑代码演示 + +如果需要在线编辑并执行,需要将代码块包裹在 `::: python-repl editable` 容器语法中。 + +````md +::: python-repl editable title="自定义标题" +```python +// your python code +``` +::: +```` + +## 示例 + +### 打印内容 + +**输入:** + +````md +::: python-repl +```python +def hello_world(): + return 'Hello World!' + +hello_world() +``` +::: +```` + +**输出:** + +::: python-repl + +```python +def hello_world(): + print('Hello World!') + +hello_world() +``` + +::: + +### 运算 + +::: python-repl + +```python +def mul(a: int, b: int) -> int: + return a * b + +print(mul(-2, 4)) +``` + +::: + +### 可编辑代码演示 + +**输入:** + +````md +::: python-repl editable +```python +class Contact: + def __init__(self, id: int, email: str): + self.id = id + self.email = email + +contact = Contact(1, 'mary@gmail.com') +print(contact.id) +``` +::: +```` + +**输出:** + +::: python-repl editable + +```python +class Contact: + def __init__(self, id: int, email: str): + self.id = id + self.email = email + +contact = Contact(1, 'mary@gmail.com') +print(contact.id) +``` + +::: diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json index 0c19a2a7..d71b3f0b 100644 --- a/plugins/plugin-md-power/package.json +++ b/plugins/plugin-md-power/package.json @@ -47,6 +47,7 @@ "less": "catalog:dev", "markdown-it": "catalog:dev", "mpegts.js": "^1.7.3", + "pyodide": "catalog:peer", "sass": "catalog:peer", "sass-embedded": "catalog:peer", "stylus": "catalog:dev", @@ -67,6 +68,9 @@ }, "mpegts.js": { "optional": true + }, + "pyodide": { + "optional": true } }, "dependencies": { diff --git a/plugins/plugin-md-power/src/client/components/CodeEditor.vue b/plugins/plugin-md-power/src/client/components/CodeEditor.vue index ac250db7..f7afabe8 100644 --- a/plugins/plugin-md-power/src/client/components/CodeEditor.vue +++ b/plugins/plugin-md-power/src/client/components/CodeEditor.vue @@ -11,7 +11,7 @@ let container: HTMLPreElement | null = null let lineNumbers: HTMLDivElement | null = null const { grammars, theme } = editorData -const lang = ref<'go' | 'rust' | 'kotlin'>() +const lang = ref<'go' | 'rust' | 'kotlin' | 'python'>() const editorEl = shallowRef() const textAreaEl = shallowRef() diff --git a/plugins/plugin-md-power/src/client/composables/codeRepl.ts b/plugins/plugin-md-power/src/client/composables/codeRepl.ts index 6342aec3..070f037c 100644 --- a/plugins/plugin-md-power/src/client/composables/codeRepl.ts +++ b/plugins/plugin-md-power/src/client/composables/codeRepl.ts @@ -1,3 +1,4 @@ +import type { PyodideInterface } from 'pyodide' import type { Ref } from 'vue' import { onMounted, ref } from 'vue' import { http } from '../utils/http.js' @@ -10,8 +11,9 @@ const api = { go: 'https://api.pengzhanbo.cn/repl/golang/run', kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run', } +let pyodide: PyodideInterface | null = null -type Lang = 'kotlin' | 'go' | 'rust' +type Lang = 'kotlin' | 'go' | 'rust' | 'python' type ExecuteFn = (code: string) => Promise type ExecuteMap = Record @@ -21,9 +23,11 @@ const langAlias: Record = { go: 'go', rust: 'rust', rs: 'rust', + py: 'python', + python: 'python', } -const supportLang: Lang[] = ['kotlin', 'go', 'rust'] +const supportLang: Lang[] = ['kotlin', 'go', 'rust', 'python'] function resolveLang(lang?: string) { return lang ? langAlias[lang] || lang : '' @@ -88,6 +92,7 @@ export function useCodeRepl(el: Ref): UseCodeReplResult { kotlin: executeKotlin, go: executeGolang, rust: executeRust, + python: executePython, } function onCleanRun(): void { @@ -190,6 +195,24 @@ export function useCodeRepl(el: Ref): UseCodeReplResult { }) } + async function executePython(code: string) { + loaded.value = false + finished.value = false + if (pyodide === null) { + const { loadPyodide, version } = await import(/* webpackChunkName: "pyodide" */ 'pyodide') + pyodide = await loadPyodide({ indexURL: `https://cdn.jsdelivr.net/pyodide/v${version}/full/` }) + } + pyodide.setStdout({ batched: msg => stdout.value.push(msg) }) + try { + stdout.value.push(pyodide.runPython(code)) + } + catch (e: unknown) { + stderr.value.push(String(e as Error)) + } + loaded.value = true + finished.value = true + } + return { onRunCode, onCleanRun, diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts index 6e70c5c0..51b175d1 100644 --- a/plugins/plugin-md-power/src/node/container/index.ts +++ b/plugins/plugin-md-power/src/node/container/index.ts @@ -44,7 +44,7 @@ export async function containerPlugin( } if (options.repl) - // ::: rust-repl / go-repl / kotlin-repl + // ::: rust-repl / go-repl / kotlin-repl / python-repl await langReplPlugin(app, md, options.repl) if (options.fileTree) { diff --git a/plugins/plugin-md-power/src/node/container/langRepl.ts b/plugins/plugin-md-power/src/node/container/langRepl.ts index c77b83e5..cd59e5f9 100644 --- a/plugins/plugin-md-power/src/node/container/langRepl.ts +++ b/plugins/plugin-md-power/src/node/container/langRepl.ts @@ -18,6 +18,7 @@ export async function langReplPlugin(app: App, md: markdownIt, { go = false, kotlin = false, rust = false, + python = false, }: ReplOptions): Promise { const container = (lang: string): void => createContainerPlugin(md, `${lang}-repl`, { before(info) { @@ -37,6 +38,9 @@ export async function langReplPlugin(app: App, md: markdownIt, { if (rust) container('rust') + if (python) + container('python') + theme ??= { light: 'github-light', dark: 'github-dark' } const data: ReplEditorData = { grammars: {} } as ReplEditorData @@ -66,6 +70,9 @@ export async function langReplPlugin(app: App, md: markdownIt, { if (rust) data.grammars.rust = await readGrammar('rust') + + if (python) + data.grammars.python = await readGrammar('python') } catch { /* istanbul ignore next -- @preserve */ diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts index 546d5c1f..6c4e97c3 100644 --- a/plugins/plugin-md-power/src/node/plugin.ts +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -30,6 +30,9 @@ export function markdownPowerPlugin( app, ['shiki/core', 'shiki/wasm', 'shiki/engine/oniguruma'], ) + + if (options.repl.python) + addViteOptimizeDepsInclude(bundlerOptions, app, ['pyodide']) } if (options.artPlayer) { addViteOptimizeDepsInclude( diff --git a/plugins/plugin-md-power/src/shared/repl.ts b/plugins/plugin-md-power/src/shared/repl.ts index cd9c8805..276c6042 100644 --- a/plugins/plugin-md-power/src/shared/repl.ts +++ b/plugins/plugin-md-power/src/shared/repl.ts @@ -13,6 +13,7 @@ export interface ReplOptions { go?: boolean kotlin?: boolean rust?: boolean + python?: boolean } export interface ReplEditorData { @@ -20,6 +21,7 @@ export interface ReplEditorData { go?: any kotlin?: any rust?: any + python?: any } theme: ThemeRegistration | { light: ThemeRegistration diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a082b80..b407510c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,9 @@ catalogs: mpegts.js: specifier: 1.7.3 version: 1.7.3 + pyodide: + specifier: ^0.27.6 + version: 0.27.6 sass-loader: specifier: ^16.0.5 version: 16.0.5 @@ -647,6 +650,9 @@ importers: nanoid: specifier: catalog:prod version: 5.1.5 + pyodide: + specifier: catalog:peer + version: 0.27.6 sass: specifier: ^1.89.0 version: 1.89.0 @@ -5787,6 +5793,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pyodide@0.27.6: + resolution: {integrity: sha512-ahiSHHs6iFKl2f8aO1wALINAlMNDLAtb44xCI87GQyH2tLDk8F8VWip3u1ZNIyglGSCYAOSFzWKwS1f9gBFVdg==} + engines: {node: '>=18.0.0'} + qs@6.13.1: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} @@ -6979,6 +6989,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -12569,6 +12591,13 @@ snapshots: punycode@2.3.1: {} + pyodide@0.27.6: + dependencies: + ws: 8.18.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + qs@6.13.1: dependencies: side-channel: 1.1.0 @@ -13798,6 +13827,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@8.18.2: {} + xml-name-validator@4.0.0: {} xmldom-sre@0.1.31: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1e208741..ba7716c0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -61,6 +61,7 @@ catalogs: hls.js: ^1.6.2 mathjax-full: ^3.2.2 mpegts.js: 1.7.3 + pyodide: ^0.27.6 sass: ^1.89.0 sass-embedded: ^1.89.0 sass-loader: ^16.0.5 diff --git a/theme/src/node/detector/dependency.ts b/theme/src/node/detector/dependency.ts index 2fbe98a9..c08c0e76 100644 --- a/theme/src/node/detector/dependency.ts +++ b/theme/src/node/detector/dependency.ts @@ -7,6 +7,7 @@ import { createTranslate, logger } from '../utils/index.js' const DEPENDENCIES: Record = { twoslash: ['@vuepress/shiki-twoslash'], + pythonRepl: ['pyodide'], chartjs: ['chart.js'], echarts: ['echarts'], @@ -49,6 +50,9 @@ export function detectDependencies(options: ThemeOptions, plugins: ThemeBuiltinP if (options.codeHighlighter && options.codeHighlighter.twoslash) add('twoslash') + if (markdown.repl && markdown.repl.python) + add('pythonRepl') + ;['chartjs', 'echarts', 'markmap', 'mermaid', 'flowchart'].forEach((dep) => { if (markdown[dep] || mdEnhance[dep]) add(dep)