UNPKG

scorm-again

Version:

A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004

1,314 lines (1,298 loc) 254 kB
this.Scorm12API = (function () { 'use strict'; const SECONDS_PER_MINUTE = 60; const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE; const getSecondsAsHHMMSS = totalSeconds => { if (!totalSeconds || totalSeconds <= 0) { return "00:00:00"; } const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR); const dateObj = new Date(totalSeconds * 1e3); const minutes = dateObj.getUTCMinutes(); const seconds = dateObj.getSeconds(); const ms = totalSeconds % 1; let msStr = ""; if (countDecimals(ms) > 0) { if (countDecimals(ms) > 2) { msStr = ms.toFixed(2); } else { msStr = String(ms); } msStr = "." + msStr.split(".")[1]; } return (hours + ":" + minutes + ":" + seconds).replace(/\b\d\b/g, "0$&") + msStr; }; const getTimeAsSeconds = memoize((timeString, timeRegex) => { if (typeof timeString === "number" || typeof timeString === "boolean") { timeString = String(timeString); } if (typeof timeRegex === "string") { timeRegex = new RegExp(timeRegex); } if (!timeString) { return 0; } if (!timeString.match(timeRegex)) { if (/^\d+(?:\.\d+)?$/.test(timeString)) { return Number(timeString); } return 0; } const parts = timeString.split(":"); const hours = Number(parts[0]); const minutes = Number(parts[1]); const seconds = Number(parts[2]); return hours * 3600 + minutes * 60 + seconds; }, // Custom key function to handle RegExp objects which can't be stringified (timeString, timeRegex) => { const timeStr = typeof timeString === "string" ? timeString : String(timeString ?? ""); const regexStr = typeof timeRegex === "string" ? timeRegex : timeRegex?.toString() ?? ""; return `${timeStr}:${regexStr}`; }); const getDurationAsSeconds = memoize((duration, durationRegex) => { if (typeof durationRegex === "string") { durationRegex = new RegExp(durationRegex); } if (!duration || !duration?.match?.(durationRegex)) { return 0; } const [, years, months, weeks, days, hours, minutes, seconds] = new RegExp(durationRegex).exec?.(duration) ?? []; let result = 0; result += Number(seconds) || 0; result += Number(minutes) * 60 || 0; result += Number(hours) * 3600 || 0; result += Number(days) * (60 * 60 * 24) || 0; result += Number(weeks) * (60 * 60 * 24 * 7) || 0; result += Number(months) * (60 * 60 * 24 * 30) || 0; result += Number(years) * (60 * 60 * 24 * 365) || 0; return result; }, // Custom key function to handle RegExp objects which can't be stringified (duration, durationRegex) => { const durationStr = duration ?? ""; const regexStr = typeof durationRegex === "string" ? durationRegex : durationRegex?.toString() ?? ""; return `${durationStr}:${regexStr}`; }); function addHHMMSSTimeStrings(first, second, timeRegex) { if (typeof timeRegex === "string") { timeRegex = new RegExp(timeRegex); } return getSecondsAsHHMMSS(getTimeAsSeconds(first, timeRegex) + getTimeAsSeconds(second, timeRegex)); } function flatten(data) { const result = {}; function recurse(cur, prop) { if (Object(cur) !== cur) { result[prop] = cur; } else if (Array.isArray(cur)) { cur.forEach((item, i) => { recurse(item, `${prop}[${i}]`); }); if (cur.length === 0) result[prop] = []; } else { const keys = Object.keys(cur).filter(p => Object.prototype.hasOwnProperty.call(cur, p)); const isEmpty = keys.length === 0; keys.forEach(p => { recurse(cur[p], prop ? `${prop}.${p}` : p); }); if (isEmpty && prop) result[prop] = {}; } } recurse(data, ""); return result; } function unflatten(data) { if (Object(data) !== data || Array.isArray(data)) return data; const result = {}; const pattern = /\.?([^.[\]]+)|\[(\d+)]/g; Object.keys(data).filter(p => Object.prototype.hasOwnProperty.call(data, p)).forEach(p => { let cur = result; let prop = ""; const regex = new RegExp(pattern); Array.from({ length: p.match(new RegExp(pattern, "g"))?.length ?? 0 }, () => regex.exec(p)).forEach(m => { if (m) { cur = cur[prop] ?? (cur[prop] = m[2] ? [] : {}); prop = m[2] || m[1] || ""; } }); cur[prop] = data[p]; }); return result[""] ?? result; } function countDecimals(num) { if (Math.floor(num) === num || String(num)?.indexOf?.(".") < 0) return 0; const parts = num.toString().split(".")?.[1]; return parts?.length ?? 0; } function formatMessage(functionName, message, CMIElement) { const baseLength = 20; let messageString = functionName ? `${String(functionName).padEnd(baseLength)}: ` : ""; if (CMIElement) { const CMIElementBaseLength = 70; messageString += CMIElement; messageString = messageString.padEnd(CMIElementBaseLength); } messageString += message ?? ""; return messageString; } function stringMatches(str, tester) { if (typeof str !== "string") { return false; } return new RegExp(tester).test(str); } function memoize(fn, keyFn) { const cache = /* @__PURE__ */new Map(); return function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } const key = keyFn ? keyFn(...args) : JSON.stringify(args); return cache.has(key) ? cache.get(key) : (() => { const result = fn(...args); cache.set(key, result); return result; })(); }; } var __defProp$m = Object.defineProperty; var __defNormalProp$m = (obj, key, value) => key in obj ? __defProp$m(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$m = (obj, key, value) => __defNormalProp$m(obj, typeof key !== "symbol" ? key + "" : key, value); class BaseCMI { /** * Constructor for BaseCMI * @param {string} cmi_element */ constructor(cmi_element) { /** * Flag used during JSON serialization to allow getter access without initialization checks. * When true, getters can be accessed before the API is initialized, which is necessary * for serializing the CMI data structure to JSON format. */ __publicField$m(this, "jsonString", false); __publicField$m(this, "_cmi_element"); __publicField$m(this, "_initialized", false); this._cmi_element = cmi_element; } /** * Getter for _initialized * @return {boolean} */ get initialized() { return this._initialized; } /** * Called when the API has been initialized after the CMI has been created */ initialize() { this._initialized = true; } } class BaseRootCMI extends BaseCMI { constructor() { super(...arguments); __publicField$m(this, "_start_time"); } /** * Start time of the session * @type {number | undefined} * @protected */ get start_time() { return this._start_time; } /** * Setter for start_time. Can only be called once. */ setStartTime() { if (this._start_time === void 0) { this._start_time = (/* @__PURE__ */new Date()).getTime(); } else { throw new Error("Start time has already been set."); } } } var __defProp$l = Object.defineProperty; var __defNormalProp$l = (obj, key, value) => key in obj ? __defProp$l(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$l = (obj, key, value) => __defNormalProp$l(obj, typeof key !== "symbol" ? key + "" : key, value); class BaseScormValidationError extends Error { constructor(CMIElement, errorCode) { super(`${CMIElement} : ${errorCode.toString()}`); __publicField$l(this, "_errorCode"); this._errorCode = errorCode; Object.setPrototypeOf(this, BaseScormValidationError.prototype); } /** * Getter for _errorCode * @return {number} */ get errorCode() { return this._errorCode; } } class ValidationError extends BaseScormValidationError { /** * Constructor to take in an error message and code * @param {string} CMIElement * @param {number} errorCode * @param {string} errorMessage * @param {string} detailedMessage */ constructor(CMIElement, errorCode, errorMessage, detailedMessage) { super(CMIElement, errorCode); __publicField$l(this, "_errorMessage"); __publicField$l(this, "_detailedMessage", ""); this.message = `${CMIElement} : ${errorMessage}`; this._errorMessage = errorMessage; if (detailedMessage) { this._detailedMessage = detailedMessage; } Object.setPrototypeOf(this, ValidationError.prototype); } /** * Getter for _errorMessage * @return {string} */ get errorMessage() { return this._errorMessage; } /** * Getter for _detailedMessage * @return {string} */ get detailedMessage() { return this._detailedMessage; } } const global_constants = { SCORM_TRUE: "true", SCORM_FALSE: "false", STATE_NOT_INITIALIZED: 0, STATE_INITIALIZED: 1, STATE_TERMINATED: 2 }; const scorm12_constants = { // Children lists cmi_children: "core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions", core_children: "student_id,student_name,lesson_location,credit,lesson_status,entry,score,total_time,lesson_mode,exit,session_time", score_children: "raw,min,max", objectives_children: "id,score,status", correct_responses_children: "pattern", student_data_children: "mastery_score,max_time_allowed,time_limit_action", student_preference_children: "audio,language,speed,text", interactions_children: "id,objectives,time,type,correct_responses,weighting,student_response,result,latency", error_descriptions: { "0": { basicMessage: "No Error", detailMessage: "No error occurred, the previous API call was successful." }, "101": { basicMessage: "General Exception", detailMessage: "No specific error code exists to describe the error." }, "201": { basicMessage: "Invalid argument error", detailMessage: "Indicates that an argument represents an invalid data model element or is otherwise incorrect." }, "202": { basicMessage: "Element cannot have children", detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_children" for a data model element that does not support the "_children" suffix.' }, "203": { basicMessage: "Element not an array - cannot have count", detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_count" for a data model element that does not support the "_count" suffix.' }, "301": { basicMessage: "Not initialized", detailMessage: "Indicates that an API call was made before the call to lmsInitialize." }, "401": { basicMessage: "Not implemented error", detailMessage: "The data model element indicated in a call to LMSGetValue or LMSSetValue is valid, but was not implemented by this LMS. SCORM 1.2 defines a set of data model elements as being optional for an LMS to implement." }, "402": { basicMessage: "Invalid set value, element is a keyword", detailMessage: 'Indicates that LMSSetValue was called on a data model element that represents a keyword (elements that end in "_children" and "_count").' }, "403": { basicMessage: "Element is read only", detailMessage: "LMSSetValue was called with a data model element that can only be read." }, "404": { basicMessage: "Element is write only", detailMessage: "LMSGetValue was called on a data model element that can only be written to." }, "405": { basicMessage: "Incorrect Data Type", detailMessage: "LMSSetValue was called with a value that is not consistent with the data format of the supplied data model element." }, "407": { basicMessage: "Element Value Out Of Range", detailMessage: "The numeric value supplied to a LMSSetValue call is outside of the numeric range allowed for the supplied data model element." }, "408": { basicMessage: "Data Model Dependency Not Established", detailMessage: "Some data model elements cannot be set until another data model element was set. This error condition indicates that the prerequisite element was not set before the dependent element." } } }; const scorm12_errors$1 = scorm12_constants.error_descriptions; class Scorm12ValidationError extends ValidationError { /** * Constructor to take in an error code * @param {string} CMIElement * @param {number} errorCode */ constructor(CMIElement, errorCode) { if ({}.hasOwnProperty.call(scorm12_errors$1, String(errorCode))) { super(CMIElement, errorCode, scorm12_errors$1[String(errorCode)]?.basicMessage || "Unknown error", scorm12_errors$1[String(errorCode)]?.detailMessage); } else { super(CMIElement, 101, scorm12_errors$1["101"]?.basicMessage, scorm12_errors$1["101"]?.detailMessage); } Object.setPrototypeOf(this, Scorm12ValidationError.prototype); } } const global_errors = { GENERAL: 101, INITIALIZATION_FAILED: 101, INITIALIZED: 101, TERMINATED: 101, TERMINATION_FAILURE: 101, TERMINATION_BEFORE_INIT: 101, MULTIPLE_TERMINATION: 101, RETRIEVE_BEFORE_INIT: 101, RETRIEVE_AFTER_TERM: 101, STORE_BEFORE_INIT: 101, STORE_AFTER_TERM: 101, COMMIT_BEFORE_INIT: 101, COMMIT_AFTER_TERM: 101, ARGUMENT_ERROR: 101, CHILDREN_ERROR: 101, COUNT_ERROR: 101, GENERAL_GET_FAILURE: 101, GENERAL_SET_FAILURE: 101, GENERAL_COMMIT_FAILURE: 101, UNDEFINED_DATA_MODEL: 101, UNIMPLEMENTED_ELEMENT: 101, VALUE_NOT_INITIALIZED: 101, INVALID_SET_VALUE: 101, READ_ONLY_ELEMENT: 101, WRITE_ONLY_ELEMENT: 101, TYPE_MISMATCH: 101, VALUE_OUT_OF_RANGE: 101, DEPENDENCY_NOT_ESTABLISHED: 101 }; const scorm12_errors = { ...global_errors, RETRIEVE_BEFORE_INIT: 301, STORE_BEFORE_INIT: 301, COMMIT_BEFORE_INIT: 301, ARGUMENT_ERROR: 201, CHILDREN_ERROR: 202, COUNT_ERROR: 203, UNDEFINED_DATA_MODEL: 401, UNIMPLEMENTED_ELEMENT: 401, VALUE_NOT_INITIALIZED: 301, INVALID_SET_VALUE: 402, READ_ONLY_ELEMENT: 403, WRITE_ONLY_ELEMENT: 404, TYPE_MISMATCH: 405, VALUE_OUT_OF_RANGE: 407, DEPENDENCY_NOT_ESTABLISHED: 408 }; var __defProp$k = Object.defineProperty; var __defNormalProp$k = (obj, key, value) => key in obj ? __defProp$k(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$k = (obj, key, value) => __defNormalProp$k(obj, typeof key !== "symbol" ? key + "" : key, value); class CMIArray extends BaseCMI { /** * Constructor cmi *.n arrays * @param {object} params */ constructor(params) { super(params.CMIElement); __publicField$k(this, "_errorCode"); __publicField$k(this, "_errorClass"); __publicField$k(this, "__children"); __publicField$k(this, "childArray"); this.__children = params.children; this._errorCode = params.errorCode ?? scorm12_errors.GENERAL; this._errorClass = params.errorClass || BaseScormValidationError; this.childArray = []; } /** * Called when the API has been reset */ reset() { let wipe = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; this._initialized = false; if (wipe) { this.childArray = []; } else { for (let i = 0; i < this.childArray.length; i++) { this.childArray[i]?.reset(); } } } /** * Getter for _children * @return {string} */ get _children() { return this.__children; } /** * Setter for _children. Just throws an error. * @param {string} _children */ set _children(_children) { throw new this._errorClass(this._cmi_element + "._children", this._errorCode); } /** * Getter for _count * @return {number} */ get _count() { return this.childArray.length; } /** * Setter for _count. Just throws an error. * @param {number} _count */ set _count(_count) { throw new this._errorClass(this._cmi_element + "._count", this._errorCode); } /** * toJSON for *.n arrays * @return {object} */ toJSON() { this.jsonString = true; const result = {}; for (let i = 0; i < this.childArray.length; i++) { result[i + ""] = this.childArray[i]; } this.jsonString = false; return result; } } const SuccessStatus = { PASSED: "passed", FAILED: "failed", UNKNOWN: "unknown" }; const CompletionStatus = { COMPLETED: "completed", INCOMPLETE: "incomplete", UNKNOWN: "unknown" }; const LogLevelEnum = { _: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4, NONE: 5 }; const DefaultSettings = { autocommit: false, autocommitSeconds: 10, throttleCommits: false, useAsynchronousCommits: false, sendFullCommit: true, lmsCommitUrl: false, dataCommitFormat: "json", commitRequestDataType: "application/json;charset=UTF-8", autoProgress: false, logLevel: LogLevelEnum.ERROR, selfReportSessionTime: false, alwaysSendTotalTime: false, renderCommonCommitFields: false, autoCompleteLessonStatus: false, strict_errors: true, xhrHeaders: {}, xhrWithCredentials: false, fetchMode: "cors", asyncModeBeaconBehavior: "never", responseHandler: async function (response) { if (typeof response !== "undefined") { let httpResult = null; try { if (typeof response.json === "function") { httpResult = await response.json(); } else if (typeof response.text === "function") { const responseText = await response.text(); if (responseText) { httpResult = JSON.parse(responseText); } } } catch (e) {} if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) { if (response.status === 200) { return { result: global_constants.SCORM_TRUE, errorCode: 0 }; } else { return { result: global_constants.SCORM_FALSE, errorCode: 101 }; } } else { return { result: httpResult.result, errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101 }; } } return { result: global_constants.SCORM_FALSE, errorCode: 101 }; }, xhrResponseHandler: function (xhr) { if (typeof xhr !== "undefined") { let httpResult = null; if (xhr.status >= 200 && xhr.status <= 299) { try { httpResult = JSON.parse(xhr.responseText); } catch (e) {} if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) { return { result: global_constants.SCORM_TRUE, errorCode: 0 }; } return { result: httpResult.result, errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101 }; } else { return { result: global_constants.SCORM_FALSE, errorCode: 101 }; } } return { result: global_constants.SCORM_FALSE, errorCode: 101 }; }, requestHandler: function (commitObject) { return commitObject; }, onLogMessage: defaultLogHandler, mastery_override: false, score_overrides_status: false, completion_status_on_failed: "completed", scoItemIds: [], scoItemIdValidator: false, globalObjectiveIds: [], // Offline support settings enableOfflineSupport: false, courseId: "", syncOnInitialize: true, syncOnTerminate: true, maxSyncAttempts: 5, // Multi-SCO support settings scoId: "", autoPopulateCommitMetadata: false, // HTTP service settings httpService: null, // Global learner preferences settings globalStudentPreferences: false }; function defaultLogHandler(messageLevel, logMessage) { switch (messageLevel) { case "4": case 4: case "ERROR": case LogLevelEnum.ERROR: console.error(logMessage); break; case "3": case 3: case "WARN": case LogLevelEnum.WARN: console.warn(logMessage); break; case "2": case 2: case "INFO": case LogLevelEnum.INFO: console.info(logMessage); break; case "1": case 1: case "DEBUG": case LogLevelEnum.DEBUG: if (console.debug) { console.debug(logMessage); } else { console.log(logMessage); } break; } } const scorm12_regex = { /** CMIString256 - Character string, max 255 chars (RTE A.1) */ CMIString256: "^[\\s\\S]{0,255}$", /** CMIString4096 - Character string, max 4096 chars (RTE A.1) */ CMIString4096: "^[\\s\\S]{0,4096}$", /** * CMIString64000 - Extended character string, max 64000 chars * * SPEC COMPLIANCE NOTE: * The SCORM 1.2 specification defines cmi.suspend_data as CMIString4096 (max 4096 chars). * This implementation intentionally increases the limit to 64000 chars (matching SCORM 2004) * for the following reasons: * * 1. Modern content frequently exceeds 4096 chars due to JSON state serialization, * base64 encoding, complex bookmark data, and rich interaction tracking * 2. The 4096 limit was set in 2001 when content was simpler; modern authoring tools * routinely generate larger suspend_data * 3. Most LMS systems can handle larger values - the API shouldn't be the bottleneck * 4. Content that gets rejected has no recovery path, causing data loss * 5. Aligns with SCORM 2004's more practical 64000 char limit * * Used by: cmi.suspend_data (SCORM 1.2) * * Strict spec pattern would be: ^[\s\S]{0,4096}$ */ CMIString64000: "^[\\s\\S]{0,64000}$", /** * CMITime - Clock time in HH:MM:SS.SS format (RTE A.2) * Optional centiseconds (1-2 decimal digits) per spec. */ CMITime: "^(?:[01]\\d|2[0123]):(?:[012345]\\d):(?:[012345]\\d)(\\.\\d{1,2})?$", /** * CMITimespan - Time interval in HHHH:MM:SS.SS format (RTE A.3) * We allow more digits for the hour to support values generated * by getSecondsAsHHMMSS which can produce larger hour values * (e.g., 17496:00:00 for very long durations). * Changed from minimum 2 digits to 1+ digits with no upper limit. */ CMITimespan: "^([0-9]+):([0-9]{2}):([0-9]{2})(\\.\\d{1,2})?$", /** * CMIInteger - Non-negative integer (RTE A.4) * * SPEC COMPLIANCE NOTE: * The SCORM 1.2 specification defines CMIInteger as 0-65536 range. * This implementation intentionally omits range validation to support * legacy content that may exceed this limit in _count fields or other * integer values. Real-world content often violates the spec by storing * larger values, and strict enforcement would break compatibility. * * Affected elements: * - cmi.objectives._count * - cmi.interactions._count * - cmi.interactions.n.objectives._count * - cmi.interactions.n.correct_responses._count */ CMIInteger: "^\\d+$", /** CMISInteger - Signed integer (RTE A.5) */ CMISInteger: "^-?([0-9]+)$", /** * CMIDecimal - Signed decimal (RTE A.6) * We set practical limits on decimals to prevent abuse while maintaining * broad compatibility with legacy content. * Increased from 3 to 10 digits before decimal to match SCORM 2004 behavior. */ CMIDecimal: "^-?([0-9]{0,10})(\\.[0-9]*)?$", /** * CMIIdentifier - Printable ASCII characters, max 255 chars (RTE A.7) * * SPEC COMPLIANCE NOTE: * The SCORM 1.2 specification defines CMIIdentifier as alphanumeric only: * letters (a-z, A-Z), numbers (0-9), hyphens (-), and underscores (_). * Spaces and periods are explicitly NOT allowed per spec. * * This implementation intentionally relaxes validation to accept all * printable ASCII characters (0x21-0x7E) plus whitespace to support * legacy content. Many real-world LMS systems and content packages use * identifiers that violate the strict spec (e.g., student IDs with spaces, * objective IDs with periods or special characters). * * Strict spec pattern would be: ^[A-Za-z0-9_-]{0,255}$ * * Affected elements: * - cmi.core.student_id * - cmi.objectives.n.id * - cmi.interactions.n.id * - cmi.interactions.n.objectives.n.id */ CMIIdentifier: "^[\\u0021-\\u007E\\s]{0,255}$", /** CMICredit - Vocabulary: credit or no-credit (RTE 3.4.2.1.3) */ CMICredit: "^(credit|no-credit)$", /** CMIEntry - Vocabulary: ab-initio, resume, or empty (RTE 3.4.2.1.4) */ CMIEntry: "^(ab-initio|resume|)$", /** CMILessonMode - Vocabulary: normal, browse, or review (RTE 3.4.2.1.10) */ CMILessonMode: "^(normal|browse|review)$", /** CMITimeLimitAction - Vocabulary: action combinations (RTE 3.4.2.1.11) */ CMITimeLimitAction: "^(exit,message|exit,no message|continue,message|continue,no message)$", /** * CMIFeedback - Relaxed for compatibility (normally CMIString255) * * SPEC COMPLIANCE NOTE: * The SCORM 1.2 specification defines CMIFeedback as CMIString255 (max 255 chars) * with format varying by interaction type (see RTE 3.4.2.7.5, 3.4.2.7.7). * * This implementation intentionally relaxes validation for two reasons: * * 1. LENGTH: Many legacy content packages store responses exceeding 255 chars, * especially for fill-in and performance interaction types. Strict enforcement * would break existing content with no user-facing benefit. * * 2. FORMAT: The spec requires type-specific formats (e.g., true-false accepts * only "0"/"1"/"t"/"f", choice accepts comma-separated single chars). However: * - Format validation requires knowing interaction type at validation time * - Legacy content often uses non-standard formats * - The SCO is responsible for response evaluation, not the API * - Strict format validation provides minimal benefit vs. compatibility cost * * Affected elements: * - cmi.interactions.n.student_response * - cmi.interactions.n.correct_responses.n.pattern * * Strict spec pattern would be: ^[\s\S]{0,255}$ with type-specific subpatterns */ CMIFeedback: "^.*$", /** CMIIndex - Pattern for array index extraction */ CMIIndex: "[._](\\d+).", /** CMIStatus - Lesson status vocabulary (RTE 3.4.2.2.3) */ CMIStatus: "^(passed|completed|failed|incomplete|browsed)$", /** CMIStatus2 - Extended status vocabulary with "not attempted" (RTE 3.4.2.6.2) */ CMIStatus2: "^(passed|completed|failed|incomplete|browsed|not attempted)$", /** CMIExit - Exit vocabulary (RTE 3.4.2.1.5) */ CMIExit: "^(time-out|suspend|logout|)$", /** CMIType - Interaction type vocabulary (RTE 3.4.2.7.2) */ CMIType: "^(true-false|choice|fill-in|matching|performance|sequencing|likert|numeric)$", /** CMIResult - Interaction result vocabulary (RTE 3.4.2.7.6) */ CMIResult: "^(correct|wrong|unanticipated|neutral|([0-9]{0,3})?(\\.[0-9]*)?)$", /** NAVEvent - Navigation event vocabulary (SCORM 1.2 extension) */ NAVEvent: "^(_?(previous|continue|start|resumeAll|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|choice|jump|_none_)$", /** score_range - Valid score range 0-100 (RTE 3.4.2.2.2) */ score_range: "0#100", /** audio_range - Audio level range -1 to 100 (RTE 3.4.2.3.1) */ audio_range: "-1#100", /** speed_range - Playback speed range -100 to 100 (RTE 3.4.2.3.2) */ speed_range: "-100#100", /** weighting_range - Interaction weighting range -100 to 100 (RTE 3.4.2.7.4) */ weighting_range: "-100#100", /** text_range - Text display preference -1 to 1 (RTE 3.4.2.3.3) */ text_range: "-1#1" }; const scorm2004_regex = { /** CMIString200 - Character string, max 200 chars (RTE C.1.1) */ CMIString200: "^[\\u0000-\\uFFFF]{0,200}$", /** CMIString250 - Character string, max 250 chars (RTE C.1.1) */ CMIString250: "^[\\u0000-\\uFFFF]{0,250}$", /** CMIString1000 - Character string, max 1000 chars (RTE C.1.1) */ CMIString1000: "^[\\u0000-\\uFFFF]{0,1000}$", /** CMIString4000 - Character string, max 4000 chars (RTE C.1.1) */ CMIString4000: "^[\\u0000-\\uFFFF]{0,4000}$", /** CMIString64000 - Character string, max 64000 chars (RTE C.1.1) */ CMIString64000: "^[\\u0000-\\uFFFF]{0,64000}$", /** * CMILang - Language code per RFC 1766/RFC 3066 (RTE C.1.2) * Primary tag: 1-8 characters (ISO 639-1: 2, ISO 639-2: 3, or i/x for IANA/private) * Subtag: 2-8 alphanumeric characters */ CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$", /** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */ CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$", /** CMILangcr - Language tag pattern with content */ CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$", /** CMILangString250cr - String with optional language tag (carriage return variant) */ CMILangString250cr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?})?(.{0,250})?)?$", /** CMILangString4000 - String with optional language tag, max 4000 chars (RTE C.1.3) */ CMILangString4000: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,4000}$)?$", /** * CMITime - ISO 8601 timestamp format (RTE C.1.4) * Year range expanded from 1970-2038 to 1970-9999 to support future dates */ CMITime: "^(19[7-9][0-9]|[2-9][0-9]{3})((-(0[1-9]|1[0-2]))((-(0[1-9]|[1-2][0-9]|3[0-1]))(T([0-1][0-9]|2[0-3])((:[0-5][0-9])((:[0-5][0-9])((\\.[0-9]{1,6})((Z|([+|-]([0-1][0-9]|2[0-3])))(:[0-5][0-9])?)?)?)?)?)?)?)?$", /** CMITimespan - ISO 8601 duration format (RTE C.1.5) */ CMITimespan: "^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:(\\d+(?:\\.\\d{1,2})?)S)?)?$", /** CMIInteger - Non-negative integer (RTE C.1.6) */ CMIInteger: "^\\d+$", /** CMISInteger - Signed integer (RTE C.1.7) */ CMISInteger: "^-?([0-9]+)$", /** * CMIDecimal - Signed decimal (RTE C.1.8) * Spec allows unlimited digits, but we set practical limits to prevent abuse * while maintaining broad compatibility: * - Up to 10 digits before decimal (supports values up to 10 billion) * - Up to 18 digits after decimal (maintains precision for scientific use) */ CMIDecimal: "^-?([0-9]{1,10})(\\.[0-9]{1,18})?$", /** * CMIIdentifier - Identifier with alphanumeric ending, max 250 chars (RTE C.1.9) * Must contain at least one word character (\w) and only allow: letters, * numbers, - ( ) + . : = @ ; $ _ ! * ' % / # * URN format is validated separately if string starts with "urn:" */ CMIIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$", /** CMIShortIdentifier - Short identifier conforming to URI syntax, max 250 chars (RTE C.1.10) */ CMIShortIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$", /** CMILongIdentifier - Long identifier supporting URN format, max 4000 chars (RTE C.1.11) */ CMILongIdentifier: "^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000}|.{1,4000})$", /** CMIFeedback - Unrestricted feedback text (RTE C.1.12) */ CMIFeedback: "^.*$", /** CMIIndex - Pattern for array index extraction */ CMIIndex: "[._](\\d+).", /** CMIIndexStore - Pattern for stored index notation */ CMIIndexStore: ".N(\\d+).", /** CMICStatus - Completion status vocabulary (RTE 4.1.4) */ CMICStatus: "^(completed|incomplete|not attempted|unknown)$", /** CMISStatus - Success status vocabulary (RTE 4.1.11) */ CMISStatus: "^(passed|failed|unknown)$", /** CMIExit - Exit vocabulary (RTE 4.1.3) */ CMIExit: "^(time-out|suspend|logout|normal)$", /** CMIType - Interaction type vocabulary (RTE 4.1.6.2) */ CMIType: "^(true-false|choice|fill-in|long-fill-in|matching|performance|sequencing|likert|numeric|other)$", /** CMIResult - Interaction result vocabulary (RTE 4.1.6.8) */ CMIResult: "^(correct|incorrect|unanticipated|neutral|-?([0-9]{1,4})(\\.[0-9]{1,18})?)$", /** NAVEvent - Navigation event vocabulary (SN Book Table 4.4.2) */ NAVEvent: "^(_?(start|resumeAll|previous|continue|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|_none_|(\\{target=(?<choice_target>\\S{0,}[a-zA-Z0-9-_]+)})?choice|(\\{target=(?<jump_target>\\S{0,}[a-zA-Z0-9-_]+)})?jump)$", /** NAVBoolean - Navigation boolean vocabulary (SN Book) */ NAVBoolean: "^(unknown|true|false)$", /** NAVTarget - Navigation target pattern (SN Book) */ NAVTarget: "^{target=\\S{0,}[a-zA-Z0-9-_]+}$", /** scaled_range - Scaled score range -1 to 1 (RTE 4.1.10.1) */ scaled_range: "-1#1", /** audio_range - Audio level range 0 to 999.9999999 (RTE 4.1.7.1) */ audio_range: "0#999.9999999", /** speed_range - Playback speed range 0 to 999.9999999 (RTE 4.1.7.4) */ speed_range: "0#999.9999999", /** text_range - Text display preference -1 to 1 (RTE 4.1.7.5) */ text_range: "-1#1", /** progress_range - Progress measure range 0 to 1 (RTE 4.1.8) */ progress_range: "0#1" }; var __defProp$j = Object.defineProperty; var __defNormalProp$j = (obj, key, value) => key in obj ? __defProp$j(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$j = (obj, key, value) => __defNormalProp$j(obj, typeof key !== "symbol" ? key + "" : key, value); class ScheduledCommit { /** * Constructor for ScheduledCommit * @param {BaseAPI} API * @param {number} when * @param {string} callback */ constructor(API, when, callback) { __publicField$j(this, "_API"); __publicField$j(this, "_cancelled", false); __publicField$j(this, "_timeout"); __publicField$j(this, "_callback"); this._API = API; this._timeout = setTimeout(this.wrapper.bind(this), when); this._callback = callback; } /** * Cancel any currently scheduled commit */ cancel() { this._cancelled = true; if (this._timeout) { clearTimeout(this._timeout); } } /** * Wrap the API commit call to check if the call has already been canceled */ wrapper() { if (!this._cancelled) { if (this._API.isInitialized()) { (async () => await this._API.commit(this._callback))(); } } } } const HIDE_LMS_UI_TOKENS = ["continue", "previous", "exit", "exitAll", "abandon", "abandonAll", "suspendAll"]; var __defProp$i = Object.defineProperty; var __defNormalProp$i = (obj, key, value) => key in obj ? __defProp$i(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$i = (obj, key, value) => __defNormalProp$i(obj, typeof key !== "symbol" ? key + "" : key, value); const _RuleCondition = class _RuleCondition extends BaseCMI { /** * Constructor for RuleCondition * @param {RuleConditionType} condition - The condition type * @param {RuleConditionOperator | null} operator - The operator (null for no operator) * @param {Map<string, any>} parameters - Additional parameters for the condition */ constructor() { let condition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "always"; let operator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; let parameters = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : /* @__PURE__ */new Map(); super("ruleCondition"); __publicField$i(this, "_condition", "always" /* ALWAYS */); __publicField$i(this, "_operator", null); __publicField$i(this, "_parameters", /* @__PURE__ */new Map()); __publicField$i(this, "_referencedObjective", null); this._condition = condition; this._operator = operator; this._parameters = parameters; } /** * Allow integrators to override the clock used for time-based rules. */ static setNowProvider(now) { if (typeof now === "function") { _RuleCondition._now = now; } } /** * Allow integrators to set an elapsed seconds hook for time limit calculations */ static setElapsedSecondsHook(hook) { _RuleCondition._getElapsedSecondsHook = hook; } /** * Called when the API needs to be reset */ reset() { this._initialized = false; this._condition = "always" /* ALWAYS */; this._operator = null; this._parameters = /* @__PURE__ */new Map(); } /** * Getter for condition * @return {RuleConditionType} */ get condition() { return this._condition; } /** * Setter for condition * @param {RuleConditionType} condition */ set condition(condition) { this._condition = condition; } /** * Getter for operator * @return {RuleConditionOperator | null} */ get operator() { return this._operator; } /** * Setter for operator * @param {RuleConditionOperator | null} operator */ set operator(operator) { this._operator = operator; } /** * Getter for parameters * @return {Map<string, any>} */ get parameters() { return this._parameters; } /** * Setter for parameters * @param {Map<string, any>} parameters */ set parameters(parameters) { this._parameters = parameters; } get referencedObjective() { return this._referencedObjective; } set referencedObjective(objectiveId) { this._referencedObjective = objectiveId; } resolveReferencedObjective(activity) { if (!this._referencedObjective) { return null; } if (activity.primaryObjective?.id === this._referencedObjective) { return activity.primaryObjective; } const objectives = activity.objectives || []; return objectives.find(obj => obj.id === this._referencedObjective) || null; } /** * Evaluate the condition for an activity * @param {Activity} activity - The activity to evaluate the condition for * @return {boolean} - True if the condition is met, false otherwise */ evaluate(activity) { let result; const referencedObjective = this.resolveReferencedObjective(activity); switch (this._condition) { case "satisfied" /* SATISFIED */: case "objectiveSatisfied" /* OBJECTIVE_SATISFIED */: if (referencedObjective) { result = referencedObjective.satisfiedStatus === true; } else { result = activity.successStatus === SuccessStatus.PASSED || activity.objectiveSatisfiedStatus === true; } break; case "objectiveStatusKnown" /* OBJECTIVE_STATUS_KNOWN */: result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus; break; case "objectiveMeasureKnown" /* OBJECTIVE_MEASURE_KNOWN */: result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus; break; case "objectiveMeasureGreaterThan" /* OBJECTIVE_MEASURE_GREATER_THAN */: { const greaterThanValue = this._parameters.get("threshold") || 0; const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus; const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure; result = !!measureStatus && measureValue > greaterThanValue; break; } case "objectiveMeasureLessThan" /* OBJECTIVE_MEASURE_LESS_THAN */: { const lessThanValue = this._parameters.get("threshold") || 0; const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus; const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure; result = !!measureStatus && measureValue < lessThanValue; break; } case "completed" /* COMPLETED */: case "activityCompleted" /* ACTIVITY_COMPLETED */: if (referencedObjective) { result = referencedObjective.completionStatus === CompletionStatus.COMPLETED; } else { result = activity.isCompleted; } break; case "progressKnown" /* PROGRESS_KNOWN */: case "activityProgressKnown" /* ACTIVITY_PROGRESS_KNOWN */: if (referencedObjective) { result = referencedObjective.completionStatus !== CompletionStatus.UNKNOWN; } else { result = activity.completionStatus !== "unknown"; } break; case "attempted" /* ATTEMPTED */: result = activity.attemptCount > 0; break; case "attemptLimitExceeded" /* ATTEMPT_LIMIT_EXCEEDED */: result = activity.hasAttemptLimitExceeded(); break; case "timeLimitExceeded" /* TIME_LIMIT_EXCEEDED */: result = this.evaluateTimeLimitExceeded(activity); break; case "outsideAvailableTimeRange" /* OUTSIDE_AVAILABLE_TIME_RANGE */: result = this.evaluateOutsideAvailableTimeRange(activity); break; case "always" /* ALWAYS */: result = true; break; case "never" /* NEVER */: result = false; break; default: result = false; break; } if (this._operator === "not" /* NOT */) { result = !result; } return result; } /** * Evaluate if time limit has been exceeded * @param {Activity} activity - The activity to evaluate * @return {boolean} * @private */ evaluateTimeLimitExceeded(activity) { let limit = activity.timeLimitDuration; if (!limit && activity.attemptAbsoluteDurationLimit) { limit = activity.attemptAbsoluteDurationLimit; } if (!limit) { return false; } const limitSeconds = getDurationAsSeconds(limit, scorm2004_regex.CMITimespan); if (limitSeconds <= 0) { return false; } let elapsedSeconds = 0; if (_RuleCondition._getElapsedSecondsHook) { try { const hookResult = _RuleCondition._getElapsedSecondsHook(activity); if (typeof hookResult === "number" && !Number.isNaN(hookResult) && hookResult >= 0) { elapsedSeconds = hookResult; } } catch { elapsedSeconds = 0; } } if (elapsedSeconds === 0 && activity.attemptExperiencedDuration) { const attemptDurationSeconds = getDurationAsSeconds(activity.attemptExperiencedDuration, scorm2004_regex.CMITimespan); if (attemptDurationSeconds > 0) { elapsedSeconds = attemptDurationSeconds; } } if (elapsedSeconds === 0 && activity.attemptAbsoluteStartTime) { try { const start = new Date(activity.attemptAbsoluteStartTime).getTime(); const nowMs = _RuleCondition._now().getTime(); if (!Number.isNaN(start) && !Number.isNaN(nowMs) && nowMs >= start) { elapsedSeconds = (nowMs - start) / 1e3; } } catch { elapsedSeconds = 0; } } return elapsedSeconds > limitSeconds; } /** * Evaluate if activity is outside available time range * @param {Activity} activity - The activity to evaluate * @return {boolean} * @private */ evaluateOutsideAvailableTimeRange(activity) { const beginTime = activity.beginTimeLimit; const endTime = activity.endTimeLimit; if (!beginTime && !endTime) { return false; } const now = _RuleCondition._now(); if (beginTime) { const beginDate = new Date(beginTime); if (now < beginDate) { return true; } } if (endTime) { const endDate = new Date(endTime); if (now > endDate) { return true; } } return false; } /** * Parse ISO 8601 duration to milliseconds * Uses the standard getDurationAsSeconds utility which supports full ISO 8601 format * including date components (years, months, weeks, days) and time components (hours, minutes, seconds). * @param {string} duration - ISO 8601 duration string (e.g., "PT1H30M", "P1D", "P1Y2M3DT4H5M6S") * @return {number} - Duration in milliseconds * @private */ parseISO8601Duration(duration) { const seconds = getDurationAsSeconds(duration, scorm2004_regex.CMITimespan); return seconds * 1e3; } /** * toJSON for RuleCondition * @return {object} */ toJSON() { this.jsonString = true; const result = { condition: this._condition, operator: this._operator, parameters: Object.fromEntries(this._parameters) }; this.jsonString = false; return result; } }; // Optional, overridable provider for current time (LMS may set via SequencingService) __publicField$i(_RuleCondition, "_now", () => /* @__PURE__ */new Date()); // Optional, overridable hook for getting elapsed seconds __publicField$i(_RuleCondition, "_getElapsedSecondsHook"); var SelectionTiming = /* @__PURE__ */(SelectionTiming2 => { SelectionTiming2["NEVER"] = "never"; SelectionTiming2["ONCE"] = "once"; SelectionTiming2["ON_EACH_NEW_ATTEMPT"] = "onEachNewAttempt"; return SelectionTiming2; })(SelectionTiming || {}); var RandomizationTiming = /* @__PURE__ */(RandomizationTiming2 => { RandomizationTiming2["NEVER"] = "never"; RandomizationTiming2["ONCE"] = "once"; RandomizationTiming2["ON_EACH_NEW_ATTEMPT"] = "onEachNewAttempt"; return RandomizationTiming2; })(RandomizationTiming || {}); class SelectionRandomization { /** * Select Children Process (SR.1) * Selects a subset of child activities based on selection controls * @param {Activity} activity - The parent activity whose children need to be selected * @return {Activity[]} - The selected child activities */ static selectChildrenProcess(activity) { const controls = activity.sequencingControls; const children = [...activity.children]; if (controls.selectionTiming === SelectionTiming.NEVER) { return children; } if (controls.selectionTiming === SelectionTiming.ONCE && controls.selectionCountStatus) { return children; } if (controls.selectionTiming !== SelectionTiming.ONCE && !controls.selectionCountStatus) { return children; } const selectCount = controls.selectCount; if (selectCount === null || selectCount >= children.length) { if (controls.selectionTiming === SelectionTiming.ONCE) { controls.selectionCountStatus = true; } return children; } const selectedChildren = []; const availableIndices = children.map((_, index) => index); for (let i = 0; i < selectCount; i++) { if (availableIndices.length === 0) break; const randomIndex = Math.floor(Math.random() * availableIndices.length); const childIndex = availableIndices[randomIndex]; if (childIndex !== void 0 && children[childIndex]) { selectedChildren.push(children[childIndex]); } availableIndices.splice(randomIndex, 1); } if (controls.selectionTiming === SelectionTiming.ONCE) { controls.selectionCountStatus = true; } for (const child of children) { if (!selectedChildren.includes(child)) { child.isHiddenFromChoice = true; child.isAvailable = false; } } return selectedChildren; } /** * Randomize Children Process (SR.2) * Randomizes the order of child activities based on randomization controls * @param {Activity} activity - The parent activity whose children need to be randomized *