mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
commit
44f51c3e65
@ -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: '图表',
|
||||
|
||||
@ -83,6 +83,7 @@ export const theme: Theme = themePlume({
|
||||
replit: true,
|
||||
codeSandbox: true,
|
||||
jsfiddle: true,
|
||||
repl: true,
|
||||
},
|
||||
comment: {
|
||||
provider: 'Giscus',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>`
|
||||
}
|
||||
|
||||
@ -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!");
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@ -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 各平台支持说明 的功能。
|
||||
|
||||
@ -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
|
||||
```
|
||||
:::
|
||||
````
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@ -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)
|
||||
|
||||
## 导入文件
|
||||
|
||||
|
||||
188
docs/notes/theme/guide/代码演示/golang.md
Normal file
188
docs/notes/theme/guide/代码演示/golang.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
100
docs/notes/theme/guide/代码演示/kotlin.md
Normal file
100
docs/notes/theme/guide/代码演示/kotlin.md
Normal 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))
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
123
docs/notes/theme/guide/代码演示/rust.md
Normal file
123
docs/notes/theme/guide/代码演示/rust.md
Normal 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");
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
@ -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
|
||||
@ -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": {
|
||||
|
||||
14
package.json
14
package.json
@ -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": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.23"
|
||||
"vue": "^3.4.25"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress-plume/plugin-content-update": "workspace:*",
|
||||
"vue": "^3.4.23"
|
||||
"vue": "^3.4.25"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"vue": "^3.4.23"
|
||||
"vue": "^3.4.25"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -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": {
|
||||
|
||||
93
plugins/plugin-md-power/src/client/components/CanIUse.vue
Normal file
93
plugins/plugin-md-power/src/client/components/CanIUse.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
204
plugins/plugin-md-power/src/client/components/LanguageRepl.vue
Normal file
204
plugins/plugin-md-power/src/client/components/LanguageRepl.vue
Normal 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>
|
||||
@ -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 {
|
||||
|
||||
213
plugins/plugin-md-power/src/client/composables/codeRepl.ts
Normal file
213
plugins/plugin-md-power/src/client/composables/codeRepl.ts
Normal 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'
|
||||
}[]
|
||||
}
|
||||
}
|
||||
134
plugins/plugin-md-power/src/client/composables/rustRepl.ts
Normal file
134
plugins/plugin-md-power/src/client/composables/rustRepl.ts
Normal 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
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
28
plugins/plugin-md-power/src/client/utils/http.ts
Normal file
28
plugins/plugin-md-power/src/client/utils/http.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
5
plugins/plugin-md-power/src/client/utils/sleep.ts
Normal file
5
plugins/plugin-md-power/src/client/utils/sleep.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
@ -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]),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
24
plugins/plugin-md-power/src/node/features/langRepl.ts
Normal file
24
plugins/plugin-md-power/src/node/features/langRepl.ts
Normal 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')
|
||||
}
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
81
plugins/plugin-md-power/src/node/prepareConfigFile.ts
Normal file
81
plugins/plugin-md-power/src/node/prepareConfigFile.ts
Normal 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')}
|
||||
}
|
||||
})
|
||||
`,
|
||||
)
|
||||
}
|
||||
@ -20,5 +20,8 @@ export interface MarkdownPowerPluginOptions {
|
||||
codeSandbox?: boolean
|
||||
jsfiddle?: boolean
|
||||
|
||||
// container
|
||||
repl?: boolean
|
||||
|
||||
caniuse?: boolean | CanIUseOptions
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:*"
|
||||
},
|
||||
|
||||
@ -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
2017
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@
|
||||
- 👀 支持 搜索、文章评论
|
||||
- 👨💻 支持 浅色/深色 主题 (包括代码高亮)
|
||||
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示 等
|
||||
- 📚 代码演示,支持 CodePen, Replit
|
||||
- 📚 代码演示,支持 CodePen, Replit, JSFiddle, CodeSandbox 等
|
||||
- 📊 嵌入图标,支持 chart.js,Echarts,Mermaid,flowchart
|
||||
- 🎛 资源嵌入,支持 PDF, bilibili视频,youtube视频等
|
||||
|
||||
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,5 +30,5 @@
|
||||
"docs/.vuepress/**/*",
|
||||
"scripts/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", ".cache", ".temp", "lib", "dist"]
|
||||
"exclude": ["**/node_modules/**", "**/.cache/**", "**/.temp/**", "**/lib/**", "**/dist/**"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user