Skip to content

Commit 29c806b

Browse files
committed
Add playground
1 parent a26053b commit 29c806b

File tree

6 files changed

+463
-1
lines changed

6 files changed

+463
-1
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
<script setup>
2+
import { ref, onMounted, watch, onUnmounted } from 'vue'
3+
import { useData } from 'vitepress'
4+
5+
const { isDark } = useData()
6+
const editorRef = ref(null)
7+
const outputRef = ref(null)
8+
const error = ref('')
9+
10+
const defaultMAML = `{
11+
project: "MAML"
12+
tags: [
13+
"minimal"
14+
"readable"
15+
]
16+
17+
# A simple nested object
18+
spec: {
19+
version: 1
20+
author: "Anton Medvedev"
21+
}
22+
23+
# Array of objects
24+
examples: [
25+
{
26+
name: "JSON", born: 2001
27+
}
28+
{
29+
name: "MAML", born: 2025
30+
}
31+
]
32+
33+
notes: """
34+
This is a multiline raw string.
35+
Keeps formatting as-is.
36+
"""
37+
}`
38+
39+
let editorView = null
40+
let outputView = null
41+
42+
onMounted(async () => {
43+
const { EditorView, basicSetup } = await import('codemirror')
44+
const { EditorState, Compartment } = await import('@codemirror/state')
45+
const { StreamLanguage } = await import('@codemirror/language')
46+
const { json } = await import('@codemirror/lang-json')
47+
const { oneDark } = await import('@codemirror/theme-one-dark')
48+
const maml = await import('maml.js')
49+
50+
const themeCompartment = new Compartment()
51+
const outputThemeCompartment = new Compartment()
52+
53+
function getTheme() {
54+
return isDark.value ? oneDark : EditorView.theme({})
55+
}
56+
57+
const mamlLanguage = StreamLanguage.define({
58+
startState() {
59+
return { inRawString: false }
60+
},
61+
token(stream, state) {
62+
if (state.inRawString) {
63+
if (stream.match('"""')) {
64+
state.inRawString = false
65+
return 'string'
66+
}
67+
stream.skipToEnd()
68+
return 'string'
69+
}
70+
71+
if (stream.eatSpace()) return null
72+
73+
if (stream.eat('#')) {
74+
stream.skipToEnd()
75+
return 'lineComment'
76+
}
77+
78+
if (stream.match('"""')) {
79+
state.inRawString = true
80+
return 'string'
81+
}
82+
83+
if (stream.eat('"')) {
84+
while (!stream.eol()) {
85+
const ch = stream.next()
86+
if (ch === '"') return 'string'
87+
if (ch === '\\') stream.next()
88+
}
89+
return 'string'
90+
}
91+
92+
if (stream.match(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?(?=[\s,\]\}]|$)/)) {
93+
return 'number'
94+
}
95+
96+
if (stream.match(/\b(?:true|false|null)\b/)) {
97+
return 'atom'
98+
}
99+
100+
if (stream.match(/[A-Za-z_][A-Za-z0-9_-]*/)) {
101+
return 'variableName'
102+
}
103+
104+
const ch = stream.next()
105+
if ('{}[],:'.includes(ch)) return 'punctuation'
106+
return null
107+
},
108+
})
109+
110+
function parse(doc) {
111+
try {
112+
const result = maml.parse(doc)
113+
error.value = ''
114+
return JSON.stringify(result, null, 2)
115+
} catch (e) {
116+
error.value = e.message
117+
return ''
118+
}
119+
}
120+
121+
function updateOutput(text) {
122+
if (!outputView) return
123+
outputView.dispatch({
124+
changes: { from: 0, to: outputView.state.doc.length, insert: text },
125+
})
126+
}
127+
128+
editorView = new EditorView({
129+
state: EditorState.create({
130+
doc: defaultMAML,
131+
extensions: [
132+
basicSetup,
133+
mamlLanguage,
134+
themeCompartment.of(getTheme()),
135+
EditorView.updateListener.of((update) => {
136+
if (update.docChanged) {
137+
updateOutput(parse(update.state.doc.toString()))
138+
}
139+
}),
140+
],
141+
}),
142+
parent: editorRef.value,
143+
})
144+
145+
const initialOutput = parse(defaultMAML)
146+
147+
outputView = new EditorView({
148+
state: EditorState.create({
149+
doc: initialOutput,
150+
extensions: [
151+
basicSetup,
152+
json(),
153+
EditorState.readOnly.of(true),
154+
outputThemeCompartment.of(getTheme()),
155+
],
156+
}),
157+
parent: outputRef.value,
158+
})
159+
160+
watch(isDark, () => {
161+
const theme = getTheme()
162+
editorView.dispatch({ effects: themeCompartment.reconfigure(theme) })
163+
outputView.dispatch({ effects: outputThemeCompartment.reconfigure(theme) })
164+
})
165+
})
166+
167+
onUnmounted(() => {
168+
editorView?.destroy()
169+
outputView?.destroy()
170+
})
171+
</script>
172+
173+
<template>
174+
<div class="playground">
175+
<div class="playground-pane">
176+
<div class="playground-label">MAML</div>
177+
<div class="playground-editor" ref="editorRef"></div>
178+
</div>
179+
<div class="playground-pane">
180+
<div class="playground-label">JSON</div>
181+
<div class="playground-error" v-if="error">{{ error }}</div>
182+
<div class="playground-editor" ref="outputRef"></div>
183+
</div>
184+
</div>
185+
</template>
186+
187+
<style scoped>
188+
.playground {
189+
display: flex;
190+
gap: 1px;
191+
background: var(--vp-c-divider);
192+
height: calc(100vh - var(--vp-nav-height));
193+
}
194+
195+
.playground-pane {
196+
flex: 1;
197+
display: flex;
198+
flex-direction: column;
199+
min-width: 0;
200+
background: var(--vp-c-bg);
201+
}
202+
203+
.playground-label {
204+
padding: 8px 16px;
205+
font-size: 13px;
206+
font-weight: 600;
207+
color: var(--vp-c-text-2);
208+
border-bottom: 1px solid var(--vp-c-divider);
209+
flex-shrink: 0;
210+
}
211+
212+
.playground-editor {
213+
flex: 1;
214+
overflow: auto;
215+
}
216+
217+
.playground-editor :deep(.cm-editor) {
218+
height: 100%;
219+
}
220+
221+
.playground-editor :deep(.cm-scroller) {
222+
font-family: var(--vp-font-family-mono);
223+
font-size: 14px;
224+
}
225+
226+
.playground-error {
227+
padding: 8px 16px;
228+
font-size: 13px;
229+
font-family: var(--vp-font-family-mono);
230+
color: var(--vp-c-danger-1);
231+
background: var(--vp-c-danger-soft);
232+
white-space: pre;
233+
overflow-x: auto;
234+
flex-shrink: 0;
235+
}
236+
237+
@media (max-width: 768px) {
238+
.playground {
239+
flex-direction: column;
240+
}
241+
242+
.playground-pane {
243+
height: 50%;
244+
}
245+
}
246+
</style>

.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default defineConfig({
3232
logo: '/logo.svg',
3333
nav: [
3434
{ text: 'Home', link: '/' },
35+
{ text: 'Playground', link: '/playground' },
3536
{ text: 'Logo', link: '/logo' },
3637
{
3738
text: 'Spec',

.vitepress/theme/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import DefaultTheme from 'vitepress/theme'
2+
import Playground from '../components/Playground.vue'
3+
4+
export default {
5+
extends: DefaultTheme,
6+
enhanceApp({ app }) {
7+
app.component('Playground', Playground)
8+
},
9+
}

0 commit comments

Comments
 (0)