Skip to content

Add scorm-export: wrap a DoenetML activity as an LMS-ready SCORM 2004 package - #3019

Open
cqnykamp wants to merge 5 commits into
Doenet:mainfrom
cqnykamp:feat-scorm-export
Open

Add scorm-export: wrap a DoenetML activity as an LMS-ready SCORM 2004 package#3019
cqnykamp wants to merge 5 commits into
Doenet:mainfrom
cqnykamp:feat-scorm-export

Conversation

@cqnykamp

Copy link
Copy Markdown
Contributor

What

A new packages/scorm-export package that wraps a single DoenetML activity in an LMS-ready SCORM 2004 4th Edition zip — no PreTeXt toolchain, no book chrome. This is the groundwork for a "Download SCORM" button on doenet.org: an instructor exports one activity, uploads the zip to their LMS, and student scores and work persist in the LMS itself (no phone-home to doenet.org).

How it works

A package is six static files in a flat zip:

  • imsmanifest.xml — minimal SCORM 2004 4th Ed. manifest (one item, one SCO)
  • index.html — chrome-free shell hosting the activity iframe
  • activity.html — loads @doenet/standalone from CDN, embeds the DoenetML, speaks SPLICE to the parent
  • ptx_scorm_events.js — PreTeXt's SCORM bridge (vendored; LMS API discovery, scoring, state save/restore, submit)
  • lti_iframe_resizer.js — PreTeXt's SPLICE lti.frameResize handler (vendored, verbatim)
  • lz-string.min.js — pinned npm dep, copied in at build time; compresses state for suspend_data

build.mjs is template-substitution + zip (Node built-ins + the zip CLI). The design is deliberately client-side-able: the same substitute-and-zip could run in the browser behind a button.

node packages/scorm-export/build.mjs sample/sample.doenet --title "My Activity"

Reusing the PreTeXt bridge

The SCORM runtime intelligence is reused from PreTeXt (js/ptx_scorm_events.js, js/lti_iframe_resizer.js) rather than reimplemented, and kept as verbatim vendored copies (see vendor/VENDORED.md) so upstream fixes re-sync cleanly. The bridge is ~96% byte-identical to upstream; the local changes (all marked VENDOR-MOD) do one thing: persist the Doenet activity state through the SCORM data model instead of localStorage-only, so student work restores on a fresh LMS launch.

  • _doenetStates is compressed (lz-string, base64) into cmi.suspend_data; the manifest declares 4th Edition for its 64,000-char suspend_data cap. A size guard drops the blob (falling back to localStorage) rather than corrupting suspend_data if it would ever overflow.
  • Upstream only saves the Doenet state blob when Runestone is present (gated on RunestoneBase.__ptxScormHooked); in a Runestone-free standalone package the state was never captured, so the SPLICE message handler now forwards state (and the real subject) into recordInteraction.

These bridge changes are a candidate to contribute upstream to PreTeXt; if accepted there, the local mod drops and we re-copy verbatim.

Verified

End-to-end on Canvas: state save → compress → persist in suspend_data → restore on relaunch → deliver back to Doenet. cmi.entry = "resume", score restored, and the compressed state (dz) round-trips.

Debugging

debug/size-probe.html is a passive diagnostic (state-blob size, suspend_data round-trip, lti.frameResize tracking). It is not in a normal package — pass --debug to inline it.

Notes / known limitations

  • The viewer loads @doenet/standalone from jsDelivr; pin --doenet-version for reproducible packages. A fully offline package would bundle the viewer into the zip.
  • Instructor per-learner review depends on the LMS offering a review-mode launch; Canvas's native SCORM player only surfaces the grade (documented in the README).
  • In Canvas's SCORM player the activity iframe resizes correctly, but the player's own outer frame is fixed-height and not SPLICE-aware, so a scrollbar there is expected (documented).

🤖 Generated with Claude Code

cqnykamp and others added 5 commits July 23, 2026 11:45
…ckage

New workspace package `@doenet-tools/scorm-export`: the basis for a
"Download SCORM" button on doenet.org. Given one DoenetML activity, it
produces an LMS-ready SCORM 2004 (4th Edition) zip with no PreTeXt
toolchain and no book chrome.

Contents:
- build.mjs: template substitution + zip packaging (Node built-ins only).
- templates/: chrome-free index.html shell, activity.html embedding the
  activity and loading @doenet/standalone from CDN, and a minimal SCORM
  2004 4th Edition manifest.
- vendor/: PreTeXt's SCORM bridge (ptx_scorm_events.js) and SPLICE resize
  handler (lti_iframe_resizer.js), plus lz-string; see vendor/VENDORED.md.

Local modifications to the vendored bridge (all marked VENDOR-MOD), a
candidate to contribute upstream to PreTeXt:
- Capture Doenet state in a Runestone-free package (upstream only saved it
  when RunestoneBase was present).
- Persist that state (compressed, LZ-string) into cmi.suspend_data so it
  restores across LMS launches, since localStorage does not survive them;
  4th Edition is declared for the 64,000-char suspend_data limit, with a
  size guard that degrades gracefully on overflow.

Verified end-to-end on Canvas: score + state save, persist, and restore.
The vendored files are Prettier-ignored to keep them verbatim. A separate
DocViewer state-restore race in @doenet/standalone is filed against the
DoenetML repo and is not addressed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ring it

lz-string is an unmodified, published package, so npm is a better fit than
a committed minified blob: version + integrity now live in package.json and
the lockfile, and `npm audit`/Renovate can track it.

- Pin `lz-string@1.5.0` in package.json (exact — these bytes ship into
  student-facing packages, so bumps should be deliberate).
- build.mjs resolves the minified build from node_modules via
  import.meta.resolve and copies it into the package; the two PreTeXt files
  (locally modified) stay vendored.
- Remove vendor/lz-string.min.js; update VENDORED.md and README.

The packaged bytes are identical to the previously-vendored copy; runtime
behavior is unchanged. README also corrected for the 4th-Edition +
compressed-suspend_data persistence (was still describing the old
localStorage-only prototype).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the state-blob / suspend_data diagnostic out of the always-shipped
index.html template into debug/size-probe.html. build.mjs inlines it into
index.html only when run with --debug; a normal package contains no trace
of it (file count unchanged, since it is inlined rather than added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend debug/size-probe.html to log each lti.frameResize: the height
Doenet reported, the height the resizer applied to the activity iframe,
and whether index.html overflows the box the LMS gave it. The overflow
clause distinguishes an inner scrollbar (ours to fix) from the LMS
player's outer frame (not controllable from the SCO). The vendored
lti_iframe_resizer.js is left byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant