feat(plugin-md-power): enable python code repl(#585) (#613)

* feat(plugin-md-power): enable python code repl(#585)

* fix(plugin-md-power): correct grammars

* fix(plugin-md-power): modify pnpm-lock.yaml

* feat: tweak

* chore: tweak

---------

Co-authored-by: Shuo Liu <sliu84@outlook.com>
Co-authored-by: pengzhanbo <volodymyr@foxmail.com>
This commit is contained in:
Shuo Liu 2025-06-06 21:14:59 +08:00 committed by GitHub
parent c21c9bdefa
commit 1f89d7f515
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 242 additions and 4 deletions

View File

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

View File

@ -80,6 +80,7 @@ export const themeGuide: ThemeNote = defineNoteConfig({
'rust',
'golang',
'kotlin',
'python',
'codepen',
'jsFiddle',
'codeSandbox',

View File

@ -50,6 +50,7 @@ export const theme: Theme = plumeTheme({
go: true,
rust: true,
kotlin: true,
python: true,
},
},

View File

@ -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 隐秘文本

View File

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

View File

@ -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": {

View File

@ -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<HTMLDivElement>()
const textAreaEl = shallowRef<HTMLTextAreaElement>()

View File

@ -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<any>
type ExecuteMap = Record<Lang, ExecuteFn>
@ -21,9 +23,11 @@ const langAlias: Record<string, string> = {
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<HTMLDivElement | null>): UseCodeReplResult {
kotlin: executeKotlin,
go: executeGolang,
rust: executeRust,
python: executePython,
}
function onCleanRun(): void {
@ -190,6 +195,24 @@ export function useCodeRepl(el: Ref<HTMLDivElement | null>): 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,

View File

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

View File

@ -18,6 +18,7 @@ export async function langReplPlugin(app: App, md: markdownIt, {
go = false,
kotlin = false,
rust = false,
python = false,
}: ReplOptions): Promise<void> {
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 */

View File

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

View File

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

31
pnpm-lock.yaml generated
View File

@ -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: {}

View File

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

View File

@ -7,6 +7,7 @@ import { createTranslate, logger } from '../utils/index.js'
const DEPENDENCIES: Record<string, string[]> = {
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)