@qti-editor/core 1.4.0 → 1.5.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/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +81 -8
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/composer/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/C,QAAQ,EAAE,YAAY,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC7D,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,KAAK,CAAC;YACb,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;YACpB,aAAa,EAAE,OAAO,CAAC;SACxB,CAAC,CAAC;KACJ,CAAC;IACF,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,WAAW,CAAC,EAAE;QACZ,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,KAAK,CAAC;YACb,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC;YACzB,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC,CAAC;KACJ,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/composer/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/C,QAAQ,EAAE,YAAY,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC7D,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,KAAK,CAAC;YACb,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;YACpB,aAAa,EAAE,OAAO,CAAC;SACxB,CAAC,CAAC;KACJ,CAAC;IACF,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,WAAW,CAAC,EAAE;QACZ,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,KAAK,CAAC;YACb,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC;YACzB,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC,CAAC;KACJ,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkGD,wBAAgB,2BAA2B,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,mBAAmB,EAAE,CAOhG;AAkFD,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAmIhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAqDzF;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAUtF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAK5E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA0DhI;AA2KD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B7C"}
|
package/dist/composer/index.js
CHANGED
|
@@ -24,6 +24,28 @@ const TEXT_ENTRY_DATA_ATTRIBUTE_MAPPINGS = [
|
|
|
24
24
|
const SELECT_POINT_DATA_ATTRIBUTE_MAPPINGS = [
|
|
25
25
|
{ source: 'area-mappings', target: 'data-area-mappings' },
|
|
26
26
|
];
|
|
27
|
+
function sanitizeIdentifier(value, fallback) {
|
|
28
|
+
const sanitized = value?.trim().replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
29
|
+
return sanitized || fallback;
|
|
30
|
+
}
|
|
31
|
+
function hashIdentifierSeed(value) {
|
|
32
|
+
let hash = 0x811c9dc5;
|
|
33
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
34
|
+
hash ^= value.charCodeAt(i);
|
|
35
|
+
hash = Math.imul(hash, 0x01000193);
|
|
36
|
+
}
|
|
37
|
+
return (hash >>> 0).toString(36);
|
|
38
|
+
}
|
|
39
|
+
function createAutoIdentifier(options) {
|
|
40
|
+
const titleBase = sanitizeIdentifier(options.title, '');
|
|
41
|
+
const base = titleBase || sanitizeIdentifier(options.baseIdentifier, 'item');
|
|
42
|
+
const serializedBody = options.body == null
|
|
43
|
+
? ''
|
|
44
|
+
: new XMLSerializer().serializeToString(options.body instanceof Document ? options.body.documentElement : options.body);
|
|
45
|
+
const seed = [base, options.itemNumber ?? 1, serializedBody].join('|');
|
|
46
|
+
const suffix = hashIdentifierSeed(seed).slice(0, 8);
|
|
47
|
+
return `${base}-${suffix}`;
|
|
48
|
+
}
|
|
27
49
|
function parseCorrectResponseValues(declaration) {
|
|
28
50
|
const value = declaration.correctResponse;
|
|
29
51
|
if (value == null)
|
|
@@ -140,7 +162,11 @@ export function buildAssessmentItemXml(itemContext) {
|
|
|
140
162
|
const root = xmlDoc.documentElement;
|
|
141
163
|
root.setAttribute('xmlns', QTI_NS);
|
|
142
164
|
root.setAttributeNS(XSI_NS, 'xsi:schemaLocation', SCHEMA_LOCATION);
|
|
143
|
-
root.setAttribute('identifier', itemContext.identifier
|
|
165
|
+
root.setAttribute('identifier', sanitizeIdentifier(itemContext.identifier, createAutoIdentifier({
|
|
166
|
+
title: itemContext.title,
|
|
167
|
+
baseIdentifier: itemContext.identifier,
|
|
168
|
+
body: itemContext.itemBody,
|
|
169
|
+
})));
|
|
144
170
|
root.setAttribute('title', itemContext.title?.trim() || 'Untitled Item');
|
|
145
171
|
root.setAttribute('adaptive', 'false');
|
|
146
172
|
root.setAttribute('time-dependent', 'false');
|
|
@@ -251,17 +277,35 @@ export function buildMultipleAssessmentItemsXml(itemContext) {
|
|
|
251
277
|
return buildAssessmentItemXml(itemContext);
|
|
252
278
|
}
|
|
253
279
|
// Build multiple items
|
|
254
|
-
const baseIdentifier = itemContext.identifier
|
|
280
|
+
const baseIdentifier = sanitizeIdentifier(itemContext.identifier, 'item');
|
|
255
281
|
const baseTitle = itemContext.title?.trim() || 'Untitled Item';
|
|
256
282
|
const lang = itemContext.lang?.trim() || 'en';
|
|
283
|
+
const usedIdentifiers = new Set();
|
|
257
284
|
const itemXmls = fragments.map((fragmentBody, index) => {
|
|
258
285
|
const itemNumber = index + 1;
|
|
259
286
|
const perItem = itemContext.items?.[index];
|
|
260
287
|
const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
|
|
261
288
|
const importedFragment = fragmentDoc.importNode(fragmentBody, true);
|
|
262
289
|
fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
|
|
290
|
+
let identifier = perItem?.identifier?.trim();
|
|
291
|
+
if (!identifier) {
|
|
292
|
+
identifier = createAutoIdentifier({
|
|
293
|
+
title: perItem?.title || `${baseTitle} ${itemNumber}`,
|
|
294
|
+
baseIdentifier: `${baseIdentifier}-${itemNumber}`,
|
|
295
|
+
itemNumber,
|
|
296
|
+
body: fragmentDoc,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
identifier = sanitizeIdentifier(identifier, `${baseIdentifier}-${itemNumber}`);
|
|
300
|
+
let uniqueIdentifier = identifier;
|
|
301
|
+
let duplicateIndex = 2;
|
|
302
|
+
while (usedIdentifiers.has(uniqueIdentifier)) {
|
|
303
|
+
uniqueIdentifier = `${identifier}-${duplicateIndex}`;
|
|
304
|
+
duplicateIndex += 1;
|
|
305
|
+
}
|
|
306
|
+
usedIdentifiers.add(uniqueIdentifier);
|
|
263
307
|
const fragmentContext = {
|
|
264
|
-
identifier:
|
|
308
|
+
identifier: uniqueIdentifier,
|
|
265
309
|
title: perItem?.title?.trim() || `${baseTitle} ${itemNumber}`,
|
|
266
310
|
lang,
|
|
267
311
|
itemBody: fragmentDoc,
|
|
@@ -308,26 +352,48 @@ export function getItemFragmentXmls(itemContext) {
|
|
|
308
352
|
// If only one fragment (no dividers), return single item
|
|
309
353
|
if (fragments.length <= 1) {
|
|
310
354
|
const perItem = itemContext.items?.[0];
|
|
311
|
-
const identifier = perItem?.identifier
|
|
355
|
+
const identifier = sanitizeIdentifier(perItem?.identifier || itemContext.identifier, createAutoIdentifier({
|
|
356
|
+
title: perItem?.title || itemContext.title,
|
|
357
|
+
baseIdentifier: itemContext.identifier,
|
|
358
|
+
itemNumber: 1,
|
|
359
|
+
body: itemContext.itemBody,
|
|
360
|
+
}));
|
|
312
361
|
const title = perItem?.title?.trim() || itemContext.title?.trim() || 'Untitled Item';
|
|
313
362
|
const xml = buildAssessmentItemXml({ ...itemContext, identifier, title });
|
|
314
363
|
return [{ identifier, title, xml }];
|
|
315
364
|
}
|
|
316
|
-
const baseIdentifier = itemContext.identifier
|
|
365
|
+
const baseIdentifier = sanitizeIdentifier(itemContext.identifier, 'item');
|
|
317
366
|
const baseTitle = itemContext.title?.trim() || 'Untitled Item';
|
|
318
367
|
const lang = itemContext.lang?.trim() || 'en';
|
|
368
|
+
const usedIdentifiers = new Set();
|
|
319
369
|
return fragments.map((fragmentBody, index) => {
|
|
320
370
|
const itemNumber = index + 1;
|
|
321
371
|
const perItem = itemContext.items?.[index];
|
|
322
|
-
const identifier = perItem?.identifier?.trim() || `${baseIdentifier}-${itemNumber}`;
|
|
323
372
|
const title = perItem?.title?.trim() || `${baseTitle} ${itemNumber}`;
|
|
324
373
|
const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
|
|
325
374
|
const importedFragment = fragmentDoc.importNode(fragmentBody, true);
|
|
326
375
|
fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
|
|
376
|
+
let identifier = perItem?.identifier?.trim();
|
|
377
|
+
if (!identifier) {
|
|
378
|
+
identifier = createAutoIdentifier({
|
|
379
|
+
title,
|
|
380
|
+
baseIdentifier: `${baseIdentifier}-${itemNumber}`,
|
|
381
|
+
itemNumber,
|
|
382
|
+
body: fragmentDoc,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
identifier = sanitizeIdentifier(identifier, `${baseIdentifier}-${itemNumber}`);
|
|
386
|
+
let uniqueIdentifier = identifier;
|
|
387
|
+
let duplicateIndex = 2;
|
|
388
|
+
while (usedIdentifiers.has(uniqueIdentifier)) {
|
|
389
|
+
uniqueIdentifier = `${identifier}-${duplicateIndex}`;
|
|
390
|
+
duplicateIndex += 1;
|
|
391
|
+
}
|
|
392
|
+
usedIdentifiers.add(uniqueIdentifier);
|
|
327
393
|
return {
|
|
328
|
-
identifier,
|
|
394
|
+
identifier: uniqueIdentifier,
|
|
329
395
|
title,
|
|
330
|
-
xml: buildAssessmentItemXml({ identifier, title, lang, itemBody: fragmentDoc }),
|
|
396
|
+
xml: buildAssessmentItemXml({ identifier: uniqueIdentifier, title, lang, itemBody: fragmentDoc }),
|
|
331
397
|
};
|
|
332
398
|
});
|
|
333
399
|
}
|
|
@@ -394,6 +460,13 @@ function composeAndNormalizeItemBody(itemBody, xmlDoc) {
|
|
|
394
460
|
element.removeAttribute('score');
|
|
395
461
|
});
|
|
396
462
|
if (declarations.length === 1 && templateCandidates.size === 1) {
|
|
463
|
+
// Normalize to the QTI reserved keyword "RESPONSE" — templates reference this well-known identifier
|
|
464
|
+
const declaration = declarations[0];
|
|
465
|
+
if (declaration.identifier !== 'RESPONSE') {
|
|
466
|
+
const element = itemBody.querySelector(`[response-identifier="${declaration.identifier}"]`);
|
|
467
|
+
element?.setAttribute('response-identifier', 'RESPONSE');
|
|
468
|
+
declaration.identifier = 'RESPONSE';
|
|
469
|
+
}
|
|
397
470
|
return { declarations, responseTemplate: Array.from(templateCandidates)[0], maxScore, hasAutomatedProcessing: true };
|
|
398
471
|
}
|
|
399
472
|
return { declarations, responseTemplate: MATCH_CORRECT_TEMPLATE, maxScore, hasAutomatedProcessing: templateCandidates.size > 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qti-editor/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "QTI semantics, composer registry, and XML export orchestration for QTI Editor",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@qti-editor/interfaces": "1.2.1",
|
|
28
|
-
"@qti-editor/interaction-associate": "1.2.
|
|
29
|
-
"@qti-editor/interaction-choice": "1.2.
|
|
30
|
-
"@qti-editor/interaction-extended-text": "1.3.
|
|
31
|
-
"@qti-editor/interaction-gap-match": "1.1.
|
|
32
|
-
"@qti-editor/interaction-hottext": "1.2.
|
|
33
|
-
"@qti-editor/interaction-inline-choice": "1.3.
|
|
34
|
-
"@qti-editor/interaction-match": "1.2.
|
|
35
|
-
"@qti-editor/interaction-order": "0.6.
|
|
36
|
-
"@qti-editor/interaction-select-point": "1.2.
|
|
37
|
-
"@qti-editor/interaction-shared": "1.3.
|
|
38
|
-
"@qti-editor/interaction-text-entry": "1.2.
|
|
39
|
-
"@qti-editor/qti-item-divider": "1.1.
|
|
28
|
+
"@qti-editor/interaction-associate": "1.2.4",
|
|
29
|
+
"@qti-editor/interaction-choice": "1.2.3",
|
|
30
|
+
"@qti-editor/interaction-extended-text": "1.3.1",
|
|
31
|
+
"@qti-editor/interaction-gap-match": "1.1.2",
|
|
32
|
+
"@qti-editor/interaction-hottext": "1.2.4",
|
|
33
|
+
"@qti-editor/interaction-inline-choice": "1.3.1",
|
|
34
|
+
"@qti-editor/interaction-match": "1.2.2",
|
|
35
|
+
"@qti-editor/interaction-order": "0.6.2",
|
|
36
|
+
"@qti-editor/interaction-select-point": "1.2.2",
|
|
37
|
+
"@qti-editor/interaction-shared": "1.3.2",
|
|
38
|
+
"@qti-editor/interaction-text-entry": "1.2.2",
|
|
39
|
+
"@qti-editor/qti-item-divider": "1.1.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.0.0",
|