Initial push, working on test cases

This commit is contained in:
Jonathan Putney
2019-11-10 12:29:43 -05:00
parent 377d9f977a
commit da4af1edba
18 changed files with 9571 additions and 0 deletions

10
.babelrc Normal file
View File

@@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
]
}

3
.gitignore vendored
View File

@@ -59,3 +59,6 @@ typings/
# next.js build output # next.js build output
.next .next
# JetBrains Project Directory
.idea/

5508
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "scorm-again",
"version": "1.0.0",
"description": "A modern SCORM JavaScript runtime library for AICC, Scorm 1.2, and Scorm 2004",
"main": "index.js",
"directories": {
"test": "test"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.2",
"@babel/node": "^7.7.0",
"@babel/plugin-proposal-class-properties": "^7.7.0",
"@babel/plugin-proposal-private-methods": "^7.6.0",
"@babel/preset-env": "^7.7.1",
"@babel/preset-flow": "^7.0.0",
"@babel/register": "^7.7.0",
"chai": "^4.2.0",
"mocha": "^6.2.2",
"nodemon": "^1.19.4",
"nyc": "^14.1.1"
},
"scripts": {
"test": "./node_modules/.bin/mocha --compilers js:@babel/register"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/jcputney/scorm-again.git"
},
"keywords": [
"scorm",
"aicc",
"tincan",
"cmi"
],
"author": "Jonathan Putney <jonathan@putney.io>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/jcputney/scorm-again/issues"
},
"homepage": "https://github.com/jcputney/scorm-again#readme"
}

11
src/.flowconfig Normal file
View File

@@ -0,0 +1,11 @@
[ignore]
[include]
[libs]
[lints]
[options]
[strict]

57
src/AICC.js Normal file
View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
import Scorm12API from './Scorm12API';
import {
CMIInteractionsCorrectResponsesObject,
CMIInteractionsObject,
CMIInteractionsObjectivesObject,
CMIObjectivesObject
} from "./cmi/scorm12_cmi";
import {CMIEvaluationCommentsObject, CMITriesObject, NAV} from "./cmi/aicc_cmi";
class AICC extends Scorm12API {
constructor() {
super();
this.nav = new NAV(this);
}
/**
* Gets or builds a new child element to add to the array.
*
* @param CMIElement
* @param value
*/
getChildElement(CMIElement, value) {
let newChild;
if (this.stringContains(CMIElement, "cmi.objectives")) {
newChild = new CMIObjectivesObject(this);
} else if (this.stringContains(CMIElement, ".correct_responses")) {
newChild = new CMIInteractionsCorrectResponsesObject(this);
} else if (this.stringContains(CMIElement, ".objectives")) {
newChild = new CMIInteractionsObjectivesObject(this);
} else if (this.stringContains(CMIElement, "cmi.interactions")) {
newChild = new CMIInteractionsObject(this);
} else if (this.stringContains(CMIElement, "cmi.evaluation.comments")) {
newChild = new CMIEvaluationCommentsObject(this);
} else if (this.stringContains(CMIElement, "cmi.student_data.tries")) {
newChild = new CMITriesObject(this);
}
return newChild;
}
/**
* Replace the whole API with another
*/
replaceWithAnotherScormAPI(newAPI) {
// Data Model
this.cmi = newAPI.cmi;
this.nav = newAPI.nav;
}
}

665
src/BaseAPI.js Normal file
View File

