@longsightgroup/qti3-core 0.9.2 → 0.9.3

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 (219) hide show
  1. package/README.md +20 -3
  2. package/dist/assert-never.d.ts +3 -0
  3. package/dist/assert-never.d.ts.map +1 -0
  4. package/dist/assert-never.js +5 -0
  5. package/dist/assert-never.js.map +1 -0
  6. package/dist/attempt-state-constants.d.ts +6 -0
  7. package/dist/attempt-state-constants.d.ts.map +1 -0
  8. package/dist/attempt-state-constants.js +6 -0
  9. package/dist/attempt-state-constants.js.map +1 -0
  10. package/dist/attempt-state.d.ts +5 -0
  11. package/dist/attempt-state.d.ts.map +1 -0
  12. package/dist/attempt-state.js +190 -0
  13. package/dist/attempt-state.js.map +1 -0
  14. package/dist/catalog.js +1 -1
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/content-text.d.ts.map +1 -1
  17. package/dist/content-text.js +3 -0
  18. package/dist/content-text.js.map +1 -1
  19. package/dist/custom-operators.d.ts +13 -0
  20. package/dist/custom-operators.d.ts.map +1 -0
  21. package/dist/custom-operators.js +2 -0
  22. package/dist/custom-operators.js.map +1 -0
  23. package/dist/delivery-redaction.js +1 -1
  24. package/dist/delivery-redaction.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/parser-custom-interactions.js +1 -1
  30. package/dist/parser-custom-interactions.js.map +1 -1
  31. package/dist/parser-declarations.d.ts.map +1 -1
  32. package/dist/parser-declarations.js +4 -4
  33. package/dist/parser-declarations.js.map +1 -1
  34. package/dist/parser-item-metadata.d.ts.map +1 -1
  35. package/dist/parser-item-metadata.js +8 -7
  36. package/dist/parser-item-metadata.js.map +1 -1
  37. package/dist/parser-processing.js +2 -2
  38. package/dist/parser-processing.js.map +1 -1
  39. package/dist/parser-values.d.ts +5 -1
  40. package/dist/parser-values.d.ts.map +1 -1
  41. package/dist/parser-values.js +33 -0
  42. package/dist/parser-values.js.map +1 -1
  43. package/dist/processing-evaluator-boolean.d.ts +8 -0
  44. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  45. package/dist/processing-evaluator-boolean.js +47 -0
  46. package/dist/processing-evaluator-boolean.js.map +1 -0
  47. package/dist/processing-evaluator-collection.d.ts +12 -0
  48. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  49. package/dist/processing-evaluator-collection.js +35 -0
  50. package/dist/processing-evaluator-collection.js.map +1 -0
  51. package/dist/processing-evaluator-comparison.d.ts +8 -0
  52. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  53. package/dist/processing-evaluator-comparison.js +88 -0
  54. package/dist/processing-evaluator-comparison.js.map +1 -0
  55. package/dist/processing-evaluator-custom.d.ts +8 -0
  56. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  57. package/dist/processing-evaluator-custom.js +14 -0
  58. package/dist/processing-evaluator-custom.js.map +1 -0
  59. package/dist/processing-evaluator-geometry.d.ts +8 -0
  60. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  61. package/dist/processing-evaluator-geometry.js +21 -0
  62. package/dist/processing-evaluator-geometry.js.map +1 -0
  63. package/dist/processing-evaluator-numeric.d.ts +8 -0
  64. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  65. package/dist/processing-evaluator-numeric.js +112 -0
  66. package/dist/processing-evaluator-numeric.js.map +1 -0
  67. package/dist/processing-evaluator-string.d.ts +8 -0
  68. package/dist/processing-evaluator-string.d.ts.map +1 -0
  69. package/dist/processing-evaluator-string.js +33 -0
  70. package/dist/processing-evaluator-string.js.map +1 -0
  71. package/dist/processing-evaluator-variable.d.ts +8 -0
  72. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  73. package/dist/processing-evaluator-variable.js +39 -0
  74. package/dist/processing-evaluator-variable.js.map +1 -0
  75. package/dist/processing-evaluator.d.ts +22 -0
  76. package/dist/processing-evaluator.d.ts.map +1 -0
  77. package/dist/processing-evaluator.js +137 -0
  78. package/dist/processing-evaluator.js.map +1 -0
  79. package/dist/processing-expression-children.d.ts +3 -0
  80. package/dist/processing-expression-children.d.ts.map +1 -0
  81. package/dist/processing-expression-children.js +68 -0
  82. package/dist/processing-expression-children.js.map +1 -0
  83. package/dist/processing-mapping.d.ts +13 -0
  84. package/dist/processing-mapping.d.ts.map +1 -0
  85. package/dist/processing-mapping.js +120 -0
  86. package/dist/processing-mapping.js.map +1 -0
  87. package/dist/processing-operators.d.ts +11 -0
  88. package/dist/processing-operators.d.ts.map +1 -0
  89. package/dist/processing-operators.js +178 -0
  90. package/dist/processing-operators.js.map +1 -0
  91. package/dist/processing-random.d.ts +2 -0
  92. package/dist/processing-random.d.ts.map +1 -0
  93. package/dist/processing-random.js +16 -0
  94. package/dist/processing-random.js.map +1 -0
  95. package/dist/processing-state.d.ts +8 -0
  96. package/dist/processing-state.d.ts.map +1 -0
  97. package/dist/processing-state.js +44 -0
  98. package/dist/processing-state.js.map +1 -0
  99. package/dist/processing-templates.d.ts +3 -0
  100. package/dist/processing-templates.d.ts.map +1 -0
  101. package/dist/processing-templates.js +12 -0
  102. package/dist/processing-templates.js.map +1 -0
  103. package/dist/processing-values.d.ts +15 -0
  104. package/dist/processing-values.d.ts.map +1 -0
  105. package/dist/processing-values.js +150 -0
  106. package/dist/processing-values.js.map +1 -0
  107. package/dist/processing-variables.d.ts +7 -0
  108. package/dist/processing-variables.d.ts.map +1 -0
  109. package/dist/processing-variables.js +40 -0
  110. package/dist/processing-variables.js.map +1 -0
  111. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  112. package/dist/serializer-processing-expressions.js +2 -8
  113. package/dist/serializer-processing-expressions.js.map +1 -1
  114. package/dist/serializer-processing-xml.js +2 -2
  115. package/dist/serializer-processing-xml.js.map +1 -1
  116. package/dist/serializer-processing.d.ts.map +1 -1
  117. package/dist/serializer-processing.js +3 -0
  118. package/dist/serializer-processing.js.map +1 -1
  119. package/dist/session.d.ts +5 -14
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +76 -1158
  122. package/dist/session.js.map +1 -1
  123. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  124. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  125. package/dist/shared-vocabulary-generated-families.js +1 -1
  126. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  127. package/dist/shared-vocabulary-support.js +3 -3
  128. package/dist/shared-vocabulary-support.js.map +1 -1
  129. package/dist/shared-vocabulary.d.ts.map +1 -1
  130. package/dist/shared-vocabulary.js +4 -3
  131. package/dist/shared-vocabulary.js.map +1 -1
  132. package/dist/support-evidence.d.ts +22 -0
  133. package/dist/support-evidence.d.ts.map +1 -0
  134. package/dist/support-evidence.js +93 -0
  135. package/dist/support-evidence.js.map +1 -0
  136. package/dist/support.d.ts.map +1 -1
  137. package/dist/support.js +30 -43
  138. package/dist/support.js.map +1 -1
  139. package/dist/trusted-item-session.d.ts.map +1 -1
  140. package/dist/trusted-item-session.js +6 -5
  141. package/dist/trusted-item-session.js.map +1 -1
  142. package/dist/tts.d.ts.map +1 -1
  143. package/dist/tts.js +16 -2
  144. package/dist/tts.js.map +1 -1
  145. package/dist/validation-geometry.d.ts +8 -0
  146. package/dist/validation-geometry.d.ts.map +1 -0
  147. package/dist/validation-geometry.js +187 -0
  148. package/dist/validation-geometry.js.map +1 -0
  149. package/dist/validation-interactions.d.ts +3 -0
  150. package/dist/validation-interactions.d.ts.map +1 -0
  151. package/dist/validation-interactions.js +644 -0
  152. package/dist/validation-interactions.js.map +1 -0
  153. package/dist/validation-primitives.d.ts +11 -0
  154. package/dist/validation-primitives.d.ts.map +1 -0
  155. package/dist/validation-primitives.js +48 -0
  156. package/dist/validation-primitives.js.map +1 -0
  157. package/dist/validation-processing.d.ts +4 -0
  158. package/dist/validation-processing.d.ts.map +1 -0
  159. package/dist/validation-processing.js +569 -0
  160. package/dist/validation-processing.js.map +1 -0
  161. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  162. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  163. package/dist/validation-shared-vocabulary-layout.js +102 -0
  164. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  165. package/dist/validation.d.ts.map +1 -1
  166. package/dist/validation.js +27 -1641
  167. package/dist/validation.js.map +1 -1
  168. package/dist/xml.js +1 -1
  169. package/dist/xml.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/assert-never.ts +4 -0
  172. package/src/attempt-state-constants.ts +5 -0
  173. package/src/attempt-state.ts +245 -0
  174. package/src/catalog.ts +1 -1
  175. package/src/content-text.ts +3 -0
  176. package/src/custom-operators.ts +12 -0
  177. package/src/delivery-redaction.ts +1 -1
  178. package/src/index.ts +2 -0
  179. package/src/parser-custom-interactions.ts +1 -1
  180. package/src/parser-declarations.ts +4 -3
  181. package/src/parser-item-metadata.ts +8 -7
  182. package/src/parser-processing.ts +2 -2
  183. package/src/parser-values.ts +36 -0
  184. package/src/processing-evaluator-boolean.ts +48 -0
  185. package/src/processing-evaluator-collection.ts +50 -0
  186. package/src/processing-evaluator-comparison.ts +106 -0
  187. package/src/processing-evaluator-custom.ts +20 -0
  188. package/src/processing-evaluator-geometry.ts +26 -0
  189. package/src/processing-evaluator-numeric.ts +155 -0
  190. package/src/processing-evaluator-string.ts +60 -0
  191. package/src/processing-evaluator-variable.ts +81 -0
  192. package/src/processing-evaluator.ts +173 -0
  193. package/src/processing-expression-children.ts +69 -0
  194. package/src/processing-mapping.ts +153 -0
  195. package/src/processing-operators.ts +184 -0
  196. package/src/processing-random.ts +16 -0
  197. package/src/processing-state.ts +69 -0
  198. package/src/processing-templates.ts +12 -0
  199. package/src/processing-values.ts +162 -0
  200. package/src/processing-variables.ts +83 -0
  201. package/src/serializer-processing-expressions.ts +2 -12
  202. package/src/serializer-processing-xml.ts +2 -2
  203. package/src/serializer-processing.ts +3 -0
  204. package/src/session.ts +128 -2006
  205. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  206. package/src/shared-vocabulary-generated-families.ts +1 -1
  207. package/src/shared-vocabulary-support.ts +3 -3
  208. package/src/shared-vocabulary.ts +3 -2
  209. package/src/support-evidence.ts +107 -0
  210. package/src/support.ts +59 -50
  211. package/src/trusted-item-session.ts +6 -4
  212. package/src/tts.ts +29 -15
  213. package/src/validation-geometry.ts +204 -0
  214. package/src/validation-interactions.ts +831 -0
  215. package/src/validation-primitives.ts +63 -0
  216. package/src/validation-processing.ts +785 -0
  217. package/src/validation-shared-vocabulary-layout.ts +117 -0
  218. package/src/validation.ts +44 -2077
  219. package/src/xml.ts +1 -1
