scorm-again 3.0.3 → 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,
@@ -2098,6 +2103,7 @@ class ActivityTreeQueries {
2098
2103
  constructor(activityTree) {
2099
2104
  this.activityTree = activityTree;
2100
2105
  }
2106
+ activityTree;
2101
2107
  /**
2102
2108
  * Check if activity is in the activity tree
2103
2109
  * @param {Activity} activity - Activity to check
@@ -2297,6 +2303,8 @@ class ChoiceConstraintValidator {
2297
2303
  this.activityTree = activityTree;
2298
2304
  this.treeQueries = treeQueries;
2299
2305
  }
2306
+ activityTree;
2307
+ treeQueries;
2300
2308
  /**
2301
2309
  * Main entry point - consolidates ALL constraint validation for choice navigation
2302
2310
  * @param {Activity | null} currentActivity - Current activity (may be null if no session started)
@@ -3649,6 +3657,8 @@ class FlowTraversalService {
3649
3657
  this.activityTree = activityTree;
3650
3658
  this.ruleEngine = ruleEngine;
3651
3659
  }
3660
+ activityTree;
3661
+ ruleEngine;
3652
3662
  /**
3653
3663
  * Flow Subprocess (SB.2.3)
3654
3664
  * Traverses the activity tree in the specified direction to find a deliverable activity
@@ -3947,6 +3957,8 @@ class FlowRequestHandler {
3947
3957
  this.activityTree = activityTree;
3948
3958
  this.traversalService = traversalService;
3949
3959
  }
3960
+ activityTree;
3961
+ traversalService;
3950
3962
  /**
3951
3963
  * Start Sequencing Request Process (SB.2.5)
3952
3964
  * Initiates a new sequencing session from the root
@@ -4078,6 +4090,10 @@ class ChoiceRequestHandler {
4078
4090
  this.traversalService = traversalService;
4079
4091
  this.treeQueries = treeQueries;
4080
4092
  }
4093
+ activityTree;
4094
+ constraintValidator;
4095
+ traversalService;
4096
+ treeQueries;
4081
4097
  /**
4082
4098
  * Choice Sequencing Request Process (SB.2.9)
4083
4099
  * Processes a choice navigation request to a specific activity
@@ -4291,6 +4307,8 @@ class ExitRequestHandler {
4291
4307
  this.activityTree = activityTree;
4292
4308
  this.ruleEngine = ruleEngine;
4293
4309
  }
4310
+ activityTree;
4311
+ ruleEngine;
4294
4312
  /**
4295
4313
  * Exit Sequencing Request Process (SB.2.11)
4296
4314
  * @param {Activity} currentActivity - The current activity
@@ -4406,6 +4424,8 @@ class RetryRequestHandler {
4406
4424
  this.activityTree = activityTree;
4407
4425
  this.traversalService = traversalService;
4408
4426
  }
4427
+ activityTree;
4428
+ traversalService;
4409
4429
  /**
4410
4430
  * Retry Sequencing Request Process (SB.2.10)
4411
4431
  * @param {Activity} currentActivity - The current activity
@@ -5348,7 +5368,19 @@ class CMIValueAccessService {
5348
5368
  if (!scorm2004) {
5349
5369
  if (isFinalAttribute) {
5350
5370
  if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
5351
- 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
+ }
5352
5384
  return { error: true };
5353
5385
  }
5354
5386
  }
@@ -5540,7 +5572,8 @@ class LoggingService {
5540
5572
  getNumericLevel(level) {
5541
5573
  if (level === void 0) return LogLevelEnum.NONE;
5542
5574
  if (typeof level === "number") return level;
5543
- switch (level) {
5575
+ const normalized = typeof level === "string" ? level.toUpperCase() : level;
5576
+ switch (normalized) {
5544
5577
  case "1":
5545
5578
  case "DEBUG":
5546
5579
  return LogLevelEnum.DEBUG;
@@ -5913,6 +5946,7 @@ class OfflineStorageService {
5913
5946
  window.addEventListener("offline", this.boundOnlineStatusChangeHandler);
5914
5947
  window.addEventListener("scorm-again:network-status", this.boundCustomNetworkStatusHandler);
5915
5948
  }
5949
+ apiLog;
5916
5950
  settings;
5917
5951
  error_codes;
5918
5952
  storeName = "scorm_again_offline_data";
@@ -15750,7 +15784,8 @@ class BaseAPI {
15750
15784
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15751
15785
  } else {
15752
15786
  const result = this.storeData(false);
15753
- if ((result.errorCode ?? 0) > 0) {
15787
+ const errorCode = result.errorCode ?? 0;
15788
+ if (errorCode > 0) {
15754
15789
  if (result.errorMessage) {
15755
15790
  this.apiLog(
15756
15791
  "commit",
@@ -15765,12 +15800,12 @@ class BaseAPI {
15765
15800
  LogLevelEnum.DEBUG
15766
15801
  );
15767
15802
  }
15768
- this.throwSCORMError("api", result.errorCode);
15803
+ this.throwSCORMError("api", errorCode);
15769
15804
  }
15770
15805
  const resultValue = result?.result ?? global_constants.SCORM_FALSE;
15771
15806
  returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
15772
15807
  this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
15773
- if (checkTerminated) this.lastErrorCode = "0";
15808
+ if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
15774
15809
  this.processListeners(callbackName);
15775
15810
  if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
15776
15811
  this._offlineStorageService.hasPendingOfflineData(this._courseId).then((hasPendingData) => {
@@ -16542,6 +16577,49 @@ class CMILearnerPreference extends BaseCMI {
16542
16577
  }
16543
16578
  }
16544
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
+
16545
16623
  class CMIInteractions extends CMIArray {
16546
16624
  /**
16547
16625
  * Constructor for `cmi.interactions` Array
@@ -16751,8 +16829,7 @@ class CMIInteractionsObject extends BaseCMI {
16751
16829
  const response_type = LearnerResponses[this.type];
16752
16830
  if (response_type) {
16753
16831
  if (response_type?.delimiter) {
16754
- const delimiter = response_type.delimiter === "[,]" ? "," : response_type.delimiter;
16755
- nodes = learner_response.split(delimiter);
16832
+ nodes = splitDelimited(learner_response, response_type.delimiter);
16756
16833
  } else {
16757
16834
  nodes[0] = learner_response;
16758
16835
  }
@@ -16760,10 +16837,10 @@ class CMIInteractionsObject extends BaseCMI {
16760
16837
  const formatRegex = new RegExp(response_type.format);
16761
16838
  for (let i = 0; i < nodes.length; i++) {
16762
16839
  if (response_type?.delimiter2) {
16763
- const delimiter2 = response_type.delimiter2 === "[.]" ? "." : response_type.delimiter2;
16764
- 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);
16765
16842
  if (values?.length === 2) {
16766
- if (this.type === "performance" && (values[0] === "" || values[1] === "")) {
16843
+ if (this.type === "performance" && values[0] === "" && values[1] === "") {
16767
16844
  throw new Scorm2004ValidationError(
16768
16845
  this._cmi_element + ".learner_response",
16769
16846
  scorm2004_errors.TYPE_MISMATCH
@@ -16982,30 +17059,13 @@ class CMIInteractionsObjectivesObject extends BaseCMI {
16982
17059
  return result;
16983
17060
  }
16984
17061
  }
16985
- function stripBrackets(delim) {
16986
- return delim.replace(/[[\]]/g, "");
16987
- }
16988
- function escapeRegex(s) {
16989
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16990
- }
16991
- function splitUnescaped(text, delim) {
16992
- const reDelim = escapeRegex(delim);
16993
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
16994
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
16995
- return text.split(splitRe).map((part) => part.replace(unescapeRe, delim));
16996
- }
16997
- function splitFirstUnescaped(text, delim) {
16998
- const reDelim = escapeRegex(delim);
16999
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
17000
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
17001
- const parts = text.split(splitRe);
17002
- const firstPart = parts[0] ?? "";
17003
- if (parts.length === 1) {
17004
- 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, "");
17005
17067
  }
17006
- const part1 = firstPart.replace(unescapeRe, delim);
17007
- const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
17008
- return [part1, part2];
17068
+ return result;
17009
17069
  }
17010
17070
  function validatePattern(type, pattern, responseDef) {
17011
17071
  if (pattern.trim() !== pattern) {
@@ -17014,8 +17074,7 @@ function validatePattern(type, pattern, responseDef) {
17014
17074
  scorm2004_errors.TYPE_MISMATCH
17015
17075
  );
17016
17076
  }
17017
- const subDelim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
17018
- const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
17077
+ const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
17019
17078
  for (const raw of rawNodes) {
17020
17079
  if (raw.trim() !== raw) {
17021
17080
  throw new Scorm2004ValidationError(
@@ -17027,20 +17086,14 @@ function validatePattern(type, pattern, responseDef) {
17027
17086
  if (type === "fill-in" && pattern === "") {
17028
17087
  return;
17029
17088
  }
17030
- const delim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
17031
- let nodes;
17032
- if (delim1) {
17033
- nodes = splitUnescaped(pattern, delim1);
17034
- } else {
17035
- nodes = [pattern];
17036
- }
17089
+ const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
17037
17090
  if (!responseDef.delimiter && pattern.includes(",")) {
17038
17091
  throw new Scorm2004ValidationError(
17039
17092
  "cmi.interactions.n.correct_responses.n.pattern",
17040
17093
  scorm2004_errors.TYPE_MISMATCH
17041
17094
  );
17042
17095
  }
17043
- if (responseDef.unique || responseDef.duplicate === false) {
17096
+ if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
17044
17097
  const seen = new Set(nodes);
17045
17098
  if (seen.size !== nodes.length) {
17046
17099
  throw new Scorm2004ValidationError(
@@ -17072,8 +17125,7 @@ function validatePattern(type, pattern, responseDef) {
17072
17125
  scorm2004_errors.TYPE_MISMATCH
17073
17126
  );
17074
17127
  }
17075
- const delim = stripBrackets(delimBracketed);
17076
- 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);
17077
17129
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
17078
17130
  throw new Scorm2004ValidationError(
17079
17131
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -17090,15 +17142,17 @@ function validatePattern(type, pattern, responseDef) {
17090
17142
  for (const node of nodes) {
17091
17143
  switch (type) {
17092
17144
  case "numeric": {
17093
- const numDelim = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : ":";
17094
- const nums = node.split(numDelim);
17095
- if (nums.length < 1 || nums.length > 2) {
17096
- throw new Scorm2004ValidationError(
17097
- "cmi.interactions.n.correct_responses.n.pattern",
17098
- scorm2004_errors.TYPE_MISMATCH
17099
- );
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;
17100
17154
  }
17101
- nums.forEach(checkSingle);
17155
+ checkSingle(node);
17102
17156
  break;
17103
17157
  }
17104
17158
  case "performance": {
@@ -17109,8 +17163,8 @@ function validatePattern(type, pattern, responseDef) {
17109
17163
  scorm2004_errors.TYPE_MISMATCH
17110
17164
  );
17111
17165
  }
17112
- const delim = stripBrackets(delimBracketed);
17113
- const parts = splitFirstUnescaped(node, delim);
17166
+ const record = stripResponsePrefixes(node);
17167
+ const parts = splitFirstDelimited(record, delimBracketed);
17114
17168
  if (parts.length !== 2) {
17115
17169
  throw new Scorm2004ValidationError(
17116
17170
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -17118,7 +17172,7 @@ function validatePattern(type, pattern, responseDef) {
17118
17172
  );
17119
17173
  }
17120
17174
  const [part1, part2] = parts;
17121
- if (part1 === "" || part2 === "" || part1 === part2) {
17175
+ if (part1 === "" && part2 === "") {
17122
17176
  throw new Scorm2004ValidationError(
17123
17177
  "cmi.interactions.n.correct_responses.n.pattern",
17124
17178
  scorm2004_errors.TYPE_MISMATCH
@@ -20182,7 +20236,7 @@ class Scorm2004ResponseValidator {
20182
20236
  checkValidResponseType(CMIElement, response_type, value, interaction_type) {
20183
20237
  let nodes = [];
20184
20238
  if (response_type?.delimiter) {
20185
- nodes = String(value).split(response_type.delimiter);
20239
+ nodes = splitDelimited(String(value), response_type.delimiter);
20186
20240
  } else {
20187
20241
  nodes[0] = value;
20188
20242
  }
@@ -20304,22 +20358,30 @@ class Scorm2004ResponseValidator {
20304
20358
  nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
20305
20359
  }
20306
20360
  if (response?.delimiter2) {
20307
- const values = nodes[i].split(response.delimiter2);
20361
+ const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
20308
20362
  if (values.length === 2) {
20309
- const matches = values[0].match(formatRegex);
20310
- if (!matches) {
20363
+ if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
20311
20364
  this.context.throwSCORMError(
20312
20365
  CMIElement,
20313
20366
  scorm2004_errors.TYPE_MISMATCH,
20314
20367
  `${interaction_type}: ${value}`
20315
20368
  );
20316
20369
  } else {
20317
- if (!response.format2 || !values[1].match(new RegExp(response.format2))) {
20370
+ const matches = values[0]?.match(formatRegex);
20371
+ if (!matches) {
20318
20372
  this.context.throwSCORMError(
20319
20373
  CMIElement,
20320
20374
  scorm2004_errors.TYPE_MISMATCH,
20321
20375
  `${interaction_type}: ${value}`
20322
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
+ }
20323
20385
  }
20324
20386
  }
20325
20387
  } else {
@@ -20330,6 +20392,9 @@ class Scorm2004ResponseValidator {
20330
20392
  );
20331
20393
  }
20332
20394
  } else {
20395
+ if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
20396
+ continue;
20397
+ }
20333
20398
  const matches = nodes[i].match(formatRegex);
20334
20399
  if (!matches && value !== "" || !matches && interaction_type === "true-false") {
20335
20400
  this.context.throwSCORMError(
@@ -20339,7 +20404,7 @@ class Scorm2004ResponseValidator {
20339
20404
  );
20340
20405
  } else {
20341
20406
  if (interaction_type === "numeric" && nodes.length > 1) {
20342
- if (Number(nodes[0]) > Number(nodes[1])) {
20407
+ if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
20343
20408
  this.context.throwSCORMError(
20344
20409
  CMIElement,
20345
20410
  scorm2004_errors.TYPE_MISMATCH,