coursecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,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
+ }