coursecode 0.1.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.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * preview-server.js - Live preview with stub LMS + Vite build watch
3
+ *
4
+ * Runs Vite in build watch mode to output to dist/, then serves dist/ with
5
+ * a stub SCORM API wrapper. Includes live reload via Server-Sent Events.
6
+ *
7
+ * Supports two modes:
8
+ * - Course project mode: expects course/ and framework/ at cwd
9
+ * - Framework dev mode: expects template/course/ and framework/ at cwd (use --framework-dev)
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import http from 'http';
15
+ import { spawn, exec } from 'child_process';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ import { generateStubPlayer } from './stub-player.js';
19
+ import { generateContentHtml } from './stub-player/content-generator.js';
20
+ import { parseElements, resolveElementByPath } from './course-parser.js';
21
+ import { getComponentCatalog, getInteractionCatalog } from './authoring-api.js';
22
+ import { handleApiRoutes } from './preview-routes-api.js';
23
+ import { handleEditingRoutes } from './preview-routes-editing.js';
24
+ import { handleLmsRoutes, createLmsStore } from './preview-routes-lms.js';
25
+ import {
26
+ validateProject, escapeHtml, getMimeType, serveFile,
27
+ countSlides, findSlideById, collectSlideIds
28
+ } from './project-utils.js';
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+
32
+ // ============================================================================
33
+ // Utility Functions
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Find an element by its structural path in HTML.
38
+ * Uses course-parser's universal element parsing for consistency across all tools.
39
+ */
40
+ function findElementByPath(html, targetPath) {
41
+ const elements = parseElements(html);
42
+ return resolveElementByPath(elements, targetPath);
43
+ }
44
+
45
+
46
+
47
+ /**
48
+ * Simple markdown-to-HTML conversion for outline display.
49
+ */
50
+ function simpleMarkdownToHtml(md) {
51
+ const lines = md.split('\n');
52
+ const html = [];
53
+ let inCodeBlock = false;
54
+ let inList = false;
55
+
56
+ for (const line of lines) {
57
+ if (line.trim().startsWith('```')) {
58
+ if (inCodeBlock) {
59
+ html.push('</code></pre>');
60
+ inCodeBlock = false;
61
+ } else {
62
+ if (inList) { html.push('</ul>'); inList = false; }
63
+ html.push('<pre><code>');
64
+ inCodeBlock = true;
65
+ }
66
+ continue;
67
+ }
68
+ if (inCodeBlock) {
69
+ html.push(escapeHtml(line));
70
+ continue;
71
+ }
72
+
73
+ const trimmed = line.trim();
74
+ if (!trimmed) {
75
+ if (inList) { html.push('</ul>'); inList = false; }
76
+ continue;
77
+ }
78
+
79
+ const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)/);
80
+ if (headerMatch) {
81
+ if (inList) { html.push('</ul>'); inList = false; }
82
+ const level = headerMatch[1].length;
83
+ html.push(`<h${level}>${inlineFormat(headerMatch[2])}</h${level}>`);
84
+ continue;
85
+ }
86
+
87
+ if (trimmed.match(/^[-*]\s+/)) {
88
+ if (!inList) { html.push('<ul>'); inList = true; }
89
+ html.push(`<li>${inlineFormat(trimmed.replace(/^[-*]\s+/, ''))}</li>`);
90
+ continue;
91
+ }
92
+
93
+ if (trimmed.match(/^\d+\.\s+/)) {
94
+ if (!inList) { html.push('<ul>'); inList = true; }
95
+ html.push(`<li>${inlineFormat(trimmed.replace(/^\d+\.\s+/, ''))}</li>`);
96
+ continue;
97
+ }
98
+
99
+ if (trimmed.match(/^---+$/)) {
100
+ if (inList) { html.push('</ul>'); inList = false; }
101
+ html.push('<hr>');
102
+ continue;
103
+ }
104
+
105
+ if (inList) { html.push('</ul>'); inList = false; }
106
+ html.push(`<p>${inlineFormat(trimmed)}</p>`);
107
+ }
108
+
109
+ if (inList) html.push('</ul>');
110
+ if (inCodeBlock) html.push('</code></pre>');
111
+ return html.join('\n');
112
+ }
113
+
114
+ function inlineFormat(text) {
115
+ return escapeHtml(text)
116
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
117
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
118
+ .replace(/`(.+?)`/g, '<code>$1</code>')
119
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
120
+ }
121
+
122
+ /**
123
+ * Returns example HTML for a given component or interaction type.
124
+ * Dynamically pulls from schema.example via catalog APIs.
125
+ */
126
+ function getExampleHtml(type, category) {
127
+ try {
128
+ if (category === 'interaction') {
129
+ const catalog = getInteractionCatalog(type);
130
+ if (catalog.example) return catalog.example;
131
+ } else {
132
+ const catalog = getComponentCatalog(type);
133
+ if (catalog.example) return catalog.example;
134
+ if (catalog.usage) return catalog.usage;
135
+ }
136
+ } catch { /* ignore catalog errors */ }
137
+
138
+ return `<div class="callout callout-info"><p>No preview available for <strong>${escapeHtml(type)}</strong>.</p></div>`;
139
+ }
140
+
141
+ /**
142
+ * Parse multipart form-data and save uploaded files to the correct assets subdirectory.
143
+ */
144
+ function parseAndSaveFiles(buffer, boundary, assetsDir) {
145
+ const boundaryBuf = Buffer.from('--' + boundary);
146
+ const uploaded = [];
147
+ let pos = 0;
148
+
149
+ while (pos < buffer.length) {
150
+ const start = buffer.indexOf(boundaryBuf, pos);
151
+ if (start === -1) break;
152
+ const end = buffer.indexOf(boundaryBuf, start + boundaryBuf.length);
153
+ if (end === -1) break;
154
+
155
+ const part = buffer.slice(start + boundaryBuf.length, end);
156
+ const headerEnd = part.indexOf('\r\n\r\n');
157
+ if (headerEnd === -1) { pos = end; continue; }
158
+
159
+ const headers = part.slice(0, headerEnd).toString('utf-8');
160
+ const filenameMatch = headers.match(/filename="([^"]+)"/);
161
+ if (!filenameMatch) { pos = end; continue; }
162
+
163
+ const filename = path.basename(filenameMatch[1]);
164
+ const fileData = part.slice(headerEnd + 4, part.length - 2);
165
+
166
+ const ext = path.extname(filename).toLowerCase();
167
+ let subdir;
168
+ if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
169
+ subdir = 'images';
170
+ } else if (['.mp3', '.wav', '.ogg', '.m4a', '.aac'].includes(ext)) {
171
+ subdir = 'audio';
172
+ } else {
173
+ subdir = 'docs';
174
+ }
175
+
176
+ const targetDir = path.join(assetsDir, subdir);
177
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
178
+
179
+ fs.writeFileSync(path.join(targetDir, filename), fileData);
180
+ uploaded.push({ filename, subdir });
181
+ console.log(` šŸ“¦ Asset uploaded: ${subdir}/${filename}`);
182
+ pos = end;
183
+ }
184
+ return uploaded;
185
+ }
186
+
187
+
188
+ function getCourseTitle(coursePath) {
189
+ const configPath = path.join(coursePath, 'course-config.js');
190
+ try {
191
+ const content = fs.readFileSync(configPath, 'utf-8');
192
+ const match = content.match(/title:\s*['"`]([^'"`]+)['"`]/);
193
+ return match ? match[1] : 'SCORM Course';
194
+ } catch {
195
+ return 'SCORM Course';
196
+ }
197
+ }
198
+
199
+
200
+
201
+ // ============================================================================
202
+ // Main Server
203
+ // ============================================================================
204
+
205
+ export async function previewServer(options = {}) {
206
+ const frameworkDev = options.frameworkDev || false;
207
+ const paths = validateProject({ frameworkDev });
208
+ const title = options.title || getCourseTitle(paths.coursePath);
209
+ const previewPort = parseInt(options.port || '4173', 10);
210
+ const distDir = path.join(process.cwd(), 'dist');
211
+
212
+ console.log('\nšŸš€ Starting preview server...');
213
+ console.log(` šŸ“‚ Course: ${paths.coursePath}`);
214
+ console.log(` šŸ”Ø Build output: ${distDir}`);
215
+
216
+ // Build tracking state
217
+ const buildState = {
218
+ errors: [],
219
+ warnings: [],
220
+ lastBuildTime: null,
221
+ lastBuildSuccess: false
222
+ };
223
+
224
+ const sseClients = new Set();
225
+ const broadcastReload = () => {
226
+ for (const client of sseClients) {
227
+ client.write('data: reload\n\n');
228
+ }
229
+ };
230
+
231
+ // Resolve Vite binary — on Unix, spawn directly to avoid zombie /bin/sh
232
+ // processes that survive when we kill the parent PID. On Windows, .cmd files
233
+ // require shell: true (no zombie risk since Windows doesn't fork /bin/sh).
234
+ const isWindows = process.platform === 'win32';
235
+ const viteBin = path.join(process.cwd(), 'node_modules', '.bin', 'vite');
236
+ const viteArgs = ['build', '--watch', '--mode', 'development', '--logLevel', 'warn'];
237
+ if (paths.viteConfig) {
238
+ viteArgs.push('--config', paths.viteConfig);
239
+ }
240
+
241
+ const env = { ...process.env };
242
+ // Expose lib dir so vite.config.js can resolve coursecode utilities
243
+ // even when npm link + Vite's .vite-temp copy breaks normal resolution
244
+ env.COURSECODE_LIB_DIR = __dirname;
245
+ // Signal to framework code that this is a local dev build.
246
+ // Vite auto-exposes VITE_* env vars to client code via import.meta.env.
247
+ // Reporters check this to suppress external reporting locally.
248
+ env.VITE_COURSECODE_LOCAL = 'true';
249
+ if (options.format) {
250
+ const previewFormat = options.format.replace(/-proxy$|-remote$/, '');
251
+ env.LMS_FORMAT = previewFormat;
252
+ if (previewFormat !== options.format) {
253
+ console.log(` šŸ“¦ Format: ${options.format} → ${previewFormat} (preview mode)\n`);
254
+ } else {
255
+ console.log(` šŸ“¦ Format override: ${options.format}\n`);
256
+ }
257
+ }
258
+
259
+ // Start Vite build in watch mode
260
+ const viteProcess = spawn(viteBin, viteArgs, {
261
+ cwd: process.cwd(),
262
+ stdio: ['inherit', 'pipe', 'pipe'],
263
+ env,
264
+ shell: isWindows
265
+ });
266
+
267
+ let initialBuildDone = false;
268
+
269
+ viteProcess.stdout.on('data', (data) => {
270
+ const output = data.toString();
271
+ process.stdout.write(output);
272
+ if (output.includes('Build complete')) {
273
+ buildState.lastBuildTime = new Date().toISOString();
274
+ buildState.lastBuildSuccess = true;
275
+ buildState.errors = [];
276
+ if (initialBuildDone) {
277
+ broadcastReload();
278
+ }
279
+ }
280
+ if (output.includes('warning') || output.includes('Warning')) {
281
+ const lines = output.split('\n').filter(l => l.includes('warning') || l.includes('Warning'));
282
+ for (const line of lines) {
283
+ if (!buildState.warnings.some(w => w.message === line.trim())) {
284
+ buildState.warnings.push({ type: 'warning', message: line.trim(), time: new Date().toISOString() });
285
+ }
286
+ }
287
+ if (buildState.warnings.length > 20) buildState.warnings = buildState.warnings.slice(-20);
288
+ }
289
+ });
290
+
291
+ viteProcess.stderr.on('data', (data) => {
292
+ const output = data.toString();
293
+ process.stderr.write(output);
294
+ if (output.includes('error') || output.includes('Error') || output.includes('ERROR')) {
295
+ buildState.lastBuildSuccess = false;
296
+ buildState.errors.push({
297
+ type: 'build',
298
+ message: output.trim(),
299
+ time: new Date().toISOString()
300
+ });
301
+ if (buildState.errors.length > 10) buildState.errors = buildState.errors.slice(-10);
302
+ }
303
+ });
304
+
305
+ // Wait for initial build
306
+ await new Promise((resolve) => {
307
+ const indexPath = path.join(distDir, 'index.html');
308
+ let attempts = 0;
309
+ const maxAttempts = 120;
310
+
311
+ const checkReady = setInterval(() => {
312
+ attempts++;
313
+ if (fs.existsSync(indexPath)) {
314
+ clearInterval(checkReady);
315
+ initialBuildDone = true;
316
+ resolve();
317
+ } else if (attempts >= maxAttempts) {
318
+ clearInterval(checkReady);
319
+ console.error('āš ļø Build timeout - index.html not found');
320
+ initialBuildDone = true;
321
+ resolve();
322
+ }
323
+ }, 500);
324
+ });
325
+
326
+ // Generate course content HTML
327
+ let courseContent = null;
328
+ if (options.content !== false) {
329
+ console.log(' Generating course content for viewer...');
330
+ courseContent = await generateContentHtml({
331
+ coursePath: paths.coursePath,
332
+ includeNarration: true
333
+ });
334
+ }
335
+
336
+ // Generate stub player HTML
337
+ const storageKey = frameworkDev ? 'scorm_framework_dev' : 'scorm_preview_live';
338
+ const stubHtml = generateStubPlayer({
339
+ title,
340
+ launchUrl: '/course/index.html',
341
+ storageKey,
342
+ password: null,
343
+ isLive: true,
344
+ liveReload: true,
345
+ courseContent,
346
+ isDesktop: options.desktop || false
347
+ });
348
+
349
+ // Shared context object passed to route modules
350
+ const ctx = {
351
+ paths,
352
+ distDir,
353
+ buildState,
354
+ sseClients,
355
+ broadcastReload,
356
+ lmsStore: createLmsStore(),
357
+ getMimeType,
358
+ findSlideById,
359
+ countSlides,
360
+ collectSlideIds,
361
+ simpleMarkdownToHtml,
362
+ getExampleHtml,
363
+ parseAndSaveFiles,
364
+ serveFile,
365
+ findElementByPath
366
+ };
367
+
368
+ // Create HTTP server and dispatch routes
369
+ const server = http.createServer(async (req, res) => {
370
+ const url = req.url.split('?')[0];
371
+
372
+ // LMS routes (state sync + testing API)
373
+ if (handleLmsRoutes(ctx, req, res, url)) return;
374
+
375
+ // API routes (read-only)
376
+ if (await handleApiRoutes(ctx, req, res, url)) return;
377
+
378
+ // Editing routes (mutations)
379
+ if (handleEditingRoutes(ctx, req, res, url)) return;
380
+
381
+ // Serve stub player for root
382
+ if (url === '/' || url === '/index.html') {
383
+ res.writeHead(200, { 'Content-Type': 'text/html' });
384
+ res.end(stubHtml);
385
+ return;
386
+ }
387
+
388
+ // Serve files from dist/ for /course/* requests
389
+ if (url.startsWith('/course/')) {
390
+ const relativePath = url.slice('/course/'.length) || 'index.html';
391
+ const filePath = path.join(distDir, relativePath);
392
+ serveFile(filePath, res);
393
+ return;
394
+ }
395
+
396
+ if (url === '/course') {
397
+ const filePath = path.join(distDir, 'index.html');
398
+ serveFile(filePath, res);
399
+ return;
400
+ }
401
+
402
+ // Serve _content-manifest.json from dist/
403
+ if (url === '/_content-manifest.json') {
404
+ const filePath = path.join(distDir, '_content-manifest.json');
405
+ serveFile(filePath, res);
406
+ return;
407
+ }
408
+
409
+ // Serve _gallery-manifest.json (generated on-the-fly in dev mode)
410
+ if (url === '/_gallery-manifest.json' || url === '/course/_gallery-manifest.json') {
411
+ const staticPath = path.join(distDir, '_gallery-manifest.json');
412
+ if (fs.existsSync(staticPath)) {
413
+ serveFile(staticPath, res);
414
+ return;
415
+ }
416
+
417
+ try {
418
+ const docsDir = path.join(paths.coursePath, 'assets', 'docs');
419
+ if (!fs.existsSync(docsDir)) {
420
+ res.writeHead(200, { 'Content-Type': 'application/json' });
421
+ res.end(JSON.stringify({ items: [] }));
422
+ return;
423
+ }
424
+
425
+ const files = fs.readdirSync(docsDir);
426
+ const thumbnailFiles = new Set(
427
+ files.filter(f => f.match(/_thumbnail\.(png|jpg|jpeg|webp)$/i))
428
+ );
429
+
430
+ const allowedTypes = new Set(['pdf', 'md', 'jpg', 'png']);
431
+ const items = [];
432
+
433
+ for (const file of files) {
434
+ if (file.startsWith('.')) continue;
435
+ if (file.match(/_thumbnail\.(png|jpg|jpeg|webp)$/i)) continue;
436
+
437
+ const ext = path.extname(file).slice(1).toLowerCase();
438
+ if (!allowedTypes.has(ext)) continue;
439
+
440
+ let type;
441
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
442
+ type = 'image';
443
+ } else if (ext === 'pdf') {
444
+ type = 'pdf';
445
+ } else if (ext === 'md') {
446
+ type = 'markdown';
447
+ } else {
448
+ type = 'file';
449
+ }
450
+
451
+ const src = `course/assets/docs/${file}`;
452
+ const baseName = path.basename(file, path.extname(file));
453
+
454
+ let thumbnail = null;
455
+ for (const thumbExt of ['png', 'jpg', 'jpeg', 'webp']) {
456
+ const thumbFile = `${baseName}_thumbnail.${thumbExt}`;
457
+ if (thumbnailFiles.has(thumbFile)) {
458
+ thumbnail = `course/assets/docs/${thumbFile}`;
459
+ break;
460
+ }
461
+ }
462
+
463
+ const label = baseName
464
+ .replace(/[_-]/g, ' ')
465
+ .replace(/\b\w/g, c => c.toUpperCase());
466
+
467
+ items.push({ src, type, label, ...(thumbnail ? { thumbnail } : {}) });
468
+ }
469
+
470
+ items.sort((a, b) => a.label.localeCompare(b.label));
471
+
472
+ res.writeHead(200, { 'Content-Type': 'application/json' });
473
+ res.end(JSON.stringify({ items }));
474
+ } catch (err) {
475
+ res.writeHead(500, { 'Content-Type': 'application/json' });
476
+ res.end(JSON.stringify({ error: err.message }));
477
+ }
478
+ return;
479
+ }
480
+
481
+ // 404 for everything else
482
+ res.writeHead(404);
483
+ res.end('Not found');
484
+ });
485
+
486
+ const contentNote = options.content !== false ? ' • Content viewer (šŸ“„ button in toolbar)\n' : '';
487
+
488
+ const startListening = (retried = false) => {
489
+ server.listen(previewPort, () => {
490
+ console.log(`
491
+ āœ… Preview server running!
492
+
493
+ šŸŽÆ Open: http://localhost:${previewPort}
494
+
495
+ Features:
496
+ • Live reload - browser updates automatically on rebuild
497
+ • Auto-rebuild on file changes (watch mode)
498
+ • Stub SCORM API with localStorage persistence
499
+ • Debug panel with API log and validation
500
+ • MCP automation bridge (coursecode mcp)
501
+ ${contentNote}
502
+ URL Parameters:
503
+ • ?skipGating=true - Bypass navigation locks
504
+ • ?debug=true - Open debug panel on load
505
+
506
+ Press Ctrl+C to stop
507
+ `);
508
+ });
509
+
510
+ server.on('error', (err) => {
511
+ if (err.code === 'EADDRINUSE' && !retried) {
512
+ console.warn(`\nāš ļø STALE PROCESS DETECTED — port ${previewPort} is already in use.`);
513
+ console.warn(' Killing stale process and retrying...');
514
+ exec(`lsof -ti :${previewPort}`, (_, stdout) => {
515
+ const pids = (stdout || '').trim();
516
+ if (pids) console.warn(` Killed PID(s): ${pids.split('\n').join(', ')}`);
517
+ exec(`lsof -ti :${previewPort} | xargs kill -9 2>/dev/null`, () => {
518
+ setTimeout(() => {
519
+ server.close();
520
+ startListening(true);
521
+ }, 500);
522
+ });
523
+ });
524
+ } else if (err.code === 'EADDRINUSE') {
525
+ console.error(`\nāŒ Port ${previewPort} is still in use after retry. Kill it manually:\n lsof -ti :${previewPort} | xargs kill -9`);
526
+ process.exit(1);
527
+ } else {
528
+ throw err;
529
+ }
530
+ });
531
+ };
532
+
533
+ startListening();
534
+
535
+ // Handle cleanup
536
+ const cleanup = () => {
537
+ console.log('\n\nShutting down...');
538
+ viteProcess.kill();
539
+ server.close();
540
+ process.exit(0);
541
+ };
542
+
543
+ process.on('SIGINT', cleanup);
544
+ process.on('SIGTERM', cleanup);
545
+
546
+ viteProcess.on('close', (code) => {
547
+ if (code !== 0 && code !== null) {
548
+ console.error(`Vite process exited with code ${code}`);
549
+ server.close();
550
+ process.exit(1);
551
+ }
552
+ });
553
+ }
554
+
555
+ // CLI entry point - allows running directly: node lib/preview-server.js [--framework-dev]
556
+ if (process.argv[1] && process.argv[1].endsWith('preview-server.js')) {
557
+ const args = process.argv.slice(2);
558
+ const options = {
559
+ frameworkDev: args.includes('--framework-dev'),
560
+ port: args.find(a => a.startsWith('--port='))?.split('=')[1] || '4173',
561
+ format: process.env.LMS_FORMAT || null
562
+ };
563
+ previewServer(options);
564
+ }