scorm-again 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -940,6 +940,11 @@ const scorm2004_regex = {
940
940
  progress_range: "0#1"
941
941
  };
942
942
 
943
+ const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
944
+ const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
945
+ const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
946
+ const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
947
+ const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
943
948
  const LearnerResponses = {
944
949
  "true-false": {
945
950
  format: "^true$|^false$",
@@ -974,8 +979,8 @@ const LearnerResponses = {
974
979
  unique: false
975
980
  },
976
981
  performance: {
977
- format: "^$|" + scorm2004_regex.CMIShortIdentifier,
978
- format2: scorm2004_regex.CMIDecimal + "|^$|" + scorm2004_regex.CMIShortIdentifier,
982
+ format: PERFORMANCE_STEP_NAME,
983
+ format2: LR_PERFORMANCE_STEP_ANSWER,
979
984
  max: 250,
980
985
  delimiter: "[,]",
981
986
  delimiter2: "[.]",
@@ -1051,10 +1056,10 @@ const CorrectResponses = {
1051
1056
  delimiter2: "[.]",
1052
1057
  unique: false,
1053
1058
  duplicate: false,
1054
- // step_name must be a non-empty short identifier
1055
- format: scorm2004_regex.CMIShortIdentifier,
1056
- // step_answer may be short identifier or numeric range (<decimal>[:<decimal>])
1057
- format2: `^(${scorm2004_regex.CMIShortIdentifier})$|^(?:\\d+(?:\\.\\d+)?(?::\\d+(?:\\.\\d+)?)?)$`
1059
+ // step_name: optional short_identifier_type
1060
+ format: PERFORMANCE_STEP_NAME,
1061
+ // step_answer: optional characterstring (spaces allowed) or numeric range
1062
+ format2: CR_PERFORMANCE_STEP_ANSWER
1058
1063
  },
1059
1064
  sequencing: {
1060
1065
  max: 36,
@@ -5363,7 +5368,19 @@ class CMIValueAccessService {
5363
5368
  if (!scorm2004) {
5364
5369
  if (isFinalAttribute) {
5365
5370
  if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
5366
- this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5371
+ if (attribute === "_children") {
5372
+ this.context.throwSCORMError(
5373
+ CMIElement,
5374
+ getErrorCode(this.context.errorCodes, "CHILDREN_ERROR")
5375
+ );
5376
+ } else if (attribute === "_count") {
5377
+ this.context.throwSCORMError(
5378
+ CMIElement,
5379
+ getErrorCode(this.context.errorCodes, "COUNT_ERROR")
5380
+ );
5381
+ } else {
5382
+ this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5383
+ }
5367
5384
  return { error: true };
5368
5385
  }
5369
5386
  }
@@ -15767,7 +15784,8 @@ class BaseAPI {
15767
15784
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15768
15785
  } else {
15769
15786
  const result = this.storeData(false);
15770
- if ((result.errorCode ?? 0) > 0) {
15787
+ const errorCode = result.errorCode ?? 0;
15788
+ if (errorCode > 0) {
15771
15789
  if (result.errorMessage) {
15772
15790
  this.apiLog(
15773
15791
  "commit",
@@ -15782,12 +15800,12 @@ class BaseAPI {
15782
15800
  LogLevelEnum.DEBUG
15783
15801
  );
15784
15802
  }
15785
- this.throwSCORMError("api", result.errorCode);
15803
+ this.throwSCORMError("api", errorCode);
15786
15804
  }
15787
15805
  const resultValue = result?.result ?? global_constants.SCORM_FALSE;
15788
15806
  returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
15789
15807
  this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
15790
- if (checkTerminated) this.lastErrorCode = "0";
15808
+ if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
15791
15809
  this.processListeners(callbackName);
15792
15810
  if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
15793
15811
  this._offlineStorageService.hasPendingOfflineData(this._courseId).then((hasPendingData) => {
@@ -16559,6 +16577,49 @@ class CMILearnerPreference extends BaseCMI {
16559
16577
  }
16560
16578
  }
16561
16579
 
16580
+ function stripBrackets(delim) {
16581
+ return delim.replace(/[[\]]/g, "");
16582
+ }
16583
+ function escapeRegex(s) {
16584
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16585
+ }
16586
+ function splitDelimited(value, bracketed) {
16587
+ if (!bracketed) {
16588
+ return [value];
16589
+ }
16590
+ if (value.includes(bracketed)) {
16591
+ return value.split(bracketed);
16592
+ }
16593
+ const bare = stripBrackets(bracketed);
16594
+ if (!bare) {
16595
+ return [value];
16596
+ }
16597
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`, "g");
16598
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
16599
+ return value.split(splitRe).map((part) => part.replace(unescapeRe, bare));
16600
+ }
16601
+ function splitFirstDelimited(value, bracketed) {
16602
+ if (!bracketed) {
16603
+ return [value];
16604
+ }
16605
+ if (value.includes(bracketed)) {
16606
+ const idx = value.indexOf(bracketed);
16607
+ return [value.slice(0, idx), value.slice(idx + bracketed.length)];
16608
+ }
16609
+ const bare = stripBrackets(bracketed);
16610
+ if (!bare) {
16611
+ return [value];
16612
+ }
16613
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`);
16614
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
16615
+ const parts = value.split(splitRe);
16616
+ const first = (parts[0] ?? "").replace(unescapeRe, bare);
16617
+ if (parts.length === 1) {
16618
+ return [first];
16619
+ }
16620
+ return [first, parts.slice(1).join(bare).replace(unescapeRe, bare)];
16621
+ }
16622
+
16562
16623
  class CMIInteractions extends CMIArray {
16563
16624
  /**
16564
16625
  * Constructor for `cmi.interactions` Array
@@ -16768,8 +16829,7 @@ class CMIInteractionsObject extends BaseCMI {
16768
16829
  const response_type = LearnerResponses[this.type];
16769
16830
  if (response_type) {
16770
16831
  if (response_type?.delimiter) {
16771
- const delimiter = response_type.delimiter === "[,]" ? "," : response_type.delimiter;
16772
- nodes = learner_response.split(delimiter);
16832
+ nodes = splitDelimited(learner_response, response_type.delimiter);
16773
16833
  } else {
16774
16834
  nodes[0] = learner_response;
16775
16835
  }
@@ -16777,10 +16837,10 @@ class CMIInteractionsObject extends BaseCMI {
16777
16837
  const formatRegex = new RegExp(response_type.format);
16778
16838
  for (let i = 0; i < nodes.length; i++) {
16779
16839
  if (response_type?.delimiter2) {
16780
- const delimiter2 = response_type.delimiter2 === "[.]" ? "." : response_type.delimiter2;
16781
- const values = nodes[i]?.split(delimiter2);
16840
+ const node = nodes[i] ?? "";
16841
+ const values = this.type === "performance" ? splitFirstDelimited(node, response_type.delimiter2) : splitDelimited(node, response_type.delimiter2);
16782
16842
  if (values?.length === 2) {
16783
- if (this.type === "performance" && (values[0] === "" || values[1] === "")) {
16843
+ if (this.type === "performance" && values[0] === "" && values[1] === "") {
16784
16844
  throw new Scorm2004ValidationError(
16785
16845
  this._cmi_element + ".learner_response",
16786
16846
  scorm2004_errors.TYPE_MISMATCH
@@ -16999,30 +17059,13 @@ class CMIInteractionsObjectivesObject extends BaseCMI {
16999
17059
  return result;
17000
17060
  }
17001
17061
  }
17002
- function stripBrackets(delim) {
17003
- return delim.replace(/[[\]]/g, "");
17004
- }
17005
- function escapeRegex(s) {
17006
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17007
- }
17008
- function splitUnescaped(text, delim) {
17009
- const reDelim = escapeRegex(delim);
17010
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
17011
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
17012
- return text.split(splitRe).map((part) => part.replace(unescapeRe, delim));
17013
- }
17014
- function splitFirstUnescaped(text, delim) {
17015
- const reDelim = escapeRegex(delim);
17016
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
17017
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
17018
- const parts = text.split(splitRe);
17019
- const firstPart = parts[0] ?? "";
17020
- if (parts.length === 1) {
17021
- return [firstPart.replace(unescapeRe, delim)];
17062
+ const RESPONSE_PREFIX_RE = /^\{(?:lang|case_matters|order_matters)=[^}]+\}/;
17063
+ function stripResponsePrefixes(node) {
17064
+ let result = node;
17065
+ while (RESPONSE_PREFIX_RE.test(result)) {
17066
+ result = result.replace(RESPONSE_PREFIX_RE, "");
17022
17067
  }
17023
- const part1 = firstPart.replace(unescapeRe, delim);
17024
- const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
17025
- return [part1, part2];
17068
+ return result;
17026
17069
  }
17027
17070
  function validatePattern(type, pattern, responseDef) {
17028
17071
  if (pattern.trim() !== pattern) {
@@ -17031,8 +17074,7 @@ function validatePattern(type, pattern, responseDef) {
17031
17074
  scorm2004_errors.TYPE_MISMATCH
17032
17075
  );
17033
17076
  }
17034
- const subDelim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
17035
- const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
17077
+ const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
17036
17078
  for (const raw of rawNodes) {
17037
17079
  if (raw.trim() !== raw) {
17038
17080
  throw new Scorm2004ValidationError(
@@ -17044,20 +17086,14 @@ function validatePattern(type, pattern, responseDef) {
17044
17086
  if (type === "fill-in" && pattern === "") {
17045
17087
  return;
17046
17088
  }
17047
- const delim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
17048
- let nodes;
17049
- if (delim1) {
17050
- nodes = splitUnescaped(pattern, delim1);
17051
- } else {
17052
- nodes = [pattern];
17053
- }
17089
+ const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
17054
17090
  if (!responseDef.delimiter && pattern.includes(",")) {
17055
17091
  throw new Scorm2004ValidationError(
17056
17092
  "cmi.interactions.n.correct_responses.n.pattern",
17057
17093
  scorm2004_errors.TYPE_MISMATCH
17058
17094
  );
17059
17095
  }
17060
- if (responseDef.unique || responseDef.duplicate === false) {
17096
+ if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
17061
17097
  const seen = new Set(nodes);
17062
17098
  if (seen.size !== nodes.length) {
17063
17099
  throw new Scorm2004ValidationError(
@@ -17089,8 +17125,7 @@ function validatePattern(type, pattern, responseDef) {
17089
17125
  scorm2004_errors.TYPE_MISMATCH
17090
17126
  );
17091
17127
  }
17092
- const delim = stripBrackets(delimBracketed);
17093
- const parts = value.split(new RegExp(`(?<!\\\\)${escapeRegex(delim)}`, "g")).map((n) => n.replace(new RegExp(`\\\\${escapeRegex(delim)}`, "g"), delim));
17128
+ const parts = splitDelimited(value, delimBracketed);
17094
17129
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
17095
17130
  throw new Scorm2004ValidationError(
17096
17131
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -17107,15 +17142,17 @@ function validatePattern(type, pattern, responseDef) {
17107
17142
  for (const node of nodes) {
17108
17143
  switch (type) {
17109
17144
  case "numeric": {
17110
- const numDelim = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : ":";
17111
- const nums = node.split(numDelim);
17112
- if (nums.length < 1 || nums.length > 2) {
17113
- throw new Scorm2004ValidationError(
17114
- "cmi.interactions.n.correct_responses.n.pattern",
17115
- scorm2004_errors.TYPE_MISMATCH
17116
- );
17145
+ if (node === "") {
17146
+ const bracketedRange = nodes.length >= 2 && !!responseDef.delimiter && pattern.includes(responseDef.delimiter);
17147
+ if (!bracketedRange) {
17148
+ throw new Scorm2004ValidationError(
17149
+ "cmi.interactions.n.correct_responses.n.pattern",
17150
+ scorm2004_errors.TYPE_MISMATCH
17151
+ );
17152
+ }
17153
+ break;
17117
17154
  }
17118
- nums.forEach(checkSingle);
17155
+ checkSingle(node);
17119
17156
  break;
17120
17157
  }
17121
17158
  case "performance": {
@@ -17126,8 +17163,8 @@ function validatePattern(type, pattern, responseDef) {
17126
17163
  scorm2004_errors.TYPE_MISMATCH
17127
17164
  );
17128
17165
  }
17129
- const delim = stripBrackets(delimBracketed);
17130
- const parts = splitFirstUnescaped(node, delim);
17166
+ const record = stripResponsePrefixes(node);
17167
+ const parts = splitFirstDelimited(record, delimBracketed);
17131
17168
  if (parts.length !== 2) {
17132
17169
  throw new Scorm2004ValidationError(
17133
17170
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -17135,7 +17172,7 @@ function validatePattern(type, pattern, responseDef) {
17135
17172
  );
17136
17173
  }
17137
17174
  const [part1, part2] = parts;
17138
- if (part1 === "" || part2 === "" || part1 === part2) {
17175
+ if (part1 === "" && part2 === "") {
17139
17176
  throw new Scorm2004ValidationError(
17140
17177
  "cmi.interactions.n.correct_responses.n.pattern",
17141
17178
  scorm2004_errors.TYPE_MISMATCH
@@ -20199,7 +20236,7 @@ class Scorm2004ResponseValidator {
20199
20236
  checkValidResponseType(CMIElement, response_type, value, interaction_type) {
20200
20237
  let nodes = [];
20201
20238
  if (response_type?.delimiter) {
20202
- nodes = String(value).split(response_type.delimiter);
20239
+ nodes = splitDelimited(String(value), response_type.delimiter);
20203
20240
  } else {
20204
20241
  nodes[0] = value;
20205
20242
  }
@@ -20321,22 +20358,30 @@ class Scorm2004ResponseValidator {
20321
20358
  nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
20322
20359
  }
20323
20360
  if (response?.delimiter2) {
20324
- const values = nodes[i].split(response.delimiter2);
20361
+ const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
20325
20362
  if (values.length === 2) {
20326
- const matches = values[0].match(formatRegex);
20327
- if (!matches) {
20363
+ if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
20328
20364
  this.context.throwSCORMError(
20329
20365
  CMIElement,
20330
20366
  scorm2004_errors.TYPE_MISMATCH,
20331
20367
  `${interaction_type}: ${value}`
20332
20368
  );
20333
20369
  } else {
20334
- if (!response.format2 || !values[1].match(new RegExp(response.format2))) {
20370
+ const matches = values[0]?.match(formatRegex);
20371
+ if (!matches) {
20335
20372
  this.context.throwSCORMError(
20336
20373
  CMIElement,
20337
20374
  scorm2004_errors.TYPE_MISMATCH,
20338
20375
  `${interaction_type}: ${value}`
20339
20376
  );
20377
+ } else {
20378
+ if (!response.format2 || !values[1]?.match(new RegExp(response.format2))) {
20379
+ this.context.throwSCORMError(
20380
+ CMIElement,
20381
+ scorm2004_errors.TYPE_MISMATCH,
20382
+ `${interaction_type}: ${value}`
20383
+ );
20384
+ }
20340
20385
  }
20341
20386
  }
20342
20387
  } else {
@@ -20347,6 +20392,9 @@ class Scorm2004ResponseValidator {
20347
20392
  );
20348
20393
  }
20349
20394
  } else {
20395
+ if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
20396
+ continue;
20397
+ }
20350
20398
  const matches = nodes[i].match(formatRegex);
20351
20399
  if (!matches && value !== "" || !matches && interaction_type === "true-false") {
20352
20400
  this.context.throwSCORMError(
@@ -20356,7 +20404,7 @@ class Scorm2004ResponseValidator {
20356
20404
  );
20357
20405
  } else {
20358
20406
  if (interaction_type === "numeric" && nodes.length > 1) {
20359
- if (Number(nodes[0]) > Number(nodes[1])) {
20407
+ if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
20360
20408
  this.context.throwSCORMError(
20361
20409
  CMIElement,
20362
20410
  scorm2004_errors.TYPE_MISMATCH,