@longsightgroup/qti3-core 0.7.3 → 0.8.1

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.
Files changed (119) hide show
  1. package/README.md +41 -10
  2. package/dist/content-text.d.ts +7 -1
  3. package/dist/content-text.d.ts.map +1 -1
  4. package/dist/content-text.js +14 -0
  5. package/dist/content-text.js.map +1 -1
  6. package/dist/index.d.ts +7 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/package-xml.d.ts +12 -0
  11. package/dist/package-xml.d.ts.map +1 -0
  12. package/dist/package-xml.js +17 -0
  13. package/dist/package-xml.js.map +1 -0
  14. package/dist/parser-custom-interactions.d.ts +8 -0
  15. package/dist/parser-custom-interactions.d.ts.map +1 -0
  16. package/dist/parser-custom-interactions.js +178 -0
  17. package/dist/parser-custom-interactions.js.map +1 -0
  18. package/dist/parser-declarations.d.ts +6 -0
  19. package/dist/parser-declarations.d.ts.map +1 -0
  20. package/dist/parser-declarations.js +136 -0
  21. package/dist/parser-declarations.js.map +1 -0
  22. package/dist/parser-item-metadata.d.ts +9 -0
  23. package/dist/parser-item-metadata.d.ts.map +1 -0
  24. package/dist/parser-item-metadata.js +158 -0
  25. package/dist/parser-item-metadata.js.map +1 -0
  26. package/dist/parser-processing.d.ts +5 -0
  27. package/dist/parser-processing.d.ts.map +1 -0
  28. package/dist/parser-processing.js +642 -0
  29. package/dist/parser-processing.js.map +1 -0
  30. package/dist/parser-values.d.ts +8 -0
  31. package/dist/parser-values.d.ts.map +1 -0
  32. package/dist/parser-values.js +49 -0
  33. package/dist/parser-values.js.map +1 -0
  34. package/dist/parser.d.ts.map +1 -1
  35. package/dist/parser.js +15 -1083
  36. package/dist/parser.js.map +1 -1
  37. package/dist/processing-expression-tags.d.ts +56 -0
  38. package/dist/processing-expression-tags.d.ts.map +1 -0
  39. package/dist/processing-expression-tags.js +52 -0
  40. package/dist/processing-expression-tags.js.map +1 -0
  41. package/dist/processing-rules.d.ts +3 -0
  42. package/dist/processing-rules.d.ts.map +1 -0
  43. package/dist/processing-rules.js +10 -0
  44. package/dist/processing-rules.js.map +1 -0
  45. package/dist/serializer-processing-expressions.d.ts +8 -0
  46. package/dist/serializer-processing-expressions.d.ts.map +1 -0
  47. package/dist/serializer-processing-expressions.js +198 -0
  48. package/dist/serializer-processing-expressions.js.map +1 -0
  49. package/dist/serializer-processing-xml.d.ts +15 -0
  50. package/dist/serializer-processing-xml.d.ts.map +1 -0
  51. package/dist/serializer-processing-xml.js +46 -0
  52. package/dist/serializer-processing-xml.js.map +1 -0
  53. package/dist/serializer-processing.d.ts +8 -0
  54. package/dist/serializer-processing.d.ts.map +1 -0
  55. package/dist/serializer-processing.js +106 -0
  56. package/dist/serializer-processing.js.map +1 -0
  57. package/dist/shared-vocabulary-authoring.d.ts +52 -0
  58. package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
  59. package/dist/shared-vocabulary-authoring.js +327 -0
  60. package/dist/shared-vocabulary-authoring.js.map +1 -0
  61. package/dist/shared-vocabulary-interaction-sets.d.ts +6 -0
  62. package/dist/shared-vocabulary-interaction-sets.d.ts.map +1 -0
  63. package/dist/shared-vocabulary-interaction-sets.js +23 -0
  64. package/dist/shared-vocabulary-interaction-sets.js.map +1 -0
  65. package/dist/shared-vocabulary-interaction-validation.d.ts +3 -0
  66. package/dist/shared-vocabulary-interaction-validation.d.ts.map +1 -0
  67. package/dist/shared-vocabulary-interaction-validation.js +99 -0
  68. package/dist/shared-vocabulary-interaction-validation.js.map +1 -0
  69. package/dist/shared-vocabulary-registry-validation.d.ts +5 -0
  70. package/dist/shared-vocabulary-registry-validation.d.ts.map +1 -0
  71. package/dist/shared-vocabulary-registry-validation.js +147 -0
  72. package/dist/shared-vocabulary-registry-validation.js.map +1 -0
  73. package/dist/shared-vocabulary-support.d.ts.map +1 -1
  74. package/dist/shared-vocabulary-support.js +160 -67
  75. package/dist/shared-vocabulary-support.js.map +1 -1
  76. package/dist/shared-vocabulary.d.ts +6 -0
  77. package/dist/shared-vocabulary.d.ts.map +1 -1
  78. package/dist/shared-vocabulary.js +12 -0
  79. package/dist/shared-vocabulary.js.map +1 -1
  80. package/dist/support.d.ts +4 -1
  81. package/dist/support.d.ts.map +1 -1
  82. package/dist/support.js +130 -0
  83. package/dist/support.js.map +1 -1
  84. package/dist/types.d.ts +32 -7
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/validation.d.ts.map +1 -1
  87. package/dist/validation.js +29 -250
  88. package/dist/validation.js.map +1 -1
  89. package/dist/xml.d.ts +10 -0
  90. package/dist/xml.d.ts.map +1 -1
  91. package/dist/xml.js +321 -272
  92. package/dist/xml.js.map +1 -1
  93. package/package.json +1 -4
  94. package/src/content-text.ts +20 -1
  95. package/src/index.ts +68 -0
  96. package/src/interaction-test-fixtures.ts +44 -0
  97. package/src/package-xml.ts +30 -0
  98. package/src/parser-custom-interactions.ts +309 -0
  99. package/src/parser-declarations.ts +174 -0
  100. package/src/parser-item-metadata.ts +194 -0
  101. package/src/parser-processing.ts +701 -0
  102. package/src/parser-values.ts +61 -0
  103. package/src/parser.ts +36 -1227
  104. package/src/processing-expression-tags.ts +65 -0
  105. package/src/processing-rules.ts +11 -0
  106. package/src/serializer-processing-expressions.ts +456 -0
  107. package/src/serializer-processing-xml.ts +67 -0
  108. package/src/serializer-processing.fixtures.ts +469 -0
  109. package/src/serializer-processing.ts +196 -0
  110. package/src/shared-vocabulary-authoring.ts +438 -0
  111. package/src/shared-vocabulary-interaction-sets.ts +28 -0
  112. package/src/shared-vocabulary-interaction-validation.ts +143 -0
  113. package/src/shared-vocabulary-registry-validation.ts +210 -0
  114. package/src/shared-vocabulary-support.ts +200 -127
  115. package/src/shared-vocabulary.ts +18 -0
  116. package/src/support.ts +144 -0
  117. package/src/types.ts +42 -7
  118. package/src/validation.ts +33 -341
  119. package/src/xml.ts +376 -276