@@ -0,0 +1,665 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
import {CMIArray} from "./cmi/common";
import {base_error_codes} from "./constants";
const api_constants = {
SCORM_TRUE: "true",
SCORM_FALSE: "false",
STATE_NOT_INITIALIZED: 0,
STATE_INITIALIZED: 1,
STATE_TERMINATED: 2,
LOG_LEVEL_DEBUG: 1,
LOG_LEVEL_INFO: 2,
LOG_LEVEL_WARNING: 3,
LOG_LEVEL_ERROR: 4,
LOG_LEVEL_NONE: 5
};
export default class BaseAPI {
#timeout;
#error_codes;
cmi;
constructor(error_codes) {
this.currentState = api_constants.STATE_NOT_INITIALIZED;
this.apiLogLevel = api_constants.LOG_LEVEL_ERROR;
this.lastErrorCode = 0;
this.listenerArray = [];
this.#timeout = null;
this.#error_codes = error_codes;
}
/**
* @returns {string} bool
*/
APIInitialize(callbackName: String, initializeMessage?: String, terminationMessage?: String) {
let returnValue = api_constants.SCORM_FALSE;
if (this.isInitialized()) {
this.throwSCORMError(this.#error_codes.INITIALIZED, initializeMessage);
} else if (this.isTerminated()) {
this.throwSCORMError(this.#error_codes.TERMINATED, terminationMessage);
} else {
this.currentState = api_constants.STATE_INITIALIZED;
this.lastErrorCode = 0;
returnValue = api_constants.SCORM_TRUE;
this.processListeners(callbackName);
}
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue);
return returnValue;
}
/**
* @returns {string} bool
*/
APITerminate(callbackName: String, checkTerminated: boolean) {
let returnValue = api_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, this.#error_codes.TERMINATION_BEFORE_INIT, this.#error_codes.MULTIPLE_TERMINATION)) {
if (checkTerminated) this.lastErrorCode = 0;
this.currentState = api_constants.STATE_TERMINATED;
returnValue = api_constants.SCORM_TRUE;
this.processListeners(callbackName);
}
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue);
return returnValue;
}
/**
* @param callbackName
* @param checkTerminated
* @param CMIElement
* @returns {string}
*/
APIGetValue(callbackName: String, checkTerminated: boolean, CMIElement) {
let returnValue = "";
if (this.checkState(checkTerminated, this.#error_codes.RETRIEVE_BEFORE_INIT, this.#error_codes.RETRIEVE_AFTER_TERM)) {
if (checkTerminated) this.lastErrorCode = 0;
returnValue = this.getCMIValue(CMIElement);
this.processListeners(callbackName, CMIElement);
}
this.apiLog(callbackName, CMIElement, ": returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue);
return returnValue;
}
/**
* @param callbackName
* @param checkTerminated
* @param CMIElement
* @param value
* @returns {string}
*/
APISetValue(callbackName: String, checkTerminated: boolean, CMIElement, value) {
let returnValue = "";
if (this.checkState(checkTerminated, this.#error_codes.STORE_BEFORE_INIT, this.#error_codes.STORE_AFTER_TERM)) {
if (checkTerminated) this.lastErrorCode = 0;
returnValue = this.setCMIValue(CMIElement, value);
this.processListeners(callbackName, CMIElement, value);
}
this.apiLog(callbackName, CMIElement, ": " + value + ": result: " + returnValue, api_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue);
return returnValue;
}
/**
* Orders LMS to store all content parameters
*
* @returns {string} bool
*/
APICommit(callbackName: String, checkTerminated: boolean) {
let returnValue = api_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, this.#error_codes.COMMIT_BEFORE_INIT, this.#error_codes.COMMIT_AFTER_TERM)) {
if (checkTerminated) this.lastErrorCode = 0;
returnValue = api_constants.SCORM_TRUE;
this.processListeners(callbackName);
}
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue);
return returnValue;
}
/**
* Returns last error code
*
* @returns {string}
*/
APIGetLastError(callbackName: String) {
let returnValue = String(this.lastErrorCode);
this.processListeners(callbackName);
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
return returnValue;
}
/**
* Returns the errorNumber error description
*
* @param callbackName
* @param CMIErrorCode
* @returns {string}
*/
APIGetErrorString(callbackName: String, CMIErrorCode) {
let returnValue = "";
if (CMIErrorCode !== null && CMIErrorCode !== "") {
returnValue = this.getLmsErrorMessageDetails(CMIErrorCode);
this.processListeners(callbackName);
}
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
return returnValue;
}
/**
* Returns a comprehensive description of the errorNumber error.
*
* @param callbackName
* @param CMIErrorCode
* @returns {string}
*/
APIGetDiagnostic(callbackName: String, CMIErrorCode) {
let returnValue = "";
if (CMIErrorCode !== null && CMIErrorCode !== "") {
returnValue = this.getLmsErrorMessageDetails(CMIErrorCode, true);
this.processListeners(callbackName);
}
this.apiLog(callbackName, null, "returned: " + returnValue, api_constants.LOG_LEVEL_INFO);
return returnValue;
}
/**
* Checks the LMS state and ensures it has been initialized
*/
checkState(checkTerminated: boolean, beforeInitError: number, afterTermError?: number) {
if (this.isNotInitialized()) {
this.throwSCORMError(beforeInitError);
return false;
} else if (checkTerminated && this.isTerminated()) {
this.throwSCORMError(afterTermError);
return false;
}
return true;
}
/**
* Logging for all SCORM actions
*
* @param functionName
* @param CMIElement
* @param logMessage
* @param messageLevel
*/
apiLog(functionName: String, CMIElement: String, logMessage: String, messageLevel: number) {
logMessage = this.formatMessage(functionName, CMIElement, logMessage);
if (messageLevel >= this.apiLogLevel) {
switch (messageLevel) {
case api_constants.LOG_LEVEL_ERROR:
console.error(logMessage);
break;
case api_constants.LOG_LEVEL_WARNING:
console.warn(logMessage);
break;
case api_constants.LOG_LEVEL_INFO:
console.info(logMessage);
break;
}
}
};
/**
* Clears the last SCORM error code on success
*/
clearSCORMError(success: String) {
if (success !== api_constants.SCORM_FALSE) {
this.lastErrorCode = 0;
}
};
/**
* Formats the SCORM messages for easy reading
*
* @param functionName
* @param CMIElement
* @param message
* @returns {string}
*/
formatMessage(functionName: String, CMIElement: String, message: String) {
let baseLength = 20;
let messageString = "";
messageString += functionName;
let fillChars = baseLength - messageString.length;
for (let i = 0; i < fillChars; i++) {
messageString += " ";
}
messageString += ": ";
if (CMIElement) {
let CMIElementBaseLength = 70;
messageString += CMIElement;
fillChars = CMIElementBaseLength - messageString.length;
for (let j = 0; j < fillChars; j++) {
messageString += " ";
}
}
if (message) {
messageString += message;
}
return messageString;
};
/**
* Checks to see if {str} contains {tester}
*
* @param str String to check against
* @param tester String to check for
*/
stringContains(str: String, tester: String) {
return str.indexOf(tester) > -1;
};
/**
* Returns the message that corresponds to errorNumber
* APIs that inherit BaseAPI should override this function
*/
getLmsErrorMessageDetails(_errorNumber, _detail) {
return "No error";
}
/**
* Gets the value for the specific element.
* APIs that inherit BaseAPI should override this function
*/
getCMIValue(_CMIElement) {
return "";
}
/**
* Sets the value for the specific element.
* APIs that inherit BaseAPI should override this function
*/
setCMIValue(_CMIElement, _value) {
return "";
}
_commonSetCMIValue(methodName: String, scorm2004: boolean, CMIElement, value) {
if (!CMIElement || CMIElement === "") {
return api_constants.SCORM_FALSE;
}
let structure = CMIElement.split(".");
let refObject = this;
let returnValue = api_constants.SCORM_FALSE;
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
const invalidErrorCode = scorm2004 ? this.#error_codes.UNDEFINED_DATA_MODEL: this.#error_codes.GENERAL;
for (let i = 0; i < structure.length; i++) {
let attribute = structure[i];
if (i === structure.length - 1) {
if (scorm2004 && (attribute.substr(0, 8) === "{target=") && (typeof refObject._isTargetValid == "function")) {
this.throwSCORMError(this.#error_codes.READ_ONLY_ELEMENT);
} else if (!refObject.hasOwnProperty(attribute)) {
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
} else {
if (this.stringContains(CMIElement, ".correct_responses")){
this.validateCorrectResponse(CMIElement, value)
}
if (!scorm2004 || this.lastErrorCode === 0) {
refObject[attribute] = value;
returnValue = api_constants.SCORM_TRUE;
}
}
} else {
refObject = refObject[attribute];
if (!refObject) {
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
break;
}
if (refObject.prototype === CMIArray) {
let index = parseInt(structure[i + 1], 10);
// SCO is trying to set an item on an array
if (!isNaN(index)) {
let item = refObject.childArray[index];
if (item) {
refObject = item;
} else {
let newChild = this.getChildElement(CMIElement, value);
if (!newChild) {
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
} else {
refObject.childArray.push(newChild);
refObject = newChild;
}
}
// Have to update i value to skip the array position
i++;
}
}
}
}
if (returnValue === api_constants.SCORM_FALSE) {
this.apiLog(methodName, null, `There was an error setting the value for: ${CMIElement}, value of: ${value}`, api_constants.LOG_LEVEL_WARNING);
}
return returnValue;
}
validateCorrectResponse(_CMIElement, _value) {
return false;
}
/**
* Gets or builds a new child element to add to the array.
* APIs that inherit BaseAPI should override this method
*/
getChildElement(_CMIElement) {
return null
}
/**
* Gets a value from the CMI Object
*
* @param methodName
* @param scorm2004
* @param CMIElement
* @returns {*}
*/
_commonGetCMIValue(methodName: String, scorm2004: boolean, CMIElement) {
if (!CMIElement || CMIElement === "") {
return "";
}
let structure = CMIElement.split(".");
let refObject = this;
let attribute = null;
for (let i = 0; i < structure.length; i++) {
attribute = structure[i];
if(!scorm2004) {
if (i === structure.length - 1) {
if (!refObject.hasOwnProperty(attribute)) {
this.throwSCORMError(101, "getCMIValue did not find a value for: " + CMIElement);
}
}
} else {
if ((String(attribute).substr(0, 8) === "{target=") && (typeof refObject._isTargetValid == "function")) {
let target = String(attribute).substr(8, String(attribute).length - 9);
return refObject._isTargetValid(target);
} else if (!refObject.hasOwnProperty(attribute)) {
this.throwSCORMError(401, "The data model element passed to GetValue (" + CMIElement + ") is not a valid SCORM data model element.");
return "";
}
}
refObject = refObject[attribute];
}
if (refObject === null || refObject === undefined) {
if(!scorm2004) {
if (attribute === "_children") {
this.throwSCORMError(202);
} else if (attribute === "_count") {
this.throwSCORMError(203);
}
}
return "";
} else {
return refObject;
}
}
/**
* Returns true if the API's current state is STATE_INITIALIZED
*/
isInitialized() {
return this.currentState === api_constants.STATE_INITIALIZED;
}
/**
* Returns true if the API's current state is STATE_NOT_INITIALIZED
*/
isNotInitialized() {
return this.currentState === api_constants.STATE_NOT_INITIALIZED;
}
/**
* Returns true if the API's current state is STATE_TERMINATED
*/
isTerminated() {
return this.currentState === api_constants.STATE_TERMINATED;
}
/**
* Provides a mechanism for attaching to a specific SCORM event
*
* @param listenerName
* @param callback
*/
on(listenerName: String, callback: function) {
if (!callback) return;
let listenerFunctions = listenerName.split(" ");
for (let i = 0; i < listenerFunctions.length; i++) {
let listenerSplit = listenerFunctions[i].split(".");
if (listenerSplit.length === 0) return;
let functionName = listenerSplit[0];
let CMIElement = null;
if (listenerSplit.length > 1) {
CMIElement = listenerName.replace(functionName + ".", "");
}
this.listenerArray.push({
functionName: functionName,
CMIElement: CMIElement,
callback: callback
});
}
};
/**
* Processes any 'on' listeners that have been created
*
* @param functionName
* @param CMIElement
* @param value
*/
processListeners(functionName: String, CMIElement: String, value: any) {
for (let i = 0; i < this.listenerArray.length; i++) {
let listener = this.listenerArray[i];
let functionsMatch = listener.functionName === functionName;
let listenerHasCMIElement = !!listener.CMIElement;
let CMIElementsMatch = listener.CMIElement === CMIElement;
if (functionsMatch && (!listenerHasCMIElement || CMIElementsMatch)) {
listener.callback(CMIElement, value);
}
}
};
/**
* Throws a SCORM error
*
* @param errorNumber
* @param message
*/
throwSCORMError(errorNumber: number, message: String) {
if (!message) {
message = this.getLmsErrorMessageDetails(errorNumber);
}
this.apiLog("throwSCORMError", null, errorNumber + ": " + message, api_constants.LOG_LEVEL_ERROR);
this.lastErrorCode = String(errorNumber);
}
/**
* Loads CMI data from a JSON object.
*/
loadFromJSON(json, CMIElement) {
if (!this.isNotInitialized()) {
console.error("loadFromJSON can only be called before the call to LMSInitialize.");
return;
}
CMIElement = CMIElement || "cmi";
for (let key in json) {
if (json.hasOwnProperty(key) && json[key]) {
let currentCMIElement = CMIElement + "." + key;
let value = json[key];
if (value["childArray"]) {
for (let i = 0; i < value["childArray"].length; i++) {
this.loadFromJSON(value["childArray"][i], currentCMIElement + "." + i);
}
} else if (value.constructor === Object) {
this.loadFromJSON(value, currentCMIElement);
} else {
this.setCMIValue(currentCMIElement, value);
}
}
}
}
renderCMIToJSON() {
let cmi = this.cmi;
// Do we want/need to return fields that have no set value?
// return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2);
return JSON.stringify({ cmi });
}
/**
* Check if the value matches the proper format. If not, throw proper error code.
*
* @param value
* @param regexPattern
* @returns {boolean}
*/
checkValidFormat(value: String, regexPattern: String) {
const formatRegex = new RegExp(regexPattern);
if(!value || !value.match(formatRegex)) {
this.throwSCORMError(this.#error_codes.TYPE_MISMATCH);
return false;
}
return true;
}
/**
* Check if the value matches the proper range. If not, throw proper error code.
*
* @param value
* @param rangePattern
* @returns {boolean}
*/
checkValidRange(value: any, rangePattern: String) {
const ranges = rangePattern.split('#');
value = value * 1.0;
if(value >= ranges[0]) {
if((ranges[1] === '*') || (value <= ranges[1])) {
this.clearSCORMError(api_constants.SCORM_TRUE);
return true;
} else {
this.throwSCORMError(this.#error_codes.VALUE_OUT_OF_RANGE);
return false;
}
} else {
this.throwSCORMError(this.#error_codes.VALUE_OUT_OF_RANGE);
return false;
}
}
/**
* Throws a SCORM error
*
* @param when the number of milliseconds to wait before committing
*/
scheduleCommit(when: number) {
this.#timeout = new ScheduledCommit(this, when);
}
/**
* Clears and cancels any currently scheduled commits
*/
clearScheduledCommit() {
if (this.#timeout) {
this.#timeout.cancel();
this.#timeout = null;
}
}
}
class ScheduledCommit {
#API;
#cancelled: false;
#timeout;
constructor(API: any, when: number) {
this.#API = API;
this.#timeout = setTimeout(this.#wrapper, when);
}
cancel() {
this.#cancelled = true;
if (this.#timeout) {
clearTimeout(this.#timeout);
}
}
#wrapper() {
if (!this.#cancelled) {
this.#API.LMSCommit();
}
}
}

181
src/Scorm12API.js Normal file
View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
import BaseAPI from './BaseAPI';
import {
CMI,
CMIInteractionsCorrectResponsesObject,
CMIInteractionsObject, CMIInteractionsObjectivesObject,
CMIObjectivesObject
} from "./cmi/scorm12_cmi";
import * as Utilities from './utilities';
import {scorm12_constants, scorm12_error_codes} from "./constants";
import {scorm12_regex} from "./regex";
const constants = scorm12_constants;
export default class Scorm12API extends BaseAPI {
constructor() {
super(scorm12_error_codes);
this.cmi = new CMI(this);
}
/**
* @returns {string} bool
*/
LMSInitialize() {
return this.APIInitialize("LMSInitialize", "LMS was already initialized!", "LMS is already finished!");
}
/**
* @returns {string} bool
*/
LMSFinish() {
return this.APITerminate("LMSFinish", false);
}
/**
* @param CMIElement
* @returns {string}
*/
LMSGetValue(CMIElement) {
return this.APIGetValue("LMSGetValue", false, CMIElement);
}
/**
* @param CMIElement
* @param value
* @returns {string}
*/
LMSSetValue(CMIElement, value) {
return this.APISetValue("LMSSetValue", false, CMIElement, value);
}
/**
* Orders LMS to store all content parameters
*
* @returns {string} bool
*/
LMSCommit() {
return this.APICommit("LMSCommit", false);
}
/**
* Returns last error code
*
* @returns {string}
*/
LMSGetLastError() {
return this.APIGetLastError("LMSGetLastError");
}
/**
* Returns the errorNumber error description
*
* @param CMIErrorCode
* @returns {string}
*/
LMSGetErrorString(CMIErrorCode) {
return this.APIGetErrorString("LMSGetErrorString", CMIErrorCode);
}
/**
* Returns a comprehensive description of the errorNumber error.
*
* @param CMIErrorCode
* @returns {string}
*/
LMSGetDiagnostic(CMIErrorCode) {
return this.APIGetDiagnostic("LMSGetDiagnostic", CMIErrorCode);
}
/**
* Sets a value on the CMI Object
*
* @param CMIElement
* @param value
* @returns {string}
*/
setCMIValue(CMIElement, value) {
this._commonSetCMIValue("LMSSetValue", false, CMIElement, value);
}
/**
* Gets a value from the CMI Object
*
* @param CMIElement
* @returns {*}
*/
getCMIValue(CMIElement) {
return this._commonGetCMIValue("getCMIValue", false, CMIElement);
}
/**
* Gets or builds a new child element to add to the array.
*
* @param CMIElement
*/
getChildElement(CMIElement, value) {
let newChild;
if (this.stringContains(CMIElement, "cmi.objectives")) {
newChild = new CMIObjectivesObject(this);
} else if (this.stringContains(CMIElement, ".correct_responses")) {
newChild = new CMIInteractionsCorrectResponsesObject(this);
} else if (this.stringContains(CMIElement, ".objectives")) {
newChild = new CMIInteractionsObjectivesObject(this);
} else if (this.stringContains(CMIElement, "cmi.interactions")) {
newChild = new CMIInteractionsObject(this);
}
return newChild;
}
validateCorrectResponse(CMIElement, value) {
return true;
}
/**
* Returns the message that corresponds to errorNumber.
*/
getLmsErrorMessageDetails(errorNumber, detail) {
let basicMessage = "No Error";
let detailMessage = "No Error";
// Set error number to string since inconsistent from modules if string or number
errorNumber = String(errorNumber);
if(constants.error_descriptions[errorNumber]) {
basicMessage = constants.error_descriptions[errorNumber].basicMessage;
detailMessage = constants.error_descriptions[errorNumber].detailMessage;
}
return detail ? detailMessage : basicMessage;
}
/**
* Adds the current session time to the existing total time.
*/
getCurrentTotalTime() {
const timeRegex = new RegExp(scorm12_regex.CMITime);
const totalTime = this.cmi.core.total_time;
const sessionTime = this.cmi.core.session_time;
const totalSeconds = Utilities.getTimeAsSeconds(totalTime, timeRegex);
const sessionSeconds = Utilities.getTimeAsSeconds(sessionTime, timeRegex);
return Utilities.getSecondsAsHHMMSS(totalSeconds + sessionSeconds);
}
/**
* Replace the whole API with another
*/
replaceWithAnotherScormAPI(newAPI) {
// Data Model
this.cmi = newAPI.cmi;
}
}

382
src/Scorm2004API.js Normal file
View File

@@ -0,0 +1,382 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
import BaseAPI from './BaseAPI';
import {
CMI,
ADL,
CMIObjectivesObject,
CMIInteractionsObject,
CMICommentsFromLearnerObject,
CMICommentsFromLMSObject, CMIInteractionsCorrectResponsesObject, CMIInteractionsObjectivesObject
} from './cmi/scorm2004_cmi';
import * as Utilities from "./utilities";
import {scorm2004_constants, correct_responses, scorm2004_error_codes} from "./constants";
import {scorm2004_regex} from "./regex";
const constants = scorm2004_constants;
const valid_languages = constants.valid_languages;
class Scorm2004API extends BaseAPI {
version: "1.0";
constructor() {
super(scorm2004_error_codes);
this.cmi = new CMI(this);
this.adl = new ADL(this);
// Rename functions to match 2004 Spec
this.Initialize = this.LMSInitialize;
this.Terminate = this.LMSTerminate;
this.GetValue = this.LMSGetValue;
this.SetValue = this.LMSSetValue;
this.Commit = this.LMSCommit;
this.GetLastError = this.LMSGetLastError;
this.GetErrorString = this.LMSGetErrorString;
this.GetDiagnostic = this.LMSGetDiagnostic;
}
/**
* @returns {string} bool
*/
LMSInitialize() {
return this.APIInitialize("Initialize");
}
/**
* @returns {string} bool
*/
LMSTerminate() {
return this.APITerminate("Terminate", true);
}
/**
* @param CMIElement
* @returns {string}
*/
LMSGetValue(CMIElement) {
return this.APIGetValue("GetValue", true, CMIElement);
}
/**
* @param CMIElement
* @param value
* @returns {string}
*/
LMSSetValue(CMIElement, value) {
return this.APISetValue("SetValue", true, CMIElement, value);
}
/**
* Orders LMS to store all content parameters
*
* @returns {string} bool
*/
LMSCommit() {
return this.APICommit("Commit");
}
/**
* Returns last error code
*
* @returns {string}
*/
LMSGetLastError() {
return this.APIGetLastError("GetLastError");
}
/**
* Returns the errorNumber error description
*
* @param CMIErrorCode
* @returns {string}
*/
LMSGetErrorString(CMIErrorCode) {
return this.APIGetErrorString("GetErrorString", CMIErrorCode);
}
/**
* Returns a comprehensive description of the errorNumber error.
*
* @param CMIErrorCode
* @returns {string}
*/
LMSGetDiagnostic(CMIErrorCode) {
return this.APIGetDiagnostic("GetDiagnostic", CMIErrorCode);
}
/**
* Sets a value on the CMI Object
*
* @param CMIElement
* @param value
* @returns {string}
*/
setCMIValue(CMIElement, value) {
this._commonSetCMIValue("SetValue", true, CMIElement, value);
}
/**
* Gets or builds a new child element to add to the array.
*
* @param CMIElement
* @param value
*/
getChildElement(CMIElement, value) {
let newChild;
if (this.stringContains(CMIElement, "cmi.objectives")) {
newChild = new CMIObjectivesObject(this);
} else if (this.stringContains(CMIElement, ".correct_responses")) {
const parts = CMIElement.split('.');
const index = Number(parts[2]);
let interaction = this.cmi.interactions.childArray[index];
if(typeof interaction.type === 'undefined') {
this.throwSCORMError(scorm2004_error_codes.DEPENDENCY_NOT_ESTABLISHED);
} else {
let interaction_type = interaction.type;
let interaction_count = interaction.correct_responses._count;
if(interaction_type === 'choice') {
for(let i = 0; i < interaction_count && this.lastErrorCode === 0; i++) {
const response = interaction.correct_responses.childArray[i];
if(response.pattern === value) {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE);
}
}
}
const response_type = correct_responses[interaction_type];
let nodes = [];
if(response_type.delimiter !== '') {
nodes = value.split(response_type.delimiter);
} else {
nodes[0] = value;
}
if(nodes.length > 0 && nodes.length <= response_type.max) {
this.#checkCorrectResponseValue(interaction_type, nodes, value);
} else if (nodes.length > response_type.max) {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE, "Data Model Element Pattern Too Long");
}
}
if(this.lastErrorCode === 0) {
newChild = new CMIInteractionsCorrectResponsesObject(this);
}
} else if (this.stringContains(CMIElement, ".objectives")) {
newChild = new CMIInteractionsObjectivesObject(this);
} else if (this.stringContains(CMIElement, "cmi.interactions")) {
newChild = new CMIInteractionsObject(this);
} else if (this.stringContains(CMIElement, "cmi.comments_from_learner")) {
newChild = new CMICommentsFromLearnerObject(this);
} else if (this.stringContains(CMIElement, "cmi.comments_from_lms")) {
newChild = new CMICommentsFromLMSObject(this);
}
return newChild;
}
validateCorrectResponse(CMIElement, value) {
const parts = CMIElement.split('.');
const index = Number(parts[2]);
const pattern_index = Number(parts[4]);
let interaction = this.cmi.interactions.childArray[index];
let interaction_type = interaction.type;
let interaction_count = interaction.correct_responses._count;
if(interaction_type === 'choice') {
for(let i = 0; i < interaction_count && this.lastErrorCode === 0; i++) {
const response = interaction.correct_responses.childArray[i];
if(response.pattern === value) {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE);
}
}
}
const response_type = scorm2004_constants.correct_responses[interaction_type];
if(typeof response_type.limit !== 'undefined' || interaction_count < response_type.limit) {
let nodes = [];
if (response_type.delimiter !== '') {
nodes = value.split(response_type.delimiter);
} else {
nodes[0] = value;
}
if(nodes.length > 0 && nodes.length <= response_type.max) {
this.#checkCorrectResponseValue(interaction_type, nodes, value);
} else if (nodes.length > response_type.max) {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE, "Data Model Element Pattern Too Long");
}
if(this.lastErrorCode === 0
&& (!response_type.duplicate || !this.#checkDuplicatedPattern(interaction.correct_responses, pattern_index, value))
|| (this.lastErrorCode === 0 && value === '')) {
// do nothing, we want the inverse
} else {
if(this.lastErrorCode === 0) {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE, "Data Model Element Pattern Already Exists");
}
}
} else {
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE, "Data Model Element Collection Limit Reached");
}
}
/**
* Gets a value from the CMI Object
*
* @param CMIElement
* @returns {*}
*/
getCMIValue(CMIElement) {
return this._commonGetCMIValue("GetValue", true, CMIElement);
}
/**
* Returns the message that corresponds to errorNumber.
*/
getLmsErrorMessageDetails(errorNumber, detail) {
let basicMessage = "";
let detailMessage = "";
// Set error number to string since inconsistent from modules if string or number
errorNumber = String(errorNumber);
if(constants.error_descriptions[errorNumber]) {
basicMessage = constants.error_descriptions[errorNumber].basicMessage;
detailMessage = constants.error_descriptions[errorNumber].detailMessage;
}
return detail ? detailMessage : basicMessage;
}
#checkDuplicatedPattern(correct_response, current_index, value) {
let found = false;
let count = correct_response._count;
for(let i = 0; i < count && !found; i++) {
if(i !== current_index && correct_response.childArray[i] === value) {
found = true;
}
}
return found;
}
#checkCorrectResponseValue(interaction_type, nodes, value) {
const response = correct_responses[interaction_type];
const formatRegex = new RegExp(response.format);
for(let i = 0; i < nodes.length && this.lastErrorCode === 0; i++) {
if(interaction_type.match('^(fill-in|long-fill-in|matching|performance|sequencing)$')) {
nodes[i] = #removeCorrectResponsePrefixes(nodes[i]);
}
if(response.delimiter2 !== undefined) {
let values = nodes[i].split(response.delimiter2);
if(values.length === 2) {
let matches = values[0].match(formatRegex);
if(!matches) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
} else {
if(!values[1].match(new RegExp(response.format2))) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
}
} else {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
} else {
let matches = nodes[i].match(formatRegex);
if((!matches && value !== '') || (!matches && interaction_type === 'true-false')) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
} else {
if (interaction_type === 'numeric' && nodes.length > 1) {
if(Number(nodes[0]) > Number(nodes[1])) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
} else {
if(nodes[i] !== '' && response.unique) {
for(let j = 0; j < i && this.lastErrorCode === 0; j++) {
if(nodes[i] === nodes[j]) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
}
}
}
}
}
}
}
#removeCorrectResponsePrefixes(node) {
let seenOrder = false;
let seenCase = false;
let seenLang = false;
let prefixRegex = '^(\{(lang|case_matters|order_matters)=([^\}]+)\})';
let matches = node.match(prefixRegex);
while(matches) {
switch (matches[2]) {
case 'lang':
let langMatches = node.match(scorm2004_regex.CMILangcr);
if(langMatches) {
let lang = langMatches[3];
if (lang !== undefined && lang.length > 0) {
if(valid_languages[lang.toLowerCase()] === undefined) {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
}
}
seenLang = true;
break;
case 'case_matters':
if(!seenLang && !seenOrder && !seenCase) {
if(matches[3] !== 'true' && matches[3] !== 'false') {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
}
seenCase = true;
break;
case 'order_matters':
if(!seenCase && !seenLang && !seenOrder) {
if(matches[3] !== 'true' && matches[3] !== 'false') {
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
}
}
seenOrder = true;
break;
default:
break;
}
node = node.substr(matches[1].length);
matches = node.match(prefixRegex);
}
return node;
}
/**
* Replace the whole API with another
*/
replaceWithAnotherScormAPI(newAPI) {
// Data Model
this.cmi = newAPI.cmi;
this.adl = newAPI.adl;
}
/**
* Adds the current session time to the existing total time.
*/
getCurrentTotalTime() {
const totalTime = this.cmi.total_time;
const sessionTime = this.cmi.session_time;
const durationRegex = scorm2004_regex.CMITimespan;
const totalSeconds = Utilities.getDurationAsSeconds(totalTime, durationRegex);
const sessionSeconds = Utilities.getDurationAsSeconds(sessionTime, durationRegex);
return Utilities.getSecondsAsISODuration(totalSeconds + sessionSeconds);
}
}

