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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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
|
+
}
|