@curio-sd/e-module-builder 0.3.1 → 0.5.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/README.md +57 -15
- package/bin/cli.js +2 -0
- package/build-pdf.mjs +452 -0
- package/build.mjs +168 -75
- package/package.json +3 -1
- package/src/css/main.css +124 -50
- package/src/js/checklist.js +1 -1
- package/src/js/exercises/exercise-runner.js +4 -1
- package/src/js/exercises/exercise-shared.js +1 -1
- package/src/js/exercises/external-exercise.js +3 -4
- package/src/js/exercises/hub.js +5 -1
- package/src/js/exercises/load-exercise.js +5 -142
- package/src/js/exercises/theory-panel.js +95 -0
- package/src/js/home.js +1 -1
- package/src/js/nav.js +63 -8
- package/src/js/theory.js +0 -2
- package/templates/index.html +17 -9
- package/templates/pages/inleveropdracht.html +32 -0
- package/templates/pages/meetmoment-praktijk.html +35 -0
- package/templates/pages/meetmoment-theorie.html +29 -0
- package/templates/pages/{toets.html → meetmoment.html} +11 -4
- package/templates/pages/oefening.html +29 -0
- package/templates/pages/oefeningen.html +46 -38
- package/templates/pages/theorie.html +6 -1
- package/vite.config.js +3 -1
- package/templates/pages/toets-praktijk.html +0 -25
- package/templates/pages/toets-theorie.html +0 -25
package/build.mjs
CHANGED
|
@@ -24,8 +24,52 @@ const marked = new Marked(
|
|
|
24
24
|
})
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
const SECTION_RE = /^([a-zA-Z]+)(\d+)$/
|
|
28
|
+
|
|
29
|
+
function sectionLabel(prefix, num) {
|
|
30
|
+
return `${prefix.charAt(0).toUpperCase()}${prefix.slice(1)} ${num}`
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
28
34
|
|
|
35
|
+
function rewriteAssetPaths(html, basePath) {
|
|
36
|
+
if (!basePath || !html) return html
|
|
37
|
+
const prefix = `../${basePath}/`
|
|
38
|
+
html = html.replace(
|
|
39
|
+
/(<img\s[^>]*\bsrc=")(?!https?:\/\/|\/|data:|\.\.)([^"]+)(")/g,
|
|
40
|
+
`$1${prefix}$2$3`
|
|
41
|
+
)
|
|
42
|
+
html = html.replace(
|
|
43
|
+
/(<a\s[^>]*\bhref=")(?!https?:\/\/|\/|#|mailto:|\.\.)([^"]+)(")/g,
|
|
44
|
+
`$1${prefix}$2$3`
|
|
45
|
+
)
|
|
46
|
+
return html
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyStaticAssets() {
|
|
50
|
+
const PUBLIC_DIR = path.join(PROJECT_DIR, 'public')
|
|
51
|
+
|
|
52
|
+
const pkgPublic = path.join(PKG_DIR, 'public')
|
|
53
|
+
if (fs.existsSync(pkgPublic)) {
|
|
54
|
+
fs.cpSync(pkgPublic, PUBLIC_DIR, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walkAndCopy(srcDir, relPath) {
|
|
58
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
59
|
+
const srcPath = path.join(srcDir, entry.name)
|
|
60
|
+
const destRel = relPath ? `${relPath}/${entry.name}` : entry.name
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
walkAndCopy(srcPath, destRel)
|
|
63
|
+
} else if (!entry.name.endsWith('.md') && !entry.name.endsWith('.html')) {
|
|
64
|
+
const destPath = path.join(PUBLIC_DIR, destRel)
|
|
65
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
|
66
|
+
fs.copyFileSync(srcPath, destPath)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walkAndCopy(CONTENT, '')
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
function readMd(filePath) {
|
|
30
74
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
31
75
|
return matter(raw)
|
|
@@ -70,8 +114,8 @@ const mod = moduleMd.data
|
|
|
70
114
|
|
|
71
115
|
const weekDirs = fs
|
|
72
116
|
.readdirSync(CONTENT)
|
|
73
|
-
.filter(d =>
|
|
74
|
-
.sort((a, b) => parseInt(a.
|
|
117
|
+
.filter(d => SECTION_RE.test(d) && fs.statSync(path.join(CONTENT, d)).isDirectory())
|
|
118
|
+
.sort((a, b) => parseInt(a.match(SECTION_RE)[2]) - parseInt(b.match(SECTION_RE)[2]))
|
|
75
119
|
|
|
76
120
|
const weekCount = mod.weeks > 0 ? mod.weeks : weekDirs.length
|
|
77
121
|
const activeWeeks = weekDirs.slice(0, weekCount)
|
|
@@ -81,7 +125,8 @@ const activeWeeks = weekDirs.slice(0, weekCount)
|
|
|
81
125
|
const weeksData = []
|
|
82
126
|
|
|
83
127
|
for (const weekDir of activeWeeks) {
|
|
84
|
-
const
|
|
128
|
+
const [, sectionPrefix, sectionNumStr] = weekDir.match(SECTION_RE)
|
|
129
|
+
const weekNum = parseInt(sectionNumStr)
|
|
85
130
|
const dir = path.join(CONTENT, weekDir)
|
|
86
131
|
|
|
87
132
|
// theory.md → src/data/theory-weekN.json
|
|
@@ -91,18 +136,18 @@ for (const weekDir of activeWeeks) {
|
|
|
91
136
|
title: theoryMd.data.title,
|
|
92
137
|
goal: theoryMd.data.goal,
|
|
93
138
|
accent: theoryMd.data.accent,
|
|
94
|
-
html: marked.parse(theoryMd.content ?? ''),
|
|
139
|
+
html: rewriteAssetPaths(marked.parse(theoryMd.content ?? ''), `week${weekNum}`),
|
|
95
140
|
}
|
|
96
141
|
writeJson(SRC_DATA, `theory-week${weekNum}.json`, theoryOut)
|
|
97
142
|
|
|
98
|
-
// quiz.md → src/data/
|
|
143
|
+
// quiz.md → src/data/meetmoment-quiz-weekN.json
|
|
99
144
|
const quizMd = readMd(path.join(dir, 'quiz.md'))
|
|
100
145
|
const quizOut = {
|
|
101
146
|
title: quizMd.data.title,
|
|
102
147
|
passScore: quizMd.data.passScore ?? 70,
|
|
103
148
|
questions: quizMd.data.questions ?? [],
|
|
104
149
|
}
|
|
105
|
-
writeJson(SRC_DATA, `
|
|
150
|
+
writeJson(SRC_DATA, `meetmoment-quiz-week${weekNum}.json`, quizOut)
|
|
106
151
|
|
|
107
152
|
// exercises/ subfolder → src/data/exercises/weekN.json
|
|
108
153
|
const exDir = path.join(dir, 'exercises')
|
|
@@ -111,9 +156,16 @@ for (const weekDir of activeWeeks) {
|
|
|
111
156
|
.filter(f => f.endsWith('.md') && f !== '_meta.md')
|
|
112
157
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
|
113
158
|
const exercises = exerciseFiles.map(f => {
|
|
114
|
-
const ex = readMd(path.join(exDir, f))
|
|
159
|
+
const { data: ex, content } = readMd(path.join(exDir, f))
|
|
115
160
|
if (!ex.type || ex.type === 'text') {
|
|
116
|
-
|
|
161
|
+
const src = content?.trim() ? content : (ex.description ?? '')
|
|
162
|
+
ex.descriptionHtml = rewriteAssetPaths(marked.parse(src), `week${weekNum}/exercises`)
|
|
163
|
+
}
|
|
164
|
+
ex.descriptionInlineHtml = marked.parseInline(ex.description ?? '')
|
|
165
|
+
if (ex.type === 'external') {
|
|
166
|
+
if (ex.task) ex.taskHtml = marked.parseInline(ex.task)
|
|
167
|
+
if (ex.hint) ex.hintHtml = marked.parse(ex.hint)
|
|
168
|
+
if (ex.solution) ex.solutionHtml = marked.parse(ex.solution)
|
|
117
169
|
}
|
|
118
170
|
return ex
|
|
119
171
|
})
|
|
@@ -132,32 +184,78 @@ for (const weekDir of activeWeeks) {
|
|
|
132
184
|
week: hwMd.data.week ?? weekNum,
|
|
133
185
|
title: hwMd.data.title,
|
|
134
186
|
subtitle: hwMd.data.subtitle ?? '',
|
|
135
|
-
html: marked.parse(hwMd.content ?? ''),
|
|
187
|
+
html: rewriteAssetPaths(marked.parse(hwMd.content ?? ''), `week${weekNum}`),
|
|
136
188
|
deliverables: hwMd.data.deliverables ?? [],
|
|
137
189
|
criteria: hwMd.data.criteria ?? [],
|
|
138
190
|
maxPoints: hwMd.data.maxPoints ?? 0,
|
|
139
191
|
tips: hwMd.data.tips ?? [],
|
|
192
|
+
...(hwMd.data.linked_theory ? { linked_theory: hwMd.data.linked_theory } : {}),
|
|
140
193
|
}
|
|
141
194
|
writeJson(SRC_DATA, `inleveropdracht-week${weekNum}.json`, hwOut)
|
|
142
195
|
|
|
143
196
|
weeksData.push({
|
|
144
197
|
week: weekNum,
|
|
198
|
+
dirName: weekDir,
|
|
199
|
+
prefix: sectionPrefix,
|
|
145
200
|
title: theoryMd.data.title,
|
|
146
|
-
summary: theoryMd.data.summary ?? '',
|
|
201
|
+
summary: marked.parseInline(theoryMd.data.summary ?? ''),
|
|
147
202
|
goal: theoryMd.data.goal,
|
|
148
203
|
leeruitkomsten: theoryMd.data.leeruitkomsten ?? [],
|
|
149
204
|
color: theoryMd.data.accent,
|
|
150
205
|
pages: [
|
|
151
|
-
{ key: 'theorie', href: `/pages
|
|
152
|
-
{ key: 'oefeningen', href: `/pages
|
|
153
|
-
{ key: '
|
|
154
|
-
{ key: 'oefening', href: `/pages
|
|
155
|
-
{ key: 'inleveropdracht', href: `/pages
|
|
206
|
+
{ key: 'theorie', href: `/pages/${weekDir}-theorie.html`, label: 'Theorie' },
|
|
207
|
+
{ key: 'oefeningen', href: `/pages/${weekDir}-oefeningen.html`, label: 'Oefeningen' },
|
|
208
|
+
{ key: 'meetmoment', href: `/pages/${weekDir}-meetmoment.html`, label: 'Meetmoment' },
|
|
209
|
+
{ key: 'oefening', href: `/pages/${weekDir}-oefening.html`, label: 'Oefening' },
|
|
210
|
+
{ key: 'inleveropdracht', href: `/pages/${weekDir}-inleveropdracht.html`, label: 'Inleveropdracht' },
|
|
156
211
|
],
|
|
157
212
|
})
|
|
158
213
|
}
|
|
159
214
|
|
|
160
|
-
// ─── 4. manifest
|
|
215
|
+
// ─── 4. assessment data (parsed early so navLabel is available for manifest) ───────
|
|
216
|
+
|
|
217
|
+
const ASSESSMENTS_DIR = path.join(CONTENT, 'assessments')
|
|
218
|
+
|
|
219
|
+
function buildAssessmentData(filePath, fallbackTitle, fallbackNavLabel, fallbackDescription) {
|
|
220
|
+
if (fs.existsSync(filePath)) {
|
|
221
|
+
const md = readMd(filePath)
|
|
222
|
+
return {
|
|
223
|
+
title: md.data.title ?? fallbackTitle,
|
|
224
|
+
navLabel: md.data.navLabel ?? fallbackNavLabel,
|
|
225
|
+
description: marked.parseInline(md.data.description ?? fallbackDescription),
|
|
226
|
+
passScore: md.data.passScore ?? 70,
|
|
227
|
+
questions: md.data.questions ?? [],
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { title: fallbackTitle, navLabel: fallbackNavLabel, description: marked.parseInline(fallbackDescription), passScore: 70, questions: [] }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildPracticalAssessmentData(filePath, fallbackTitle, fallbackNavLabel) {
|
|
234
|
+
if (fs.existsSync(filePath)) {
|
|
235
|
+
const md = readMd(filePath)
|
|
236
|
+
return {
|
|
237
|
+
title: md.data.title ?? fallbackTitle,
|
|
238
|
+
navLabel: md.data.navLabel ?? fallbackNavLabel,
|
|
239
|
+
description: marked.parseInline(md.data.description ?? ''),
|
|
240
|
+
html: marked.parse(md.content ?? ''),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { title: fallbackTitle, navLabel: fallbackNavLabel, description: '', html: '' }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const theoryAssessmentData = buildAssessmentData(
|
|
247
|
+
path.join(ASSESSMENTS_DIR, 'theory-assessment.md'),
|
|
248
|
+
`Meetmoment theorie — ${mod.name}`,
|
|
249
|
+
'Meetmoment Theorie',
|
|
250
|
+
'Meerkeuzevragen over de module. Minimaal 70% om te slagen.'
|
|
251
|
+
)
|
|
252
|
+
const practicalAssessmentData = buildPracticalAssessmentData(
|
|
253
|
+
path.join(ASSESSMENTS_DIR, 'practical-assessment.md'),
|
|
254
|
+
`Meetmoment praktijk — ${mod.name}`,
|
|
255
|
+
'Meetmoment Praktijk'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// ─── 5. manifest.json ────────────────────────────────────────────────────────
|
|
161
259
|
|
|
162
260
|
const manifest = {
|
|
163
261
|
module: {
|
|
@@ -166,7 +264,7 @@ const manifest = {
|
|
|
166
264
|
youtube: mod.youtube ?? null,
|
|
167
265
|
weeks: weekCount,
|
|
168
266
|
language: mod.language ?? 'nl',
|
|
169
|
-
description: mod.description ?? '',
|
|
267
|
+
description: marked.parseInline(mod.description ?? ''),
|
|
170
268
|
logoAlt: mod.logoAlt ?? mod.name,
|
|
171
269
|
exerciseMode: mod.exerciseMode ?? 'external',
|
|
172
270
|
},
|
|
@@ -174,34 +272,37 @@ const manifest = {
|
|
|
174
272
|
nav: {
|
|
175
273
|
home: { href: '/index.html', label: 'Home' },
|
|
176
274
|
weeks: weeksData.map(wk => ({
|
|
177
|
-
label:
|
|
275
|
+
label: sectionLabel(wk.prefix, wk.week),
|
|
178
276
|
title: wk.title,
|
|
179
277
|
children: [
|
|
180
|
-
{ href: `/pages
|
|
181
|
-
{ href: `/pages
|
|
182
|
-
{ href: `/pages
|
|
183
|
-
{ href: `/pages
|
|
278
|
+
{ href: `/pages/${wk.dirName}-theorie.html`, label: 'Theorie' },
|
|
279
|
+
{ href: `/pages/${wk.dirName}-oefeningen.html`, label: 'Oefeningen' },
|
|
280
|
+
{ href: `/pages/${wk.dirName}-meetmoment.html`, label: 'Quiz' },
|
|
281
|
+
{ href: `/pages/${wk.dirName}-inleveropdracht.html`, label: 'Inleveropdracht' },
|
|
184
282
|
],
|
|
185
283
|
})),
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
284
|
+
assessmentSection: {
|
|
285
|
+
label: mod.assessmentSectionLabel ?? 'Afronding',
|
|
286
|
+
children: [
|
|
287
|
+
{ href: '/pages/checklist.html', label: 'Checklist' },
|
|
288
|
+
{ href: '/pages/meetmoment-theorie.html', label: theoryAssessmentData.navLabel },
|
|
289
|
+
{ href: '/pages/meetmoment-praktijk.html', label: practicalAssessmentData.navLabel },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
191
292
|
},
|
|
192
293
|
pages: {
|
|
193
294
|
static: [
|
|
194
295
|
'index.html',
|
|
195
296
|
'pages/checklist.html',
|
|
196
|
-
'pages/
|
|
197
|
-
'pages/
|
|
297
|
+
'pages/meetmoment-theorie.html',
|
|
298
|
+
'pages/meetmoment-praktijk.html',
|
|
198
299
|
],
|
|
199
300
|
week: weeksData.flatMap(wk => [
|
|
200
|
-
`pages
|
|
201
|
-
`pages
|
|
202
|
-
`pages
|
|
203
|
-
`pages
|
|
204
|
-
`pages
|
|
301
|
+
`pages/${wk.dirName}-theorie.html`,
|
|
302
|
+
`pages/${wk.dirName}-oefeningen.html`,
|
|
303
|
+
`pages/${wk.dirName}-meetmoment.html`,
|
|
304
|
+
`pages/${wk.dirName}-oefening.html`,
|
|
305
|
+
`pages/${wk.dirName}-inleveropdracht.html`,
|
|
205
306
|
]),
|
|
206
307
|
},
|
|
207
308
|
content: {
|
|
@@ -215,12 +316,13 @@ writeJson(SRC_DATA, 'manifest.json', manifest)
|
|
|
215
316
|
// ─── 5. checklist.json ───────────────────────────────────────────────────────
|
|
216
317
|
|
|
217
318
|
const checklistGroups = weeksData.map(wk => ({
|
|
218
|
-
id:
|
|
219
|
-
title:
|
|
319
|
+
id: wk.dirName,
|
|
320
|
+
title: `${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
220
321
|
color: wk.color,
|
|
221
322
|
items: (wk.leeruitkomsten ?? []).map((text, i) => ({
|
|
222
|
-
id:
|
|
323
|
+
id: `${wk.dirName}-item-${i}`,
|
|
223
324
|
text,
|
|
325
|
+
textHtml: marked.parseInline(text),
|
|
224
326
|
})),
|
|
225
327
|
}))
|
|
226
328
|
|
|
@@ -229,37 +331,16 @@ if (mod.algemeen?.length) {
|
|
|
229
331
|
id: 'algemeen',
|
|
230
332
|
title: 'Algemeen',
|
|
231
333
|
color: 'slate',
|
|
232
|
-
items: mod.algemeen.map((text, i) => ({ id: `algemeen-item-${i}`, text })),
|
|
334
|
+
items: mod.algemeen.map((text, i) => ({ id: `algemeen-item-${i}`, text, textHtml: marked.parseInline(text) })),
|
|
233
335
|
})
|
|
234
336
|
}
|
|
235
337
|
|
|
236
338
|
writeJson(SRC_DATA, 'checklist.json', { groups: checklistGroups })
|
|
237
339
|
|
|
238
|
-
// ─── 5b.
|
|
239
|
-
|
|
240
|
-
function buildExamData(filePath, fallbackTitle) {
|
|
241
|
-
if (fs.existsSync(filePath)) {
|
|
242
|
-
const md = readMd(filePath)
|
|
243
|
-
return {
|
|
244
|
-
title: md.data.title ?? fallbackTitle,
|
|
245
|
-
passScore: md.data.passScore ?? 70,
|
|
246
|
-
questions: md.data.questions ?? [],
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return { title: fallbackTitle, passScore: 70, questions: [] }
|
|
250
|
-
}
|
|
340
|
+
// ─── 5b. write assessment data JSON files ──────────────────────────────────────────
|
|
251
341
|
|
|
252
|
-
|
|
253
|
-
writeJson(
|
|
254
|
-
SRC_DATA,
|
|
255
|
-
'toets-theorie.json',
|
|
256
|
-
buildExamData(path.join(EXAMS_DIR, 'theory-exam.md'), `Eindtoets theorie — ${mod.name}`)
|
|
257
|
-
)
|
|
258
|
-
writeJson(
|
|
259
|
-
SRC_DATA,
|
|
260
|
-
'toets-praktijk.json',
|
|
261
|
-
buildExamData(path.join(EXAMS_DIR, 'practical-exam.md'), `Eindtoets praktijk — ${mod.name}`)
|
|
262
|
-
)
|
|
342
|
+
writeJson(SRC_DATA, 'meetmoment-theorie.json', theoryAssessmentData)
|
|
343
|
+
writeJson(SRC_DATA, 'meetmoment-praktijk.json', practicalAssessmentData)
|
|
263
344
|
|
|
264
345
|
// ─── 6. generate per-week page stubs ─────────────────────────────────────────
|
|
265
346
|
|
|
@@ -267,27 +348,27 @@ const PAGE_TYPES = [
|
|
|
267
348
|
{
|
|
268
349
|
tplFile: 'theorie.html',
|
|
269
350
|
suffix: 'theorie',
|
|
270
|
-
pageTitle: wk => `Theorie
|
|
351
|
+
pageTitle: wk => `Theorie ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
271
352
|
},
|
|
272
353
|
{
|
|
273
|
-
tplFile: '
|
|
274
|
-
suffix: '
|
|
275
|
-
pageTitle: wk => `
|
|
354
|
+
tplFile: 'meetmoment.html',
|
|
355
|
+
suffix: 'meetmoment',
|
|
356
|
+
pageTitle: wk => `Meetmoment ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
276
357
|
},
|
|
277
358
|
{
|
|
278
359
|
tplFile: 'oefeningen.html',
|
|
279
360
|
suffix: 'oefeningen',
|
|
280
|
-
pageTitle: wk => `Oefeningen
|
|
361
|
+
pageTitle: wk => `Oefeningen ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
281
362
|
},
|
|
282
363
|
{
|
|
283
364
|
tplFile: 'oefening.html',
|
|
284
365
|
suffix: 'oefening',
|
|
285
|
-
pageTitle: wk => `Oefening —
|
|
366
|
+
pageTitle: wk => `Oefening — ${sectionLabel(wk.prefix, wk.week)}`,
|
|
286
367
|
},
|
|
287
368
|
{
|
|
288
369
|
tplFile: 'inleveropdracht.html',
|
|
289
370
|
suffix: 'inleveropdracht',
|
|
290
|
-
pageTitle: wk => `Inleveropdracht
|
|
371
|
+
pageTitle: wk => `Inleveropdracht ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
291
372
|
},
|
|
292
373
|
]
|
|
293
374
|
|
|
@@ -302,21 +383,31 @@ for (const { tplFile, suffix, pageTitle } of PAGE_TYPES) {
|
|
|
302
383
|
weekTitle: wk.title,
|
|
303
384
|
pageTitle: pageTitle(wk),
|
|
304
385
|
})
|
|
305
|
-
fs.writeFileSync(path.join(PAGES,
|
|
386
|
+
fs.writeFileSync(path.join(PAGES, `${wk.dirName}-${suffix}.html`), out)
|
|
306
387
|
}
|
|
307
388
|
}
|
|
308
389
|
|
|
309
|
-
// ─── 7. copy static pages (checklist,
|
|
390
|
+
// ─── 7. copy static pages (checklist, assessments) with title substitution ─────────
|
|
310
391
|
|
|
311
392
|
const STATIC_PAGES = [
|
|
312
393
|
{ src: 'checklist.html', pageTitle: `Checklist — ${mod.name}` },
|
|
313
|
-
{
|
|
314
|
-
|
|
394
|
+
{
|
|
395
|
+
src: 'meetmoment-theorie.html',
|
|
396
|
+
pageTitle: `${theoryAssessmentData.navLabel} — ${mod.name}`,
|
|
397
|
+
assessmentTitle: theoryAssessmentData.navLabel,
|
|
398
|
+
assessmentDescription: theoryAssessmentData.description,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
src: 'meetmoment-praktijk.html',
|
|
402
|
+
pageTitle: `${practicalAssessmentData.navLabel} — ${mod.name}`,
|
|
403
|
+
assessmentTitle: practicalAssessmentData.navLabel,
|
|
404
|
+
assessmentDescription: practicalAssessmentData.description,
|
|
405
|
+
},
|
|
315
406
|
]
|
|
316
407
|
|
|
317
|
-
for (const { src, pageTitle } of STATIC_PAGES) {
|
|
408
|
+
for (const { src, pageTitle, assessmentTitle, assessmentDescription } of STATIC_PAGES) {
|
|
318
409
|
const tpl = fs.readFileSync(path.join(TEMPLATES, src), 'utf8')
|
|
319
|
-
const out = applyTemplate(tpl, { pageTitle })
|
|
410
|
+
const out = applyTemplate(tpl, { pageTitle, assessmentTitle, assessmentDescription })
|
|
320
411
|
fs.writeFileSync(path.join(PAGES, src), out)
|
|
321
412
|
}
|
|
322
413
|
|
|
@@ -332,4 +423,6 @@ for (const f of htmlFiles) {
|
|
|
332
423
|
const indexTpl = fs.readFileSync(path.join(PKG_DIR, 'templates/index.html'), 'utf8')
|
|
333
424
|
fs.writeFileSync(path.join(PROJECT_DIR, 'index.html'), applyTemplate(indexTpl, { pageTitle: mod.name }))
|
|
334
425
|
|
|
426
|
+
copyStaticAssets()
|
|
427
|
+
|
|
335
428
|
console.log(`Build complete: ${weekCount} weeks → src/data/ and pages/`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curio-sd/e-module-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A tool for building e-modules for Curio SD",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"templates",
|
|
12
12
|
"public",
|
|
13
13
|
"build.mjs",
|
|
14
|
+
"build-pdf.mjs",
|
|
14
15
|
"vite.config.js",
|
|
15
16
|
"vite-plugin-html-includes.js"
|
|
16
17
|
],
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"marked": "^18.0.5",
|
|
31
32
|
"marked-highlight": "^2.2.4",
|
|
32
33
|
"monaco-editor": "^0.53.0",
|
|
34
|
+
"pdfkit": "^0.19.1",
|
|
33
35
|
"tailwindcss": "^4.1.8",
|
|
34
36
|
"vite": "^8.0.16",
|
|
35
37
|
"yaml": "^2.9.0"
|
package/src/css/main.css
CHANGED
|
@@ -23,39 +23,6 @@
|
|
|
23
23
|
body {
|
|
24
24
|
@apply text-zinc-800;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
/* Given that markdown is placed inside main, we want some basic styling for headings */
|
|
28
|
-
main h2 {
|
|
29
|
-
@apply mt-8 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
main h3 {
|
|
33
|
-
@apply mt-4 text-base font-bold text-zinc-900
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
main h4 {
|
|
37
|
-
@apply mt-3 text-sm font-semibold text-zinc-900
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
main h5 {
|
|
41
|
-
@apply mt-2 text-sm font-normal uppercase text-zinc-900
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
main h6 {
|
|
45
|
-
@apply mt-2 text-sm font-light uppercase text-zinc-900
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main p {
|
|
49
|
-
@apply mt-2 text-sm leading-relaxed text-zinc-700;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
main ul {
|
|
53
|
-
@apply mt-2 text-sm list-inside list-disc space-y-1.5 pl-1;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
main ol {
|
|
57
|
-
@apply mt-2 text-sm list-inside list-decimal space-y-1.5 pl-1;
|
|
58
|
-
}
|
|
59
26
|
}
|
|
60
27
|
|
|
61
28
|
@layer components {
|
|
@@ -189,73 +156,103 @@
|
|
|
189
156
|
@apply mx-3 mb-2 mt-1 flex items-center gap-2 border border-zinc-600 bg-zinc-900 px-3 py-2.5 text-[13px] font-medium text-zinc-100 transition hover:border-zinc-500 hover:bg-zinc-800 hover:text-white;
|
|
190
157
|
}
|
|
191
158
|
|
|
159
|
+
.nav-group-toggle {
|
|
160
|
+
@apply flex w-full items-start justify-between px-3 pb-1 pt-4 text-left;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nav-group-chevron {
|
|
164
|
+
@apply mt-0.5 h-3 w-3 shrink-0 text-zinc-500 transition-transform duration-200;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.nav-group-children {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-rows: 1fr;
|
|
170
|
+
transition: grid-template-rows 0.2s ease;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.nav-group-children.collapsed {
|
|
174
|
+
grid-template-rows: 0fr;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.nav-group-inner {
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
192
181
|
/* ── Theory prose typography ─────────────────────────────────────────────── */
|
|
193
182
|
|
|
194
|
-
|
|
183
|
+
main {
|
|
195
184
|
@apply space-y-6 text-zinc-700;
|
|
196
185
|
}
|
|
197
186
|
|
|
198
|
-
|
|
187
|
+
main h2 {
|
|
199
188
|
@apply mt-8 text-xl font-semibold text-zinc-900 first:mt-0;
|
|
200
189
|
}
|
|
201
190
|
|
|
202
|
-
|
|
191
|
+
main h3 {
|
|
203
192
|
@apply mt-4 text-base font-semibold text-zinc-900;
|
|
204
193
|
}
|
|
205
194
|
|
|
206
|
-
|
|
195
|
+
main p {
|
|
207
196
|
@apply leading-relaxed;
|
|
208
197
|
}
|
|
209
198
|
|
|
210
|
-
|
|
199
|
+
main ul {
|
|
211
200
|
@apply list-inside list-disc space-y-1.5 pl-1;
|
|
212
201
|
}
|
|
213
202
|
|
|
214
|
-
|
|
203
|
+
main ol {
|
|
215
204
|
@apply list-inside list-decimal space-y-1.5 pl-1;
|
|
216
205
|
}
|
|
217
206
|
|
|
218
|
-
|
|
207
|
+
main li {
|
|
219
208
|
@apply leading-relaxed;
|
|
220
209
|
}
|
|
221
210
|
|
|
222
|
-
|
|
211
|
+
main a {
|
|
223
212
|
@apply font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition hover:decoration-zinc-900;
|
|
224
213
|
}
|
|
225
214
|
|
|
226
|
-
|
|
215
|
+
main code {
|
|
227
216
|
@apply rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-sm text-zinc-800;
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
|
|
219
|
+
main pre {
|
|
231
220
|
@apply overflow-x-auto bg-zinc-950 p-5 font-mono text-[13px] leading-relaxed text-zinc-300;
|
|
232
221
|
}
|
|
233
222
|
|
|
234
|
-
|
|
223
|
+
main pre code {
|
|
235
224
|
@apply bg-transparent p-0 text-inherit;
|
|
236
225
|
}
|
|
237
226
|
|
|
238
|
-
|
|
227
|
+
main blockquote {
|
|
239
228
|
@apply border-l-4 border-zinc-300 pl-4 italic text-zinc-500;
|
|
240
229
|
}
|
|
241
230
|
|
|
242
|
-
|
|
231
|
+
main table {
|
|
243
232
|
@apply w-full text-left text-sm;
|
|
244
233
|
}
|
|
245
234
|
|
|
246
|
-
|
|
235
|
+
main thead tr {
|
|
247
236
|
@apply border-b border-zinc-200 text-zinc-500;
|
|
248
237
|
}
|
|
249
238
|
|
|
250
|
-
|
|
239
|
+
main tbody tr {
|
|
251
240
|
@apply border-b border-zinc-100 last:border-0;
|
|
252
241
|
}
|
|
253
242
|
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
main th,
|
|
244
|
+
main td {
|
|
256
245
|
@apply py-2 pr-4;
|
|
257
246
|
}
|
|
258
247
|
|
|
248
|
+
main details {
|
|
249
|
+
@apply rounded bg-zinc-100 p-4;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main details summary {
|
|
253
|
+
@apply cursor-pointer font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition hover:decoration-zinc-900;
|
|
254
|
+
}
|
|
255
|
+
|
|
259
256
|
/* ── Custom components ───────────────────────────────────────────────────── */
|
|
260
257
|
|
|
261
258
|
x-card {
|
|
@@ -313,4 +310,81 @@
|
|
|
313
310
|
x-nav>p>a:last-child {
|
|
314
311
|
@apply border-transparent bg-zinc-900 text-white hover:border-transparent hover:bg-zinc-800 focus-visible:ring-zinc-900;
|
|
315
312
|
}
|
|
313
|
+
|
|
314
|
+
/* ── Theory side panel ───────────────────────────────────────────────────── */
|
|
315
|
+
|
|
316
|
+
.theory-panel {
|
|
317
|
+
@apply fixed inset-y-0 right-0 z-40 flex w-[480px] max-w-[90vw] flex-col border-l border-zinc-200 bg-white shadow-xl;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.theory-panel.panel-ready {
|
|
321
|
+
@apply transition-transform duration-300 ease-out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.theory-panel.panel-hidden {
|
|
325
|
+
@apply translate-x-full;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.theory-panel-header {
|
|
329
|
+
@apply flex shrink-0 items-center border-b border-zinc-200 bg-zinc-50;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.theory-panel-tabs {
|
|
333
|
+
@apply flex flex-1 overflow-x-auto;
|
|
334
|
+
scrollbar-width: none;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.theory-panel-tabs::-webkit-scrollbar {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.theory-tab {
|
|
342
|
+
@apply whitespace-nowrap border-b-2 border-transparent px-4 py-3 text-sm font-medium text-zinc-500 transition hover:text-zinc-900;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.theory-tab-active {
|
|
346
|
+
@apply border-zinc-900 text-zinc-900;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.theory-panel-close {
|
|
350
|
+
@apply shrink-0 p-3 text-zinc-400 transition hover:text-zinc-700;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.theory-panel-body {
|
|
354
|
+
@apply relative min-h-0 flex-1 flex flex-col;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.theory-panel-loader {
|
|
358
|
+
@apply absolute inset-0 z-10 flex items-center justify-center bg-white/80 backdrop-blur-sm;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.theory-panel-loader-hidden {
|
|
362
|
+
@apply hidden;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.theory-panel-spinner {
|
|
366
|
+
@apply h-8 w-8 animate-spin rounded-full border-2 border-zinc-200 border-t-zinc-700;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.theory-panel-iframe {
|
|
370
|
+
@apply min-h-0 flex-1 w-full border-0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.theory-panel-toggle {
|
|
374
|
+
@apply fixed bottom-6 right-6 shadow z-50 flex items-center gap-2 bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white shadow-lg transition hover:bg-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-900 focus-visible:ring-offset-2;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── Embedded theory mode (inside iframe) ────────────────────────────────── */
|
|
378
|
+
|
|
379
|
+
[data-embedded] {
|
|
380
|
+
@apply bg-white;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
[data-embedded] [data-theory] {
|
|
384
|
+
@apply px-6 py-8;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
[data-embedded] main {
|
|
388
|
+
@apply !p-0;
|
|
389
|
+
}
|
|
316
390
|
}
|
package/src/js/checklist.js
CHANGED
|
@@ -20,7 +20,7 @@ export function initChecklist() {
|
|
|
20
20
|
class="mt-0.5 h-4 w-4 border-zinc-300 text-zinc-900 focus:ring-zinc-900"
|
|
21
21
|
${checked ? 'checked' : ''}
|
|
22
22
|
/>
|
|
23
|
-
<span class="text-sm leading-relaxed ${checked ? 'text-zinc-400 line-through' : 'text-zinc-700'}">${item.text}</span>
|
|
23
|
+
<span class="text-sm leading-relaxed ${checked ? 'text-zinc-400 line-through' : 'text-zinc-700'}">${item.textHtml ?? item.text}</span>
|
|
24
24
|
</label>
|
|
25
25
|
`
|
|
26
26
|
})
|