@qti-editor/core 1.3.2 → 1.4.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 +4 -0
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +36 -66
- package/package.json +14 -14
package/dist/composer/index.d.ts
CHANGED
|
@@ -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;
|
|
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;AAiED,wBAAgB,2BAA2B,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,mBAAmB,EAAE,CAOhG;AAkFD,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAyHhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAmCzF;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,CAiChI;AAoKD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B7C"}
|
package/dist/composer/index.js
CHANGED
|
@@ -153,7 +153,7 @@ export function buildAssessmentItemXml(itemContext) {
|
|
|
153
153
|
const composedItemBody = sourceBodyRoot != null
|
|
154
154
|
? xmlDoc.importNode(sourceBodyRoot, true)
|
|
155
155
|
: xmlDoc.createElementNS(QTI_NS, 'qti-item-body');
|
|
156
|
-
const { declarations, responseTemplate, maxScore } = composeAndNormalizeItemBody(composedItemBody, xmlDoc);
|
|
156
|
+
const { declarations, responseTemplate, maxScore, hasAutomatedProcessing } = composeAndNormalizeItemBody(composedItemBody, xmlDoc);
|
|
157
157
|
declarations.forEach(declaration => {
|
|
158
158
|
const responseDeclaration = xmlDoc.createElementNS(QTI_NS, 'qti-response-declaration');
|
|
159
159
|
responseDeclaration.setAttribute('identifier', declaration.identifier);
|
|
@@ -218,17 +218,20 @@ export function buildAssessmentItemXml(itemContext) {
|
|
|
218
218
|
root.appendChild(maxScoreOutcomeDeclaration);
|
|
219
219
|
}
|
|
220
220
|
root.appendChild(composedItemBody);
|
|
221
|
-
if (maxScore > 0) {
|
|
222
|
-
|
|
221
|
+
if (maxScore > 0 && hasAutomatedProcessing) {
|
|
222
|
+
const single = declarations.length === 1 ? declarations[0] : null;
|
|
223
|
+
const canUseTemplate = single !== null &&
|
|
224
|
+
!(single.responseProcessingKind === 'match_correct' && (single.score ?? 1) !== 1);
|
|
225
|
+
if (canUseTemplate) {
|
|
223
226
|
const responseProcessing = xmlDoc.createElementNS(QTI_NS, 'qti-response-processing');
|
|
224
227
|
responseProcessing.setAttribute('template', responseTemplate);
|
|
225
228
|
root.appendChild(responseProcessing);
|
|
226
229
|
}
|
|
227
|
-
else
|
|
230
|
+
else {
|
|
228
231
|
root.appendChild(buildMultiInteractionResponseProcessing(xmlDoc, declarations));
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
|
-
return new XMLSerializer().serializeToString(xmlDoc);
|
|
234
|
+
return new XMLSerializer().serializeToString(xmlDoc).replace(/\s+xmlns=""/g, '');
|
|
232
235
|
}
|
|
233
236
|
/**
|
|
234
237
|
* Build multiple QTI assessment items from a single editor document.
|
|
@@ -253,12 +256,13 @@ export function buildMultipleAssessmentItemsXml(itemContext) {
|
|
|
253
256
|
const lang = itemContext.lang?.trim() || 'en';
|
|
254
257
|
const itemXmls = fragments.map((fragmentBody, index) => {
|
|
255
258
|
const itemNumber = index + 1;
|
|
259
|
+
const perItem = itemContext.items?.[index];
|
|
256
260
|
const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
|
|
257
261
|
const importedFragment = fragmentDoc.importNode(fragmentBody, true);
|
|
258
262
|
fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
|
|
259
263
|
const fragmentContext = {
|
|
260
|
-
identifier: `${baseIdentifier}-${itemNumber}`,
|
|
261
|
-
title: `${baseTitle} ${itemNumber}`,
|
|
264
|
+
identifier: perItem?.identifier?.trim() || `${baseIdentifier}-${itemNumber}`,
|
|
265
|
+
title: perItem?.title?.trim() || `${baseTitle} ${itemNumber}`,
|
|
262
266
|
lang,
|
|
263
267
|
itemBody: fragmentDoc,
|
|
264
268
|
};
|
|
@@ -303,33 +307,27 @@ export function getItemFragmentXmls(itemContext) {
|
|
|
303
307
|
const fragments = splitItemBodyAtDividers(itemContext.itemBody);
|
|
304
308
|
// If only one fragment (no dividers), return single item
|
|
305
309
|
if (fragments.length <= 1) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}];
|
|
310
|
+
const perItem = itemContext.items?.[0];
|
|
311
|
+
const identifier = perItem?.identifier?.trim() || itemContext.identifier?.trim() || 'item-1';
|
|
312
|
+
const title = perItem?.title?.trim() || itemContext.title?.trim() || 'Untitled Item';
|
|
313
|
+
const xml = buildAssessmentItemXml({ ...itemContext, identifier, title });
|
|
314
|
+
return [{ identifier, title, xml }];
|
|
312
315
|
}
|
|
313
316
|
const baseIdentifier = itemContext.identifier?.trim() || 'item';
|
|
314
317
|
const baseTitle = itemContext.title?.trim() || 'Untitled Item';
|
|
315
318
|
const lang = itemContext.lang?.trim() || 'en';
|
|
316
319
|
return fragments.map((fragmentBody, index) => {
|
|
317
320
|
const itemNumber = index + 1;
|
|
321
|
+
const perItem = itemContext.items?.[index];
|
|
322
|
+
const identifier = perItem?.identifier?.trim() || `${baseIdentifier}-${itemNumber}`;
|
|
323
|
+
const title = perItem?.title?.trim() || `${baseTitle} ${itemNumber}`;
|
|
318
324
|
const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
|
|
319
325
|
const importedFragment = fragmentDoc.importNode(fragmentBody, true);
|
|
320
326
|
fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
|
|
321
|
-
const identifier = `${baseIdentifier}-${itemNumber}`;
|
|
322
|
-
const title = `${baseTitle} ${itemNumber}`;
|
|
323
|
-
const fragmentContext = {
|
|
324
|
-
identifier,
|
|
325
|
-
title,
|
|
326
|
-
lang,
|
|
327
|
-
itemBody: fragmentDoc,
|
|
328
|
-
};
|
|
329
327
|
return {
|
|
330
328
|
identifier,
|
|
331
329
|
title,
|
|
332
|
-
xml: buildAssessmentItemXml(
|
|
330
|
+
xml: buildAssessmentItemXml({ identifier, title, lang, itemBody: fragmentDoc }),
|
|
333
331
|
};
|
|
334
332
|
});
|
|
335
333
|
}
|
|
@@ -368,14 +366,17 @@ function composeAndNormalizeItemBody(itemBody, xmlDoc) {
|
|
|
368
366
|
if (composeResult.responseDeclaration) {
|
|
369
367
|
maxScore += composeResult.responseDeclaration.score ?? 1;
|
|
370
368
|
}
|
|
371
|
-
if (composeResult.responseDeclaration
|
|
369
|
+
if (composeResult.responseDeclaration) {
|
|
370
|
+
const declaration = composeResult.responseDeclaration;
|
|
371
|
+
if (seenIdentifiers.has(declaration.identifier)) {
|
|
372
|
+
const freshId = `RESPONSE_${crypto.randomUUID()}`;
|
|
373
|
+
composeResult.normalizedElement.setAttribute('response-identifier', freshId);
|
|
374
|
+
declaration.identifier = freshId;
|
|
375
|
+
}
|
|
372
376
|
const responseProcessingKind = composeResult
|
|
373
377
|
.responseProcessingKind;
|
|
374
|
-
declarations.push({
|
|
375
|
-
|
|
376
|
-
responseProcessingKind,
|
|
377
|
-
});
|
|
378
|
-
seenIdentifiers.add(composeResult.responseDeclaration.identifier);
|
|
378
|
+
declarations.push({ ...declaration, responseProcessingKind });
|
|
379
|
+
seenIdentifiers.add(declaration.identifier);
|
|
379
380
|
}
|
|
380
381
|
if (composeResult.responseProcessingTemplate && composeResult.responseDeclaration) {
|
|
381
382
|
templateCandidates.add(composeResult.responseProcessingTemplate);
|
|
@@ -392,16 +393,17 @@ function composeAndNormalizeItemBody(itemBody, xmlDoc) {
|
|
|
392
393
|
itemBody.querySelectorAll('[score]').forEach(element => {
|
|
393
394
|
element.removeAttribute('score');
|
|
394
395
|
});
|
|
395
|
-
normalizeResponseIdentifiers(itemBody, declarations);
|
|
396
396
|
if (declarations.length === 1 && templateCandidates.size === 1) {
|
|
397
|
-
return { declarations, responseTemplate: Array.from(templateCandidates)[0], maxScore };
|
|
397
|
+
return { declarations, responseTemplate: Array.from(templateCandidates)[0], maxScore, hasAutomatedProcessing: true };
|
|
398
398
|
}
|
|
399
|
-
return { declarations, responseTemplate: MATCH_CORRECT_TEMPLATE, maxScore };
|
|
399
|
+
return { declarations, responseTemplate: MATCH_CORRECT_TEMPLATE, maxScore, hasAutomatedProcessing: templateCandidates.size > 0 };
|
|
400
400
|
}
|
|
401
401
|
function buildMultiInteractionResponseProcessing(xmlDoc, declarations) {
|
|
402
402
|
const responseProcessing = xmlDoc.createElementNS(QTI_NS, 'qti-response-processing');
|
|
403
403
|
declarations.forEach(declaration => {
|
|
404
|
-
|
|
404
|
+
if (declaration.responseProcessingKind === undefined)
|
|
405
|
+
return;
|
|
406
|
+
const kind = declaration.responseProcessingKind;
|
|
405
407
|
if (kind === 'match_correct') {
|
|
406
408
|
responseProcessing.appendChild(createMatchCorrectContribution(xmlDoc, declaration.identifier, declaration.score ?? 1));
|
|
407
409
|
return;
|
|
@@ -434,16 +436,12 @@ function createMatchCorrectContribution(xmlDoc, responseIdentifier, score = 1) {
|
|
|
434
436
|
}
|
|
435
437
|
function createMapResponseContribution(xmlDoc, responseIdentifier) {
|
|
436
438
|
const mapResponse = xmlDoc.createElementNS(QTI_NS, 'qti-map-response');
|
|
437
|
-
|
|
438
|
-
variable.setAttribute('identifier', responseIdentifier);
|
|
439
|
-
mapResponse.appendChild(variable);
|
|
439
|
+
mapResponse.setAttribute('identifier', responseIdentifier);
|
|
440
440
|
return createScoreIncrement(xmlDoc, mapResponse);
|
|
441
441
|
}
|
|
442
442
|
function createMapResponsePointContribution(xmlDoc, responseIdentifier) {
|
|
443
443
|
const mapResponsePoint = xmlDoc.createElementNS(QTI_NS, 'qti-map-response-point');
|
|
444
|
-
|
|
445
|
-
variable.setAttribute('identifier', responseIdentifier);
|
|
446
|
-
mapResponsePoint.appendChild(variable);
|
|
444
|
+
mapResponsePoint.setAttribute('identifier', responseIdentifier);
|
|
447
445
|
return createScoreIncrement(xmlDoc, mapResponsePoint);
|
|
448
446
|
}
|
|
449
447
|
function createScoreIncrement(xmlDoc, contribution) {
|
|
@@ -457,34 +455,6 @@ function createScoreIncrement(xmlDoc, contribution) {
|
|
|
457
455
|
setOutcomeValue.appendChild(sum);
|
|
458
456
|
return setOutcomeValue;
|
|
459
457
|
}
|
|
460
|
-
function normalizeResponseIdentifiers(itemBody, declarations) {
|
|
461
|
-
if (declarations.length === 0)
|
|
462
|
-
return;
|
|
463
|
-
const identifierMap = new Map();
|
|
464
|
-
if (declarations.length === 1) {
|
|
465
|
-
identifierMap.set(declarations[0].identifier, 'RESPONSE');
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
declarations.forEach((declaration, index) => {
|
|
469
|
-
identifierMap.set(declaration.identifier, `RESPONSE${index + 1}`);
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
itemBody.querySelectorAll('[response-identifier]').forEach(interaction => {
|
|
473
|
-
const currentIdentifier = interaction.getAttribute('response-identifier')?.trim();
|
|
474
|
-
if (!currentIdentifier)
|
|
475
|
-
return;
|
|
476
|
-
const mappedIdentifier = identifierMap.get(currentIdentifier);
|
|
477
|
-
if (mappedIdentifier) {
|
|
478
|
-
interaction.setAttribute('response-identifier', mappedIdentifier);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
declarations.forEach(declaration => {
|
|
482
|
-
const mappedIdentifier = identifierMap.get(declaration.identifier);
|
|
483
|
-
if (mappedIdentifier) {
|
|
484
|
-
declaration.identifier = mappedIdentifier;
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
458
|
export function formatXml(xml) {
|
|
489
459
|
const PADDING = ' ';
|
|
490
460
|
const reg = /(>)(<)(\/*)/g;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qti-editor/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "QTI semantics, composer registry, and XML export orchestration for QTI Editor",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@qti-editor/interfaces": "1.2.
|
|
28
|
-
"@qti-editor/interaction-associate": "1.2.
|
|
29
|
-
"@qti-editor/interaction-choice": "1.2.
|
|
30
|
-
"@qti-editor/interaction-extended-text": "1.
|
|
31
|
-
"@qti-editor/interaction-gap-match": "1.1.
|
|
32
|
-
"@qti-editor/interaction-hottext": "1.2.
|
|
33
|
-
"@qti-editor/interaction-inline-choice": "1.
|
|
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.
|
|
27
|
+
"@qti-editor/interfaces": "1.2.1",
|
|
28
|
+
"@qti-editor/interaction-associate": "1.2.3",
|
|
29
|
+
"@qti-editor/interaction-choice": "1.2.2",
|
|
30
|
+
"@qti-editor/interaction-extended-text": "1.3.0",
|
|
31
|
+
"@qti-editor/interaction-gap-match": "1.1.1",
|
|
32
|
+
"@qti-editor/interaction-hottext": "1.2.3",
|
|
33
|
+
"@qti-editor/interaction-inline-choice": "1.3.0",
|
|
34
|
+
"@qti-editor/interaction-match": "1.2.1",
|
|
35
|
+
"@qti-editor/interaction-order": "0.6.1",
|
|
36
|
+
"@qti-editor/interaction-select-point": "1.2.1",
|
|
37
|
+
"@qti-editor/interaction-shared": "1.3.1",
|
|
38
|
+
"@qti-editor/interaction-text-entry": "1.2.1",
|
|
39
|
+
"@qti-editor/qti-item-divider": "1.1.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.0.0",
|