UNPKG

scorm-again

Version:

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

1,316 lines (1,302 loc) 919 kB
(function () { 'use strict'; const SECONDS_PER_SECOND = 1; const SECONDS_PER_MINUTE = 60; const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE; const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR; const designations = { D: SECONDS_PER_DAY, H: SECONDS_PER_HOUR, M: SECONDS_PER_MINUTE, S: SECONDS_PER_SECOND }; 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 getSecondsAsISODuration = seconds => { if (!seconds || seconds <= 0) { return "PT0S"; } let duration = "P"; let remainder = seconds; const designationEntries = Object.entries(designations); designationEntries.forEach(_ref => { let [designationsKey, current_seconds] = _ref; let value = Math.floor(remainder / current_seconds); remainder = remainder % current_seconds; if (countDecimals(remainder) > 2) { remainder = Number(Number(remainder).toFixed(2)); } if (designationsKey === "S" && remainder > 0) { value += remainder; } if (value) { const needsTimeSeparator = (duration.indexOf("D") > 0 || ["H", "M", "S"].includes(designationsKey)) && duration.indexOf("T") === -1; if (needsTimeSeparator) { duration += "T"; } duration += `${value}${designationsKey}`; } }); return duration; }; 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}`; }); const validateISO8601Duration = memoize((duration, durationRegex) => { if (typeof durationRegex === "string") { durationRegex = new RegExp(durationRegex); } return !(!duration || !duration?.match?.(durationRegex)); }); function addTwoDurations(first, second, durationRegex) { const regex = new RegExp(durationRegex) ; return getSecondsAsISODuration(getDurationAsSeconds(first, regex) + getDurationAsSeconds(second, regex)); } 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; })(); }; } function parseNavigationRequest(navRequest) { const validCommands = /* @__PURE__ */new Set(["start", "resumeAll", "continue", "previous", "choice", "jump", "exit", "exitAll", "abandon", "abandonAll", "suspendAll", "_none_"]); const trimmed = navRequest.trim(); if (!trimmed) { return { command: "_none_", targetActivityId: null, valid: false, error: "Empty navigation request" }; } if (validCommands.has(trimmed)) { return { command: trimmed, targetActivityId: null, valid: true }; } const dotIndex = trimmed.indexOf("."); if (dotIndex > 0) { const command = trimmed.substring(0, dotIndex); const targetActivityId = trimmed.substring(dotIndex + 1); if ((command === "choice" || command === "jump") && targetActivityId) { if (/^[a-zA-Z0-9._-]+$/.test(targetActivityId)) { return { command, targetActivityId, valid: true }; } else { return { command: "_none_", targetActivityId: null, valid: false, error: `Invalid target activity ID: contains disallowed characters` }; } } } return { command: "_none_", targetActivityId: null, valid: false, error: `Unrecognized navigation command: "${trimmed}"` }; } var __defProp$1d = Object.defineProperty; var __defNormalProp$1d = (obj, key, value) => key in obj ? __defProp$1d(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1d = (obj, key, value) => __defNormalProp$1d(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$1d(this, "jsonString", false); __publicField$1d(this, "_cmi_element"); __publicField$1d(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$1d(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$1c = Object.defineProperty; var __defNormalProp$1c = (obj, key, value) => key in obj ? __defProp$1c(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1c = (obj, key, value) => __defNormalProp$1c(obj, typeof key !== "symbol" ? key + "" : key, value); class BaseScormValidationError extends Error { constructor(CMIElement, errorCode) { super(`${CMIElement} : ${errorCode.toString()}`); __publicField$1c(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$1c(this, "_errorMessage"); __publicField$1c(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 scorm2004_constants = { // Children lists cmi_children: "_version,comments_from_learner,comments_from_lms,completion_status,completion_threshold,credit,entry,exit,interactions,launch_data,learner_id,learner_name,learner_preference,location,max_time_allowed,mode,objectives,progress_measure,scaled_passing_score,score,session_time,success_status,suspend_data,time_limit_action,total_time", comments_children: "comment,timestamp,location", score_children: "max,raw,scaled,min", objectives_children: "progress_measure,completion_status,success_status,description,score,id", correct_responses_children: "pattern", student_preference_children: "audio_level,audio_captioning,delivery_speed,language", interactions_children: "id,type,objectives,timestamp,correct_responses,weighting,learner_response,result,latency,description", adl_data_children: "id,store", 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." }, "102": { basicMessage: "General Initialization Failure", detailMessage: "Call to Initialize failed for an unknown reason." }, "103": { basicMessage: "Already Initialized", detailMessage: "Call to Initialize failed because Initialize was already called." }, "104": { basicMessage: "Content Instance Terminated", detailMessage: "Call to Initialize failed because Terminate was already called." }, "111": { basicMessage: "General Termination Failure", detailMessage: "Call to Terminate failed for an unknown reason." }, "112": { basicMessage: "Termination Before Initialization", detailMessage: "Call to Terminate failed because it was made before the call to Initialize." }, "113": { basicMessage: "Termination After Termination", detailMessage: "Call to Terminate failed because Terminate was already called." }, "122": { basicMessage: "Retrieve Data Before Initialization", detailMessage: "Call to GetValue failed because it was made before the call to Initialize." }, "123": { basicMessage: "Retrieve Data After Termination", detailMessage: "Call to GetValue failed because it was made after the call to Terminate." }, "132": { basicMessage: "Store Data Before Initialization", detailMessage: "Call to SetValue failed because it was made before the call to Initialize." }, "133": { basicMessage: "Store Data After Termination", detailMessage: "Call to SetValue failed because it was made after the call to Terminate." }, "142": { basicMessage: "Commit Before Initialization", detailMessage: "Call to Commit failed because it was made before the call to Initialize." }, "143": { basicMessage: "Commit After Termination", detailMessage: "Call to Commit failed because it was made after the call to Terminate." }, "201": { basicMessage: "General Argument Error", detailMessage: "An invalid argument was passed to an API method (usually indicates that Initialize, Commit or Terminate did not receive the expected empty string argument." }, "301": { basicMessage: "General Get Failure", detailMessage: "Indicates a failed GetValue call where no other specific error code is applicable. Use GetDiagnostic for more information." }, "351": { basicMessage: "General Set Failure", detailMessage: "Indicates a failed SetValue call where no other specific error code is applicable. Use GetDiagnostic for more information." }, "391": { basicMessage: "General Commit Failure", detailMessage: "Indicates a failed Commit call where no other specific error code is applicable. Use GetDiagnostic for more information." }, "401": { basicMessage: "Undefined Data Model Element", detailMessage: "The data model element name passed to GetValue or SetValue is not a valid SCORM data model element." }, "402": { basicMessage: "Unimplemented Data Model Element", detailMessage: "The data model element indicated in a call to GetValue or SetValue is valid, but was not implemented by this LMS. In SCORM 2004, this error would indicate an LMS that is not fully SCORM conformant." }, "403": { basicMessage: "Data Model Element Value Not Initialized", detailMessage: "Attempt to read a data model element that has not been initialized by the LMS or through a SetValue call. This error condition is often reached during normal execution of a SCO." }, "404": { basicMessage: "Data Model Element Is Read Only", detailMessage: "SetValue was called with a data model element that can only be read." }, "405": { basicMessage: "Data Model Element Is Write Only", detailMessage: "GetValue was called on a data model element that can only be written to." }, "406": { basicMessage: "Data Model Element Type Mismatch", detailMessage: "SetValue was called with a value that is not consistent with the data format of the supplied data model element." }, "407": { basicMessage: "Data Model Element Value Out Of Range", detailMessage: "The numeric value supplied to a SetValue 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 scorm2004_errors$1 = scorm2004_constants.error_descriptions; class Scorm2004ValidationError extends ValidationError { /** * Constructor to take in an error code * @param {string} CMIElement * @param {number} errorCode */ constructor(CMIElement, errorCode) { if ({}.hasOwnProperty.call(scorm2004_errors$1, String(errorCode))) { super(CMIElement, errorCode, scorm2004_errors$1[String(errorCode)]?.basicMessage || "Unknown error", scorm2004_errors$1[String(errorCode)]?.detailMessage); } else { super(CMIElement, 101, scorm2004_errors$1["101"]?.basicMessage, scorm2004_errors$1["101"]?.detailMessage); } Object.setPrototypeOf(this, Scorm2004ValidationError.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 }; const scorm2004_errors = { ...global_errors, INITIALIZATION_FAILED: 102, INITIALIZED: 103, TERMINATED: 104, TERMINATION_FAILURE: 111, TERMINATION_BEFORE_INIT: 112, MULTIPLE_TERMINATION: 113, MULTIPLE_TERMINATIONS: 113, RETRIEVE_BEFORE_INIT: 122, RETRIEVE_AFTER_TERM: 123, STORE_BEFORE_INIT: 132, STORE_AFTER_TERM: 133, COMMIT_BEFORE_INIT: 142, COMMIT_AFTER_TERM: 143, ARGUMENT_ERROR: 201, GENERAL_GET_FAILURE: 301, GENERAL_SET_FAILURE: 351, GENERAL_COMMIT_FAILURE: 391, UNDEFINED_DATA_MODEL: 401, UNIMPLEMENTED_ELEMENT: 402, VALUE_NOT_INITIALIZED: 403, READ_ONLY_ELEMENT: 404, WRITE_ONLY_ELEMENT: 405, TYPE_MISMATCH: 406, VALUE_OUT_OF_RANGE: 407, DEPENDENCY_NOT_ESTABLISHED: 408 }; var __defProp$1b = Object.defineProperty; var __defNormalProp$1b = (obj, key, value) => key in obj ? __defProp$1b(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1b = (obj, key, value) => __defNormalProp$1b(obj, typeof key !== "symbol" ? key + "" : key, value); class CMIArray extends BaseCMI { /** * Constructor cmi *.n arrays * @param {object} params */ constructor(params) { super(params.CMIElement); __publicField$1b(this, "_errorCode"); __publicField$1b(this, "_errorClass"); __publicField$1b(this, "__children"); __publicField$1b(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 NAVBoolean = { UNKNOWN: "unknown", TRUE: "true", FALSE: "false" }; 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" }; const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier; const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}"; const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?"; const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$"; const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$"; const LearnerResponses = { "true-false": { format: "^true$|^false$", max: 1, delimiter: "", unique: false }, choice: { format: scorm2004_regex.CMILongIdentifier, max: 36, delimiter: "[,]", unique: true }, "fill-in": { format: scorm2004_regex.CMILangString250, max: 10, delimiter: "[,]", unique: false }, "long-fill-in": { format: scorm2004_regex.CMILangString4000, max: 1, delimiter: "", unique: false }, matching: { format: scorm2004_regex.CMIShortIdentifier, format2: scorm2004_regex.CMIShortIdentifier, max: 36, delimiter: "[,]", delimiter2: "[.]", unique: false }, performance: { format: PERFORMANCE_STEP_NAME, format2: LR_PERFORMANCE_STEP_ANSWER, max: 250, delimiter: "[,]", delimiter2: "[.]", unique: false }, sequencing: { format: scorm2004_regex.CMIShortIdentifier, max: 36, delimiter: "[,]", unique: false }, likert: { format: scorm2004_regex.CMIShortIdentifier, max: 1, delimiter: "", unique: false }, numeric: { format: scorm2004_regex.CMIDecimal, max: 1, delimiter: "", unique: false }, other: { format: scorm2004_regex.CMIString4000, max: 1, delimiter: "", unique: false } }; const CorrectResponses = { "true-false": { max: 1, delimiter: "", unique: false, duplicate: false, format: "^true$|^false$", limit: 1 }, choice: { max: 36, delimiter: "[,]", unique: true, duplicate: false, format: scorm2004_regex.CMILongIdentifier }, "fill-in": { max: 10, delimiter: "[,]", unique: false, duplicate: false, format: scorm2004_regex.CMILangString250cr }, "long-fill-in": { max: 1, delimiter: "", unique: false, duplicate: true, format: scorm2004_regex.CMILangString4000 }, matching: { max: 36, delimiter: "[,]", delimiter2: "[.]", unique: false, duplicate: false, format: scorm2004_regex.CMIShortIdentifier, format2: scorm2004_regex.CMIShortIdentifier }, performance: { max: 250, delimiter: "[,]", delimiter2: "[.]", unique: false, duplicate: false, // step_name: optional short_identifier_type format: PERFORMANCE_STEP_NAME, // step_answer: optional characterstring (spaces allowed) or numeric range format2: CR_PERFORMANCE_STEP_ANSWER }, sequencing: { max: 36, delimiter: "[,]", unique: false, duplicate: false, format: scorm2004_regex.CMIShortIdentifier }, likert: { max: 1, delimiter: "", unique: false, duplicate: false, format: scorm2004_regex.CMIShortIdentifier, limit: 1 }, numeric: { max: 2, delimiter: "[:]", unique: false, duplicate: false, format: scorm2004_regex.CMIDecimal, limit: 1 }, other: { max: 1, delimiter: "", unique: false, duplicate: false, format: scorm2004_regex.CMIString4000, limit: 1 } }; const ValidLanguages = ["aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", "ay", "az", "ba", "be", "bg", "bh", "bi", "bm", "bn", "bo", "br", "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", "ee", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", "kk", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la