Skip to content

Commit d50dbf7

Browse files
committed
setup tutorial src markdown support
1 parent 0f3b06b commit d50dbf7

21 files changed

+211
-43
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,4 @@ temp/
107107
TODOs.md
108108
src/api/index.json
109109
src/examples/data.json
110+
src/tutorial/data.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"devDependencies": {
1313
"@types/node": "^16.9.1",
1414
"cheap-watch": "^1.0.3",
15-
"vitepress": "^0.20.2"
15+
"vitepress": "^0.20.4"
1616
}
1717
}

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/genExamplesData.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ exports.genExamplesData = () => {
1515
})
1616
}
1717

18+
exports.readExample = readExample
19+
1820
function genExamples() {
1921
const srcDir = path.resolve(__dirname, '../src/examples/src')
2022
const examples = fs.readdirSync(srcDir)

scripts/genTutorialData.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* watch / generate an all-tutorial data temporary json file (ignored by Git)
3+
*/
4+
5+
// @ts-check
6+
const fs = require('fs')
7+
const path = require('path')
8+
const { watch } = require('./watch')
9+
const { readExample } = require('./genExamplesData')
10+
const { createMarkdownRenderer } = require('vitepress')
11+
12+
exports.genTutorialData = () => {
13+
const md = createMarkdownRenderer(process.cwd())
14+
15+
watch({
16+
src: 'tutorial/src',
17+
out: 'tutorial/data.json',
18+
genData: () => genTutorialSteps(md)
19+
})
20+
}
21+
22+
/**
23+
* @param {import('vitepress').MarkdownRenderer} md
24+
*/
25+
function genTutorialSteps(md) {
26+
const srcDir = path.resolve(__dirname, '../src/tutorial/src')
27+
const steps = fs.readdirSync(srcDir).sort()
28+
const data = {}
29+
30+
for (const name of steps) {
31+
const step = readExample(path.join(srcDir, name))
32+
const desc = step['description.md']
33+
if (desc) {
34+
step['description.md'] = md.render(desc).html
35+
}
36+
37+
data[name] = step
38+
}
39+
return data
40+
}

src/.vitepress/config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ const fs = require('fs')
33
const path = require('path')
44
const { genApiIndex } = require('../../scripts/genApiIndex')
55
const { genExamplesData } = require('../../scripts/genExamplesData')
6-
const { headerPlugin } = require('./header')
6+
const { genTutorialData } = require('../../scripts/genTutorialData')
7+
const { headerPlugin } = require('./headerMdPlugin')
78

89
const nav = [
910
{
@@ -460,11 +461,11 @@ const sidebar = {
460461
text: 'Tutorial',
461462
items: [
462463
{
463-
text: '1. Hello World',
464+
text: '1. Adding Data',
464465
link: '/tutorial/#step-1'
465466
},
466467
{
467-
text: '2. Render a List',
468+
text: '2. Two-way Binding',
468469
link: '/tutorial/#step-2'
469470
}
470471
]
@@ -474,6 +475,7 @@ const sidebar = {
474475

475476
genApiIndex(sidebar['/api/'])
476477
genExamplesData()
478+
genTutorialData()
477479

478480
/**
479481
* @type {import('vitepress').UserConfig}
File renamed without changes.

src/.vitepress/theme/components/PreferenceSwitch.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
1212
const route = useRoute()
1313
const show = computed(() => /^\/(guide|tutorial|examples)\//.test(route.path))
14+
const showSFC = computed(() => !/^\/guide/.test(route.path))
1415
const isOpen = ref(
1516
typeof localStorage !== 'undefined' &&
1617
!localStorage.getItem(preferCompositionKey)
@@ -79,7 +80,7 @@ function useToggleFn(
7980
@click="closeSideBar"
8081
>?</a>
8182
</div>
82-
<div class="switch-container" v-if="route.path.startsWith('/examples')">
83+
<div class="switch-container" v-if="showSFC">
8384
<label class="no-sfc-label" @click="toggleSFC(false)">HTML</label>
8485
<VTSwitch
8586
class="sfc-switch"

src/examples/ExampleRepl.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Repl, ReplStore } from '@vue/repl'
33
import '@vue/repl/style.css'
44
import data from './data.json'
55
import { inject, watchEffect, version, Ref } from 'vue'
6-
import { resolveSFCExample, resolveNoBuildExample } from './utils'
6+
import { resolveSFCExample, resolveNoBuildExample, onHashChange } from './utils'
77
88
const store = new ReplStore({
99
defaultVueRuntimeURL: `https://unpkg.com/vue@${version}/dist/vue.esm-browser.js`
@@ -13,7 +13,7 @@ const preferComposition = inject('prefer-composition') as Ref<boolean>
1313
const preferSFC = inject('prefer-sfc') as Ref<boolean>
1414
1515
watchEffect(updateExample)
16-
window.addEventListener('hashchange', updateExample)
16+
onHashChange(updateExample)
1717
1818
/**
1919
* We perform some runtime logic to transform source files into different

src/examples/utils.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { onBeforeUnmount } from 'vue'
2+
13
type ExampleData = {
24
[key: string]: Record<string, string>
35
} & {
@@ -66,7 +68,7 @@ function forEachComponent(
6668
) {
6769
for (const filename in raw) {
6870
const content = raw[filename]
69-
if (filename === 'description.txt') {
71+
if (filename === 'description.txt' || filename === 'description.md') {
7072
continue
7173
} else if (typeof content === 'string') {
7274
files[filename] = content
@@ -93,14 +95,18 @@ function injectCreateApp(src: string): string {
9395
return src.replace(/export default ({[^]*\n})/, "createApp($1).mount('#app')")
9496
}
9597

96-
export function resolveSFCExample(raw: ExampleData, preferComposition: boolean) {
98+
export function resolveSFCExample(
99+
raw: ExampleData,
100+
preferComposition: boolean
101+
) {
97102
const files: Record<string, string> = {}
98103
forEachComponent(
99104
raw,
100105
files,
101106
(filename, { template, composition, options, style }) => {
107+
const desc = raw['description.txt']
102108
let sfcContent =
103-
filename === 'App' ? `<!--\n${raw['description.txt']}\n-->\n\n` : ``
109+
desc && filename === 'App' ? `<!--\n${desc}\n-->\n\n` : ``
104110
if (preferComposition) {
105111
sfcContent += `<script setup>\n${toScriptSetup(
106112
composition,
@@ -119,10 +125,14 @@ export function resolveSFCExample(raw: ExampleData, preferComposition: boolean)
119125
return files
120126
}
121127

122-
export function resolveNoBuildExample(raw: ExampleData, preferComposition: boolean) {
128+
export function resolveNoBuildExample(
129+
raw: ExampleData,
130+
preferComposition: boolean
131+
) {
123132
const files: Record<string, string> = {}
124133

125-
let html = `<!--\n${raw['description.txt']}\n-->\n\n`
134+
const desc = raw['description.txt']
135+
let html = desc ? `<!--\n${desc})}\n-->\n\n` : ``
126136
let css = ''
127137

128138
// set it first for ordering
@@ -140,7 +150,7 @@ export function resolveNoBuildExample(raw: ExampleData, preferComposition: boole
140150

141151
if (filename === 'App') {
142152
html += `<script type="module">\n${injectCreateApp(js)}<\/script>`
143-
html += `\n\n<div id="app">\n${_template}</div>`
153+
html += `\n\n<div id="app">\n${_template}\n</div>`
144154
} else {
145155
// html += `\n\n<template id="${filename}">\n${_template}</template>`
146156
js = js.replace(
@@ -158,4 +168,9 @@ export function resolveNoBuildExample(raw: ExampleData, preferComposition: boole
158168
return files
159169
}
160170

161-
171+
export function onHashChange(cb) {
172+
window.addEventListener('hashchange', cb)
173+
onBeforeUnmount(() => {
174+
window.removeEventListener('hashchange', cb)
175+
})
176+
}

src/tutorial/TutorialRepl.vue

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
22
import { Repl, ReplStore } from '@vue/repl'
3-
import { inject, watchEffect, version, Ref } from 'vue'
3+
import { inject, watch, version, Ref, ref, computed } from 'vue'
44
import data from './data.json'
5-
import { resolveSFCExample, resolveNoBuildExample } from '../examples/utils'
5+
import { resolveSFCExample, resolveNoBuildExample, onHashChange } from '../examples/utils'
66
import '@vue/repl/style.css'
77
88
const store = new ReplStore({
@@ -12,33 +12,80 @@ const store = new ReplStore({
1212
const preferComposition = inject('prefer-composition') as Ref<boolean>
1313
const preferSFC = inject('prefer-sfc') as Ref<boolean>
1414
15+
const currentStep = ref('')
16+
17+
const currentDescription = computed(() => {
18+
return data[currentStep.value]?.['description.md']
19+
})
20+
21+
const nextStep = computed(() => {
22+
const next = `step-${parseInt(currentStep.value, 10) + 1}`
23+
if (data.hasOwnProperty(next)) {
24+
return next
25+
}
26+
})
27+
28+
const userEditedState = ref<object | null>(null)
29+
const buttonText = computed(() => (userEditedState.value ? 'Reset' : 'Show me!'))
30+
1531
function updateExample() {
32+
console.log('Update example triggered')
1633
let hash = location.hash.slice(1)
1734
if (!data.hasOwnProperty(hash)) {
18-
hash = 'hello-world'
35+
hash = 'step-1'
1936
location.hash = `#${hash}`
2037
}
38+
currentStep.value = hash
39+
40+
const content = userEditedState.value ? data[nextStep.value] : data[hash]
41+
2142
store.setFiles(
2243
preferSFC.value
23-
? resolveSFCExample(data[hash], preferComposition.value)
24-
: resolveNoBuildExample(data[hash], preferComposition.value),
44+
? resolveSFCExample(content, preferComposition.value)
45+
: resolveNoBuildExample(content, preferComposition.value),
2546
preferSFC.value ? 'App.vue' : 'index.html'
2647
)
2748
}
2849
29-
watchEffect(updateExample)
30-
window.addEventListener('hashchange', updateExample)
50+
function toggleResult() {
51+
if (userEditedState.value) {
52+
store.setFiles(userEditedState.value)
53+
userEditedState.value = null
54+
} else {
55+
userEditedState.value = store.getFiles()
56+
updateExample()
57+
}
58+
}
59+
60+
watch([preferComposition, preferSFC], () => {
61+
userEditedState.value = null
62+
updateExample()
63+
})
64+
65+
onHashChange(() => {
66+
userEditedState.value = null
67+
updateExample()
68+
})
69+
70+
updateExample()
3171
</script>
3272

3373
<template>
34-
<div class="tutorial">
35-
<div class="instruction">
36-
<h1>1. Hello World</h1>
37-
<p>Let's do get something on the screen!</p>
38-
<p>Next Step &gt;</p>
39-
</div>
40-
<Repl :store="store" :showCompileOutput="false" />
41-
</div>
74+
<section class="tutorial">
75+
<article class="instruction">
76+
<div class="vt-doc" v-html="currentDescription"></div>
77+
<footer class="footer">
78+
<button @click="toggleResult">{{ buttonText }}</button>
79+
<a v-if="nextStep" :href="`#${nextStep}`">Next Step &gt;</a>
80+
</footer>
81+
</article>
82+
<Repl
83+
:store="store"
84+
:showCompileOutput="false"
85+
:clearConsole="false"
86+
:showImportMap="false"
87+
/>
88+
</section>
4289
</template>
4390

4491
<style scoped>
@@ -76,4 +123,9 @@ window.addEventListener('hashchange', updateExample)
76123
height: calc(100vh - var(--vp-nav-height) - var(--ins-height) - 48px);
77124
}
78125
}
126+
127+
.vt-doc :deep(h1) {
128+
font-size: 1.5em;
129+
margin-bottom: 1em;
130+
}
79131
</style>

src/tutorial/data.json

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ref } from 'vue'
2+
3+
export default {
4+
name: 'App',
5+
setup() {}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
name: 'App',
3+
data() {
4+
return {}
5+
}
6+
}

0 commit comments

Comments
 (0)