@qti-editor/core 1.2.0 → 1.3.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.
@@ -34,6 +34,7 @@ export interface ResponseDeclaration {
34
34
  }>;
35
35
  };
36
36
  sourceTag: string;
37
+ score?: number;
37
38
  }
38
39
  export declare function extractResponseDeclarations(itemBodyRoot?: Element | null): ResponseDeclaration[];
39
40
  export declare function buildAssessmentItemXml(itemContext?: ComposerItemContext): string;
@@ -47,5 +48,25 @@ export declare function buildAssessmentItemXml(itemContext?: ComposerItemContext
47
48
  * or a single assessment item if no dividers are found.
48
49
  */
49
50
  export declare function buildMultipleAssessmentItemsXml(itemContext?: ComposerItemContext): string;
51
+ /**
52
+ * Build a single QTI assessment item, converting any dividers to <hr /> elements.
53
+ * Use this when you want to export the entire editor content as one item.
54
+ */
55
+ export declare function buildSingleAssessmentItemXml(itemContext?: ComposerItemContext): string;
56
+ /**
57
+ * Count how many items would be generated from an item body document.
58
+ * Returns 1 if no dividers, or dividers.length + 1 otherwise.
59
+ */
60
+ export declare function countItemFragments(itemContext?: ComposerItemContext): number;
61
+ /**
62
+ * Get an array of individual assessment item XMLs.
63
+ * Each item is generated from a segment between dividers.
64
+ * Returns an array with identifier and XML for each item.
65
+ */
66
+ export declare function getItemFragmentXmls(itemContext?: ComposerItemContext): Array<{
67
+ identifier: string;
68
+ title: string;
69
+ xml: string;
70
+ }>;
50
71
  export declare function formatXml(xml: string): string;
51
72
  //# sourceMappingURL=index.d.ts.map
@@ -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,CAAC;IACzB,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;CACnB;AAoCD,wBAAgB,2BAA2B,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,mBAAmB,EAAE,CAOhG;AAsDD,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAqHhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAkCzF;AAgLD,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;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,CAAC;IACzB,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;AAoCD,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;AAgMD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B7C"}
@@ -40,6 +40,29 @@ export function extractResponseDeclarations(itemBodyRoot) {
40
40
  const { declarations } = composeAndNormalizeItemBody(tempRoot, tempDoc);
41
41
  return declarations;
42
42
  }
43
+ /**
44
+ * Convert qti-item-divider elements to <hr /> elements.
45
+ * Used when composing a single item that should preserve dividers as visual separators.
46
+ */
47
+ function convertDividersToHr(itemBodyDoc) {
48
+ const itemBodyRoot = itemBodyDoc.querySelector('qti-item-body') ??
49
+ (itemBodyDoc.documentElement?.tagName.toLowerCase() === 'qti-item-body'
50
+ ? itemBodyDoc.documentElement
51
+ : null);
52
+ if (!itemBodyRoot)
53
+ return itemBodyDoc;
54
+ // Clone the document to avoid mutating the original
55
+ const clonedDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
56
+ const clonedRoot = clonedDoc.importNode(itemBodyRoot, true);
57
+ clonedDoc.replaceChild(clonedRoot, clonedDoc.documentElement);
58
+ // Find and replace all dividers with <hr />
59
+ const dividers = Array.from(clonedRoot.querySelectorAll('qti-item-divider'));
60
+ for (const divider of dividers) {
61
+ const hr = clonedDoc.createElementNS(QTI_NS, 'hr');
62
+ divider.parentNode?.replaceChild(hr, divider);
63
+ }
64
+ return clonedDoc;
65
+ }
43
66
  /**
44
67
  * Split an item body document at qti-item-divider elements.
45
68
  * Returns an array of item body fragments, one for each item.
@@ -220,6 +243,71 @@ export function buildMultipleAssessmentItemsXml(itemContext) {
220
243
  const separator = '\n\n<!-- Next Assessment Item -->\n\n';
221
244
  return itemXmls.join(separator);
222
245
  }
246
+ /**
247
+ * Build a single QTI assessment item, converting any dividers to <hr /> elements.
248
+ * Use this when you want to export the entire editor content as one item.
249
+ */
250
+ export function buildSingleAssessmentItemXml(itemContext) {
251
+ if (!itemContext?.itemBody)
252
+ return '';
253
+ // Convert dividers to <hr /> elements
254
+ const convertedDoc = convertDividersToHr(itemContext.itemBody);
255
+ return buildAssessmentItemXml({
256
+ ...itemContext,
257
+ itemBody: convertedDoc,
258
+ });
259
+ }
260
+ /**
261
+ * Count how many items would be generated from an item body document.
262
+ * Returns 1 if no dividers, or dividers.length + 1 otherwise.
263
+ */
264
+ export function countItemFragments(itemContext) {
265
+ if (!itemContext?.itemBody)
266
+ return 0;
267
+ const fragments = splitItemBodyAtDividers(itemContext.itemBody);
268
+ return fragments.length;
269
+ }
270
+ /**
271
+ * Get an array of individual assessment item XMLs.
272
+ * Each item is generated from a segment between dividers.
273
+ * Returns an array with identifier and XML for each item.
274
+ */
275
+ export function getItemFragmentXmls(itemContext) {
276
+ if (!itemContext?.itemBody)
277
+ return [];
278
+ const fragments = splitItemBodyAtDividers(itemContext.itemBody);
279
+ // If only one fragment (no dividers), return single item
280
+ if (fragments.length <= 1) {
281
+ const xml = buildAssessmentItemXml(itemContext);
282
+ return [{
283
+ identifier: itemContext.identifier?.trim() || 'item-1',
284
+ title: itemContext.title?.trim() || 'Untitled Item',
285
+ xml,
286
+ }];
287
+ }
288
+ const baseIdentifier = itemContext.identifier?.trim() || 'item';
289
+ const baseTitle = itemContext.title?.trim() || 'Untitled Item';
290
+ const lang = itemContext.lang?.trim() || 'en';
291
+ return fragments.map((fragmentBody, index) => {
292
+ const itemNumber = index + 1;
293
+ const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
294
+ const importedFragment = fragmentDoc.importNode(fragmentBody, true);
295
+ fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
296
+ const identifier = `${baseIdentifier}-${itemNumber}`;
297
+ const title = `${baseTitle} ${itemNumber}`;
298
+ const fragmentContext = {
299
+ identifier,
300
+ title,
301
+ lang,
302
+ itemBody: fragmentDoc,
303
+ };
304
+ return {
305
+ identifier,
306
+ title,
307
+ xml: buildAssessmentItemXml(fragmentContext),
308
+ };
309
+ });
310
+ }
223
311
  function composeAndNormalizeItemBody(itemBody, xmlDoc) {
224
312
  const declarations = [];
225
313
  const seenIdentifiers = new Set();
@@ -238,9 +326,21 @@ function composeAndNormalizeItemBody(itemBody, xmlDoc) {
238
326
  const parent = element.parentNode;
239
327
  if (parent) {
240
328
  parent.replaceChild(composeResult.normalizedElement, element);
329
+ // Insert any additional elements (like rubric blocks) after the interaction
330
+ if (composeResult.additionalElements?.length) {
331
+ const nextSibling = composeResult.normalizedElement.nextSibling;
332
+ for (const additionalElement of composeResult.additionalElements) {
333
+ if (nextSibling) {
334
+ parent.insertBefore(additionalElement, nextSibling);
335
+ }
336
+ else {
337
+ parent.appendChild(additionalElement);
338
+ }
339
+ }
340
+ }
241
341
  }
242
342
  if (composeResult.responseDeclaration) {
243
- maxScore += 1;
343
+ maxScore += composeResult.responseDeclaration.score ?? 1;
244
344
  }
245
345
  if (composeResult.responseDeclaration && !seenIdentifiers.has(composeResult.responseDeclaration.identifier)) {
246
346
  const responseProcessingKind = composeResult
@@ -263,6 +363,9 @@ function composeAndNormalizeItemBody(itemBody, xmlDoc) {
263
363
  itemBody.querySelectorAll('[correct-response]').forEach(interaction => {
264
364
  interaction.removeAttribute('correct-response');
265
365
  });
366
+ itemBody.querySelectorAll('[score]').forEach(element => {
367
+ element.removeAttribute('score');
368
+ });
266
369
  normalizeResponseIdentifiers(itemBody, declarations);
267
370
  if (declarations.length === 1 && templateCandidates.size === 1) {
268
371
  return { declarations, responseTemplate: Array.from(templateCandidates)[0], maxScore };
@@ -274,7 +377,7 @@ function buildMultiInteractionResponseProcessing(xmlDoc, declarations) {
274
377
  declarations.forEach(declaration => {
275
378
  const kind = declaration.responseProcessingKind ?? 'match_correct';
276
379
  if (kind === 'match_correct') {
277
- responseProcessing.appendChild(createMatchCorrectContribution(xmlDoc, declaration.identifier));
380
+ responseProcessing.appendChild(createMatchCorrectContribution(xmlDoc, declaration.identifier, declaration.score ?? 1));
278
381
  return;
279
382
  }
280
383
  if (kind === 'map_response') {
@@ -285,7 +388,7 @@ function buildMultiInteractionResponseProcessing(xmlDoc, declarations) {
285
388
  });
286
389
  return responseProcessing;
287
390
  }
288
- function createMatchCorrectContribution(xmlDoc, responseIdentifier) {
391
+ function createMatchCorrectContribution(xmlDoc, responseIdentifier, score = 1) {
289
392
  const responseCondition = xmlDoc.createElementNS(QTI_NS, 'qti-response-condition');
290
393
  const responseIf = xmlDoc.createElementNS(QTI_NS, 'qti-response-if');
291
394
  const match = xmlDoc.createElementNS(QTI_NS, 'qti-match');
@@ -298,7 +401,7 @@ function createMatchCorrectContribution(xmlDoc, responseIdentifier) {
298
401
  responseIf.appendChild(match);
299
402
  const incrementValue = xmlDoc.createElementNS(QTI_NS, 'qti-base-value');
300
403
  incrementValue.setAttribute('base-type', 'float');
301
- incrementValue.textContent = '1';
404
+ incrementValue.textContent = String(score);
302
405
  responseIf.appendChild(createScoreIncrement(xmlDoc, incrementValue));
303
406
  responseCondition.appendChild(responseIf);
304
407
  return responseCondition;
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/interactions/composer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,wBAAwB,CAAC;AAmChC,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,2BAA2B,GAAG,SAAS,CAEvG;AAED,wBAAgB,4CAA4C,CAC1D,YAAY,EAAE,MAAM,GACnB,2BAA2B,GAAG,SAAS,CAEzC;AAED,wBAAgB,2CAA2C,CACzD,YAAY,EAAE,MAAM,GACnB,0BAA0B,GAAG,SAAS,CAExC;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS,CAErG;AAED,wBAAgB,+BAA+B,IAAI,aAAa,CAAC,0BAA0B,CAAC,CAE3F;AAED,wBAAgB,0BAA0B,IAAI,aAAa,CAAC,qBAAqB,CAAC,CAEjF;AAED,wBAAgB,8BAA8B,IAAI,aAAa,CAC7D,WAAW,CAAC,qBAAqB,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,CAC9D,CAEA"}
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/interactions/composer.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,wBAAwB,CAAC;AAoChC,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,2BAA2B,GAAG,SAAS,CAEvG;AAED,wBAAgB,4CAA4C,CAC1D,YAAY,EAAE,MAAM,GACnB,2BAA2B,GAAG,SAAS,CAEzC;AAED,wBAAgB,2CAA2C,CACzD,YAAY,EAAE,MAAM,GACnB,0BAA0B,GAAG,SAAS,CAExC;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS,CAErG;AAED,wBAAgB,+BAA+B,IAAI,aAAa,CAAC,0BAA0B,CAAC,CAE3F;AAED,wBAAgB,0BAA0B,IAAI,aAAa,CAAC,qBAAqB,CAAC,CAEjF;AAED,wBAAgB,8BAA8B,IAAI,aAAa,CAC7D,WAAW,CAAC,qBAAqB,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,CAC9D,CAEA"}
@@ -1,6 +1,7 @@
1
1
  import { associateInteractionDescriptor } from '@qti-editor/interaction-associate';
2
2
  import { choiceInteractionDescriptor } from '@qti-editor/interaction-choice';
3
3
  import { extendedTextInteractionDescriptor } from '@qti-editor/interaction-extended-text';
4
+ import { gapMatchInteractionDescriptor } from '@qti-editor/interaction-gap-match';
4
5
  import { hottextInteractionDescriptor } from '@qti-editor/interaction-hottext';
5
6
  import { inlineChoiceInteractionDescriptor } from '@qti-editor/interaction-inline-choice';
6
7
  import { matchInteractionDescriptor } from '@qti-editor/interaction-match';
@@ -12,6 +13,7 @@ const registeredDescriptors = [
12
13
  associateInteractionDescriptor,
13
14
  choiceInteractionDescriptor,
14
15
  extendedTextInteractionDescriptor,
16
+ gapMatchInteractionDescriptor,
15
17
  hottextInteractionDescriptor,
16
18
  inlineChoiceInteractionDescriptor,
17
19
  matchInteractionDescriptor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qti-editor/core",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "QTI semantics, composer registry, and XML export orchestration for QTI Editor",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,18 +24,19 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@qti-editor/interfaces": "1.1.0",
28
- "@qti-editor/interaction-associate": "1.1.3",
29
- "@qti-editor/interaction-choice": "1.1.3",
30
- "@qti-editor/interaction-extended-text": "1.1.3",
31
- "@qti-editor/interaction-hottext": "1.1.3",
32
- "@qti-editor/interaction-inline-choice": "1.1.1",
33
- "@qti-editor/interaction-match": "1.1.1",
34
- "@qti-editor/interaction-order": "0.5.1",
35
- "@qti-editor/interaction-select-point": "1.1.1",
36
- "@qti-editor/interaction-shared": "1.2.0",
37
- "@qti-editor/interaction-text-entry": "1.1.2",
38
- "@qti-editor/qti-item-divider": "1.0.0"
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"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^20.0.0",