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,1246 @@
1
+ /**
2
+ * Export Content Command v2
3
+ * Extracts text content from SCORM course source files into structured Markdown.
4
+ * Uses source-based extraction (not HTML parsing) for reliable, clean output.
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { pathToFileURL } from 'url';
10
+ import { parseSlideSource, extractAssessment } from './course-parser.js';
11
+ import {
12
+ formatInteraction
13
+ } from './interaction-formatters.js';
14
+
15
+ /**
16
+ * Validate that we're in a valid SCORM project directory
17
+ * @param {string} coursePath - Path to course directory
18
+ * @param {boolean} [silent=false] - If true, return null instead of exiting on error
19
+ */
20
+ function validateProject(coursePath, silent = false) {
21
+ const cwd = process.cwd();
22
+ const fullCoursePath = path.isAbsolute(coursePath) ? coursePath : path.join(cwd, coursePath);
23
+
24
+ const hasConfigFile = fs.existsSync(path.join(fullCoursePath, 'course-config.js'));
25
+
26
+ if (!hasConfigFile) {
27
+ if (silent) {
28
+ return null;
29
+ }
30
+ console.error(`
31
+ ❌ Could not find course-config.js in ${fullCoursePath}
32
+
33
+ Make sure you're running this command from a SCORM project root,
34
+ or specify the correct path with --course-path.
35
+ `);
36
+ process.exit(1);
37
+ }
38
+
39
+ return fullCoursePath;
40
+ }
41
+
42
+ /**
43
+ * Load course configuration
44
+ * @param {string} coursePath - Path to course directory
45
+ * @returns {Object} Course configuration
46
+ */
47
+ async function loadCourseConfig(coursePath) {
48
+ const configPath = path.join(coursePath, 'course-config.js');
49
+ const configUrl = pathToFileURL(configPath).href;
50
+
51
+ try {
52
+ const module = await import(configUrl);
53
+ return module.courseConfig || module.default;
54
+ } catch (error) {
55
+ console.error(`\n❌ Failed to load course-config.js: ${error.message}\n`);
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Read slide file source code
62
+ * @param {string} componentPath - Component path
63
+ * @param {string} coursePath - Base course path
64
+ * @returns {string} File contents
65
+ */
66
+ function readSlideSource(componentPath, coursePath) {
67
+ try {
68
+ let resolvedPath = componentPath;
69
+ if (componentPath.startsWith('@slides/')) {
70
+ resolvedPath = path.join(coursePath, 'slides', componentPath.replace('@slides/', ''));
71
+ } else if (!path.isAbsolute(componentPath)) {
72
+ resolvedPath = path.join(coursePath, componentPath);
73
+ }
74
+ return fs.readFileSync(resolvedPath, 'utf-8');
75
+ } catch (_error) {
76
+ console.warn(` ⚠ Could not read: ${componentPath}`);
77
+ return '';
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Convert header to Markdown
83
+ * @param {Object} header - Parsed header with title and description
84
+ * @returns {string} Markdown
85
+ */
86
+ function headerToMarkdown(header) {
87
+ const lines = [];
88
+ if (header?.title) {
89
+ lines.push(`**${header.title}**`);
90
+ }
91
+ if (header?.description) {
92
+ lines.push(header.description);
93
+ }
94
+ if (lines.length > 0) lines.push('');
95
+ return lines.join('\n');
96
+ }
97
+
98
+ /**
99
+ * Format element as Markdown based on semantic type
100
+ * @param {object} el - Parsed element
101
+ * @returns {string|null}
102
+ */
103
+ function formatElementAsMarkdown(el) {
104
+ if (el.tag === 'pre' && el.innerText) {
105
+ return `\`\`\`\n${el.innerText}\n\`\`\`\n`;
106
+ }
107
+
108
+ // pre handles block code; skip nested <code> to avoid duplicates.
109
+ if (el.tag === 'code' && el.parentPath?.includes('/pre.')) {
110
+ return null;
111
+ }
112
+
113
+ switch (el.semantic) {
114
+ case 'title':
115
+ return el.innerText ? `**${el.innerText}**\n` : null;
116
+ case 'description':
117
+ return el.innerText ? `${el.innerText}\n` : null;
118
+ case 'heading':
119
+ return el.innerText ? `## ${el.innerText}\n` : null;
120
+ case 'subheading':
121
+ return el.innerText ? `### ${el.innerText}\n` : null;
122
+ case 'paragraph':
123
+ return el.innerText ? `${el.innerText}\n` : null;
124
+ case 'callout':
125
+ // Structured callouts (headings/lists/paragraphs) render better via child elements.
126
+ if (el.children && el.children.length > 0) {
127
+ return null;
128
+ }
129
+ return el.innerText ? `> ${el.innerText}\n` : null;
130
+ case 'list-item':
131
+ return el.innerText ? `- ${el.innerText}` : null;
132
+
133
+ // Pattern layouts - format for readable review
134
+ case 'intro-cards':
135
+ case 'features':
136
+ return formatPatternCards(el);
137
+ case 'steps':
138
+ return formatPatternSteps(el);
139
+ case 'timeline':
140
+ return formatPatternTimeline(el);
141
+ case 'comparison':
142
+ return formatPatternComparison(el);
143
+ case 'stats':
144
+ return formatPatternStats(el);
145
+ case 'checklist':
146
+ return formatPatternChecklist(el);
147
+ case 'hero':
148
+ return formatPatternHero(el);
149
+ case 'quote':
150
+ return el.innerText ? `> *"${el.innerText}"*\n` : null;
151
+ case 'content-image':
152
+ return el.innerText ? `${el.innerText}\n` : null;
153
+ case 'tabs':
154
+ return formatPatternTabs(el);
155
+
156
+ case 'accordion':
157
+ if (!el.children || el.children.length === 0) return null;
158
+ {
159
+ let md = '';
160
+ for (const panel of el.children) {
161
+ const title = panel.attributes?.['data-title'] || 'Untitled';
162
+ const content = panel.innerText || '';
163
+ md += `<details>\n<summary>${title}</summary>\n\n${content}\n\n</details>\n\n`;
164
+ }
165
+ return md;
166
+ }
167
+ case 'accordion-panel':
168
+ return null; // Handled by parent
169
+ case 'card':
170
+ case 'flip-card':
171
+ return null; // Handled by parent
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+
177
+ // Helper functions for extracting child content from patterns
178
+ function getChildHeading(el) {
179
+ const heading = el.children?.find(c => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(c.tag));
180
+ return heading?.innerText || '';
181
+ }
182
+
183
+ function getChildParagraph(el) {
184
+ const para = el.children?.find(c => c.tag === 'p');
185
+ return para?.innerText || '';
186
+ }
187
+
188
+ function getChildListItems(el) {
189
+ return el.children?.filter(c => c.tag === 'li').map(c => c.innerText || '') || [];
190
+ }
191
+
192
+ /**
193
+ * Format intro-cards/features as bullet list
194
+ */
195
+ function formatPatternCards(el) {
196
+ if (!el.children || el.children.length === 0) return null;
197
+
198
+ let md = '';
199
+ for (const child of el.children) {
200
+ if (child.className?.includes('card') || child.className?.includes('intro-card') || child.className?.includes('feature')) {
201
+ const title = getChildHeading(child);
202
+ const content = getChildParagraph(child);
203
+ if (title) {
204
+ md += `- **${title}**`;
205
+ if (content) md += ` - ${content}`;
206
+ md += '\n';
207
+ } else if (content) {
208
+ md += `- ${content}\n`;
209
+ }
210
+ }
211
+ }
212
+ return md || null;
213
+ }
214
+
215
+ /**
216
+ * Format steps as numbered list
217
+ */
218
+ function formatPatternSteps(el) {
219
+ if (!el.children || el.children.length === 0) return null;
220
+
221
+ let md = '';
222
+ let stepNum = 1;
223
+ for (const child of el.children) {
224
+ if (child.className?.includes('step')) {
225
+ const title = getChildHeading(child);
226
+ const content = getChildParagraph(child);
227
+ if (title || content) {
228
+ md += `${stepNum}. `;
229
+ if (title) md += `**${title}**`;
230
+ if (title && content) md += ' - ';
231
+ if (content) md += content;
232
+ md += '\n';
233
+ stepNum++;
234
+ }
235
+ }
236
+ }
237
+ return md || null;
238
+ }
239
+
240
+ /**
241
+ * Format timeline as dated entries
242
+ */
243
+ function formatPatternTimeline(el) {
244
+ if (!el.children || el.children.length === 0) return null;
245
+
246
+ let md = '';
247
+ for (const child of el.children) {
248
+ if (child.className?.includes('event') || child.className?.includes('timeline')) {
249
+ const date = child.attributes?.['data-date'] || child.attributes?.['data-year'] || '';
250
+ const title = getChildHeading(child);
251
+ const content = getChildParagraph(child);
252
+ if (date || title || content) {
253
+ md += '- ';
254
+ if (date) md += `**${date}**: `;
255
+ if (title) md += title;
256
+ if (title && content) md += ' - ';
257
+ if (content) md += content;
258
+ md += '\n';
259
+ }
260
+ }
261
+ }
262
+ return md || null;
263
+ }
264
+
265
+ /**
266
+ * Format comparison as columns
267
+ */
268
+ function formatPatternComparison(el) {
269
+ if (!el.children || el.children.length < 2) return null;
270
+
271
+ let md = '';
272
+ for (const child of el.children) {
273
+ const title = getChildHeading(child);
274
+ const items = getChildListItems(child);
275
+ if (title) md += `**${title}**\n`;
276
+ for (const item of items) {
277
+ md += `- ${item}\n`;
278
+ }
279
+ md += '\n';
280
+ }
281
+ return md || null;
282
+ }
283
+
284
+ /**
285
+ * Format stats as key metrics
286
+ */
287
+ function formatPatternStats(el) {
288
+ if (!el.children || el.children.length === 0) return null;
289
+
290
+ let md = '';
291
+ for (const child of el.children) {
292
+ if (child.className?.includes('stat')) {
293
+ const title = getChildHeading(child);
294
+ const content = getChildParagraph(child);
295
+ if (title || content) {
296
+ md += `- **${title || ''}**`;
297
+ if (content) md += ` - ${content}`;
298
+ md += '\n';
299
+ }
300
+ }
301
+ }
302
+ return md || null;
303
+ }
304
+
305
+ /**
306
+ * Format checklist as checked items
307
+ */
308
+ function formatPatternChecklist(el) {
309
+ const items = getChildListItems(el);
310
+ if (items.length === 0) return null;
311
+
312
+ let md = '';
313
+ for (const item of items) {
314
+ md += `- [x] ${item}\n`;
315
+ }
316
+ return md;
317
+ }
318
+
319
+ /**
320
+ * Format hero section
321
+ */
322
+ function formatPatternHero(el) {
323
+ const title = getChildHeading(el);
324
+ const content = getChildParagraph(el);
325
+ if (!title && !content) return null;
326
+
327
+ let md = '';
328
+ if (title) md += `### ${title}\n\n`;
329
+ if (content) md += `${content}\n`;
330
+ return md;
331
+ }
332
+
333
+ /**
334
+ * Format tabs as expandable sections
335
+ */
336
+ function formatPatternTabs(el) {
337
+ if (!el.children || el.children.length === 0) return null;
338
+
339
+ let md = '';
340
+ for (const child of el.children) {
341
+ const title = child.attributes?.['data-tab'] || child.attributes?.['data-title'] || 'Tab';
342
+ const content = child.innerText || '';
343
+ md += `<details>\n<summary>${title}</summary>\n\n${content}\n\n</details>\n\n`;
344
+ }
345
+ return md;
346
+ }
347
+
348
+ /**
349
+ * Convert elements to Markdown
350
+ * @param {Array} elements - Parsed elements from course-parser
351
+ * @param {object} options - { skipHeader: true to skip title/description }
352
+ * @returns {string}
353
+ */
354
+ function elementsToMarkdown(elements, options = {}) {
355
+ const { skipHeader = true } = options;
356
+ const lines = [];
357
+ const containerPaths = [];
358
+
359
+ const containerSemantics = new Set([
360
+ 'intro-cards',
361
+ 'features',
362
+ 'steps',
363
+ 'timeline',
364
+ 'comparison',
365
+ 'stats',
366
+ 'checklist',
367
+ 'hero',
368
+ 'tabs',
369
+ 'accordion',
370
+ ]);
371
+
372
+ const isInsideHandledContainer = (path) => {
373
+ for (const containerPath of containerPaths) {
374
+ if (path.startsWith(containerPath + '/')) {
375
+ return true;
376
+ }
377
+ }
378
+ return false;
379
+ };
380
+
381
+ for (const el of elements) {
382
+ // Skip header elements if they're handled separately
383
+ if (skipHeader && (el.semantic === 'title' || el.semantic === 'description')) {
384
+ continue;
385
+ }
386
+
387
+ if (isInsideHandledContainer(el.path)) {
388
+ continue;
389
+ }
390
+
391
+ const md = formatElementAsMarkdown(el);
392
+ if (md) {
393
+ lines.push(md);
394
+ if (containerSemantics.has(el.semantic)) {
395
+ containerPaths.push(el.path);
396
+ }
397
+ }
398
+ }
399
+
400
+ return lines.join('\n');
401
+ }
402
+
403
+ /**
404
+ * Format course metadata section
405
+ * @param {Object} config - Course configuration
406
+ * @returns {string} Markdown
407
+ */
408
+ function formatMetadata(config) {
409
+ const meta = config.metadata || {};
410
+ let md = `# ${meta.title || 'Untitled Course'}\n`;
411
+
412
+ if (meta.description) {
413
+ md += `> ${meta.description}\n`;
414
+ }
415
+
416
+ md += '\n';
417
+
418
+ if (meta.version) md += `**Version:** ${meta.version} \n`;
419
+ if (meta.author) md += `**Author:** ${meta.author} \n`;
420
+ if (meta.language) md += `**Language:** ${meta.language}\n`;
421
+
422
+ return md;
423
+ }
424
+
425
+ /**
426
+ * Generate structure overview with markdown links and filenames
427
+ * @param {Array} structure - Course structure
428
+ * @param {number} depth - Current nesting depth
429
+ * @returns {string} Structure tree
430
+ */
431
+ function generateStructureOverview(structure, depth = 0) {
432
+ const lines = [];
433
+ const indent = ' '.repeat(depth);
434
+
435
+ structure.forEach(item => {
436
+ const menu = item.menu || {};
437
+ // Note: menu.icon contains icon identifiers (e.g., 'refresh-cw', 'user') meant for
438
+ // rendering with an icon library - we don't include these in markdown output since
439
+ // they can't be rendered and the type indicators (📄, 📂, 📝) already serve this purpose
440
+ const label = menu.label || item.title || item.id;
441
+
442
+ let notes = [];
443
+ if (menu.hidden) notes.push('hidden');
444
+ if (item.engagement?.required) notes.push('engagement required');
445
+ if (item.navigation?.gating) notes.push('gated');
446
+ if (item.navigation?.sequence?.includeByDefault === false) notes.push('conditional');
447
+
448
+ const notesStr = notes.length > 0 ? ` *(${notes.join(', ')})*` : '';
449
+
450
+ // Extract filename from component path for display
451
+ const filename = item.component ? item.component.replace(/^@slides\//, '') : null;
452
+ const filenameStr = filename ? ` \`${filename}\`` : '';
453
+
454
+ if (item.type === 'section') {
455
+ // Sections link to their section header using ID
456
+ const sectionAnchor = `#section-${item.id}`;
457
+ lines.push(`${indent}- **📂 [${label}](${sectionAnchor})**`);
458
+ if (item.children) {
459
+ lines.push(generateStructureOverview(item.children, depth + 1));
460
+ }
461
+ } else if (item.type === 'assessment') {
462
+ // Assessments link to their slide header
463
+ const assessmentAnchor = `#slide-${item.id}`;
464
+ lines.push(`${indent}- 📝 [${label}](${assessmentAnchor})${filenameStr}${notesStr}`);
465
+ } else {
466
+ // Regular slides link to their slide header
467
+ const slideAnchor = `#slide-${item.id}`;
468
+ lines.push(`${indent}- 📄 [${label}](${slideAnchor})${filenameStr}${notesStr}`);
469
+ }
470
+ });
471
+
472
+ return lines.join('\n');
473
+ }
474
+
475
+ /**
476
+ * Format engagement requirements
477
+ * @param {Object} engagement - Engagement config
478
+ * @returns {string} Human-readable description
479
+ */
480
+ function formatEngagementDescription(engagement) {
481
+ if (!engagement?.required || !engagement.requirements?.length) {
482
+ return '';
483
+ }
484
+
485
+ const descriptions = engagement.requirements.map(req => {
486
+ switch (req.type) {
487
+ case 'viewAllTabs': return 'View all tabs';
488
+ case 'viewAllPanels': return 'View all accordion panels';
489
+ case 'viewAllFlipCards': return 'View all flip cards';
490
+ case 'viewAllHotspots': return 'View all hotspots';
491
+ case 'interactionComplete': return `Complete: ${req.interactionId}`;
492
+ case 'allInteractionsComplete': return 'Complete all interactions';
493
+ case 'slideAudioComplete': return 'Listen to slide audio';
494
+ case 'audioComplete': return `Listen to audio: ${req.audioId}`;
495
+ case 'modalAudioComplete': return `Listen to modal audio: ${req.modalId}`;
496
+ case 'scrollDepth': return `Scroll to ${req.percentage}%`;
497
+ case 'timeOnSlide': return `Spend ${req.minSeconds}s on slide`;
498
+ case 'flag': return `Flag: ${req.key}`;
499
+ default: return req.message || req.type;
500
+ }
501
+ });
502
+
503
+ return descriptions.join(', ');
504
+ }
505
+
506
+ /**
507
+ * Format gating conditions
508
+ * @param {Object} gating - Gating config
509
+ * @returns {string} Human-readable description
510
+ */
511
+ function formatGatingDescription(gating) {
512
+ if (!gating?.conditions?.length) return '';
513
+
514
+ const descriptions = gating.conditions.map(cond => {
515
+ switch (cond.type) {
516
+ case 'objectiveStatus':
517
+ return `Objective "${cond.objectiveId}" ${cond.completion_status || cond.success_status || 'completed'}`;
518
+ case 'assessmentStatus':
519
+ return `Assessment "${cond.assessmentId}" ${cond.requires}`;
520
+ case 'timeOnSlide':
521
+ return `${cond.minSeconds}s on slide "${cond.slideId}"`;
522
+ case 'flag':
523
+ return `Flag "${cond.key}" = ${cond.equals ?? true}`;
524
+ default:
525
+ return JSON.stringify(cond);
526
+ }
527
+ });
528
+
529
+ const mode = gating.mode === 'any' ? 'any of' : 'all of';
530
+ return `Requires ${mode}: ${descriptions.join('; ')}`;
531
+ }
532
+
533
+ /**
534
+ * Format a slide for export
535
+ * @param {Object} item - Slide item from structure
536
+ * @param {string} coursePath - Course path
537
+ * @param {Object} options - Export options
538
+ * @returns {string} Markdown for slide
539
+ */
540
+ function formatSlide(item, coursePath, options) {
541
+ const { includeAnswers, includeNarration, includeFeedback, excludeInteractions, includeAnchors } = options;
542
+
543
+ const menu = item.menu || {};
544
+ const title = item.title || menu.label || item.id;
545
+ const anchor = includeAnchors ? `<a id="slide-${item.id}"></a>\n\n` : '';
546
+ let md = `\n---\n\n${anchor}# ${title}\n`;
547
+ md += `**ID:** \`${item.id}\` | **Component:** \`${item.component || 'N/A'}\`\n\n`;
548
+
549
+ // Menu info (only if different from title or has special properties)
550
+ if (menu.hidden) {
551
+ md += '**Menu:** *(hidden)*\n';
552
+ } else if (menu.icon) {
553
+ md += `**Menu:** ${menu.icon} ${menu.label || title}\n`;
554
+ }
555
+
556
+ // Audio
557
+ if (item.audio?.src) {
558
+ md += `**Audio:** \`${item.audio.src}\`\n`;
559
+ }
560
+
561
+ // Engagement
562
+ const engagementDesc = formatEngagementDescription(item.engagement);
563
+ if (engagementDesc) {
564
+ md += `**Engagement:** ${engagementDesc}\n`;
565
+ }
566
+
567
+ // Gating
568
+ const gatingDesc = formatGatingDescription(item.navigation?.gating);
569
+ if (gatingDesc) {
570
+ md += `**Gating:** ${gatingDesc}\n`;
571
+ }
572
+
573
+ // Read and parse source
574
+ const source = readSlideSource(item.component, coursePath);
575
+ if (!source) {
576
+ md += '\n*[Source not available]*\n';
577
+ return md;
578
+ }
579
+
580
+ let parsed;
581
+ try {
582
+ parsed = parseSlideSource(source, item.id);
583
+ } catch (err) {
584
+ md += `\n*[Error parsing slide: ${err.message}]*\n`;
585
+ return md;
586
+ }
587
+
588
+ // Content from parsed elements
589
+ md += '### Content\n\n';
590
+
591
+ const headerMd = headerToMarkdown(parsed.header);
592
+ if (headerMd.trim()) {
593
+ md += headerMd + '\n';
594
+ }
595
+
596
+ if (parsed.elements && parsed.elements.length > 0) {
597
+ const contentMd = elementsToMarkdown(parsed.elements);
598
+ if (contentMd.trim()) {
599
+ md += contentMd + '\n\n';
600
+ }
601
+ } else if (!headerMd.trim()) {
602
+ md += '*[No static content]*\n\n';
603
+ }
604
+
605
+ // Interactions
606
+ if (!excludeInteractions && parsed.interactions && parsed.interactions.length > 0) {
607
+ md += '### Interactions\n';
608
+ for (const interaction of parsed.interactions) {
609
+ md += '\n' + formatInteraction(interaction, { includeAnswers, includeFeedback }) + '\n';
610
+ }
611
+ }
612
+
613
+ // Narration
614
+ if (includeNarration && parsed.narration) {
615
+ md += '\n### Narration\n\n';
616
+ for (const [key, text] of Object.entries(parsed.narration)) {
617
+ if (Object.keys(parsed.narration).length > 1 && key !== 'slide') {
618
+ md += `**${key}:**\n`;
619
+ }
620
+ md += text + '\n\n';
621
+ }
622
+ }
623
+
624
+ md += '\n[↑ Back to Course Structure](#course-structure)\n';
625
+
626
+ return md;
627
+ }
628
+
629
+ /**
630
+ * Format an assessment for export
631
+ * @param {Object} item - Assessment item from structure
632
+ * @param {string} coursePath - Course path
633
+ * @param {Object} options - Export options
634
+ * @returns {string} Markdown for assessment
635
+ */
636
+ function formatAssessment(item, coursePath, options) {
637
+ const { includeAnswers, includeFeedback, excludeInteractions, includeAnchors } = options;
638
+
639
+ const menu = item.menu || {};
640
+ const title = item.title || menu.label || item.id;
641
+ const anchor = includeAnchors ? `<a id="slide-${item.id}"></a>\n\n` : '';
642
+ let md = `\n---\n\n${anchor}# ${title}\n`;
643
+ md += `**ID:** \`${item.id}\` | **Component:** \`${item.component || 'N/A'}\` | **Type:** Assessment\n\n`;
644
+
645
+ // Menu info (only if has icon)
646
+ if (menu.icon) {
647
+ md += `**Menu:** ${menu.icon} ${menu.label || title}\n`;
648
+ }
649
+
650
+ // Read and parse source
651
+ const source = readSlideSource(item.component, coursePath);
652
+ if (!source) {
653
+ md += '\n*[Source not available]*\n';
654
+ return md;
655
+ }
656
+
657
+ let parsed;
658
+ try {
659
+ parsed = extractAssessment(source, item.id);
660
+ } catch (err) {
661
+ md += `\n*[Error parsing assessment: ${err.message}]*\n`;
662
+ return md;
663
+ }
664
+
665
+ if (parsed) {
666
+ if (parsed.title) {
667
+ md += `**Title:** ${parsed.title}\n`;
668
+ }
669
+
670
+ // Settings table
671
+ if (parsed.settings) {
672
+ md += '\n## Settings\n\n';
673
+ md += '| Setting | Value |\n';
674
+ md += '|---------|-------|\n';
675
+
676
+ const s = parsed.settings;
677
+ if (s.passingScore !== undefined) md += `| Passing Score | ${s.passingScore}% |\n`;
678
+ if (s.allowReview !== undefined) md += `| Allow Review | ${s.allowReview ? 'Yes' : 'No'} |\n`;
679
+ if (s.showProgress !== undefined) md += `| Show Progress | ${s.showProgress ? 'Yes' : 'No'} |\n`;
680
+ if (s.allowRetake !== undefined) md += `| Allow Retake | ${s.allowRetake ? 'Yes' : 'No'} |\n`;
681
+ if (s.randomizeQuestions !== undefined) md += `| Randomize Questions | ${s.randomizeQuestions ? 'Yes' : 'No'} |\n`;
682
+ if (s.randomizeOnRetake !== undefined) md += `| Randomize on Retake | ${s.randomizeOnRetake ? 'Yes' : 'No'} |\n`;
683
+ }
684
+ }
685
+
686
+ // Questions
687
+ if (!excludeInteractions && parsed.questions && parsed.questions.length > 0) {
688
+ md += '\n## Questions\n';
689
+
690
+ let qNum = 1;
691
+ for (const q of parsed.questions) {
692
+ md += `\n### Q${qNum}: ${q.id}\n`;
693
+ md += `**Type:** ${formatTypeName(q.type)}\n`;
694
+ if (q.weight !== undefined) md += `**Weight:** ${q.weight}\n`;
695
+ md += '\n';
696
+ md += formatInteraction(q, { includeAnswers, includeFeedback, skipHeader: true });
697
+ qNum++;
698
+ }
699
+ }
700
+
701
+ md += '\n[↑ Back to Course Structure](#course-structure)\n';
702
+
703
+ return md;
704
+ }
705
+
706
+ /**
707
+ * Format interaction type name
708
+ */
709
+ function formatTypeName(type) {
710
+ if (!type) return 'Unknown';
711
+ return type.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
712
+ }
713
+
714
+ /**
715
+ * Format a section for export
716
+ * @param {Object} section - Section from structure
717
+ * @param {string} coursePath - Course path
718
+ * @param {Object} options - Export options
719
+ * @returns {string} Markdown for section
720
+ */
721
+ function formatSection(section, coursePath, options) {
722
+ const { includeAnchors } = options;
723
+ const menu = section.menu || {};
724
+ const label = menu.label || section.id;
725
+
726
+ // Use section ID as anchor target for reliable linking (when includeAnchors is true)
727
+ const anchor = includeAnchors ? `<a id="section-${section.id}"></a>\n\n` : '';
728
+ let md = `\n---\n\n${anchor}# ${label}\n`;
729
+ md += `**ID:** \`${section.id}\` | **Type:** Section\n\n`;
730
+
731
+ // Process children
732
+ for (const child of section.children || []) {
733
+ if (child.type === 'slide') {
734
+ md += formatSlide(child, coursePath, options);
735
+ } else if (child.type === 'assessment') {
736
+ md += formatAssessment(child, coursePath, options);
737
+ } else if (child.type === 'section') {
738
+ md += formatSection(child, coursePath, options);
739
+ }
740
+ }
741
+
742
+ md += '\n[↑ Back to Course Structure](#course-structure)\n';
743
+
744
+ return md;
745
+ }
746
+
747
+ /**
748
+ * Format learning objectives
749
+ * @param {Object} config - Course configuration
750
+ * @returns {string} Markdown
751
+ */
752
+ function formatObjectives(config) {
753
+ const objectives = config.objectives;
754
+ if (!objectives || objectives.length === 0) return '';
755
+
756
+ let md = '\n---\n\n## Learning Objectives\n\n';
757
+ md += '| ID | Description | Criteria |\n';
758
+ md += '|----|-------------|----------|\n';
759
+
760
+ for (const obj of objectives) {
761
+ let criteria = '*Manual*';
762
+ if (obj.criteria) {
763
+ switch (obj.criteria.type) {
764
+ case 'slideVisited':
765
+ criteria = `Slide \`${obj.criteria.slideId}\` visited`;
766
+ break;
767
+ case 'allSlidesVisited':
768
+ criteria = `All slides visited: ${obj.criteria.slideIds.map(s => `\`${s}\``).join(', ')}`;
769
+ break;
770
+ case 'timeOnSlide':
771
+ criteria = `Time on \`${obj.criteria.slideId}\` ≥ ${obj.criteria.minSeconds}s`;
772
+ break;
773
+ case 'flag':
774
+ criteria = `Flag \`${obj.criteria.key}\` = ${obj.criteria.equals ?? true}`;
775
+ break;
776
+ case 'allFlags':
777
+ criteria = 'All flags set';
778
+ break;
779
+ default:
780
+ criteria = obj.criteria.type;
781
+ }
782
+ }
783
+
784
+ md += `| \`${obj.id}\` | ${obj.description} | ${criteria} |\n`;
785
+ }
786
+
787
+ return md;
788
+ }
789
+
790
+ /**
791
+ * Format branding section
792
+ * @param {Object} config - Course configuration
793
+ * @returns {string} Markdown
794
+ */
795
+ function formatBranding(config) {
796
+ const branding = config.branding;
797
+ if (!branding) return '';
798
+
799
+ let md = '\n---\n\n## Branding\n\n';
800
+ md += '| Property | Value |\n';
801
+ md += '|----------|-------|\n';
802
+
803
+ if (branding.companyName) md += `| Company Name | ${branding.companyName} |\n`;
804
+ if (branding.courseTitle) md += `| Course Title | ${branding.courseTitle} |\n`;
805
+ if (branding.logo) md += `| Logo | \`${branding.logo}\` |\n`;
806
+ if (branding.logoAlt) md += `| Logo Alt Text | ${branding.logoAlt} |\n`;
807
+
808
+ return md;
809
+ }
810
+
811
+ /**
812
+ * Format features section
813
+ * @param {Object} config - Course configuration
814
+ * @returns {string} Markdown
815
+ */
816
+ function formatFeatures(config) {
817
+ const features = config.features;
818
+ if (!features) return '';
819
+
820
+ let md = '\n---\n\n## Features\n\n';
821
+ md += '| Feature | Enabled |\n';
822
+ md += '|---------|--------|\n';
823
+
824
+ if (features.accessibility) {
825
+ const a11y = features.accessibility;
826
+ if (a11y.darkMode !== undefined) md += `| Dark Mode | ${a11y.darkMode ? '✓' : '✗'} |\n`;
827
+ if (a11y.fontSize !== undefined) md += `| Font Size Control | ${a11y.fontSize ? '✓' : '✗'} |\n`;
828
+ if (a11y.highContrast !== undefined) md += `| High Contrast | ${a11y.highContrast ? '✓' : '✗'} |\n`;
829
+ if (a11y.reducedMotion !== undefined) md += `| Reduced Motion | ${a11y.reducedMotion ? '✓' : '✗'} |\n`;
830
+ }
831
+
832
+ if (features.security !== undefined) md += `| Secure Assessment Mode | ${features.security ? '✓' : '✗'} |\n`;
833
+ if (features.offline !== undefined) md += `| Offline Mode | ${features.offline ? '✓' : '✗'} |\n`;
834
+ if (features.analytics !== undefined) md += `| Learning Analytics | ${features.analytics ? '✓' : '✗'} |\n`;
835
+
836
+ return md;
837
+ }
838
+
839
+ /**
840
+ * Filter structure to only include specified slide IDs
841
+ * @param {Array} structure - Course structure
842
+ * @param {Array} slideIds - Slide IDs to include
843
+ * @returns {Array} Filtered structure
844
+ */
845
+ function filterStructure(structure, slideIds) {
846
+ const result = [];
847
+
848
+ for (const item of structure) {
849
+ if (item.type === 'section') {
850
+ const filteredChildren = filterStructure(item.children || [], slideIds);
851
+ if (filteredChildren.length > 0) {
852
+ result.push({ ...item, children: filteredChildren });
853
+ }
854
+ } else if (slideIds.includes(item.id)) {
855
+ result.push(item);
856
+ }
857
+ }
858
+
859
+ return result;
860
+ }
861
+
862
+ /**
863
+ * Collect all interactions from the course structure
864
+ * @param {Array} structure - Course structure
865
+ * @param {string} coursePath - Path to course directory
866
+ * @returns {Object} Object with slideInteractions and assessmentQuestions arrays
867
+ */
868
+ function collectAllInteractions(structure, coursePath) {
869
+ const slideInteractions = [];
870
+ const assessmentQuestions = [];
871
+
872
+ function processItem(item) {
873
+ if (item.type === 'section') {
874
+ for (const child of item.children || []) {
875
+ processItem(child);
876
+ }
877
+ } else if (item.type === 'assessment') {
878
+ const source = readSlideSource(item.component, coursePath);
879
+ if (source) {
880
+ const parsed = extractAssessment(source, item.id);
881
+ if (parsed.questions && parsed.questions.length > 0) {
882
+ assessmentQuestions.push({
883
+ slideId: item.id,
884
+ slideTitle: item.title || item.id,
885
+ assessmentId: parsed.id || item.id,
886
+ assessmentTitle: parsed.title || item.title,
887
+ settings: parsed.settings || {},
888
+ questions: parsed.questions
889
+ });
890
+ }
891
+ }
892
+ } else if (item.type === 'slide') {
893
+ const source = readSlideSource(item.component, coursePath);
894
+ if (source) {
895
+ const parsed = parseSlideSource(source, item.id);
896
+ if (parsed.interactions && parsed.interactions.length > 0) {
897
+ slideInteractions.push({
898
+ slideId: item.id,
899
+ slideTitle: item.title || item.id,
900
+ interactions: parsed.interactions
901
+ });
902
+ }
903
+ }
904
+ }
905
+ }
906
+
907
+ for (const item of structure) {
908
+ processItem(item);
909
+ }
910
+
911
+ return { slideInteractions, assessmentQuestions };
912
+ }
913
+
914
+ /**
915
+ * Format interactions-only Markdown output
916
+ * @param {Object} config - Course configuration
917
+ * @param {string} coursePath - Path to course directory
918
+ * @param {Array} structure - Course structure (possibly filtered)
919
+ * @param {Object} options - Export options
920
+ * @returns {string} Markdown output
921
+ */
922
+ function formatInteractionsOnlyMarkdown(config, coursePath, structure, options) {
923
+ const { includeAnswers, includeFeedback } = options;
924
+ const { slideInteractions, assessmentQuestions } = collectAllInteractions(structure, coursePath);
925
+
926
+ const meta = config.metadata || {};
927
+ let md = `# ${meta.title || 'Untitled Course'} - Interactions Export\n\n`;
928
+ md += '> This document contains only the interactions and assessment questions from the course.\n\n';
929
+
930
+ // Summary counts
931
+ const totalSlideInteractions = slideInteractions.reduce((sum, s) => sum + s.interactions.length, 0);
932
+ const totalAssessmentQuestions = assessmentQuestions.reduce((sum, a) => sum + a.questions.length, 0);
933
+
934
+ md += '**Summary:**\n';
935
+ md += `- Slides with interactions: ${slideInteractions.length}\n`;
936
+ md += `- Total slide interactions: ${totalSlideInteractions}\n`;
937
+ md += `- Assessments: ${assessmentQuestions.length}\n`;
938
+ md += `- Total assessment questions: ${totalAssessmentQuestions}\n`;
939
+ md += `- **Grand total: ${totalSlideInteractions + totalAssessmentQuestions} items**\n`;
940
+
941
+ // Table of contents
942
+ md += '\n---\n\n## Table of Contents\n\n';
943
+
944
+ if (slideInteractions.length > 0) {
945
+ md += '### Slide Interactions\n\n';
946
+ for (const slide of slideInteractions) {
947
+ const anchor = `slide-${slide.slideId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
948
+ md += `- [${slide.slideTitle}](#${anchor}) (${slide.interactions.length} interaction${slide.interactions.length !== 1 ? 's' : ''})\n`;
949
+ }
950
+ md += '\n';
951
+ }
952
+
953
+ if (assessmentQuestions.length > 0) {
954
+ md += '### Assessments\n\n';
955
+ for (const assessment of assessmentQuestions) {
956
+ const anchor = `assessment-${assessment.assessmentId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
957
+ md += `- [${assessment.assessmentTitle || assessment.slideTitle}](#${anchor}) (${assessment.questions.length} question${assessment.questions.length !== 1 ? 's' : ''})\n`;
958
+ }
959
+ md += '\n';
960
+ }
961
+
962
+ // Slide Interactions
963
+ if (slideInteractions.length > 0) {
964
+ md += '\n---\n\n# Slide Interactions\n\n';
965
+
966
+ for (const slide of slideInteractions) {
967
+ md += `## Slide: ${slide.slideId}\n`;
968
+ md += `**Title:** ${slide.slideTitle}\n\n`;
969
+
970
+ for (const interaction of slide.interactions) {
971
+ md += formatInteraction(interaction, { includeAnswers, includeFeedback });
972
+ md += '\n';
973
+ }
974
+
975
+ md += '---\n\n';
976
+ }
977
+ }
978
+
979
+ // Assessment Questions
980
+ if (assessmentQuestions.length > 0) {
981
+ md += '\n---\n\n# Assessment Questions\n\n';
982
+
983
+ for (const assessment of assessmentQuestions) {
984
+ md += `## Assessment: ${assessment.assessmentId}\n`;
985
+ md += `**Title:** ${assessment.assessmentTitle || assessment.slideTitle}\n`;
986
+
987
+ // Settings summary
988
+ const s = assessment.settings;
989
+ if (s.passingScore !== undefined) {
990
+ md += `**Passing Score:** ${s.passingScore}%\n`;
991
+ }
992
+ md += '\n';
993
+
994
+ let qNum = 1;
995
+ for (const q of assessment.questions) {
996
+ md += `### Q${qNum}: ${q.id}\n`;
997
+ md += `**Type:** ${formatTypeName(q.type)}\n`;
998
+ if (q.weight !== undefined) md += `**Weight:** ${q.weight}\n`;
999
+ md += '\n';
1000
+ md += formatInteraction(q, { includeAnswers, includeFeedback });
1001
+ md += '\n';
1002
+ qNum++;
1003
+ }
1004
+
1005
+ md += '---\n\n';
1006
+ }
1007
+ }
1008
+
1009
+ // No interactions found
1010
+ if (slideInteractions.length === 0 && assessmentQuestions.length === 0) {
1011
+ md += '\n---\n\n*No interactions or assessment questions found in the selected content.*\n';
1012
+ }
1013
+
1014
+ return md;
1015
+ }
1016
+
1017
+ /**
1018
+ * Main export function
1019
+ * @param {Object} options - Command options
1020
+ */
1021
+ export async function exportContent(options = {}) {
1022
+ const {
1023
+ output = null,
1024
+ includeAnswers = true,
1025
+ includeNarration = false,
1026
+ includeFeedback = true,
1027
+ excludeInteractions = false,
1028
+ interactionsOnly = false,
1029
+ includeAnchors = true,
1030
+ slides = null,
1031
+ format = 'md',
1032
+ coursePath = './course'
1033
+ } = options;
1034
+
1035
+ // Validate project
1036
+ const fullCoursePath = validateProject(coursePath);
1037
+
1038
+ console.log('\n📄 Exporting course content (v2)...\n');
1039
+
1040
+ // Load course config
1041
+ const config = await loadCourseConfig(fullCoursePath);
1042
+
1043
+ let structure = config.structure || [];
1044
+ if (slides) {
1045
+ const slideIds = slides.split(',').map(s => s.trim());
1046
+ structure = filterStructure(structure, slideIds);
1047
+ }
1048
+
1049
+ const exportOptions = {
1050
+ includeAnswers,
1051
+ includeNarration,
1052
+ includeFeedback,
1053
+ excludeInteractions,
1054
+ interactionsOnly,
1055
+ includeAnchors
1056
+ };
1057
+
1058
+ let result;
1059
+
1060
+ if (format === 'json') {
1061
+ // JSON output
1062
+ result = JSON.stringify(await generateJsonOutput(config, fullCoursePath, structure, exportOptions), null, 2);
1063
+ } else if (interactionsOnly) {
1064
+ // Interactions-only Markdown output
1065
+ result = formatInteractionsOnlyMarkdown(config, fullCoursePath, structure, exportOptions);
1066
+ } else {
1067
+ // Markdown output
1068
+ let md = '';
1069
+
1070
+ // Metadata
1071
+ md += formatMetadata(config);
1072
+
1073
+ // Structure overview
1074
+ md += '\n---\n\n## Course Structure\n\n';
1075
+ md += generateStructureOverview(structure);
1076
+ md += '\n';
1077
+
1078
+ // Learning Objectives
1079
+ md += formatObjectives(config);
1080
+
1081
+ // Process each item in structure
1082
+ for (const item of structure) {
1083
+ if (item.type === 'slide') {
1084
+ md += formatSlide(item, fullCoursePath, exportOptions);
1085
+ } else if (item.type === 'assessment') {
1086
+ md += formatAssessment(item, fullCoursePath, exportOptions);
1087
+ } else if (item.type === 'section') {
1088
+ md += formatSection(item, fullCoursePath, exportOptions);
1089
+ }
1090
+ }
1091
+
1092
+ // Configuration sections at end
1093
+ md += '\n---\n\n# Configuration\n';
1094
+ md += formatBranding(config);
1095
+ md += formatFeatures(config);
1096
+
1097
+ result = md;
1098
+ }
1099
+
1100
+ // Output result
1101
+ if (output) {
1102
+ const outputPath = path.isAbsolute(output) ? output : path.join(process.cwd(), output);
1103
+ fs.writeFileSync(outputPath, result, 'utf-8');
1104
+ console.log(`✅ Content exported to: ${outputPath}\n`);
1105
+ } else {
1106
+ console.log(result);
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * Programmatic content export (for MCP, preview-server, preview-export)
1112
+ * Returns the content as a string instead of writing to files/stdout.
1113
+ *
1114
+ * @param {Object} options - Export options
1115
+ * @param {string} [options.coursePath='./course'] - Path to course directory
1116
+ * @param {boolean} [options.includeAnswers=true] - Include correct answers
1117
+ * @param {boolean} [options.includeNarration=false] - Include narration text
1118
+ * @param {boolean} [options.includeFeedback=true] - Include feedback text
1119
+ * @param {boolean} [options.excludeInteractions=false] - Exclude interactions
1120
+ * @param {boolean} [options.interactionsOnly=false] - Only interactions/assessments
1121
+ * @param {boolean} [options.includeAnchors=true] - HTML anchors for linking
1122
+ * @param {string} [options.slides] - Comma-separated slide IDs
1123
+ * @param {string} [options.format='md'] - Output format: 'md' or 'json'
1124
+ * @returns {Promise<string|null>} Content string (Markdown or JSON) or null on error
1125
+ */
1126
+ export async function getContentExport(options = {}) {
1127
+ const {
1128
+ coursePath = './course',
1129
+ includeAnswers = true,
1130
+ includeNarration = false,
1131
+ includeFeedback = true,
1132
+ excludeInteractions = false,
1133
+ interactionsOnly = false,
1134
+ includeAnchors = true,
1135
+ slides = null,
1136
+ format = 'md'
1137
+ } = options;
1138
+
1139
+ const fullCoursePath = validateProject(coursePath, true);
1140
+ if (!fullCoursePath) return null;
1141
+
1142
+ try {
1143
+ const config = await loadCourseConfig(fullCoursePath);
1144
+
1145
+ let structure = config.structure || [];
1146
+ if (slides) {
1147
+ const slideIds = (typeof slides === 'string' ? slides.split(',') : slides).map(s => s.trim());
1148
+ structure = filterStructure(structure, slideIds);
1149
+ }
1150
+
1151
+ const exportOptions = {
1152
+ includeAnswers,
1153
+ includeNarration,
1154
+ includeFeedback,
1155
+ excludeInteractions,
1156
+ interactionsOnly,
1157
+ includeAnchors
1158
+ };
1159
+
1160
+ if (format === 'json') {
1161
+ return JSON.stringify(await generateJsonOutput(config, fullCoursePath, structure, exportOptions), null, 2);
1162
+ }
1163
+
1164
+ if (interactionsOnly) {
1165
+ return formatInteractionsOnlyMarkdown(config, fullCoursePath, structure, exportOptions);
1166
+ }
1167
+
1168
+ let md = '';
1169
+ md += formatMetadata(config);
1170
+ md += '\n---\n\n## Course Structure\n\n';
1171
+ md += generateStructureOverview(structure);
1172
+ md += '\n';
1173
+ md += formatObjectives(config);
1174
+
1175
+ for (const item of structure) {
1176
+ if (item.type === 'slide') {
1177
+ md += formatSlide(item, fullCoursePath, exportOptions);
1178
+ } else if (item.type === 'assessment') {
1179
+ md += formatAssessment(item, fullCoursePath, exportOptions);
1180
+ } else if (item.type === 'section') {
1181
+ md += formatSection(item, fullCoursePath, exportOptions);
1182
+ }
1183
+ }
1184
+
1185
+ md += '\n---\n\n# Configuration\n';
1186
+ md += formatBranding(config);
1187
+ md += formatFeatures(config);
1188
+
1189
+ return md;
1190
+ } catch (error) {
1191
+ console.error('Failed to generate content export:', error.message);
1192
+ return null;
1193
+ }
1194
+ }
1195
+
1196
+ /**
1197
+ * Generate JSON output
1198
+ */
1199
+ async function generateJsonOutput(config, coursePath, structure, options) {
1200
+ const output = {
1201
+ metadata: config.metadata || {},
1202
+ branding: config.branding || {},
1203
+ features: config.features || {},
1204
+ objectives: config.objectives || [],
1205
+ structure: []
1206
+ };
1207
+
1208
+ function processItem(item) {
1209
+ const entry = {
1210
+ type: item.type,
1211
+ id: item.id,
1212
+ title: item.title,
1213
+ menu: item.menu
1214
+ };
1215
+
1216
+ if (item.type === 'section') {
1217
+ entry.children = (item.children || []).map(processItem);
1218
+ } else if (item.type === 'slide' || item.type === 'assessment') {
1219
+ const source = readSlideSource(item.component, coursePath);
1220
+ if (source) {
1221
+ if (item.type === 'assessment') {
1222
+ const parsed = extractAssessment(source, item.id);
1223
+ entry.config = {
1224
+ id: parsed.id,
1225
+ title: parsed.title,
1226
+ settings: parsed.settings
1227
+ };
1228
+ entry.questions = parsed.questions;
1229
+ } else {
1230
+ const parsed = parseSlideSource(source, item.id);
1231
+ entry.content = parsed.elements || [];
1232
+ entry.interactions = parsed.interactions;
1233
+ if (options.includeNarration) {
1234
+ entry.narration = parsed.narration;
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ return entry;
1241
+ }
1242
+
1243
+ output.structure = structure.map(processItem);
1244
+
1245
+ return output;
1246
+ }