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,438 @@
1
+ /**
2
+ * @file scorm-validators.js
3
+ * @description SCORM 2004 4th Edition data format constants and validation utilities.
4
+ * Ensures data conforms to SCORM 2004 specifications.
5
+ */
6
+
7
+ /**
8
+ * SCORM 2004 4th Edition valid interaction types.
9
+ * @constant {string[]}
10
+ */
11
+ export const SCORM_INTERACTION_TYPES = [
12
+ 'true-false',
13
+ 'choice',
14
+ 'fill-in',
15
+ 'long-fill-in',
16
+ 'matching',
17
+ 'performance',
18
+ 'sequencing',
19
+ 'likert',
20
+ 'numeric',
21
+ 'other'
22
+ ];
23
+
24
+ /**
25
+ * SCORM 2004 4th Edition valid interaction result values.
26
+ * @constant {string[]}
27
+ */
28
+ export const SCORM_INTERACTION_RESULTS = [
29
+ 'correct',
30
+ 'incorrect',
31
+ 'unanticipated',
32
+ 'neutral'
33
+ ];
34
+
35
+ // ============================================================================
36
+ // SCORM 2004 Learner Response Formatting
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Formats a learner response for SCORM 2004 cmi.interactions.n.learner_response.
41
+ * SCORM 2004 requires specific string formats for each interaction type.
42
+ *
43
+ * Format requirements by type:
44
+ * - true-false: "true" or "false" (literal strings)
45
+ * - choice: "a[,]b[,]c" (short identifiers separated by [,])
46
+ * - matching: "source1[.]target1[,]source2[.]target2" (pairs with [.] separator)
47
+ * - sequencing: "item1[,]item2[,]item3" (ordered items separated by [,])
48
+ * - fill-in: plain text string
49
+ * - numeric: numeric string
50
+ * - likert: single character or short identifier
51
+ * - other: any string representation
52
+ *
53
+ * @param {string} interactionType - The SCORM interaction type
54
+ * @param {*} response - The internal response format (varies by type)
55
+ * @returns {string} SCORM 2004 compliant learner_response string
56
+ * @example
57
+ * formatLearnerResponseForScorm('true-false', true) // "true"
58
+ * formatLearnerResponseForScorm('matching', {pair1: 'match1', pair2: 'match2'}) // "pair1[.]match1[,]pair2[.]match2"
59
+ * formatLearnerResponseForScorm('sequencing', ['a', 'b', 'c']) // "a[,]b[,]c"
60
+ * formatLearnerResponseForScorm('choice', ['a', 'b']) // "a[,]b"
61
+ */
62
+ export function formatLearnerResponseForScorm(interactionType, response) {
63
+ // Handle null/undefined
64
+ if (response === null || response === undefined) {
65
+ return '';
66
+ }
67
+
68
+ switch (interactionType) {
69
+ case 'true-false':
70
+ // Must be literal "true" or "false" string
71
+ if (typeof response === 'boolean') {
72
+ return response ? 'true' : 'false';
73
+ }
74
+ if (typeof response === 'string') {
75
+ const normalized = response.toLowerCase().trim();
76
+ if (normalized === 'true' || normalized === 'false') {
77
+ return normalized;
78
+ }
79
+ }
80
+ // Invalid response for true-false - return empty (will fail validation, but that's correct)
81
+ return '';
82
+
83
+ case 'choice':
84
+ // Format: identifier[,]identifier[,]identifier
85
+ if (Array.isArray(response)) {
86
+ return response.join('[,]');
87
+ }
88
+ if (typeof response === 'string') {
89
+ // Already a string, might be single choice or already formatted
90
+ return response;
91
+ }
92
+ return '';
93
+
94
+ case 'matching':
95
+ // Format: source[.]target[,]source[.]target
96
+ if (typeof response === 'object' && response !== null && !Array.isArray(response)) {
97
+ const pairs = Object.entries(response)
98
+ .filter(([_key, value]) => value !== null && value !== undefined && String(value).trim() !== '')
99
+ .map(([source, target]) => `${source}[.]${target}`);
100
+ return pairs.join('[,]');
101
+ }
102
+ if (typeof response === 'string') {
103
+ // Try to parse as JSON and format
104
+ try {
105
+ const parsed = JSON.parse(response);
106
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
107
+ const pairs = Object.entries(parsed)
108
+ .filter(([_key, value]) => value !== null && value !== undefined && String(value).trim() !== '')
109
+ .map(([source, target]) => `${source}[.]${target}`);
110
+ return pairs.join('[,]');
111
+ }
112
+ } catch {
113
+ // Not JSON, return as-is
114
+ return response;
115
+ }
116
+ }
117
+ return '';
118
+
119
+ case 'sequencing':
120
+ // Format: item[,]item[,]item
121
+ if (Array.isArray(response)) {
122
+ return response.join('[,]');
123
+ }
124
+ if (typeof response === 'string') {
125
+ // Try to parse as JSON array
126
+ try {
127
+ const parsed = JSON.parse(response);
128
+ if (Array.isArray(parsed)) {
129
+ return parsed.join('[,]');
130
+ }
131
+ } catch {
132
+ // Not JSON, return as-is
133
+ return response;
134
+ }
135
+ }
136
+ return '';
137
+
138
+ case 'fill-in':
139
+ case 'long-fill-in':
140
+ // Plain text string
141
+ if (typeof response === 'object') {
142
+ // fill-in might return {blankId: answer} - join values
143
+ const values = Object.values(response).filter(v => v !== null && v !== undefined);
144
+ return values.join('[,]');
145
+ }
146
+ return String(response);
147
+
148
+ case 'numeric':
149
+ // Numeric string or range object
150
+ // SCORM 2004 correct_responses format: "value" or "min[:max]"
151
+ if (typeof response === 'number') {
152
+ return String(response);
153
+ }
154
+ if (typeof response === 'object' && response !== null) {
155
+ // Handle correctRange object: {exact, min, max}
156
+ // Try to parse if it's a JSON string representation
157
+ let rangeObj = response;
158
+ if (typeof response === 'string') {
159
+ try {
160
+ rangeObj = JSON.parse(response);
161
+ } catch {
162
+ return response; // Return as-is if not parseable
163
+ }
164
+ }
165
+
166
+ if (rangeObj.exact !== undefined) {
167
+ return String(rangeObj.exact);
168
+ }
169
+ if (rangeObj.min !== undefined && rangeObj.max !== undefined) {
170
+ return `${rangeObj.min}[:${rangeObj.max}]`;
171
+ }
172
+ if (rangeObj.min !== undefined) {
173
+ return String(rangeObj.min);
174
+ }
175
+ if (rangeObj.max !== undefined) {
176
+ return String(rangeObj.max);
177
+ }
178
+ }
179
+ if (typeof response === 'string') {
180
+ // Try to parse as JSON for correctRange
181
+ try {
182
+ const parsed = JSON.parse(response);
183
+ if (typeof parsed === 'object' && parsed !== null) {
184
+ if (parsed.exact !== undefined) {
185
+ return String(parsed.exact);
186
+ }
187
+ if (parsed.min !== undefined && parsed.max !== undefined) {
188
+ return `${parsed.min}[:${parsed.max}]`;
189
+ }
190
+ if (parsed.min !== undefined) {
191
+ return String(parsed.min);
192
+ }
193
+ if (parsed.max !== undefined) {
194
+ return String(parsed.max);
195
+ }
196
+ }
197
+ } catch {
198
+ // Not JSON, return as-is
199
+ return response;
200
+ }
201
+ }
202
+ return String(response);
203
+
204
+ case 'likert':
205
+ // Single identifier or value
206
+ if (typeof response === 'object' && response !== null) {
207
+ // Likert might return {questionId: value} - use first value
208
+ const values = Object.values(response);
209
+ return values.length > 0 ? String(values[0]) : '';
210
+ }
211
+ return String(response);
212
+
213
+ case 'performance':
214
+ case 'other':
215
+ default:
216
+ // For other/performance types, convert to string representation
217
+ if (typeof response === 'object') {
218
+ try {
219
+ // If it's an empty object, return empty string
220
+ if (Object.keys(response).length === 0) {
221
+ return '';
222
+ }
223
+ return JSON.stringify(response);
224
+ } catch {
225
+ return '';
226
+ }
227
+ }
228
+ return String(response);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * SCORM 2004 4th Edition valid completion status values.
234
+ * @constant {string[]}
235
+ */
236
+ export const SCORM_COMPLETION_STATUS = [
237
+ 'completed',
238
+ 'incomplete',
239
+ 'not attempted',
240
+ 'unknown'
241
+ ];
242
+
243
+ /**
244
+ * SCORM 2004 4th Edition valid success status values.
245
+ * @constant {string[]}
246
+ */
247
+ export const SCORM_SUCCESS_STATUS = [
248
+ 'passed',
249
+ 'failed',
250
+ 'unknown'
251
+ ];
252
+
253
+ /**
254
+ * Generates a SCORM 2004 4th Edition compliant timestamp for the current time.
255
+ * SCORM 2004 requires: YYYY-MM-DDTHH:MM:SS (no milliseconds, no Z suffix)
256
+ *
257
+ * @returns {string} SCORM 2004 compliant timestamp (e.g., "2025-01-15T10:30:00")
258
+ * @example
259
+ * generateScormTimestamp() // "2025-01-15T10:30:00"
260
+ */
261
+ export function generateScormTimestamp() {
262
+ const date = new Date();
263
+
264
+ // Format: YYYY-MM-DDTHH:MM:SS (UTC, no milliseconds, no Z suffix)
265
+ const year = date.getUTCFullYear();
266
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
267
+ const day = String(date.getUTCDate()).padStart(2, '0');
268
+ const hours = String(date.getUTCHours()).padStart(2, '0');
269
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
270
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
271
+
272
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
273
+ }
274
+
275
+ /**
276
+ * Validates if a string is a valid ISO 8601 timestamp.
277
+ * @param {string} timestamp - The timestamp to validate
278
+ * @returns {boolean} True if valid ISO 8601 timestamp
279
+ */
280
+ export function isValidISO8601Timestamp(timestamp) {
281
+ if (typeof timestamp !== 'string') {
282
+ return false;
283
+ }
284
+
285
+ try {
286
+ const date = new Date(timestamp);
287
+ // Check if it's a valid date and can be converted back to ISO format
288
+ return date.toISOString() === timestamp || !isNaN(date.getTime());
289
+ } catch {
290
+ return false;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Validates if a string is a valid ISO 8601 duration format.
296
+ * Format: P[nY][nM][nD][T[nH][nM][nS]]
297
+ * @param {string} duration - The duration to validate
298
+ * @returns {boolean} True if valid ISO 8601 duration
299
+ */
300
+ export function isValidISO8601Duration(duration) {
301
+ if (typeof duration !== 'string') {
302
+ return false;
303
+ }
304
+
305
+ // ISO 8601 duration pattern: P[nY][nM][nD][T[nH][nM][nS]]
306
+ // Each component is optional, but at least one must be present
307
+ const durationPattern = /^P(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
308
+
309
+ // Must match pattern and not be just 'P' or 'PT' (empty duration)
310
+ return durationPattern.test(duration) && duration !== 'P' && duration !== 'PT';
311
+ }
312
+
313
+ /**
314
+ * Validates if a value is in an allowed list of values.
315
+ * @param {*} value - The value to validate
316
+ * @param {Array} allowedValues - Array of allowed values
317
+ * @param {string} [fieldName='value'] - Name of the field for error messaging
318
+ * @returns {{valid: boolean, error: string|null}} Validation result
319
+ */
320
+ export function validateEnum(value, allowedValues, fieldName = 'value') {
321
+ if (!allowedValues.includes(value)) {
322
+ return {
323
+ valid: false,
324
+ error: `Invalid ${fieldName} "${value}". Must be one of: ${allowedValues.join(', ')}`
325
+ };
326
+ }
327
+ return { valid: true, error: null };
328
+ }
329
+
330
+ /**
331
+ * Validates if a value is a valid number.
332
+ * @param {*} value - The value to validate
333
+ * @param {string} [fieldName='value'] - Name of the field for error messaging
334
+ * @returns {{valid: boolean, error: string|null}} Validation result
335
+ */
336
+ export function validateNumeric(value, fieldName = 'value') {
337
+ const num = Number(value);
338
+ if (isNaN(num)) {
339
+ return {
340
+ valid: false,
341
+ error: `Invalid ${fieldName} "${value}". Must be a number`
342
+ };
343
+ }
344
+ return { valid: true, error: null };
345
+ }
346
+
347
+ /**
348
+ * Validates if a value is an array.
349
+ * @param {*} value - The value to validate
350
+ * @param {string} [fieldName='value'] - Name of the field for error messaging
351
+ * @returns {{valid: boolean, error: string|null}} Validation result
352
+ */
353
+ export function validateArray(value, fieldName = 'value') {
354
+ if (!Array.isArray(value)) {
355
+ return {
356
+ valid: false,
357
+ error: `Field "${fieldName}" must be an array`
358
+ };
359
+ }
360
+ return { valid: true, error: null };
361
+ }
362
+
363
+ /**
364
+ * Validates if an array contains only string values.
365
+ * @param {Array} arr - The array to validate
366
+ * @param {string} [fieldName='value'] - Name of the field for error messaging
367
+ * @returns {{valid: boolean, error: string|null}} Validation result
368
+ */
369
+ export function validateStringArray(arr, fieldName = 'value') {
370
+ if (!Array.isArray(arr)) {
371
+ return {
372
+ valid: false,
373
+ error: `Field "${fieldName}" must be an array`
374
+ };
375
+ }
376
+
377
+ if (arr.some(item => typeof item !== 'string')) {
378
+ return {
379
+ valid: false,
380
+ error: `Field "${fieldName}" must contain only string values`
381
+ };
382
+ }
383
+
384
+ return { valid: true, error: null };
385
+ }
386
+
387
+ /**
388
+ * Validates required fields are present in an object.
389
+ * @param {Object} data - The data object to validate
390
+ * @param {string[]} requiredFields - Array of required field names
391
+ * @returns {{valid: boolean, errors: string[]}} Validation result with array of missing fields
392
+ */
393
+ export function validateRequiredFields(data, requiredFields) {
394
+ const errors = [];
395
+
396
+ for (const field of requiredFields) {
397
+ if (data[field] === undefined || data[field] === null || data[field] === '') {
398
+ errors.push(`Missing required field "${field}"`);
399
+ }
400
+ }
401
+
402
+ return {
403
+ valid: errors.length === 0,
404
+ errors
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Creates a detailed validation error message from multiple errors.
410
+ * @param {string[]} errors - Array of error messages
411
+ * @param {string} [context=''] - Optional context (e.g., record ID)
412
+ * @param {string} [prefix='Validation failed'] - Error message prefix
413
+ * @returns {string} Formatted error message
414
+ */
415
+ export function formatValidationError(errors, context = '', prefix = 'Validation failed') {
416
+ const contextStr = context ? ` for ${context}` : '';
417
+ return `${prefix}${contextStr}:\n - ${errors.join('\n - ')}`;
418
+ }
419
+
420
+ /** Convenience function to validate SCORM interaction type. */
421
+ export function validateInteractionType(type) {
422
+ return validateEnum(type, SCORM_INTERACTION_TYPES, 'type');
423
+ }
424
+
425
+ /** Convenience function to validate SCORM interaction result. */
426
+ export function validateInteractionResult(result) {
427
+ return validateEnum(result, SCORM_INTERACTION_RESULTS, 'result');
428
+ }
429
+
430
+ /** Convenience function to validate SCORM completion status. */
431
+ export function validateCompletionStatus(status) {
432
+ return validateEnum(status, SCORM_COMPLETION_STATUS, 'completion_status');
433
+ }
434
+
435
+ /** Convenience function to validate SCORM success status. */
436
+ export function validateSuccessStatus(status) {
437
+ return validateEnum(status, SCORM_SUCCESS_STATUS, 'success_status');
438
+ }