scorm-again
Version:
A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004
1,316 lines (1,302 loc) • 919 kB
JavaScript
(function () {
'use strict';
const SECONDS_PER_SECOND = 1;
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
const designations = {
D: SECONDS_PER_DAY,
H: SECONDS_PER_HOUR,
M: SECONDS_PER_MINUTE,
S: SECONDS_PER_SECOND
};
const getSecondsAsHHMMSS = totalSeconds => {
if (!totalSeconds || totalSeconds <= 0) {
return "00:00:00";
}
const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
const dateObj = new Date(totalSeconds * 1e3);
const minutes = dateObj.getUTCMinutes();
const seconds = dateObj.getSeconds();
const ms = totalSeconds % 1;
let msStr = "";
if (countDecimals(ms) > 0) {
if (countDecimals(ms) > 2) {
msStr = ms.toFixed(2);
} else {
msStr = String(ms);
}
msStr = "." + msStr.split(".")[1];
}
return (hours + ":" + minutes + ":" + seconds).replace(/\b\d\b/g, "0$&") + msStr;
};
const getSecondsAsISODuration = seconds => {
if (!seconds || seconds <= 0) {
return "PT0S";
}
let duration = "P";
let remainder = seconds;
const designationEntries = Object.entries(designations);
designationEntries.forEach(_ref => {
let [designationsKey, current_seconds] = _ref;
let value = Math.floor(remainder / current_seconds);
remainder = remainder % current_seconds;
if (countDecimals(remainder) > 2) {
remainder = Number(Number(remainder).toFixed(2));
}
if (designationsKey === "S" && remainder > 0) {
value += remainder;
}
if (value) {
const needsTimeSeparator = (duration.indexOf("D") > 0 || ["H", "M", "S"].includes(designationsKey)) && duration.indexOf("T") === -1;
if (needsTimeSeparator) {
duration += "T";
}
duration += `${value}${designationsKey}`;
}
});
return duration;
};
const getTimeAsSeconds = memoize((timeString, timeRegex) => {
if (typeof timeString === "number" || typeof timeString === "boolean") {
timeString = String(timeString);
}
if (typeof timeRegex === "string") {
timeRegex = new RegExp(timeRegex);
}
if (!timeString) {
return 0;
}
if (!timeString.match(timeRegex)) {
if (/^\d+(?:\.\d+)?$/.test(timeString)) {
return Number(timeString);
}
return 0;
}
const parts = timeString.split(":");
const hours = Number(parts[0]);
const minutes = Number(parts[1]);
const seconds = Number(parts[2]);
return hours * 3600 + minutes * 60 + seconds;
},
// Custom key function to handle RegExp objects which can't be stringified
(timeString, timeRegex) => {
const timeStr = typeof timeString === "string" ? timeString : String(timeString ?? "");
const regexStr = typeof timeRegex === "string" ? timeRegex : timeRegex?.toString() ?? "";
return `${timeStr}:${regexStr}`;
});
const getDurationAsSeconds = memoize((duration, durationRegex) => {
if (typeof durationRegex === "string") {
durationRegex = new RegExp(durationRegex);
}
if (!duration || !duration?.match?.(durationRegex)) {
return 0;
}
const [, years, months, weeks, days, hours, minutes, seconds] = new RegExp(durationRegex).exec?.(duration) ?? [];
let result = 0;
result += Number(seconds) || 0;
result += Number(minutes) * 60 || 0;
result += Number(hours) * 3600 || 0;
result += Number(days) * (60 * 60 * 24) || 0;
result += Number(weeks) * (60 * 60 * 24 * 7) || 0;
result += Number(months) * (60 * 60 * 24 * 30) || 0;
result += Number(years) * (60 * 60 * 24 * 365) || 0;
return result;
},
// Custom key function to handle RegExp objects which can't be stringified
(duration, durationRegex) => {
const durationStr = duration ?? "";
const regexStr = typeof durationRegex === "string" ? durationRegex : durationRegex?.toString() ?? "";
return `${durationStr}:${regexStr}`;
});
const validateISO8601Duration = memoize((duration, durationRegex) => {
if (typeof durationRegex === "string") {
durationRegex = new RegExp(durationRegex);
}
return !(!duration || !duration?.match?.(durationRegex));
});
function addTwoDurations(first, second, durationRegex) {
const regex = new RegExp(durationRegex) ;
return getSecondsAsISODuration(getDurationAsSeconds(first, regex) + getDurationAsSeconds(second, regex));
}
function addHHMMSSTimeStrings(first, second, timeRegex) {
if (typeof timeRegex === "string") {
timeRegex = new RegExp(timeRegex);
}
return getSecondsAsHHMMSS(getTimeAsSeconds(first, timeRegex) + getTimeAsSeconds(second, timeRegex));
}
function flatten(data) {
const result = {};
function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
cur.forEach((item, i) => {
recurse(item, `${prop}[${i}]`);
});
if (cur.length === 0) result[prop] = [];
} else {
const keys = Object.keys(cur).filter(p => Object.prototype.hasOwnProperty.call(cur, p));
const isEmpty = keys.length === 0;
keys.forEach(p => {
recurse(cur[p], prop ? `${prop}.${p}` : p);
});
if (isEmpty && prop) result[prop] = {};
}
}
recurse(data, "");
return result;
}
function unflatten(data) {
if (Object(data) !== data || Array.isArray(data)) return data;
const result = {};
const pattern = /\.?([^.[\]]+)|\[(\d+)]/g;
Object.keys(data).filter(p => Object.prototype.hasOwnProperty.call(data, p)).forEach(p => {
let cur = result;
let prop = "";
const regex = new RegExp(pattern);
Array.from({
length: p.match(new RegExp(pattern, "g"))?.length ?? 0
}, () => regex.exec(p)).forEach(m => {
if (m) {
cur = cur[prop] ?? (cur[prop] = m[2] ? [] : {});
prop = m[2] || m[1] || "";
}
});
cur[prop] = data[p];
});
return result[""] ?? result;
}
function countDecimals(num) {
if (Math.floor(num) === num || String(num)?.indexOf?.(".") < 0) return 0;
const parts = num.toString().split(".")?.[1];
return parts?.length ?? 0;
}
function formatMessage(functionName, message, CMIElement) {
const baseLength = 20;
let messageString = functionName ? `${String(functionName).padEnd(baseLength)}: ` : "";
if (CMIElement) {
const CMIElementBaseLength = 70;
messageString += CMIElement;
messageString = messageString.padEnd(CMIElementBaseLength);
}
messageString += message ?? "";
return messageString;
}
function stringMatches(str, tester) {
if (typeof str !== "string") {
return false;
}
return new RegExp(tester).test(str);
}
function memoize(fn, keyFn) {
const cache = /* @__PURE__ */new Map();
return function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
return cache.has(key) ? cache.get(key) : (() => {
const result = fn(...args);
cache.set(key, result);
return result;
})();
};
}
function parseNavigationRequest(navRequest) {
const validCommands = /* @__PURE__ */new Set(["start", "resumeAll", "continue", "previous", "choice", "jump", "exit", "exitAll", "abandon", "abandonAll", "suspendAll", "_none_"]);
const trimmed = navRequest.trim();
if (!trimmed) {
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: "Empty navigation request"
};
}
if (validCommands.has(trimmed)) {
return {
command: trimmed,
targetActivityId: null,
valid: true
};
}
const dotIndex = trimmed.indexOf(".");
if (dotIndex > 0) {
const command = trimmed.substring(0, dotIndex);
const targetActivityId = trimmed.substring(dotIndex + 1);
if ((command === "choice" || command === "jump") && targetActivityId) {
if (/^[a-zA-Z0-9._-]+$/.test(targetActivityId)) {
return {
command,
targetActivityId,
valid: true
};
} else {
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: `Invalid target activity ID: contains disallowed characters`
};
}
}
}
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: `Unrecognized navigation command: "${trimmed}"`
};
}
var __defProp$1d = Object.defineProperty;
var __defNormalProp$1d = (obj, key, value) => key in obj ? __defProp$1d(obj, key, {
enumerable: true,
configurable: true,
writable: true,
value
}) : obj[key] = value;
var __publicField$1d = (obj, key, value) => __defNormalProp$1d(obj, typeof key !== "symbol" ? key + "" : key, value);
class BaseCMI {
/**
* Constructor for BaseCMI
* @param {string} cmi_element
*/
constructor(cmi_element) {
/**
* Flag used during JSON serialization to allow getter access without initialization checks.
* When true, getters can be accessed before the API is initialized, which is necessary
* for serializing the CMI data structure to JSON format.
*/
__publicField$1d(this, "jsonString", false);
__publicField$1d(this, "_cmi_element");
__publicField$1d(this, "_initialized", false);
this._cmi_element = cmi_element;
}
/**
* Getter for _initialized
* @return {boolean}
*/
get initialized() {
return this._initialized;
}
/**
* Called when the API has been initialized after the CMI has been created
*/
initialize() {
this._initialized = true;
}
}
class BaseRootCMI extends BaseCMI {
constructor() {
super(...arguments);
__publicField$1d(this, "_start_time");
}
/**
* Start time of the session
* @type {number | undefined}
* @protected
*/
get start_time() {
return this._start_time;
}
/**
* Setter for start_time. Can only be called once.
*/
setStartTime() {
if (this._start_time === void 0) {
this._start_time = (/* @__PURE__ */new Date()).getTime();
} else {
throw new Error("Start time has already been set.");
}
}
}
var __defProp$1c = Object.defineProperty;
var __defNormalProp$1c = (obj, key, value) => key in obj ? __defProp$1c(obj, key, {
enumerable: true,
configurable: true,
writable: true,
value
}) : obj[key] = value;
var __publicField$1c = (obj, key, value) => __defNormalProp$1c(obj, typeof key !== "symbol" ? key + "" : key, value);
class BaseScormValidationError extends Error {
constructor(CMIElement, errorCode) {
super(`${CMIElement} : ${errorCode.toString()}`);
__publicField$1c(this, "_errorCode");
this._errorCode = errorCode;
Object.setPrototypeOf(this, BaseScormValidationError.prototype);
}
/**
* Getter for _errorCode
* @return {number}
*/
get errorCode() {
return this._errorCode;
}
}
class ValidationError extends BaseScormValidationError {
/**
* Constructor to take in an error message and code
* @param {string} CMIElement
* @param {number} errorCode
* @param {string} errorMessage
* @param {string} detailedMessage
*/
constructor(CMIElement, errorCode, errorMessage, detailedMessage) {
super(CMIElement, errorCode);
__publicField$1c(this, "_errorMessage");
__publicField$1c(this, "_detailedMessage", "");
this.message = `${CMIElement} : ${errorMessage}`;
this._errorMessage = errorMessage;
if (detailedMessage) {
this._detailedMessage = detailedMessage;
}
Object.setPrototypeOf(this, ValidationError.prototype);
}
/**
* Getter for _errorMessage
* @return {string}
*/
get errorMessage() {
return this._errorMessage;
}
/**
* Getter for _detailedMessage
* @return {string}
*/
get detailedMessage() {
return this._detailedMessage;
}
}
const global_constants = {
SCORM_TRUE: "true",
SCORM_FALSE: "false",
STATE_NOT_INITIALIZED: 0,
STATE_INITIALIZED: 1,
STATE_TERMINATED: 2
};
const scorm12_constants = {
// Children lists
cmi_children: "core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions",
core_children: "student_id,student_name,lesson_location,credit,lesson_status,entry,score,total_time,lesson_mode,exit,session_time",
score_children: "raw,min,max",
objectives_children: "id,score,status",
correct_responses_children: "pattern",
student_data_children: "mastery_score,max_time_allowed,time_limit_action",
student_preference_children: "audio,language,speed,text",
interactions_children: "id,objectives,time,type,correct_responses,weighting,student_response,result,latency",
error_descriptions: {
"0": {
basicMessage: "No Error",
detailMessage: "No error occurred, the previous API call was successful."
},
"101": {
basicMessage: "General Exception",
detailMessage: "No specific error code exists to describe the error."
},
"201": {
basicMessage: "Invalid argument error",
detailMessage: "Indicates that an argument represents an invalid data model element or is otherwise incorrect."
},
"202": {
basicMessage: "Element cannot have children",
detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_children" for a data model element that does not support the "_children" suffix.'
},
"203": {
basicMessage: "Element not an array - cannot have count",
detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_count" for a data model element that does not support the "_count" suffix.'
},
"301": {
basicMessage: "Not initialized",
detailMessage: "Indicates that an API call was made before the call to lmsInitialize."
},
"401": {
basicMessage: "Not implemented error",
detailMessage: "The data model element indicated in a call to LMSGetValue or LMSSetValue is valid, but was not implemented by this LMS. SCORM 1.2 defines a set of data model elements as being optional for an LMS to implement."
},
"402": {
basicMessage: "Invalid set value, element is a keyword",
detailMessage: 'Indicates that LMSSetValue was called on a data model element that represents a keyword (elements that end in "_children" and "_count").'
},
"403": {
basicMessage: "Element is read only",
detailMessage: "LMSSetValue was called with a data model element that can only be read."
},
"404": {
basicMessage: "Element is write only",
detailMessage: "LMSGetValue was called on a data model element that can only be written to."
},
"405": {
basicMessage: "Incorrect Data Type",
detailMessage: "LMSSetValue was called with a value that is not consistent with the data format of the supplied data model element."
},
"407": {
basicMessage: "Element Value Out Of Range",
detailMessage: "The numeric value supplied to a LMSSetValue call is outside of the numeric range allowed for the supplied data model element."
},
"408": {
basicMessage: "Data Model Dependency Not Established",
detailMessage: "Some data model elements cannot be set until another data model element was set. This error condition indicates that the prerequisite element was not set before the dependent element."
}
}
};
const scorm2004_constants = {
// Children lists
cmi_children: "_version,comments_from_learner,comments_from_lms,completion_status,completion_threshold,credit,entry,exit,interactions,launch_data,learner_id,learner_name,learner_preference,location,max_time_allowed,mode,objectives,progress_measure,scaled_passing_score,score,session_time,success_status,suspend_data,time_limit_action,total_time",
comments_children: "comment,timestamp,location",
score_children: "max,raw,scaled,min",
objectives_children: "progress_measure,completion_status,success_status,description,score,id",
correct_responses_children: "pattern",
student_preference_children: "audio_level,audio_captioning,delivery_speed,language",
interactions_children: "id,type,objectives,timestamp,correct_responses,weighting,learner_response,result,latency,description",
adl_data_children: "id,store",
error_descriptions: {
"0": {
basicMessage: "No Error",
detailMessage: "No error occurred, the previous API call was successful."
},
"101": {
basicMessage: "General Exception",
detailMessage: "No specific error code exists to describe the error."
},
"102": {
basicMessage: "General Initialization Failure",
detailMessage: "Call to Initialize failed for an unknown reason."
},
"103": {
basicMessage: "Already Initialized",
detailMessage: "Call to Initialize failed because Initialize was already called."
},
"104": {
basicMessage: "Content Instance Terminated",
detailMessage: "Call to Initialize failed because Terminate was already called."
},
"111": {
basicMessage: "General Termination Failure",
detailMessage: "Call to Terminate failed for an unknown reason."
},
"112": {
basicMessage: "Termination Before Initialization",
detailMessage: "Call to Terminate failed because it was made before the call to Initialize."
},
"113": {
basicMessage: "Termination After Termination",
detailMessage: "Call to Terminate failed because Terminate was already called."
},
"122": {
basicMessage: "Retrieve Data Before Initialization",
detailMessage: "Call to GetValue failed because it was made before the call to Initialize."
},
"123": {
basicMessage: "Retrieve Data After Termination",
detailMessage: "Call to GetValue failed because it was made after the call to Terminate."
},
"132": {
basicMessage: "Store Data Before Initialization",
detailMessage: "Call to SetValue failed because it was made before the call to Initialize."
},
"133": {
basicMessage: "Store Data After Termination",
detailMessage: "Call to SetValue failed because it was made after the call to Terminate."
},
"142": {
basicMessage: "Commit Before Initialization",
detailMessage: "Call to Commit failed because it was made before the call to Initialize."
},
"143": {
basicMessage: "Commit After Termination",
detailMessage: "Call to Commit failed because it was made after the call to Terminate."
},
"201": {
basicMessage: "General Argument Error",
detailMessage: "An invalid argument was passed to an API method (usually indicates that Initialize, Commit or Terminate did not receive the expected empty string argument."
},
"301": {
basicMessage: "General Get Failure",
detailMessage: "Indicates a failed GetValue call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"351": {
basicMessage: "General Set Failure",
detailMessage: "Indicates a failed SetValue call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"391": {
basicMessage: "General Commit Failure",
detailMessage: "Indicates a failed Commit call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"401": {
basicMessage: "Undefined Data Model Element",
detailMessage: "The data model element name passed to GetValue or SetValue is not a valid SCORM data model element."
},
"402": {
basicMessage: "Unimplemented Data Model Element",
detailMessage: "The data model element indicated in a call to GetValue or SetValue is valid, but was not implemented by this LMS. In SCORM 2004, this error would indicate an LMS that is not fully SCORM conformant."
},
"403": {
basicMessage: "Data Model Element Value Not Initialized",
detailMessage: "Attempt to read a data model element that has not been initialized by the LMS or through a SetValue call. This error condition is often reached during normal execution of a SCO."
},
"404": {
basicMessage: "Data Model Element Is Read Only",
detailMessage: "SetValue was called with a data model element that can only be read."
},
"405": {
basicMessage: "Data Model Element Is Write Only",
detailMessage: "GetValue was called on a data model element that can only be written to."
},
"406": {
basicMessage: "Data Model Element Type Mismatch",
detailMessage: "SetValue was called with a value that is not consistent with the data format of the supplied data model element."
},
"407": {
basicMessage: "Data Model Element Value Out Of Range",
detailMessage: "The numeric value supplied to a SetValue call is outside of the numeric range allowed for the supplied data model element."
},
"408": {
basicMessage: "Data Model Dependency Not Established",
detailMessage: "Some data model elements cannot be set until another data model element was set. This error condition indicates that the prerequisite element was not set before the dependent element."
}
}
};
const scorm12_errors$1 = scorm12_constants.error_descriptions;
class Scorm12ValidationError extends ValidationError {
/**
* Constructor to take in an error code
* @param {string} CMIElement
* @param {number} errorCode
*/
constructor(CMIElement, errorCode) {
if ({}.hasOwnProperty.call(scorm12_errors$1, String(errorCode))) {
super(CMIElement, errorCode, scorm12_errors$1[String(errorCode)]?.basicMessage || "Unknown error", scorm12_errors$1[String(errorCode)]?.detailMessage);
} else {
super(CMIElement, 101, scorm12_errors$1["101"]?.basicMessage, scorm12_errors$1["101"]?.detailMessage);
}
Object.setPrototypeOf(this, Scorm12ValidationError.prototype);
}
}
const scorm2004_errors$1 = scorm2004_constants.error_descriptions;
class Scorm2004ValidationError extends ValidationError {
/**
* Constructor to take in an error code
* @param {string} CMIElement
* @param {number} errorCode
*/
constructor(CMIElement, errorCode) {
if ({}.hasOwnProperty.call(scorm2004_errors$1, String(errorCode))) {
super(CMIElement, errorCode, scorm2004_errors$1[String(errorCode)]?.basicMessage || "Unknown error", scorm2004_errors$1[String(errorCode)]?.detailMessage);
} else {
super(CMIElement, 101, scorm2004_errors$1["101"]?.basicMessage, scorm2004_errors$1["101"]?.detailMessage);
}
Object.setPrototypeOf(this, Scorm2004ValidationError.prototype);
}
}
const global_errors = {
GENERAL: 101,
INITIALIZATION_FAILED: 101,
INITIALIZED: 101,
TERMINATED: 101,
TERMINATION_FAILURE: 101,
TERMINATION_BEFORE_INIT: 101,
MULTIPLE_TERMINATION: 101,
RETRIEVE_BEFORE_INIT: 101,
RETRIEVE_AFTER_TERM: 101,
STORE_BEFORE_INIT: 101,
STORE_AFTER_TERM: 101,
COMMIT_BEFORE_INIT: 101,
COMMIT_AFTER_TERM: 101,
ARGUMENT_ERROR: 101,
CHILDREN_ERROR: 101,
COUNT_ERROR: 101,
GENERAL_GET_FAILURE: 101,
GENERAL_SET_FAILURE: 101,
GENERAL_COMMIT_FAILURE: 101,
UNDEFINED_DATA_MODEL: 101,
UNIMPLEMENTED_ELEMENT: 101,
VALUE_NOT_INITIALIZED: 101,
INVALID_SET_VALUE: 101,
READ_ONLY_ELEMENT: 101,
WRITE_ONLY_ELEMENT: 101,
TYPE_MISMATCH: 101,
VALUE_OUT_OF_RANGE: 101,
DEPENDENCY_NOT_ESTABLISHED: 101
};
const scorm12_errors = {
...global_errors,
RETRIEVE_BEFORE_INIT: 301,
STORE_BEFORE_INIT: 301,
COMMIT_BEFORE_INIT: 301,
ARGUMENT_ERROR: 201,
CHILDREN_ERROR: 202,
COUNT_ERROR: 203,
UNDEFINED_DATA_MODEL: 401,
UNIMPLEMENTED_ELEMENT: 401,
VALUE_NOT_INITIALIZED: 301,
INVALID_SET_VALUE: 402,
READ_ONLY_ELEMENT: 403,
WRITE_ONLY_ELEMENT: 404,
TYPE_MISMATCH: 405,
VALUE_OUT_OF_RANGE: 407,
DEPENDENCY_NOT_ESTABLISHED: 408
};
const scorm2004_errors = {
...global_errors,
INITIALIZATION_FAILED: 102,
INITIALIZED: 103,
TERMINATED: 104,
TERMINATION_FAILURE: 111,
TERMINATION_BEFORE_INIT: 112,
MULTIPLE_TERMINATION: 113,
MULTIPLE_TERMINATIONS: 113,
RETRIEVE_BEFORE_INIT: 122,
RETRIEVE_AFTER_TERM: 123,
STORE_BEFORE_INIT: 132,
STORE_AFTER_TERM: 133,
COMMIT_BEFORE_INIT: 142,
COMMIT_AFTER_TERM: 143,
ARGUMENT_ERROR: 201,
GENERAL_GET_FAILURE: 301,
GENERAL_SET_FAILURE: 351,
GENERAL_COMMIT_FAILURE: 391,
UNDEFINED_DATA_MODEL: 401,
UNIMPLEMENTED_ELEMENT: 402,
VALUE_NOT_INITIALIZED: 403,
READ_ONLY_ELEMENT: 404,
WRITE_ONLY_ELEMENT: 405,
TYPE_MISMATCH: 406,
VALUE_OUT_OF_RANGE: 407,
DEPENDENCY_NOT_ESTABLISHED: 408
};
var __defProp$1b = Object.defineProperty;
var __defNormalProp$1b = (obj, key, value) => key in obj ? __defProp$1b(obj, key, {
enumerable: true,
configurable: true,
writable: true,
value
}) : obj[key] = value;
var __publicField$1b = (obj, key, value) => __defNormalProp$1b(obj, typeof key !== "symbol" ? key + "" : key, value);
class CMIArray extends BaseCMI {
/**
* Constructor cmi *.n arrays
* @param {object} params
*/
constructor(params) {
super(params.CMIElement);
__publicField$1b(this, "_errorCode");
__publicField$1b(this, "_errorClass");
__publicField$1b(this, "__children");
__publicField$1b(this, "childArray");
this.__children = params.children;
this._errorCode = params.errorCode ?? scorm12_errors.GENERAL;
this._errorClass = params.errorClass || BaseScormValidationError;
this.childArray = [];
}
/**
* Called when the API has been reset
*/
reset() {
let wipe = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
this._initialized = false;
if (wipe) {
this.childArray = [];
} else {
for (let i = 0; i < this.childArray.length; i++) {
this.childArray[i]?.reset();
}
}
}
/**
* Getter for _children
* @return {string}
*/
get _children() {
return this.__children;
}
/**
* Setter for _children. Just throws an error.
* @param {string} _children
*/
set _children(_children) {
throw new this._errorClass(this._cmi_element + "._children", this._errorCode);
}
/**
* Getter for _count
* @return {number}
*/
get _count() {
return this.childArray.length;
}
/**
* Setter for _count. Just throws an error.
* @param {number} _count
*/
set _count(_count) {
throw new this._errorClass(this._cmi_element + "._count", this._errorCode);
}
/**
* toJSON for *.n arrays
* @return {object}
*/
toJSON() {
this.jsonString = true;
const result = {};
for (let i = 0; i < this.childArray.length; i++) {
result[i + ""] = this.childArray[i];
}
this.jsonString = false;
return result;
}
}
const NAVBoolean = {
UNKNOWN: "unknown",
TRUE: "true",
FALSE: "false"
};
const SuccessStatus = {
PASSED: "passed",
FAILED: "failed",
UNKNOWN: "unknown"
};
const CompletionStatus = {
COMPLETED: "completed",
INCOMPLETE: "incomplete",
UNKNOWN: "unknown"
};
const LogLevelEnum = {
_: 0,
DEBUG: 1,
INFO: 2,
WARN: 3,
ERROR: 4,
NONE: 5
};
const DefaultSettings = {
autocommit: false,
autocommitSeconds: 10,
throttleCommits: false,
useAsynchronousCommits: false,
sendFullCommit: true,
lmsCommitUrl: false,
dataCommitFormat: "json",
commitRequestDataType: "application/json;charset=UTF-8",
autoProgress: false,
logLevel: LogLevelEnum.ERROR,
selfReportSessionTime: false,
alwaysSendTotalTime: false,
renderCommonCommitFields: false,
autoCompleteLessonStatus: false,
strict_errors: true,
xhrHeaders: {},
xhrWithCredentials: false,
fetchMode: "cors",
asyncModeBeaconBehavior: "never",
responseHandler: async function (response) {
if (typeof response !== "undefined") {
let httpResult = null;
try {
if (typeof response.json === "function") {
httpResult = await response.json();
} else if (typeof response.text === "function") {
const responseText = await response.text();
if (responseText) {
httpResult = JSON.parse(responseText);
}
}
} catch (e) {}
if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) {
if (response.status === 200) {
return {
result: global_constants.SCORM_TRUE,
errorCode: 0
};
} else {
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
}
} else {
return {
result: httpResult.result,
errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101
};
}
}
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
},
xhrResponseHandler: function (xhr) {
if (typeof xhr !== "undefined") {
let httpResult = null;
if (xhr.status >= 200 && xhr.status <= 299) {
try {
httpResult = JSON.parse(xhr.responseText);
} catch (e) {}
if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) {
return {
result: global_constants.SCORM_TRUE,
errorCode: 0
};
}
return {
result: httpResult.result,
errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101
};
} else {
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
}
}
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
},
requestHandler: function (commitObject) {
return commitObject;
},
onLogMessage: defaultLogHandler,
mastery_override: false,
score_overrides_status: false,
completion_status_on_failed: "completed",
scoItemIds: [],
scoItemIdValidator: false,
globalObjectiveIds: [],
// Offline support settings
enableOfflineSupport: false,
courseId: "",
syncOnInitialize: true,
syncOnTerminate: true,
maxSyncAttempts: 5,
// Multi-SCO support settings
scoId: "",
autoPopulateCommitMetadata: false,
// HTTP service settings
httpService: null,
// Global learner preferences settings
globalStudentPreferences: false
};
function defaultLogHandler(messageLevel, logMessage) {
switch (messageLevel) {
case "4":
case 4:
case "ERROR":
case LogLevelEnum.ERROR:
console.error(logMessage);
break;
case "3":
case 3:
case "WARN":
case LogLevelEnum.WARN:
console.warn(logMessage);
break;
case "2":
case 2:
case "INFO":
case LogLevelEnum.INFO:
console.info(logMessage);
break;
case "1":
case 1:
case "DEBUG":
case LogLevelEnum.DEBUG:
if (console.debug) {
console.debug(logMessage);
} else {
console.log(logMessage);
}
break;
}
}
const scorm12_regex = {
/** CMIString256 - Character string, max 255 chars (RTE A.1) */
CMIString256: "^[\\s\\S]{0,255}$",
/** CMIString4096 - Character string, max 4096 chars (RTE A.1) */
CMIString4096: "^[\\s\\S]{0,4096}$",
/**
* CMIString64000 - Extended character string, max 64000 chars
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines cmi.suspend_data as CMIString4096 (max 4096 chars).
* This implementation intentionally increases the limit to 64000 chars (matching SCORM 2004)
* for the following reasons:
*
* 1. Modern content frequently exceeds 4096 chars due to JSON state serialization,
* base64 encoding, complex bookmark data, and rich interaction tracking
* 2. The 4096 limit was set in 2001 when content was simpler; modern authoring tools
* routinely generate larger suspend_data
* 3. Most LMS systems can handle larger values - the API shouldn't be the bottleneck
* 4. Content that gets rejected has no recovery path, causing data loss
* 5. Aligns with SCORM 2004's more practical 64000 char limit
*
* Used by: cmi.suspend_data (SCORM 1.2)
*
* Strict spec pattern would be: ^[\s\S]{0,4096}$
*/
CMIString64000: "^[\\s\\S]{0,64000}$",
/**
* CMITime - Clock time in HH:MM:SS.SS format (RTE A.2)
* Optional centiseconds (1-2 decimal digits) per spec.
*/
CMITime: "^(?:[01]\\d|2[0123]):(?:[012345]\\d):(?:[012345]\\d)(\\.\\d{1,2})?$",
/**
* CMITimespan - Time interval in HHHH:MM:SS.SS format (RTE A.3)
* We allow more digits for the hour to support values generated
* by getSecondsAsHHMMSS which can produce larger hour values
* (e.g., 17496:00:00 for very long durations).
* Changed from minimum 2 digits to 1+ digits with no upper limit.
*/
CMITimespan: "^([0-9]+):([0-9]{2}):([0-9]{2})(\\.\\d{1,2})?$",
/**
* CMIInteger - Non-negative integer (RTE A.4)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIInteger as 0-65536 range.
* This implementation intentionally omits range validation to support
* legacy content that may exceed this limit in _count fields or other
* integer values. Real-world content often violates the spec by storing
* larger values, and strict enforcement would break compatibility.
*
* Affected elements:
* - cmi.objectives._count
* - cmi.interactions._count
* - cmi.interactions.n.objectives._count
* - cmi.interactions.n.correct_responses._count
*/
CMIInteger: "^\\d+$",
/** CMISInteger - Signed integer (RTE A.5) */
CMISInteger: "^-?([0-9]+)$",
/**
* CMIDecimal - Signed decimal (RTE A.6)
* We set practical limits on decimals to prevent abuse while maintaining
* broad compatibility with legacy content.
* Increased from 3 to 10 digits before decimal to match SCORM 2004 behavior.
*/
CMIDecimal: "^-?([0-9]{0,10})(\\.[0-9]*)?$",
/**
* CMIIdentifier - Printable ASCII characters, max 255 chars (RTE A.7)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIIdentifier as alphanumeric only:
* letters (a-z, A-Z), numbers (0-9), hyphens (-), and underscores (_).
* Spaces and periods are explicitly NOT allowed per spec.
*
* This implementation intentionally relaxes validation to accept all
* printable ASCII characters (0x21-0x7E) plus whitespace to support
* legacy content. Many real-world LMS systems and content packages use
* identifiers that violate the strict spec (e.g., student IDs with spaces,
* objective IDs with periods or special characters).
*
* Strict spec pattern would be: ^[A-Za-z0-9_-]{0,255}$
*
* Affected elements:
* - cmi.core.student_id
* - cmi.objectives.n.id
* - cmi.interactions.n.id
* - cmi.interactions.n.objectives.n.id
*/
CMIIdentifier: "^[\\u0021-\\u007E\\s]{0,255}$",
/** CMICredit - Vocabulary: credit or no-credit (RTE 3.4.2.1.3) */
CMICredit: "^(credit|no-credit)$",
/** CMIEntry - Vocabulary: ab-initio, resume, or empty (RTE 3.4.2.1.4) */
CMIEntry: "^(ab-initio|resume|)$",
/** CMILessonMode - Vocabulary: normal, browse, or review (RTE 3.4.2.1.10) */
CMILessonMode: "^(normal|browse|review)$",
/** CMITimeLimitAction - Vocabulary: action combinations (RTE 3.4.2.1.11) */
CMITimeLimitAction: "^(exit,message|exit,no message|continue,message|continue,no message)$",
/**
* CMIFeedback - Relaxed for compatibility (normally CMIString255)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIFeedback as CMIString255 (max 255 chars)
* with format varying by interaction type (see RTE 3.4.2.7.5, 3.4.2.7.7).
*
* This implementation intentionally relaxes validation for two reasons:
*
* 1. LENGTH: Many legacy content packages store responses exceeding 255 chars,
* especially for fill-in and performance interaction types. Strict enforcement
* would break existing content with no user-facing benefit.
*
* 2. FORMAT: The spec requires type-specific formats (e.g., true-false accepts
* only "0"/"1"/"t"/"f", choice accepts comma-separated single chars). However:
* - Format validation requires knowing interaction type at validation time
* - Legacy content often uses non-standard formats
* - The SCO is responsible for response evaluation, not the API
* - Strict format validation provides minimal benefit vs. compatibility cost
*
* Affected elements:
* - cmi.interactions.n.student_response
* - cmi.interactions.n.correct_responses.n.pattern
*
* Strict spec pattern would be: ^[\s\S]{0,255}$ with type-specific subpatterns
*/
CMIFeedback: "^.*$",
/** CMIIndex - Pattern for array index extraction */
CMIIndex: "[._](\\d+).",
/** CMIStatus - Lesson status vocabulary (RTE 3.4.2.2.3) */
CMIStatus: "^(passed|completed|failed|incomplete|browsed)$",
/** CMIStatus2 - Extended status vocabulary with "not attempted" (RTE 3.4.2.6.2) */
CMIStatus2: "^(passed|completed|failed|incomplete|browsed|not attempted)$",
/** CMIExit - Exit vocabulary (RTE 3.4.2.1.5) */
CMIExit: "^(time-out|suspend|logout|)$",
/** CMIType - Interaction type vocabulary (RTE 3.4.2.7.2) */
CMIType: "^(true-false|choice|fill-in|matching|performance|sequencing|likert|numeric)$",
/** CMIResult - Interaction result vocabulary (RTE 3.4.2.7.6) */
CMIResult: "^(correct|wrong|unanticipated|neutral|([0-9]{0,3})?(\\.[0-9]*)?)$",
/** NAVEvent - Navigation event vocabulary (SCORM 1.2 extension) */
NAVEvent: "^(_?(previous|continue|start|resumeAll|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|choice|jump|_none_)$",
/** score_range - Valid score range 0-100 (RTE 3.4.2.2.2) */
score_range: "0#100",
/** audio_range - Audio level range -1 to 100 (RTE 3.4.2.3.1) */
audio_range: "-1#100",
/** speed_range - Playback speed range -100 to 100 (RTE 3.4.2.3.2) */
speed_range: "-100#100",
/** weighting_range - Interaction weighting range -100 to 100 (RTE 3.4.2.7.4) */
weighting_range: "-100#100",
/** text_range - Text display preference -1 to 1 (RTE 3.4.2.3.3) */
text_range: "-1#1"
};
const scorm2004_regex = {
/** CMIString200 - Character string, max 200 chars (RTE C.1.1) */
CMIString200: "^[\\u0000-\\uFFFF]{0,200}$",
/** CMIString250 - Character string, max 250 chars (RTE C.1.1) */
CMIString250: "^[\\u0000-\\uFFFF]{0,250}$",
/** CMIString1000 - Character string, max 1000 chars (RTE C.1.1) */
CMIString1000: "^[\\u0000-\\uFFFF]{0,1000}$",
/** CMIString4000 - Character string, max 4000 chars (RTE C.1.1) */
CMIString4000: "^[\\u0000-\\uFFFF]{0,4000}$",
/** CMIString64000 - Character string, max 64000 chars (RTE C.1.1) */
CMIString64000: "^[\\u0000-\\uFFFF]{0,64000}$",
/**
* CMILang - Language code per RFC 1766/RFC 3066 (RTE C.1.2)
* Primary tag: 1-8 characters (ISO 639-1: 2, ISO 639-2: 3, or i/x for IANA/private)
* Subtag: 2-8 alphanumeric characters
*/
CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
/** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
/** CMILangcr - Language tag pattern with content */
CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
/** CMILangString250cr - String with optional language tag (carriage return variant) */
CMILangString250cr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?})?(.{0,250})?)?$",
/** CMILangString4000 - String with optional language tag, max 4000 chars (RTE C.1.3) */
CMILangString4000: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,4000}$)?$",
/**
* CMITime - ISO 8601 timestamp format (RTE C.1.4)
* Year range expanded from 1970-2038 to 1970-9999 to support future dates
*/
CMITime: "^(19[7-9][0-9]|[2-9][0-9]{3})((-(0[1-9]|1[0-2]))((-(0[1-9]|[1-2][0-9]|3[0-1]))(T([0-1][0-9]|2[0-3])((:[0-5][0-9])((:[0-5][0-9])((\\.[0-9]{1,6})((Z|([+|-]([0-1][0-9]|2[0-3])))(:[0-5][0-9])?)?)?)?)?)?)?)?$",
/** CMITimespan - ISO 8601 duration format (RTE C.1.5) */
CMITimespan: "^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:(\\d+(?:\\.\\d{1,2})?)S)?)?$",
/** CMIInteger - Non-negative integer (RTE C.1.6) */
CMIInteger: "^\\d+$",
/** CMISInteger - Signed integer (RTE C.1.7) */
CMISInteger: "^-?([0-9]+)$",
/**
* CMIDecimal - Signed decimal (RTE C.1.8)
* Spec allows unlimited digits, but we set practical limits to prevent abuse
* while maintaining broad compatibility:
* - Up to 10 digits before decimal (supports values up to 10 billion)
* - Up to 18 digits after decimal (maintains precision for scientific use)
*/
CMIDecimal: "^-?([0-9]{1,10})(\\.[0-9]{1,18})?$",
/**
* CMIIdentifier - Identifier with alphanumeric ending, max 250 chars (RTE C.1.9)
* Must contain at least one word character (\w) and only allow: letters,
* numbers, - ( ) + . : = @ ; $ _ ! * ' % / #
* URN format is validated separately if string starts with "urn:"
*/
CMIIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$",
/** CMIShortIdentifier - Short identifier conforming to URI syntax, max 250 chars (RTE C.1.10) */
CMIShortIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$",
/** CMILongIdentifier - Long identifier supporting URN format, max 4000 chars (RTE C.1.11) */
CMILongIdentifier: "^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000}|.{1,4000})$",
/** CMIFeedback - Unrestricted feedback text (RTE C.1.12) */
CMIFeedback: "^.*$",
/** CMIIndex - Pattern for array index extraction */
CMIIndex: "[._](\\d+).",
/** CMIIndexStore - Pattern for stored index notation */
CMIIndexStore: ".N(\\d+).",
/** CMICStatus - Completion status vocabulary (RTE 4.1.4) */
CMICStatus: "^(completed|incomplete|not attempted|unknown)$",
/** CMISStatus - Success status vocabulary (RTE 4.1.11) */
CMISStatus: "^(passed|failed|unknown)$",
/** CMIExit - Exit vocabulary (RTE 4.1.3) */
CMIExit: "^(time-out|suspend|logout|normal)$",
/** CMIType - Interaction type vocabulary (RTE 4.1.6.2) */
CMIType: "^(true-false|choice|fill-in|long-fill-in|matching|performance|sequencing|likert|numeric|other)$",
/** CMIResult - Interaction result vocabulary (RTE 4.1.6.8) */
CMIResult: "^(correct|incorrect|unanticipated|neutral|-?([0-9]{1,4})(\\.[0-9]{1,18})?)$",
/** NAVEvent - Navigation event vocabulary (SN Book Table 4.4.2) */
NAVEvent: "^(_?(start|resumeAll|previous|continue|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|_none_|(\\{target=(?<choice_target>\\S{0,}[a-zA-Z0-9-_]+)})?choice|(\\{target=(?<jump_target>\\S{0,}[a-zA-Z0-9-_]+)})?jump)$",
/** NAVBoolean - Navigation boolean vocabulary (SN Book) */
NAVBoolean: "^(unknown|true|false)$",
/** NAVTarget - Navigation target pattern (SN Book) */
NAVTarget: "^{target=\\S{0,}[a-zA-Z0-9-_]+}$",
/** scaled_range - Scaled score range -1 to 1 (RTE 4.1.10.1) */
scaled_range: "-1#1",
/** audio_range - Audio level range 0 to 999.9999999 (RTE 4.1.7.1) */
audio_range: "0#999.9999999",
/** speed_range - Playback speed range 0 to 999.9999999 (RTE 4.1.7.4) */
speed_range: "0#999.9999999",
/** text_range - Text display preference -1 to 1 (RTE 4.1.7.5) */
text_range: "-1#1",
/** progress_range - Progress measure range 0 to 1 (RTE 4.1.8) */
progress_range: "0#1"
};
const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
const LearnerResponses = {
"true-false": {
format: "^true$|^false$",
max: 1,
delimiter: "",
unique: false
},
choice: {
format: scorm2004_regex.CMILongIdentifier,
max: 36,
delimiter: "[,]",
unique: true
},
"fill-in": {
format: scorm2004_regex.CMILangString250,
max: 10,
delimiter: "[,]",
unique: false
},
"long-fill-in": {
format: scorm2004_regex.CMILangString4000,
max: 1,
delimiter: "",
unique: false
},
matching: {
format: scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIShortIdentifier,
max: 36,
delimiter: "[,]",
delimiter2: "[.]",
unique: false
},
performance: {
format: PERFORMANCE_STEP_NAME,
format2: LR_PERFORMANCE_STEP_ANSWER,
max: 250,
delimiter: "[,]",
delimiter2: "[.]",
unique: false
},
sequencing: {
format: scorm2004_regex.CMIShortIdentifier,
max: 36,
delimiter: "[,]",
unique: false
},
likert: {
format: scorm2004_regex.CMIShortIdentifier,
max: 1,
delimiter: "",
unique: false
},
numeric: {
format: scorm2004_regex.CMIDecimal,
max: 1,
delimiter: "",
unique: false
},
other: {
format: scorm2004_regex.CMIString4000,
max: 1,
delimiter: "",
unique: false
}
};
const CorrectResponses = {
"true-false": {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: "^true$|^false$",
limit: 1
},
choice: {
max: 36,
delimiter: "[,]",
unique: true,
duplicate: false,
format: scorm2004_regex.CMILongIdentifier
},
"fill-in": {
max: 10,
delimiter: "[,]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMILangString250cr
},
"long-fill-in": {
max: 1,
delimiter: "",
unique: false,
duplicate: true,
format: scorm2004_regex.CMILangString4000
},
matching: {
max: 36,
delimiter: "[,]",
delimiter2: "[.]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIShortIdentifier
},
performance: {
max: 250,
delimiter: "[,]",
delimiter2: "[.]",
unique: false,
duplicate: false,
// step_name: optional short_identifier_type
format: PERFORMANCE_STEP_NAME,
// step_answer: optional characterstring (spaces allowed) or numeric range
format2: CR_PERFORMANCE_STEP_ANSWER
},
sequencing: {
max: 36,
delimiter: "[,]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier
},
likert: {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier,
limit: 1
},
numeric: {
max: 2,
delimiter: "[:]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIDecimal,
limit: 1
},
other: {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIString4000,
limit: 1
}
};
const ValidLanguages = ["aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", "ay", "az", "ba", "be", "bg", "bh", "bi", "bm", "bn", "bo", "br", "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", "ee", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", "kk", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la