Merge pull request #71 from pengzhanbo/lang-repl

Code Repl & Can I Use
This commit is contained in:
pengzhanbo 2024-04-29 00:39:37 +08:00 committed by GitHub
commit 44f51c3e65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 2715 additions and 1275 deletions

View File

@ -29,7 +29,13 @@ export const zhNotes = definePlumeNotesConfig({
text: '代码块',
dir: '代码',
icon: 'ph:code-bold',
items: ['介绍', '特性支持', '代码组', '导入代码', 'codepen', 'jsFiddle', 'codeSandbox', 'replit', 'twoslash', '代码演示'],
items: ['介绍', '特性支持', '代码组', '导入代码', 'twoslash'],
},
{
text: '代码演示',
dir: '代码演示',
icon: 'carbon:demo',
items: ['前端', 'rust', 'golang', 'kotlin', 'codepen', 'jsFiddle', 'codeSandbox', 'replit'],
},
{
text: '图表',

View File

@ -83,6 +83,7 @@ export const theme: Theme = themePlume({
replit: true,
codeSandbox: true,
jsfiddle: true,
repl: true,
},
comment: {
provider: 'Giscus',

View File

@ -1,101 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { onClickOutside, useLocalStorage, useThrottleFn } from '@vueuse/core'
import { resolveCanIUse } from '../composables/caniuse.js'
import { shallowRef } from 'vue'
import { useCaniuse, useCaniuseFeaturesSearch, useCaniuseVersionSelect } from '../composables/caniuse.js'
import CodeViewer from './CodeViewer.vue'
interface Feature {
label: string
value: string
}
const listEl = shallowRef<HTMLUListElement | null>(null)
const inputEl = shallowRef<HTMLInputElement | null>(null)
const api = 'https://api.pengzhanbo.cn/caniuse/features'
const features = useLocalStorage('caniuse-features', [] as Feature[])
onMounted(async () => {
const res = await fetch(api)
const data = await res.json()
features.value = data || features.value || []
})
const browserVersionList = ref([
{ label: '新版本(当前版本 + 3', value: 3, checked: false },
{ label: '新版本(当前版本 + 2', value: 2, checked: false },
{ label: '新版本(当前版本 + 1', value: 1, checked: false },
{ label: '当前版本', value: 0, disabled: true, checked: true },
{ label: '旧版本(当前版本 - 1', value: -1, checked: false },
{ label: '旧版本(当前版本 - 2', value: -2, checked: false },
{ label: '旧版本(当前版本 - 3', value: -3, checked: false },
{ label: '旧版本(当前版本 - 4', value: -4, checked: false },
{ label: '旧版本(当前版本 - 5', value: -5, checked: false },
])
const input = ref('')
const isFocus = ref(false)
const searched = ref<Feature[]>()
const selected = ref<Feature | null>(null)
const embedType = ref('')
const browserVersion = computed(() => {
const values = browserVersionList.value.filter(item => item.checked).map(item => item.value)
if (values.length === 1 && values[0] === 0)
return ''
return values.join(',')
})
watch(() => [features.value, isFocus.value], () => {
if (!isFocus.value)
searched.value = features.value
}, { immediate: true })
const listEl = ref<HTMLUListElement | null>(null)
const inputEl = ref<HTMLInputElement | null>(null)
onClickOutside(listEl, () => {
isFocus.value = false
}, { ignore: [inputEl] })
const onInput = useThrottleFn(() => {
selected.value = null
if (!input.value) {
searched.value = features.value
}
else {
searched.value = features.value.filter(item => item.label.includes(input.value) || item.value.includes(input.value))
if (searched.value.length === 1)
selected.value = searched.value[0]
}
}, 300)
function onSelect(item: Feature) {
selected.value = item
input.value = item.label
isFocus.value = false
}
const output = computed(() => {
let content = '@[caniuse'
if (embedType.value)
content += ` ${embedType.value}`
if (browserVersion.value && !embedType.value)
content += `{${browserVersion.value}}`
content += ']('
if (selected.value)
content += selected.value.value
return `${content})`
})
const rendered = ref('')
function render() {
if (!selected.value)
return
rendered.value = resolveCanIUse(selected.value.value, embedType.value, browserVersion.value)
}
const { feature, featureList, onSelect, isFocus } = useCaniuseFeaturesSearch(inputEl, listEl)
const { past, pastList, future, futureList, embedType, embedTypeList } = useCaniuseVersionSelect()
const { output, rendered } = useCaniuse({ feature, embedType, past, future })
</script>
<template>
@ -105,12 +18,19 @@ function render() {
<label for="feature">选择特性</label>
<div class="feature-input">
<input
ref="inputEl" v-model="input" class="feature-input__input" type="text" name="feature"
placeholder="输入特性" @focus="isFocus = true" @input="onInput"
ref="inputEl"
class="feature-input__input"
type="text"
name="feature"
placeholder="输入特性"
>
<span class="vpi-chevron-down" />
<ul v-show="isFocus" ref="listEl" class="feature-list">
<li v-for="item in searched" :key="item.value" @click="onSelect(item)">
<li
v-for="item in featureList"
:key="item.value"
@click="onSelect(item)"
>
{{ item.label }}
</li>
</ul>
@ -119,41 +39,35 @@ function render() {
<div class="caniuse-form-item">
<label for="embedType">嵌入方式</label>
<div class="caniuse-embed-type">
<label>
<input type="radio" name="embedType" value="" :checked="embedType === ''" @click="embedType = ''">
<span>iframe</span>
</label>
<label>
<input
type="radio" name="embedType" value="image" :checked="embedType === 'image'"
@click="embedType = 'image'"
> <span>image</span>
</label>
</div>
</div>
<div v-if="!embedType" class="caniuse-form-item">
<label for="browserVersion">浏览器版本</label>
<div class="caniuse-browser-version">
<label v-for="item in browserVersionList" :key="item.value">
<input
v-model="item.checked" type="checkbox" name="browserVersion" :checked="item.checked"
:disabled="item.disabled"
>
<label v-for="item in embedTypeList" :key="item.label">
<input v-model="embedType" type="radio" name="embedType" :value="item.value">
<span>{{ item.label }}</span>
</label>
</div>
</div>
<div class="caniuse-render">
<button class="caniuse-render-button" type="button" :disabled="!selected" @click="render">
生成预览
</button>
<div v-if="!embedType" class="caniuse-form-item">
<span>浏览器版本</span>
<div class="caniuse-browser-version">
<select v-model="past">
<option v-for="item in pastList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
<span>-</span>
<select v-model="future">
<option v-for="item in futureList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
</div>
</div>
</form>
<div class="caniuse-output">
<h4>输出</h4>
<CodeViewer lang="md" :content="output" />
</div>
<div v-html="rendered" />
<div v-if="embedType === 'image'" v-html="rendered" />
<CanIUseViewer v-else-if="feature" :feature="feature" :past="past" :future="future" />
</div>
</template>
@ -183,6 +97,7 @@ function render() {
.caniuse-form-item:nth-child(3) {
align-items: baseline;
margin-bottom: 0;
}
.feature-input {
@ -251,24 +166,40 @@ function render() {
.caniuse-browser-version {
flex: 1;
flex-wrap: wrap;
margin-left: 10px;
}
.caniuse-browser-version label {
display: block;
.caniuse-browser-version span {
display: none;
}
.caniuse-browser-version select {
flex: 1;
width: 100%;
cursor: pointer;
padding: 3px 16px;
background-color: var(--vp-c-bg);
border: solid 1px var(--vp-c-divider);
transition: border var(--t-color), background-color var(--t-color);
}
.caniuse-browser-version select:first-of-type {
margin-bottom: 16px;
}
@media (min-width: 768px) {
.caniuse-browser-version {
display: flex;
gap: 10px 0;
gap: 10px;
align-items: center;
justify-content: center;
}
.caniuse-browser-version label {
width: 50%;
.caniuse-browser-version span {
display: block;
}
.caniuse-browser-version select:first-of-type {
margin-bottom: 0;
}
}

View File

@ -1,46 +1,164 @@
export function resolveCanIUse(feature: string, mode: string, versions: string): string {
if (!feature)
return ''
import { type Ref, computed, onMounted, readonly, ref, watch } from 'vue'
import { onClickOutside, useDebounceFn, useEventListener, useLocalStorage } from '@vueuse/core'
if (mode === 'image') {
const link = 'https://caniuse.bitsofco.de/image/'
const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com`
return `<p><picture>
<source type="image/webp" srcset="${link}${feature}.webp">
<source type="image/png" srcset="${link}${feature}.png">
<img src="${link}${feature}.jpg" alt="${alt}" width="100%">
</picture></p>`
interface Feature {
label: string
value: string
}
interface SelectItem {
label: string
value: string
}
const api = 'https://caniuse.pengzhanbo.cn/features.json'
const pastVersions: SelectItem[] = [
{ label: '不显示旧版本', value: '0' },
...Array(5).fill(0).map((_, i) => ({
label: `旧版本(当前版本 - ${i + 1}`,
value: `${i + 1}`,
})),
]
const futureVersions: SelectItem[] = [
{ label: '不显示未来版本', value: '0' },
...Array(3).fill(0).map((_, i) => ({
label: `未来版本(当前版本 + ${i + 1}`,
value: `${i + 1}`,
})),
]
const embedTypes: SelectItem[] = [
{ label: 'iframe', value: '' },
{ label: 'image', value: 'image' },
]
export function useCaniuseVersionSelect() {
const past = ref('2')
const future = ref('1')
const embedType = ref('')
const pastList = readonly(pastVersions)
const futureList = readonly(futureVersions)
const embedTypeList = readonly(embedTypes)
return {
past,
future,
pastList,
futureList,
embedType,
embedTypeList,
}
}
export function useCaniuseFeaturesSearch(
inputEl: Ref<HTMLInputElement | null>,
listEl: Ref<HTMLUListElement | null>,
) {
const features = useLocalStorage('caniuse-features', [] as Feature[])
const featuresUpdated = useLocalStorage('caniuse-features-updated', Date.now())
const maxAge = 1000 * 60 * 60 * 24 * 3 // 3 days
onMounted(async () => {
if (typeof document === 'undefined')
return
if (features.value.length && Date.now() - featuresUpdated.value < maxAge)
return
const data = await fetch(api).then(res => res.json())
features.value = data || features.value || []
})
const input = ref('')
const isFocus = ref(false)
const searched = ref<Feature[]>()
const selected = ref<Feature | null>(null)
watch(() => [features.value, isFocus.value], () => {
if (!isFocus.value)
searched.value = features.value
}, { immediate: true })
onClickOutside(listEl, () => {
isFocus.value = false
}, { ignore: [inputEl] })
useEventListener(inputEl, 'input', useDebounceFn(() => {
selected.value = null
input.value = inputEl.value?.value || ''
if (!input.value) {
searched.value = features.value
}
else {
searched.value = features.value.filter(item => item.label.includes(input.value) || item.value.includes(input.value))
if (searched.value.length === 1)
selected.value = searched.value[0]
}
}, 500))
useEventListener(inputEl, 'focus', () => {
isFocus.value = true
})
function onSelect(item: Feature) {
selected.value = item
isFocus.value = false
if (inputEl.value)
inputEl.value.value = item.label
}
const periods = resolveVersions(versions)
const accessible = 'false'
const image = 'none'
const url = 'https://caniuse.bitsofco.de/embed/index.html'
const src = `${url}?feat=${feature}&periods=${periods}&accessible-colours=${accessible}&image-base=${image}`
return `<div class="ciu_embed" style="margin:16px 0" data-feature="${feature}"><iframe src="${src}" frameborder="0" width="100%" height="400px" title="Can I use ${feature}"></iframe></div>`
return {
featureList: searched,
isFocus,
onSelect,
feature: computed(() => selected.value?.value || ''),
}
}
function resolveVersions(versions: string): string {
if (!versions)
return 'future_1,current,past_1,past_2'
export function useCaniuse({ feature, embedType, past, future }: {
feature: Ref<string>
embedType: Ref<string>
past: Ref<string>
future: Ref<string>
}) {
const output = computed(() => {
let content = '@[caniuse'
if (embedType.value)
content += ` ${embedType.value}`
const list = versions
.split(',')
.map(v => Number(v.trim()))
.filter(v => !Number.isNaN(v) && v >= -5 && v <= 3)
if (past.value !== '-2' || future.value !== '1') {
if (past.value === '0' && future.value === '0')
content += '{0}'
else
content += `{-${past.value},${future.value}}`
}
list.push(0)
content += ']('
const uniq = [...new Set(list)].sort((a, b) => b - a)
const result: string[] = []
uniq.forEach((v) => {
if (v < 0)
result.push(`past_${Math.abs(v)}`)
if (v === 0)
result.push('current')
if (v > 0)
result.push(`future_${v}`)
if (feature.value)
content += feature.value
return `${content})`
})
return result.join(',')
const rendered = computed(() => {
if (!feature.value || !embedType.value)
return ''
return resolveCanIUse(feature.value)
})
return { output, rendered }
}
function resolveCanIUse(feature: string): string {
const link = 'https://caniuse.bitsofco.de/image/'
const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com`
return `<p><picture>
<source type="image/webp" srcset="${link}${feature}.webp">
<source type="image/png" srcset="${link}${feature}.png">
<img src="${link}${feature}.jpg" alt="${alt}" width="100%">
</picture></p>`
}

View File

@ -4,3 +4,133 @@ author: Plume Theme
createTime: 2024/03/01 22:56:03
permalink: /article/z8zvx0ru/
---
:::go-repl
```go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
```
:::
:::go-repl
```go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
dur := time.Duration(rand.Intn(1000)) * time.Millisecond
fmt.Printf("Sleeping for %v\n", dur)
// Sleep for a random duration between 0-1000ms
time.Sleep(dur)
}
fmt.Println("Done!")
}
```
:::
::: go-repl
```go
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"os"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, playground")
})
log.Println("Starting server...")
l, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(http.Serve(l, nil))
}()
log.Println("Sending request...")
res, err := http.Get("http://localhost:8080/hello")
if err != nil {
log.Fatal(err)
}
log.Println("Reading response...")
if _, err := io.Copy(os.Stdout, res.Body); err != nil {
log.Fatal(err)
}
}
```
:::
::: kotlin-repl
```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
```kotlin
fun mul(a: Int, b: Int): Int {
return a * b
}
fun main(args: Array<String>) {
print(mul(-2, 4))
}
```
:::
::: rust-repl
```rust
fn main() {
println!("Hello, world!");
}
```
:::
::: rust-repl
```rust
fn main() {
printlnl!("Hello, world!");
}
```
:::

View File

@ -5,6 +5,11 @@ createTime: 2024/03/11 17:22:52
permalink: /plugins/plugin-caniuse/
---
:::warning Deprecated
该插件功能已整合到 [vuepress-plugin-md-power](/plugins/plugin-md-power) 。
因此,此插件不再更新维护,并标记为 弃用。
:::
## 指南
为你的 vuepress 站点,在编写文章时, 提供嵌入 [can-i-use](https://caniuse.com/) WEB feature 各平台支持说明 的功能。

View File

@ -41,7 +41,8 @@ pnpm add vuepress-plugin-md-power
```ts
// .vuepress/config.ts
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
module.exports = {
export default {
// ...
plugins: [
markdownPowerPlugin({
@ -72,6 +73,7 @@ interface MarkdownPowerPluginOptions {
jsfiddle?: boolean
caniuse?: boolean | CanIUseOptions
repl?: boolean
}
```
@ -85,23 +87,23 @@ interface MarkdownPowerPluginOptions {
```md
@[caniuse](feature)
@[caniuse image](feature)
@[caniuse embed{versions}](feature)
@[caniuse image](feature) // 不再推荐使用
@[caniuse embed{versionRange}](feature)
```
你可以从 [caniuse](https://caniuse.bitsofco.de/) 获取 feature 的值。
你可以从 [caniuse](https://caniuse.pengzhanbo.cn/) 获取 feature 的值。
默认情况下,插件通过 `iframe` 嵌入 `caniuse` 的支持情况查看器。
你也可以使用 `@[caniuse image](feature)` 直接嵌入图片。
~~你也可以使用 `@[caniuse image](feature)` 直接嵌入图片。~~
caniuse 默认查看最近的5个浏览器版本。你可以通过 `{versions}` 手动设置查看的浏览器版本。
格式为 `{number,number,...}`。取值范围为 `-5 ~ 3`
caniuse 默认查看最近的5个浏览器版本。你可以通过 `{versionRange}` 手动设置查看的浏览器版本。
格式为 `{past,future}` 表示 `{过去版本,未来版本}`。取值范围为 `-5 ~ 3`
- 小于0 表示低于当前浏览器版本的支持情况
- 0 表示当前浏览器版本的支持情况
- 大于0 表示高于当前浏览器版本的支持情况
`{-2,-1,1,2}` 表示查看低于当前 2 个版本 到 高于当前 2 个版本的支持情况。
`{-2,2}` 表示查看低于当前 2 个版本 到 高于当前 2 个版本的支持情况。
### pdf
@ -280,3 +282,32 @@ pnpm add @iconify/json
- `tab`: 选项卡, 可选值:`"js" | "css" | "html" | "result"`, 多个用 `","` 分割,
顺序将决定选项卡的排序,默认为 `js,css,html,result`
- `height`: 高度
### Repl
插件默认不启用该功能,你需要手动设置 `repl``true`
提供在 markdown 中为 `golang``kotlin``rust` 语言的 在线代码演示 支持。
在线编译执行代码,并输出结果。
#### 语法
````md
::: go-repl
```go
// your go lang code
```
:::
::: kotlin-repl
```kotlin
// your kotlin code
```
:::
:::rust-repl
```rust
// your rust code
```
:::
````

View File

@ -31,6 +31,7 @@ export default defineUserConfig({
// codeSandbox: true, // @[codesandbox](id) 嵌入 CodeSandbox
// jsfiddle: true, // @[jsfiddle](id) 嵌入 jsfiddle
// caniuse: true, // @[caniuse](feature) 嵌入 caniuse
// repl: true, // :::go-repl :::kotlin-repl :::rust-repl
}
}
}),

View File

@ -431,15 +431,15 @@ export default defineUserConfig({
- `feature`
必填。 正确取值请参考 [https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)
必填。 正确取值请参考 [caniuse-embed.vercel.app](https://caniuse-embed.vercel.app/zh-CN)
- `{browser_versions}`
可选。当前特性在多个版本中的支持情况。
默认值为: `{-2,-1,1}`
默认值为: `{-2,1}`
格式: `{number,number,...}` 取值范围为 `-5 ~ 3`
格式: `{past,future}` 取值范围为 `-5 ~ 3`
- 小于`0` 表示低于当前浏览器版本的支持情况
- `0` 表示当前浏览器版本的支持情况
@ -453,6 +453,11 @@ export default defineUserConfig({
默认值为:`'embed'`
:::caution
不再推荐使用 image 类型,建议使用 embed 类型,主题更换了 embed 实现技术方案,
现在的 embed 类型优势已远远超过 image 类型,加载速度更快,体积更小,交互体验更好。
:::
### 示例
**获取 css 伪类选择器 `:dir()` 在各个浏览器的支持情况:**
@ -478,12 +483,12 @@ export default defineUserConfig({
**获取 css 伪类选择器 `:dir()` 特定范围浏览器的支持情况:**
```md
@[caniuse{-2,-1,1,2,3}](css-matches-pseudo)
@[caniuse{-2,3}](css-matches-pseudo)
```
效果:
@[caniuse{-2,-1,1,2,3}](css-matches-pseudo)
@[caniuse{-2,3}](css-matches-pseudo)
## 导入文件

View File

@ -0,0 +1,188 @@
---
title: Golang
author: pengzhanbo
icon: devicon-plain:go
createTime: 2024/04/22 09:44:30
permalink: /guide/repl/golang/
---
## 概述
主题提供了 Golang 代码演示,支持 在线运行 Go 代码。
::: important
该功能通过将 代码提交到 服务器 进行 编译并执行,且一次只能提交单个代码文件。
因此,请不要使用此功能 执行 过于复杂的代码,也不要过于频繁的进行执行请求。
:::
## 配置
该功能默认不启用,你可以通过配置来启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
repl: true,
},
}
})
})
```
:::
## 使用
使用 `::: go-repl` 容器语法 将 Go 代码块包裹起来。主题会检查代码块并添加执行按钮。
````md
::: go-repl
```go
// your rust code
```
:::
````
## 示例
### 打印内容
**输入:**
````md
:::go-repl
```go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
```
:::
````
**输出:**
:::go-repl
```go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
```
:::
### 循环随机延迟打印
**输入:**
````md
:::go-repl
```go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
dur := time.Duration(rand.Intn(1000)) * time.Millisecond
fmt.Printf("Sleeping for %v\n", dur)
// Sleep for a random duration between 0-1000ms
time.Sleep(dur)
}
fmt.Println("Done!")
}
```
:::
````
**输出:**
:::go-repl
```go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
dur := time.Duration(rand.Intn(1000)) * time.Millisecond
fmt.Printf("Sleeping for %v\n", dur)
// Sleep for a random duration between 0-1000ms
time.Sleep(dur)
}
fmt.Println("Done!")
}
```
:::
### 网络请求
::: go-repl
```go
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"os"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, playground")
})
log.Println("Starting server...")
l, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(http.Serve(l, nil))
}()
log.Println("Sending request...")
res, err := http.Get("http://localhost:8080/hello")
if err != nil {
log.Fatal(err)
}
log.Println("Reading response...")
if _, err := io.Copy(os.Stdout, res.Body); err != nil {
log.Fatal(err)
}
}
```
:::

View File

@ -0,0 +1,100 @@
---
title: Kotlin
author: pengzhanbo
icon: tabler:brand-kotlin
createTime: 2024/04/22 09:44:37
permalink: /guide/repl/kotlin/
---
## 概述
主题提供了 Kotlin 代码演示,支持 在线运行 Kotlin 代码。
::: important
该功能通过将 代码提交到 服务器 进行 编译并执行,且一次只能提交单个代码文件。
因此,请不要使用此功能 执行 过于复杂的代码,也不要过于频繁的进行执行请求。
:::
## 配置
该功能默认不启用,你可以通过配置来启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
repl: true,
},
}
})
})
```
:::
## 使用
使用 `::: kotlin-repl` 容器语法 将 Rust 代码块包裹起来。主题会检查代码块并添加执行按钮。
````md
::: kotlin-repl
```kotlin
// your kotlin code
```
:::
````
## 示例
### 打印内容
**输入:**
````md
::: kotlin-repl
```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
```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
```kotlin
fun mul(a: Int, b: Int): Int {
return a * b
}
fun main(args: Array<String>) {
print(mul(-2, 4))
}
```
:::

View File

@ -0,0 +1,123 @@
---
title: Rust
author: pengzhanbo
icon: logos:rust
createTime: 2024/04/22 09:44:43
permalink: /guide/repl/rust/
---
## 概述
主题提供了 Rust 代码演示,支持 在线运行 Rust 代码。
::: important
该功能通过将 代码提交到 服务器 进行 编译并执行,且一次只能提交单个代码文件。
因此,请不要使用此功能 执行 过于复杂的代码,也不要过于频繁的进行执行请求。
:::
## 配置
该功能默认不启用,你可以通过配置来启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
repl: true,
},
}
})
})
```
:::
## 使用
使用 `::: rust-repl` 容器语法 将 Rust 代码块包裹起来。主题会检查代码块并添加执行按钮。
````md
::: rust-repl
```rust
// your rust code
```
:::
````
## 示例
### 打印内容
**输入:**
````md
::: rust-repl
```rust
fn main() {
println!("Hello, world!");
}
```
:::
````
**输出:**
::: rust-repl
```rust
fn main() {
println!("Hello, world!");
}
```
:::
点击 执行 按钮,即可执行代码。
### 打印错误信息
**输入:**
````md
::: rust-repl
```rust
fn main() {
printlnl!("Hello, world!");
}
```
:::
````
**输出:**
::: rust-repl
```rust
fn main() {
printlnl!("Hello, world!");
}
```
:::
### 等待子进程执行
::: 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");
}
```
:::

View File

@ -1,14 +1,16 @@
---
title: 代码演示
title: 前端
author: pengzhanbo
icon: carbon:demo
icon: icon-park-outline:html-five
createTime: 2024/04/04 11:39:05
permalink: /guide/code/demo/
permalink: /guide/repl/frontend/
---
## 概述
代码演示 默认不启用,你可以通过配置来启用它。
前端代码演示 由 [vuepress-plugin-md-enhance](https://plugin-md-enhance.vuejs.press/zh/) 提供支持。
前端 代码演示 默认不启用,你可以通过配置来启用它。
::: code-tabs
@tab .vuepress/config.ts

View File

@ -12,14 +12,14 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@iconify/json": "^2.2.202",
"@iconify/json": "^2.2.204",
"@vuepress/bundler-vite": "2.0.0-rc.9",
"anywhere": "^1.6.0",
"chart.js": "^4.4.2",
"echarts": "^5.5.0",
"flowchart.ts": "^3.0.0",
"mermaid": "^10.9.0",
"vue": "^3.4.23",
"vue": "^3.4.25",
"vuepress-theme-plume": "workspace:*"
},
"devDependencies": {

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.53",
"private": true,
"packageManager": "pnpm@9.0.4",
"packageManager": "pnpm@9.0.6",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"keywords": [
@ -39,10 +39,10 @@
"release:version": "bumpp package.json plugins/*/package.json theme/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all --tag --push"
},
"devDependencies": {
"@commitlint/cli": "^19.2.2",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@pengzhanbo/eslint-config-vue": "^1.9.0",
"@pengzhanbo/stylelint-config": "^1.9.0",
"@pengzhanbo/eslint-config-vue": "^1.9.1",
"@pengzhanbo/stylelint-config": "^1.9.1",
"@types/lodash.merge": "^4.6.9",
"@types/node": "20.9.1",
"@types/webpack-env": "^1.18.4",
@ -52,14 +52,14 @@
"conventional-changelog-cli": "^4.1.0",
"cpx2": "^7.0.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.0.0",
"eslint": "^9.1.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"rimraf": "^5.0.5",
"stylelint": "^16.3.1",
"stylelint": "^16.4.0",
"tsconfig-vuepress": "^4.5.0",
"typescript": "^5.4.5",
"vite": "^5.2.9"
"vite": "^5.2.10"
},
"pnpm": {
"patchedDependencies": {

View File

@ -43,7 +43,7 @@
"@vue/devtools-api": "6.5.1",
"chokidar": "^3.6.0",
"create-filter": "^1.0.1",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

View File

@ -40,7 +40,7 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

View File

@ -41,7 +41,7 @@
},
"dependencies": {
"@vuepress-plume/plugin-content-update": "workspace:*",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

View File

@ -41,7 +41,7 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

View File

@ -51,10 +51,10 @@
"local-pkg": "^0.5.0",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.7",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"devDependencies": {
"@iconify/json": "^2.2.202",
"@iconify/json": "^2.2.204",
"@types/markdown-it": "^14.0.1"
},
"publishConfig": {

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed, getCurrentInstance, ref } from 'vue'
import { useEventListener } from '@vueuse/core'
interface MessageData {
type: string
payload?: {
feature?: string
meta?: string
height: number
}
}
const props = withDefaults(defineProps<{
feature: string
past?: string
future?: string
meta?: string
}>(), {
past: '2',
future: '1',
meta: '',
})
const url = 'https://caniuse.pengzhanbo.cn/'
const current = getCurrentInstance()
const height = ref('330px')
const isDark = computed(() => current?.appContext.config.globalProperties.$isDark.value)
const source = computed(() => {
const source = `${url}${props.feature}#past=${props.past}&future=${props.future}&meta=${props.meta}&theme=${isDark.value ? 'dark' : 'light'}`
return source
})
useEventListener('message', (event) => {
const data = parseData(event.data)
const { type, payload } = data
if (
type === 'ciu_embed'
&& payload
&& payload.feature === props.feature
&& payload.meta === props.meta
)
height.value = `${Math.ceil(payload.height)}px`
})
function parseData(data: string | MessageData): MessageData {
if (typeof data === 'string') {
try {
return JSON.parse(data)
}
catch {
return { type: '' }
}
}
return data
}
</script>
<template>
<div
class="ciu_embed"
:data-feature="feature"
:data-meta="meta"
:data-past="past"
:data-future="future"
>
<iframe
:src="source"
:style="{ height }"
:title="`Can I use ${feature}`"
/>
</div>
</template>
<style scoped>
.ciu_embed {
margin: 16px -24px;
}
.ciu_embed iframe {
width: 100%;
border: none;
}
@media (min-width: 768px) {
.ciu_embed {
margin: 16px 0;
}
}
</style>

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"
/>
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20 19V7H4v12zm0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-7 14v-2h5v2zm-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z"
/>
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M21.409 9.353a2.998 2.998 0 0 1 0 5.294L8.597 21.614C6.534 22.737 4 21.277 4 18.968V5.033c0-2.31 2.534-3.769 4.597-2.648z"
/>
</svg>
</template>

View File

@ -0,0 +1,204 @@
<script setup lang="ts">
import { 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'
const replEl = shallowRef<HTMLDivElement | null>(null)
const outputEl = shallowRef<HTMLDivElement | null>(null)
const {
onRunCode,
onCleanRun,
firstRun,
stderr,
stdout,
error,
loaded,
finished,
lang,
backendVersion,
} = useCodeRepl(replEl)
function runCode() {
onRunCode()
if (outputEl.value)
outputEl.value.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
}
</script>
<template>
<div ref="replEl" class="code-repl">
<span v-show="loaded && finished" class="icon-run" title="Run Code" @click="runCode">
<IconRun />
</span>
<slot />
<div ref="outputEl" class="code-repl-pin" />
<div v-if="!firstRun" class="code-repl-output">
<div class="output-head">
<IconConsole class="icon-console" />
<span class="title">console</span>
<span v-if="lang && backendVersion" class="output-version">
Running on: {{ lang }} <i>{{ backendVersion }}</i>
</span>
<IconClose class="icon-close" @click="onCleanRun" />
</div>
<div v-if="!loaded" class="output-content">
<Loading />
</div>
<div v-else class="output-content" :class="lang">
<p v-if="error" class="error">
{{ error }}
</p>
<div v-if="stderr.length" class="stderr">
<h4>Stderr:</h4>
<p
v-for="(item, index) in stderr" :key="index"
:class="{ error: lang === 'rust' && item.startsWith('error') }"
>
<pre>{{ item }}</pre>
</p>
</div>
<div v-if="stdout.length" class="stdout">
<h4 v-if="stderr.length">
Stdout:
</h4>
<p v-for="(item, index) in stdout" :key="index">
<pre>{{ item }}</pre>
</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.code-repl {
position: relative;
margin-bottom: 16px;
}
.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);
}
@media (min-width: 768px) {
.code-repl-output {
margin: 0;
border-bottom-right-radius: 6px;
border-bottom-left-radius: 6px;
}
}
.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);
cursor: pointer;
background-color: var(--vp-c-brand-1);
border-radius: 100%;
transition: var(--t-color);
transition-property: color, background-color;
}
@media (min-width: 768px) {
.icon-run {
top: 60px;
right: 16px;
}
}
.icon-run:hover {
background-color: var(--vp-c-brand-2);
}
.code-repl-output .output-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 10px 4px 20px;
border-top: solid 2px var(--vp-c-border);
transition: border-color var(--t-color);
}
.output-head .title {
flex: 1;
margin-left: 10px;
font-size: 14px;
font-weight: 500;
}
.output-head .output-version {
font-size: 12px;
color: var(--vp-c-text-3);
transition: color var(--t-color);
}
.output-head .icon-close {
width: 20px;
height: 20px;
margin-left: 20px;
color: var(--vp-c-text-3);
cursor: pointer;
transition: color var(--t-color);
}
.output-head .icon-close:hover {
color: var(--vp-c-text-2);
}
.output-content {
padding: 12px 20px 24px;
overflow-x: auto;
}
.output-content h4 {
margin: 8px 0;
font-size: 16px;
}
.output-content p {
margin: 0;
font-size: 14px;
line-height: 20px;
}
.output-content p pre {
width: fit-content;
padding: 0 20px 0 0;
margin: 0;
overflow-x: initial;
}
.output-content .error,
.output-content .stderr p,
.output-content.rust .stderr p.error {
color: var(--vp-c-danger-1, #b8272c);
transition: color var(--t-color);
}
.output-content.rust .stderr p {
color: var(--vp-c-text-1);
}
.output-content .stderr + .stdout {
margin-top: 12px;
border-top: 1px solid var(--vp-c-divider);
transition: border-color var(--t-color);
}
</style>

View File

@ -29,7 +29,8 @@ defineProps<{
justify-content: center;
font-size: 36px;
color: currentcolor;
background-color: var(--vp-c-bg, #fff);
background-color: inherit;
transition: background-color var(--t-color);
}
.md-power-loading.absolute {

View File

@ -0,0 +1,213 @@
import { type Ref, ref } from 'vue'
import { http } from '../utils/http.js'
import { sleep } from '../utils/sleep.js'
import { rustExecute } from './rustRepl.js'
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
const RE_LANGUAGE = /language-([\w]+)/
const api = {
go: 'https://api.pengzhanbo.cn/repl/golang/run',
kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run',
}
type Lang = 'kotlin' | 'go' | 'rust'
type ExecuteFn = (code: string) => Promise<any>
type ExecuteMap = Record<Lang, ExecuteFn>
const langAlias: Record<string, string> = {
kt: 'kotlin',
kotlin: 'kotlin',
go: 'go',
rust: 'rust',
rs: 'rust',
}
const supportLang: Lang[] = ['kotlin', 'go', 'rust']
function resolveLang(lang?: string) {
return lang ? langAlias[lang] || lang : ''
}
export function resolveCodeInfo(el: HTMLDivElement) {
const wrapper = el.querySelector('[class*=language-]')
const lang = wrapper?.className.match(RE_LANGUAGE)?.[1]
const codeEl = wrapper?.querySelector('pre code')
let code = ''
if (codeEl) {
const clone = codeEl.cloneNode(true) as HTMLElement
clone
.querySelectorAll(ignoredNodes.join(','))
.forEach(node => node.remove())
code = clone.textContent || ''
}
return { lang: resolveLang(lang) as Lang, code }
}
export function useCodeRepl(el: Ref<HTMLDivElement | null>) {
const lang = ref<Lang>()
const loaded = ref(true)
const firstRun = ref(true)
const finished = ref(true)
const stdout = ref<string[]>([]) // like print
const stderr = ref<string[]>([]) // like print error
const error = ref('') // execute error
const backendVersion = ref('')
const executeMap: ExecuteMap = {
kotlin: executeKotlin,
go: executeGolang,
rust: executeRust,
}
function onCleanRun() {
loaded.value = false
finished.value = false
stdout.value = []
stderr.value = []
error.value = ''
firstRun.value = true
backendVersion.value = ''
}
async function onRunCode() {
if (!el.value || !loaded.value)
return
const info = resolveCodeInfo(el.value)
lang.value = info.lang
if (!lang.value || !info.code || !supportLang.includes(lang.value))
return
if (firstRun.value)
firstRun.value = false
loaded.value = false
finished.value = false
stdout.value = []
stderr.value = []
error.value = ''
await executeMap[lang.value]?.(info.code)
}
async function executeGolang(code: string) {
const res = await http.post<GolangRequest, GolangResponse>(api.go, { code })
backendVersion.value = `v${res.version}`
loaded.value = true
if (res.error) {
error.value = res.error
finished.value = true
return
}
const events = res.events || []
for (const event of events) {
if (event.kind === 'stdout') {
if (event.delay)
await sleep(event.delay / 1000000)
stdout.value.push(event.message)
}
else if (event.kind === 'stderr') {
stderr.value.push(event.message)
}
}
finished.value = true
}
async function executeKotlin(code: string) {
const filename = 'File.kt'
const res = await http.post<KotlinRequest, KotlinResponse>(api.kotlin, {
args: '',
files: [{ name: filename, publicId: '', text: code }],
})
backendVersion.value = `v${res.version}`
loaded.value = true
if (res.errors) {
const errors = Array.isArray(res.errors[filename]) ? res.errors[filename] : [res.errors[filename]]
if (errors.length) {
errors.forEach(
({ message, severity }) => severity === 'ERROR' && stderr.value.push(message),
)
}
}
stdout.value.push(res.text)
finished.value = true
}
async function executeRust(code: string) {
await rustExecute(code, {
onBegin: () => {
loaded.value = true
finished.value = false
stdout.value = []
stderr.value = []
error.value = ''
backendVersion.value = 'release'
},
onError(message) {
error.value = message
},
onStdout(message) {
stdout.value.push(message)
},
onStderr(message) {
stderr.value.push(message)
},
onEnd: () => {
finished.value = true
},
})
}
return {
onRunCode,
onCleanRun,
lang,
backendVersion,
firstRun,
stderr,
stdout,
loaded,
finished,
error,
}
}
interface GolangRequest {
code: string
version?: '' | 'goprev' | 'gotip'
}
interface GolangResponse {
events?: {
message: ''
kind: 'stdout' | 'stderr'
delay: number
}[]
error?: string
version: string
}
interface KotlinRequest {
args?: string
files: {
name: string
publicId: string
text: string
}[]
}
interface KotlinResponse {
text: string
version: string
errors: {
[filename: string]: {
message: string
severity: 'ERROR' | 'WARNING'
}[]
}
}

View File

@ -0,0 +1,134 @@
/**
* golang kotlin
* rust websocket
* 便 codeRepl 使
*/
import { tryOnScopeDispose } from '@vueuse/core'
const wsUrl = 'wss://play.rust-lang.org/websocket'
const payloadType = {
connected: 'websocket/connected',
request: 'output/execute/wsExecuteRequest',
execute: {
begin: 'output/execute/wsExecuteBegin',
// status: 'output/execute/wsExecuteStatus',
stderr: 'output/execute/wsExecuteStderr',
stdout: 'output/execute/wsExecuteStdout',
end: 'output/execute/wsExecuteEnd',
},
}
let ws: WebSocket | null = null
let isOpen = false
let uuid = 0
function connect(): Promise<void> {
if (isOpen)
return Promise.resolve()
ws = new WebSocket(wsUrl)
uuid = 0
ws.addEventListener('open', () => {
isOpen = true
send(
payloadType.connected,
{ iAcceptThisIsAnUnsupportedApi: true },
{ websocket: true, sequenceNumber: uuid },
)
})
ws.addEventListener('close', () => {
isOpen = false
ws = null
})
tryOnScopeDispose(() => ws?.close())
return new Promise((resolve) => {
function connected(e: WebSocketEventMap['message']) {
const data = JSON.parse(e.data)
if (data.type === payloadType.connected) {
ws?.removeEventListener('message', connected)
resolve()
}
}
ws?.addEventListener('message', connected)
})
}
function send(type: string, payload: Record<string, any>, meta: Record<string, any>) {
const msg = { type, meta, payload }
ws?.send(JSON.stringify(msg))
}
export async function rustExecute(
code: string,
{ onEnd, onError, onStderr, onStdout, onBegin }: RustExecuteOptions,
) {
await connect()
const meta = { sequenceNumber: uuid++ }
const payload = {
backtrace: false,
channel: 'stable',
crateType: 'bin',
edition: '2021',
mode: 'release',
tests: false,
code,
}
send(payloadType.request, payload, meta)
let stdout = ''
let stderr = ''
function onMessage(e: WebSocketEventMap['message']) {
const data = JSON.parse(e.data)
const { type, payload, meta: _meta = {} } = data
if (_meta.sequenceNumber !== meta.sequenceNumber)
return
if (type === payloadType.execute.begin)
onBegin?.()
if (type === payloadType.execute.stdout) {
stdout += payload
if (stdout.endsWith('\n')) {
onStdout?.(stdout)
stdout = ''
}
}
if (type === payloadType.execute.stderr) {
stderr += payload
if (stderr.endsWith('\n')) {
if (stderr.startsWith('error:')) {
const index = stderr.indexOf('\n')
onStderr?.(stderr.slice(0, index))
onStderr?.(stderr.slice(index + 1))
}
else {
onStderr?.(stderr)
}
stderr = ''
}
}
if (type === payloadType.execute.end) {
if (payload.success === false)
onError?.(payload.exitDetail)
ws?.removeEventListener('message', onMessage)
onEnd?.()
}
}
ws?.addEventListener('message', onMessage)
}
interface RustExecuteOptions {
onBegin?: () => void
onStdout?: (message: string) => void
onStderr?: (message: string) => void
onEnd?: () => void
onError?: (message: string) => void
}

View File

@ -1,20 +0,0 @@
let isBind = false
export function setupCanIUse(): void {
if (isBind)
return
isBind = true
window.addEventListener('message', (message) => {
const data = message.data
if (typeof data === 'string' && data.includes('ciu_embed')) {
const [, feature, height] = data.split(':')
const el = document.querySelector(`.ciu_embed[data-feature="${feature}"]:not([data-skip])`)
if (el) {
const h = Number.parseInt(height) + 30
;(el.childNodes[0] as any).height = `${h}px`
el.setAttribute('data-skip', 'true')
}
}
})
}

View File

@ -1,45 +0,0 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { pluginOptions } from './options.js'
import { setupCanIUse } from './composables/setupCanIUse.js'
import PDFViewer from './components/PDFViewer.vue'
import Bilibili from './components/Bilibili.vue'
import Youtube from './components/Youtube.vue'
import Replit from './components/Replit.vue'
import CodeSandbox from './components/CodeSandbox.vue'
import Plot from './components/Plot.vue'
import '@internal/md-power/icons.css'
declare const __VUEPRESS_SSR__: boolean
export default defineClientConfig({
enhance({ router, app }) {
if (pluginOptions.pdf)
app.component('PDFViewer', PDFViewer)
if (pluginOptions.bilibili)
app.component('VideoBilibili', Bilibili)
if (pluginOptions.youtube)
app.component('VideoYoutube', Youtube)
if (pluginOptions.replit)
app.component('ReplitViewer', Replit)
if (pluginOptions.codeSandbox)
app.component('CodeSandboxViewer', CodeSandbox)
if (pluginOptions.plot)
app.component('Plot', Plot)
if (__VUEPRESS_SSR__)
return
if (pluginOptions.caniuse) {
router.afterEach(() => {
setupCanIUse()
})
}
},
}) as ClientConfig

View File

@ -0,0 +1,28 @@
export const http = {
get: async <T extends object = object, R = any>(
url: string,
query?: T,
): Promise<R> => {
const _url = new URL(url)
if (query) {
for (const [key, value] of Object.entries(query))
_url.searchParams.append(key, value)
}
const res = await fetch(_url.toString())
return await res.json()
},
post: async <T extends object = object, R = any>(
url: string,
data?: T,
): Promise<R> => {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
})
return await res.json()
},
}

View File

@ -0,0 +1,5 @@
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}

View File

@ -7,8 +7,11 @@ 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 container from 'markdown-it-container'
import { customAlphabet } from 'nanoid'
import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../../shared/index.js'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5)
// @[caniuse]()
const minLength = 12
@ -17,6 +20,7 @@ const START_CODES = [64, 91, 99, 97, 110, 105, 117, 115, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[caniuse(?:\s*?(embed|image)?(?:{([0-9,\-]*?)})?)\]\(([^)]*)\)/
const UNDERLINE_RE = /_+/g
function createCanIUseRuleBlock(defaultMode: CanIUseMode): RuleBlock {
return (state, startLine, endLine, silent) => {
@ -76,18 +80,16 @@ function resolveCanIUse({ feature, mode, versions }: CanIUseTokenMeta): string {
</picture></p></ClientOnly>`
}
const periods = resolveVersions(versions)
const accessible = 'false'
const image = 'none'
const url = 'https://caniuse.bitsofco.de/embed/index.html'
const src = `${url}?feat=${feature}&periods=${periods}&accessible-colours=${accessible}&image-base=${image}`
feature = feature.replace(UNDERLINE_RE, '_')
const { past, future } = resolveVersions(versions)
const meta = nanoid()
return `<ClientOnly><div class="ciu_embed" style="margin:16px 0" data-feature="${feature}"><iframe src="${src}" frameborder="0" width="100%" height="400px" title="Can I use ${feature}"></iframe></div></ClientOnly>`
return `<CanIUseViewer feature="${feature}" meta="${meta}" past="${past}" future="${future}" />`
}
function resolveVersions(versions: string): string {
function resolveVersions(versions: string): { past: number, future: number } {
if (!versions)
return 'future_1,current,past_1,past_2'
return { past: 2, future: 1 }
const list = versions
.split(',')
@ -97,16 +99,10 @@ function resolveVersions(versions: string): string {
list.push(0)
const uniq = [...new Set(list)].sort((a, b) => b - a)
const result: string[] = []
uniq.forEach((v) => {
if (v < 0)
result.push(`past_${Math.abs(v)}`)
if (v === 0)
result.push('current')
if (v > 0)
result.push(`future_${v}`)
})
return result.join(',')
return {
future: uniq[0],
past: Math.abs(uniq[uniq.length - 1]),
}
}
/**

View File

@ -0,0 +1,24 @@
import type markdownIt from 'markdown-it'
import container from 'markdown-it-container'
import type Token from 'markdown-it/lib/token.mjs'
function createReplContainer(md: markdownIt, type: string) {
const validate = (info: string): boolean => info.trim().startsWith(type)
const render = (tokens: Token[], index: number): string => {
const token = tokens[index]
if (token.nesting === 1)
return '<LanguageRepl>'
else
return '</LanguageRepl>'
}
md.use(container, type, { validate, render })
}
export function langReplPlugin(md: markdownIt) {
createReplContainer(md, 'kotlin-repl')
createReplContainer(md, 'go-repl')
createReplContainer(md, 'rust-repl')
}

View File

@ -1,5 +1,4 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import type { CanIUseOptions, MarkdownPowerPluginOptions } from '../shared/index.js'
import { caniusePlugin, legacyCaniuse } from './features/caniuse.js'
import { pdfPlugin } from './features/pdf.js'
@ -11,8 +10,8 @@ import { replitPlugin } from './features/replit.js'
import { codeSandboxPlugin } from './features/codeSandbox.js'
import { jsfiddlePlugin } from './features/jsfiddle.js'
import { plotPlugin } from './features/plot.js'
const __dirname = getDirname(import.meta.url)
import { langReplPlugin } from './features/langRepl.js'
import { prepareConfigFile } from './prepareConfigFile.js'
export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin {
return (app) => {
@ -21,7 +20,8 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
return {
name: '@vuepress-plume/plugin-md-power',
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
// clientConfigFile: path.resolve(__dirname, '../client/config.js'),
clientConfigFile: app => prepareConfigFile(app, options),
define: {
__MD_POWER_INJECT_OPTIONS__: options,
@ -85,6 +85,9 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
// =|plot|=
md.use(plotPlugin)
}
if (options.repl)
langReplPlugin(md)
},
}
}

View File

@ -0,0 +1,81 @@
import { getDirname, path } from 'vuepress/utils'
import { ensureEndingSlash } from '@vuepress/helper'
import type { App } from 'vuepress/core'
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
const { url: filepath } = import.meta
const __dirname = getDirname(filepath)
const CLIENT_FOLDER = ensureEndingSlash(
path.resolve(__dirname, '../client'),
)
export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOptions) {
const imports = new Set<string>()
const enhances = new Set<string>()
imports.add(`import '@internal/md-power/icons.css'`)
if (options.pdf) {
imports.add(`import PDFViewer from '${CLIENT_FOLDER}components/PDFViewer.vue'`)
enhances.add(`app.component('PDFViewer', PDFViewer)`)
}
if (options.bilibili) {
imports.add(`import Bilibili from '${CLIENT_FOLDER}components/Bilibili.vue'`)
enhances.add(`app.component('VideoBilibili', Bilibili)`)
}
if (options.youtube) {
imports.add(`import Youtube from '${CLIENT_FOLDER}components/Youtube.vue'`)
enhances.add(`app.component('VideoYoutube', Youtube)`)
}
if (options.replit) {
imports.add(`import Replit from '${CLIENT_FOLDER}components/Replit.vue'`)
enhances.add(`app.component('ReplitViewer', Replit)`)
}
if (options.codeSandbox) {
imports.add(`import CodeSandbox from '${CLIENT_FOLDER}components/CodeSandbox.vue'`)
enhances.add(`app.component('CodeSandboxViewer', CodeSandbox)`)
}
if (options.plot) {
imports.add(`import Plot from '${CLIENT_FOLDER}components/Plot.vue'`)
enhances.add(`app.component('Plot', Plot)`)
}
if (options.repl) {
imports.add(`import LanguageRepl from '${CLIENT_FOLDER}components/LanguageRepl.vue'`)
enhances.add(`app.component('LanguageRepl', LanguageRepl)`)
}
// enhances.add(`if (__VUEPRESS_SSR__) return`)
if (options.caniuse) {
imports.add(`import CanIUse from '${CLIENT_FOLDER}components/CanIUse.vue'`)
enhances.add(`app.component('CanIUseViewer', CanIUse)`)
}
// if (options.caniuse) {
// imports.add(`import { setupCanIUse } from '${CLIENT_FOLDER}composables/setupCanIUse.js'`)
// enhances.add(`router.afterEach(() => setupCanIUse())`)
// }
return app.writeTemp(
'md-power/config.js',
`\
import { defineClientConfig } from 'vuepress/client'
${Array.from(imports.values()).join('\n')}
export default defineClientConfig({
enhance({ router, app }) {
${Array.from(enhances.values())
.map(item => ` ${item}`)
.join('\n')}
}
})
`,
)
}

View File

@ -20,5 +20,8 @@ export interface MarkdownPowerPluginOptions {
codeSandbox?: boolean
jsfiddle?: boolean
// container
repl?: boolean
caniuse?: boolean | CanIUseOptions
}

View File

@ -52,7 +52,7 @@
"dotenv": "^16.4.5",
"esbuild": "^0.20.2",
"execa": "^8.0.1",
"netlify-cli": "^17.22.1",
"netlify-cli": "^17.23.0",
"portfinder": "^1.0.32"
},
"devDependencies": {

View File

@ -43,7 +43,7 @@
"@vue/devtools-api": "6.5.1",
"chokidar": "^3.6.0",
"create-filter": "^1.0.1",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

View File

@ -36,7 +36,7 @@
"dependencies": {
"@netlify/functions": "^2.6.0",
"leancloud-storage": "^4.15.2",
"vue": "^3.4.23",
"vue": "^3.4.25",
"vue-router": "4.3.0",
"vuepress-plugin-netlify-functions": "workspace:*"
},

View File

@ -48,7 +48,7 @@
"mark.js": "^8.11.1",
"minisearch": "^6.3.0",
"p-map": "^7.0.2",
"vue": "^3.4.23"
"vue": "^3.4.25"
},
"publishConfig": {
"access": "public"

2017
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@
- 👀 支持 搜索、文章评论
- 👨‍💻‍ 支持 浅色/深色 主题 (包括代码高亮)
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示 等
- 📚 代码演示,支持 CodePen, Replit
- 📚 代码演示,支持 CodePen, Replit, JSFiddle, CodeSandbox 等
- 📊 嵌入图标,支持 chart.jsEchartsMermaidflowchart
- 🎛 资源嵌入,支持 PDF, bilibili视频youtube视频等

View File

@ -97,9 +97,9 @@
"katex": "^0.16.10",
"lodash.merge": "^4.6.2",
"nanoid": "^5.0.7",
"vue": "^3.4.23",
"vue": "^3.4.25",
"vue-router": "4.3.0",
"vuepress-plugin-md-enhance": "2.0.0-rc.36",
"vuepress-plugin-md-enhance": "2.0.0-rc.37",
"vuepress-plugin-md-power": "workspace:*"
}
}

View File

@ -30,5 +30,5 @@
"docs/.vuepress/**/*",
"scripts/**/*"
],
"exclude": ["node_modules", ".cache", ".temp", "lib", "dist"]
"exclude": ["**/node_modules/**", "**/.cache/**", "**/.temp/**", "**/lib/**", "**/dist/**"]
}