@curio-sd/e-module-builder 0.3.0 → 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/README.md CHANGED
@@ -131,7 +131,6 @@ The Markdown **body** is split on blank lines: the first paragraph becomes the `
131
131
  week: 1
132
132
  title: Build a page layout
133
133
  subtitle: Practical assignment
134
- client: Acme Corp
135
134
  deliverables:
136
135
  - A working HTML/CSS page
137
136
  criteria:
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,8 +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
- client: hwMd.data.client ?? '',
136
- html: marked.parse(hwMd.content ?? ''),
173
+ html: rewriteAssetPaths(marked.parse(hwMd.content ?? ''), `week${weekNum}`),
137
174
  deliverables: hwMd.data.deliverables ?? [],
138
175
  criteria: hwMd.data.criteria ?? [],
139
176
  maxPoints: hwMd.data.maxPoints ?? 0,
@@ -333,4 +370,6 @@ for (const f of htmlFiles) {
333
370
  const indexTpl = fs.readFileSync(path.join(PKG_DIR, 'templates/index.html'), 'utf8')
334
371
  fs.writeFileSync(path.join(PROJECT_DIR, 'index.html'), applyTemplate(indexTpl, { pageTitle: mod.name }))
335
372
 
373
+ copyStaticAssets()
374
+
336
375
  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.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "A tool for building e-modules for Curio SD",
6
6
  "license": "MIT",
package/src/css/main.css CHANGED
@@ -23,6 +23,39 @@
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
+ }
26
59
  }
27
60
 
28
61
  @layer components {
@@ -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
- .news-grid {
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
-
@@ -38,11 +38,6 @@ export function initInleveropdracht(data) {
38
38
  const tipsHtml = data.tips.map((t) => `<li class="text-sm text-zinc-600">${t}</li>`).join('')
39
39
 
40
40
  container.innerHTML = `
41
- <section class="card mb-6">
42
- <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Casus</p>
43
- <h2 class="mt-2 text-xl font-medium text-zinc-900">${data.client}</h2>
44
- </section>
45
-
46
41
  <section class="card prose-inleveropdracht mb-6">
47
42
  ${data.html}
48
43
  </section>
@@ -109,8 +104,6 @@ export function initInleveropdracht(data) {
109
104
  data.title,
110
105
  '='.repeat(40),
111
106
  '',
112
- `Klant: ${data.client}`,
113
- '',
114
107
  'Criteria:',
115
108
  ]
116
109
  for (const c of data.criteria) {
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(pkgDir, 'public'),
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(),