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,936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Course Parser - Unified parsing utility for CourseCode
|
|
3
|
+
*
|
|
4
|
+
* Provides a single source of truth for course data:
|
|
5
|
+
* - Universal element parsing (all HTML elements with paths + offsets)
|
|
6
|
+
* - Schema-driven interaction extraction
|
|
7
|
+
* - Narration extraction
|
|
8
|
+
* - Course config inclusion
|
|
9
|
+
*
|
|
10
|
+
* Consumers: preview-server, export-content, vite-plugin, linter
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { pathToFileURL } from 'url';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// PUBLIC API
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse an entire course
|
|
23
|
+
* @param {string} coursePath - Path to course directory
|
|
24
|
+
* @returns {Promise<CourseData>}
|
|
25
|
+
*/
|
|
26
|
+
export async function parseCourse(coursePath) {
|
|
27
|
+
// Load config (direct import, no parsing needed)
|
|
28
|
+
const configPath = path.join(coursePath, 'course-config.js');
|
|
29
|
+
const configUrl = pathToFileURL(configPath).href + `?t=${Date.now()}`;
|
|
30
|
+
const configModule = await import(configUrl);
|
|
31
|
+
const config = configModule.courseConfig || configModule.default;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Parse all slides
|
|
36
|
+
const slidesDir = path.join(coursePath, 'slides');
|
|
37
|
+
const files = fs.existsSync(slidesDir)
|
|
38
|
+
? fs.readdirSync(slidesDir).filter(f => f.endsWith('.js'))
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
const slides = {};
|
|
42
|
+
const assessments = [];
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const filePath = path.join(slidesDir, file);
|
|
46
|
+
const slideId = path.basename(file, '.js');
|
|
47
|
+
const source = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
|
|
49
|
+
// Check if it's an assessment
|
|
50
|
+
const assessment = extractAssessment(source, slideId);
|
|
51
|
+
if (assessment) {
|
|
52
|
+
assessments.push(assessment);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse as regular slide
|
|
57
|
+
slides[slideId] = {
|
|
58
|
+
file,
|
|
59
|
+
...parseSlideSource(source, slideId)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
config,
|
|
65
|
+
slides,
|
|
66
|
+
assessments
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a single slide source (for on-demand editing)
|
|
72
|
+
* @param {string} source - JavaScript source code
|
|
73
|
+
* @param {string} slideId - Slide ID
|
|
74
|
+
* @param {object} schemas - Interaction schemas
|
|
75
|
+
* @returns {SlideData}
|
|
76
|
+
*/
|
|
77
|
+
export function parseSlideSource(source, slideId) {
|
|
78
|
+
// Step 1: Extract template literals
|
|
79
|
+
const templates = findTemplateAssignments(source);
|
|
80
|
+
const html = templates.map(t => t.content).join('\n');
|
|
81
|
+
|
|
82
|
+
// Step 2: Parse ALL elements universally
|
|
83
|
+
const elements = parseElements(html, templates);
|
|
84
|
+
|
|
85
|
+
// Step 3: Extract interactions (id and type only - schemas loaded at runtime)
|
|
86
|
+
const interactions = extractInteractions(source, slideId);
|
|
87
|
+
|
|
88
|
+
// Step 4: Extract narration
|
|
89
|
+
const narration = extractNarration(source);
|
|
90
|
+
|
|
91
|
+
// Step 5: Compute header convenience accessor
|
|
92
|
+
const header = computeHeader(elements);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
templateHtml: templates.map(t => t.content),
|
|
96
|
+
elements,
|
|
97
|
+
interactions,
|
|
98
|
+
narration,
|
|
99
|
+
header
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve an element by its structural path
|
|
105
|
+
* @param {Element[]} elements - Parsed elements array
|
|
106
|
+
* @param {string} targetPath - Path like "header.0/h1.0"
|
|
107
|
+
* @returns {Element|null}
|
|
108
|
+
*/
|
|
109
|
+
export function resolveElementByPath(elements, targetPath) {
|
|
110
|
+
return elements.find(el => el.path === targetPath) || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// UNIVERSAL ELEMENT PARSER
|
|
116
|
+
// =============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Semantic detection table - maps class/tag/attribute patterns to semantic types
|
|
120
|
+
*/
|
|
121
|
+
const SEMANTIC_PATTERNS = [
|
|
122
|
+
{ match: (el) => el.tag === 'h1' && el.parentPath?.includes('slide-header'), semantic: 'title' },
|
|
123
|
+
{ match: (el) => el.tag === 'p' && el.parentPath?.includes('slide-header'), semantic: 'description' },
|
|
124
|
+
{ match: (el) => el.className?.includes('callout'), semantic: 'callout' },
|
|
125
|
+
{ match: (el) => el.className?.includes('card') && !el.className?.includes('flip-card'), semantic: 'card' },
|
|
126
|
+
{ match: (el) => el.attributes?.['data-component'], semanticFn: (el) => el.attributes['data-component'] },
|
|
127
|
+
{ match: (el) => el.tag === 'table', semantic: 'table' },
|
|
128
|
+
{ match: (el) => el.className?.includes('pattern-intro-cards'), semantic: 'intro-cards' },
|
|
129
|
+
{ match: (el) => el.className?.includes('pattern-steps'), semantic: 'steps' },
|
|
130
|
+
{ match: (el) => el.className?.includes('pattern-features'), semantic: 'features' },
|
|
131
|
+
{ match: (el) => el.className?.includes('pattern-comparison'), semantic: 'comparison' },
|
|
132
|
+
{ match: (el) => el.className?.includes('pattern-stats'), semantic: 'stats' },
|
|
133
|
+
{ match: (el) => el.className?.includes('pattern-content-image'), semantic: 'content-image' },
|
|
134
|
+
{ match: (el) => el.className?.includes('pattern-hero'), semantic: 'hero' },
|
|
135
|
+
{ match: (el) => el.className?.includes('pattern-timeline'), semantic: 'timeline' },
|
|
136
|
+
{ match: (el) => el.className?.includes('pattern-quote'), semantic: 'quote' },
|
|
137
|
+
{ match: (el) => el.className?.includes('pattern-checklist'), semantic: 'checklist' },
|
|
138
|
+
{ match: (el) => el.tag === 'h2', semantic: 'heading' },
|
|
139
|
+
{ match: (el) => el.tag === 'h3', semantic: 'subheading' },
|
|
140
|
+
{ match: (el) => el.tag === 'p' && !el.parentPath?.includes('slide-header'), semantic: 'paragraph' },
|
|
141
|
+
{ match: (el) => el.tag === 'li', semantic: 'list-item' },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse ALL HTML elements into a flat array with paths and offsets
|
|
146
|
+
* @param {string} html - HTML content
|
|
147
|
+
* @param {Array} templates - Template info with line offsets
|
|
148
|
+
* @returns {Element[]}
|
|
149
|
+
*/
|
|
150
|
+
export function parseElements(html) {
|
|
151
|
+
const elements = [];
|
|
152
|
+
const stack = [];
|
|
153
|
+
const siblingCounters = [{}];
|
|
154
|
+
|
|
155
|
+
// Tag pattern - matches opening and closing tags
|
|
156
|
+
const tagRegex = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^>]*)?)(\/?)>/g;
|
|
157
|
+
|
|
158
|
+
// Self-closing tags
|
|
159
|
+
const selfClosingTags = new Set(['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr']);
|
|
160
|
+
|
|
161
|
+
let match;
|
|
162
|
+
while ((match = tagRegex.exec(html)) !== null) {
|
|
163
|
+
const [fullMatch, isClosing, tagName, attrString, isSelfClosingSlash] = match;
|
|
164
|
+
const tag = tagName.toLowerCase();
|
|
165
|
+
|
|
166
|
+
// Skip comments and non-content
|
|
167
|
+
if (tag.startsWith('!')) continue;
|
|
168
|
+
|
|
169
|
+
const isSelfClosing = isSelfClosingSlash === '/' || selfClosingTags.has(tag);
|
|
170
|
+
|
|
171
|
+
if (isClosing) {
|
|
172
|
+
// Closing tag - pop from stack and finalize element
|
|
173
|
+
if (stack.length > 0) {
|
|
174
|
+
const el = stack.pop();
|
|
175
|
+
el.innerEnd = match.index;
|
|
176
|
+
el.endOffset = match.index + fullMatch.length;
|
|
177
|
+
siblingCounters.pop();
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Opening tag - parse attributes and push to stack
|
|
181
|
+
const attributes = parseAttributes(attrString);
|
|
182
|
+
const className = attributes.class || '';
|
|
183
|
+
|
|
184
|
+
// Calculate sibling index for path
|
|
185
|
+
const counters = siblingCounters[siblingCounters.length - 1];
|
|
186
|
+
const pathKey = className.split(' ')[0] || tag;
|
|
187
|
+
counters[pathKey] = counters[pathKey] || 0;
|
|
188
|
+
const siblingIndex = counters[pathKey]++;
|
|
189
|
+
|
|
190
|
+
// Build path
|
|
191
|
+
const parentPath = stack.length > 0 ? stack[stack.length - 1].path : '';
|
|
192
|
+
const segment = `${pathKey}.${siblingIndex}`;
|
|
193
|
+
const elementPath = parentPath ? `${parentPath}/${segment}` : segment;
|
|
194
|
+
|
|
195
|
+
const element = {
|
|
196
|
+
path: elementPath,
|
|
197
|
+
parentPath,
|
|
198
|
+
tag,
|
|
199
|
+
className,
|
|
200
|
+
attributes,
|
|
201
|
+
startOffset: match.index,
|
|
202
|
+
innerStart: match.index + fullMatch.length,
|
|
203
|
+
innerEnd: null,
|
|
204
|
+
endOffset: null,
|
|
205
|
+
innerText: null,
|
|
206
|
+
semantic: null
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (isSelfClosing) {
|
|
210
|
+
element.innerEnd = element.innerStart;
|
|
211
|
+
element.endOffset = element.innerStart;
|
|
212
|
+
} else {
|
|
213
|
+
stack.push(element);
|
|
214
|
+
siblingCounters.push({});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
elements.push(element);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract inner text and detect semantics for finalized elements
|
|
222
|
+
for (const el of elements) {
|
|
223
|
+
if (el.innerEnd !== null) {
|
|
224
|
+
el.innerText = stripTags(html.slice(el.innerStart, el.innerEnd)).trim();
|
|
225
|
+
el.semantic = detectSemantic(el);
|
|
226
|
+
}
|
|
227
|
+
el.children = []; // Initialize children array
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build parent-child relationships
|
|
231
|
+
const elementsByPath = new Map(elements.map(el => [el.path, el]));
|
|
232
|
+
for (const el of elements) {
|
|
233
|
+
if (el.parentPath) {
|
|
234
|
+
const parent = elementsByPath.get(el.parentPath);
|
|
235
|
+
if (parent) {
|
|
236
|
+
parent.children.push(el);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return elements;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse HTML attributes from attribute string
|
|
246
|
+
* @param {string} attrString - Attribute portion of tag
|
|
247
|
+
* @returns {object}
|
|
248
|
+
*/
|
|
249
|
+
function parseAttributes(attrString) {
|
|
250
|
+
const attrs = {};
|
|
251
|
+
if (!attrString) return attrs;
|
|
252
|
+
|
|
253
|
+
const attrRegex = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
254
|
+
let attrMatch;
|
|
255
|
+
|
|
256
|
+
while ((attrMatch = attrRegex.exec(attrString)) !== null) {
|
|
257
|
+
const name = attrMatch[1];
|
|
258
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? true;
|
|
259
|
+
attrs[name] = value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return attrs;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Detect semantic type for an element
|
|
267
|
+
* @param {Element} el - Element to check
|
|
268
|
+
* @returns {string|null}
|
|
269
|
+
*/
|
|
270
|
+
function detectSemantic(el) {
|
|
271
|
+
for (const pattern of SEMANTIC_PATTERNS) {
|
|
272
|
+
if (pattern.match(el)) {
|
|
273
|
+
return pattern.semanticFn ? pattern.semanticFn(el) : pattern.semantic;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Compute header from elements (convenience accessor)
|
|
281
|
+
* @param {Element[]} elements
|
|
282
|
+
* @returns {{ title?: string, description?: string }}
|
|
283
|
+
*/
|
|
284
|
+
function computeHeader(elements) {
|
|
285
|
+
const title = elements.find(el => el.semantic === 'title');
|
|
286
|
+
const description = elements.find(el => el.semantic === 'description');
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
title: title?.innerText || null,
|
|
290
|
+
description: description?.innerText || null
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// TEMPLATE EXTRACTION (JS → HTML)
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Find template literal assignments (.innerHTML = `...`)
|
|
300
|
+
* @param {string} source - JavaScript source code
|
|
301
|
+
* @returns {Array<{start: number, end: number, content: string, lineOffset: number}>}
|
|
302
|
+
*/
|
|
303
|
+
export function findTemplateAssignments(source) {
|
|
304
|
+
const templates = [];
|
|
305
|
+
const pattern = /\.innerHTML\s*=\s*`/g;
|
|
306
|
+
let match;
|
|
307
|
+
|
|
308
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
309
|
+
const startPos = match.index + match[0].length - 1;
|
|
310
|
+
const templateContent = extractTemplateLiteral(source, startPos);
|
|
311
|
+
|
|
312
|
+
if (templateContent) {
|
|
313
|
+
const lineOffset = source.substring(0, startPos).split('\n').length;
|
|
314
|
+
templates.push({
|
|
315
|
+
start: startPos,
|
|
316
|
+
end: startPos + templateContent.length + 2,
|
|
317
|
+
content: cleanTemplateString(templateContent),
|
|
318
|
+
lineOffset
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return templates;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract a template literal starting at the opening backtick
|
|
328
|
+
* @param {string} source
|
|
329
|
+
* @param {number} startPos
|
|
330
|
+
* @returns {string|null}
|
|
331
|
+
*/
|
|
332
|
+
function extractTemplateLiteral(source, startPos) {
|
|
333
|
+
if (source[startPos] !== '`') return null;
|
|
334
|
+
|
|
335
|
+
let i = startPos + 1;
|
|
336
|
+
let depth = 0;
|
|
337
|
+
|
|
338
|
+
while (i < source.length) {
|
|
339
|
+
const char = source[i];
|
|
340
|
+
const prevChar = i > 0 ? source[i - 1] : '';
|
|
341
|
+
|
|
342
|
+
if (prevChar === '\\') { i++; continue; }
|
|
343
|
+
if (char === '$' && source[i + 1] === '{') { depth++; i += 2; continue; }
|
|
344
|
+
if (depth > 0) {
|
|
345
|
+
if (char === '{') depth++;
|
|
346
|
+
if (char === '}') depth--;
|
|
347
|
+
i++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (char === '`') {
|
|
351
|
+
return source.slice(startPos + 1, i);
|
|
352
|
+
}
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Clean template string - remove ${...} expressions using brace-balanced extraction
|
|
361
|
+
* @param {string} template
|
|
362
|
+
* @returns {string}
|
|
363
|
+
*/
|
|
364
|
+
function cleanTemplateString(template) {
|
|
365
|
+
let result = '';
|
|
366
|
+
let i = 0;
|
|
367
|
+
|
|
368
|
+
while (i < template.length) {
|
|
369
|
+
if (template[i] === '$' && template[i + 1] === '{') {
|
|
370
|
+
// Skip the entire ${...} expression using brace balancing
|
|
371
|
+
let depth = 1;
|
|
372
|
+
i += 2; // Skip past ${
|
|
373
|
+
while (i < template.length && depth > 0) {
|
|
374
|
+
if (template[i] === '{') depth++;
|
|
375
|
+
else if (template[i] === '}') depth--;
|
|
376
|
+
i++;
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
result += template[i];
|
|
380
|
+
i++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result.replace(/\s+/g, ' ').trim();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Strip HTML tags from string
|
|
389
|
+
* @param {string} html
|
|
390
|
+
* @returns {string}
|
|
391
|
+
*/
|
|
392
|
+
function stripTags(html) {
|
|
393
|
+
return html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// INTERACTION EXTRACTION
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
// Use AST-based extractor to avoid importing runtime dependencies during build
|
|
401
|
+
import { getRegisteredTypes, getFullSchema, getFactoryName } from './schema-extractor.js';
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extract interaction configs from slide source
|
|
405
|
+
* Extracts id, type, AND full schema from the catalog
|
|
406
|
+
* @param {string} content - Source code
|
|
407
|
+
* @param {string} slideId - Slide ID
|
|
408
|
+
* @returns {Array}
|
|
409
|
+
*/
|
|
410
|
+
export function extractInteractions(content, slideId) {
|
|
411
|
+
const interactions = [];
|
|
412
|
+
const types = getRegisteredTypes();
|
|
413
|
+
|
|
414
|
+
for (const type of types) {
|
|
415
|
+
const factoryName = getFactoryName(type);
|
|
416
|
+
if (!factoryName) continue;
|
|
417
|
+
|
|
418
|
+
const factoryRegex = new RegExp(`${factoryName}\\s*\\(\\s*([\\w]+|\\{)`, 'g');
|
|
419
|
+
let match;
|
|
420
|
+
|
|
421
|
+
while ((match = factoryRegex.exec(content)) !== null) {
|
|
422
|
+
const configArg = match[1];
|
|
423
|
+
let configObject = null;
|
|
424
|
+
|
|
425
|
+
if (configArg === '{') {
|
|
426
|
+
configObject = extractObjectLiteral(content, match.index + match[0].length - 1);
|
|
427
|
+
} else {
|
|
428
|
+
configObject = findVariableDefinition(content, configArg);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (configObject) {
|
|
432
|
+
const id = extractStringProperty(configObject, 'id');
|
|
433
|
+
if (id) {
|
|
434
|
+
const schema = getFullSchema(type);
|
|
435
|
+
const config = parseSimpleObject(configObject);
|
|
436
|
+
interactions.push({ ...config, type, slideId, schema });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return interactions;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// =============================================================================
|
|
446
|
+
// NARRATION EXTRACTION
|
|
447
|
+
// =============================================================================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Extract narration from source
|
|
451
|
+
* @param {string} source
|
|
452
|
+
* @returns {Object|null}
|
|
453
|
+
*/
|
|
454
|
+
export function extractNarration(source) {
|
|
455
|
+
const strippedSource = source.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length));
|
|
456
|
+
|
|
457
|
+
// Pattern 1: template literal
|
|
458
|
+
const simplePattern = /export\s+const\s+narration\s*=\s*`([\s\S]*?)`;/;
|
|
459
|
+
let match = simplePattern.exec(strippedSource);
|
|
460
|
+
if (match) {
|
|
461
|
+
const actualContent = extractTemplateLiteralAt(source, match.index + match[0].indexOf('`'));
|
|
462
|
+
return { slide: (actualContent || match[1]).trim() };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Pattern 2: string literal
|
|
466
|
+
const simpleQuotePattern = /export\s+const\s+narration\s*=\s*(['"])([\s\S]*?)\1;/;
|
|
467
|
+
match = simpleQuotePattern.exec(strippedSource);
|
|
468
|
+
if (match) {
|
|
469
|
+
return { slide: match[2].trim() };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Pattern 3: object
|
|
473
|
+
const objectPattern = /export\s+const\s+narration\s*=\s*\{/g;
|
|
474
|
+
match = objectPattern.exec(strippedSource);
|
|
475
|
+
if (match) {
|
|
476
|
+
const objStr = extractObjectLiteral(source, match.index + match[0].length - 1);
|
|
477
|
+
if (objStr) {
|
|
478
|
+
return parseNarrationObject(objStr);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function extractTemplateLiteralAt(source, pos) {
|
|
486
|
+
while (pos < source.length && source[pos] !== '`') pos++;
|
|
487
|
+
if (pos >= source.length) return null;
|
|
488
|
+
return extractTemplateLiteral(source, pos);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseNarrationObject(objStr) {
|
|
492
|
+
const result = {};
|
|
493
|
+
const keyPattern = /(['"]?)(\w+|[\w-]+)\1\s*:\s*([`'"])([^]*?)\3/g;
|
|
494
|
+
let match;
|
|
495
|
+
|
|
496
|
+
while ((match = keyPattern.exec(objStr)) !== null) {
|
|
497
|
+
const key = match[2];
|
|
498
|
+
const value = match[4].trim();
|
|
499
|
+
if (!['voice_id', 'stability', 'similarity_boost'].includes(key)) {
|
|
500
|
+
result[key] = value;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// =============================================================================
|
|
508
|
+
// ASSESSMENT EXTRACTION
|
|
509
|
+
// =============================================================================
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Check if file is an assessment and extract config
|
|
513
|
+
* @param {string} source
|
|
514
|
+
* @param {string} slideId
|
|
515
|
+
* @returns {object|null}
|
|
516
|
+
*/
|
|
517
|
+
export function extractAssessment(source, slideId) {
|
|
518
|
+
if (!source.includes('export const config') || !source.includes('assessmentId:')) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const configMatch = source.match(/export\s+const\s+config\s*=\s*\{/);
|
|
523
|
+
if (!configMatch) return null;
|
|
524
|
+
|
|
525
|
+
const configStr = extractObjectLiteral(source, configMatch.index + configMatch[0].length - 1);
|
|
526
|
+
if (!configStr) return null;
|
|
527
|
+
|
|
528
|
+
const id = extractStringProperty(configStr, 'id');
|
|
529
|
+
if (!id) return null;
|
|
530
|
+
|
|
531
|
+
const assessment = {
|
|
532
|
+
id,
|
|
533
|
+
slideId,
|
|
534
|
+
title: extractStringProperty(configStr, 'title') || id,
|
|
535
|
+
settings: {},
|
|
536
|
+
questions: [],
|
|
537
|
+
questionBanks: []
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const settingsMatch = configStr.match(/settings\s*:\s*\{/);
|
|
541
|
+
if (settingsMatch) {
|
|
542
|
+
const settingsStr = extractObjectLiteral(configStr, settingsMatch.index + settingsMatch[0].length - 1);
|
|
543
|
+
if (settingsStr) {
|
|
544
|
+
const rawSettings = {
|
|
545
|
+
passingScore: extractNumberProperty(settingsStr, 'passingScore'),
|
|
546
|
+
allowRetake: extractBooleanProperty(settingsStr, 'allowRetake'),
|
|
547
|
+
allowReview: extractBooleanProperty(settingsStr, 'allowReview'),
|
|
548
|
+
randomizeQuestions: extractBooleanProperty(settingsStr, 'randomizeQuestions'),
|
|
549
|
+
showProgress: extractBooleanProperty(settingsStr, 'showProgress')
|
|
550
|
+
};
|
|
551
|
+
assessment.settings = Object.fromEntries(
|
|
552
|
+
Object.entries(rawSettings).filter(([, v]) => v !== null)
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Extract questions array (direct questions)
|
|
558
|
+
const questionsMatch = source.match(/const\s+questions\s*=\s*\[/);
|
|
559
|
+
if (questionsMatch) {
|
|
560
|
+
const questionsStr = extractArrayLiteral(source, questionsMatch.index + questionsMatch[0].length - 1);
|
|
561
|
+
if (questionsStr) {
|
|
562
|
+
assessment.questions = parseQuestionsArray(questionsStr, slideId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Extract questionBanks array from config
|
|
567
|
+
const questionBanksMatch = configStr.match(/questionBanks\s*:\s*\[/);
|
|
568
|
+
if (questionBanksMatch) {
|
|
569
|
+
const questionBanksStr = extractArrayLiteral(configStr, questionBanksMatch.index + questionBanksMatch[0].length - 1);
|
|
570
|
+
if (questionBanksStr) {
|
|
571
|
+
assessment.questionBanks = parseQuestionBanksArray(questionBanksStr, slideId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return assessment;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Parse questions array - extracts full question objects with schema
|
|
580
|
+
* @param {string} questionsStr - Array literal string
|
|
581
|
+
* @param {string} slideId - Slide ID for context
|
|
582
|
+
* @returns {Array}
|
|
583
|
+
*/
|
|
584
|
+
function parseQuestionsArray(questionsStr, slideId) {
|
|
585
|
+
const questions = [];
|
|
586
|
+
const content = questionsStr.slice(1, -1);
|
|
587
|
+
let depth = 0;
|
|
588
|
+
let objStart = -1;
|
|
589
|
+
|
|
590
|
+
for (let i = 0; i < content.length; i++) {
|
|
591
|
+
if (content[i] === '{') {
|
|
592
|
+
if (depth === 0) objStart = i;
|
|
593
|
+
depth++;
|
|
594
|
+
} else if (content[i] === '}') {
|
|
595
|
+
depth--;
|
|
596
|
+
if (depth === 0 && objStart !== -1) {
|
|
597
|
+
const objStr = content.slice(objStart, i + 1);
|
|
598
|
+
const id = extractStringProperty(objStr, 'id');
|
|
599
|
+
const type = extractStringProperty(objStr, 'type');
|
|
600
|
+
|
|
601
|
+
if (id && type) {
|
|
602
|
+
const parsedQuestion = parseSimpleObject(objStr);
|
|
603
|
+
const normalizedType = type === 'multiple-choice-single' || type === 'multiple-choice-multiple'
|
|
604
|
+
? 'multiple-choice'
|
|
605
|
+
: type;
|
|
606
|
+
const schema = getFullSchema(type) || getFullSchema(normalizedType);
|
|
607
|
+
|
|
608
|
+
questions.push({
|
|
609
|
+
...parsedQuestion,
|
|
610
|
+
id,
|
|
611
|
+
type,
|
|
612
|
+
slideId,
|
|
613
|
+
schema
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
objStart = -1;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return questions;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Parse questionBanks array - extracts bank metadata and nested questions
|
|
626
|
+
* @param {string} questionBanksStr - Array literal string
|
|
627
|
+
* @param {string} slideId - Slide ID for context
|
|
628
|
+
* @returns {Array}
|
|
629
|
+
*/
|
|
630
|
+
function parseQuestionBanksArray(questionBanksStr, slideId) {
|
|
631
|
+
const banks = [];
|
|
632
|
+
const content = questionBanksStr.slice(1, -1); // Remove outer brackets
|
|
633
|
+
let depth = 0;
|
|
634
|
+
let bankStart = -1;
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < content.length; i++) {
|
|
637
|
+
if (content[i] === '{') {
|
|
638
|
+
if (depth === 0) bankStart = i;
|
|
639
|
+
depth++;
|
|
640
|
+
} else if (content[i] === '}') {
|
|
641
|
+
depth--;
|
|
642
|
+
if (depth === 0 && bankStart !== -1) {
|
|
643
|
+
const bankStr = content.slice(bankStart, i + 1);
|
|
644
|
+
|
|
645
|
+
// Extract bank metadata
|
|
646
|
+
const bankId = extractStringProperty(bankStr, 'id');
|
|
647
|
+
const selectCount = extractNumberProperty(bankStr, 'selectCount');
|
|
648
|
+
|
|
649
|
+
// Extract nested questions array
|
|
650
|
+
const questionsMatch = bankStr.match(/questions\s*:\s*\[/);
|
|
651
|
+
let questions = [];
|
|
652
|
+
if (questionsMatch) {
|
|
653
|
+
const questionsStr = extractArrayLiteral(bankStr, questionsMatch.index + questionsMatch[0].length - 1);
|
|
654
|
+
if (questionsStr) {
|
|
655
|
+
questions = parseQuestionsArray(questionsStr, slideId);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (bankId) {
|
|
660
|
+
banks.push({
|
|
661
|
+
id: bankId,
|
|
662
|
+
selectCount: selectCount || questions.length,
|
|
663
|
+
questions: questions
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
bankStart = -1;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return banks;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// =============================================================================
|
|
676
|
+
// LOW-LEVEL UTILITIES
|
|
677
|
+
// =============================================================================
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Escape special regex characters in a string
|
|
681
|
+
* @param {string} str
|
|
682
|
+
* @returns {string}
|
|
683
|
+
*/
|
|
684
|
+
function escapeRegex(str) {
|
|
685
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function extractObjectLiteral(content, startPos) {
|
|
689
|
+
if (content[startPos] !== '{') return null;
|
|
690
|
+
|
|
691
|
+
let depth = 0;
|
|
692
|
+
let inString = false;
|
|
693
|
+
let stringChar = null;
|
|
694
|
+
|
|
695
|
+
for (let i = startPos; i < content.length; i++) {
|
|
696
|
+
const char = content[i];
|
|
697
|
+
const prevChar = i > 0 ? content[i - 1] : '';
|
|
698
|
+
|
|
699
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
700
|
+
if (!inString) { inString = true; stringChar = char; }
|
|
701
|
+
else if (char === stringChar) { inString = false; stringChar = null; }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!inString) {
|
|
705
|
+
if (char === '{' || char === '[') depth++;
|
|
706
|
+
if (char === '}' || char === ']') depth--;
|
|
707
|
+
if (depth === 0) return content.slice(startPos, i + 1);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function extractArrayLiteral(content, startPos) {
|
|
715
|
+
if (content[startPos] !== '[') return null;
|
|
716
|
+
|
|
717
|
+
let depth = 0;
|
|
718
|
+
let inString = false;
|
|
719
|
+
let stringChar = null;
|
|
720
|
+
|
|
721
|
+
for (let i = startPos; i < content.length; i++) {
|
|
722
|
+
const char = content[i];
|
|
723
|
+
const prevChar = i > 0 ? content[i - 1] : '';
|
|
724
|
+
|
|
725
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
726
|
+
if (!inString) { inString = true; stringChar = char; }
|
|
727
|
+
else if (char === stringChar) { inString = false; stringChar = null; }
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!inString) {
|
|
731
|
+
if (char === '[' || char === '{') depth++;
|
|
732
|
+
if (char === ']' || char === '}') depth--;
|
|
733
|
+
if (depth === 0 && char === ']') return content.slice(startPos, i + 1);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function findVariableDefinition(content, varName) {
|
|
741
|
+
const regex = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*\\{`, 'g');
|
|
742
|
+
const match = regex.exec(content);
|
|
743
|
+
if (match) return extractObjectLiteral(content, match.index + match[0].length - 1);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function extractStringProperty(configStr, propName) {
|
|
748
|
+
const escapedName = escapeRegex(propName);
|
|
749
|
+
const regex = new RegExp(`${escapedName}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`);
|
|
750
|
+
const match = configStr.match(regex);
|
|
751
|
+
return match ? match[2] : null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function extractNumberProperty(configStr, propName) {
|
|
755
|
+
const escapedName = escapeRegex(propName);
|
|
756
|
+
const regex = new RegExp(`${escapedName}\\s*:\\s*(-?[\\d.]+)`);
|
|
757
|
+
const match = configStr.match(regex);
|
|
758
|
+
return match ? parseFloat(match[1]) : null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function extractBooleanProperty(configStr, propName) {
|
|
762
|
+
const escapedName = escapeRegex(propName);
|
|
763
|
+
const regex = new RegExp(`${escapedName}\\s*:\\s*(true|false)`);
|
|
764
|
+
const match = configStr.match(regex);
|
|
765
|
+
return match ? match[1] === 'true' : null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Parse a simple object literal, handling strings with proper quote matching
|
|
772
|
+
* @param {string} objStr - Object literal string including braces
|
|
773
|
+
* @returns {object}
|
|
774
|
+
*/
|
|
775
|
+
function parseSimpleObject(objStr) {
|
|
776
|
+
const result = {};
|
|
777
|
+
const content = objStr.slice(1, -1); // Remove outer braces
|
|
778
|
+
|
|
779
|
+
let i = 0;
|
|
780
|
+
while (i < content.length) {
|
|
781
|
+
// Skip whitespace
|
|
782
|
+
while (i < content.length && /\s/.test(content[i])) i++;
|
|
783
|
+
if (i >= content.length) break;
|
|
784
|
+
|
|
785
|
+
// Parse property name (identifier)
|
|
786
|
+
const nameMatch = content.slice(i).match(/^(\w+)\s*:/);
|
|
787
|
+
if (!nameMatch) { i++; continue; }
|
|
788
|
+
|
|
789
|
+
const propName = nameMatch[1];
|
|
790
|
+
i += nameMatch[0].length;
|
|
791
|
+
|
|
792
|
+
// Skip whitespace after colon
|
|
793
|
+
while (i < content.length && /\s/.test(content[i])) i++;
|
|
794
|
+
|
|
795
|
+
const char = content[i];
|
|
796
|
+
|
|
797
|
+
// String value
|
|
798
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
799
|
+
const quote = char;
|
|
800
|
+
let value = '';
|
|
801
|
+
i++; // Skip opening quote
|
|
802
|
+
while (i < content.length) {
|
|
803
|
+
if (content[i] === '\\' && i + 1 < content.length) {
|
|
804
|
+
// Handle escape sequences
|
|
805
|
+
value += content[i + 1];
|
|
806
|
+
i += 2;
|
|
807
|
+
} else if (content[i] === quote) {
|
|
808
|
+
i++; // Skip closing quote
|
|
809
|
+
break;
|
|
810
|
+
} else {
|
|
811
|
+
value += content[i];
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
result[propName] = value;
|
|
816
|
+
}
|
|
817
|
+
// Boolean value
|
|
818
|
+
else if (content.slice(i, i + 4) === 'true') {
|
|
819
|
+
result[propName] = true;
|
|
820
|
+
i += 4;
|
|
821
|
+
}
|
|
822
|
+
else if (content.slice(i, i + 5) === 'false') {
|
|
823
|
+
result[propName] = false;
|
|
824
|
+
i += 5;
|
|
825
|
+
}
|
|
826
|
+
// Number value
|
|
827
|
+
else if (/[-\d]/.test(char)) {
|
|
828
|
+
const numMatch = content.slice(i).match(/^-?\d+\.?\d*/);
|
|
829
|
+
if (numMatch) {
|
|
830
|
+
result[propName] = parseFloat(numMatch[0]);
|
|
831
|
+
i += numMatch[0].length;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Array value - use brace-balanced extraction
|
|
835
|
+
else if (char === '[') {
|
|
836
|
+
const arrayStr = extractArrayLiteral(content, i);
|
|
837
|
+
if (arrayStr) {
|
|
838
|
+
result[propName] = parseSimpleArray(arrayStr);
|
|
839
|
+
i += arrayStr.length;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Nested object - use brace-balanced extraction
|
|
843
|
+
else if (char === '{') {
|
|
844
|
+
const nestedStr = extractObjectLiteral(content, i);
|
|
845
|
+
if (nestedStr) {
|
|
846
|
+
result[propName] = parseSimpleObject(nestedStr);
|
|
847
|
+
i += nestedStr.length;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
i++;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Skip comma and whitespace
|
|
855
|
+
while (i < content.length && (content[i] === ',' || /\s/.test(content[i]))) i++;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Parse a simple array literal
|
|
863
|
+
* @param {string} arrayStr - Array literal string including brackets
|
|
864
|
+
* @returns {Array}
|
|
865
|
+
*/
|
|
866
|
+
function parseSimpleArray(arrayStr) {
|
|
867
|
+
const values = [];
|
|
868
|
+
const content = arrayStr.slice(1, -1); // Remove brackets
|
|
869
|
+
|
|
870
|
+
let i = 0;
|
|
871
|
+
while (i < content.length) {
|
|
872
|
+
// Skip whitespace and commas
|
|
873
|
+
while (i < content.length && (/\s/.test(content[i]) || content[i] === ',')) i++;
|
|
874
|
+
if (i >= content.length) break;
|
|
875
|
+
|
|
876
|
+
const char = content[i];
|
|
877
|
+
|
|
878
|
+
// String value
|
|
879
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
880
|
+
const quote = char;
|
|
881
|
+
let value = '';
|
|
882
|
+
i++;
|
|
883
|
+
while (i < content.length) {
|
|
884
|
+
if (content[i] === '\\' && i + 1 < content.length) {
|
|
885
|
+
value += content[i + 1];
|
|
886
|
+
i += 2;
|
|
887
|
+
} else if (content[i] === quote) {
|
|
888
|
+
i++;
|
|
889
|
+
break;
|
|
890
|
+
} else {
|
|
891
|
+
value += content[i];
|
|
892
|
+
i++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
values.push(value);
|
|
896
|
+
}
|
|
897
|
+
// Number
|
|
898
|
+
else if (/[-\d]/.test(char)) {
|
|
899
|
+
const numMatch = content.slice(i).match(/^-?\d+\.?\d*/);
|
|
900
|
+
if (numMatch) {
|
|
901
|
+
values.push(parseFloat(numMatch[0]));
|
|
902
|
+
i += numMatch[0].length;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Boolean
|
|
906
|
+
else if (content.slice(i, i + 4) === 'true') {
|
|
907
|
+
values.push(true);
|
|
908
|
+
i += 4;
|
|
909
|
+
}
|
|
910
|
+
else if (content.slice(i, i + 5) === 'false') {
|
|
911
|
+
values.push(false);
|
|
912
|
+
i += 5;
|
|
913
|
+
}
|
|
914
|
+
// Nested object
|
|
915
|
+
else if (char === '{') {
|
|
916
|
+
const objStr = extractObjectLiteral(content, i);
|
|
917
|
+
if (objStr) {
|
|
918
|
+
values.push(parseSimpleObject(objStr));
|
|
919
|
+
i += objStr.length;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// Nested array
|
|
923
|
+
else if (char === '[') {
|
|
924
|
+
const nestedArr = extractArrayLiteral(content, i);
|
|
925
|
+
if (nestedArr) {
|
|
926
|
+
values.push(parseSimpleArray(nestedArr));
|
|
927
|
+
i += nestedArr.length;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
i++;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return values;
|
|
936
|
+
}
|