@curio-sd/e-module-builder 0.1.0 → 0.2.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/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
- Copyright 2026 Curio Software Developer
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
-
5
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
-
7
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright 2026 Curio Software Developer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # `@curio-sd/e-module-builder`
2
+
3
+ A CLI build tool for creating interactive e-learning modules. It processes a structured `content/` directory of Markdown files and produces a Vite-powered, single-page-style site with theory pages, quizzes, exercises, and assignments — one per week.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @curio-sd/e-module-builder
9
+ ```
10
+
11
+ Add these scripts to your project's `package.json`:
12
+
13
+ ```json
14
+ {
15
+ "scripts": {
16
+ "dev": "e-module-builder dev",
17
+ "build": "e-module-builder build",
18
+ "preview": "e-module-builder preview"
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ | Command | Description |
26
+ | ------- | ----------- |
27
+ | `dev` | Start dev server at `localhost:5173`, watches `content/` and hot-reloads |
28
+ | `build` | Production build to `dist/` |
29
+ | `preview` | Locally preview the `dist/` build |
30
+
31
+ ## Project structure
32
+
33
+ Your project only needs a `content/` directory. Everything else (`src/data/`, `pages/`, `index.html`) is generated automatically.
34
+
35
+ ```txt
36
+ content/
37
+ module.md ← module metadata (name, weeks, language, exercise mode)
38
+ week1/
39
+ theory.md ← theory content (Markdown + YAML frontmatter)
40
+ quiz.md ← mid-week quiz questions
41
+ assignment.md ← hand-in assignment
42
+ exercises/
43
+ _meta.md ← exercise set metadata (week, title, color)
44
+ 1.md ← exercise 1
45
+ 2.md ← exercise 2
46
+
47
+ week2/ … weekN/ ← same structure
48
+ exams/
49
+ theory-exam.md ← final theory exam (optional)
50
+ practical-exam.md ← final practical exam (optional)
51
+ ```
52
+
53
+ ## Content file formats
54
+
55
+ ### `content/module.md`
56
+
57
+ ```yaml
58
+ ---
59
+ name: CSS Grid
60
+ subtitle: E-module
61
+ weeks: 4
62
+ language: nl
63
+ exerciseMode: interactive # or: external
64
+ description: Learn CSS Grid from the ground up.
65
+ youtube: https://www.youtube.com/watch?v=...
66
+ logoAlt: My module logo
67
+ algemeen:
68
+ - I can explain the difference between Flexbox and Grid
69
+ ---
70
+ ```
71
+
72
+ | Field | Required | Description |
73
+ | ----- | -------- | ----------- |
74
+ | `name` | yes | Module title |
75
+ | `weeks` | yes | Number of weeks to include (limits which `weekN/` dirs are processed) |
76
+ | `exerciseMode` | yes | `interactive` (Monaco editor) or `external` (link-out) |
77
+ | `language` | no | UI language, default `nl` |
78
+ | `subtitle` | no | Shown below the title |
79
+ | `description` | no | Short module description |
80
+ | `youtube` | no | Intro video URL |
81
+ | `algemeen` | no | General learning outcomes added to the checklist |
82
+
83
+ ---
84
+
85
+ ### `content/weekN/theory.md`
86
+
87
+ YAML frontmatter + Markdown body. The body supports standard Markdown, syntax-highlighted code blocks, and custom block-level elements (see [Custom elements](#custom-elements)).
88
+
89
+ ```yaml
90
+ ---
91
+ week: 1
92
+ title: The building blocks
93
+ goal: You understand what CSS Grid is and when to use it.
94
+ accent: indigo # Tailwind color name used as the week's accent color
95
+ summary: Short summary shown on the home page.
96
+ leeruitkomsten:
97
+ - I can explain what a grid container is
98
+ - I can define columns with grid-template-columns
99
+ ---
100
+
101
+ Markdown content here…
102
+ ```
103
+
104
+ ---
105
+
106
+ ### `content/weekN/quiz.md`
107
+
108
+ ```yaml
109
+ ---
110
+ title: Mid-week quiz — Week 1
111
+ passScore: 70
112
+ questions:
113
+ - id: q1
114
+ question: What does display:grid do?
115
+ options:
116
+ - Creates a flex container
117
+ - Activates CSS Grid on the element
118
+ correct: 1
119
+ explanation: display:grid activates CSS Grid.
120
+ ---
121
+ ```
122
+
123
+ ---
124
+
125
+ ### `content/weekN/assignment.md`
126
+
127
+ The Markdown **body** is split on blank lines: the first paragraph becomes the `case`, the rest becomes the `assignment` description.
128
+
129
+ ```yaml
130
+ ---
131
+ week: 1
132
+ title: Build a page layout
133
+ subtitle: Practical assignment
134
+ client: Acme Corp
135
+ deliverables:
136
+ - A working HTML/CSS page
137
+ criteria:
138
+ - Grid is used for the overall layout
139
+ maxPoints: 10
140
+ tips:
141
+ - Start with the grid container
142
+ ---
143
+
144
+ Case description paragraph.
145
+
146
+ Assignment instructions paragraph.
147
+ ```
148
+
149
+ ---
150
+
151
+ ### `content/weekN/exercises/_meta.md`
152
+
153
+ ```yaml
154
+ ---
155
+ week: 1
156
+ title: CSS Grid exercises
157
+ color: indigo
158
+ mode: interactive # optional, overrides module-level exerciseMode for this set
159
+ ---
160
+ ```
161
+
162
+ ---
163
+
164
+ ### `content/weekN/exercises/N.md`
165
+
166
+ Each file is a single exercise defined entirely in YAML frontmatter.
167
+
168
+ **Text/instructions exercise:**
169
+
170
+ ```yaml
171
+ ---
172
+ type: text
173
+ title: Columns
174
+ description: |
175
+ Create a grid with two equal columns.
176
+ ---
177
+ ```
178
+
179
+ **Interactive (Monaco editor) exercise:**
180
+
181
+ ```yaml
182
+ ---
183
+ type: interactive
184
+ title: Add a gap
185
+ starterHtml: "<div class='grid'>…</div>"
186
+ starterCss: ".grid { display: grid; }"
187
+ solution: ".grid { display: grid; gap: 16px; }"
188
+ ---
189
+ ```
190
+
191
+ **External exercise (link-out):**
192
+
193
+ ```yaml
194
+ ---
195
+ type: external
196
+ title: Grid Garden
197
+ url: https://cssgridgarden.com
198
+ ---
199
+ ```
200
+
201
+ ---
202
+
203
+ ### `content/exams/theory-exam.md` and `practical-exam.md`
204
+
205
+ Same structure as `quiz.md`. Practical exam questions may include a `preview` field with `css` and `html` for a live CSS preview alongside the question.
206
+
207
+ ---
208
+
209
+ ## Custom elements in Markdown
210
+
211
+ Theory pages support these custom block elements in Markdown:
212
+
213
+ | Element | Purpose |
214
+ | ------- | ------- |
215
+ | `<x-callout>` | Highlighted note block. Add `type="warning"` for warnings. |
216
+ | `<x-card title="…">` | Content card with a title. |
217
+ | `<x-compare>` / `<x-compare-item title="…">` | Side-by-side comparison columns. |
218
+ | `<x-nav label="…">` | Bottom navigation links (one Markdown link per line). |
219
+
220
+ Example:
221
+
222
+ ```markdown
223
+ <x-callout type="warning">
224
+ Watch out: only **direct children** of the grid container become grid items.
225
+ </x-callout>
226
+
227
+ <x-compare>
228
+ <x-compare-item title="Flexbox — one direction">
229
+
230
+ Use for components: navbars, button rows.
231
+
232
+ </x-compare-item>
233
+ <x-compare-item title="Grid — two directions">
234
+
235
+ Use for full page layouts.
236
+
237
+ </x-compare-item>
238
+ </x-compare>
239
+ ```
240
+
241
+ ---
242
+
243
+ ## What gets generated
244
+
245
+ The build pipeline runs before Vite and produces:
246
+
247
+ | Output | Source |
248
+ | ------ | ------ |
249
+ | `src/data/manifest.json` | `module.md` + all week frontmatter |
250
+ | `src/data/theory-weekN.json` | `weekN/theory.md` |
251
+ | `src/data/tussentoets-weekN.json` | `weekN/quiz.md` |
252
+ | `src/data/exercises/weekN.json` | `weekN/exercises/` |
253
+ | `src/data/inleveropdracht-weekN.json` | `weekN/assignment.md` |
254
+ | `src/data/checklist.json` | `leeruitkomsten` from all weeks |
255
+ | `src/data/toets-theorie.json` | `exams/theory-exam.md` |
256
+ | `src/data/toets-praktijk.json` | `exams/practical-exam.md` |
257
+ | `pages/weekN-theorie.html` | generated from template |
258
+ | `pages/weekN-oefeningen.html` | generated from template |
259
+ | `pages/weekN-toets.html` | generated from template |
260
+ | `pages/weekN-oefening.html` | generated from template |
261
+ | `pages/weekN-inleveropdracht.html` | generated from template |
262
+ | `pages/checklist.html` | generated from template |
263
+ | `pages/toets-theorie.html` | generated from template |
264
+ | `pages/toets-praktijk.html` | generated from template |
265
+ | `index.html` | generated from template |
266
+
267
+ In `dev` mode, changes to `content/` trigger an automatic rebuild and browser reload.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curio-sd/e-module-builder",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "A tool for building e-modules for Curio SD",
6
6
  "license": "MIT",
@@ -17,13 +17,24 @@
17
17
  "bin": {
18
18
  "e-module-builder": "./bin/cli.js"
19
19
  },
20
+ "scripts": {
21
+ "dev": "node scripts/dev-testbed.mjs",
22
+ "build:testbed": "node scripts/build-testbed.mjs",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
20
26
  "dependencies": {
21
27
  "@tailwindcss/vite": "^4.1.8",
22
28
  "gray-matter": "^4.0.3",
29
+ "highlight.js": "^11.11.1",
23
30
  "marked": "^18.0.5",
31
+ "marked-highlight": "^2.2.4",
24
32
  "monaco-editor": "^0.55.1",
25
33
  "tailwindcss": "^4.1.8",
26
34
  "vite": "^6.3.5",
27
35
  "yaml": "^2.9.0"
36
+ },
37
+ "devDependencies": {
38
+ "vitest": "^3.2.4"
28
39
  }
29
- }
40
+ }
@@ -1,11 +1,11 @@
1
1
  import { getItem, setItem } from './storage.js'
2
2
 
3
3
  function storageKey(week) {
4
- return `thuiswerk:week${week}`
4
+ return `inleveropdracht:week${week}`
5
5
  }
6
6
 
7
- export function initThuiswerk(data) {
8
- const container = document.querySelector('[data-thuiswerk]')
7
+ export function initInleveropdracht(data) {
8
+ const container = document.querySelector('[data-inleveropdracht]')
9
9
  if (!container) return
10
10
 
11
11
  const state = getItem(storageKey(data.week), { criteria: {}, notes: '', submitted: false })
@@ -69,7 +69,7 @@ export function initThuiswerk(data) {
69
69
  <p class="text-sm font-medium text-zinc-900">Notities / link naar je bestanden</p>
70
70
  <p class="mt-1 text-sm text-zinc-500">Bijv. GitHub-link, Google Drive, of een korte toelichting voor je docent.</p>
71
71
  <textarea
72
- data-thuiswerk-notes
72
+ data-inleveropdracht-notes
73
73
  class="mt-3 min-h-[120px] w-full border border-zinc-200 bg-white p-4 text-sm text-zinc-700 focus:border-zinc-900 focus:outline-none"
74
74
  placeholder="Plak hier je inlever-link of notities..."
75
75
  >${state.notes || ''}</textarea>
@@ -82,7 +82,7 @@ export function initThuiswerk(data) {
82
82
  </section>
83
83
 
84
84
  <div class="flex flex-wrap gap-3">
85
- <button type="button" data-export-thuiswerk class="btn-secondary">Exporteer checklist</button>
85
+ <button type="button" data-export-inleveropdracht class="btn-secondary">Exporteer checklist</button>
86
86
  <button type="button" data-mark-submitted class="btn-primary">${state.submitted ? 'Ingeleverd ✓' : 'Markeer als ingeleverd'}</button>
87
87
  </div>
88
88
  <p data-submitted-hint class="mt-3 text-sm text-zinc-500 ${state.submitted ? '' : 'hidden'}">Je hebt deze opdracht gemarkeerd als ingeleverd. Lever ook in via het kanaal van je docent.</p>
@@ -115,12 +115,12 @@ export function initThuiswerk(data) {
115
115
  })
116
116
  })
117
117
 
118
- const notesEl = container.querySelector('[data-thuiswerk-notes]')
118
+ const notesEl = container.querySelector('[data-inleveropdracht-notes]')
119
119
  notesEl?.addEventListener('input', () => {
120
120
  saveState({ notes: notesEl.value })
121
121
  })
122
122
 
123
- container.querySelector('[data-export-thuiswerk]')?.addEventListener('click', async () => {
123
+ container.querySelector('[data-export-inleveropdracht]')?.addEventListener('click', async () => {
124
124
  const criteria = getItem(storageKey(data.week), {}).criteria || {}
125
125
  const notes = notesEl?.value || ''
126
126
  const lines = [
@@ -141,7 +141,7 @@ export function initThuiswerk(data) {
141
141
  }
142
142
  try {
143
143
  await navigator.clipboard.writeText(lines.join('\n'))
144
- const btn = container.querySelector('[data-export-thuiswerk]')
144
+ const btn = container.querySelector('[data-export-inleveropdracht]')
145
145
  btn.textContent = 'Gekopieerd!'
146
146
  setTimeout(() => { btn.textContent = 'Exporteer checklist' }, 2000)
147
147
  } catch { /* ignore */ }
package/src/js/theory.js CHANGED
@@ -1,5 +1,3 @@
1
- import './components.js'
2
-
3
1
  function esc(s) {
4
2
  return String(s)
5
3
  .replace(/&/g, '&amp;')
@@ -1,28 +1,33 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="nl">
3
- <head>
4
- <!-- include:head -->
5
- <title>{{pageTitle}}</title>
6
- </head>
7
- <body class="font-sans antialiased">
8
- <div data-page-content>
9
- <main class="px-4 py-10 md:px-10">
10
- <div class="mx-auto max-w-3xl">
11
- <span class="week-label">Week {{weekPadded}} — Inleveropdracht</span>
12
- <h1 class="text-3xl font-semibold tracking-tight text-zinc-900" data-thuiswerk-title></h1>
13
- <p class="mt-2 text-zinc-600">Werk de casus uit en lever in via het kanaal van je docent.</p>
14
- <div data-thuiswerk class="mt-8"></div>
15
- </div>
16
- </main>
17
- </div>
18
- <script type="module">
19
- import { initPage } from '/src/js/nav.js'
20
- import { initThuiswerk } from '/src/js/thuiswerk.js'
21
- import thuiswerkData from '/src/data/thuiswerk-week{{week}}.json'
22
- initPage({ breadcrumbs: [{ label: 'Home', href: '/index.html' }, { label: 'Week {{week}}', href: '/pages/week{{week}}-theorie.html' }, { label: 'Inleveropdracht' }] })
23
- const titleEl = document.querySelector('[data-thuiswerk-title]')
24
- if (titleEl) titleEl.textContent = thuiswerkData.title
25
- initThuiswerk(thuiswerkData)
26
- </script>
27
- </body>
28
- </html>
3
+
4
+ <head>
5
+ <!-- include:head -->
6
+ <title>{{pageTitle}}</title>
7
+ </head>
8
+
9
+ <body class="font-sans antialiased">
10
+ <div data-page-content>
11
+ <main class="px-4 py-10 md:px-10">
12
+ <div class="mx-auto max-w-3xl">
13
+ <span class="week-label">Week {{weekPadded}} Inleveropdracht</span>
14
+ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900"
15
+ data-inleveropdracht-title></h1>
16
+ <p class="mt-2 text-zinc-600">Werk de casus uit en lever in via het kanaal van je docent.</p>
17
+ <div data-inleveropdracht
18
+ class="mt-8"></div>
19
+ </div>
20
+ </main>
21
+ </div>
22
+ <script type="module">
23
+ import { initPage } from '/src/js/nav.js'
24
+ import { initInleveropdracht } from '/src/js/inleveropdracht.js'
25
+ import inleveropdrachtData from '/src/data/inleveropdracht-week{{week}}.json'
26
+ initPage({ breadcrumbs: [{ label: 'Home', href: '/index.html' }, { label: 'Week {{week}}', href: '/pages/week{{week}}-theorie.html' }, { label: 'Inleveropdracht' }] })
27
+ const titleEl = document.querySelector('[data-inleveropdracht-title]')
28
+ if (titleEl) titleEl.textContent = inleveropdrachtData.title
29
+ initInleveropdracht(inleveropdrachtData)
30
+ </script>
31
+ </body>
32
+
33
+ </html>
@@ -1,33 +1,38 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="nl">
3
- <head>
4
- <!-- include:head -->
5
- <title>{{pageTitle}}</title>
6
- </head>
7
- <body class="font-sans antialiased">
8
- <div data-page-content>
9
- <main class="px-4 py-10 md:px-10">
10
- <div class="mx-auto max-w-3xl">
11
- <span class="week-label">Week {{weekPadded}} — Tussentoets</span>
12
- <h1 class="text-3xl font-semibold tracking-tight text-zinc-900">{{weekTitle}}</h1>
13
- <p class="mt-2 text-zinc-600">Test je kennis van week {{week}}. Minimaal 70% om te slagen.</p>
14
- <div data-quiz class="mt-8"></div>
15
- <div class="card mt-10 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
16
- <div>
17
- <p class="font-medium text-zinc-900">Toets afgerond?</p>
18
- <p class="mt-1 text-sm text-zinc-500">Werk de inleveropdracht uit.</p>
19
- </div>
20
- <a href="week{{week}}-thuiswerk.html" class="btn-primary shrink-0">Naar inleveropdracht</a>
3
+
4
+ <head>
5
+ <!-- include:head -->
6
+ <title>{{pageTitle}}</title>
7
+ </head>
8
+
9
+ <body class="font-sans antialiased">
10
+ <div data-page-content>
11
+ <main class="px-4 py-10 md:px-10">
12
+ <div class="mx-auto max-w-3xl">
13
+ <span class="week-label">Week {{weekPadded}} Tussentoets</span>
14
+ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900">{{weekTitle}}</h1>
15
+ <p class="mt-2 text-zinc-600">Test je kennis van week {{week}}. Minimaal 70% om te slagen.</p>
16
+ <div data-quiz
17
+ class="mt-8"></div>
18
+ <div class="card mt-10 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
19
+ <div>
20
+ <p class="font-medium text-zinc-900">Toets afgerond?</p>
21
+ <p class="mt-1 text-sm text-zinc-500">Werk de inleveropdracht uit.</p>
21
22
  </div>
23
+ <a href="week{{week}}-inleveropdracht.html"
24
+ class="btn-primary shrink-0">Naar inleveropdracht</a>
22
25
  </div>
23
- </main>
24
- </div>
25
- <script type="module">
26
- import { initPage } from '/src/js/nav.js'
27
- import { initQuiz } from '/src/js/quiz.js'
28
- import quizData from '/src/data/tussentoets-week{{week}}.json'
29
- initPage({ breadcrumbs: [{ label: 'Home', href: '/index.html' }, { label: 'Week {{week}}', href: '/pages/week{{week}}-theorie.html' }, { label: 'Tussentoets' }] })
30
- initQuiz(quizData)
31
- </script>
32
- </body>
33
- </html>
26
+ </div>
27
+ </main>
28
+ </div>
29
+ <script type="module">
30
+ import { initPage } from '/src/js/nav.js'
31
+ import { initQuiz } from '/src/js/quiz.js'
32
+ import quizData from '/src/data/tussentoets-week{{week}}.json'
33
+ initPage({ breadcrumbs: [{ label: 'Home', href: '/index.html' }, { label: 'Week {{week}}', href: '/pages/week{{week}}-theorie.html' }, { label: 'Tussentoets' }] })
34
+ initQuiz(quizData)
35
+ </script>
36
+ </body>
37
+
38
+ </html>