@@ -0,0 +1,174 @@
1
+ import {
2
+ coerceValue,
3
+ normalizeValueForCardinality,
4
+ parseCardinality,
5
+ parseCoords,
6
+ parseShape,
7
+ } from "./parser-values.js";
8
+ import type {
9
+ QtiBaseType,
10
+ QtiLookupTable,
11
+ QtiOutcomeDeclaration,
12
+ QtiResponseDeclaration,
13
+ QtiScalarValue,
14
+ QtiTemplateDeclaration,
15
+ QtiValue,
16
+ } from "./types.js";
17
+ import { childElements, textContent, type XmlNode } from "./xml.js";
18
+
19
+ export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
20
+ const cardinality = parseCardinality(node.attributes.cardinality);
21
+ const baseType = node.attributes["base-type"] as QtiResponseDeclaration["baseType"];
22
+ return {
23
+ kind: "response",
24
+ identifier: node.attributes.identifier ?? "",
25
+ cardinality,
26
+ baseType,
27
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
28
+ correctResponse: normalizeValueForCardinality(
29
+ parseVariableValue(childElements(node, "qti-correct-response")[0], baseType),
30
+ cardinality,
31
+ ),
32
+ mapping: parseMapping(childElements(node, "qti-mapping")[0]),
33
+ areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
34
+ attributes: node.attributes,
35
+ source: node.source,
36
+ };
37
+ }
38
+
39
+ export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
40
+ const baseType = node.attributes["base-type"] as QtiOutcomeDeclaration["baseType"];
41
+ return {
42
+ kind: "outcome",
43
+ identifier: node.attributes.identifier ?? "",
44
+ cardinality: parseCardinality(node.attributes.cardinality),
45
+ baseType,
46
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
47
+ lookupTable: parseLookupTable(node, baseType),
48
+ attributes: node.attributes,
49
+ source: node.source,
50
+ };
51
+ }
52
+
53
+ export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
54
+ const baseType = node.attributes["base-type"] as QtiTemplateDeclaration["baseType"];
55
+ return {
56
+ kind: "template",
57
+ identifier: node.attributes.identifier ?? "",
58
+ cardinality: parseCardinality(node.attributes.cardinality),
59
+ baseType,
60
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
61
+ attributes: node.attributes,
62
+ source: node.source,
63
+ };
64
+ }
65
+
66
+ function parseVariableValue(
67
+ node: XmlNode | undefined,
68
+ baseType: QtiBaseType | undefined,
69
+ ): QtiValue {
70
+ if (!node) return null;
71
+ const valueNodes = childElements(node, "qti-value");
72
+ const entries = valueNodes.map((valueNode) => ({
73
+ fieldIdentifier: valueNode.attributes["field-identifier"],
74
+ value: coerceValue(textContent(valueNode), valueNode.attributes["base-type"] ?? baseType),
75
+ }));
76
+ const recordEntries = entries.filter(
77
+ (entry): entry is { fieldIdentifier: string; value: QtiScalarValue } =>
78
+ Boolean(entry.fieldIdentifier),
79
+ );
80
+ if (recordEntries.length > 0) {
81
+ return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
82
+ }
83
+ if (entries.length === 0) {
84
+ const text = textContent(node);
85
+ return text.length > 0 ? coerceValue(text, baseType) : null;
86
+ }
87
+ if (entries.length === 1) return entries[0]?.value ?? null;
88
+ return entries.map((entry) => entry.value);
89
+ }
90
+
91
+ function parseMapping(node: XmlNode | undefined): QtiResponseDeclaration["mapping"] | undefined {
92
+ if (!node) return undefined;
93
+ return {
94
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
95
+ attributes: node.attributes,
96
+ source: node.source,
97
+ entries: childElements(node, "qti-map-entry").map((entry) => ({
98
+ mapKey: entry.attributes["map-key"],
99
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
100
+ attributes: entry.attributes,
101
+ source: entry.source,
102
+ })),
103
+ };
104
+ }
105
+
106
+ function parseAreaMapping(
107
+ node: XmlNode | undefined,
108
+ ): QtiResponseDeclaration["areaMapping"] | undefined {
109
+ if (!node) return undefined;
110
+ return {
111
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
112
+ attributes: node.attributes,
113
+ source: node.source,
114
+ entries: childElements(node, "qti-area-map-entry").map((entry) => ({
115
+ shape: parseShape(entry.attributes.shape),
116
+ coords: parseCoords(entry.attributes.coords),
117
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
118
+ attributes: entry.attributes,
119
+ source: entry.source,
120
+ })),
121
+ };
122
+ }
123
+
124
+ function parseLookupTable(
125
+ node: XmlNode,
126
+ baseType: QtiOutcomeDeclaration["baseType"],
127
+ ): QtiLookupTable | undefined {
128
+ const matchTable = childElements(node, "qti-match-table")[0];
129
+ if (matchTable) return parseMatchTable(matchTable, baseType);
130
+ const interpolationTable = childElements(node, "qti-interpolation-table")[0];
131
+ if (interpolationTable) return parseInterpolationTable(interpolationTable, baseType);
132
+ return undefined;
133
+ }
134
+
135
+ function parseMatchTable(
136
+ node: XmlNode,
137
+ baseType: QtiOutcomeDeclaration["baseType"],
138
+ ): QtiLookupTable {
139
+ return {
140
+ type: "match",
141
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
142
+ attributes: node.attributes,
143
+ source: node.source,
144
+ entries: childElements(node, "qti-match-table-entry").map((entry) => ({
145
+ sourceValue: Number(entry.attributes["source-value"]),
146
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
147
+ attributes: entry.attributes,
148
+ source: entry.source,
149
+ })),
150
+ };
151
+ }
152
+
153
+ function parseInterpolationTable(
154
+ node: XmlNode,
155
+ baseType: QtiOutcomeDeclaration["baseType"],
156
+ ): QtiLookupTable {
157
+ return {
158
+ type: "interpolation",
159
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
160
+ attributes: node.attributes,
161
+ source: node.source,
162
+ entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
163
+ sourceValue: Number(entry.attributes["source-value"]),
164
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
165
+ includeBoundary: entry.attributes["include-boundary"] !== "false",
166
+ attributes: entry.attributes,
167
+ source: entry.source,
168
+ })),
169
+ };
170
+ }
171
+
172
+ function parseLookupValue(value: string | undefined, baseType: string | undefined): QtiValue {
173
+ return value === undefined ? null : coerceValue(value, baseType);
174
+ }
@@ -0,0 +1,194 @@
1
+ import type {
2
+ QtiCatalogCard,
3
+ QtiCatalogCardEntry,
4
+ QtiCatalogFileHref,
5
+ QtiCatalogHtmlContent,
6
+ QtiCatalogInfo,
7
+ QtiCatalogReference,
8
+ QtiCompanionMaterialsInfo,
9
+ QtiCompanionMaterialsUnparsedChild,
10
+ QtiContentNode,
11
+ QtiDiagnostic,
12
+ QtiModalFeedback,
13
+ QtiPhysicalMaterial,
14
+ QtiStylesheet,
15
+ } from "./types.js";
16
+ import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
17
+
18
+ export function firstChildElement(
19
+ parent: XmlNode,
20
+ childName: string,
21
+ diagnostics: QtiDiagnostic[],
22
+ duplicateCode: string,
23
+ ): XmlNode | undefined {
24
+ const matches = childElements(parent, childName);
25
+ if (matches.length > 1) {
26
+ diagnostics.push({
27
+ code: duplicateCode,
28
+ severity: "error",
29
+ message: `${parent.localName} allows at most one ${childName} child.`,
30
+ path: matches[1]?.source?.path ?? parent.source?.path,
31
+ source: matches[1]?.source ?? parent.source,
32
+ });
33
+ }
34
+ return matches[0];
35
+ }
36
+
37
+ export function parseStylesheet(node: XmlNode): QtiStylesheet {
38
+ return {
39
+ href: node.attributes.href ?? "",
40
+ type: node.attributes.type,
41
+ media: node.attributes.media,
42
+ title: node.attributes.title,
43
+ attributes: node.attributes,
44
+ source: node.source,
45
+ };
46
+ }
47
+
48
+ export function parseCatalogReferences(node: XmlNode): QtiCatalogReference[] {
49
+ const references = [
50
+ ...(node.attributes["data-catalog-idref"] ? [node] : []),
51
+ ...descendants(node, (child) => Boolean(child.attributes["data-catalog-idref"])),
52
+ ];
53
+ return references.map((reference) => ({
54
+ idref: reference.attributes["data-catalog-idref"] ?? "",
55
+ source: reference.source,
56
+ }));
57
+ }
58
+
59
+ export function parseModalFeedback(node: XmlNode): QtiModalFeedback {
60
+ const showHide = node.attributes["show-hide"] === "hide" ? "hide" : "show";
61
+ return {
62
+ identifier: node.attributes.identifier ?? "",
63
+ outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
64
+ showHide,
65
+ text: textContent(node),
66
+ source: node.source,
67
+ };
68
+ }
69
+
70
+ export function parseCatalogInfo(node: XmlNode | undefined): QtiCatalogInfo | undefined {
71
+ if (!node) return undefined;
72
+ return {
73
+ catalogs: childElements(node, "qti-catalog").map((catalog) => ({
74
+ id: catalog.attributes.id ?? "",
75
+ attributes: catalog.attributes,
76
+ cards: childElements(catalog, "qti-card").map(parseCatalogCard),
77
+ source: catalog.source,
78
+ })),
79
+ source: node.source,
80
+ };
81
+ }
82
+
83
+ function parseCatalogCard(node: XmlNode): QtiCatalogCard {
84
+ return {
85
+ support: node.attributes.support ?? "",
86
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
87
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
88
+ entries: childElements(node, "qti-card-entry").map(parseCatalogCardEntry),
89
+ attributes: node.attributes,
90
+ source: node.source,
91
+ };
92
+ }
93
+
94
+ function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
95
+ return {
96
+ language: node.attributes["xml:lang"] ?? node.attributes.lang,
97
+ default: node.attributes.default === "true",
98
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
99
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
100
+ attributes: node.attributes,
101
+ source: node.source,
102
+ };
103
+ }
104
+
105
+ function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
106
+ if (!node) return undefined;
107
+ return {
108
+ text: textContent(node),
109
+ children: parseCatalogHtmlChildren(node),
110
+ attributes: node.attributes,
111
+ source: node.source,
112
+ };
113
+ }
114
+
115
+ function parseCatalogHtmlChildren(node: XmlNode): QtiContentNode[] {
116
+ const content: QtiContentNode[] = [];
117
+ for (const entry of node.content) {
118
+ if (typeof entry === "string") {
119
+ if (entry.length > 0) content.push({ kind: "text", text: entry, source: node.source });
120
+ continue;
121
+ }
122
+ content.push({
123
+ kind: "element",
124
+ qtiName: entry.localName,
125
+ attributes: entry.attributes,
126
+ children: parseCatalogHtmlChildren(entry),
127
+ source: entry.source,
128
+ });
129
+ }
130
+ return content;
131
+ }
132
+
133
+ function parseCatalogFileHref(node: XmlNode): QtiCatalogFileHref {
134
+ return {
135
+ href: textContent(node).trim(),
136
+ mimeType: node.attributes["mime-type"],
137
+ attributes: node.attributes,
138
+ source: node.source,
139
+ };
140
+ }
141
+
142
+ export function parseCompanionMaterialsInfo(
143
+ node: XmlNode | undefined,
144
+ diagnostics: QtiDiagnostic[],
145
+ ): QtiCompanionMaterialsInfo | undefined {
146
+ if (!node) return undefined;
147
+
148
+ const physicalMaterials: QtiPhysicalMaterial[] = [];
149
+ const unparsedChildren: QtiCompanionMaterialsUnparsedChild[] = [];
150
+
151
+ for (const child of childElements(node)) {
152
+ if (child.localName === "qti-physical-material") {
153
+ const material = parsePhysicalMaterial(child, diagnostics);
154
+ if (material) physicalMaterials.push(material);
155
+ continue;
156
+ }
157
+
158
+ unparsedChildren.push({ qtiName: child.localName, source: child.source });
159
+ diagnostics.push({
160
+ code: "companionMaterials.child.unsupported",
161
+ severity: "warning",
162
+ message: `${child.localName} is not currently modeled in companion materials parsing.`,
163
+ path: child.source?.path,
164
+ source: child.source,
165
+ });
166
+ }
167
+
168
+ return {
169
+ physicalMaterials,
170
+ unparsedChildren,
171
+ source: node.source,
172
+ };
173
+ }
174
+
175
+ function parsePhysicalMaterial(
176
+ node: XmlNode,
177
+ diagnostics: QtiDiagnostic[],
178
+ ): QtiPhysicalMaterial | undefined {
179
+ const text = textContent(node).trim();
180
+ if (text.length === 0) {
181
+ diagnostics.push({
182
+ code: "companionMaterials.physicalMaterial.empty",
183
+ severity: "warning",
184
+ message: "qti-physical-material requires non-empty text content.",
185
+ path: node.source?.path,
186
+ source: node.source,
187
+ });
188
+ return undefined;
189
+ }
190
+ return {
191
+ text,
192
+ source: node.source,
193
+ };
194
+ }