coursecast 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecast",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "One prompt → a full interactive course — live widgets, quizzes, a mastery dashboard — rendered to self-contained HTML and deployed live.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -141,10 +141,41 @@ export function validateLesson(obj) {
141
141
  if (obj.simulation) {
142
142
  if (!obj.simulation.html) e.push('"simulation" is missing "html"')
143
143
  if (!obj.simulation.js) e.push('"simulation" is missing "js"')
144
+ e.push(...validateSimulationWiring(obj.simulation))
144
145
  }
145
146
  return e
146
147
  }
147
148
 
149
+ // The simulation's markup and its script are written as two separate strings, so a typo in
150
+ // one doesn't show up until a learner clicks and the console throws. These checks catch the
151
+ // two ways that actually happens, and because they run inside validate+repair the model gets
152
+ // a chance to fix its own mistake before the lesson is ever rendered.
153
+ function validateSimulationWiring({ html = '', js = '' }) {
154
+ const e = []
155
+ if (!html || !js) return e
156
+
157
+ // 1. inline handlers (onclick="doThing()") that name a function the script never defines
158
+ const handlers = new Set()
159
+ for (const m of String(html).matchAll(/\son[a-z]+\s*=\s*(["'])(.*?)\1/gi)) {
160
+ const fn = m[2].match(/^\s*([A-Za-z_$][\w$]*)\s*\(/)
161
+ if (fn) handlers.add(fn[1])
162
+ }
163
+ for (const fn of handlers) {
164
+ const defined = new RegExp(
165
+ `(function\\s+${fn}\\b|\\b(?:var|let|const)\\s+${fn}\\s*=|\\b${fn}\\s*=\\s*(?:function|\\()|window\\.${fn}\\s*=)`,
166
+ ).test(js)
167
+ if (!defined) e.push(`simulation.html calls "${fn}()" from an inline handler, but simulation.js never defines it`)
168
+ }
169
+
170
+ // 2. getElementById('x') where the markup has no element with that id
171
+ const ids = new Set([...String(html).matchAll(/\sid\s*=\s*(["'])(.*?)\1/gi)].map((m) => m[2].trim()))
172
+ for (const m of String(js).matchAll(/getElementById\(\s*(["'])(.*?)\1\s*\)/gi)) {
173
+ const want = m[2].trim()
174
+ if (want && !ids.has(want)) e.push(`simulation.js looks up id "${want}", which does not exist in simulation.html`)
175
+ }
176
+ return e.slice(0, 6)
177
+ }
178
+
148
179
  export function validateSyllabus(obj) {
149
180
  const e = []
150
181
  if (!obj || typeof obj !== 'object') return ['the output was not a JSON object']