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.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
+ # scorm-again
2
+
3
+ A modern, fully-tested JavaScript runtime for SCORM 1.2 and SCORM 2004.
4
+
5
+ ---
6
+ <details>
7
+ <summary><b>Audits & consulting →</b></summary>
8
+
9
+ I do one-week, fixed-price code audits for B2B SaaS companies on AWS:
10
+
11
+ - Multi-Tenant SaaS on AWS: architecture, cost, and SOC 2 alignment
12
+ - AI-on-AWS for Regulated SaaS: Bedrock, RAG, and guardrails under SOC 2, HIPAA, or 21 CFR Part 11
13
+
14
+ Fixed price, written report, walkthrough call. Details at [audits.putney.io](https://audits.putney.io).
15
+
16
+ </details>
17
+
18
+ ---
19
+
1
20
  [![Github Actions](https://img.shields.io/github/actions/workflow/status/jcputney/scorm-again/main.yml?style=for-the-badge "Build Status")](https://github.com/jcputney/scorm-again/actions)
2
21
  [![Codecov](https://img.shields.io/codecov/c/github/jcputney/scorm-again?style=for-the-badge "Code Coverage")](https://codecov.io/gh/jcputney/scorm-again)
3
22
  ![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hy/scorm-again?style=for-the-badge&label=jsDelivr%20Downloads)
@@ -7,10 +26,6 @@
7
26
  ![GitHub License](https://img.shields.io/github/license/jcputney/scorm-again?style=for-the-badge)
8
27
  [![donate](https://img.shields.io/badge/paypal-donate-success?style=for-the-badge)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NF5MPZJAV26LE)
9
28
 
10
- # scorm-again
11
-
12
- A modern, fully-tested JavaScript runtime for SCORM 1.2 and SCORM 2004.
13
-
14
29
  > **Upgrading from v2.x?** See the [Migration Guide](MIGRATION.md) for breaking changes and migration steps.
15
30
 
16
31
  ## Overview
@@ -1160,6 +1160,11 @@ const scorm2004_regex = {
1160
1160
  progress_range: "0#1"
1161
1161
  };
1162
1162
 
1163
+ const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
1164
+ const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
1165
+ const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
1166
+ const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
1167
+ const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
1163
1168
  const LearnerResponses = {
1164
1169
  "true-false": {
1165
1170
  format: "^true$|^false$",
@@ -1194,8 +1199,8 @@ const LearnerResponses = {
1194
1199
  unique: false
1195
1200
  },
1196
1201
  performance: {
1197
- format: "^$|" + scorm2004_regex.CMIShortIdentifier,
1198
- format2: scorm2004_regex.CMIDecimal + "|^$|" + scorm2004_regex.CMIShortIdentifier,
1202
+ format: PERFORMANCE_STEP_NAME,
1203
+ format2: LR_PERFORMANCE_STEP_ANSWER,
1199
1204
  max: 250,
1200
1205
  delimiter: "[,]",
1201
1206
  delimiter2: "[.]",
@@ -1271,10 +1276,10 @@ const CorrectResponses = {
1271
1276
  delimiter2: "[.]",
1272
1277
  unique: false,
1273
1278
  duplicate: false,
1274
- // step_name must be a non-empty short identifier
1275
- format: scorm2004_regex.CMIShortIdentifier,
1276
- // step_answer may be short identifier or numeric range (<decimal>[:<decimal>])
1277
- format2: `^(${scorm2004_regex.CMIShortIdentifier})$|^(?:\\d+(?:\\.\\d+)?(?::\\d+(?:\\.\\d+)?)?)$`
1279
+ // step_name: optional short_identifier_type
1280
+ format: PERFORMANCE_STEP_NAME,
1281
+ // step_answer: optional characterstring (spaces allowed) or numeric range
1282
+ format2: CR_PERFORMANCE_STEP_ANSWER
1278
1283
  },
1279
1284
  sequencing: {
1280
1285
  max: 36,
@@ -2318,6 +2323,7 @@ class ActivityTreeQueries {
2318
2323
  constructor(activityTree) {
2319
2324
  this.activityTree = activityTree;
2320
2325
  }
2326
+ activityTree;
2321
2327
  /**
2322
2328
  * Check if activity is in the activity tree
2323
2329
  * @param {Activity} activity - Activity to check
@@ -2517,6 +2523,8 @@ class ChoiceConstraintValidator {
2517
2523
  this.activityTree = activityTree;
2518
2524
  this.treeQueries = treeQueries;
2519
2525
  }
2526
+ activityTree;
2527
+ treeQueries;
2520
2528
  /**
2521
2529
  * Main entry point - consolidates ALL constraint validation for choice navigation
2522
2530
  * @param {Activity | null} currentActivity - Current activity (may be null if no session started)
@@ -3869,6 +3877,8 @@ class FlowTraversalService {
3869
3877
  this.activityTree = activityTree;
3870
3878
  this.ruleEngine = ruleEngine;
3871
3879
  }
3880
+ activityTree;
3881
+ ruleEngine;
3872
3882
  /**
3873
3883
  * Flow Subprocess (SB.2.3)
3874
3884
  * Traverses the activity tree in the specified direction to find a deliverable activity
@@ -4167,6 +4177,8 @@ class FlowRequestHandler {
4167
4177
  this.activityTree = activityTree;
4168
4178
  this.traversalService = traversalService;
4169
4179
  }
4180
+ activityTree;
4181
+ traversalService;
4170
4182
  /**
4171
4183
  * Start Sequencing Request Process (SB.2.5)
4172
4184
  * Initiates a new sequencing session from the root
@@ -4298,6 +4310,10 @@ class ChoiceRequestHandler {
4298
4310
  this.traversalService = traversalService;
4299
4311
  this.treeQueries = treeQueries;
4300
4312
  }
4313
+ activityTree;
4314
+ constraintValidator;
4315
+ traversalService;
4316
+ treeQueries;
4301
4317
  /**
4302
4318
  * Choice Sequencing Request Process (SB.2.9)
4303
4319
  * Processes a choice navigation request to a specific activity
@@ -4511,6 +4527,8 @@ class ExitRequestHandler {
4511
4527
  this.activityTree = activityTree;
4512
4528
  this.ruleEngine = ruleEngine;
4513
4529
  }
4530
+ activityTree;
4531
+ ruleEngine;
4514
4532
  /**
4515
4533
  * Exit Sequencing Request Process (SB.2.11)
4516
4534
  * @param {Activity} currentActivity - The current activity
@@ -4626,6 +4644,8 @@ class RetryRequestHandler {
4626
4644
  this.activityTree = activityTree;
4627
4645
  this.traversalService = traversalService;
4628
4646
  }
4647
+ activityTree;
4648
+ traversalService;
4629
4649
  /**
4630
4650
  * Retry Sequencing Request Process (SB.2.10)
4631
4651
  * @param {Activity} currentActivity - The current activity
@@ -5568,7 +5588,19 @@ class CMIValueAccessService {
5568
5588
  if (!scorm2004) {
5569
5589
  if (isFinalAttribute) {
5570
5590
  if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
5571
- this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5591
+ if (attribute === "_children") {
5592
+ this.context.throwSCORMError(
5593
+ CMIElement,
5594
+ getErrorCode(this.context.errorCodes, "CHILDREN_ERROR")
5595
+ );
5596
+ } else if (attribute === "_count") {
5597
+ this.context.throwSCORMError(
5598
+ CMIElement,
5599
+ getErrorCode(this.context.errorCodes, "COUNT_ERROR")
5600
+ );
5601
+ } else {
5602
+ this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5603
+ }
5572
5604
  return { error: true };
5573
5605
  }
5574
5606
  }
@@ -5760,7 +5792,8 @@ class LoggingService {
5760
5792
  getNumericLevel(level) {
5761
5793
  if (level === void 0) return LogLevelEnum.NONE;
5762
5794
  if (typeof level === "number") return level;
5763
- switch (level) {
5795
+ const normalized = typeof level === "string" ? level.toUpperCase() : level;
5796
+ switch (normalized) {
5764
5797
  case "1":
5765
5798
  case "DEBUG":
5766
5799
  return LogLevelEnum.DEBUG;
@@ -6133,6 +6166,7 @@ class OfflineStorageService {
6133
6166
  window.addEventListener("offline", this.boundOnlineStatusChangeHandler);
6134
6167
  window.addEventListener("scorm-again:network-status", this.boundCustomNetworkStatusHandler);
6135
6168
  }
6169
+ apiLog;
6136
6170
  settings;
6137
6171
  error_codes;
6138
6172
  storeName = "scorm_again_offline_data";
@@ -15970,7 +16004,8 @@ class BaseAPI {
15970
16004
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15971
16005
  } else {
15972
16006
  const result = this.storeData(false);
15973
- if ((result.errorCode ?? 0) > 0) {
16007
+ const errorCode = result.errorCode ?? 0;
16008
+ if (errorCode > 0) {
15974
16009
  if (result.errorMessage) {
15975
16010
  this.apiLog(
15976
16011
  "commit",
@@ -15985,12 +16020,12 @@ class BaseAPI {
15985
16020
  LogLevelEnum.DEBUG
15986
16021
  );
15987
16022
  }
15988
- this.throwSCORMError("api", result.errorCode);
16023
+ this.throwSCORMError("api", errorCode);
15989
16024
  }
15990
16025
  const resultValue = result?.result ?? global_constants.SCORM_FALSE;
15991
16026
  returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
15992
16027
  this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
15993
- if (checkTerminated) this.lastErrorCode = "0";
16028
+ if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
15994
16029
  this.processListeners(callbackName);
15995
16030
  if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
15996
16031
  this._offlineStorageService.hasPendingOfflineData(this._courseId).then((hasPendingData) => {
@@ -18913,6 +18948,49 @@ class CMILearnerPreference extends BaseCMI {
18913
18948
  }
18914
18949
  }
18915
18950
 
18951
+ function stripBrackets(delim) {
18952
+ return delim.replace(/[[\]]/g, "");
18953
+ }
18954
+ function escapeRegex(s) {
18955
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18956
+ }
18957
+ function splitDelimited(value, bracketed) {
18958
+ if (!bracketed) {
18959
+ return [value];
18960
+ }
18961
+ if (value.includes(bracketed)) {
18962
+ return value.split(bracketed);
18963
+ }
18964
+ const bare = stripBrackets(bracketed);
18965
+ if (!bare) {
18966
+ return [value];
18967
+ }
18968
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`, "g");
18969
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
18970
+ return value.split(splitRe).map((part) => part.replace(unescapeRe, bare));
18971
+ }
18972
+ function splitFirstDelimited(value, bracketed) {
18973
+ if (!bracketed) {
18974
+ return [value];
18975
+ }
18976
+ if (value.includes(bracketed)) {
18977
+ const idx = value.indexOf(bracketed);
18978
+ return [value.slice(0, idx), value.slice(idx + bracketed.length)];
18979
+ }
18980
+ const bare = stripBrackets(bracketed);
18981
+ if (!bare) {
18982
+ return [value];
18983
+ }
18984
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`);
18985
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
18986
+ const parts = value.split(splitRe);
18987
+ const first = (parts[0] ?? "").replace(unescapeRe, bare);
18988
+ if (parts.length === 1) {
18989
+ return [first];
18990
+ }
18991
+ return [first, parts.slice(1).join(bare).replace(unescapeRe, bare)];
18992
+ }
18993
+
18916
18994
  class CMIInteractions extends CMIArray {
18917
18995
  /**
18918
18996
  * Constructor for `cmi.interactions` Array
@@ -19122,8 +19200,7 @@ class CMIInteractionsObject extends BaseCMI {
19122
19200
  const response_type = LearnerResponses[this.type];
19123
19201
  if (response_type) {
19124
19202
  if (response_type?.delimiter) {
19125
- const delimiter = response_type.delimiter === "[,]" ? "," : response_type.delimiter;
19126
- nodes = learner_response.split(delimiter);
19203
+ nodes = splitDelimited(learner_response, response_type.delimiter);
19127
19204
  } else {
19128
19205
  nodes[0] = learner_response;
19129
19206
  }
@@ -19131,10 +19208,10 @@ class CMIInteractionsObject extends BaseCMI {
19131
19208
  const formatRegex = new RegExp(response_type.format);
19132
19209
  for (let i = 0; i < nodes.length; i++) {
19133
19210
  if (response_type?.delimiter2) {
19134
- const delimiter2 = response_type.delimiter2 === "[.]" ? "." : response_type.delimiter2;
19135
- const values = nodes[i]?.split(delimiter2);
19211
+ const node = nodes[i] ?? "";
19212
+ const values = this.type === "performance" ? splitFirstDelimited(node, response_type.delimiter2) : splitDelimited(node, response_type.delimiter2);
19136
19213
  if (values?.length === 2) {
19137
- if (this.type === "performance" && (values[0] === "" || values[1] === "")) {
19214
+ if (this.type === "performance" && values[0] === "" && values[1] === "") {
19138
19215
  throw new Scorm2004ValidationError(
19139
19216
  this._cmi_element + ".learner_response",
19140
19217
  scorm2004_errors.TYPE_MISMATCH
@@ -19353,30 +19430,13 @@ class CMIInteractionsObjectivesObject extends BaseCMI {
19353
19430
  return result;
19354
19431
  }
19355
19432
  }
19356
- function stripBrackets(delim) {
19357
- return delim.replace(/[[\]]/g, "");
19358
- }
19359
- function escapeRegex(s) {
19360
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19361
- }
19362
- function splitUnescaped(text, delim) {
19363
- const reDelim = escapeRegex(delim);
19364
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
19365
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
19366
- return text.split(splitRe).map((part) => part.replace(unescapeRe, delim));
19367
- }
19368
- function splitFirstUnescaped(text, delim) {
19369
- const reDelim = escapeRegex(delim);
19370
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
19371
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
19372
- const parts = text.split(splitRe);
19373
- const firstPart = parts[0] ?? "";
19374
- if (parts.length === 1) {
19375
- return [firstPart.replace(unescapeRe, delim)];
19433
+ const RESPONSE_PREFIX_RE = /^\{(?:lang|case_matters|order_matters)=[^}]+\}/;
19434
+ function stripResponsePrefixes(node) {
19435
+ let result = node;
19436
+ while (RESPONSE_PREFIX_RE.test(result)) {
19437
+ result = result.replace(RESPONSE_PREFIX_RE, "");
19376
19438
  }
19377
- const part1 = firstPart.replace(unescapeRe, delim);
19378
- const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
19379
- return [part1, part2];
19439
+ return result;
19380
19440
  }
19381
19441
  function validatePattern(type, pattern, responseDef) {
19382
19442
  if (pattern.trim() !== pattern) {
@@ -19385,8 +19445,7 @@ function validatePattern(type, pattern, responseDef) {
19385
19445
  scorm2004_errors.TYPE_MISMATCH
19386
19446
  );
19387
19447
  }
19388
- const subDelim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
19389
- const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
19448
+ const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
19390
19449
  for (const raw of rawNodes) {
19391
19450
  if (raw.trim() !== raw) {
19392
19451
  throw new Scorm2004ValidationError(
@@ -19398,20 +19457,14 @@ function validatePattern(type, pattern, responseDef) {
19398
19457
  if (type === "fill-in" && pattern === "") {
19399
19458
  return;
19400
19459
  }
19401
- const delim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
19402
- let nodes;
19403
- if (delim1) {
19404
- nodes = splitUnescaped(pattern, delim1);
19405
- } else {
19406
- nodes = [pattern];
19407
- }
19460
+ const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
19408
19461
  if (!responseDef.delimiter && pattern.includes(",")) {
19409
19462
  throw new Scorm2004ValidationError(
19410
19463
  "cmi.interactions.n.correct_responses.n.pattern",
19411
19464
  scorm2004_errors.TYPE_MISMATCH
19412
19465
  );
19413
19466
  }
19414
- if (responseDef.unique || responseDef.duplicate === false) {
19467
+ if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
19415
19468
  const seen = new Set(nodes);
19416
19469
  if (seen.size !== nodes.length) {
19417
19470
  throw new Scorm2004ValidationError(
@@ -19443,8 +19496,7 @@ function validatePattern(type, pattern, responseDef) {
19443
19496
  scorm2004_errors.TYPE_MISMATCH
19444
19497
  );
19445
19498
  }
19446
- const delim = stripBrackets(delimBracketed);
19447
- const parts = value.split(new RegExp(`(?<!\\\\)${escapeRegex(delim)}`, "g")).map((n) => n.replace(new RegExp(`\\\\${escapeRegex(delim)}`, "g"), delim));
19499
+ const parts = splitDelimited(value, delimBracketed);
19448
19500
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
19449
19501
  throw new Scorm2004ValidationError(
19450
19502
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -19461,15 +19513,17 @@ function validatePattern(type, pattern, responseDef) {
19461
19513
  for (const node of nodes) {
19462
19514
  switch (type) {
19463
19515
  case "numeric": {
19464
- const numDelim = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : ":";
19465
- const nums = node.split(numDelim);
19466
- if (nums.length < 1 || nums.length > 2) {
19467
- throw new Scorm2004ValidationError(
19468
- "cmi.interactions.n.correct_responses.n.pattern",
19469
- scorm2004_errors.TYPE_MISMATCH
19470
- );
19516
+ if (node === "") {
19517
+ const bracketedRange = nodes.length >= 2 && !!responseDef.delimiter && pattern.includes(responseDef.delimiter);
19518
+ if (!bracketedRange) {
19519
+ throw new Scorm2004ValidationError(
19520
+ "cmi.interactions.n.correct_responses.n.pattern",
19521
+ scorm2004_errors.TYPE_MISMATCH
19522
+ );
19523
+ }
19524
+ break;
19471
19525
  }
19472
- nums.forEach(checkSingle);
19526
+ checkSingle(node);
19473
19527
  break;
19474
19528
  }
19475
19529
  case "performance": {
@@ -19480,8 +19534,8 @@ function validatePattern(type, pattern, responseDef) {
19480
19534
  scorm2004_errors.TYPE_MISMATCH
19481
19535
  );
19482
19536
  }
19483
- const delim = stripBrackets(delimBracketed);
19484
- const parts = splitFirstUnescaped(node, delim);
19537
+ const record = stripResponsePrefixes(node);
19538
+ const parts = splitFirstDelimited(record, delimBracketed);
19485
19539
  if (parts.length !== 2) {
19486
19540
  throw new Scorm2004ValidationError(
19487
19541
  "cmi.interactions.n.correct_responses.n.pattern",
@@ -19489,7 +19543,7 @@ function validatePattern(type, pattern, responseDef) {
19489
19543
  );
19490
19544
  }
19491
19545
  const [part1, part2] = parts;
19492
- if (part1 === "" || part2 === "" || part1 === part2) {
19546
+ if (part1 === "" && part2 === "") {
19493
19547
  throw new Scorm2004ValidationError(
19494
19548
  "cmi.interactions.n.correct_responses.n.pattern",
19495
19549
  scorm2004_errors.TYPE_MISMATCH
@@ -22368,7 +22422,7 @@ class Scorm2004ResponseValidator {
22368
22422
  checkValidResponseType(CMIElement, response_type, value, interaction_type) {
22369
22423
  let nodes = [];
22370
22424
  if (response_type?.delimiter) {
22371
- nodes = String(value).split(response_type.delimiter);
22425
+ nodes = splitDelimited(String(value), response_type.delimiter);
22372
22426
  } else {
22373
22427
  nodes[0] = value;
22374
22428
  }
@@ -22490,22 +22544,30 @@ class Scorm2004ResponseValidator {
22490
22544
  nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
22491
22545
  }
22492
22546
  if (response?.delimiter2) {
22493
- const values = nodes[i].split(response.delimiter2);
22547
+ const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
22494
22548
  if (values.length === 2) {
22495
- const matches = values[0].match(formatRegex);
22496
- if (!matches) {
22549
+ if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
22497
22550
  this.context.throwSCORMError(
22498
22551
  CMIElement,
22499
22552
  scorm2004_errors.TYPE_MISMATCH,
22500
22553
  `${interaction_type}: ${value}`
22501
22554
  );
22502
22555
  } else {
22503
- if (!response.format2 || !values[1].match(new RegExp(response.format2))) {
22556
+ const matches = values[0]?.match(formatRegex);
22557
+ if (!matches) {
22504
22558
  this.context.throwSCORMError(
22505
22559
  CMIElement,
22506
22560
  scorm2004_errors.TYPE_MISMATCH,
22507
22561
  `${interaction_type}: ${value}`
22508
22562
  );
22563
+ } else {
22564
+ if (!response.format2 || !values[1]?.match(new RegExp(response.format2))) {
22565
+ this.context.throwSCORMError(
22566
+ CMIElement,
22567
+ scorm2004_errors.TYPE_MISMATCH,
22568
+ `${interaction_type}: ${value}`
22569
+ );
22570
+ }
22509
22571
  }
22510
22572
  }
22511
22573
  } else {
@@ -22516,6 +22578,9 @@ class Scorm2004ResponseValidator {
22516
22578
  );
22517
22579
  }
22518
22580
  } else {
22581
+ if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
22582
+ continue;
22583
+ }
22519
22584
  const matches = nodes[i].match(formatRegex);
22520
22585
  if (!matches && value !== "" || !matches && interaction_type === "true-false") {
22521
22586
  this.context.throwSCORMError(
@@ -22525,7 +22590,7 @@ class Scorm2004ResponseValidator {
22525
22590
  );
22526
22591
  } else {
22527
22592
  if (interaction_type === "numeric" && nodes.length > 1) {
22528
- if (Number(nodes[0]) > Number(nodes[1])) {
22593
+ if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
22529
22594
  this.context.throwSCORMError(
22530
22595
  CMIElement,
22531
22596
  scorm2004_errors.TYPE_MISMATCH,