@longsightgroup/qti3-core 0.1.1 → 0.2.0
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 +1 -0
- package/dist/catalog.d.ts +32 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +126 -0
- package/dist/catalog.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +293 -6
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +3 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +68 -3
- package/dist/session.js.map +1 -1
- package/dist/support.js +7 -1
- package/dist/support.js.map +1 -1
- package/dist/tts.d.ts +61 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +368 -0
- package/dist/tts.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +243 -11
- package/dist/validation.js.map +1 -1
- package/package.json +4 -2
- package/src/catalog.ts +193 -0
- package/src/index.ts +85 -0
- package/src/parser.ts +1859 -0
- package/src/session.ts +2284 -0
- package/src/support.ts +253 -0
- package/src/tts.ts +555 -0
- package/src/types.ts +703 -0
- package/src/validation.ts +2449 -0
- package/src/xml.ts +139 -0
package/dist/validation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
1
2
|
const BUILT_IN_COMPLETION_STATUS = "completionStatus";
|
|
2
3
|
export function validateAssessmentItem(document) {
|
|
3
4
|
const diagnostics = [];
|
|
@@ -9,6 +10,7 @@ export function validateAssessmentItem(document) {
|
|
|
9
10
|
validateModalFeedback(item, diagnostics);
|
|
10
11
|
validateCatalogInfo(item, diagnostics);
|
|
11
12
|
validateStylesheets(item, diagnostics);
|
|
13
|
+
diagnostics.push(...validateQtiDataSsmlMetadata(item));
|
|
12
14
|
validateProcessingReferences(item, diagnostics);
|
|
13
15
|
return {
|
|
14
16
|
ok: diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
|
|
@@ -1141,7 +1143,9 @@ function validateInteractions(item, diagnostics) {
|
|
|
1141
1143
|
validateInteractionChoices(interaction, diagnostics);
|
|
1142
1144
|
validateInteractionChildren(interaction, diagnostics);
|
|
1143
1145
|
validateInteractionRequiredAttributes(interaction, diagnostics);
|
|
1146
|
+
validatePortableCustomInteraction(interaction, item, diagnostics);
|
|
1144
1147
|
validateInteractionLimitAttributes(interaction, diagnostics);
|
|
1148
|
+
validateGraphicHotspotObjectDimensions(interaction, diagnostics);
|
|
1145
1149
|
validateCorrectResponseReferences(interaction, interaction.responseIdentifier
|
|
1146
1150
|
? responseDeclarations.get(interaction.responseIdentifier)
|
|
1147
1151
|
: undefined, diagnostics);
|
|
@@ -1152,7 +1156,7 @@ function validateInteractions(item, diagnostics) {
|
|
|
1152
1156
|
}
|
|
1153
1157
|
function validateInteractionResponseReference(interaction, responseIdentifiers, diagnostics) {
|
|
1154
1158
|
if (!interaction.responseIdentifier) {
|
|
1155
|
-
if (interaction.type !== "endAttempt"
|
|
1159
|
+
if (interaction.type !== "endAttempt") {
|
|
1156
1160
|
diagnostics.push({
|
|
1157
1161
|
code: "interaction.responseIdentifier",
|
|
1158
1162
|
severity: "error",
|
|
@@ -1323,18 +1327,19 @@ function validateInteractionChildren(interaction, diagnostics) {
|
|
|
1323
1327
|
}
|
|
1324
1328
|
}
|
|
1325
1329
|
function validateInteractionRequiredAttributes(interaction, diagnostics) {
|
|
1326
|
-
if (requiresObject(interaction) && !interaction
|
|
1330
|
+
if (requiresObject(interaction) && !hasRequiredObjectAsset(interaction)) {
|
|
1327
1331
|
diagnostics.push({
|
|
1328
1332
|
code: "interaction.object.required",
|
|
1329
1333
|
severity: "error",
|
|
1330
|
-
message:
|
|
1334
|
+
message: interaction.type === "drawing"
|
|
1335
|
+
? `${interaction.qtiName} requires an object, img, or picture canvas with a data/src attribute.`
|
|
1336
|
+
: `${interaction.qtiName} requires an object, img, audio, or video child with a data/src attribute or media sources.`,
|
|
1331
1337
|
path: interaction.source?.path,
|
|
1332
1338
|
source: interaction.source,
|
|
1333
1339
|
});
|
|
1334
1340
|
}
|
|
1335
1341
|
if (interaction.type === "portableCustom") {
|
|
1336
1342
|
requireInteractionAttribute(interaction, "custom-interaction-type-identifier", "interaction.portableCustom.typeIdentifier", diagnostics);
|
|
1337
|
-
requireInteractionAttribute(interaction, "module", "interaction.portableCustom.module", diagnostics);
|
|
1338
1343
|
}
|
|
1339
1344
|
if (interaction.type === "slider") {
|
|
1340
1345
|
const lower = interaction.attributes["lower-bound"];
|
|
@@ -1374,7 +1379,16 @@ function requiresObject(interaction) {
|
|
|
1374
1379
|
interaction.type === "hotspot" ||
|
|
1375
1380
|
interaction.type === "selectPoint" ||
|
|
1376
1381
|
interaction.type === "positionObject" ||
|
|
1377
|
-
interaction.type === "media"
|
|
1382
|
+
interaction.type === "media" ||
|
|
1383
|
+
interaction.type === "drawing");
|
|
1384
|
+
}
|
|
1385
|
+
function hasRequiredObjectAsset(interaction) {
|
|
1386
|
+
if (interaction.type === "media") {
|
|
1387
|
+
return Boolean(interaction.object?.data || interaction.object?.sources.some((source) => Boolean(source.src)));
|
|
1388
|
+
}
|
|
1389
|
+
if (interaction.type === "drawing")
|
|
1390
|
+
return Boolean(interaction.object?.data);
|
|
1391
|
+
return Boolean(interaction.object?.data);
|
|
1378
1392
|
}
|
|
1379
1393
|
function requireInteractionAttribute(interaction, attribute, code, diagnostics) {
|
|
1380
1394
|
if (interaction.attributes[attribute])
|
|
@@ -1387,6 +1401,95 @@ function requireInteractionAttribute(interaction, attribute, code, diagnostics)
|
|
|
1387
1401
|
source: interaction.source,
|
|
1388
1402
|
});
|
|
1389
1403
|
}
|
|
1404
|
+
function validatePortableCustomInteraction(interaction, item, diagnostics) {
|
|
1405
|
+
if (interaction.type !== "portableCustom")
|
|
1406
|
+
return;
|
|
1407
|
+
const definition = interaction.portableCustom;
|
|
1408
|
+
if (!definition)
|
|
1409
|
+
return;
|
|
1410
|
+
const configuredModules = definition.interactionModules?.modules ?? [];
|
|
1411
|
+
const hasModuleAttribute = Boolean(definition.module?.trim());
|
|
1412
|
+
const hasConfiguredModule = configuredModules.some((module) => Boolean(module.id?.trim()));
|
|
1413
|
+
if (!hasModuleAttribute && !hasConfiguredModule) {
|
|
1414
|
+
diagnostics.push({
|
|
1415
|
+
code: "interaction.portableCustom.module",
|
|
1416
|
+
severity: "error",
|
|
1417
|
+
message: `${interaction.qtiName} requires a module attribute or at least one qti-interaction-module id.`,
|
|
1418
|
+
path: interaction.source?.path,
|
|
1419
|
+
source: interaction.source,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
for (const module of configuredModules) {
|
|
1423
|
+
if (!module.id?.trim()) {
|
|
1424
|
+
diagnostics.push({
|
|
1425
|
+
code: "interaction.portableCustom.moduleId",
|
|
1426
|
+
severity: "error",
|
|
1427
|
+
message: "qti-interaction-module requires a non-empty id.",
|
|
1428
|
+
path: module.source?.path,
|
|
1429
|
+
source: module.source,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
warnExternalPortableCustomUrl(module.primaryPath, module.source, diagnostics);
|
|
1433
|
+
warnExternalPortableCustomUrl(module.fallbackPath, module.source, diagnostics);
|
|
1434
|
+
}
|
|
1435
|
+
warnExternalPortableCustomUrl(definition.interactionModules?.primaryConfiguration, definition.interactionModules?.source, diagnostics);
|
|
1436
|
+
warnExternalPortableCustomUrl(definition.interactionModules?.secondaryConfiguration, definition.interactionModules?.source, diagnostics);
|
|
1437
|
+
const templateIdentifiers = new Set(item.templateDeclarations.map((declaration) => declaration.identifier));
|
|
1438
|
+
for (const variable of definition.templateVariables) {
|
|
1439
|
+
if (!variable.identifier?.trim()) {
|
|
1440
|
+
diagnostics.push({
|
|
1441
|
+
code: "interaction.portableCustom.templateVariable",
|
|
1442
|
+
severity: "error",
|
|
1443
|
+
message: "qti-template-variable requires template-identifier or identifier.",
|
|
1444
|
+
path: variable.source?.path,
|
|
1445
|
+
source: variable.source,
|
|
1446
|
+
});
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (!templateIdentifiers.has(variable.identifier)) {
|
|
1450
|
+
diagnostics.push({
|
|
1451
|
+
code: "interaction.portableCustom.templateVariable.reference",
|
|
1452
|
+
severity: "error",
|
|
1453
|
+
message: `qti-template-variable references missing template declaration ${variable.identifier}.`,
|
|
1454
|
+
path: variable.source?.path,
|
|
1455
|
+
source: variable.source,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
for (const variable of definition.contextVariables) {
|
|
1460
|
+
if (variable.identifier?.trim())
|
|
1461
|
+
continue;
|
|
1462
|
+
diagnostics.push({
|
|
1463
|
+
code: "interaction.portableCustom.contextVariable",
|
|
1464
|
+
severity: "error",
|
|
1465
|
+
message: "qti-context-variable requires identifier.",
|
|
1466
|
+
path: variable.source?.path,
|
|
1467
|
+
source: variable.source,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
for (const stylesheet of definition.stylesheets) {
|
|
1471
|
+
if (stylesheet.href.trim().length > 0)
|
|
1472
|
+
continue;
|
|
1473
|
+
diagnostics.push({
|
|
1474
|
+
code: "stylesheet.href.required",
|
|
1475
|
+
severity: "error",
|
|
1476
|
+
message: "qti-stylesheet requires a non-empty href attribute.",
|
|
1477
|
+
path: stylesheet.source?.path,
|
|
1478
|
+
source: stylesheet.source,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function warnExternalPortableCustomUrl(url, source, diagnostics) {
|
|
1483
|
+
if (!url || !/^https?:\/\//i.test(url))
|
|
1484
|
+
return;
|
|
1485
|
+
diagnostics.push({
|
|
1486
|
+
code: "interaction.portableCustom.externalModuleUrl",
|
|
1487
|
+
severity: "warning",
|
|
1488
|
+
message: `Portable custom interaction module URL ${url} requires host delivery policy approval.`,
|
|
1489
|
+
path: source?.path,
|
|
1490
|
+
source,
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1390
1493
|
function invalidNumber(interaction, attribute, value, diagnostics) {
|
|
1391
1494
|
diagnostics.push({
|
|
1392
1495
|
code: "interaction.numericAttribute",
|
|
@@ -1405,6 +1508,13 @@ function validateInteractionLimitAttributes(interaction, diagnostics) {
|
|
|
1405
1508
|
validateNonNegativeIntegerAttribute(interaction, "expected-lines", diagnostics);
|
|
1406
1509
|
validateMinMaxPair(interaction, "min-choices", "max-choices", diagnostics);
|
|
1407
1510
|
validateMinMaxPair(interaction, "min-associations", "max-associations", diagnostics);
|
|
1511
|
+
if (interaction.type === "media") {
|
|
1512
|
+
validateNonNegativeIntegerAttribute(interaction, "max-plays", diagnostics);
|
|
1513
|
+
validateNonNegativeIntegerAttribute(interaction, "min-plays", diagnostics);
|
|
1514
|
+
validateBooleanAttribute(interaction, "autostart", diagnostics);
|
|
1515
|
+
validateBooleanAttribute(interaction, "loop", diagnostics);
|
|
1516
|
+
validateMinMaxPair(interaction, "min-plays", "max-plays", diagnostics);
|
|
1517
|
+
}
|
|
1408
1518
|
}
|
|
1409
1519
|
function validateChoiceLimitAttributes(choice, diagnostics) {
|
|
1410
1520
|
if (requiresMatchMax(choice) && !choice.attributes["match-max"]) {
|
|
@@ -1482,6 +1592,89 @@ function validateHotspotGeometry(choice, diagnostics) {
|
|
|
1482
1592
|
});
|
|
1483
1593
|
}
|
|
1484
1594
|
}
|
|
1595
|
+
function validateGraphicHotspotObjectDimensions(interaction, diagnostics) {
|
|
1596
|
+
if (!usesGraphicHotspots(interaction))
|
|
1597
|
+
return;
|
|
1598
|
+
const hotspotChoices = interaction.choices.filter(isHotspotChoice);
|
|
1599
|
+
if (hotspotChoices.length === 0)
|
|
1600
|
+
return;
|
|
1601
|
+
const width = positiveDimension(interaction.object?.width);
|
|
1602
|
+
const height = positiveDimension(interaction.object?.height);
|
|
1603
|
+
if (width === undefined || height === undefined) {
|
|
1604
|
+
diagnostics.push({
|
|
1605
|
+
code: "interaction.graphicObjectDimensions",
|
|
1606
|
+
severity: "warning",
|
|
1607
|
+
message: `${interaction.qtiName} should declare object width and height so hotspot coords map to the rendered image.`,
|
|
1608
|
+
path: interaction.object?.source?.path ?? interaction.source?.path,
|
|
1609
|
+
source: interaction.object?.source ?? interaction.source,
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
for (const choice of hotspotChoices) {
|
|
1614
|
+
const bounds = hotspotBounds(choice);
|
|
1615
|
+
if (!bounds)
|
|
1616
|
+
continue;
|
|
1617
|
+
if (bounds.left >= 0 && bounds.top >= 0 && bounds.right <= width && bounds.bottom <= height) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
diagnostics.push({
|
|
1621
|
+
code: "choice.coords.bounds",
|
|
1622
|
+
severity: "warning",
|
|
1623
|
+
message: `${choice.qtiName} ${choice.identifier} coords extend outside the ${width} by ${height} object image.`,
|
|
1624
|
+
path: choice.source?.path,
|
|
1625
|
+
source: choice.source,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function usesGraphicHotspots(interaction) {
|
|
1630
|
+
return (interaction.type === "graphicOrder" ||
|
|
1631
|
+
interaction.type === "graphicAssociate" ||
|
|
1632
|
+
interaction.type === "graphicGapMatch" ||
|
|
1633
|
+
interaction.type === "hotspot");
|
|
1634
|
+
}
|
|
1635
|
+
function isHotspotChoice(choice) {
|
|
1636
|
+
return choice.qtiName === "qti-hotspot-choice" || choice.qtiName === "qti-associable-hotspot";
|
|
1637
|
+
}
|
|
1638
|
+
function positiveDimension(value) {
|
|
1639
|
+
if (!value || value.trim().endsWith("%"))
|
|
1640
|
+
return undefined;
|
|
1641
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?|\.\d+)(?:px)?$/i);
|
|
1642
|
+
if (!match?.[1])
|
|
1643
|
+
return undefined;
|
|
1644
|
+
const parsed = Number(match[1]);
|
|
1645
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
1646
|
+
}
|
|
1647
|
+
function hotspotBounds(choice) {
|
|
1648
|
+
const shape = choice.attributes.shape;
|
|
1649
|
+
const coords = choice.attributes.coords;
|
|
1650
|
+
if (!shape || !coords || !isHotspotShape(shape) || !isNumericCsv(coords))
|
|
1651
|
+
return undefined;
|
|
1652
|
+
const values = numericCsv(coords);
|
|
1653
|
+
if (!hasValidShapeCoordinateCount(shape, values))
|
|
1654
|
+
return undefined;
|
|
1655
|
+
if (shape === "default")
|
|
1656
|
+
return undefined;
|
|
1657
|
+
if (shape === "circle") {
|
|
1658
|
+
const [x, y, radius] = values;
|
|
1659
|
+
return { left: x - radius, top: y - radius, right: x + radius, bottom: y + radius };
|
|
1660
|
+
}
|
|
1661
|
+
if (shape === "ellipse") {
|
|
1662
|
+
const [x, y, radiusX, radiusY] = values;
|
|
1663
|
+
return { left: x - radiusX, top: y - radiusY, right: x + radiusX, bottom: y + radiusY };
|
|
1664
|
+
}
|
|
1665
|
+
if (shape === "rect") {
|
|
1666
|
+
const [left, top, right, bottom] = values;
|
|
1667
|
+
return { left, top, right, bottom };
|
|
1668
|
+
}
|
|
1669
|
+
const xs = values.filter((_, index) => index % 2 === 0);
|
|
1670
|
+
const ys = values.filter((_, index) => index % 2 === 1);
|
|
1671
|
+
return {
|
|
1672
|
+
left: Math.min(...xs),
|
|
1673
|
+
top: Math.min(...ys),
|
|
1674
|
+
right: Math.max(...xs),
|
|
1675
|
+
bottom: Math.max(...ys),
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1485
1678
|
function isHotspotShape(value) {
|
|
1486
1679
|
return (value === "circle" ||
|
|
1487
1680
|
value === "default" ||
|
|
@@ -1528,6 +1721,18 @@ function validateNonNegativeIntegerAttribute(interaction, attribute, diagnostics
|
|
|
1528
1721
|
source: interaction.source,
|
|
1529
1722
|
});
|
|
1530
1723
|
}
|
|
1724
|
+
function validateBooleanAttribute(interaction, attribute, diagnostics) {
|
|
1725
|
+
const value = interaction.attributes[attribute];
|
|
1726
|
+
if (value === undefined || isBooleanAttribute(value))
|
|
1727
|
+
return;
|
|
1728
|
+
diagnostics.push({
|
|
1729
|
+
code: "interaction.booleanAttribute",
|
|
1730
|
+
severity: "error",
|
|
1731
|
+
message: `${interaction.qtiName} requires boolean ${attribute}, got ${value}.`,
|
|
1732
|
+
path: interaction.source?.path,
|
|
1733
|
+
source: interaction.source,
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1531
1736
|
function validateChoiceNonNegativeIntegerAttribute(choice, attribute, diagnostics) {
|
|
1532
1737
|
const value = choice.attributes[attribute];
|
|
1533
1738
|
if (value === undefined || isNonNegativeInteger(value))
|
|
@@ -1615,13 +1820,20 @@ function allowedInteractionChildren(interaction) {
|
|
|
1615
1820
|
return setOf(common, ["object", "img", "qti-position-object-stage"]);
|
|
1616
1821
|
case "selectPoint":
|
|
1617
1822
|
case "media":
|
|
1618
|
-
return setOf(common, ["object", "img"]);
|
|
1823
|
+
return setOf(common, ["audio", "video", "object", "img"]);
|
|
1619
1824
|
case "drawing":
|
|
1620
|
-
return setOf(common, ["object", "img"]);
|
|
1825
|
+
return setOf(common, ["object", "img", "picture"]);
|
|
1621
1826
|
case "extendedText":
|
|
1622
1827
|
return new Set(common);
|
|
1623
1828
|
case "portableCustom":
|
|
1624
|
-
return setOf(common, [
|
|
1829
|
+
return setOf(common, [
|
|
1830
|
+
"qti-interaction-markup",
|
|
1831
|
+
"qti-interaction-modules",
|
|
1832
|
+
"qti-template-variable",
|
|
1833
|
+
"qti-context-variable",
|
|
1834
|
+
"qti-stylesheet",
|
|
1835
|
+
"qti-catalog-info",
|
|
1836
|
+
]);
|
|
1625
1837
|
case "slider":
|
|
1626
1838
|
case "textEntry":
|
|
1627
1839
|
case "upload":
|
|
@@ -1662,6 +1874,9 @@ function isInteger(value) {
|
|
|
1662
1874
|
function isNonNegativeInteger(value) {
|
|
1663
1875
|
return /^\d+$/.test(value);
|
|
1664
1876
|
}
|
|
1877
|
+
function isBooleanAttribute(value) {
|
|
1878
|
+
return value === "true" || value === "false" || value === "1" || value === "0";
|
|
1879
|
+
}
|
|
1665
1880
|
function isPoint(value) {
|
|
1666
1881
|
const parts = value.trim().split(/\s+/);
|
|
1667
1882
|
return parts.length === 2 && parts.every(isFiniteNumber);
|
|
@@ -1675,7 +1890,7 @@ function expectedResponseShape(interaction) {
|
|
|
1675
1890
|
return { cardinalities: ["single"], baseTypes: ["boolean"] };
|
|
1676
1891
|
}
|
|
1677
1892
|
if (interaction.type === "media")
|
|
1678
|
-
return
|
|
1893
|
+
return { cardinalities: ["single"], baseTypes: ["integer"] };
|
|
1679
1894
|
if (interaction.type === "custom")
|
|
1680
1895
|
return undefined;
|
|
1681
1896
|
if (interaction.type === "order" || interaction.type === "graphicOrder") {
|
|
@@ -1701,8 +1916,25 @@ function expectedResponseShape(interaction) {
|
|
|
1701
1916
|
if (interaction.type === "textEntry" || interaction.type === "extendedText") {
|
|
1702
1917
|
return { cardinalities: ["single"], baseTypes: ["string"] };
|
|
1703
1918
|
}
|
|
1704
|
-
if (interaction.type === "drawing"
|
|
1705
|
-
return { cardinalities: ["single"], baseTypes: ["
|
|
1919
|
+
if (interaction.type === "drawing")
|
|
1920
|
+
return { cardinalities: ["single"], baseTypes: ["file"] };
|
|
1921
|
+
if (interaction.type === "portableCustom") {
|
|
1922
|
+
return {
|
|
1923
|
+
cardinalities: ["single", "multiple", "ordered", "record"],
|
|
1924
|
+
baseTypes: [
|
|
1925
|
+
"identifier",
|
|
1926
|
+
"boolean",
|
|
1927
|
+
"integer",
|
|
1928
|
+
"float",
|
|
1929
|
+
"string",
|
|
1930
|
+
"point",
|
|
1931
|
+
"pair",
|
|
1932
|
+
"directedPair",
|
|
1933
|
+
"duration",
|
|
1934
|
+
"file",
|
|
1935
|
+
"uri",
|
|
1936
|
+
],
|
|
1937
|
+
};
|
|
1706
1938
|
}
|
|
1707
1939
|
return { cardinalities: ["single", "multiple"], baseTypes: ["identifier"] };
|
|
1708
1940
|
}
|