@@ -0,0 +1,204 @@
1
+ import type { QtiChoice, QtiDiagnostic, QtiInteraction } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import { numericTuple3, numericTuple4 } from "./parser-values.js";
4
+ import { isFiniteNumber } from "./validation-primitives.js";
5
+
6
+ export function validateHotspotGeometry(choice: QtiChoice, diagnostics: QtiDiagnostic[]): void {
7
+ if (choice.qtiName !== "qti-hotspot-choice" && choice.qtiName !== "qti-associable-hotspot") {
8
+ return;
9
+ }
10
+
11
+ const shape = choice.attributes.shape;
12
+ const coords = choice.attributes.coords;
13
+ if (!shape) {
14
+ diagnostics.push({
15
+ code: "choice.shape.required",
16
+ severity: "error",
17
+ message: `${choice.qtiName} ${choice.identifier} requires shape.`,
18
+ path: choice.source?.path,
19
+ source: choice.source,
20
+ });
21
+ } else if (!isHotspotShape(shape)) {
22
+ diagnostics.push({
23
+ code: "choice.shape",
24
+ severity: "error",
25
+ message: `${choice.qtiName} ${choice.identifier} has unsupported shape ${shape}.`,
26
+ path: choice.source?.path,
27
+ source: choice.source,
28
+ });
29
+ }
30
+
31
+ if (!coords) {
32
+ diagnostics.push({
33
+ code: "choice.coords.required",
34
+ severity: "error",
35
+ message: `${choice.qtiName} ${choice.identifier} requires coords.`,
36
+ path: choice.source?.path,
37
+ source: choice.source,
38
+ });
39
+ return;
40
+ }
41
+
42
+ const values = coords.split(",").map((value) => value.trim());
43
+ if (values.length === 0 || values.some((value) => value.length === 0 || !isFiniteNumber(value))) {
44
+ diagnostics.push({
45
+ code: "choice.coords",
46
+ severity: "error",
47
+ message: `${choice.qtiName} ${choice.identifier} requires comma-separated numeric coords.`,
48
+ path: choice.source?.path,
49
+ source: choice.source,
50
+ });
51
+ return;
52
+ }
53
+
54
+ if (shape && isHotspotShape(shape) && !hasValidShapeCoordinateCount(shape, values.map(Number))) {
55
+ diagnostics.push({
56
+ code: "choice.coords.shape",
57
+ severity: "error",
58
+ message: `${choice.qtiName} ${choice.identifier} shape ${shape} has invalid coords arity.`,
59
+ path: choice.source?.path,
60
+ source: choice.source,
61
+ });
62
+ }
63
+ }
64
+
65
+ export function validateGraphicHotspotObjectDimensions(
66
+ interaction: QtiInteraction,
67
+ diagnostics: QtiDiagnostic[],
68
+ ): void {
69
+ if (!usesGraphicHotspots(interaction)) return;
70
+ const hotspotChoices = interaction.choices.filter(isHotspotChoice);
71
+ if (hotspotChoices.length === 0) return;
72
+
73
+ const width = positiveDimension(interaction.object?.width);
74
+ const height = positiveDimension(interaction.object?.height);
75
+ if (width === undefined || height === undefined) {
76
+ diagnostics.push({
77
+ code: "interaction.graphicObjectDimensions",
78
+ severity: "warning",
79
+ message: `${interaction.qtiName} should declare object width and height so hotspot coords map to the rendered image.`,
80
+ path: interaction.object?.source?.path ?? interaction.source?.path,
81
+ source: interaction.object?.source ?? interaction.source,
82
+ });
83
+ return;
84
+ }
85
+
86
+ for (const choice of hotspotChoices) {
87
+ const bounds = hotspotBounds(choice);
88
+ if (!bounds) continue;
89
+ if (bounds.left >= 0 && bounds.top >= 0 && bounds.right <= width && bounds.bottom <= height) {
90
+ continue;
91
+ }
92
+ diagnostics.push({
93
+ code: "choice.coords.bounds",
94
+ severity: "warning",
95
+ message: `${choice.qtiName} ${choice.identifier} coords extend outside the ${width} by ${height} object image.`,
96
+ path: choice.source?.path,
97
+ source: choice.source,
98
+ });
99
+ }
100
+ }
101
+
102
+ function usesGraphicHotspots(interaction: QtiInteraction): boolean {
103
+ return (
104
+ interaction.type === "graphicOrder" ||
105
+ interaction.type === "graphicAssociate" ||
106
+ interaction.type === "graphicGapMatch" ||
107
+ interaction.type === "hotspot"
108
+ );
109
+ }
110
+
111
+ function isHotspotChoice(choice: QtiChoice): boolean {
112
+ return choice.qtiName === "qti-hotspot-choice" || choice.qtiName === "qti-associable-hotspot";
113
+ }
114
+
115
+ function positiveDimension(value: string | undefined): number | undefined {
116
+ if (!value || value.trim().endsWith("%")) return undefined;
117
+ const match = value.trim().match(/^(\d+(?:\.\d+)?|\.\d+)(?:px)?$/i);
118
+ if (!match?.[1]) return undefined;
119
+ const parsed = Number(match[1]);
120
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
121
+ }
122
+
123
+ function hotspotBounds(
124
+ choice: QtiChoice,
125
+ ): { left: number; top: number; right: number; bottom: number } | undefined {
126
+ const shape = choice.attributes.shape;
127
+ const coords = choice.attributes.coords;
128
+ if (!shape || !coords || !isHotspotShape(shape) || !isNumericCsv(coords)) return undefined;
129
+ const values = numericCsv(coords);
130
+ if (!hasValidShapeCoordinateCount(shape, values)) return undefined;
131
+
132
+ if (shape === "default") return undefined;
133
+ if (shape === "circle") {
134
+ const tuple = numericTuple3(values);
135
+ if (!tuple) return undefined;
136
+ const [x, y, radius] = tuple;
137
+ return { left: x - radius, top: y - radius, right: x + radius, bottom: y + radius };
138
+ }
139
+ if (shape === "ellipse") {
140
+ const tuple = numericTuple4(values);
141
+ if (!tuple) return undefined;
142
+ const [x, y, radiusX, radiusY] = tuple;
143
+ return { left: x - radiusX, top: y - radiusY, right: x + radiusX, bottom: y + radiusY };
144
+ }
145
+ if (shape === "rect") {
146
+ const tuple = numericTuple4(values);
147
+ if (!tuple) return undefined;
148
+ const [left, top, right, bottom] = tuple;
149
+ return { left, top, right, bottom };
150
+ }
151
+
152
+ const xs = values.filter((_, index) => index % 2 === 0);
153
+ const ys = values.filter((_, index) => index % 2 === 1);
154
+ return {
155
+ left: Math.min(...xs),
156
+ top: Math.min(...ys),
157
+ right: Math.max(...xs),
158
+ bottom: Math.max(...ys),
159
+ };
160
+ }
161
+
162
+ type QtiHotspotShape = "circle" | "default" | "ellipse" | "poly" | "rect";
163
+
164
+ function isHotspotShape(value: string): value is QtiHotspotShape {
165
+ return (
166
+ value === "circle" ||
167
+ value === "default" ||
168
+ value === "ellipse" ||
169
+ value === "poly" ||
170
+ value === "rect"
171
+ );
172
+ }
173
+
174
+ export function isAreaShape(value: string): boolean {
175
+ return value === "circle" || value === "default" || value === "poly" || value === "rect";
176
+ }
177
+
178
+ export function isNumericCsv(value: string): boolean {
179
+ return value
180
+ .split(",")
181
+ .map((part) => part.trim())
182
+ .every((part) => part.length > 0 && isFiniteNumber(part));
183
+ }
184
+
185
+ export function numericCsv(value: string): number[] {
186
+ return value.split(",").map((part) => Number(part.trim()));
187
+ }
188
+
189
+ export function hasValidShapeCoordinateCount(shape: string, coords: number[]): boolean {
190
+ if (!isHotspotShape(shape)) return false;
191
+ switch (shape) {
192
+ case "circle":
193
+ return coords.length === 3;
194
+ case "ellipse":
195
+ case "rect":
196
+ return coords.length === 4;
197
+ case "poly":
198
+ return coords.length >= 6 && coords.length % 2 === 0;
199
+ case "default":
200
+ return true;
201
+ default:
202
+ return assertNever(shape);
203
+ }
204
+ }