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,568 @@
1
+ /**
2
+ * Interaction Formatters
3
+ * Type-specific formatters for converting interaction configs to Markdown/JSON
4
+ */
5
+
6
+ /**
7
+ * Format feedback using GitHub-style alerts for better visual presentation
8
+ * @param {Object} feedback - Feedback object with correct/incorrect messages
9
+ * @returns {string} Formatted markdown with alerts
10
+ */
11
+ function formatFeedbackBlock(feedback) {
12
+ if (!feedback) return '';
13
+
14
+ let md = '\n';
15
+ if (feedback.correct) {
16
+ md += `> [!TIP] ✓ Correct\n> ${feedback.correct}\n\n`;
17
+ }
18
+ if (feedback.incorrect) {
19
+ md += `> [!CAUTION] ✗ Incorrect\n> ${feedback.incorrect}\n\n`;
20
+ }
21
+ return md;
22
+ }
23
+
24
+ /**
25
+ * Format a multiple-choice interaction
26
+ * @param {Object} interaction - The interaction config
27
+ * @param {Object} options - Formatting options
28
+ * @returns {string} Formatted Markdown
29
+ */
30
+ export function formatMultipleChoice(interaction, options = {}) {
31
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
32
+
33
+ let md = '';
34
+ if (!skipHeader) {
35
+ md += `#### ${interaction.id}\n`;
36
+ md += '**Type:** Multiple Choice\n';
37
+ if (interaction.weight) {
38
+ md += `**Weight:** ${interaction.weight}\n`;
39
+ }
40
+ }
41
+
42
+ md += `\n**Prompt:** ${interaction.prompt || '*No prompt*'}\n\n`;
43
+
44
+ const choices = interaction.choices || [];
45
+ if (choices.length > 0) {
46
+ md += `| Choice | Text |${includeAnswers ? ' Correct |' : ''}\n`;
47
+ md += `|--------|------|${includeAnswers ? '---------|' : ''}\n`;
48
+
49
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
50
+ choices.forEach((choice, i) => {
51
+ const letter = letters[i] || `${i + 1}`;
52
+ const correctMark = includeAnswers && choice.correct ? ' ✓ ' : '';
53
+ md += `| ${letter} | ${choice.text || choice.value || ''} |${includeAnswers ? correctMark + '|' : ''}\n`;
54
+ });
55
+ } else {
56
+ md += '*No choices configured*\n';
57
+ }
58
+
59
+ if (includeFeedback && interaction.feedback) {
60
+ md += formatFeedbackBlock(interaction.feedback);
61
+ }
62
+
63
+ return md;
64
+ }
65
+
66
+ /**
67
+ * Format a true/false interaction
68
+ * @param {Object} interaction - The interaction config
69
+ * @param {Object} options - Formatting options
70
+ * @returns {string} Formatted Markdown
71
+ */
72
+ export function formatTrueFalse(interaction, options = {}) {
73
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
74
+
75
+ let md = '';
76
+ if (!skipHeader) {
77
+ md += `#### ${interaction.id}\n`;
78
+ md += '**Type:** True/False\n';
79
+ if (interaction.weight) {
80
+ md += `**Weight:** ${interaction.weight}\n`;
81
+ }
82
+ }
83
+
84
+ md += `\n**Prompt:** ${interaction.prompt}\n`;
85
+
86
+ if (includeAnswers) {
87
+ md += `\n**Correct Answer:** ${interaction.correctAnswer === true || interaction.correctAnswer === 'true' ? 'True' : 'False'}\n`;
88
+ }
89
+
90
+ if (includeFeedback && interaction.feedback) {
91
+ md += formatFeedbackBlock(interaction.feedback);
92
+ }
93
+
94
+ return md;
95
+ }
96
+
97
+ /**
98
+ * Format a fill-in-the-blank interaction
99
+ * @param {Object} interaction - The interaction config
100
+ * @param {Object} options - Formatting options
101
+ * @returns {string} Formatted Markdown
102
+ */
103
+ export function formatFillInBlank(interaction, options = {}) {
104
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
105
+
106
+ let md = '';
107
+ if (!skipHeader) {
108
+ md += `#### ${interaction.id}\n`;
109
+ md += '**Type:** Fill-in-the-Blank\n';
110
+ if (interaction.weight) {
111
+ md += `**Weight:** ${interaction.weight}\n`;
112
+ }
113
+ }
114
+
115
+ // Show template if present
116
+ if (interaction.template) {
117
+ md += `\n**Template:** ${interaction.template}\n\n`;
118
+ } else if (interaction.prompt) {
119
+ md += `\n**Prompt:** ${interaction.prompt}\n\n`;
120
+ }
121
+
122
+ // Handle both object-style ({ answer: {...} }) and array-style blanks
123
+ const blanks = interaction.blanks || {};
124
+ const blanksArray = Array.isArray(blanks)
125
+ ? blanks
126
+ : Object.entries(blanks).map(([key, config]) => ({ label: key, ...config }));
127
+
128
+ if (blanksArray.length > 0) {
129
+ md += `| Blank | Placeholder |${includeAnswers ? ' Answer |' : ''}\n`;
130
+ md += `|-------|-------------|${includeAnswers ? '--------|' : ''}\n`;
131
+
132
+ blanksArray.forEach(blank => {
133
+ const label = blank.label || '';
134
+ const placeholder = blank.placeholder || '';
135
+ // Handle array-style correct answers
136
+ const correctAnswer = Array.isArray(blank.correct) ? blank.correct.join(', ') : (blank.correct || '');
137
+ const answer = includeAnswers ? ` \`${correctAnswer}\` |` : '';
138
+ md += `| ${label} | ${placeholder} |${answer}\n`;
139
+ });
140
+ } else {
141
+ md += '*No blanks configured*\n';
142
+ }
143
+
144
+ if (includeFeedback && interaction.feedback) {
145
+ md += formatFeedbackBlock(interaction.feedback);
146
+ }
147
+
148
+ return md;
149
+ }
150
+
151
+ /**
152
+ * Format a drag-and-drop interaction
153
+ * @param {Object} interaction - The interaction config
154
+ * @param {Object} options - Formatting options
155
+ * @returns {string} Formatted Markdown
156
+ */
157
+ export function formatDragDrop(interaction, options = {}) {
158
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
159
+
160
+ let md = '';
161
+ if (!skipHeader) {
162
+ md += `#### ${interaction.id}\n`;
163
+ md += '**Type:** Drag & Drop\n';
164
+ if (interaction.weight) {
165
+ md += `**Weight:** ${interaction.weight}\n`;
166
+ }
167
+ }
168
+
169
+ md += `\n**Prompt:** ${interaction.prompt || '*No prompt*'}\n\n`;
170
+
171
+ const items = interaction.items || [];
172
+ if (items.length > 0) {
173
+ md += '**Draggable Items:**\n';
174
+ items.forEach((item, index) => {
175
+ md += `${index + 1}. ${item.content || item.text || item.id}\n`;
176
+ });
177
+ } else {
178
+ md += '**Draggable Items:** *None configured*\n';
179
+ }
180
+
181
+ const dropZones = interaction.dropZones || [];
182
+ if (dropZones.length > 0) {
183
+ md += '\n**Drop Zones:**\n';
184
+ md += `| Zone | ${includeAnswers ? 'Accepts | ' : ''}Max Items |\n`;
185
+ md += `|------|${includeAnswers ? '---------|' : ''}-----------|\n`;
186
+
187
+ dropZones.forEach(zone => {
188
+ const accepts = includeAnswers && zone.accepts ? zone.accepts.join(', ') : '';
189
+ const maxItems = zone.maxItems || '-';
190
+ md += `| ${zone.label || zone.id} |${includeAnswers ? ` ${accepts} |` : ''} ${maxItems} |\n`;
191
+ });
192
+ } else {
193
+ md += '\n**Drop Zones:** *None configured*\n';
194
+ }
195
+
196
+ if (includeFeedback && interaction.feedback) {
197
+ md += formatFeedbackBlock(interaction.feedback);
198
+ }
199
+
200
+ return md;
201
+ }
202
+
203
+ /**
204
+ * Format a numeric input interaction
205
+ * @param {Object} interaction - The interaction config
206
+ * @param {Object} options - Formatting options
207
+ * @returns {string} Formatted Markdown
208
+ */
209
+ export function formatNumeric(interaction, options = {}) {
210
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
211
+
212
+ let md = '';
213
+ if (!skipHeader) {
214
+ md += `#### ${interaction.id}\n`;
215
+ md += '**Type:** Numeric\n';
216
+ if (interaction.weight) {
217
+ md += `**Weight:** ${interaction.weight}\n`;
218
+ }
219
+ }
220
+
221
+ md += `\n**Prompt:** ${interaction.prompt}\n`;
222
+
223
+ if (includeAnswers) {
224
+ let correctValue = '';
225
+ if (interaction.correctRange) {
226
+ if (interaction.correctRange.exact !== undefined) {
227
+ correctValue = interaction.correctRange.exact;
228
+ } else if (interaction.correctRange.min !== undefined && interaction.correctRange.max !== undefined) {
229
+ correctValue = `${interaction.correctRange.min} to ${interaction.correctRange.max}`;
230
+ }
231
+ } else if (interaction.correctAnswer !== undefined) {
232
+ correctValue = interaction.correctAnswer;
233
+ }
234
+
235
+ md += `\n- **Correct Answer:** ${correctValue}\n`;
236
+ if (interaction.tolerance !== undefined) {
237
+ md += `- **Tolerance:** ±${interaction.tolerance}\n`;
238
+ }
239
+ }
240
+
241
+ if (interaction.units) {
242
+ md += `- **Units:** ${interaction.units}\n`;
243
+ }
244
+
245
+ if (includeFeedback && interaction.feedback) {
246
+ md += formatFeedbackBlock(interaction.feedback);
247
+ }
248
+
249
+ return md;
250
+ }
251
+
252
+ /**
253
+ * Format a sequencing interaction
254
+ * @param {Object} interaction - The interaction config
255
+ * @param {Object} options - Formatting options
256
+ * @returns {string} Formatted Markdown
257
+ */
258
+ export function formatSequencing(interaction, options = {}) {
259
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
260
+
261
+ let md = '';
262
+ if (!skipHeader) {
263
+ md += `#### ${interaction.id}\n`;
264
+ md += '**Type:** Sequencing\n';
265
+ if (interaction.weight) {
266
+ md += `**Weight:** ${interaction.weight}\n`;
267
+ }
268
+ }
269
+
270
+ md += `\n**Prompt:** ${interaction.prompt}\n\n`;
271
+
272
+ md += '**Items:**\n';
273
+ (interaction.items || []).forEach((item, _i) => {
274
+ const label = item.content || item.text || item.id;
275
+ if (includeAnswers) {
276
+ const correctOrder = (interaction.correctOrder || []).indexOf(item.id) + 1;
277
+ md += `- ${label} ${correctOrder > 0 ? `(Correct Position: ${correctOrder})` : ''}\n`;
278
+ } else {
279
+ md += `- ${label}\n`;
280
+ }
281
+ });
282
+
283
+ if (includeFeedback && interaction.feedback) {
284
+ md += formatFeedbackBlock(interaction.feedback);
285
+ }
286
+
287
+ return md;
288
+ }
289
+
290
+ /**
291
+ * Format a matching interaction
292
+ * @param {Object} interaction - The interaction config
293
+ * @param {Object} options - Formatting options
294
+ * @returns {string} Formatted Markdown
295
+ */
296
+ export function formatMatching(interaction, options = {}) {
297
+ const { includeAnswers = false, includeFeedback = false, skipHeader = false } = options;
298
+
299
+ let md = '';
300
+ if (!skipHeader) {
301
+ md += `#### ${interaction.id}\n`;
302
+ md += '**Type:** Matching\n';
303
+ if (interaction.weight) {
304
+ md += `**Weight:** ${interaction.weight}\n`;
305
+ }
306
+ }
307
+
308
+ md += `\n**Prompt:** ${interaction.prompt}\n\n`;
309
+
310
+ md += '**Pairs:**\n';
311
+ md += `| Item | ${includeAnswers ? 'Match |' : ''}\n`;
312
+ md += `|------|${includeAnswers ? '-------|' : ''}\n`;
313
+
314
+ const pairs = interaction.pairs || [];
315
+ if (pairs.length > 0) {
316
+ pairs.forEach(pair => {
317
+ // Use 'text' (actual property name) or fallback to 'item'/'left'
318
+ const item = pair.text || pair.item || pair.left || pair.id || '';
319
+ const match = includeAnswers ? (pair.match || pair.right || '') : '';
320
+ md += `| ${item} |${includeAnswers ? ` ${match} |` : ''}\n`;
321
+ });
322
+ } else {
323
+ md += '*No pairs configured*\n';
324
+ }
325
+
326
+ if (includeFeedback && interaction.feedback) {
327
+ md += formatFeedbackBlock(interaction.feedback);
328
+ }
329
+
330
+ return md;
331
+ }
332
+
333
+ /**
334
+ * Format a Likert scale interaction
335
+ * @param {Object} interaction - The interaction config
336
+ * @param {Object} options - Formatting options
337
+ * @returns {string} Formatted Markdown
338
+ */
339
+ export function formatLikert(interaction, options = {}) {
340
+ const { skipHeader = false } = options;
341
+
342
+ let md = '';
343
+ if (!skipHeader) {
344
+ md += `#### ${interaction.id}\n`;
345
+ md += '**Type:** Likert Scale\n';
346
+ }
347
+
348
+ md += `\n**Prompt:** ${interaction.prompt || ''}\n\n`;
349
+
350
+ if (interaction.scale) {
351
+ md += '**Scale Options:**\n';
352
+ (interaction.scale || []).forEach(option => {
353
+ md += `- ${option.label || option.text || option.value}\n`;
354
+ });
355
+ }
356
+
357
+ if (interaction.questions && interaction.questions.length > 0) {
358
+ md += '\n**Questions:**\n';
359
+ interaction.questions.forEach((q, idx) => {
360
+ md += `${idx + 1}. ${q.text || q.prompt || q}\n`;
361
+ });
362
+ }
363
+
364
+ return md;
365
+ }
366
+
367
+ /**
368
+ * Format a hotspot interaction
369
+ * @param {Object} interaction - The interaction config
370
+ * @param {Object} options - Formatting options
371
+ * @returns {string} Formatted Markdown
372
+ */
373
+ export function formatHotspot(interaction, options = {}) {
374
+ const { skipHeader = false } = options;
375
+
376
+ let md = '';
377
+ if (!skipHeader) {
378
+ md += `#### ${interaction.id}\n`;
379
+ md += '**Type:** Hotspot (Visual Interaction)\n';
380
+ }
381
+
382
+ if (interaction.image) {
383
+ md += `\n**Image:** \`${interaction.image}\`\n`;
384
+ }
385
+
386
+ md += '\n**Hotspots:**\n';
387
+ (interaction.hotspots || []).forEach((hotspot, idx) => {
388
+ const label = hotspot.label || hotspot.id || `Hotspot ${idx + 1}`;
389
+ const description = hotspot.description || hotspot.content || '';
390
+ md += `- **${label}**${description ? `: ${description}` : ''}\n`;
391
+ });
392
+
393
+ md += '\n*Note: This is a visual interaction - layout positions are not exported.*\n';
394
+
395
+ return md;
396
+ }
397
+
398
+ /**
399
+ * Format any interaction based on its type
400
+ * @param {Object} interaction - The interaction config
401
+ * @param {Object} options - Formatting options
402
+ * @returns {string} Formatted Markdown
403
+ */
404
+ export function formatInteraction(interaction, options = {}) {
405
+ const type = (interaction.type || '').toLowerCase().replace(/-/g, '').replace(/_/g, '');
406
+
407
+ switch (type) {
408
+ case 'multiplechoice':
409
+ case 'mcq':
410
+ return formatMultipleChoice(interaction, options);
411
+
412
+ case 'truefalse':
413
+ case 'tf':
414
+ return formatTrueFalse(interaction, options);
415
+
416
+ case 'fillin':
417
+ case 'fillinblank':
418
+ case 'fillintheblank':
419
+ case 'fillintheblanks':
420
+ case 'blank':
421
+ return formatFillInBlank(interaction, options);
422
+
423
+ case 'dragdrop':
424
+ case 'draganddrop':
425
+ case 'dd':
426
+ return formatDragDrop(interaction, options);
427
+
428
+ case 'numeric':
429
+ case 'number':
430
+ return formatNumeric(interaction, options);
431
+
432
+ case 'sequencing':
433
+ case 'sequence':
434
+ case 'ordering':
435
+ return formatSequencing(interaction, options);
436
+
437
+ case 'matching':
438
+ case 'match':
439
+ return formatMatching(interaction, options);
440
+
441
+ case 'likert':
442
+ case 'likertscale':
443
+ return formatLikert(interaction, options);
444
+
445
+ case 'hotspot':
446
+ case 'hotspots':
447
+ return formatHotspot(interaction, options);
448
+
449
+ default:
450
+ // Generic fallback
451
+ const { skipHeader: skip = false } = options;
452
+ let md = '';
453
+ if (!skip) {
454
+ md += `#### ${interaction.id}\n`;
455
+ md += `**Type:** ${interaction.type || 'Unknown'}\n`;
456
+ }
457
+ if (interaction.prompt) {
458
+ md += `\n**Prompt:** ${interaction.prompt}\n`;
459
+ }
460
+ return md;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Convert interaction to JSON format
466
+ * @param {Object} interaction - The interaction config
467
+ * @param {Object} options - Formatting options
468
+ * @returns {Object} JSON representation
469
+ */
470
+ export function interactionToJson(interaction, options = {}) {
471
+ const { includeAnswers = false, includeFeedback = false } = options;
472
+
473
+ const result = {
474
+ id: interaction.id,
475
+ type: interaction.type,
476
+ prompt: interaction.prompt
477
+ };
478
+
479
+ if (interaction.weight) {
480
+ result.weight = interaction.weight;
481
+ }
482
+
483
+ // Type-specific fields
484
+ switch ((interaction.type || '').toLowerCase().replace(/-/g, '')) {
485
+ case 'multiplechoice':
486
+ case 'mcq':
487
+ result.choices = (interaction.choices || []).map(c => ({
488
+ value: c.value,
489
+ text: c.text,
490
+ ...(includeAnswers && { correct: c.correct })
491
+ }));
492
+ break;
493
+
494
+ case 'truefalse':
495
+ if (includeAnswers) {
496
+ result.correctAnswer = interaction.correctAnswer;
497
+ }
498
+ break;
499
+
500
+ case 'fillin':
501
+ case 'fillinblank':
502
+ result.blanks = (interaction.blanks || []).map(b => ({
503
+ label: b.label,
504
+ placeholder: b.placeholder,
505
+ ...(includeAnswers && { correct: b.correct })
506
+ }));
507
+ break;
508
+
509
+ case 'dragdrop':
510
+ case 'draganddrop':
511
+ result.items = (interaction.items || []).map(i => ({
512
+ id: i.id,
513
+ content: i.content || i.text
514
+ }));
515
+ result.dropZones = (interaction.dropZones || []).map(z => ({
516
+ id: z.id,
517
+ label: z.label,
518
+ maxItems: z.maxItems,
519
+ ...(includeAnswers && { accepts: z.accepts })
520
+ }));
521
+ break;
522
+
523
+ case 'numeric':
524
+ if (includeAnswers) {
525
+ result.correctValue = interaction.correctRange?.exact ?? interaction.correctAnswer;
526
+ result.tolerance = interaction.tolerance;
527
+ }
528
+ result.units = interaction.units;
529
+ break;
530
+
531
+ case 'sequencing':
532
+ result.items = (interaction.items || []).map(i => ({
533
+ id: i.id,
534
+ content: i.content || i.text
535
+ }));
536
+ if (includeAnswers) {
537
+ result.correctOrder = interaction.correctOrder;
538
+ }
539
+ break;
540
+
541
+ case 'matching':
542
+ result.pairs = (interaction.pairs || []).map(p => ({
543
+ item: p.item || p.left,
544
+ ...(includeAnswers && { match: p.match || p.right })
545
+ }));
546
+ break;
547
+
548
+ case 'likert':
549
+ result.scale = interaction.scale;
550
+ result.questions = interaction.questions;
551
+ break;
552
+
553
+ case 'hotspot':
554
+ result.image = interaction.image;
555
+ result.hotspots = (interaction.hotspots || []).map(h => ({
556
+ id: h.id,
557
+ label: h.label,
558
+ description: h.description
559
+ }));
560
+ break;
561
+ }
562
+
563
+ if (includeFeedback && interaction.feedback) {
564
+ result.feedback = interaction.feedback;
565
+ }
566
+
567
+ return result;
568
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @file cmi5-manifest.js
3
+ * @description Generates cmi5.xml course structure file.
4
+ *
5
+ * cmi5 uses a different structure than SCORM:
6
+ * - cmi5.xml instead of imsmanifest.xml
7
+ * - AU (Assignable Unit) definitions
8
+ * - Launch URLs with move-on criteria
9
+ *
10
+ * For cmi5-remote format, the AU URL is absolute (pointing to CDN).
11
+ */
12
+
13
+ /**
14
+ * Generates the cmi5 course structure XML.
15
+ * @param {Object} config - Course configuration
16
+ * @param {string[]} files - List of files (used for reference, not enumerated)
17
+ * @param {Object} options - Additional options
18
+ * @param {string} options.externalUrl - External URL for cmi5-remote format
19
+ * @returns {string} The cmi5.xml content
20
+ */
21
+ export function generateCmi5Manifest(config, _files, options = {}) {
22
+ // cmi5 course identifier - use configured identifier or generate from title
23
+ const courseId = config.identifier ||
24
+ `urn:coursecode:${config.title.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
25
+
26
+ // Calculate mastery score from passing percentage (convert percentage to 0-1 scale)
27
+ const masteryScore = config.passingScore ? (config.passingScore / 100).toFixed(2) : '0.8';
28
+
29
+ // AU identifier - derive from course ID
30
+ const auId = `${courseId}/au/1`;
31
+
32
+ // URL: absolute for cmi5-remote, relative for standard cmi5
33
+ const auUrl = options.externalUrl
34
+ ? `${options.externalUrl.replace(/\/$/, '')}/index.html`
35
+ : 'index.html';
36
+
37
+ return `<?xml version="1.0" encoding="UTF-8"?>
38
+ <!-- cmi5 Course Structure - GENERATED FILE - DO NOT EDIT MANUALLY -->
39
+ <courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
40
+
41
+ <course id="${courseId}">
42
+ <title>
43
+ <langstring lang="${config.language}">${config.title}</langstring>
44
+ </title>
45
+ <description>
46
+ <langstring lang="${config.language}">${config.description}</langstring>
47
+ </description>
48
+ </course>
49
+
50
+ <au id="${auId}" moveOn="Completed" masteryScore="${masteryScore}" launchMethod="OwnWindow">
51
+ <title>
52
+ <langstring lang="${config.language}">${config.title}</langstring>
53
+ </title>
54
+ <description>
55
+ <langstring lang="${config.language}">${config.description}</langstring>
56
+ </description>
57
+ <url>${auUrl}</url>
58
+ </au>
59
+
60
+ </courseStructure>
61
+ `;
62
+ }
63
+
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @file lti-tool-config.js
3
+ * @description Generates LTI 1.3 tool registration configuration.
4
+ * Produces a JSON file that platform admins use to register the tool.
5
+ * Supports Dynamic Registration (RFC) format.
6
+ */
7
+
8
+ /**
9
+ * Generates LTI 1.3 tool configuration JSON.
10
+ * @param {Object} config - Course configuration
11
+ * @param {Object} options - Additional options (externalUrl)
12
+ * @returns {string} JSON string of tool configuration
13
+ */
14
+ export function generateLtiToolConfig(config, options = {}) {
15
+ const baseUrl = options.externalUrl || config.externalUrl || 'https://your-course-host.example.com';
16
+ const title = config.title || 'CourseCode Course';
17
+ const description = config.description || '';
18
+
19
+ const toolConfig = {
20
+ // LTI 1.3 Tool Configuration
21
+ // See: https://www.imsglobal.org/spec/lti-dr/v1p0
22
+ 'application_type': 'web',
23
+ 'response_types': ['id_token'],
24
+ 'grant_types': ['implicit', 'client_credentials'],
25
+ 'initiate_login_uri': `${baseUrl}/lti/login`,
26
+ 'redirect_uris': [
27
+ `${baseUrl}/index.html`,
28
+ `${baseUrl}/lti/launch`
29
+ ],
30
+ 'client_name': title,
31
+ 'jwks_uri': `${baseUrl}/lti/jwks`,
32
+ 'logo_uri': `${baseUrl}/assets/logo.png`,
33
+ 'token_endpoint_auth_method': 'private_key_jwt',
34
+ 'scope': 'https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
35
+
36
+ // LTI-specific claims
37
+ 'https://purl.imsglobal.org/spec/lti-tool-configuration': {
38
+ 'domain': new URL(baseUrl).hostname,
39
+ 'description': description,
40
+ 'target_link_uri': `${baseUrl}/index.html`,
41
+ 'claims': ['iss', 'sub', 'name', 'given_name', 'family_name', 'email'],
42
+ 'messages': [
43
+ {
44
+ 'type': 'LtiResourceLinkRequest',
45
+ 'target_link_uri': `${baseUrl}/index.html`,
46
+ 'label': title
47
+ }
48
+ ]
49
+ }
50
+ };
51
+
52
+ return JSON.stringify(toolConfig, null, 2);
53
+ }