mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(plugin-md-power): 支持可编辑的代码演示
This commit is contained in:
parent
2127ca44a8
commit
5addb31e91
@ -83,7 +83,11 @@ export const theme: Theme = themePlume({
|
||||
replit: true,
|
||||
codeSandbox: true,
|
||||
jsfiddle: true,
|
||||
repl: true,
|
||||
repl: {
|
||||
go: true,
|
||||
rust: true,
|
||||
kotlin: true,
|
||||
},
|
||||
},
|
||||
comment: {
|
||||
provider: 'Giscus',
|
||||
|
||||
@ -11,7 +11,7 @@ permalink: /guide/repl/golang/
|
||||
主题提供了 Golang 代码演示,支持 在线运行 Go 代码。
|
||||
|
||||
::: important
|
||||
该功能通过将 代码提交到 服务器 进行 编译并执行,且一次只能提交单个代码文件。
|
||||
该功能通过将 代码提交到 服务器 进行 编译并执行。
|
||||
|
||||
因此,请不要使用此功能 执行 过于复杂的代码,也不要过于频繁的进行执行请求。
|
||||
:::
|
||||
@ -28,7 +28,9 @@ export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
plugins: {
|
||||
markdownPower: {
|
||||
repl: true,
|
||||
repl: {
|
||||
go: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -41,10 +43,26 @@ export default defineUserConfig({
|
||||
|
||||
使用 `::: go-repl` 容器语法 将 Go 代码块包裹起来。主题会检查代码块并添加执行按钮。
|
||||
|
||||
### 只读代码演示
|
||||
|
||||
golang 代码演示默认是只读的,不可编辑。
|
||||
|
||||
````md
|
||||
::: go-repl
|
||||
::: go-repl 自定义标题
|
||||
```go
|
||||
// your rust code
|
||||
// your go code
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
### 可编辑代码演示
|
||||
|
||||
如果需要在线编辑并执行,需要将代码块包裹在 `::: go-repl#editable` 容器语法中
|
||||
|
||||
````md
|
||||
::: go-repl#editable 自定义标题
|
||||
```go
|
||||
// your go code
|
||||
```
|
||||
:::
|
||||
````
|
||||
@ -88,6 +106,44 @@ func main() {
|
||||
|
||||
:::
|
||||
|
||||
### 可编辑代码演示
|
||||
|
||||
**输入:**
|
||||
|
||||
````md
|
||||
:::go-repl#editable
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello World")
|
||||
}
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
:::go-repl#editable
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello World")
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 循环随机延迟打印
|
||||
|
||||
**输入:**
|
||||
@ -186,3 +242,32 @@ func main() {
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 多文件
|
||||
|
||||
::: go-repl
|
||||
|
||||
```go{10-12}
|
||||
package main
|
||||
|
||||
import (
|
||||
"play.ground/foo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
foo.Bar()
|
||||
}
|
||||
-- go.mod --
|
||||
module play.ground
|
||||
-- foo/foo.go --
|
||||
package foo
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Bar() {
|
||||
fmt.Println("This function lives in an another file!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@ -28,7 +28,9 @@ export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
plugins: {
|
||||
markdownPower: {
|
||||
repl: true,
|
||||
repl: {
|
||||
kotlin: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -39,10 +41,26 @@ export default defineUserConfig({
|
||||
|
||||
## 使用
|
||||
|
||||
使用 `::: kotlin-repl` 容器语法 将 Rust 代码块包裹起来。主题会检查代码块并添加执行按钮。
|
||||
使用 `::: kotlin-repl` 容器语法 将 kotlin 代码块包裹起来。主题会检查代码块并添加执行按钮。
|
||||
|
||||
### 只读代码演示
|
||||
|
||||
kotlin 代码演示默认是只读的,不可编辑。
|
||||
|
||||
````md
|
||||
::: kotlin-repl
|
||||
::: kotlin-repl 自定义标题
|
||||
```kotlin
|
||||
// your kotlin code
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
### 可编辑代码演示
|
||||
|
||||
如果需要在线编辑并执行,需要将代码块包裹在 `::: kotlin-repl#editable` 容器语法中
|
||||
|
||||
````md
|
||||
::: kotlin-repl#editable 自定义标题
|
||||
```kotlin
|
||||
// your kotlin code
|
||||
```
|
||||
@ -98,3 +116,35 @@ fun main(args: Array<String>) {
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 可编辑代码演示
|
||||
|
||||
**输入:**
|
||||
|
||||
````md
|
||||
::: kotlin-repl#editable
|
||||
```kotlin
|
||||
class Contact(val id: Int, var email: String)
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val contact = Contact(1, "mary@gmail.com")
|
||||
println(contact.id)
|
||||
}
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
::: kotlin-repl#editable
|
||||
|
||||
```kotlin
|
||||
class Contact(val id: Int, var email: String)
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val contact = Contact(1, "mary@gmail.com")
|
||||
println(contact.id)
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@ -28,7 +28,9 @@ export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
plugins: {
|
||||
markdownPower: {
|
||||
repl: true,
|
||||
repl: {
|
||||
rust: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -41,8 +43,24 @@ export default defineUserConfig({
|
||||
|
||||
使用 `::: rust-repl` 容器语法 将 Rust 代码块包裹起来。主题会检查代码块并添加执行按钮。
|
||||
|
||||
### 只读代码演示
|
||||
|
||||
rust 代码演示默认是只读的,不可编辑。
|
||||
|
||||
````md
|
||||
::: rust-repl
|
||||
::: rust-repl 自定义标题
|
||||
```rust
|
||||
// your rust code
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
### 可编辑代码演示
|
||||
|
||||
如果需要在线编辑并执行,需要将代码块包裹在 `::: rust-repl#editable` 容器语法中
|
||||
|
||||
````md
|
||||
::: rust-repl#editable 自定义标题
|
||||
```rust
|
||||
// your rust code
|
||||
```
|
||||
@ -56,7 +74,7 @@ export default defineUserConfig({
|
||||
**输入:**
|
||||
|
||||
````md
|
||||
::: rust-repl
|
||||
::: rust-repl 打印内容
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
@ -67,7 +85,7 @@ fn main() {
|
||||
|
||||
**输出:**
|
||||
|
||||
::: rust-repl
|
||||
::: rust-repl 打印内容
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
@ -107,6 +125,25 @@ fn main() {
|
||||
|
||||
### 等待子进程执行
|
||||
|
||||
**输入:**
|
||||
|
||||
````md
|
||||
::: rust-repl
|
||||
```rust
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let mut child = Command::new("sleep").arg("5").spawn().unwrap();
|
||||
let _result = child.wait().unwrap();
|
||||
|
||||
println!("reached end of main");
|
||||
}
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
::: rust-repl
|
||||
|
||||
```rust
|
||||
@ -121,3 +158,29 @@ fn main() {
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 可编辑的演示
|
||||
|
||||
**输入:**
|
||||
|
||||
````md
|
||||
::: rust-repl#editable
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
::: rust-repl#editable
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
143
plugins/plugin-md-power/src/client/components/CodeEditor.vue
Normal file
143
plugins/plugin-md-power/src/client/components/CodeEditor.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { getHighlighterCore } from 'shiki/core'
|
||||
import type { HighlighterCore } from 'shiki/core'
|
||||
import editorData from '@internal/md-power/replEditorData'
|
||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { resolveCodeInfo } from '../composables/codeRepl.js'
|
||||
|
||||
let highlighter: HighlighterCore | null = null
|
||||
let container: HTMLPreElement | null = null
|
||||
let lineNumbers: HTMLDivElement | null = null
|
||||
const { grammars, theme } = editorData
|
||||
|
||||
const lang = ref<'go' | 'rust' | 'kotlin'>()
|
||||
|
||||
const editorEl = shallowRef<HTMLDivElement>()
|
||||
const textAreaEl = shallowRef<HTMLTextAreaElement>()
|
||||
const input = ref('')
|
||||
|
||||
async function init() {
|
||||
highlighter = await getHighlighterCore({
|
||||
themes: 'light' in theme && 'dark' in theme ? [theme.light, theme.dark] : [theme],
|
||||
langs: Object.keys(grammars).map(key => grammars[key]),
|
||||
loadWasm: () => import('shiki/wasm'),
|
||||
})
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
if (highlighter && lang.value && input.value) {
|
||||
const output = highlighter.codeToHtml(input.value, {
|
||||
lang: lang.value,
|
||||
...('light' in theme && 'dark' in theme
|
||||
? { themes: theme, defaultColor: false }
|
||||
: { theme }),
|
||||
})
|
||||
if (container) {
|
||||
container.innerHTML = output
|
||||
.replace(/^<pre[^]+?>/, '')
|
||||
.replace(/<\/pre>$/, '')
|
||||
.replace(/(<span class="line">)(<\/span>)/g, '$1<wbr>$2')
|
||||
}
|
||||
if (lineNumbers) {
|
||||
lineNumbers.innerHTML = output
|
||||
.split('\n')
|
||||
.map(() => '<div class="line-number"></div>')
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateScroll() {
|
||||
container && (container.scrollLeft = textAreaEl.value?.scrollLeft || 0)
|
||||
}
|
||||
|
||||
watch([input], highlight, { flush: 'post' })
|
||||
|
||||
onMounted(async () => {
|
||||
if (!editorEl.value || !textAreaEl.value)
|
||||
return
|
||||
await init()
|
||||
container = editorEl.value.querySelector('pre')
|
||||
lineNumbers = editorEl.value.querySelector('.line-numbers')
|
||||
const info = resolveCodeInfo(editorEl.value)
|
||||
lang.value = info.lang
|
||||
input.value = info.code
|
||||
textAreaEl.value.addEventListener('scroll', updateScroll, { passive: false })
|
||||
window.addEventListener('resize', updateScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
textAreaEl.value?.removeEventListener('scroll', updateScroll)
|
||||
window.removeEventListener('resize', updateScroll)
|
||||
highlighter = null
|
||||
container = null
|
||||
lineNumbers = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="editorEl" class="code-repl-editor">
|
||||
<slot />
|
||||
<textarea ref="textAreaEl" v-model="input" class="code-repl-input" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.code-repl-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-repl-editor :deep(div[class*="language-"] pre) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.code-repl-editor:hover :deep(.copy-code-button) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-repl-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1.5rem;
|
||||
bottom: 0;
|
||||
left: -1.5rem;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
padding: 1.3rem 1.5rem;
|
||||
overflow-x: auto;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: var(--vp-code-font-size);
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none;
|
||||
color: transparent;
|
||||
text-align: left;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
white-space: pre;
|
||||
caret-color: gray;
|
||||
resize: none;
|
||||
background-color: transparent;
|
||||
word-spacing: normal;
|
||||
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: auto;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.code-repl-input {
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(div[class*="language-"].line-numbers-mode) + .code-repl-input {
|
||||
padding-left: 1rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue'
|
||||
import { defineAsyncComponent, shallowRef } from 'vue'
|
||||
import { useCodeRepl } from '../composables/codeRepl.js'
|
||||
import IconRun from './IconRun.vue'
|
||||
import Loading from './Loading.vue'
|
||||
import IconConsole from './IconConsole.vue'
|
||||
import IconClose from './IconClose.vue'
|
||||
|
||||
defineProps<{
|
||||
editable?: boolean
|
||||
title?: string
|
||||
}>()
|
||||
|
||||
const Editor = defineAsyncComponent(() => import('./CodeEditor.vue'))
|
||||
|
||||
const replEl = shallowRef<HTMLDivElement | null>(null)
|
||||
const outputEl = shallowRef<HTMLDivElement | null>(null)
|
||||
const {
|
||||
@ -31,10 +38,16 @@ function runCode() {
|
||||
|
||||
<template>
|
||||
<div ref="replEl" class="code-repl">
|
||||
<span v-show="loaded && finished" class="icon-run" title="Run Code" @click="runCode">
|
||||
<IconRun />
|
||||
</span>
|
||||
<slot />
|
||||
<div class="code-repl-title">
|
||||
<h4>{{ title }}</h4>
|
||||
<span v-show="loaded && finished" class="icon-run" title="Run Code" @click="runCode">
|
||||
<IconRun />
|
||||
</span>
|
||||
</div>
|
||||
<Editor v-if="editable">
|
||||
<slot />
|
||||
</Editor>
|
||||
<slot v-else />
|
||||
<div ref="outputEl" class="code-repl-pin" />
|
||||
<div v-if="!firstRun" class="code-repl-output">
|
||||
<div class="output-head">
|
||||
@ -80,16 +93,43 @@ function runCode() {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.code-repl :deep(div[class*="language-"]) {
|
||||
margin: 0 -1.5rem;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.code-repl-output {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
padding-top: 6px;
|
||||
margin: 0 -1.5rem;
|
||||
background-color: var(--vp-code-block-bg);
|
||||
transition: background-color, var(--t-color);
|
||||
transition: background-color var(--t-color);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.code-repl-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
margin: 0 -1.5rem;
|
||||
background-color: var(--vp-code-block-bg);
|
||||
border-bottom: solid 1px var(--vp-c-divider);
|
||||
transition: var(--t-color);
|
||||
transition-property: background, border;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.code-repl-title {
|
||||
margin: 0;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.code-repl :deep(div[class*="language-"]) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-repl-output {
|
||||
margin: 0;
|
||||
border-bottom-right-radius: 6px;
|
||||
@ -97,34 +137,36 @@ function runCode() {
|
||||
}
|
||||
}
|
||||
|
||||
.code-repl-title h4 {
|
||||
flex: 1;
|
||||
padding: 0 12px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 48px;
|
||||
color: var(--vp-code-tab-active-text-color);
|
||||
white-space: nowrap;
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.icon-run {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-bg);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
cursor: pointer;
|
||||
background-color: var(--vp-c-brand-1);
|
||||
border: solid 1px var(--vp-c-text-3);
|
||||
border-radius: 100%;
|
||||
transition: var(--t-color);
|
||||
transition-property: color, background-color;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.icon-run {
|
||||
top: 60px;
|
||||
right: 16px;
|
||||
}
|
||||
transition-property: color, border;
|
||||
}
|
||||
|
||||
.icon-run:hover {
|
||||
background-color: var(--vp-c-brand-2);
|
||||
color: var(--vp-c-text-2);
|
||||
border-color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.code-repl-output .output-head {
|
||||
@ -132,7 +174,7 @@ function runCode() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px 4px 20px;
|
||||
border-top: solid 2px var(--vp-c-border);
|
||||
border-top: solid 2px var(--vp-c-divider);
|
||||
transition: border-color var(--t-color);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type Ref, ref } from 'vue'
|
||||
import { type Ref, onMounted, ref } from 'vue'
|
||||
import { http } from '../utils/http.js'
|
||||
import { sleep } from '../utils/sleep.js'
|
||||
import { rustExecute } from './rustRepl.js'
|
||||
@ -28,20 +28,23 @@ function resolveLang(lang?: string) {
|
||||
return lang ? langAlias[lang] || lang : ''
|
||||
}
|
||||
|
||||
export function resolveCode(el: HTMLElement): string {
|
||||
const clone = el.cloneNode(true) as HTMLElement
|
||||
clone
|
||||
.querySelectorAll(ignoredNodes.join(','))
|
||||
.forEach(node => node.remove())
|
||||
|
||||
return clone.textContent || ''
|
||||
}
|
||||
|
||||
export function resolveCodeInfo(el: HTMLDivElement) {
|
||||
const wrapper = el.querySelector('[class*=language-]')
|
||||
const wrapper = el.querySelector('div[class*=language-]')
|
||||
const lang = wrapper?.className.match(RE_LANGUAGE)?.[1]
|
||||
const codeEl = wrapper?.querySelector('pre code')
|
||||
const codeEl = wrapper?.querySelector('pre') as HTMLElement
|
||||
let code = ''
|
||||
|
||||
if (codeEl) {
|
||||
const clone = codeEl.cloneNode(true) as HTMLElement
|
||||
clone
|
||||
.querySelectorAll(ignoredNodes.join(','))
|
||||
.forEach(node => node.remove())
|
||||
|
||||
code = clone.textContent || ''
|
||||
}
|
||||
if (codeEl)
|
||||
code = resolveCode(codeEl)
|
||||
|
||||
return { lang: resolveLang(lang) as Lang, code }
|
||||
}
|
||||
@ -57,6 +60,13 @@ export function useCodeRepl(el: Ref<HTMLDivElement | null>) {
|
||||
const error = ref('') // execute error
|
||||
const backendVersion = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
if (el.value) {
|
||||
const info = resolveCodeInfo(el.value)
|
||||
lang.value = info.lang
|
||||
}
|
||||
})
|
||||
|
||||
const executeMap: ExecuteMap = {
|
||||
kotlin: executeKotlin,
|
||||
go: executeGolang,
|
||||
|
||||
9
plugins/plugin-md-power/src/client/shim.d.ts
vendored
9
plugins/plugin-md-power/src/client/shim.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
declare module '*.vue' {
|
||||
import type { ComponentOptions } from 'vue'
|
||||
import type { ReplEditorData } from '../shared/repl.js'
|
||||
|
||||
const comp: ComponentOptions
|
||||
export default comp
|
||||
declare module '@internal/md-power/replEditorData' {
|
||||
|
||||
const res: ReplEditorData
|
||||
export default res
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import type { PluginWithOptions } from 'markdown-it'
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import container from 'markdown-it-container'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../../shared/index.js'
|
||||
@ -145,7 +145,7 @@ export const caniusePlugin: PluginWithOptions<CanIUseOptions> = (
|
||||
* ```
|
||||
*/
|
||||
export function legacyCaniuse(
|
||||
md: Markdown,
|
||||
md: MarkdownIt,
|
||||
{ mode = 'embed' }: CanIUseOptions = {},
|
||||
): void {
|
||||
const modeMap: CanIUseMode[] = ['image', 'embed']
|
||||
|
||||
@ -1,24 +1,82 @@
|
||||
import type markdownIt from 'markdown-it'
|
||||
import container from 'markdown-it-container'
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { App } from 'vuepress/core'
|
||||
import { fs, getDirname, path } from 'vuepress/utils'
|
||||
import type { ReplEditorData, ReplOptions } from '../../shared/repl.js'
|
||||
|
||||
function createReplContainer(md: markdownIt, type: string) {
|
||||
const RE_INFO = /^(#editable)?\s*?(.*)$/
|
||||
|
||||
function createReplContainer(md: markdownIt, lang: string) {
|
||||
const type = `${lang}-repl`
|
||||
const validate = (info: string): boolean => info.trim().startsWith(type)
|
||||
|
||||
const render = (tokens: Token[], index: number): string => {
|
||||
const token = tokens[index]
|
||||
const info = token.info.trim().slice(type.length).trim() || ''
|
||||
// :::lang-repl#editable title
|
||||
const [, editable, title] = info.match(RE_INFO) ?? []
|
||||
|
||||
if (token.nesting === 1)
|
||||
return '<LanguageRepl>'
|
||||
return `<CodeRepl ${editable ? 'editable' : ''} title="${title || `${lang} playground`}">`
|
||||
|
||||
else
|
||||
return '</LanguageRepl>'
|
||||
return '</CodeRepl>'
|
||||
}
|
||||
|
||||
md.use(container, type, { validate, render })
|
||||
}
|
||||
|
||||
export function langReplPlugin(md: markdownIt) {
|
||||
createReplContainer(md, 'kotlin-repl')
|
||||
createReplContainer(md, 'go-repl')
|
||||
createReplContainer(md, 'rust-repl')
|
||||
export async function langReplPlugin(app: App, md: markdownIt, {
|
||||
theme,
|
||||
go = false,
|
||||
kotlin = false,
|
||||
rust = false,
|
||||
}: ReplOptions) {
|
||||
kotlin && createReplContainer(md, 'kotlin')
|
||||
go && createReplContainer(md, 'go')
|
||||
rust && createReplContainer(md, 'rust')
|
||||
|
||||
theme ??= { light: 'github-light', dark: 'github-dark' }
|
||||
|
||||
const data: ReplEditorData = { grammars: {} } as ReplEditorData
|
||||
|
||||
const themesPath = getDirname(import.meta.resolve('tm-themes'))
|
||||
const grammarsPath = getDirname(import.meta.resolve('tm-grammars'))
|
||||
|
||||
const readTheme = (theme: string) => read(path.join(themesPath, 'themes', `${theme}.json`))
|
||||
const readGrammar = (grammar: string) => read(path.join(grammarsPath, 'grammars', `${grammar}.json`))
|
||||
|
||||
if (typeof theme === 'string') {
|
||||
data.theme = await readTheme(theme)
|
||||
}
|
||||
else {
|
||||
data.theme = await Promise.all([
|
||||
readTheme(theme.light),
|
||||
readTheme(theme.dark),
|
||||
]).then(([light, dark]) => ({ light, dark }))
|
||||
}
|
||||
|
||||
if (kotlin)
|
||||
data.grammars.kotlin = await readGrammar('kotlin')
|
||||
|
||||
if (go)
|
||||
data.grammars.go = await readGrammar('go')
|
||||
|
||||
if (rust)
|
||||
data.grammars.rust = await readGrammar('rust')
|
||||
|
||||
await app.writeTemp(
|
||||
'internal/md-power/replEditorData.js',
|
||||
`export default ${JSON.stringify(data, null, 2)}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function read(file: string): Promise<any> {
|
||||
try {
|
||||
const content = await fs.readFile(file, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
}
|
||||
catch {}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Plugin } from 'vuepress/core'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { CanIUseOptions, MarkdownPowerPluginOptions } from '../shared/index.js'
|
||||
import { caniusePlugin, legacyCaniuse } from './features/caniuse.js'
|
||||
import { pdfPlugin } from './features/pdf.js'
|
||||
@ -18,7 +19,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
|
||||
const { initIcon, addIcon } = createIconCSSWriter(app, options.icons)
|
||||
|
||||
return {
|
||||
name: '@vuepress-plume/plugin-md-power',
|
||||
name: 'vuepress-plugin-md-power',
|
||||
|
||||
// clientConfigFile: path.resolve(__dirname, '../client/config.js'),
|
||||
clientConfigFile: app => prepareConfigFile(app, options),
|
||||
@ -29,7 +30,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
|
||||
|
||||
onInitialized: async () => await initIcon(),
|
||||
|
||||
extendsMarkdown(md) {
|
||||
extendsMarkdown: async (md: MarkdownIt, app) => {
|
||||
if (options.caniuse) {
|
||||
const caniuse = options.caniuse === true ? {} : options.caniuse
|
||||
// @[caniuse](feature_name)
|
||||
@ -87,7 +88,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
|
||||
}
|
||||
|
||||
if (options.repl)
|
||||
langReplPlugin(md)
|
||||
await langReplPlugin(app, md, options.repl)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,8 +47,8 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
}
|
||||
|
||||
if (options.repl) {
|
||||
imports.add(`import LanguageRepl from '${CLIENT_FOLDER}components/LanguageRepl.vue'`)
|
||||
enhances.add(`app.component('LanguageRepl', LanguageRepl)`)
|
||||
imports.add(`import CodeRepl from '${CLIENT_FOLDER}components/CodeRepl.vue'`)
|
||||
enhances.add(`app.component('CodeRepl', CodeRepl)`)
|
||||
}
|
||||
|
||||
// enhances.add(`if (__VUEPRESS_SSR__) return`)
|
||||
|
||||
@ -2,6 +2,7 @@ import type { CanIUseOptions } from './caniuse.js'
|
||||
import type { PDFOptions } from './pdf.js'
|
||||
import type { IconsOptions } from './icons.js'
|
||||
import type { PlotOptions } from './plot.js'
|
||||
import type { ReplOptions } from './repl.js'
|
||||
|
||||
export interface MarkdownPowerPluginOptions {
|
||||
pdf?: boolean | PDFOptions
|
||||
@ -24,7 +25,7 @@ export interface MarkdownPowerPluginOptions {
|
||||
jsfiddle?: boolean
|
||||
|
||||
// container
|
||||
repl?: boolean
|
||||
repl?: false | ReplOptions
|
||||
|
||||
caniuse?: boolean | CanIUseOptions
|
||||
}
|
||||
|
||||
28
plugins/plugin-md-power/src/shared/repl.ts
Normal file
28
plugins/plugin-md-power/src/shared/repl.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { BuiltinTheme, ThemeRegistration } from 'shiki'
|
||||
|
||||
export type ThemeOptions =
|
||||
| BuiltinTheme
|
||||
| {
|
||||
light: BuiltinTheme
|
||||
dark: BuiltinTheme
|
||||
}
|
||||
|
||||
export interface ReplOptions {
|
||||
theme?: ThemeOptions
|
||||
|
||||
go?: boolean
|
||||
kotlin?: boolean
|
||||
rust?: boolean
|
||||
}
|
||||
|
||||
export interface ReplEditorData {
|
||||
grammars: {
|
||||
go?: any
|
||||
kotlin?: any
|
||||
rust?: any
|
||||
}
|
||||
theme: ThemeRegistration | {
|
||||
light: ThemeRegistration
|
||||
dark: ThemeRegistration
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,13 @@
|
||||
"extends": "../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"types": [
|
||||
"vuepress/client-types",
|
||||
"vite/client",
|
||||
"webpack-env"
|
||||
],
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"files": [],
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import { sitemapPlugin } from '@vuepress/plugin-sitemap'
|
||||
import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
|
||||
import { searchPlugin } from '@vuepress-plume/plugin-search'
|
||||
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
|
||||
import { isObject } from '@pengzhanbo/utils'
|
||||
import type {
|
||||
PlumeThemeEncrypt,
|
||||
PlumeThemeLocaleOptions,
|
||||
@ -166,9 +167,11 @@ export function setupPlugins(
|
||||
}
|
||||
|
||||
const shikiOption = options.shiki || options.shikiji
|
||||
let shikiTheme: any = { light: 'vitesse-light', dark: 'vitesse-dark' }
|
||||
if (shikiOption !== false) {
|
||||
shikiTheme = shikiOption?.theme ?? shikiTheme
|
||||
plugins.push(shikiPlugin({
|
||||
theme: { light: 'vitesse-light', dark: 'vitesse-dark' },
|
||||
theme: shikiTheme,
|
||||
...(shikiOption ?? {}),
|
||||
}))
|
||||
}
|
||||
@ -206,6 +209,9 @@ export function setupPlugins(
|
||||
plugins.push(markdownPowerPlugin({
|
||||
caniuse: options.caniuse,
|
||||
...options.markdownPower || {},
|
||||
repl: options.markdownPower?.repl
|
||||
? { theme: shikiTheme, ...options.markdownPower?.repl }
|
||||
: options.markdownPower?.repl,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
"@internal/notesData": [
|
||||
"./plugins/plugin-notes-data/src/client/notesData.d.ts"
|
||||
],
|
||||
"@internal/md-power/replEditorData": [
|
||||
"./plugins/plugin-md-power/src/client/shim.d.ts"
|
||||
],
|
||||
"@internal/pageComponents": ["./docs/.vuepress/.temp/internal/pageComponents.js"],
|
||||
"@internal/*": ["./docs/.vuepress/.temp/internal/*"],
|
||||
"@vuepress-plume/*": ["./plugins/*/src/node/index.ts"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user