@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.
@@ -10,6 +10,10 @@ export interface ComposerItemContext {
10
10
  lang?: string;
11
11
  title?: string;
12
12
  itemBody?: Document;
13
+ items?: Array<{
14
+ identifier?: string;
15
+ title?: string;
16
+ }>;
13
17
  }
14
18
  export interface ResponseDeclaration {
15
19
  identifier: string;
@@ -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;CACrB;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,CAqHhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAkCzF;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,CAyChI;AAiMD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B7C"}
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"}
@@ -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
- if (declarations.length === 1) {
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 if (declarations.length > 1) {
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 xml = buildAssessmentItemXml(itemContext);
307
- return [{
308
- identifier: itemContext.identifier?.trim() || 'item-1',
309
- title: itemContext.title?.trim() || 'Untitled Item',
310
- xml,
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(fragmentContext),
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 && !seenIdentifiers.has(composeResult.responseDeclaration.identifier)) {
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
- ...composeResult.responseDeclaration,
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
- const kind = declaration.responseProcessingKind ?? 'match_correct';
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
- const variable = xmlDoc.createElementNS(QTI_NS, 'qti-variable');
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
- const variable = xmlDoc.createElementNS(QTI_NS, 'qti-variable');
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.2",
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.0",
28
- "@qti-editor/interaction-associate": "1.2.1",
29
- "@qti-editor/interaction-choice": "1.2.1",
30
- "@qti-editor/interaction-extended-text": "1.2.1",
31
- "@qti-editor/interaction-gap-match": "1.1.0",
32
- "@qti-editor/interaction-hottext": "1.2.2",
33
- "@qti-editor/interaction-inline-choice": "1.2.1",
34
- "@qti-editor/interaction-match": "1.2.0",
35
- "@qti-editor/interaction-order": "0.6.0",
36
- "@qti-editor/interaction-select-point": "1.2.0",
37
- "@qti-editor/interaction-shared": "1.3.0",
38
- "@qti-editor/interaction-text-entry": "1.2.0",
39
- "@qti-editor/qti-item-divider": "1.1.0"
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",