120
src/cmi/aicc_cmi.js Normal file
View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
import * as Scorm12CMI from './scorm12_cmi';
import {BaseCMI, CMIArray, CMIScore} from "./common";
import {aicc_constants} from "../constants";
import {aicc_regex} from "../regex";
const constants = aicc_constants;
const regex = aicc_regex;
export class CMI extends Scorm12CMI.CMI {
constructor(API) {
super(API, constants.cmi_children, new AICCCMIStudentData(API));
this.evaluation = new CMIEvaluation(API);
}
}
class CMIEvaluation extends BaseCMI {
constructor(API) {
super(API);
}
comments = new class extends CMIArray {
constructor(API) {
super(API, constants.comments_children, 402);
}
};
}
class AICCCMIStudentData extends Scorm12CMI.CMIStudentData {
constructor(API) {
super(API, constants.student_data_children);
}
#tries_during_lesson = "";
get tries_during_lesson() { return this.#tries_during_lesson; }
set tries_during_lesson(tries_during_lesson) { this.API.isNotInitialized() ? this.#tries_during_lesson = tries_during_lesson : this.throwReadOnlyError() }
tries = new class extends CMIArray {
constructor(API) {
super(API, aicc_constants.tries_children);
}
};
}
export class CMITriesObject extends BaseCMI {
constructor(API) {
super(API);
}
#status = "";
#time = "";
get status() { return this.#status; }
set status(status) {
if(this.API.checkValidFormat(status, regex.CMIStatus2)) {
this.#status = status;
}
}
get time() { return this.#time; }
set time(time) {
if(this.API.checkValidFormat(time, regex.CMITime)) {
this.#time = time;
}
}
score = new CMIScore(API);
}
export class CMIEvaluationCommentsObject extends BaseCMI {
constructor(API) {
super(API);
}
#content = "";
#location = "";
#time = "";
get content() { return this.#content; }
set content(content) {
if(this.API.checkValidFormat(content, regex.CMIString256)) {
this.#content = content;
}
}
get location() { return this.#location; }
set location(location) {
if(this.API.checkValidFormat(location, regex.CMIString256)) {
this.#location = location;
}
}
get time() { return this.#time; }
set time(time) {
if(this.API.checkValidFormat(time, regex.CMITime)) {
this.#time = time;
}
}
}
export class NAV extends BaseCMI {
constructor(API) {
super(API);
}
#event = "";
get event() { return (!this.jsonString) ? this.API.throwSCORMError(404) : this.#event; }
set event(event) {
if(this.API.checkValidFormat(event, regex.NAVEvent)) {
this.#event = event;
}
}
}

95
src/cmi/common.js Normal file
View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
import {scorm12_constants, scorm12_error_codes} from "../constants";
export class BaseCMI {
jsonString = false;
API;
constructor(API: any) {
this.API = API;
}
}
export class CMIScore extends BaseCMI {
constructor(API, score_children?, score_range?, invalidErrorCode) {
super(API);
this.#_children = score_children? score_children : scorm12_constants.score_children;
this.#_score_range = score_range? score_range : false;
this.#_invalid_error_code = invalidErrorCode ? invalidErrorCode : scorm12_error_codes.INVALID_SET_VALUE;
}
#_children;
#_score_range;
#_invalid_error_code;
#raw = "";
#min = "";
#max = "100";
get _children() { return this.#_children; }
set _children(_children) { this.API.throwSCORMError(this.#_invalid_error_code); }
get raw() { return this.#raw; }
set raw(raw) {
if(this.API.checkValidFormat(raw, scorm12_constants.CMIDecimal)
&& (!this.#_score_range || this.API.checkValidRange(raw, this.#_score_range))) {
this.#raw = raw;
}
}
get min() { return this.#min; }
set min(min) {
if(this.API.checkValidFormat(min, scorm12_constants.CMIDecimal)
&& (!this.#_score_range || this.API.checkValidRange(min, this.#_score_range))) {
this.#min = min;
}
}
get max() { return this.#max; }
set max(max) {
if(this.API.checkValidFormat(max, scorm12_constants.CMIDecimal)
&& (!this.#_score_range || this.API.checkValidRange(max, this.#_score_range))) {
this.#max = max;
}
}
toJSON = () => {
return {
'raw': this.raw,
'min': this.min,
'max': this.max
}
}
}
export class CMIArray extends BaseCMI {
constructor({API, children, errorCode}) {
super(API);
this.#_children = children;
this.#errorCode = errorCode;
this.childArray = [];
}
#errorCode;
#_children;
get _children() { return this.#_children; }
set _children(_children) { this.API.throwSCORMError(this.#errorCode); }
get _count() { return this.childArray.length; }
set _count(_count) { this.API.throwSCORMError(this.#errorCode); }
toJSON = () => {
this.jsonString = true;
let result = {};
for(let i = 0; i < this.childArray.length; i++) {
result[i + ''] = this.childArray[i];
}
delete this.jsonString;
return result;
}
}

467
src/cmi/scorm12_cmi.js Normal file
View File

@@ -0,0 +1,467 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
import {BaseCMI, CMIArray, CMIScore} from './common';
import {scorm12_constants, scorm12_error_codes} from "../constants";
import {scorm12_regex} from "../regex";
const constants = scorm12_constants;
const regex = scorm12_regex;
function throwReadOnlyError(API) {
API.throwSCORMError(scorm12_error_codes.READ_ONLY_ELEMENT);
}
function throwWriteOnlyError(API) {
API.throwSCORMError(scorm12_error_codes.WRITE_ONLY_ELEMENT);
}
function throwInvalidValueError(API) {
API.throwSCORMError(scorm12_error_codes.INVALID_SET_VALUE);
}
export class CMI extends BaseCMI {
#_children = "";
#_version = "3.4";
#suspend_data = "";
#launch_data = "";
#comments = "";
#comments_from_lms = "";
student_data = null;
constructor(API, cmi_children, student_data) {
super(API);
this.#_children = cmi_children ? cmi_children : constants.cmi_children;
this.core = new CMICore(API);
this.objectives = new CMIObjectives(API);
this.student_data = student_data ? student_data : new CMIStudentData(API);
this.student_preference = new CMIStudentPreference(API);
this.interactions = new CMIInteractions(API);
}
toJSON = () => {
this.jsonString = true;
const result = {
'suspend_data': this.suspend_data,
'launch_data': this.launch_data,
'comments': this.comments,
'comments_from_lms': this.comments_from_lms,
'core': this.core,
'objectives': this.objectives,
'student_data': this.student_data,
'student_preference': this.student_preference,
'interactions': this.interactions
};
delete this.jsonString;
return result;
};
get _version() { return this.#_version; }
set _version(_version) { throwInvalidValueError(this.API); }
get _children() { return this.#_children; }
set _children(_children) { throwInvalidValueError(this.API); }
get suspend_data() { return this.#suspend_data; }
set suspend_data(suspend_data) {
if(this.API.checkValidFormat(suspend_data, regex.CMIString4096)) {
this.#suspend_data = suspend_data;
}
}
get launch_data() { return this.#launch_data; }
set launch_data(launch_data) { this.API.isNotInitialized() ? this.#launch_data = launch_data : throwReadOnlyError(this.API); }
get comments() { return this.#comments; }
set comments(comments) {
if(this.API.checkValidFormat(comments, regex.CMIString4096)) {
this.#comments = comments;
}
}
get comments_from_lms() { return this.#comments_from_lms; }
set comments_from_lms(comments_from_lms) { this.API.isNotInitialized() ? this.#comments_from_lms = comments_from_lms : throwReadOnlyError(this.API); }
}
class CMICore extends BaseCMI {
constructor(API) {
super(API);
this.score = new CMIScore(API, constants.score_children, regex.score_range);
}
#_children = constants.core_children;
#student_id = "";
#student_name = "";
#lesson_location = "";
#credit = "";
#lesson_status = "";
#entry = "";
#total_time = "";
#lesson_mode = "normal";
#exit = "";
#session_time = "00:00:00";
get _children() { return this.#_children; }
set _children(_children) { throwInvalidValueError(this.API); }
get student_id() { return this.#student_id; }
set student_id(student_id) { this.API.isNotInitialized() ? this.#student_id = student_id : throwReadOnlyError(this.API); }
get student_name() { return this.#student_name; }
set student_name(student_name) { this.API.isNotInitialized() ? this.#student_name = student_name : throwReadOnlyError(this.API); }
get lesson_location() { return this.#lesson_location; }
set lesson_location(lesson_location) {
if(this.API.checkValidFormat(lesson_location, regex.CMIString256)) {
this.#lesson_location = lesson_location;
}
}
get credit() { return this.#credit; }
set credit(credit) { this.API.isNotInitialized() ? this.#credit = credit : throwReadOnlyError(this.API); }
get lesson_status() { return this.#lesson_status; }
set lesson_status(lesson_status) {
if(this.API.checkValidFormat(lesson_status, regex.CMIStatus)) {
this.#lesson_status = lesson_status;
}
}
get entry() { return this.#entry; }
set entry(entry) { this.API.isNotInitialized() ? this.#entry = entry : throwReadOnlyError(this.API); }
get total_time() { return this.#total_time; }
set total_time(total_time) { this.API.isNotInitialized() ? this.#total_time = total_time : throwReadOnlyError(this.API); }
get lesson_mode() { return this.#lesson_mode; }
set lesson_mode(lesson_mode) { this.API.isNotInitialized() ? this.#lesson_mode = lesson_mode : throwReadOnlyError(this.API); }
get exit() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#exit; }
set exit(exit) {
if(this.API.checkValidFormat(exit, regex.CMIExit)) {
this.#exit = exit;
}
}
get session_time() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#session_time; }
set session_time(session_time) {
if(this.API.checkValidFormat(session_time, regex.CMITimespan)) {
this.#session_time = session_time;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'student_id': this.student_id,
'student_name': this.student_name,
'lesson_location': this.lesson_location,
'credit': this.credit,
'lesson_status': this.lesson_status,
'entry': this.entry,
'total_time': this.total_time,
'lesson_mode': this.lesson_mode,
'exit': this.exit,
'session_time': this.session_time,
'score': this.score
};
delete this.jsonString;
return result;
}
}
class CMIObjectives extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.objectives_children,
errorCode: scorm12_error_codes.INVALID_SET_VALUE
});
}
}
export class CMIStudentData extends BaseCMI {
#_children;
#mastery_score = "";
#max_time_allowed = "";
#time_limit_action = "";
constructor(API, student_data_children) {
super(API);
this.#_children = student_data_children? student_data_children : constants.student_data_children;
}
get _children() { return this.#_children; }
set _children(_children) { throwInvalidValueError(this.API); }
get mastery_score() { return this.#mastery_score; }
set mastery_score(mastery_score) { this.API.isNotInitialized() ? this.#mastery_score = mastery_score : throwReadOnlyError(this.API); }
get max_time_allowed() { return this.#max_time_allowed; }
set max_time_allowed(max_time_allowed) { this.API.isNotInitialized() ? this.#max_time_allowed = max_time_allowed : throwReadOnlyError(this.API); }
get time_limit_action() { return this.#time_limit_action; }
set time_limit_action(time_limit_action) { this.API.isNotInitialized() ? this.#time_limit_action = time_limit_action : throwReadOnlyError(this.API); }
toJSON = () => {
this.jsonString = true;
const result = {
'mastery_score': this.mastery_score,
'max_time_allowed': this.max_time_allowed,
'time_limit_action': this.time_limit_action
};
delete this.jsonString;
return result;
}
}
class CMIStudentPreference extends BaseCMI {
constructor(API) {
super(API);
}
#_children = constants.student_preference_children;
#audio = "";
#language = "";
#speed = "";
#text = "";
get _children() { return this.#_children; }
set _children(_children) { throwInvalidValueError(this.API); }
get audio() { return this.#audio; }
set audio(audio) {
if(this.API.checkValidFormat(audio, regex.CMISInteger)
&& this.API.checkValidRange(audio, regex.audio_range)) {
this.#audio = audio;
}
}
get language() { return this.#language; }
set language(language) {
if(this.API.checkValidFormat(language, regex.CMIString256)) {
this.#language = language;
}
}
get speed() { return this.#speed; }
set speed(speed) {
if(this.API.checkValidFormat(speed, regex.CMISInteger)
&& this.API.checkValidRange(speed, regex.speed_range)) {
this.#speed = speed;
}
}
get text() { return this.#text; }
set text(text) {
if(this.API.checkValidFormat(text, regex.CMISInteger)
&& this.API.checkValidRange(text, regex.text_range)) {
this.#text = text;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'audio': this.audio,
'language': this.language,
'speed': this.speed,
'text': this.text
};
delete this.jsonString;
return result;
}
}
class CMIInteractions extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.interactions_children,
errorCode: scorm12_error_codes.INVALID_SET_VALUE
});
}
}
export class CMIInteractionsObject extends BaseCMI {
constructor(API) {
super(API);
this.objectives = new CMIArray({
API: API,
errorCode: 402,
children: constants.objectives_children
});
this.correct_responses = new CMIArray({
API: API,
errorCode: 402,
children: constants.correct_responses_children
});
}
#id: "";
#time: "";
#type: "";
#weighting: "";
#student_response: "";
#result: "";
#latency: "";
get id() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMIIdentifier)) {
this.#id = id;
}
}
get time() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#time; }
set time(time) {
if(this.API.checkValidFormat(time, regex.CMITime)) {
this.#time = time;
}
}
get type() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#type; }
set type(type) {
if(this.API.checkValidFormat(type, regex.CMIType)) {
this.#type = type;
}
}
get weighting() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#weighting; }
set weighting(weighting) {
if(this.API.checkValidFormat(weighting, regex.CMIDecimal)
&& this.API.checkValidRange(weighting, regex.weighting_range)) {
this.#weighting = weighting;
}
}
get student_response() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#student_response; }
set student_response(student_response) {
if(this.API.checkValidFormat(student_response, regex.CMIFeedback)) {
this.#student_response = student_response;
}
}
get result() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#result; }
set result(result) {
if(this.API.checkValidFormat(result, regex.CMIResult)) {
this.#result = result;
}
}
get latency() { return (!this.jsonString) ? throwWriteOnlyError(this.API) : this.#latency; }
set latency(latency) {
if(this.API.checkValidFormat(latency, regex.CMITimespan)) {
this.#latency = latency;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'id': this.id,
'time': this.time,
'type': this.type,
'weighting': this.weighting,
'student_response': this.student_response,
'result': this.result,
'latency': this.latency,
'objectives': this.objectives,
'correct_responses': this.correct_responses
};
delete this.jsonString;
return result;
}
}
export class CMIObjectivesObject extends BaseCMI {
constructor(API) {
super(API);
this.score = new CMIScore(API);
}
#id: "";
#status: "";
get id() { return this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMIIdentifier)) {
this.#id = id;
}
}
get status() { return this.#status; }
set status(status) {
if(this.API.checkValidFormat(status, regex.CMIStatus2)) {
this.#status = status;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'id': this.id,
'status': this.status,
'score': this.score
};
delete this.jsonString;
return result;
}
}
export class CMIInteractionsObjectivesObject extends BaseCMI {
constructor(API) {
super(API);
}
#id: "";
get id() { return this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMIIdentifier)) {
this.#id = id;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'id': this.id,
};
delete this.jsonString;
return result;
}
}
export class CMIInteractionsCorrectResponsesObject extends BaseCMI {
constructor(API) {
super(API);
}
#pattern: "";
get pattern() { return this.#pattern; }
set pattern(pattern) {
if(this.API.checkValidFormat(pattern, regex.CMIFeedback)) {
this.#pattern = pattern;
}
}
toJSON = () => {
this.jsonString = true;
const result = {
'pattern': this.pattern,
};
delete this.jsonString;
return result;
}
}

522
src/cmi/scorm2004_cmi.js Normal file
View File

@@ -0,0 +1,522 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
import {BaseCMI, CMIScore, CMIArray} from './common';
import {scorm2004_constants, learner_responses} from "../constants";
import {scorm2004_regex} from "../regex";
const constants = scorm2004_constants;
const regex = scorm2004_regex;
export class CMI extends BaseCMI {
constructor(API) {
super(API);
this.learner_preference = new CMILearnerPreference(API);
}
#_version: "1.0";
#_children = constants.cmi_children;
#completion_status: "unknown";
#completion_threshold: "";
#credit: "credit";
#entry: "";
#exit: "";
#launch_data: "";
#learner_id: "";
#learner_name: "";
#location: "";
#max_time_allowed: "";
#mode: "normal";
#progress_measure: "";
#scaled_passing_score: "";
#session_time: "PT0H0M0S";
#success_status: "unknown";
#suspend_data: "";
#time_limit_action: "continue,no message";
#total_time: "0";
get _version() { return this.#_version; }
set _version(_version) { this.throwReadOnlyError(); }
get _children() { return this.#_children; }
set _children(_children) { this.throwReadOnlyError(); }
get completion_status() { return this.#completion_status; }
set completion_status(completion_status) {
if(this.API.checkValidFormat(completion_status, regex.CMICStatus)) {
this.#completion_status = completion_status;
}
}
get completion_threshold() { return this.#completion_threshold; }
set completion_threshold(completion_threshold) { this.API.isNotInitialized() ? this.#completion_threshold = completion_threshold : this.throwReadOnlyError(); }
get credit() { return this.#credit; }
set credit(credit) { this.API.isNotInitialized() ? this.#credit = credit : this.throwReadOnlyError(); }
get entry() { return this.#entry; }
set entry(entry) { this.API.isNotInitialized() ? this.#entry = entry : this.throwReadOnlyError(); }
get exit() { return (!this.jsonString) ? this.throwWriteOnlyError() : this.#exit; }
set exit(exit) {
if(this.API.checkValidFormat(exit, regex.CMIExit)) {
this.#exit = exit;
}
}
get launch_data() { return this.#launch_data; }
set launch_data(launch_data) { this.API.isNotInitialized() ? this.#launch_data = launch_data : this.throwReadOnlyError(); }
get learner_id() { return this.#learner_id; }
set learner_id(learner_id) { this.API.isNotInitialized() ? this.#learner_id = learner_id : this.throwReadOnlyError(); }
get learner_name() { return this.#learner_name; }
set learner_name(learner_name) { this.API.isNotInitialized() ? this.#learner_name = learner_name : this.throwReadOnlyError(); }
get location() { return this.#location; }
set location(location) {
if(this.API.checkValidFormat(location, regex.CMIString1000)) {
this.#location = location;
}
}
get max_time_allowed() { return this.#max_time_allowed; }
set max_time_allowed(max_time_allowed) { this.API.isNotInitialized() ? this.#max_time_allowed = max_time_allowed : this.throwReadOnlyError(); }
get mode() { return this.#mode; }
set mode(mode) { this.API.isNotInitialized() ? this.#mode = mode : this.throwReadOnlyError(); }
get progress_measure() { return this.#progress_measure; }
set progress_measure(progress_measure) {
if(this.API.checkValidFormat(progress_measure, regex.CMIDecimal)
&& this.API.checkValidRange(progress_measure, regex.progress_range)) {
this.#progress_measure = progress_measure;
}
}
get scaled_passing_score() { return this.#scaled_passing_score; }
set scaled_passing_score(scaled_passing_score) { this.API.isNotInitialized() ? this.#scaled_passing_score = scaled_passing_score : this.throwReadOnlyError(); }
get session_time() { return (!this.jsonString) ? this.API.throwSCORMError(405) : this.#session_time; }
set session_time(session_time) {
if(this.API.checkValidFormat(session_time, regex.CMITimespan)) {
this.#session_time = session_time;
}
}
get success_status() { return this.#success_status; }
set success_status(success_status) {
if(this.API.checkValidFormat(success_status, regex.CMISStatus)) {
this.#success_status = success_status;
}
}
get suspend_data() { return this.#suspend_data; }
set suspend_data(suspend_data) {
if(this.API.checkValidFormat(suspend_data, regex.CMIString64000)) {
this.#suspend_data = suspend_data;
}
}
get time_limit_action() { return this.#time_limit_action; }
set time_limit_action(time_limit_action) { this.API.isNotInitialized() ? this.#time_limit_action = time_limit_action : this.throwReadOnlyError(); }
get total_time() { return this.#total_time; }
set total_time(total_time) { this.API.isNotInitialized() ? this.#total_time = total_time : this.throwReadOnlyError(); }
comments_from_learner = new class extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.comments_children,
errorCode: 404
});
}
};
comments_from_lms = new class extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.comments_children,
errorCode: 404
});
}
};
interactions = new class extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.interactions_children,
errorCode: 404
});
}
};
objectives = new class extends CMIArray {
constructor(API) {
super({
API: API,
children: constants.objectives_children,
errorCode: 404
});
}
};
}
class CMILearnerPreference extends BaseCMI {
constructor(API) {
super(API);
}
#_children: constants.student_preference_children;
#audio_level: "1";
#language: "";
#delivery_speed: "1";
#audio_captioning: "0";
get _children() { return this.#_children; }
set _children(_children) { this.throwReadOnlyError(); }
get audio_level() { return this.#audio_level; }
set audio_level(audio_level) {
if(this.API.checkValidFormat(audio_level, regex.CMIDecimal)
&& this.API.checkValidRange(audio_level, regex.audio_range)) {
this.#audio_level = audio_level;
}
}
get language() { return this.#language; }
set language(language) {
if(this.API.checkValidFormat(language, regex.CMILang)) {
this.#language = language;
}
}
get delivery_speed() { return this.#delivery_speed; }
set delivery_speed(delivery_speed) {
if(this.API.checkValidFormat(delivery_speed, regex.CMIDecimal)
&& this.API.checkValidRange(delivery_speed, regex.speed_range)) {
this.#delivery_speed = delivery_speed;
}
}
get audio_captioning() { return this.#audio_captioning; }
set audio_captioning(audio_captioning) {
if(this.API.checkValidFormat(audio_captioning, regex.CMISInteger)
&& this.API.checkValidRange(audio_captioning, regex.text_range)) {
this.#audio_captioning = audio_captioning;
}
}
}
export class CMIInteractionsObject extends BaseCMI {
constructor(API) {
super(API);
this.objectives = new CMIArray({
API: API,
errorCode: 404,
children: constants.objectives_children
});
this.correct_responses = new CMIArray({
API: API,
errorCode: 404,
children: constants.correct_responses_children
});
}
#id: "";
#type: "";
#timestamp: "";
#weighting: "";
#learner_response: "";
#result: "";
#latency: "";
#description: "";
get id() { return this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) {
this.#id = id;
}
}
get type() { return this.#type; }
set type(type) {
if(this.API.checkValidFormat(type, regex.CMIType)) {
this.#type = type;
}
}
get timestamp() { return this.#timestamp; }
set timestamp(timestamp) {
if(this.API.checkValidFormat(timestamp, regex.CMITime)) {
this.#timestamp = timestamp;
}
}
get weighting() { return this.#weighting; }
set weighting(weighting) {
if (this.API.checkValidFormat(weighting, regex.CMIDecimal)) {
this.#weighting = weighting;
}
}
get learner_response() { return this.#learner_response; }
set learner_response(learner_response) {
if(typeof this.type === 'undefined') {
this.API.throwSCORMError(this.API.error.DEPENDENCY_NOT_ESTABLISHED);
} else {
let nodes = [];
let response_type = learner_responses[this.type];
if(response_type.delimiter !== '') {
nodes = learner_response.split(response_type.delimiter);
} else {
nodes[0] = learner_response;
}
if((nodes.length > 0) && (nodes.length <= response_type.max)) {
const formatRegex = new RegExp(response_type.format);
for(let i = 0; (i < nodes.length) && (this.API.lastErrorCode === 0); i++) {
if(typeof response_type.delimiter2 !== 'undefined') {
let values = nodes[i].split(response_type.delimiter2);
if(values.length === 2) {
if(!values[0].match(formatRegex)) {
this.API.throwSCORMError(this.API.error.TYPE_MISMATCH);
} else {
if(!values[1].match(new RegExp(response_type.format2))) {
this.API.throwSCORMError(this.API.error.TYPE_MISMATCH);
}
}
} else {
this.API.throwSCORMError(this.API.error.TYPE_MISMATCH);
}
} else {
if(!nodes[i].match(formatRegex)) {
this.API.throwSCORMError(this.API.error.TYPE_MISMATCH);
} else {
if(nodes[i] !== '' && response_type.unique) {
for(let j = 0; (j < i) && this.API.lastErrorCode === 0; j++) {
if(nodes[i] === nodes[j]) {
this.API.throwSCORMError(this.API.error.TYPE_MISMATCH);
}
}
}
}
}
}
} else {
this.API.throwSCORMError(this.API.error.GENERAL_SET_FAILURE);
}
}
}
get result() { return this.#result; }
set result(result) {
if(this.API.checkValidFormat(result, regex.CMIResult)) {
this.#result = result;
}
}
get latency() { return this.#latency; }
set latency(latency) {
if(this.API.checkValidFormat(latency, regex.CMITimespan)) {
this.#latency = latency;
}
}
get description() { return this.#description; }
set description(description) {
if(this.API.checkValidFormat(description, regex.CMILangString250)) {
this.#description = description;
}
}
}
export class CMIObjectivesObject extends BaseCMI {
constructor(API) {
super(API);
}
#id: "";
#success_status: "unknown"; // Allowed values: "passed", "failed", "unknown"
#completion_status: "unknown"; // Allowed values: "completed", "incomplete", "not attempted", "unknown"
#progress_measure: ""; // Data type: real (10,7). Range: 0.0 to 1.0
#description: ""; // SPM 250 characters
get id() { return this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) {
this.#id = id;
}
}
get success_status() { return this.#success_status; }
set success_status(success_status) {
if(this.API.checkValidFormat(success_status, regex.CMISStatus)) {
this.#success_status = success_status;
}
}
get completion_status() { return this.#completion_status; }
set completion_status(completion_status) {
if(this.API.checkValidFormat(completion_status, regex.CMICStatus)) {
this.#completion_status = completion_status;
}
}
get progress_measure() { return this.#progress_measure; }
set progress_measure(progress_measure) {
if(this.API.checkValidFormat(progress_measure, regex.CMIDecimal)
&& this.API.checkValidRange(progress_measure, regex.progress_range)) {
this.#progress_measure = progress_measure;
}
}
get description() { return this.#description; }
set description(description) {
if(this.API.checkValidFormat(description, regex.CMILangString250)) {
this.#description = description;
}
}
score = new Scorm2004CMIScore(this.API);
}
class Scorm2004CMIScore extends CMIScore {
constructor(API) {
super(API, constants.score_children);
this.max = "";
}
#scaled: "";
get scaled() { return this.#scaled; }
set scaled(scaled) {
if(this.API.checkValidFormat(scaled, regex.CMIDecimal)
&& this.API.checkValidRange(scaled, regex.scaled_range)) {
this.#scaled = scaled;
}
}
}
export class CMICommentsFromLearnerObject extends BaseCMI {
constructor(API) {
super(API);
}
#comment = "";
#location = "";
#timestamp = "";
get comment() { return this.#comment; }
set comment(comment) {
if(this.API.checkValidFormat(comment, regex.CMILangString4000)) {
this.#comment = comment;
}
}
get location() { return this.#location; }
set location(location) {
if(this.API.checkValidFormat(location, regex.CMIString250)) {
this.#location = location;
}
}
get timestamp() { return this.#timestamp; }
set timestamp(timestamp) {
if(this.API.checkValidFormat(timestamp, regex.CMITime)) {
this.#timestamp = timestamp;
}
}
}
export class CMICommentsFromLMSObject extends CMICommentsFromLearnerObject {
constructor(API) {
super(API);
}
set comment(comment) { this.API.isNotInitialized() ? this.comment = comment : this.throwReadOnlyError(); }
set location(location) { this.API.isNotInitialized() ? this.location = location : this.throwReadOnlyError(); }
set timestamp(timestamp) { this.API.isNotInitialized() ? this.timestamp = timestamp : this.throwReadOnlyError(); }
}
export class CMIInteractionsObjectivesObject extends BaseCMI {
constructor(API) {
super(API);
}
#id: "";
get id() { return this.#id; }
set id(id) {
if(this.API.checkValidFormat(id, regex.CMILongIdentifier)) {
this.#id = id;
}
}
}
export class CMIInteractionsCorrectResponsesObject extends BaseCMI {
constructor(API) {
super(API);
}
#pattern: "";
get pattern() { return this.#pattern; }
set pattern(pattern) {
if(this.API.checkValidFormat(pattern, regex.CMIFeedback)) {
this.#pattern = pattern;
}
}
}
export class ADL extends BaseCMI {
constructor(API) {
super(API);
}
nav = new class extends BaseCMI {
constructor(API) {
super(API);
}
#request = "_none_"; // Allowed values: "continue", "previous", "choice", "jump", "exit", "exitAll", "abandon", "abandonAll", "_none_"
get request() { return this.#request; }
set request(request) {
if(this.API.checkValidFormat(request, regex.NAVEvent)) {
this.#request = request;
}
}
request_valid = new class extends BaseCMI {
constructor(API) {
super(API);
}
#continue = "unknown"; // Allowed values: "true", "false", "unknown"
#previous = "unknown"; // Allowed values: "true", "false", "unknown"
get continue() { return this.#continue; }
set continue(_) { API.throwSCORMError(404); }
get previous() { return this.#previous; }
set previous(_) { API.throwSCORMError(404); }
choice = class {
_isTargetValid = (_target) => "unknown";
};
jump = class {
_isTargetValid = (_target) => "unknown";
};
}
};
}

814
src/constants.js Normal file
View File

@@ -0,0 +1,814 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
import {scorm2004_regex} from "./regex";
export const base_error_codes = {
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
};
export const scorm12_error_codes = {
...base_error_codes, ...{
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
}
};
export const scorm2004_error_codes = {
...base_error_codes, ...{
INITIALIZATION_FAILED: 102,
INITIALIZED: 103,
TERMINATED: 104,
TERMINATION_FAILURE: 111,
TERMINATION_BEFORE_INIT: 112,
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
}
};
export 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',
comments_children: 'content,location,time',
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: {
"101": {
basicMessage: "General Exception",
detailMessage: "No specific error code exists to describe the error. Use LMSGetDiagnostic for more information",
},
"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.",
}
}
};
export const aicc_constants = {
...scorm12_constants, ...{
cmi_children: 'core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions,evaluation',
student_data_children: 'attempt_number,tries,mastery_score,max_time_allowed,time_limit_action',
tries_children: "time,status,score"
}
};
export const scorm2004_constants = {
// Children lists
cmi_children: '_version,comments_from_learner,comments_from_lms,completion_status,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_data_children: 'mastery_score,max_time_allowed,time_limit_action',
student_preference_children: 'audio_level,audio_captioning,delivery_speed,language',
interactions_children: 'id,type,objectives,timestamp,correct_responses,weighting,learner_response,result,latency,description',
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. Use GetDiagnostic for more information.",
},
"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.",
}
},
valid_languages: {
'aa': 'aa',
'ab': 'ab',
'ae': 'ae',
'af': 'af',
'ak': 'ak',
'am': 'am',
'an': 'an',
'ar': 'ar',
'as': 'as',
'av': 'av',
'ay': 'ay',
'az': 'az',
'ba': 'ba',
'be': 'be',
'bg': 'bg',
'bh': 'bh',
'bi': 'bi',
'bm': 'bm',
'bn': 'bn',
'bo': 'bo',
'br': 'br',
'bs': 'bs',
'ca': 'ca',
'ce': 'ce',
'ch': 'ch',
'co': 'co',
'cr': 'cr',
'cs': 'cs',
'cu': 'cu',
'cv': 'cv',
'cy': 'cy',
'da': 'da',
'de': 'de',
'dv': 'dv',
'dz': 'dz',
'ee': 'ee',
'el': 'el',
'en': 'en',
'eo': 'eo',
'es': 'es',
'et': 'et',
'eu': 'eu',
'fa': 'fa',
'ff': 'ff',
'fi': 'fi',
'fj': 'fj',
'fo': 'fo',
'fr': 'fr',
'fy': 'fy',
'ga': 'ga',
'gd': 'gd',
'gl': 'gl',
'gn': 'gn',
'gu': 'gu',
'gv': 'gv',
'ha': 'ha',
'he': 'he',
'hi': 'hi',
'ho': 'ho',
'hr': 'hr',
'ht': 'ht',
'hu': 'hu',
'hy': 'hy',
'hz': 'hz',
'ia': 'ia',
'id': 'id',
'ie': 'ie',
'ig': 'ig',
'ii': 'ii',
'ik': 'ik',
'io': 'io',
'is': 'is',
'it': 'it',
'iu': 'iu',
'ja': 'ja',
'jv': 'jv',
'ka': 'ka',
'kg': 'kg',
'ki': 'ki',
'kj': 'kj',
'kk': 'kk',
'kl': 'kl',
'km': 'km',
'kn': 'kn',
'ko': 'ko',
'kr': 'kr',
'ks': 'ks',
'ku': 'ku',
'kv': 'kv',
'kw': 'kw',
'ky': 'ky',
'la': 'la',
'lb': 'lb',
'lg': 'lg',
'li': 'li',
'ln': 'ln',
'lo': 'lo',
'lt': 'lt',
'lu': 'lu',
'lv': 'lv',
'mg': 'mg',
'mh': 'mh',
'mi': 'mi',
'mk': 'mk',
'ml': 'ml',
'mn': 'mn',
'mo': 'mo',
'mr': 'mr',
'ms': 'ms',
'mt': 'mt',
'my': 'my',
'na': 'na',
'nb': 'nb',
'nd': 'nd',
'ne': 'ne',
'ng': 'ng',
'nl': 'nl',
'nn': 'nn',
'no': 'no',
'nr': 'nr',
'nv': 'nv',
'ny': 'ny',
'oc': 'oc',
'oj': 'oj',
'om': 'om',
'or': 'or',
'os': 'os',
'pa': 'pa',
'pi': 'pi',
'pl': 'pl',
'ps': 'ps',
'pt': 'pt',
'qu': 'qu',
'rm': 'rm',
'rn': 'rn',
'ro': 'ro',
'ru': 'ru',
'rw': 'rw',
'sa': 'sa',
'sc': 'sc',
'sd': 'sd',
'se': 'se',
'sg': 'sg',
'sh': 'sh',
'si': 'si',
'sk': 'sk',
'sl': 'sl',
'sm': 'sm',
'sn': 'sn',
'so': 'so',
'sq': 'sq',
'sr': 'sr',
'ss': 'ss',
'st': 'st',
'su': 'su',
'sv': 'sv',
'sw': 'sw',
'ta': 'ta',
'te': 'te',
'tg': 'tg',
'th': 'th',
'ti': 'ti',
'tk': 'tk',
'tl': 'tl',
'tn': 'tn',
'to': 'to',
'tr': 'tr',
'ts': 'ts',
'tt': 'tt',
'tw': 'tw',
'ty': 'ty',
'ug': 'ug',
'uk': 'uk',
'ur': 'ur',
'uz': 'uz',
've': 've',
'vi': 'vi',
'vo': 'vo',
'wa': 'wa',
'wo': 'wo',
'xh': 'xh',
'yi': 'yi',
'yo': 'yo',
'za': 'za',
'zh': 'zh',
'zu': 'zu',
'aar': 'aar',
'abk': 'abk',
'ave': 'ave',
'afr': 'afr',
'aka': 'aka',
'amh': 'amh',
'arg': 'arg',
'ara': 'ara',
'asm': 'asm',
'ava': 'ava',
'aym': 'aym',
'aze': 'aze',
'bak': 'bak',
'bel': 'bel',
'bul': 'bul',
'bih': 'bih',
'bis': 'bis',
'bam': 'bam',
'ben': 'ben',
'tib': 'tib',
'bod': 'bod',
'bre': 'bre',
'bos': 'bos',
'cat': 'cat',
'che': 'che',
'cha': 'cha',
'cos': 'cos',
'cre': 'cre',
'cze': 'cze',
'ces': 'ces',
'chu': 'chu',
'chv': 'chv',
'wel': 'wel',
'cym': 'cym',
'dan': 'dan',
'ger': 'ger',
'deu': 'deu',
'div': 'div',
'dzo': 'dzo',
'ewe': 'ewe',
'gre': 'gre',
'ell': 'ell',
'eng': 'eng',
'epo': 'epo',
'spa': 'spa',
'est': 'est',
'baq': 'baq',
'eus': 'eus',
'per': 'per',
'fas': 'fas',
'ful': 'ful',
'fin': 'fin',
'fij': 'fij',
'fao': 'fao',
'fre': 'fre',
'fra': 'fra',
'fry': 'fry',
'gle': 'gle',
'gla': 'gla',
'glg': 'glg',
'grn': 'grn',
'guj': 'guj',
'glv': 'glv',
'hau': 'hau',
'heb': 'heb',
'hin': 'hin',
'hmo': 'hmo',
'hrv': 'hrv',
'hat': 'hat',
'hun': 'hun',
'arm': 'arm',
'hye': 'hye',
'her': 'her',
'ina': 'ina',
'ind': 'ind',
'ile': 'ile',
'ibo': 'ibo',
'iii': 'iii',
'ipk': 'ipk',
'ido': 'ido',
'ice': 'ice',
'isl': 'isl',
'ita': 'ita',
'iku': 'iku',
'jpn': 'jpn',
'jav': 'jav',
'geo': 'geo',
'kat': 'kat',
'kon': 'kon',
'kik': 'kik',
'kua': 'kua',
'kaz': 'kaz',
'kal': 'kal',
'khm': 'khm',
'kan': 'kan',
'kor': 'kor',
'kau': 'kau',
'kas': 'kas',
'kur': 'kur',
'kom': 'kom',
'cor': 'cor',
'kir': 'kir',
'lat': 'lat',
'ltz': 'ltz',
'lug': 'lug',
'lim': 'lim',
'lin': 'lin',
'lao': 'lao',
'lit': 'lit',
'lub': 'lub',
'lav': 'lav',
'mlg': 'mlg',
'mah': 'mah',
'mao': 'mao',
'mri': 'mri',
'mac': 'mac',
'mkd': 'mkd',
'mal': 'mal',
'mon': 'mon',
'mol': 'mol',
'mar': 'mar',
'may': 'may',
'msa': 'msa',
'mlt': 'mlt',
'bur': 'bur',
'mya': 'mya',
'nau': 'nau',
'nob': 'nob',
'nde': 'nde',
'nep': 'nep',
'ndo': 'ndo',
'dut': 'dut',
'nld': 'nld',
'nno': 'nno',
'nor': 'nor',
'nbl': 'nbl',
'nav': 'nav',
'nya': 'nya',
'oci': 'oci',
'oji': 'oji',
'orm': 'orm',
'ori': 'ori',
'oss': 'oss',
'pan': 'pan',
'pli': 'pli',
'pol': 'pol',
'pus': 'pus',
'por': 'por',
'que': 'que',
'roh': 'roh',
'run': 'run',
'rum': 'rum',
'ron': 'ron',
'rus': 'rus',
'kin': 'kin',
'san': 'san',
'srd': 'srd',
'snd': 'snd',
'sme': 'sme',
'sag': 'sag',
'slo': 'slo',
'sin': 'sin',
'slk': 'slk',
'slv': 'slv',
'smo': 'smo',
'sna': 'sna',
'som': 'som',
'alb': 'alb',
'sqi': 'sqi',
'srp': 'srp',
'ssw': 'ssw',
'sot': 'sot',
'sun': 'sun',
'swe': 'swe',
'swa': 'swa',
'tam': 'tam',
'tel': 'tel',
'tgk': 'tgk',
'tha': 'tha',
'tir': 'tir',
'tuk': 'tuk',
'tgl': 'tgl',
'tsn': 'tsn',
'ton': 'ton',
'tur': 'tur',
'tso': 'tso',
'tat': 'tat',
'twi': 'twi',
'tah': 'tah',
'uig': 'uig',
'ukr': 'ukr',
'urd': 'urd',
'uzb': 'uzb',
'ven': 'ven',
'vie': 'vie',
'vol': 'vol',
'wln': 'wln',
'wol': 'wol',
'xho': 'xho',
'yid': 'yid',
'yor': 'yor',
'zha': 'zha',
'chi': 'chi',
'zho': 'zho',
'zul': 'zul'
}
};
export const learner_responses = {
'true-false': {
format: '^true$|^false$',
max: 1,
delimiter: '',
unique: false
},
'choice': {
format: scorm2004_regex.CMIShortIdentifier,
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: '^$|' + scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIDecimal + '|^$|' + scorm2004_regex.CMIShortIdentifier,
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
}
};
export const correct_responses = {
'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.CMIShortIdentifier
},
'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,
format: '^$|' + scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIDecimal + '|^$|' + scorm2004_regex.CMIShortIdentifier
},
'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
}
};

76
src/regex.js Normal file
View File

@@ -0,0 +1,76 @@
// @flow
export const scorm12_regex = {
CMIString256: '^.{0,255}$',
CMIString4096: '^.{0,4096}$',
CMITime: '^([0-2]{1}[0-9]{1}):([0-5]{1}[0-9]{1}):([0-5]{1}[0-9]{1})(\.[0-9]{1,6})?$',
CMITimespan: '^([0-9]{2,4}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,2})?$',
CMIInteger: '^\\d+$',
CMISInteger: '^-?([0-9]+)$',
CMIDecimal: '^-?([0-9]{0,3})(\.[0-9]*)?$',
CMIIdentifier: '^[\\u0021-\\u007E]{0,255}$',
CMIFeedback: '^.{0,255}$', // This must be redefined
CMIIndex: '[._](\\d+).',
// Vocabulary Data Type Definition
CMIStatus: '^passed$|^completed$|^failed$|^incomplete$|^browsed$',
CMIStatus2: '^passed$|^completed$|^failed$|^incomplete$|^browsed$|^not attempted$',
CMIExit: '^time-out$|^suspend$|^logout$|^$',
CMIType: '^true-false$|^choice$|^fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$',
CMIResult: '^correct$|^wrong$|^unanticipated$|^neutral$|^([0-9]{0,3})?(\.[0-9]*)?$',
NAVEvent: '^previous$|^continue$',
// Data ranges
score_range: '0#100',
audio_range: '-1#100',
speed_range: '-100#100',
weighting_range: '-100#100',
text_range: '-1#1',
};
export const aicc_regex = {
...scorm12_regex, ...{
CMIIdentifier: '^\\w{1,255}$',
}
};
export const scorm2004_regex = {
CMIString200: '^[\\u0000-\\uFFFF]{0,200}$',
CMIString250: '^[\\u0000-\\uFFFF]{0,250}$',
CMIString1000: '^[\\u0000-\\uFFFF]{0,1000}$',
CMIString4000: '^[\\u0000-\\uFFFF]{0,4000}$',
CMIString64000: '^[\\u0000-\\uFFFF]{0,64000}$',
CMILang: '^([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?$|^$',
CMILangString250: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,250}$)?',
CMILangcr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\}))(.*?)$',
CMILangString250cr: '^((\{lang=([a-zA-Z]{2,3}|i|x)?(\-[a-zA-Z0-9\-]{2,8})?\})?(.{0,250})?)?$',
CMILangString4000: '^(\{lang=([a-zA-Z]{2,3}|i|x)(\-[a-zA-Z0-9\-]{2,8})?\})?([^\{].{0,4000}$)?',
CMITime: '^(19[7-9]{1}[0-9]{1}|20[0-2]{1}[0-9]{1}|203[0-8]{1})((-(0[1-9]{1}|1[0-2]{1}))((-(0[1-9]{1}|[1-2]{1}[0-9]{1}|3[0-1]{1}))(T([0-1]{1}[0-9]{1}|2[0-3]{1})((:[0-5]{1}[0-9]{1})((:[0-5]{1}[0-9]{1})((\\.[0-9]{1,2})((Z|([+|-]([0-1]{1}[0-9]{1}|2[0-3]{1})))(:[0-5]{1}[0-9]{1})?)?)?)?)?)?)?)?$',
CMITimespan: '^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:([.,\\d]+)S)?)?$',
CMIInteger: '^\\d+$',
CMISInteger: '^-?([0-9]+)$',
CMIDecimal: '^-?([0-9]{1,5})(\\.[0-9]{1,18})?$',
CMIIdentifier: '^\\S{1,250}[a-zA-Z0-9]$',
CMIShortIdentifier: '^[\\w\.]{1,250}$',
CMILongIdentifier: '^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000})$',
CMIFeedback: '^.*$', // This must be redefined
CMIIndex: '[._](\\d+).',
CMIIndexStore: '.N(\\d+).',
// Vocabulary Data Type Definition
CMICStatus: '^completed$|^incomplete$|^not attempted$|^unknown$',
CMISStatus: '^passed$|^failed$|^unknown$',
CMIExit: '^time-out$|^suspend$|^logout$|^normal$|^$',
CMIType: '^true-false$|^choice$|^(long-)?fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$|^other$',
CMIResult: '^correct$|^incorrect$|^unanticipated$|^neutral$|^-?([0-9]{1,4})(\\.[0-9]{1,18})?$',
NAVEvent: '^previous$|^continue$|^exit$|^exitAll$|^abandon$|^abandonAll$|^suspendAll$|^\{target=\\S{0,200}[a-zA-Z0-9]\}choice|jump$',
NAVBoolean: '^unknown$|^true$|^false$',
NAVTarget: '^previous$|^continue$|^choice.{target=\\S{0,200}[a-zA-Z0-9]}$',
// Data ranges
scaled_range: '-1#1',
audio_range: '0#*',
speed_range: '0#*',
text_range: '-1#1',
progress_range: '0#1',
};

116
src/utilities.js Normal file
View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) Noverant, Inc - All Rights Reserved Unauthorized copying of this file, via any
* medium is strictly prohibited Proprietary and confidential
*/
// @flow
export const SECONDS_PER_SECOND = 1;
export const SECONDS_PER_MINUTE = 60;
export const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
export 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],
];
/**
* Converts a Number to a String of HH:MM:SS
*
* @param totalSeconds
* @returns {string}
*/
export function getSecondsAsHHMMSS(totalSeconds: Number) {
// SCORM spec does not deal with negative durations, give zero back
if(!totalSeconds || totalSeconds <= 0) {
return '00:00:00';
}
let hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
let dateObj = new Date(totalSeconds * 1000);
let minutes = dateObj.getUTCMinutes();
let seconds = dateObj.getSeconds();
return hours.toString().padStart(2, '0') + ':' +
minutes.toString().padStart(2, '0') + ':' +
seconds.toString().padStart(2, '0');
}
/**
* Calculate the number of seconds from ISO 8601 Duration
*
* @param seconds
* @returns {String}
*/
export function getSecondsAsISODuration(seconds: Number) {
// SCORM spec does not deal with negative durations, give zero back
if(!seconds || seconds <= 0) {
return 'P0S';
}
let duration = 'P';
let remainder = seconds;
designations.forEach(([sign, current_seconds]) => {
const value = Math.floor(remainder / current_seconds);
remainder = remainder % current_seconds;
if (value) {
duration += `${value}${sign}`;
}
});
return duration;
}
/**
* Calculate the number of seconds from HH:MM:SS.DDDDDD
*
* @param timeString
* @param timeRegex
* @returns {number}
*/
export function getTimeAsSeconds(timeString: String, timeRegex: RegExp) {
if(!timeString || typeof timeString !== 'string' || !timeString.match(timeRegex)) {
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;
}
/**
* Calculate the number of seconds from ISO 8601 Duration
*
* @param duration
* @param durationRegex
* @returns {number}
*/
export function getDurationAsSeconds(duration: String, durationRegex: RegExp) {
if(!duration || !duration.match(durationRegex)) {
return 0;
}
let [,years,months,,days,hours,minutes,seconds] = new RegExp(durationRegex).exec(duration) || [];
let now = new Date();
let anchor = new Date(now);
anchor.setFullYear(anchor.getFullYear() + Number(years || 0));
anchor.setMonth(anchor.getMonth() + Number(months || 0));
anchor.setDate(anchor.getDate() + Number(days || 0));
anchor.setHours(anchor.getHours() + Number(hours || 0));
anchor.setMinutes(anchor.getMinutes() + Number(minutes || 0));
anchor.setSeconds(anchor.getSeconds() + Number(seconds || 0));
if(seconds) {
let milliseconds = Number(Number(seconds) % 1).toFixed(6) * 1000.0;
anchor.setMilliseconds(anchor.getMilliseconds() + milliseconds);
}
return (anchor - now) / 1000.0;
}

282
test/Scorm12API.spec.js Normal file
View File

@@ -0,0 +1,282 @@
import { expect, assert } from 'chai';
import {describe, it, before, beforeEach, after, afterEach, } from "mocha";
import Scorm12API from "../src/Scorm12API";
import {scorm12_constants, scorm12_error_codes} from "../src/constants";
let API;
const checkFieldConstraintSize = (fieldName: String, limit: Number, expectedValue: String = '') => {
describe(`Field: ${fieldName}`, () => {
it(`Should be able to read from ${fieldName}`, () => {
expect(eval(`API.${fieldName}`)).to.equal(expectedValue);
});
it(`Should be able to write upto ${limit} characters to ${fieldName}`, () => {
eval(`API.${fieldName} = 'x'.repeat(${limit})`);
expect(0).to.equal(API.lastErrorCode);
});
it(`Should fail to write more than ${limit} characters to ${fieldName}`, () => {
eval(`API.${fieldName} = 'x'.repeat(${limit + 1})`);
expect(scorm12_error_codes.TYPE_MISMATCH + '').to.equal(API.lastErrorCode);
});
});
};
const checkInvalidSet = (fieldName: String, expectedValue: String = '') => {
describe(`Field: ${fieldName}`, () => {
it(`Should be able to read from ${fieldName}`, () => {
expect(eval(`API.${fieldName}`)).to.equal(expectedValue);
});
it(`Should fail to write to ${fieldName}`, () => {
eval(`API.${fieldName} = 'xxx'`);
expect(API.lastErrorCode).to.equal(scorm12_error_codes.INVALID_SET_VALUE + '');
});
});
};
const checkReadOnly = (fieldName: String, expectedValue: String = '') => {
describe(`Field: ${fieldName}`, () => {
it(`Should be able to read from ${fieldName}`, () => {
expect(eval(`API.${fieldName}`)).to.equal(expectedValue);
});
it(`Should fail to write to ${fieldName}`, () => {
eval(`API.${fieldName} = 'xxx'`);
expect(API.lastErrorCode).to.equal(scorm12_error_codes.READ_ONLY_ELEMENT + '');
});
});
};
const checkRead = (fieldName: String, expectedValue: String = '') => {
describe(`Field: ${fieldName}`, () => {
it(`Should be able to read from ${fieldName}`, () => {
expect(eval(`API.${fieldName}`)).to.equal(expectedValue);
});
});
};
const checkWriteOnly = (fieldName: String, valueToTest: String = 'xxx') => {
describe(`Field: ${fieldName}`, () => {
it(`Should fail to read from ${fieldName}`, () => {
eval(`API.${fieldName}`);
expect(API.lastErrorCode).to.equal(scorm12_error_codes.WRITE_ONLY_ELEMENT + '');
});
it(`Should successfully write to ${fieldName}`, () => {
eval(`API.${fieldName} = '${valueToTest}'`);
expect(API.lastErrorCode).to.equal(0);
});
});
};
const checkWrite = (fieldName: String, valueToTest: String = 'xxx') => {
describe(`Field: ${fieldName}`, () => {
it(`Should successfully write to ${fieldName}`, () => {
eval(`API.${fieldName} = '${valueToTest}'`);
expect(API.lastErrorCode).to.equal(0);
});
});
};
const checkValidValues = (fieldName: String, expectedError: Number, validValues: Array, invalidValues: Array) => {
describe(`Field: ${fieldName}`, () => {
for(let idx in validValues) {
it(`Should successfully write '${validValues[idx]}' to ${fieldName}`, () => {
eval(`API.${fieldName} = '${validValues[idx]}'`);
expect(API.lastErrorCode).to.equal(0);
});
}
for(let idx in invalidValues) {
it(`Should fail to write '${invalidValues[idx]}' to ${fieldName}`, () => {
eval(`API.${fieldName} = '${invalidValues[idx]}'`);
expect(API.lastErrorCode).to.equal(expectedError + '');
});
}
});
};
describe('SCORM 1.2 API Tests', () => {
describe('CMI Spec Tests', () => {
describe('Post-LMSInitialize Tests', () => {
beforeEach('Create the API object', () => {
API = new Scorm12API();
API.LMSInitialize();
});
afterEach('Destroy API object', () => {
API = null;
});
it('LMSInitialize should create CMI object', () => {
assert(API.cmi !== undefined, 'CMI object is created');
});
it('Exporting CMI to JSON produces proper Object', () => {
expect(
JSON.parse(API.renderCMIToJSON())
).to.hasOwnProperty('core')
});
/**
* Base CMI Properties
*/
checkInvalidSet('cmi._version', '3.4');
checkInvalidSet('cmi._children', scorm12_constants.cmi_children);
checkFieldConstraintSize('cmi.suspend_data', 4096);
checkReadOnly('cmi.launch_data');
checkFieldConstraintSize('cmi.comments', 4096);
checkReadOnly('cmi.comments_from_lms');
/**
* cmi.core Properties
*/
checkInvalidSet('cmi.core._children', scorm12_constants.core_children);
checkReadOnly('cmi.core.student_id');
checkReadOnly('cmi.core.student_name');
checkFieldConstraintSize('cmi.core.lesson_location', 255);
checkReadOnly('cmi.core.credit');
checkRead('cmi.core.lesson_status');
checkValidValues('cmi.core.lesson_status', scorm12_error_codes.TYPE_MISMATCH, [
'passed',
'completed',
'failed',
'incomplete',
'browsed'
], [
'Passed',
'P',
'F',
'p',
'true',
'false',
'complete'
]);
checkReadOnly('cmi.core.entry');
checkReadOnly('cmi.core.total_time');
checkReadOnly('cmi.core.lesson_mode', 'normal');
checkWrite('cmi.core.exit', 'suspend');
checkValidValues('cmi.core.exit', scorm12_error_codes.TYPE_MISMATCH, [
'time-out',
'suspend',
'logout',
], [
'complete',
'exit'
]);
checkWriteOnly('cmi.core.session_time', '00:00:00');
checkValidValues('cmi.core.session_time', scorm12_error_codes.TYPE_MISMATCH, [
'10:06:57',
'00:00:01.56',
'23:59:59',
'47:59:59',
], [
'06:5:13',
'23:59:59.123',
'P1DT23H59M59S'
]);
/**
* cmi.core.score Properties
*/
checkInvalidSet('cmi.core.score._children', scorm12_constants.score_children);
checkValidValues('cmi.core.score.raw', scorm12_error_codes.VALUE_OUT_OF_RANGE, [
'0',
'25.1',
'50.5',
'75',
'100',
], [
'-1',
'101'
]);
checkValidValues('cmi.core.score.min', scorm12_error_codes.VALUE_OUT_OF_RANGE, [
'0',
'25.1',
'50.5',
'75',
'100',
], [
'-1',
'101'
]);
checkValidValues('cmi.core.score.max', scorm12_error_codes.VALUE_OUT_OF_RANGE, [
'0',
'25.1',
'50.5',
'75',
'100',
], [
'-1',
'101'
]);
/**
* cmi.objectives Properties
*/
checkInvalidSet('cmi.objectives._children', scorm12_constants.objectives_children);
checkInvalidSet('cmi.objectives._count', 0);
/**
* cmi.student_data Properties
*/
checkInvalidSet('cmi.student_data._children', scorm12_constants.student_data_children);
checkReadOnly('cmi.student_data.mastery_score');
checkReadOnly('cmi.student_data.max_time_allowed');
checkReadOnly('cmi.student_data.time_limit_action');
/**
* cmi.student_preference Properties
*/
checkInvalidSet('cmi.student_preference._children', scorm12_constants.student_preference_children);
checkValidValues('cmi.student_preference.audio', scorm12_error_codes.TYPE_MISMATCH, [
'1',
'-1',
'50',
'100',
], [
'invalid',
'a100'
]);
checkValidValues('cmi.student_preference.audio', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [
'101',
'5000000',
'-500'
]);
checkFieldConstraintSize('cmi.student_preference.language', 255);
checkValidValues('cmi.student_preference.speed', scorm12_error_codes.TYPE_MISMATCH, [
'1',
'-100',
'50',
'100',
], [
'invalid',
'a100'
]);
checkValidValues('cmi.student_preference.speed', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [
'101',
'-101',
'5000000',
'-500'
]);
checkValidValues('cmi.student_preference.text', scorm12_error_codes.TYPE_MISMATCH, [
'1',
'-1'
], [
'invalid',
'a100'
]);
checkValidValues('cmi.student_preference.text', scorm12_error_codes.VALUE_OUT_OF_RANGE, [], [
'2',
'-2'
]);
/**
* cmi.interactions Properties
*/
checkInvalidSet('cmi.interactions._children', scorm12_constants.interactions_children);
checkInvalidSet('cmi.interactions._count', 0);
});
});
});

220
test/utilities.spec.js Normal file
View File

@@ -0,0 +1,220 @@
import { expect } from 'chai';
import {describe, it} from "mocha";
import * as Utilities from '../src/utilities';
import {scorm12_regex, scorm2004_regex} from "../src/regex";
describe('Utility Tests', () => {
describe('function getSecondsAsHHMMSS()', () => {
it('10 returns 00:00:10', () => {
expect(
Utilities.getSecondsAsHHMMSS(10)
).to.equal('00:00:10');
});
it('60 returns 00:01:00', () => {
expect(
Utilities.getSecondsAsHHMMSS(60)
).to.equal('00:01:00');
});
it('3600 returns 01:00:00', () => {
expect(
Utilities.getSecondsAsHHMMSS(3600)
).to.equal('01:00:00');
});
it('70 returns 00:01:10', () => {
expect(
Utilities.getSecondsAsHHMMSS(70)
).to.equal('00:01:10');
});
it('3670 returns 01:01:10', () => {
expect(
Utilities.getSecondsAsHHMMSS(3670)
).to.equal('01:01:10');
});
it('90000 returns 25:00:00, check for hours greater than 24', () => {
expect(
Utilities.getSecondsAsHHMMSS(90000)
).to.equal('25:00:00');
});
it('-3600 returns 00:00:00, negative time not allowed in SCORM session times', () => {
expect(
Utilities.getSecondsAsHHMMSS(-3600)
).to.equal('00:00:00');
});
it('Empty seconds returns 00:00:00', () => {
expect(
Utilities.getSecondsAsHHMMSS(null)
).to.equal('00:00:00');
});
});
describe('function getSecondsAsISODuration()', () => {
it('10 returns P10S', () => {
expect(
Utilities.getSecondsAsISODuration(10)
).to.equal('P10S');
});
it('60 returns P1M', () => {
expect(
Utilities.getSecondsAsISODuration(60)
).to.equal('P1M');
});
it('3600 returns P1H', () => {
expect(
Utilities.getSecondsAsISODuration(3600)
).to.equal('P1H');
});
it('70 returns P1M10S', () => {
expect(
Utilities.getSecondsAsISODuration(70)
).to.equal('P1M10S');
});
it('3670 returns P1H1M10S', () => {
expect(
Utilities.getSecondsAsISODuration(3670)
).to.equal('P1H1M10S');
});
it('90000 returns P1D1H', () => {
expect(
Utilities.getSecondsAsISODuration(90000)
).to.equal('P1D1H');
});
it('90061 returns P1D1H1M1S', () => {
expect(
Utilities.getSecondsAsISODuration(90061)
).to.equal('P1D1H1M1S');
});
it('-3600 returns P0S, negative time not allowed in SCORM session times', () => {
expect(
Utilities.getSecondsAsISODuration(-3600)
).to.equal('P0S');
});
it('Empty seconds returns P0S', () => {
expect(
Utilities.getSecondsAsISODuration(null)
).to.equal('P0S');
});
});
describe('function getTimeAsSeconds()', () => {
it('00:00:10 returns 10', () => {
expect(
Utilities.getTimeAsSeconds('00:00:10', scorm12_regex.CMITimespan)
).to.equal(10);
});
it('00:01:10 returns 70', () => {
expect(
Utilities.getTimeAsSeconds('00:01:10', scorm12_regex.CMITimespan)
).to.equal(70);
});
it('01:01:10 returns 3670', () => {
expect(
Utilities.getTimeAsSeconds('01:01:10', scorm12_regex.CMITimespan)
).to.equal(3670);
});
it('100:00:00 returns 3670', () => {
expect(
Utilities.getTimeAsSeconds('100:00:00', scorm12_regex.CMITimespan)
).to.equal(360000);
});
it('-01:00:00 returns 0', () => {
expect(
Utilities.getTimeAsSeconds('-01:00:00', scorm12_regex.CMITimespan)
).to.equal(0);
});
it('Number value returns 0', () => {
expect(
Utilities.getTimeAsSeconds(999, scorm12_regex.CMITimespan)
).to.equal(0);
});
it('boolean value returns 0', () => {
expect(
Utilities.getTimeAsSeconds(true, scorm12_regex.CMITimespan)
).to.equal(0);
});
it('Empty value returns 0', () => {
expect(
Utilities.getTimeAsSeconds(null, scorm12_regex.CMITimespan)
).to.equal(0);
});
});
describe('function getDurationAsSeconds()', () => {
it('P0S returns 0', () => {
expect(
Utilities.getDurationAsSeconds('P0S', scorm2004_regex.CMITimespan)
).to.equal(0);
});
it('P70S returns 70', () => {
expect(
Utilities.getDurationAsSeconds('P70S', scorm2004_regex.CMITimespan)
).to.equal(70);
});
it('PT1M10S returns 70', () => {
expect(
Utilities.getDurationAsSeconds('PT1M10S', scorm2004_regex.CMITimespan)
).to.equal(70);
});
it('P1D returns 86400', () => {
expect(
Utilities.getDurationAsSeconds('P1D', scorm2004_regex.CMITimespan)
).to.equal(86400);
});
it('P1M returns number of seconds for one month from now', () => {
const now = new Date();
let oneMonthFromNow = new Date(now);
oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1);
expect(
Utilities.getDurationAsSeconds('P1M', scorm2004_regex.CMITimespan)
).to.equal((oneMonthFromNow - now) / 1000.0);
});
it('P1Y returns number of seconds for one year from now', () => {
const now = new Date();
let oneYearFromNow = new Date(now);
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
expect(
Utilities.getDurationAsSeconds('P1Y', scorm2004_regex.CMITimespan)
).to.equal((oneYearFromNow - now) / 1000.0);
});
it('Invalid duration returns 0', () => {
expect(
Utilities.getDurationAsSeconds('T1M10S', scorm2004_regex.CMITimespan)
).to.equal(0);
});
it('Empty duration returns 0', () => {
expect(
Utilities.getDurationAsSeconds(null, scorm2004_regex.CMITimespan)
).to.equal(0);
});
});
});