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
package/lib/import.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Command
|
|
3
|
+
*
|
|
4
|
+
* Imports a PowerPoint file as a CourseCode presentation course.
|
|
5
|
+
* Uses PowerPoint (via AppleScript on macOS) to export slides as PNGs,
|
|
6
|
+
* extracts text to markdown, and scaffolds a complete course project.
|
|
7
|
+
*
|
|
8
|
+
* Also supports --slides-dir for pre-exported slide images (no PowerPoint needed).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import fsp from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { create } from './create.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
// ─── Core Logic ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Import a PowerPoint into an existing course directory (in-place).
|
|
25
|
+
* Pure logic — no process.exit(), no console output.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} pptxPath - Absolute path to .pptx file
|
|
28
|
+
* @param {string} courseDir - Absolute path to course/ directory (e.g., /project/course)
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {string} [options.slidesDir] - Pre-exported slide images directory (skips PowerPoint export)
|
|
31
|
+
* @returns {Promise<{slideCount: number, sourceFile: string, textExtracted: boolean}>}
|
|
32
|
+
*/
|
|
33
|
+
export async function importInPlace(pptxPath, courseDir, options = {}) {
|
|
34
|
+
const ext = path.extname(pptxPath).toLowerCase();
|
|
35
|
+
if (ext !== '.pptx') {
|
|
36
|
+
throw new Error('Only .pptx files are supported.');
|
|
37
|
+
}
|
|
38
|
+
if (!fs.existsSync(pptxPath)) {
|
|
39
|
+
throw new Error(`File not found: ${pptxPath}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Acquire slide images ────────────────────────────────────
|
|
43
|
+
let pngFiles;
|
|
44
|
+
let tempDir = null;
|
|
45
|
+
|
|
46
|
+
if (options.slidesDir) {
|
|
47
|
+
const slidesDir = path.resolve(options.slidesDir);
|
|
48
|
+
if (!fs.existsSync(slidesDir)) {
|
|
49
|
+
throw new Error(`Slides directory not found: ${slidesDir}`);
|
|
50
|
+
}
|
|
51
|
+
pngFiles = await findExportedPngs(slidesDir);
|
|
52
|
+
} else {
|
|
53
|
+
if (process.platform !== 'darwin') {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Automated PowerPoint export is only supported on macOS. ' +
|
|
56
|
+
'Export slides to PNG manually and use the slidesDir option.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!detectPowerPoint()) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Microsoft PowerPoint not found. ' +
|
|
62
|
+
'Export slides to PNG manually and use the slidesDir option.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tempDir = path.join(PACKAGE_ROOT, '.tmp-slide-export');
|
|
67
|
+
await fsp.mkdir(tempDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
exportSlidesToPng(pptxPath, tempDir);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
await fsp.rm(tempDir, { recursive: true, force: true });
|
|
73
|
+
throw new Error(`PowerPoint export failed: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pngFiles = await findExportedPngs(tempDir);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (pngFiles.length === 0) {
|
|
80
|
+
if (tempDir) await fsp.rm(tempDir, { recursive: true, force: true });
|
|
81
|
+
throw new Error('No slide images found (.png, .jpg, .jpeg).');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Copy images to assets/slides/ ───────────────────────────
|
|
85
|
+
const assetsDir = path.join(courseDir, 'assets', 'slides');
|
|
86
|
+
await fsp.mkdir(assetsDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
for (const png of pngFiles) {
|
|
89
|
+
const imgExt = path.extname(png.name).toLowerCase();
|
|
90
|
+
const destName = `slide-${String(png.num).padStart(2, '0')}${imgExt}`;
|
|
91
|
+
await fsp.copyFile(png.path, path.join(assetsDir, destName));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Clear existing slides ───────────────────────────────────
|
|
95
|
+
const slidesDir = path.join(courseDir, 'slides');
|
|
96
|
+
await fsp.mkdir(slidesDir, { recursive: true });
|
|
97
|
+
const existingSlides = await fsp.readdir(slidesDir);
|
|
98
|
+
for (const file of existingSlides) {
|
|
99
|
+
if (file.endsWith('.html') || file.endsWith('.js')) {
|
|
100
|
+
await fsp.unlink(path.join(slidesDir, file));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Generate slide HTML files ───────────────────────────────
|
|
105
|
+
for (let i = 0; i < pngFiles.length; i++) {
|
|
106
|
+
const slideNum = i + 1;
|
|
107
|
+
const paddedNum = String(slideNum).padStart(2, '0');
|
|
108
|
+
const imgExt = path.extname(pngFiles[i].name).toLowerCase();
|
|
109
|
+
const html = generateSlideHtml(paddedNum, imgExt);
|
|
110
|
+
await fsp.writeFile(
|
|
111
|
+
path.join(slidesDir, `slide-${paddedNum}.html`),
|
|
112
|
+
html,
|
|
113
|
+
'utf-8'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Extract text to references ──────────────────────────────
|
|
118
|
+
let textExtracted = false;
|
|
119
|
+
try {
|
|
120
|
+
const markdown = await extractText(pptxPath);
|
|
121
|
+
if (markdown) {
|
|
122
|
+
const refsDir = path.join(courseDir, 'references', 'converted');
|
|
123
|
+
await fsp.mkdir(refsDir, { recursive: true });
|
|
124
|
+
await fsp.writeFile(
|
|
125
|
+
path.join(refsDir, `${path.basename(pptxPath, ext)}.md`),
|
|
126
|
+
markdown,
|
|
127
|
+
'utf-8'
|
|
128
|
+
);
|
|
129
|
+
textExtracted = true;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Text extraction is best-effort
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Write course-config.js ──────────────────────────────────
|
|
136
|
+
const name = path.basename(pptxPath, ext).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
137
|
+
const configContent = generateCourseConfig(name, pngFiles.length);
|
|
138
|
+
await fsp.writeFile(
|
|
139
|
+
path.join(courseDir, 'course-config.js'),
|
|
140
|
+
configContent,
|
|
141
|
+
'utf-8'
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// ── Cleanup ─────────────────────────────────────────────────
|
|
145
|
+
if (tempDir) {
|
|
146
|
+
await fsp.rm(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
slideCount: pngFiles.length,
|
|
151
|
+
sourceFile: path.basename(pptxPath),
|
|
152
|
+
textExtracted
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── CLI Entry Point ─────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* CLI import command — creates a new project and imports PowerPoint into it.
|
|
160
|
+
*/
|
|
161
|
+
export async function importPresentation(source, options = {}) {
|
|
162
|
+
const sourcePath = path.resolve(source);
|
|
163
|
+
const ext = path.extname(sourcePath).toLowerCase();
|
|
164
|
+
|
|
165
|
+
if (ext !== '.pptx') {
|
|
166
|
+
console.error('\n❌ Only .pptx files are supported.\n');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(sourcePath)) {
|
|
171
|
+
console.error(`\n❌ File not found: ${sourcePath}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const name = options.name || path.basename(sourcePath, ext).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
176
|
+
|
|
177
|
+
console.log(`\n📊 Importing PowerPoint: ${path.basename(sourcePath)}`);
|
|
178
|
+
console.log(` Project name: ${name}\n`);
|
|
179
|
+
|
|
180
|
+
// Create blank project (no example slides — they'd just be deleted)
|
|
181
|
+
console.log(' ⏳ Creating course project...\n');
|
|
182
|
+
await create(name, { blank: true, install: options.install });
|
|
183
|
+
|
|
184
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
185
|
+
const courseDir = path.join(targetDir, 'course');
|
|
186
|
+
|
|
187
|
+
// Import presentation in-place
|
|
188
|
+
console.log('\n ⏳ Importing presentation slides...');
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const result = await importInPlace(sourcePath, courseDir, {
|
|
192
|
+
slidesDir: options.slidesDir
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(` ✅ ${result.slideCount} presentation slides created`);
|
|
196
|
+
if (result.textExtracted) {
|
|
197
|
+
console.log(' ✅ Text extracted to markdown');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`
|
|
201
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
202
|
+
✅ Presentation imported successfully!
|
|
203
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
204
|
+
|
|
205
|
+
${result.slideCount} slides from "${result.sourceFile}" → "${name}/"
|
|
206
|
+
|
|
207
|
+
Next steps:
|
|
208
|
+
|
|
209
|
+
cd ${name}
|
|
210
|
+
coursecode preview # Preview the presentation
|
|
211
|
+
coursecode build # Build SCORM/cmi5 package
|
|
212
|
+
|
|
213
|
+
Enhance with AI:
|
|
214
|
+
- Add assessments between slides
|
|
215
|
+
- Replace image slides with interactive HTML
|
|
216
|
+
- Add engagement tracking requirements
|
|
217
|
+
- Reference: course/references/converted/ for slide text
|
|
218
|
+
|
|
219
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
220
|
+
`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(`\n❌ Import failed: ${error.message}\n`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if Microsoft PowerPoint is installed (macOS)
|
|
231
|
+
*/
|
|
232
|
+
function detectPowerPoint() {
|
|
233
|
+
const appPaths = [
|
|
234
|
+
'/Applications/Microsoft PowerPoint.app',
|
|
235
|
+
path.join(process.env.HOME, 'Applications/Microsoft PowerPoint.app')
|
|
236
|
+
];
|
|
237
|
+
return appPaths.some(p => fs.existsSync(p));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Export PPTX slides to PNG via AppleScript (macOS)
|
|
242
|
+
*/
|
|
243
|
+
function exportSlidesToPng(pptxPath, outputDir) {
|
|
244
|
+
const script = `
|
|
245
|
+
tell application "Microsoft PowerPoint"
|
|
246
|
+
activate
|
|
247
|
+
open POSIX file "${pptxPath}"
|
|
248
|
+
delay 2
|
|
249
|
+
save active presentation in POSIX file "${outputDir}" as save as PNG
|
|
250
|
+
close active presentation saving no
|
|
251
|
+
end tell
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
256
|
+
timeout: 60000,
|
|
257
|
+
stdio: 'pipe'
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new Error(`AppleScript failed: ${error.stderr?.toString() || error.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Find exported image files in directory, sorted by slide number.
|
|
266
|
+
* Supports PNG, JPG, JPEG.
|
|
267
|
+
*/
|
|
268
|
+
async function findExportedPngs(dir) {
|
|
269
|
+
const images = [];
|
|
270
|
+
const imageExts = ['.png', '.jpg', '.jpeg'];
|
|
271
|
+
|
|
272
|
+
async function scan(scanDir) {
|
|
273
|
+
const entries = await fsp.readdir(scanDir, { withFileTypes: true });
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const fullPath = path.join(scanDir, entry.name);
|
|
276
|
+
if (entry.isDirectory()) {
|
|
277
|
+
await scan(fullPath);
|
|
278
|
+
} else {
|
|
279
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
280
|
+
if (imageExts.includes(ext)) {
|
|
281
|
+
const match = entry.name.match(/(\d+)/);
|
|
282
|
+
const num = match ? parseInt(match[1], 10) : 0;
|
|
283
|
+
images.push({ path: fullPath, name: entry.name, num });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await scan(dir);
|
|
290
|
+
|
|
291
|
+
// If no numbers found, assign sequential numbers by alphabetical order
|
|
292
|
+
if (images.every(img => img.num === 0)) {
|
|
293
|
+
images.sort((a, b) => a.name.localeCompare(b.name));
|
|
294
|
+
images.forEach((img, i) => { img.num = i + 1; });
|
|
295
|
+
} else {
|
|
296
|
+
images.sort((a, b) => a.num - b.num);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return images;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Extract text content from PPTX as markdown
|
|
304
|
+
*/
|
|
305
|
+
async function extractText(pptxPath) {
|
|
306
|
+
const PptxParser = (await import('node-pptx-parser')).default;
|
|
307
|
+
|
|
308
|
+
const parser = new PptxParser(pptxPath);
|
|
309
|
+
const textContent = await parser.extractText();
|
|
310
|
+
|
|
311
|
+
let markdown = '';
|
|
312
|
+
let slideNum = 0;
|
|
313
|
+
|
|
314
|
+
for (const slide of textContent) {
|
|
315
|
+
slideNum++;
|
|
316
|
+
markdown += `# Slide ${slideNum}\n\n`;
|
|
317
|
+
|
|
318
|
+
if (slide.text && slide.text.length > 0) {
|
|
319
|
+
for (const text of slide.text) {
|
|
320
|
+
if (text && text.trim()) {
|
|
321
|
+
markdown += `${text.trim()}\n\n`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
markdown += '---\n\n';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return markdown.trim();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate HTML for a single slide (image wrapper)
|
|
334
|
+
*/
|
|
335
|
+
function generateSlideHtml(paddedNum, imgExt = '.png') {
|
|
336
|
+
return `<img src="assets/slides/slide-${paddedNum}${imgExt}" alt="Slide ${parseInt(paddedNum, 10)}" class="img-contain">
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Generate course-config.js for presentation import
|
|
342
|
+
*/
|
|
343
|
+
function generateCourseConfig(name, slideCount) {
|
|
344
|
+
const title = name
|
|
345
|
+
.replace(/-/g, ' ')
|
|
346
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
347
|
+
|
|
348
|
+
const slides = [];
|
|
349
|
+
for (let i = 1; i <= slideCount; i++) {
|
|
350
|
+
const padded = String(i).padStart(2, '0');
|
|
351
|
+
slides.push(` { id: 'slide-${padded}', title: 'Slide ${i}', file: 'slides/slide-${padded}.html' }`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return `/**
|
|
355
|
+
* Course Configuration
|
|
356
|
+
* Imported from PowerPoint presentation
|
|
357
|
+
*/
|
|
358
|
+
export const courseConfig = {
|
|
359
|
+
title: '${title}',
|
|
360
|
+
source: 'powerpoint-import',
|
|
361
|
+
layout: 'presentation',
|
|
362
|
+
|
|
363
|
+
navigation: {
|
|
364
|
+
sidebar: { enabled: false },
|
|
365
|
+
breadcrumbs: { enabled: false }
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
slideDefaults: {
|
|
369
|
+
contentWidth: 'full'
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
structure: [
|
|
373
|
+
${slides.join(',\n')}
|
|
374
|
+
]
|
|
375
|
+
};
|
|
376
|
+
`;
|
|
377
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CourseCode Framework - Library Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This module exports the core utilities for programmatic course building.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Stub Player - generates browser-based course player
|
|
8
|
+
export { generateStubPlayer } from './stub-player.js';
|
|
9
|
+
|
|
10
|
+
// Manifest Generation - creates SCORM/cmi5 manifests
|
|
11
|
+
export { generateManifest, getSchemaFiles } from './manifest/manifest-factory.js';
|
|
12
|
+
|
|
13
|
+
// Content Parsing - unified parser for courses
|
|
14
|
+
export {
|
|
15
|
+
parseCourse,
|
|
16
|
+
parseSlideSource,
|
|
17
|
+
extractAssessment,
|
|
18
|
+
extractNarration,
|
|
19
|
+
extractInteractions,
|
|
20
|
+
parseElements,
|
|
21
|
+
resolveElementByPath
|
|
22
|
+
} from './course-parser.js';
|
|
23
|
+
|
|
24
|
+
// Build utilities
|
|
25
|
+
export { build } from './build.js';
|
|
26
|
+
export {
|
|
27
|
+
createStandardPackage,
|
|
28
|
+
createProxyPackage,
|
|
29
|
+
createRemotePackage,
|
|
30
|
+
createExternalPackagesForClients,
|
|
31
|
+
validateExternalHostingConfig
|
|
32
|
+
} from './build-packaging.js';
|
|
33
|
+
|
|
34
|
+
// Build Linter - validate course configuration
|
|
35
|
+
export {
|
|
36
|
+
lintCourse,
|
|
37
|
+
lint
|
|
38
|
+
} from './build-linter.js';
|
|
39
|
+
|
|
40
|
+
// Shared Validation Rules (used by both browser and Node.js linters)
|
|
41
|
+
export {
|
|
42
|
+
flattenStructure,
|
|
43
|
+
validateAssessmentConfig,
|
|
44
|
+
validateQuestionConfig,
|
|
45
|
+
validateEngagement,
|
|
46
|
+
validateRequirementConfig,
|
|
47
|
+
validateGlobalConfig,
|
|
48
|
+
formatLintResults
|
|
49
|
+
} from './validation-rules.js';
|
|
50
|
+
|
|
51
|
+
// Re-export path utilities for template access
|
|
52
|
+
import { fileURLToPath } from 'url';
|
|
53
|
+
import { dirname, join } from 'path';
|
|
54
|
+
|
|
55
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
56
|
+
const __dirname = dirname(__filename);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the absolute path to the template directory
|
|
60
|
+
* @returns {string} Path to template directory
|
|
61
|
+
*/
|
|
62
|
+
export function getTemplatePath() {
|
|
63
|
+
return join(__dirname, '..', 'template');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the absolute path to the framework directory
|
|
68
|
+
* @returns {string} Path to framework directory
|
|
69
|
+
*/
|
|
70
|
+
export function getFrameworkPath() {
|
|
71
|
+
return join(__dirname, '..', 'framework');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the absolute path to the schemas directory
|
|
76
|
+
* @returns {string} Path to schemas directory
|
|
77
|
+
*/
|
|
78
|
+
export function getSchemasPath() {
|
|
79
|
+
return join(__dirname, '..', 'schemas');
|
|
80
|
+
}
|
package/lib/info.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info command - show info about current project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
export async function info() {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
// CLI info
|
|
16
|
+
const cliPkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
17
|
+
|
|
18
|
+
console.log(`
|
|
19
|
+
📦 CourseCode CLI v${cliPkg.version}
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
// Check if in a project
|
|
23
|
+
const rcPath = path.join(cwd, '.coursecoderc.json');
|
|
24
|
+
const courseConfigPath = path.join(cwd, 'course', 'course-config.js');
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(rcPath) && !fs.existsSync(path.join(cwd, 'course'))) {
|
|
27
|
+
console.log(` Not in a CourseCode project directory.
|
|
28
|
+
|
|
29
|
+
Create a new project:
|
|
30
|
+
coursecode create my-course
|
|
31
|
+
`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(` Project directory: ${cwd}`);
|
|
36
|
+
|
|
37
|
+
// Read .coursecoderc.json if exists
|
|
38
|
+
if (fs.existsSync(rcPath)) {
|
|
39
|
+
const rcConfig = JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
|
|
40
|
+
console.log(` Framework version: ${rcConfig.frameworkVersion}`);
|
|
41
|
+
if (rcConfig.createdAt) {
|
|
42
|
+
console.log(` Created: ${new Date(rcConfig.createdAt).toLocaleDateString()}`);
|
|
43
|
+
}
|
|
44
|
+
if (rcConfig.upgradedAt) {
|
|
45
|
+
console.log(` Last upgraded: ${new Date(rcConfig.upgradedAt).toLocaleDateString()}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Read course config for metadata
|
|
50
|
+
if (fs.existsSync(courseConfigPath)) {
|
|
51
|
+
const configContent = fs.readFileSync(courseConfigPath, 'utf-8');
|
|
52
|
+
|
|
53
|
+
// Extract metadata with regex
|
|
54
|
+
const titleMatch = configContent.match(/title:\s*["']([^"']+)["']/);
|
|
55
|
+
const versionMatch = configContent.match(/version:\s*["']([^"']+)["']/);
|
|
56
|
+
|
|
57
|
+
if (titleMatch || versionMatch) {
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' Course info:');
|
|
60
|
+
if (titleMatch) console.log(` Title: ${titleMatch[1]}`);
|
|
61
|
+
if (versionMatch) console.log(` Version: ${versionMatch[1]}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for dist
|
|
66
|
+
const distDir = path.join(cwd, 'dist');
|
|
67
|
+
if (fs.existsSync(distDir)) {
|
|
68
|
+
const files = fs.readdirSync(distDir);
|
|
69
|
+
console.log(`\n Build output: dist/ (${files.length} files)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for ZIP
|
|
73
|
+
const zipFiles = fs.readdirSync(cwd).filter(f => f.endsWith('.zip'));
|
|
74
|
+
if (zipFiles.length > 0) {
|
|
75
|
+
console.log(` Course package: ${zipFiles[zipFiles.length - 1]}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|