@longsightgroup/qti3-core 0.6.0 → 0.7.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.
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.js +13 -1
- package/dist/parser.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.d.ts +10 -0
- package/dist/shared-vocabulary-coverage-policy.d.ts.map +1 -0
- package/dist/shared-vocabulary-coverage-policy.js +61 -0
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -0
- package/dist/shared-vocabulary-generated-families.d.ts +21 -0
- package/dist/shared-vocabulary-generated-families.d.ts.map +1 -0
- package/dist/shared-vocabulary-generated-families.js +194 -0
- package/dist/shared-vocabulary-generated-families.js.map +1 -0
- package/dist/shared-vocabulary-levels.d.ts +4 -0
- package/dist/shared-vocabulary-levels.d.ts.map +1 -0
- package/dist/shared-vocabulary-levels.js +4 -0
- package/dist/shared-vocabulary-levels.js.map +1 -0
- package/dist/shared-vocabulary-support.d.ts +3 -0
- package/dist/shared-vocabulary-support.d.ts.map +1 -0
- package/dist/shared-vocabulary-support.js +208 -0
- package/dist/shared-vocabulary-support.js.map +1 -0
- package/dist/shared-vocabulary-validation.d.ts +40 -0
- package/dist/shared-vocabulary-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-validation.js +108 -0
- package/dist/shared-vocabulary-validation.js.map +1 -0
- package/dist/shared-vocabulary.d.ts +35 -0
- package/dist/shared-vocabulary.d.ts.map +1 -0
- package/dist/shared-vocabulary.js +128 -0
- package/dist/shared-vocabulary.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +364 -0
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +100 -0
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +56 -0
- package/src/parser.ts +17 -1
- package/src/shared-vocabulary-coverage-policy.ts +79 -0
- package/src/shared-vocabulary-generated-families.ts +252 -0
- package/src/shared-vocabulary-levels.ts +7 -0
- package/src/shared-vocabulary-support.ts +340 -0
- package/src/shared-vocabulary-validation.ts +182 -0
- package/src/shared-vocabulary.ts +177 -0
- package/src/types.ts +11 -0
- package/src/validation.ts +466 -0
- package/src/xml.ts +107 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SHARED_VOCABULARY_CHOICE_WRITING_ORIENTATIONS,
|
|
3
|
+
SHARED_VOCABULARY_CONTENT_ALIGNMENTS,
|
|
4
|
+
SHARED_VOCABULARY_CONTENT_FLOAT_SUFFIXES,
|
|
5
|
+
SHARED_VOCABULARY_CONTENT_LIST_STYLE_TYPES,
|
|
6
|
+
SHARED_VOCABULARY_CONTENT_TEXT_INDENT_SUFFIXES,
|
|
7
|
+
SHARED_VOCABULARY_CONTENT_VALIGNS,
|
|
8
|
+
SHARED_VOCABULARY_CONTENT_WRITING_MODES,
|
|
9
|
+
SHARED_VOCABULARY_LAYOUT_COLUMN_SPAN_COUNT,
|
|
10
|
+
SHARED_VOCABULARY_LAYOUT_OFFSET_COUNT,
|
|
11
|
+
} from "./shared-vocabulary-generated-families.js";
|
|
12
|
+
import {
|
|
13
|
+
SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES,
|
|
14
|
+
SHARED_VOCABULARY_EXTENDED_TEXT_HEIGHT_LINES,
|
|
15
|
+
SHARED_VOCABULARY_INPUT_WIDTHS,
|
|
16
|
+
} from "./shared-vocabulary.js";
|
|
17
|
+
import type { QtiInteractionType, SharedVocabularyClassSupport } from "./types.js";
|
|
18
|
+
|
|
19
|
+
const sharedVocabularyFixture = [
|
|
20
|
+
"packages/fixtures/packages/basic-item-player/valid-item-only/items/shared-vocabulary.xml",
|
|
21
|
+
];
|
|
22
|
+
const fixtureTests = ["packages/fixtures/src/fixtures.test.ts", "packages/cli/src/index.test.ts"];
|
|
23
|
+
const sharedVocabularyMatrixTests = ["tests/browser/player-shared-vocabulary.spec.ts"];
|
|
24
|
+
const contentTests = [
|
|
25
|
+
...fixtureTests,
|
|
26
|
+
"packages/core/src/core.test.ts",
|
|
27
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
28
|
+
...sharedVocabularyMatrixTests,
|
|
29
|
+
];
|
|
30
|
+
const sharedVocabularyUnitTests = [
|
|
31
|
+
"packages/core/src/shared-vocabulary.test.ts",
|
|
32
|
+
"packages/player/src/interactions/shared-vocabulary.test.ts",
|
|
33
|
+
];
|
|
34
|
+
const sharedVocabularyValidationTests = [
|
|
35
|
+
"packages/core/src/core.test.ts",
|
|
36
|
+
"packages/core/src/shared-vocabulary-validation.test.ts",
|
|
37
|
+
];
|
|
38
|
+
const browserBehaviorTests = [
|
|
39
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
40
|
+
...sharedVocabularyMatrixTests,
|
|
41
|
+
];
|
|
42
|
+
const graphicBrowserTests = ["tests/browser/player-graphic.spec.ts"];
|
|
43
|
+
const mediaBrowserTests = ["tests/browser/player.spec.ts", ...sharedVocabularyMatrixTests];
|
|
44
|
+
const mediaPlayerFixture =
|
|
45
|
+
"packages/fixtures/packages/sv-matrix/items/media-controls-and-pause.xml";
|
|
46
|
+
const interactionInputWidthFixtures = [
|
|
47
|
+
"packages/fixtures/packages/sv-matrix/items/interaction-input-width-standalone.xml",
|
|
48
|
+
"packages/fixtures/packages/sv-matrix/items/interaction-input-width-embedded.xml",
|
|
49
|
+
];
|
|
50
|
+
const orderMinMaxMessagesFixture =
|
|
51
|
+
"packages/fixtures/packages/sv-matrix/items/order-min-max-messages.xml";
|
|
52
|
+
|
|
53
|
+
const choiceAndOrder: QtiInteractionType[] = ["choice", "order"];
|
|
54
|
+
const choicesLayoutInteractions: QtiInteractionType[] = [
|
|
55
|
+
"match",
|
|
56
|
+
"gapMatch",
|
|
57
|
+
"graphicGapMatch",
|
|
58
|
+
"order",
|
|
59
|
+
];
|
|
60
|
+
const selectionPresentationInteractions: QtiInteractionType[] = [
|
|
61
|
+
"choice",
|
|
62
|
+
"hottext",
|
|
63
|
+
"hotspot",
|
|
64
|
+
"graphicGapMatch",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function svEntry(
|
|
68
|
+
className: string,
|
|
69
|
+
scope: SharedVocabularyClassSupport["scope"],
|
|
70
|
+
level: SharedVocabularyClassSupport["level"],
|
|
71
|
+
options: Omit<SharedVocabularyClassSupport, "className" | "scope" | "level"> = {},
|
|
72
|
+
): SharedVocabularyClassSupport {
|
|
73
|
+
return {
|
|
74
|
+
className,
|
|
75
|
+
scope,
|
|
76
|
+
level,
|
|
77
|
+
...options,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function contentStylesheetEntry(className: string, notes?: string): SharedVocabularyClassSupport {
|
|
82
|
+
return svEntry(className, "content", "stylesheet", {
|
|
83
|
+
fixtures: sharedVocabularyFixture,
|
|
84
|
+
tests: contentTests,
|
|
85
|
+
notes,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function interactionFullEntry(
|
|
90
|
+
className: string,
|
|
91
|
+
interactions: QtiInteractionType[],
|
|
92
|
+
tests: string[],
|
|
93
|
+
notes?: string,
|
|
94
|
+
): SharedVocabularyClassSupport {
|
|
95
|
+
return svEntry(className, "interaction", "full", {
|
|
96
|
+
interactions,
|
|
97
|
+
fixtures: sharedVocabularyFixture,
|
|
98
|
+
tests,
|
|
99
|
+
notes,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function interactionStylesheetEntry(
|
|
104
|
+
className: string,
|
|
105
|
+
interactions: QtiInteractionType[],
|
|
106
|
+
tests: string[],
|
|
107
|
+
notes?: string,
|
|
108
|
+
): SharedVocabularyClassSupport {
|
|
109
|
+
return svEntry(className, "interaction", "stylesheet", {
|
|
110
|
+
interactions,
|
|
111
|
+
fixtures: sharedVocabularyFixture,
|
|
112
|
+
tests,
|
|
113
|
+
notes,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mediaPlayerSvEntry(
|
|
118
|
+
className: string,
|
|
119
|
+
notes: string,
|
|
120
|
+
tests: string[] = mediaBrowserTests,
|
|
121
|
+
): SharedVocabularyClassSupport {
|
|
122
|
+
return svEntry(className, "interaction", "full", {
|
|
123
|
+
interactions: ["media"],
|
|
124
|
+
fixtures: [mediaPlayerFixture],
|
|
125
|
+
tests,
|
|
126
|
+
notes,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const sharedVocabularyClassSupport: SharedVocabularyClassSupport[] = [
|
|
131
|
+
contentStylesheetEntry(
|
|
132
|
+
"qti-layout-row",
|
|
133
|
+
"Twelve-column content layout row; validated for supported child column spans and offsets.",
|
|
134
|
+
),
|
|
135
|
+
...Array.from(
|
|
136
|
+
{ length: SHARED_VOCABULARY_LAYOUT_COLUMN_SPAN_COUNT },
|
|
137
|
+
(_, index) => index + 1,
|
|
138
|
+
).flatMap((span) => [
|
|
139
|
+
contentStylesheetEntry(`qti-layout-col${span}`),
|
|
140
|
+
contentStylesheetEntry(`qti-layout-col-${span}`),
|
|
141
|
+
]),
|
|
142
|
+
...Array.from({ length: SHARED_VOCABULARY_LAYOUT_OFFSET_COUNT }, (_, index) => index + 1).flatMap(
|
|
143
|
+
(offset) => [
|
|
144
|
+
contentStylesheetEntry(`qti-layout-offset${offset}`),
|
|
145
|
+
contentStylesheetEntry(`qti-layout-offset-${offset}`),
|
|
146
|
+
],
|
|
147
|
+
),
|
|
148
|
+
...SHARED_VOCABULARY_CONTENT_ALIGNMENTS.map((alignment) =>
|
|
149
|
+
contentStylesheetEntry(`qti-align-${alignment}`),
|
|
150
|
+
),
|
|
151
|
+
...SHARED_VOCABULARY_CONTENT_VALIGNS.map((alignment) =>
|
|
152
|
+
contentStylesheetEntry(`qti-valign-${alignment}`),
|
|
153
|
+
),
|
|
154
|
+
contentStylesheetEntry("qti-fullwidth"),
|
|
155
|
+
contentStylesheetEntry("qti-width-full"),
|
|
156
|
+
contentStylesheetEntry("qti-hidden", "Hides content from visual and assistive output."),
|
|
157
|
+
contentStylesheetEntry(
|
|
158
|
+
"qti-visually-hidden",
|
|
159
|
+
"Keeps content available to assistive technology while removing it from visual flow.",
|
|
160
|
+
),
|
|
161
|
+
svEntry("qti-keyword-emphasis", "content", "conditional", {
|
|
162
|
+
fixtures: ["packages/fixtures/packages/sv-matrix/items/content-keyword-emphasis.xml"],
|
|
163
|
+
tests: sharedVocabularyMatrixTests,
|
|
164
|
+
notes:
|
|
165
|
+
"Rendered with additional emphasis only when candidate PNP keyword-emphasis support is enabled by the host.",
|
|
166
|
+
}),
|
|
167
|
+
...SHARED_VOCABULARY_CONTENT_TEXT_INDENT_SUFFIXES.map((suffix) =>
|
|
168
|
+
contentStylesheetEntry(`qti-text-indent-${suffix}`),
|
|
169
|
+
),
|
|
170
|
+
...SHARED_VOCABULARY_CONTENT_WRITING_MODES.map((mode) =>
|
|
171
|
+
contentStylesheetEntry(`qti-writing-mode-${mode}`),
|
|
172
|
+
),
|
|
173
|
+
contentStylesheetEntry("qti-text-orientation-upright"),
|
|
174
|
+
contentStylesheetEntry("qti-text-combine-upright-all"),
|
|
175
|
+
...SHARED_VOCABULARY_CONTENT_FLOAT_SUFFIXES.map((floatClass) =>
|
|
176
|
+
contentStylesheetEntry(`qti-float-${floatClass}`),
|
|
177
|
+
),
|
|
178
|
+
contentStylesheetEntry("qti-bordered"),
|
|
179
|
+
contentStylesheetEntry("qti-well"),
|
|
180
|
+
...SHARED_VOCABULARY_CONTENT_LIST_STYLE_TYPES.map((styleType) =>
|
|
181
|
+
contentStylesheetEntry(`qti-list-style-type-${styleType}`),
|
|
182
|
+
),
|
|
183
|
+
contentStylesheetEntry("qti-underline"),
|
|
184
|
+
contentStylesheetEntry("qti-italic"),
|
|
185
|
+
contentStylesheetEntry("qti-display-inline-block"),
|
|
186
|
+
|
|
187
|
+
...["none", "decimal", "cjk-ideographic", "lower-alpha", "upper-alpha"].map((labelStyle) =>
|
|
188
|
+
interactionFullEntry(`qti-labels-${labelStyle}`, choiceAndOrder, [
|
|
189
|
+
...sharedVocabularyUnitTests,
|
|
190
|
+
...browserBehaviorTests,
|
|
191
|
+
]),
|
|
192
|
+
),
|
|
193
|
+
...["none", "period", "parenthesis"].map((suffix) =>
|
|
194
|
+
interactionFullEntry(`qti-labels-suffix-${suffix}`, choiceAndOrder, [
|
|
195
|
+
...sharedVocabularyUnitTests,
|
|
196
|
+
...browserBehaviorTests,
|
|
197
|
+
]),
|
|
198
|
+
),
|
|
199
|
+
...["horizontal", "vertical"].map((orientation) =>
|
|
200
|
+
interactionFullEntry(
|
|
201
|
+
`qti-orientation-${orientation}`,
|
|
202
|
+
["choice", "order"],
|
|
203
|
+
[
|
|
204
|
+
"packages/player/src/interactions/choice-layout.test.ts",
|
|
205
|
+
...sharedVocabularyUnitTests,
|
|
206
|
+
...browserBehaviorTests,
|
|
207
|
+
],
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
...Array.from({ length: 5 }, (_, index) => index + 1).map((stacking) =>
|
|
211
|
+
interactionFullEntry(
|
|
212
|
+
`qti-choices-stacking-${stacking}`,
|
|
213
|
+
["choice"],
|
|
214
|
+
[
|
|
215
|
+
"packages/player/src/interactions/choice-layout.test.ts",
|
|
216
|
+
"packages/core/src/core.test.ts",
|
|
217
|
+
...browserBehaviorTests,
|
|
218
|
+
],
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
interactionStylesheetEntry(
|
|
222
|
+
"qti-input-control-hidden",
|
|
223
|
+
["choice", "hottext"],
|
|
224
|
+
[...browserBehaviorTests, "tests/browser/player-keyboard-a11y.spec.ts"],
|
|
225
|
+
),
|
|
226
|
+
...SHARED_VOCABULARY_CHOICE_WRITING_ORIENTATIONS.map((orientation) =>
|
|
227
|
+
interactionStylesheetEntry(
|
|
228
|
+
`qti-writing-orientation-${orientation}`,
|
|
229
|
+
["choice"],
|
|
230
|
+
[...browserBehaviorTests],
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
...["light", "dark"].map((tone) =>
|
|
234
|
+
interactionStylesheetEntry(`qti-selections-${tone}`, selectionPresentationInteractions, [
|
|
235
|
+
...browserBehaviorTests,
|
|
236
|
+
...graphicBrowserTests,
|
|
237
|
+
]),
|
|
238
|
+
),
|
|
239
|
+
interactionStylesheetEntry("qti-unselected-hidden", selectionPresentationInteractions, [
|
|
240
|
+
...browserBehaviorTests,
|
|
241
|
+
...graphicBrowserTests,
|
|
242
|
+
]),
|
|
243
|
+
...["top", "bottom", "left", "right"].map((position) =>
|
|
244
|
+
interactionFullEntry(`qti-choices-${position}`, choicesLayoutInteractions, [
|
|
245
|
+
...sharedVocabularyUnitTests,
|
|
246
|
+
...browserBehaviorTests,
|
|
247
|
+
...graphicBrowserTests,
|
|
248
|
+
]),
|
|
249
|
+
),
|
|
250
|
+
svEntry("data-min-selections-message", "interaction", "full", {
|
|
251
|
+
interactions: ["order"],
|
|
252
|
+
fixtures: [orderMinMaxMessagesFixture],
|
|
253
|
+
tests: sharedVocabularyMatrixTests,
|
|
254
|
+
notes: "Overrides minimum response validation text for order interactions.",
|
|
255
|
+
}),
|
|
256
|
+
svEntry("data-max-selections-message", "interaction", "full", {
|
|
257
|
+
interactions: ["choice", "order"],
|
|
258
|
+
fixtures: [orderMinMaxMessagesFixture],
|
|
259
|
+
tests: ["tests/browser/player-validation.spec.ts", ...sharedVocabularyMatrixTests],
|
|
260
|
+
notes: "Overrides maximum response validation text for choice and order interactions.",
|
|
261
|
+
}),
|
|
262
|
+
interactionFullEntry(
|
|
263
|
+
"qti-match-tabular",
|
|
264
|
+
["match"],
|
|
265
|
+
[
|
|
266
|
+
"packages/core/src/core.test.ts",
|
|
267
|
+
...browserBehaviorTests,
|
|
268
|
+
"tests/browser/player-keyboard-a11y.spec.ts",
|
|
269
|
+
],
|
|
270
|
+
),
|
|
271
|
+
svEntry("qti-header-hidden", "interaction", "conditional", {
|
|
272
|
+
interactions: ["match"],
|
|
273
|
+
fixtures: sharedVocabularyFixture,
|
|
274
|
+
tests: ["packages/core/src/core.test.ts", ...browserBehaviorTests],
|
|
275
|
+
notes: "Applied when qti-match-tabular selects the tabular match renderer.",
|
|
276
|
+
}),
|
|
277
|
+
interactionFullEntry(
|
|
278
|
+
"qti-gap-placement",
|
|
279
|
+
["gapMatch"],
|
|
280
|
+
[...sharedVocabularyUnitTests, "packages/core/src/core.test.ts", ...browserBehaviorTests],
|
|
281
|
+
),
|
|
282
|
+
...SHARED_VOCABULARY_INPUT_WIDTHS.map((width) =>
|
|
283
|
+
svEntry(`qti-input-width-${width}`, "interaction", "full", {
|
|
284
|
+
interactions: ["textEntry", "inlineChoice"],
|
|
285
|
+
fixtures: interactionInputWidthFixtures,
|
|
286
|
+
tests: [
|
|
287
|
+
"packages/core/src/shared-vocabulary.test.ts",
|
|
288
|
+
"packages/core/src/core.test.ts",
|
|
289
|
+
...sharedVocabularyMatrixTests,
|
|
290
|
+
],
|
|
291
|
+
}),
|
|
292
|
+
),
|
|
293
|
+
...SHARED_VOCABULARY_INPUT_WIDTHS.map((width) =>
|
|
294
|
+
svEntry(`qti-input-width-${width}`, "gap", "full", {
|
|
295
|
+
interactions: ["gapMatch"],
|
|
296
|
+
fixtures: sharedVocabularyFixture,
|
|
297
|
+
tests: [
|
|
298
|
+
"packages/core/src/shared-vocabulary.test.ts",
|
|
299
|
+
"packages/core/src/core.test.ts",
|
|
300
|
+
...browserBehaviorTests,
|
|
301
|
+
],
|
|
302
|
+
notes: "Supported on qti-gap targets when gap placement is authored.",
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
305
|
+
...SHARED_VOCABULARY_EXTENDED_TEXT_HEIGHT_LINES.map((lines) =>
|
|
306
|
+
interactionFullEntry(
|
|
307
|
+
`qti-height-lines-${lines}`,
|
|
308
|
+
["extendedText"],
|
|
309
|
+
[
|
|
310
|
+
...sharedVocabularyValidationTests,
|
|
311
|
+
"packages/core/src/shared-vocabulary.test.ts",
|
|
312
|
+
...browserBehaviorTests,
|
|
313
|
+
],
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
...SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES.map((className) =>
|
|
317
|
+
interactionFullEntry(
|
|
318
|
+
className,
|
|
319
|
+
["extendedText"],
|
|
320
|
+
[
|
|
321
|
+
...sharedVocabularyValidationTests,
|
|
322
|
+
"packages/core/src/shared-vocabulary.test.ts",
|
|
323
|
+
...browserBehaviorTests,
|
|
324
|
+
],
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
mediaPlayerSvEntry(
|
|
328
|
+
"data-qti-media-player-controls",
|
|
329
|
+
"Supports tokens none, default, play, rewind, captions, and audioDescription on media interactions and rendered media assets.",
|
|
330
|
+
[...mediaBrowserTests, "packages/core/src/shared-vocabulary-validation.test.ts"],
|
|
331
|
+
),
|
|
332
|
+
mediaPlayerSvEntry(
|
|
333
|
+
"data-qti-media-player-pause-delay",
|
|
334
|
+
"Reflects authored pause-delay values on rendered media assets. Pause timer behavior is covered in tests/browser/player.spec.ts.",
|
|
335
|
+
),
|
|
336
|
+
mediaPlayerSvEntry(
|
|
337
|
+
"data-qti-media-player-pause-duration",
|
|
338
|
+
"Reflects authored pause-duration values on rendered media assets. Pause timer behavior is covered in tests/browser/player.spec.ts.",
|
|
339
|
+
),
|
|
340
|
+
];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSupportedExtendedTextCounterClasses,
|
|
3
|
+
formatSupportedExtendedTextHeightLinesClasses,
|
|
4
|
+
formatSupportedInputWidthClasses,
|
|
5
|
+
formatSupportedMediaPlayerControlsTokens,
|
|
6
|
+
isSupportedExtendedTextCounterClassName,
|
|
7
|
+
isSupportedExtendedTextHeightLinesClassName,
|
|
8
|
+
isSupportedInputWidthClassName,
|
|
9
|
+
supportedExtendedTextCounterClassNames,
|
|
10
|
+
supportedExtendedTextHeightLinesClassNames,
|
|
11
|
+
supportedInputWidthClassNames,
|
|
12
|
+
unsupportedMediaPlayerControlsTokens,
|
|
13
|
+
} from "./shared-vocabulary.js";
|
|
14
|
+
import type { QtiDiagnostic, QtiSourceLocation } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export interface SharedVocabularyNumericPrefixValidationOptions {
|
|
17
|
+
classNames: string[];
|
|
18
|
+
subjectQtiName: string;
|
|
19
|
+
path?: string | undefined;
|
|
20
|
+
source?: QtiSourceLocation | undefined;
|
|
21
|
+
classPrefix: string;
|
|
22
|
+
supportedClassNames: (classNames: string[]) => string[];
|
|
23
|
+
isSupportedClassName: (className: string) => boolean;
|
|
24
|
+
formatSupportedClasses: () => string;
|
|
25
|
+
conflictCode: string;
|
|
26
|
+
invalidCode: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateSharedVocabularyNumericPrefix(
|
|
30
|
+
options: SharedVocabularyNumericPrefixValidationOptions,
|
|
31
|
+
): QtiDiagnostic[] {
|
|
32
|
+
const {
|
|
33
|
+
classNames,
|
|
34
|
+
subjectQtiName,
|
|
35
|
+
path,
|
|
36
|
+
source,
|
|
37
|
+
classPrefix,
|
|
38
|
+
supportedClassNames,
|
|
39
|
+
isSupportedClassName,
|
|
40
|
+
formatSupportedClasses,
|
|
41
|
+
conflictCode,
|
|
42
|
+
invalidCode,
|
|
43
|
+
} = options;
|
|
44
|
+
const diagnostics: QtiDiagnostic[] = [];
|
|
45
|
+
const supported = supportedClassNames(classNames);
|
|
46
|
+
if (new Set(supported).size > 1) {
|
|
47
|
+
diagnostics.push({
|
|
48
|
+
code: conflictCode,
|
|
49
|
+
severity: "warning",
|
|
50
|
+
message: `${subjectQtiName} should not include multiple supported ${classPrefix}* classes: ${[...new Set(supported)].join(", ")}. The first class in class attribute order takes precedence at runtime.`,
|
|
51
|
+
path,
|
|
52
|
+
source,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const className of classNames) {
|
|
57
|
+
if (!className.startsWith(classPrefix)) continue;
|
|
58
|
+
if (isSupportedClassName(className)) continue;
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
code: invalidCode,
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: `${subjectQtiName} shared vocabulary class ${className} is not supported; expected ${formatSupportedClasses()}.`,
|
|
63
|
+
path,
|
|
64
|
+
source,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return diagnostics;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SharedVocabularyInputWidthValidationOptions {
|
|
71
|
+
classNames: string[];
|
|
72
|
+
subjectQtiName: string;
|
|
73
|
+
path?: string | undefined;
|
|
74
|
+
source?: QtiSourceLocation | undefined;
|
|
75
|
+
conflictCode: string;
|
|
76
|
+
invalidCode: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateSharedVocabularyInputWidth(
|
|
80
|
+
options: SharedVocabularyInputWidthValidationOptions,
|
|
81
|
+
): QtiDiagnostic[] {
|
|
82
|
+
const { classNames, subjectQtiName, path, source, conflictCode, invalidCode } = options;
|
|
83
|
+
return validateSharedVocabularyNumericPrefix({
|
|
84
|
+
classNames,
|
|
85
|
+
subjectQtiName,
|
|
86
|
+
path,
|
|
87
|
+
source,
|
|
88
|
+
classPrefix: "qti-input-width-",
|
|
89
|
+
supportedClassNames: supportedInputWidthClassNames,
|
|
90
|
+
isSupportedClassName: isSupportedInputWidthClassName,
|
|
91
|
+
formatSupportedClasses: formatSupportedInputWidthClasses,
|
|
92
|
+
conflictCode,
|
|
93
|
+
invalidCode,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SharedVocabularyExtendedTextValidationOptions {
|
|
98
|
+
classNames: string[];
|
|
99
|
+
subjectQtiName: string;
|
|
100
|
+
path?: string | undefined;
|
|
101
|
+
source?: QtiSourceLocation | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function validateSharedVocabularyExtendedTextHeightLines(
|
|
105
|
+
options: SharedVocabularyExtendedTextValidationOptions,
|
|
106
|
+
): QtiDiagnostic[] {
|
|
107
|
+
const { classNames, subjectQtiName, path, source } = options;
|
|
108
|
+
return validateSharedVocabularyNumericPrefix({
|
|
109
|
+
classNames,
|
|
110
|
+
subjectQtiName,
|
|
111
|
+
path,
|
|
112
|
+
source,
|
|
113
|
+
classPrefix: "qti-height-lines-",
|
|
114
|
+
supportedClassNames: supportedExtendedTextHeightLinesClassNames,
|
|
115
|
+
isSupportedClassName: isSupportedExtendedTextHeightLinesClassName,
|
|
116
|
+
formatSupportedClasses: formatSupportedExtendedTextHeightLinesClasses,
|
|
117
|
+
conflictCode: "interaction.sharedVocabulary.extendedTextHeightLinesConflict",
|
|
118
|
+
invalidCode: "interaction.sharedVocabulary.extendedTextHeightLinesInvalid",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateSharedVocabularyExtendedTextCounter(
|
|
123
|
+
options: SharedVocabularyExtendedTextValidationOptions,
|
|
124
|
+
): QtiDiagnostic[] {
|
|
125
|
+
const { classNames, subjectQtiName, path, source } = options;
|
|
126
|
+
const diagnostics: QtiDiagnostic[] = [];
|
|
127
|
+
const counterClasses = supportedExtendedTextCounterClassNames(classNames);
|
|
128
|
+
if (new Set(counterClasses).size > 1) {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
code: "interaction.sharedVocabulary.extendedTextCounterConflict",
|
|
131
|
+
severity: "warning",
|
|
132
|
+
message: `${subjectQtiName} should not include multiple qti-counter-* classes: ${[...new Set(counterClasses)].join(", ")}. The first class in class attribute order takes precedence at runtime.`,
|
|
133
|
+
path,
|
|
134
|
+
source,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const className of classNames) {
|
|
139
|
+
if (!className.startsWith("qti-counter-")) continue;
|
|
140
|
+
if (isSupportedExtendedTextCounterClassName(className)) continue;
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
code: "interaction.sharedVocabulary.extendedTextCounterInvalid",
|
|
143
|
+
severity: "warning",
|
|
144
|
+
message: `${subjectQtiName} shared vocabulary class ${className} is not supported; expected ${formatSupportedExtendedTextCounterClasses()}.`,
|
|
145
|
+
path,
|
|
146
|
+
source,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return diagnostics;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateSharedVocabularyExtendedText(
|
|
153
|
+
options: SharedVocabularyExtendedTextValidationOptions,
|
|
154
|
+
): QtiDiagnostic[] {
|
|
155
|
+
return [
|
|
156
|
+
...validateSharedVocabularyExtendedTextHeightLines(options),
|
|
157
|
+
...validateSharedVocabularyExtendedTextCounter(options),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface SharedVocabularyMediaPlayerControlsValidationOptions {
|
|
162
|
+
value: string | undefined;
|
|
163
|
+
subjectQtiName: string;
|
|
164
|
+
path?: string | undefined;
|
|
165
|
+
source?: QtiSourceLocation | undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function validateSharedVocabularyMediaPlayerControls(
|
|
169
|
+
options: SharedVocabularyMediaPlayerControlsValidationOptions,
|
|
170
|
+
): QtiDiagnostic[] {
|
|
171
|
+
const unsupported = unsupportedMediaPlayerControlsTokens(options.value);
|
|
172
|
+
if (unsupported.length === 0) return [];
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
code: "interaction.sharedVocabulary.mediaPlayerControlsInvalid",
|
|
176
|
+
severity: "warning",
|
|
177
|
+
message: `${options.subjectQtiName} data-qti-media-player-controls token ${[...new Set(unsupported)].join(", ")} is not supported; expected ${formatSupportedMediaPlayerControlsTokens()}.`,
|
|
178
|
+
path: options.path,
|
|
179
|
+
source: options.source,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { QtiInteraction } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Normative QTI 3 shared vocabulary input-width token values (1EdTech vocab). */
|
|
4
|
+
export const SHARED_VOCABULARY_INPUT_WIDTHS = [
|
|
5
|
+
1, 2, 3, 4, 6, 10, 15, 20, 25, 30, 35, 40, 45, 50, 72,
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export const SHARED_VOCABULARY_GAP_INPUT_WIDTHS = SHARED_VOCABULARY_INPUT_WIDTHS;
|
|
9
|
+
|
|
10
|
+
/** Normative QTI 3 shared vocabulary extended text height token values (1EdTech vocab). */
|
|
11
|
+
export const SHARED_VOCABULARY_EXTENDED_TEXT_HEIGHT_LINES = [3, 6, 15] as const;
|
|
12
|
+
|
|
13
|
+
export const SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES = [
|
|
14
|
+
"qti-counter-up",
|
|
15
|
+
"qti-counter-down",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type SharedVocabularyExtendedTextCounterPosition = "up" | "down";
|
|
19
|
+
|
|
20
|
+
export const SHARED_VOCABULARY_MEDIA_PLAYER_CONTROLS = [
|
|
21
|
+
"none",
|
|
22
|
+
"default",
|
|
23
|
+
"play",
|
|
24
|
+
"rewind",
|
|
25
|
+
"captions",
|
|
26
|
+
"audioDescription",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export type SharedVocabularyMediaPlayerControls =
|
|
30
|
+
(typeof SHARED_VOCABULARY_MEDIA_PLAYER_CONTROLS)[number];
|
|
31
|
+
|
|
32
|
+
const inputWidthSet = new Set<number>(SHARED_VOCABULARY_INPUT_WIDTHS);
|
|
33
|
+
const extendedTextHeightLinesSet = new Set<number>(SHARED_VOCABULARY_EXTENDED_TEXT_HEIGHT_LINES);
|
|
34
|
+
const extendedTextCounterClassSet = new Set<string>(
|
|
35
|
+
SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES,
|
|
36
|
+
);
|
|
37
|
+
const mediaPlayerControlsSet = new Set<string>(SHARED_VOCABULARY_MEDIA_PLAYER_CONTROLS);
|
|
38
|
+
|
|
39
|
+
const inputWidthClassPattern = /^qti-input-width-(\d+)$/;
|
|
40
|
+
const extendedTextHeightLinesClassPattern = /^qti-height-lines-(\d+)$/;
|
|
41
|
+
|
|
42
|
+
const extendedTextCounterPositionByClass: Record<
|
|
43
|
+
(typeof SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES)[number],
|
|
44
|
+
SharedVocabularyExtendedTextCounterPosition
|
|
45
|
+
> = {
|
|
46
|
+
"qti-counter-up": "up",
|
|
47
|
+
"qti-counter-down": "down",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function sharedVocabularyClassNames(attributes: Record<string, string>): string[] {
|
|
51
|
+
return (attributes.class ?? "").split(/\s+/).filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function firstMatchingSharedVocabularyClass<T extends string>(
|
|
55
|
+
classNames: string[],
|
|
56
|
+
tokens: readonly T[],
|
|
57
|
+
): T | undefined {
|
|
58
|
+
const tokenSet = new Set<string>(tokens);
|
|
59
|
+
for (const className of classNames) {
|
|
60
|
+
if (tokenSet.has(className)) return className as T;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function inputWidthFromAttributes(attributes: Record<string, string>): number | undefined {
|
|
66
|
+
for (const className of sharedVocabularyClassNames(attributes)) {
|
|
67
|
+
const value = inputWidthClassPattern.exec(className)?.[1];
|
|
68
|
+
if (value === undefined) continue;
|
|
69
|
+
const width = Number(value);
|
|
70
|
+
if (inputWidthSet.has(width)) return width;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const gapInputWidthFromAttributes = inputWidthFromAttributes;
|
|
76
|
+
|
|
77
|
+
export function extendedTextHeightLinesFromAttributes(
|
|
78
|
+
attributes: Record<string, string>,
|
|
79
|
+
): number | undefined {
|
|
80
|
+
for (const className of sharedVocabularyClassNames(attributes)) {
|
|
81
|
+
const value = extendedTextHeightLinesClassPattern.exec(className)?.[1];
|
|
82
|
+
if (value === undefined) continue;
|
|
83
|
+
const lines = Number(value);
|
|
84
|
+
if (extendedTextHeightLinesSet.has(lines)) return lines;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function extendedTextHeightLines(interaction: QtiInteraction): number | undefined {
|
|
90
|
+
return extendedTextHeightLinesFromAttributes(interaction.attributes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function extendedTextCounterPositionFromAttributes(
|
|
94
|
+
attributes: Record<string, string>,
|
|
95
|
+
): SharedVocabularyExtendedTextCounterPosition | undefined {
|
|
96
|
+
const matched = firstMatchingSharedVocabularyClass(
|
|
97
|
+
sharedVocabularyClassNames(attributes),
|
|
98
|
+
SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES,
|
|
99
|
+
);
|
|
100
|
+
return matched === undefined ? undefined : extendedTextCounterPositionByClass[matched];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extendedTextCounterPosition(
|
|
104
|
+
interaction: QtiInteraction,
|
|
105
|
+
): SharedVocabularyExtendedTextCounterPosition | undefined {
|
|
106
|
+
return extendedTextCounterPositionFromAttributes(interaction.attributes);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function supportedInputWidthClassNames(classNames: string[]): string[] {
|
|
110
|
+
return classNames.filter((className) => {
|
|
111
|
+
const value = inputWidthClassPattern.exec(className)?.[1];
|
|
112
|
+
return value !== undefined && inputWidthSet.has(Number(value));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const supportedGapInputWidthClassNames = supportedInputWidthClassNames;
|
|
117
|
+
|
|
118
|
+
export function supportedExtendedTextHeightLinesClassNames(classNames: string[]): string[] {
|
|
119
|
+
return classNames.filter((className) => {
|
|
120
|
+
const value = extendedTextHeightLinesClassPattern.exec(className)?.[1];
|
|
121
|
+
return value !== undefined && extendedTextHeightLinesSet.has(Number(value));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function supportedExtendedTextCounterClassNames(classNames: string[]): string[] {
|
|
126
|
+
return classNames.filter((className) => extendedTextCounterClassSet.has(className));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function mediaPlayerControlsTokens(value: string | undefined): string[] {
|
|
130
|
+
return (value ?? "").split(/\s+/).filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function unsupportedMediaPlayerControlsTokens(value: string | undefined): string[] {
|
|
134
|
+
return mediaPlayerControlsTokens(value).filter((token) => !mediaPlayerControlsSet.has(token));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isSupportedMediaPlayerControlsToken(token: string): boolean {
|
|
138
|
+
return mediaPlayerControlsSet.has(token);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function formatSupportedMediaPlayerControlsTokens(): string {
|
|
142
|
+
return SHARED_VOCABULARY_MEDIA_PLAYER_CONTROLS.join(", ");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isSupportedInputWidthClassName(className: string): boolean {
|
|
146
|
+
if (!className.startsWith("qti-input-width-")) return false;
|
|
147
|
+
const value = inputWidthClassPattern.exec(className)?.[1];
|
|
148
|
+
return value !== undefined && inputWidthSet.has(Number(value));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const isSupportedGapInputWidthClassName = isSupportedInputWidthClassName;
|
|
152
|
+
|
|
153
|
+
export function isSupportedExtendedTextHeightLinesClassName(className: string): boolean {
|
|
154
|
+
if (!className.startsWith("qti-height-lines-")) return false;
|
|
155
|
+
const value = extendedTextHeightLinesClassPattern.exec(className)?.[1];
|
|
156
|
+
return value !== undefined && extendedTextHeightLinesSet.has(Number(value));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isSupportedExtendedTextCounterClassName(className: string): boolean {
|
|
160
|
+
return extendedTextCounterClassSet.has(className);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function formatSupportedInputWidthClasses(): string {
|
|
164
|
+
return SHARED_VOCABULARY_INPUT_WIDTHS.map((width) => `qti-input-width-${width}`).join(", ");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const formatSupportedGapInputWidthClasses = formatSupportedInputWidthClasses;
|
|
168
|
+
|
|
169
|
+
export function formatSupportedExtendedTextHeightLinesClasses(): string {
|
|
170
|
+
return SHARED_VOCABULARY_EXTENDED_TEXT_HEIGHT_LINES.map(
|
|
171
|
+
(lines) => `qti-height-lines-${lines}`,
|
|
172
|
+
).join(", ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function formatSupportedExtendedTextCounterClasses(): string {
|
|
176
|
+
return SHARED_VOCABULARY_EXTENDED_TEXT_COUNTER_CLASSES.join(", ");
|
|
177
|
+
}
|