@curio-sd/e-module-builder 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.mjs +43 -3
- package/package.json +1 -1
- package/src/js/exercises/exercise-runner.js +4 -1
- package/src/js/exercises/load-exercise.js +3 -143
- package/src/js/theory.js +0 -2
- package/vite.config.js +3 -1
package/build.mjs
CHANGED
|
@@ -26,6 +26,44 @@ const marked = new Marked(
|
|
|
26
26
|
|
|
27
27
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
+
function rewriteAssetPaths(html, basePath) {
|
|
30
|
+
if (!basePath || !html) return html
|
|
31
|
+
const prefix = `../${basePath}/`
|
|
32
|
+
html = html.replace(
|
|
33
|
+
/(<img\s[^>]*\bsrc=")(?!https?:\/\/|\/|data:|\.\.)([^"]+)(")/g,
|
|
34
|
+
`$1${prefix}$2$3`
|
|
35
|
+
)
|
|
36
|
+
html = html.replace(
|
|
37
|
+
/(<a\s[^>]*\bhref=")(?!https?:\/\/|\/|#|mailto:|\.\.)([^"]+)(")/g,
|
|
38
|
+
`$1${prefix}$2$3`
|
|
39
|
+
)
|
|
40
|
+
return html
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function copyStaticAssets() {
|
|
44
|
+
const PUBLIC_DIR = path.join(PROJECT_DIR, 'public')
|
|
45
|
+
|
|
46
|
+
const pkgPublic = path.join(PKG_DIR, 'public')
|
|
47
|
+
if (fs.existsSync(pkgPublic)) {
|
|
48
|
+
fs.cpSync(pkgPublic, PUBLIC_DIR, { recursive: true })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function walkAndCopy(srcDir, relPath) {
|
|
52
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
53
|
+
const srcPath = path.join(srcDir, entry.name)
|
|
54
|
+
const destRel = relPath ? `${relPath}/${entry.name}` : entry.name
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
walkAndCopy(srcPath, destRel)
|
|
57
|
+
} else if (!entry.name.endsWith('.md') && !entry.name.endsWith('.html')) {
|
|
58
|
+
const destPath = path.join(PUBLIC_DIR, destRel)
|
|
59
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
|
60
|
+
fs.copyFileSync(srcPath, destPath)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walkAndCopy(CONTENT, '')
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
function readMd(filePath) {
|
|
30
68
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
31
69
|
return matter(raw)
|
|
@@ -91,7 +129,7 @@ for (const weekDir of activeWeeks) {
|
|
|
91
129
|
title: theoryMd.data.title,
|
|
92
130
|
goal: theoryMd.data.goal,
|
|
93
131
|
accent: theoryMd.data.accent,
|
|
94
|
-
html: marked.parse(theoryMd.content ?? ''),
|
|
132
|
+
html: rewriteAssetPaths(marked.parse(theoryMd.content ?? ''), `week${weekNum}`),
|
|
95
133
|
}
|
|
96
134
|
writeJson(SRC_DATA, `theory-week${weekNum}.json`, theoryOut)
|
|
97
135
|
|
|
@@ -113,7 +151,7 @@ for (const weekDir of activeWeeks) {
|
|
|
113
151
|
const exercises = exerciseFiles.map(f => {
|
|
114
152
|
const ex = readMd(path.join(exDir, f)).data
|
|
115
153
|
if (!ex.type || ex.type === 'text') {
|
|
116
|
-
ex.descriptionHtml = marked.parse(ex.description ?? '')
|
|
154
|
+
ex.descriptionHtml = rewriteAssetPaths(marked.parse(ex.description ?? ''), `week${weekNum}/exercises`)
|
|
117
155
|
}
|
|
118
156
|
return ex
|
|
119
157
|
})
|
|
@@ -132,7 +170,7 @@ for (const weekDir of activeWeeks) {
|
|
|
132
170
|
week: hwMd.data.week ?? weekNum,
|
|
133
171
|
title: hwMd.data.title,
|
|
134
172
|
subtitle: hwMd.data.subtitle ?? '',
|
|
135
|
-
html: marked.parse(hwMd.content ?? ''),
|
|
173
|
+
html: rewriteAssetPaths(marked.parse(hwMd.content ?? ''), `week${weekNum}`),
|
|
136
174
|
deliverables: hwMd.data.deliverables ?? [],
|
|
137
175
|
criteria: hwMd.data.criteria ?? [],
|
|
138
176
|
maxPoints: hwMd.data.maxPoints ?? 0,
|
|
@@ -332,4 +370,6 @@ for (const f of htmlFiles) {
|
|
|
332
370
|
const indexTpl = fs.readFileSync(path.join(PKG_DIR, 'templates/index.html'), 'utf8')
|
|
333
371
|
fs.writeFileSync(path.join(PROJECT_DIR, 'index.html'), applyTemplate(indexTpl, { pageTitle: mod.name }))
|
|
334
372
|
|
|
373
|
+
copyStaticAssets()
|
|
374
|
+
|
|
335
375
|
console.log(`Build complete: ${weekCount} weeks → src/data/ and pages/`)
|
package/package.json
CHANGED
|
@@ -66,6 +66,9 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
|
|
|
66
66
|
|
|
67
67
|
if (!containerInput || !preview) return
|
|
68
68
|
|
|
69
|
+
const outerClassMatch = exercise.previewHtml.match(/<\w+[^>]*\bclass="([^"]*)"/)
|
|
70
|
+
const gridClass = (outerClassMatch?.[1] ?? '').split(/\s+/)[0] || 'grid-container'
|
|
71
|
+
|
|
69
72
|
const updatePreview = () => {
|
|
70
73
|
const areas = containerInput.value.trim()
|
|
71
74
|
const itemStyles = Array.from(selects)
|
|
@@ -74,7 +77,7 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
|
|
|
74
77
|
|
|
75
78
|
preview.innerHTML = `
|
|
76
79
|
<style>
|
|
77
|
-
|
|
80
|
+
.${gridClass} {
|
|
78
81
|
display: grid;
|
|
79
82
|
grid-template-columns: ${exercise.gridColumns || '1fr 1fr 1fr'};
|
|
80
83
|
grid-template-areas: ${areas || '"a b c"'};
|
|
@@ -1,108 +1,57 @@
|
|
|
1
1
|
import { renderExerciseMeta, markExerciseSolved } from './exercise-shared.js'
|
|
2
2
|
import { sitePath } from '../site-path.js'
|
|
3
|
-
|
|
4
3
|
import { initExternalExercise } from './external-exercise.js'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
5
|
async function loadWeekData(weekNum) {
|
|
9
|
-
|
|
10
6
|
return import(`../../data/exercises/week${weekNum}.json`).then((m) => m.default)
|
|
11
|
-
|
|
12
7
|
}
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
9
|
function isExternalMode(weekData, exercise) {
|
|
17
|
-
|
|
18
10
|
return weekData?.mode === 'external' || exercise?.type === 'external'
|
|
19
|
-
|
|
20
11
|
}
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
13
|
function showMissingContent(weekNum) {
|
|
25
|
-
|
|
26
14
|
const panel = document.querySelector('[data-exercise-content]')
|
|
27
15
|
|
|
28
16
|
panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
|
|
29
|
-
|
|
30
17
|
panel?.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
31
|
-
|
|
32
18
|
panel?.querySelector('[data-hint]')?.closest('.mb-4')?.classList.add('hidden')
|
|
33
|
-
|
|
34
19
|
panel?.insertAdjacentHTML(
|
|
35
|
-
|
|
36
20
|
'beforeend',
|
|
37
|
-
|
|
38
21
|
`<div class="card border-l-4 border-l-amber-500">
|
|
39
|
-
|
|
40
22
|
<p class="font-medium text-zinc-900">Content ontbreekt</p>
|
|
41
|
-
|
|
42
|
-
<p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar is nog niet gegenereerd. Run in een terminal:</p>
|
|
43
|
-
|
|
44
|
-
<pre class="code-block mt-3 text-sm"><code>npm run generate-content</code></pre>
|
|
45
|
-
|
|
46
|
-
<p class="mt-2 text-sm text-zinc-500">Herlaad daarna deze pagina.</p>
|
|
47
|
-
|
|
23
|
+
<p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar de inhoud is niet beschikbaar.</p>
|
|
48
24
|
</div>`
|
|
49
|
-
|
|
50
25
|
)
|
|
51
|
-
|
|
52
26
|
}
|
|
53
27
|
|
|
54
|
-
|
|
55
|
-
|
|
56
28
|
export async function initExercisePage(weekNum) {
|
|
57
|
-
|
|
58
29
|
const params = new URLSearchParams(window.location.search)
|
|
59
|
-
|
|
60
30
|
const id = parseInt(params.get('id') || '1', 10)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
31
|
let weekData
|
|
65
32
|
|
|
66
33
|
try {
|
|
67
|
-
|
|
68
34
|
weekData = await loadWeekData(weekNum)
|
|
69
|
-
|
|
70
35
|
} catch {
|
|
71
|
-
|
|
72
36
|
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
73
|
-
|
|
74
37
|
'beforeend',
|
|
75
|
-
|
|
76
38
|
`<p class="text-red-600">Oefeningdata voor week ${weekNum} niet gevonden.</p>`
|
|
77
|
-
|
|
78
39
|
)
|
|
79
40
|
|
|
80
41
|
return
|
|
81
|
-
|
|
82
42
|
}
|
|
83
43
|
|
|
84
|
-
|
|
85
|
-
|
|
86
44
|
const exercise = weekData?.exercises?.find((e) => e.id === id)
|
|
87
45
|
|
|
88
|
-
|
|
89
|
-
|
|
90
46
|
if (!exercise) {
|
|
91
|
-
|
|
92
47
|
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
93
|
-
|
|
94
48
|
'beforeend',
|
|
95
|
-
|
|
96
49
|
'<p class="text-red-600">Oefening niet gevonden.</p>'
|
|
97
|
-
|
|
98
50
|
)
|
|
99
51
|
|
|
100
52
|
return
|
|
101
|
-
|
|
102
53
|
}
|
|
103
54
|
|
|
104
|
-
|
|
105
|
-
|
|
106
55
|
if (!exercise.type || exercise.type === 'text') {
|
|
107
56
|
const panel = document.querySelector('[data-exercise-content]')
|
|
108
57
|
panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
|
|
@@ -121,189 +70,100 @@ export async function initExercisePage(weekNum) {
|
|
|
121
70
|
}
|
|
122
71
|
|
|
123
72
|
if (isExternalMode(weekData, exercise)) {
|
|
124
|
-
|
|
125
73
|
const missing = !exercise.task?.trim() && !exercise.description?.trim()
|
|
126
74
|
|
|
127
75
|
if (missing) {
|
|
128
|
-
|
|
129
76
|
showMissingContent(weekNum)
|
|
130
|
-
|
|
131
77
|
return
|
|
132
|
-
|
|
133
78
|
}
|
|
134
79
|
|
|
135
80
|
initExternalExercise(exercise, weekNum, {
|
|
136
|
-
|
|
137
81
|
onSolved: () => markExerciseSolved(weekNum, exercise.id),
|
|
138
|
-
|
|
139
82
|
})
|
|
140
83
|
|
|
141
84
|
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
142
|
-
|
|
143
85
|
return
|
|
144
|
-
|
|
145
86
|
}
|
|
146
87
|
|
|
147
|
-
|
|
148
|
-
|
|
149
88
|
renderExerciseMeta(exercise)
|
|
150
89
|
|
|
151
|
-
|
|
152
|
-
|
|
153
90
|
const missingContent =
|
|
154
|
-
|
|
155
|
-
!exercise.starterCss?.trim() ||
|
|
156
|
-
|
|
91
|
+
(exercise.type === 'areas' ? !exercise.starterAreas?.trim() : !exercise.starterCss?.trim()) ||
|
|
157
92
|
!exercise.previewHtml?.trim() ||
|
|
158
|
-
|
|
159
93
|
(exercise.type !== 'areas' && !exercise.checks?.length)
|
|
160
94
|
|
|
161
|
-
|
|
162
|
-
|
|
163
95
|
if (missingContent) {
|
|
164
|
-
|
|
165
96
|
showMissingContent(weekNum)
|
|
166
|
-
|
|
167
97
|
return
|
|
168
|
-
|
|
169
98
|
}
|
|
170
99
|
|
|
171
|
-
|
|
172
|
-
|
|
173
100
|
const {
|
|
174
|
-
|
|
175
101
|
initCssPlayground,
|
|
176
|
-
|
|
177
102
|
initAreasExercise,
|
|
178
|
-
|
|
179
103
|
initResponsiveExercise,
|
|
180
|
-
|
|
104
|
+
renderAreaSelects,
|
|
181
105
|
} = await import('./exercise-runner.js')
|
|
182
106
|
|
|
183
|
-
|
|
184
|
-
|
|
185
107
|
document.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
186
|
-
|
|
187
108
|
document.querySelector('[data-exercise-interactive]')?.classList.remove('hidden')
|
|
188
109
|
|
|
189
|
-
|
|
190
|
-
|
|
191
110
|
const cssPanel = document.querySelector('[data-exercise-css-panel]')
|
|
192
|
-
|
|
193
111
|
const areasPanel = document.querySelector('[data-exercise-areas-panel]')
|
|
194
|
-
|
|
195
112
|
const responsiveBar = document.querySelector('[data-responsive-bar]')
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
113
|
const onSolved = () => markExerciseSolved(weekNum, exercise.id)
|
|
200
114
|
|
|
201
|
-
|
|
202
|
-
|
|
203
115
|
if (exercise.type === 'areas') {
|
|
204
|
-
|
|
205
116
|
cssPanel?.classList.add('hidden')
|
|
206
|
-
|
|
207
117
|
areasPanel?.classList.remove('hidden')
|
|
208
|
-
|
|
209
118
|
responsiveBar?.classList.add('hidden')
|
|
210
|
-
|
|
211
119
|
document.querySelector('[data-solution]')?.classList.add('hidden')
|
|
212
120
|
|
|
213
|
-
|
|
214
|
-
|
|
215
121
|
const containerInput = document.querySelector('[data-areas-container]')
|
|
216
122
|
|
|
217
123
|
if (containerInput) containerInput.value = exercise.starterAreas || ''
|
|
218
124
|
|
|
219
|
-
|
|
220
|
-
|
|
221
125
|
const selectsEl = document.querySelector('[data-area-selects]')
|
|
222
126
|
|
|
223
127
|
if (selectsEl) {
|
|
224
|
-
|
|
225
128
|
selectsEl.innerHTML = renderAreaSelects(exercise.areaItems, exercise.areaOptions)
|
|
226
|
-
|
|
227
129
|
}
|
|
228
130
|
|
|
229
|
-
|
|
230
|
-
|
|
231
131
|
initAreasExercise(exercise, { onSolved })
|
|
232
|
-
|
|
233
132
|
} else {
|
|
234
|
-
|
|
235
133
|
areasPanel?.classList.add('hidden')
|
|
236
|
-
|
|
237
134
|
cssPanel?.classList.remove('hidden')
|
|
238
135
|
|
|
239
|
-
|
|
240
|
-
|
|
241
136
|
if (exercise.type === 'responsive') {
|
|
242
|
-
|
|
243
137
|
responsiveBar?.classList.remove('hidden')
|
|
244
|
-
|
|
245
138
|
initResponsiveExercise(exercise, { onSolved })
|
|
246
|
-
|
|
247
139
|
} else {
|
|
248
|
-
|
|
249
140
|
responsiveBar?.classList.add('hidden')
|
|
250
|
-
|
|
251
141
|
initCssPlayground(exercise, { onSolved })
|
|
252
|
-
|
|
253
142
|
}
|
|
254
|
-
|
|
255
143
|
}
|
|
256
144
|
|
|
257
|
-
|
|
258
|
-
|
|
259
145
|
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
260
|
-
|
|
261
146
|
}
|
|
262
147
|
|
|
263
|
-
|
|
264
|
-
|
|
265
148
|
function renderNavButtons(week, currentId, total) {
|
|
266
|
-
|
|
267
149
|
const prev = document.querySelector('[data-prev-exercise]')
|
|
268
|
-
|
|
269
150
|
const next = document.querySelector('[data-next-exercise]')
|
|
270
151
|
|
|
271
|
-
|
|
272
|
-
|
|
273
152
|
if (prev) {
|
|
274
|
-
|
|
275
153
|
if (currentId > 1) {
|
|
276
|
-
|
|
277
154
|
prev.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId - 1}`
|
|
278
|
-
|
|
279
155
|
prev.classList.remove('hidden')
|
|
280
|
-
|
|
281
156
|
} else {
|
|
282
|
-
|
|
283
157
|
prev.classList.add('hidden')
|
|
284
|
-
|
|
285
158
|
}
|
|
286
|
-
|
|
287
159
|
}
|
|
288
160
|
|
|
289
|
-
|
|
290
|
-
|
|
291
161
|
if (next) {
|
|
292
|
-
|
|
293
162
|
if (currentId < total) {
|
|
294
|
-
|
|
295
163
|
next.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId + 1}`
|
|
296
|
-
|
|
297
164
|
next.classList.remove('hidden')
|
|
298
|
-
|
|
299
165
|
} else {
|
|
300
|
-
|
|
301
166
|
next.classList.add('hidden')
|
|
302
|
-
|
|
303
167
|
}
|
|
304
|
-
|
|
305
168
|
}
|
|
306
|
-
|
|
307
169
|
}
|
|
308
|
-
|
|
309
|
-
|
package/src/js/theory.js
CHANGED
|
@@ -15,7 +15,6 @@ export async function initTheory(week) {
|
|
|
15
15
|
} catch {
|
|
16
16
|
container.innerHTML = `
|
|
17
17
|
<p class="text-red-600">Theorie voor week ${week} niet gevonden.</p>
|
|
18
|
-
<p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
|
|
19
18
|
`
|
|
20
19
|
return
|
|
21
20
|
}
|
|
@@ -23,7 +22,6 @@ export async function initTheory(week) {
|
|
|
23
22
|
if (!data?.html) {
|
|
24
23
|
container.innerHTML = `
|
|
25
24
|
<p class="text-amber-700">Theorie voor week ${week} is nog leeg.</p>
|
|
26
|
-
<p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
|
|
27
25
|
`
|
|
28
26
|
return
|
|
29
27
|
}
|
package/vite.config.js
CHANGED
|
@@ -9,7 +9,9 @@ export function createConfig({ projectDir, pkgDir }) {
|
|
|
9
9
|
return defineConfig({
|
|
10
10
|
root: projectDir,
|
|
11
11
|
base: './',
|
|
12
|
-
publicDir: path.join(
|
|
12
|
+
publicDir: existsSync(path.join(projectDir, 'public'))
|
|
13
|
+
? path.join(projectDir, 'public')
|
|
14
|
+
: path.join(pkgDir, 'public'),
|
|
13
15
|
plugins: [
|
|
14
16
|
htmlIncludes({ partialsDir: path.join(pkgDir, 'src', 'partials') }),
|
|
15
17
|
tailwindcss(),
|