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,586 @@
1
+ /**
2
+ * stub-player/content-generator.js - Server-side content HTML generator
3
+ *
4
+ * Handles rendering of course content directly to HTML for the content viewer.
5
+ */
6
+
7
+ import { parseCourse } from '../course-parser.js';
8
+
9
+ /**
10
+ * Escape HTML special characters
11
+ */
12
+ function escapeHtml(text) {
13
+ if (!text) return '';
14
+ return text
15
+ .replace(/&/g, '&')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;');
19
+ }
20
+
21
+ /**
22
+ * Render an element to HTML based on its semantic type
23
+ */
24
+ function renderElement(el) {
25
+ const text = escapeHtml(el.innerText);
26
+
27
+ switch (el.semantic) {
28
+ case 'title':
29
+ return text ? `<p><strong>${text}</strong></p>` : '';
30
+ case 'description':
31
+ return text ? `<p>${text}</p>` : '';
32
+ case 'heading':
33
+ return text ? `<h4>${text}</h4>` : '';
34
+ case 'subheading':
35
+ return text ? `<h5>${text}</h5>` : '';
36
+ case 'paragraph':
37
+ return text ? `<p>${text}</p>` : '';
38
+ case 'callout':
39
+ return text ? `<blockquote>${text}</blockquote>` : '';
40
+ case 'list-item':
41
+ return text ? `<li>${text}</li>` : '';
42
+
43
+ // Pattern layouts - extract child content for readability
44
+ case 'intro-cards':
45
+ return renderPatternCards(el, 'intro-card');
46
+ case 'steps':
47
+ return renderPatternSteps(el);
48
+ case 'features':
49
+ return renderPatternCards(el, 'feature');
50
+ case 'comparison':
51
+ return renderPatternComparison(el);
52
+ case 'timeline':
53
+ return renderPatternTimeline(el);
54
+ case 'stats':
55
+ return renderPatternStats(el);
56
+ case 'checklist':
57
+ return renderPatternChecklist(el);
58
+ case 'hero':
59
+ return renderPatternHero(el);
60
+ case 'quote':
61
+ return text ? `<blockquote class="pattern-quote">${text}</blockquote>` : '';
62
+ case 'content-image':
63
+ // Just render the text content, images are visual
64
+ return text ? `<p>${text}</p>` : '';
65
+
66
+ case 'accordion':
67
+ if (!el.children || el.children.length === 0) return '';
68
+ let html = '<div class="accordion-content">';
69
+ for (const panel of el.children) {
70
+ const title = escapeHtml(panel.attributes?.['data-title']) || 'Untitled';
71
+ const content = escapeHtml(panel.innerText) || '';
72
+ html += `<details><summary>${title}</summary><div class="accordion-panel">${content}</div></details>`;
73
+ }
74
+ html += '</div>';
75
+ return html;
76
+ case 'accordion-panel':
77
+ return ''; // Handled by parent
78
+ case 'tabs':
79
+ return renderPatternTabs(el);
80
+ case 'card':
81
+ case 'flip-card':
82
+ return ''; // Handled by parent
83
+ default:
84
+ return '';
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Render intro-cards or feature cards as a clean list
90
+ */
91
+ function renderPatternCards(el, childClass) {
92
+ if (!el.children || el.children.length === 0) return '';
93
+
94
+ let html = '<div class="pattern-cards">';
95
+ for (const child of el.children) {
96
+ // Find cards within this pattern
97
+ if (child.className?.includes(childClass) || child.className?.includes('card')) {
98
+ const title = getChildHeading(child);
99
+ const content = getChildParagraph(child);
100
+ if (title || content) {
101
+ html += '<div class="pattern-card-item">';
102
+ if (title) html += `<h5>${escapeHtml(title)}</h5>`;
103
+ if (content) html += `<p>${escapeHtml(content)}</p>`;
104
+ html += '</div>';
105
+ }
106
+ }
107
+ }
108
+ html += '</div>';
109
+ return html;
110
+ }
111
+
112
+ /**
113
+ * Render steps pattern as numbered list
114
+ */
115
+ function renderPatternSteps(el) {
116
+ if (!el.children || el.children.length === 0) return '';
117
+
118
+ let html = '<ol class="pattern-steps">';
119
+ for (const child of el.children) {
120
+ if (child.className?.includes('step')) {
121
+ const title = getChildHeading(child);
122
+ const content = getChildParagraph(child);
123
+ html += '<li>';
124
+ if (title) html += `<strong>${escapeHtml(title)}</strong> - `;
125
+ if (content) html += escapeHtml(content);
126
+ html += '</li>';
127
+ }
128
+ }
129
+ html += '</ol>';
130
+ return html;
131
+ }
132
+
133
+ /**
134
+ * Render timeline as sequential entries
135
+ */
136
+ function renderPatternTimeline(el) {
137
+ if (!el.children || el.children.length === 0) return '';
138
+
139
+ let html = '<div class="pattern-timeline">';
140
+ for (const child of el.children) {
141
+ if (child.className?.includes('event') || child.className?.includes('timeline')) {
142
+ const date = child.attributes?.['data-date'] || child.attributes?.['data-year'] || '';
143
+ const title = getChildHeading(child);
144
+ const content = getChildParagraph(child);
145
+ html += '<div class="timeline-entry">';
146
+ if (date) html += `<span class="timeline-date"><strong>${escapeHtml(date)}</strong></span> `;
147
+ if (title) html += `<span class="timeline-title">${escapeHtml(title)}</span>: `;
148
+ if (content) html += `<span class="timeline-content">${escapeHtml(content)}</span>`;
149
+ html += '</div>';
150
+ }
151
+ }
152
+ html += '</div>';
153
+ return html;
154
+ }
155
+
156
+ /**
157
+ * Render comparison as two-column display
158
+ */
159
+ function renderPatternComparison(el) {
160
+ if (!el.children || el.children.length < 2) return '';
161
+
162
+ let html = '<div class="pattern-comparison">';
163
+ for (const child of el.children) {
164
+ const title = getChildHeading(child);
165
+ const items = getChildListItems(child);
166
+ html += '<div class="comparison-column">';
167
+ if (title) html += `<h5>${escapeHtml(title)}</h5>`;
168
+ if (items.length > 0) {
169
+ html += '<ul>';
170
+ for (const item of items) {
171
+ html += `<li>${escapeHtml(item)}</li>`;
172
+ }
173
+ html += '</ul>';
174
+ }
175
+ html += '</div>';
176
+ }
177
+ html += '</div>';
178
+ return html;
179
+ }
180
+
181
+ /**
182
+ * Render stats as key metrics
183
+ */
184
+ function renderPatternStats(el) {
185
+ if (!el.children || el.children.length === 0) return '';
186
+
187
+ let html = '<div class="pattern-stats">';
188
+ for (const child of el.children) {
189
+ if (child.className?.includes('stat')) {
190
+ const value = getChildByClass(child, 'stat-value') || getChildHeading(child);
191
+ const label = getChildByClass(child, 'stat-label') || getChildParagraph(child);
192
+ if (value || label) {
193
+ html += '<div class="stat-item">';
194
+ if (value) html += `<strong>${escapeHtml(value)}</strong>`;
195
+ if (label) html += ` - ${escapeHtml(label)}`;
196
+ html += '</div>';
197
+ }
198
+ }
199
+ }
200
+ html += '</div>';
201
+ return html;
202
+ }
203
+
204
+ /**
205
+ * Render checklist as bullet points
206
+ */
207
+ function renderPatternChecklist(el) {
208
+ const items = getChildListItems(el);
209
+ if (items.length === 0) return '';
210
+
211
+ let html = '<ul class="pattern-checklist">';
212
+ for (const item of items) {
213
+ html += `<li>☑ ${escapeHtml(item)}</li>`;
214
+ }
215
+ html += '</ul>';
216
+ return html;
217
+ }
218
+
219
+ /**
220
+ * Render hero as prominent heading
221
+ */
222
+ function renderPatternHero(el) {
223
+ const title = getChildHeading(el);
224
+ const content = getChildParagraph(el);
225
+ if (!title && !content) return '';
226
+
227
+ let html = '<div class="pattern-hero">';
228
+ if (title) html += `<h3>${escapeHtml(title)}</h3>`;
229
+ if (content) html += `<p>${escapeHtml(content)}</p>`;
230
+ html += '</div>';
231
+ return html;
232
+ }
233
+
234
+ /**
235
+ * Render tabs content
236
+ */
237
+ function renderPatternTabs(el) {
238
+ if (!el.children || el.children.length === 0) return '';
239
+
240
+ let html = '<div class="pattern-tabs">';
241
+ for (const child of el.children) {
242
+ const title = child.attributes?.['data-tab'] || child.attributes?.['data-title'] || 'Tab';
243
+ const content = escapeHtml(child.innerText) || '';
244
+ html += `<details><summary>${escapeHtml(title)}</summary><div class="tab-content">${content}</div></details>`;
245
+ }
246
+ html += '</div>';
247
+ return html;
248
+ }
249
+
250
+ // Helper functions to extract child content
251
+ function getChildHeading(el) {
252
+ const heading = el.children?.find(c => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(c.tag));
253
+ return heading?.innerText || '';
254
+ }
255
+
256
+ function getChildParagraph(el) {
257
+ const para = el.children?.find(c => c.tag === 'p');
258
+ return para?.innerText || '';
259
+ }
260
+
261
+ function getChildListItems(el) {
262
+ return el.children?.filter(c => c.tag === 'li').map(c => c.innerText || '') || [];
263
+ }
264
+
265
+ function getChildByClass(el, className) {
266
+ const child = el.children?.find(c => c.className?.includes(className));
267
+ return child?.innerText || '';
268
+ }
269
+
270
+ /**
271
+ * Render elements array to HTML
272
+ */
273
+ function renderElements(elements, skipHeader = true) {
274
+ const parts = [];
275
+ let inList = false;
276
+
277
+ for (const el of elements) {
278
+ if (skipHeader && (el.semantic === 'title' || el.semantic === 'description')) {
279
+ continue;
280
+ }
281
+
282
+ const isListItem = el.semantic === 'list-item';
283
+
284
+ if (isListItem && !inList) {
285
+ parts.push('<ul>');
286
+ inList = true;
287
+ } else if (!isListItem && inList) {
288
+ parts.push('</ul>');
289
+ inList = false;
290
+ }
291
+
292
+ const html = renderElement(el);
293
+ if (html) parts.push(html);
294
+ }
295
+
296
+ if (inList) parts.push('</ul>');
297
+ return parts.join('\n');
298
+ }
299
+
300
+ /**
301
+ * Render course header/metadata
302
+ */
303
+ function renderMetadata(config) {
304
+ const meta = config.metadata || {};
305
+ const title = meta.title || 'CourseCode';
306
+ const desc = meta.description || '';
307
+
308
+ let html = '<div class="content-hero">';
309
+ html += `<h1 id="course-title">${escapeHtml(title)}</h1>`;
310
+ if (desc) {
311
+ html += `<p class="content-hero-description">${escapeHtml(desc)}</p>`;
312
+ }
313
+
314
+ const details = [];
315
+ if (meta.version) details.push(`<span class="meta-item"><span class="meta-label">Version</span> ${escapeHtml(meta.version)}</span>`);
316
+ if (meta.author) details.push(`<span class="meta-item"><span class="meta-label">Author</span> ${escapeHtml(meta.author)}</span>`);
317
+ if (meta.language) details.push(`<span class="meta-item"><span class="meta-label">Language</span> ${escapeHtml(meta.language)}</span>`);
318
+
319
+ if (details.length > 0) {
320
+ html += `<div class="content-hero-meta">${details.join('')}</div>`;
321
+ }
322
+ html += '</div>';
323
+
324
+ return html;
325
+ }
326
+
327
+ /**
328
+ * Render course structure overview
329
+ */
330
+ function renderStructureOverview(structure, depth = 0) {
331
+ const items = [];
332
+
333
+ for (const item of structure) {
334
+ const typeClass = item.type === 'section' ? 'toc-section' :
335
+ item.type === 'assessment' ? 'toc-assessment' : 'toc-slide';
336
+ const typeLabel = item.type === 'section' ? 'Section' :
337
+ item.type === 'assessment' ? 'Assessment' : 'Slide';
338
+ const title = item.title || item.menu?.label || item.id;
339
+ const anchor = `slide-${item.id}`;
340
+
341
+ let entry = `<li class="toc-item ${typeClass}">`;
342
+ entry += `<a href="#${anchor}" class="toc-link">`;
343
+ entry += `<span class="toc-type-badge">${typeLabel}</span>`;
344
+ entry += `<span class="toc-title">${escapeHtml(title)}</span>`;
345
+
346
+ // Add tags
347
+ const tags = [];
348
+ if (item.engagement?.required) tags.push('Engagement');
349
+ if (item.navigation?.gating) tags.push('Gated');
350
+ if (tags.length > 0) {
351
+ entry += `<span class="toc-tags">${tags.map(t => `<span class="toc-tag">${t}</span>`).join('')}</span>`;
352
+ }
353
+ entry += '</a>';
354
+
355
+ // Recurse for sections
356
+ if (item.children && item.children.length > 0) {
357
+ entry += '<ul class="toc-children">' + renderStructureOverview(item.children, depth + 1) + '</ul>';
358
+ }
359
+
360
+ entry += '</li>';
361
+ items.push(entry);
362
+ }
363
+
364
+ return items.join('\n');
365
+ }
366
+
367
+ /**
368
+ * Render an interaction to HTML
369
+ */
370
+ function renderInteractionHtml(interaction, options = {}) {
371
+ const { includeAnswers = true, includeFeedback = true } = options;
372
+
373
+ const typeLabel = interaction.type?.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) || 'Interaction';
374
+ const id = interaction.id || 'unknown';
375
+
376
+ let html = '<div class="interaction-card">';
377
+ html += '<div class="interaction-header">';
378
+ html += `<span class="interaction-type-badge">${escapeHtml(typeLabel)}</span>`;
379
+ html += `<span class="interaction-id"><code>${escapeHtml(id)}</code></span>`;
380
+ html += '</div>';
381
+ html += '<div class="interaction-body">';
382
+
383
+ // Prompt/Question
384
+ if (interaction.prompt) {
385
+ html += `<div class="interaction-prompt">${escapeHtml(interaction.prompt)}</div>`;
386
+ }
387
+
388
+ // Options for choice-based interactions
389
+ if (interaction.options && interaction.options.length > 0) {
390
+ html += '<ul class="interaction-options">';
391
+ for (const opt of interaction.options) {
392
+ const isCorrect = includeAnswers && opt.correct;
393
+ const correctClass = isCorrect ? 'option-correct' : '';
394
+ html += `<li class="interaction-option ${correctClass}">`;
395
+ html += escapeHtml(opt.text || opt.label || opt);
396
+ if (isCorrect) html += ' <span class="correct-indicator">✓</span>';
397
+ html += '</li>';
398
+ }
399
+ html += '</ul>';
400
+ }
401
+
402
+ // Correct answer for non-choice interactions
403
+ if (includeAnswers && interaction.correctAnswer && !interaction.options) {
404
+ html += `<div class="interaction-answer"><strong>Answer:</strong> <code>${escapeHtml(String(interaction.correctAnswer))}</code></div>`;
405
+ }
406
+
407
+ // Feedback
408
+ if (includeFeedback) {
409
+ if (interaction.feedback?.correct) {
410
+ html += `<div class="interaction-feedback feedback-correct"><span class="feedback-label">Correct:</span> ${escapeHtml(interaction.feedback.correct)}</div>`;
411
+ }
412
+ if (interaction.feedback?.incorrect) {
413
+ html += `<div class="interaction-feedback feedback-incorrect"><span class="feedback-label">Incorrect:</span> ${escapeHtml(interaction.feedback.incorrect)}</div>`;
414
+ }
415
+ }
416
+
417
+ html += '</div></div>';
418
+ return html;
419
+ }
420
+
421
+ /**
422
+ * Render a slide to HTML
423
+ */
424
+ function renderSlide(item, parsedData, options = {}) {
425
+ const { includeNarration = true, includeAnswers = true, includeFeedback = true } = options;
426
+ const title = item.title || item.menu?.label || item.id;
427
+ const typeClass = item.type === 'assessment' ? 'slide-card-assessment' : 'slide-card-standard';
428
+ const typeLabel = item.type === 'assessment' ? 'Assessment' : 'Slide';
429
+
430
+ let html = `<span id="slide-${item.id}"></span>`;
431
+ html += `<article class="slide-card ${typeClass}">`;
432
+ html += '<header class="slide-card-header">';
433
+ html += '<div class="slide-card-title-row">';
434
+ html += `<span class="slide-type-badge">${typeLabel}</span>`;
435
+ html += `<h2 class="slide-card-title">${escapeHtml(title)}</h2>`;
436
+ html += '</div>';
437
+ html += '<div class="slide-card-meta">';
438
+ html += `<span class="slide-meta-item"><span class="meta-label">ID</span> <code>${item.id}</code></span>`;
439
+ if (item.component) {
440
+ html += `<span class="slide-meta-item"><span class="meta-label">Component</span> <code>${item.component}</code></span>`;
441
+ }
442
+ html += '</div>';
443
+ html += '</header>';
444
+
445
+ html += '<div class="slide-card-body">';
446
+
447
+ if (parsedData) {
448
+ // Header content
449
+ if (parsedData.header?.title || parsedData.header?.description) {
450
+ html += '<div class="slide-content-header">';
451
+ if (parsedData.header.title) {
452
+ html += `<h3 class="slide-content-title">${escapeHtml(parsedData.header.title)}</h3>`;
453
+ }
454
+ if (parsedData.header.description) {
455
+ html += `<p class="slide-content-description">${escapeHtml(parsedData.header.description)}</p>`;
456
+ }
457
+ html += '</div>';
458
+ }
459
+
460
+ // Elements
461
+ if (parsedData.elements && parsedData.elements.length > 0) {
462
+ html += '<div class="slide-elements">';
463
+ html += renderElements(parsedData.elements);
464
+ html += '</div>';
465
+ }
466
+
467
+ // Interactions
468
+ if (parsedData.interactions && parsedData.interactions.length > 0) {
469
+ html += '<div class="slide-interactions">';
470
+ html += `<h3 class="section-label">Interactions <span class="count-badge">${parsedData.interactions.length}</span></h3>`;
471
+ for (const interaction of parsedData.interactions) {
472
+ html += renderInteractionHtml(interaction, { includeAnswers, includeFeedback });
473
+ }
474
+ html += '</div>';
475
+ }
476
+
477
+ // Narration
478
+ if (includeNarration && parsedData.narration) {
479
+ html += '<div class="slide-narration">';
480
+ html += '<h3 class="section-label narration-header">Narration</h3>';
481
+ html += '<div class="narration-section">';
482
+ for (const [key, text] of Object.entries(parsedData.narration)) {
483
+ if (Object.keys(parsedData.narration).length > 1 && key !== 'slide') {
484
+ html += `<p class="narration-key"><strong>${escapeHtml(key)}:</strong></p>`;
485
+ }
486
+ html += `<p>${escapeHtml(text)}</p>`;
487
+ }
488
+ html += '</div>';
489
+ html += '</div>';
490
+ }
491
+ } else {
492
+ html += '<p class="no-content"><em>No content available</em></p>';
493
+ }
494
+
495
+ html += '</div>';
496
+ html += '<footer class="slide-card-footer"><a href="#course-structure" class="back-to-toc">↑ Back to Course Structure</a></footer>';
497
+ html += '</article>';
498
+ return html;
499
+ }
500
+
501
+ /**
502
+ * Render a section to HTML
503
+ */
504
+ function renderSection(section, slides, options) {
505
+ const title = section.title || section.menu?.label || section.id;
506
+ let html = `<span id="slide-${section.id}"></span>`;
507
+ html += '<div class="section-container">';
508
+ html += '<div class="section-header">';
509
+ html += '<span class="section-icon">📂</span>';
510
+ html += `<h2 class="section-title">${escapeHtml(title)}</h2>`;
511
+ html += '</div>';
512
+ html += '<div class="section-slides">';
513
+
514
+ if (section.children) {
515
+ for (const child of section.children) {
516
+ if (child.type === 'slide' || child.type === 'assessment') {
517
+ const parsedData = slides[child.id] || null;
518
+ html += renderSlide(child, parsedData, options);
519
+ } else if (child.type === 'section') {
520
+ html += renderSection(child, slides, options);
521
+ }
522
+ }
523
+ }
524
+
525
+ html += '</div></div>';
526
+ return html;
527
+ }
528
+
529
+ /**
530
+ * Generate course content HTML (server-side rendering)
531
+ * Used by preview-server to embed content directly in stub player
532
+ * @param {Object} options - { coursePath, includeNarration }
533
+ * @returns {Promise<string|null>} HTML content or null on error
534
+ */
535
+ export async function generateContentHtml(options = {}) {
536
+ const {
537
+ coursePath = './course',
538
+ includeNarration = true
539
+ } = options;
540
+
541
+ try {
542
+ const courseData = await parseCourse(coursePath);
543
+ const { config, slides } = courseData;
544
+ const structure = config.structure || [];
545
+
546
+ const renderOptions = {
547
+ includeNarration,
548
+ includeAnswers: true,
549
+ includeFeedback: true
550
+ };
551
+
552
+ let html = '<div class="course-content">';
553
+
554
+ // Metadata
555
+ html += renderMetadata(config);
556
+ html += '<hr>';
557
+
558
+ // Structure overview
559
+ html += '<nav class="toc-section">';
560
+ html += '<h2 id="course-structure" class="toc-heading">Course Structure</h2>';
561
+ html += '<ul class="toc-list">';
562
+ html += renderStructureOverview(structure);
563
+ html += '</ul>';
564
+ html += '</nav>';
565
+
566
+ // Process each item
567
+ for (const item of structure) {
568
+ if (item.type === 'slide') {
569
+ const parsedData = slides[item.id] || null;
570
+ html += renderSlide(item, parsedData, renderOptions);
571
+ } else if (item.type === 'assessment') {
572
+ const parsedData = slides[item.id] || null;
573
+ html += renderSlide(item, parsedData, renderOptions);
574
+ } else if (item.type === 'section') {
575
+ html += renderSection(item, slides, renderOptions);
576
+ }
577
+ }
578
+
579
+ html += '</div>';
580
+ return html;
581
+
582
+ } catch (error) {
583
+ console.error('Failed to generate content HTML:', error.message);
584
+ return null;
585
+ }
586
+ }