@qti-editor/core 1.1.0 → 1.2.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.
@@ -37,5 +37,15 @@ export interface ResponseDeclaration {
37
37
  }
38
38
  export declare function extractResponseDeclarations(itemBodyRoot?: Element | null): ResponseDeclaration[];
39
39
  export declare function buildAssessmentItemXml(itemContext?: ComposerItemContext): string;
40
+ /**
41
+ * Build multiple QTI assessment items from a single editor document.
42
+ *
43
+ * Splits the item body at qti-item-divider elements and generates
44
+ * a separate <qti-assessment-item> for each segment.
45
+ *
46
+ * @returns XML string containing multiple assessment items wrapped in a container,
47
+ * or a single assessment item if no dividers are found.
48
+ */
49
+ export declare function buildMultipleAssessmentItemsXml(itemContext?: ComposerItemContext): string;
40
50
  export declare function formatXml(xml: string): string;
41
51
  //# 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;AAED,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAqHhF;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;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"}
@@ -40,6 +40,51 @@ export function extractResponseDeclarations(itemBodyRoot) {
40
40
  const { declarations } = composeAndNormalizeItemBody(tempRoot, tempDoc);
41
41
  return declarations;
42
42
  }
43
+ /**
44
+ * Split an item body document at qti-item-divider elements.
45
+ * Returns an array of item body fragments, one for each item.
46
+ */
47
+ function splitItemBodyAtDividers(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 [];
54
+ // Find all dividers
55
+ const dividers = Array.from(itemBodyRoot.querySelectorAll('qti-item-divider'));
56
+ // If no dividers, return the whole body as a single item
57
+ if (dividers.length === 0) {
58
+ return [itemBodyRoot];
59
+ }
60
+ const fragments = [];
61
+ const children = Array.from(itemBodyRoot.childNodes);
62
+ let currentFragment = [];
63
+ for (const child of children) {
64
+ // Check if this child is a divider
65
+ if (child.nodeType === Node.ELEMENT_NODE &&
66
+ child.tagName.toLowerCase() === 'qti-item-divider') {
67
+ // Save the current fragment if it has content
68
+ if (currentFragment.length > 0) {
69
+ const fragmentBody = itemBodyDoc.createElementNS(QTI_NS, 'qti-item-body');
70
+ currentFragment.forEach(node => fragmentBody.appendChild(node.cloneNode(true)));
71
+ fragments.push(fragmentBody);
72
+ currentFragment = [];
73
+ }
74
+ // Skip the divider itself - it doesn't go into any fragment
75
+ }
76
+ else {
77
+ currentFragment.push(child);
78
+ }
79
+ }
80
+ // Add the last fragment if it has content
81
+ if (currentFragment.length > 0) {
82
+ const fragmentBody = itemBodyDoc.createElementNS(QTI_NS, 'qti-item-body');
83
+ currentFragment.forEach(node => fragmentBody.appendChild(node.cloneNode(true)));
84
+ fragments.push(fragmentBody);
85
+ }
86
+ return fragments;
87
+ }
43
88
  export function buildAssessmentItemXml(itemContext) {
44
89
  if (!itemContext)
45
90
  return '';
@@ -137,6 +182,44 @@ export function buildAssessmentItemXml(itemContext) {
137
182
  }
138
183
  return new XMLSerializer().serializeToString(xmlDoc);
139
184
  }
185
+ /**
186
+ * Build multiple QTI assessment items from a single editor document.
187
+ *
188
+ * Splits the item body at qti-item-divider elements and generates
189
+ * a separate <qti-assessment-item> for each segment.
190
+ *
191
+ * @returns XML string containing multiple assessment items wrapped in a container,
192
+ * or a single assessment item if no dividers are found.
193
+ */
194
+ export function buildMultipleAssessmentItemsXml(itemContext) {
195
+ if (!itemContext?.itemBody)
196
+ return '';
197
+ const fragments = splitItemBodyAtDividers(itemContext.itemBody);
198
+ // If only one fragment (no dividers), use the regular single-item builder
199
+ if (fragments.length <= 1) {
200
+ return buildAssessmentItemXml(itemContext);
201
+ }
202
+ // Build multiple items
203
+ const baseIdentifier = itemContext.identifier?.trim() || 'item';
204
+ const baseTitle = itemContext.title?.trim() || 'Untitled Item';
205
+ const lang = itemContext.lang?.trim() || 'en';
206
+ const itemXmls = fragments.map((fragmentBody, index) => {
207
+ const itemNumber = index + 1;
208
+ const fragmentDoc = document.implementation.createDocument(QTI_NS, 'qti-item-body', null);
209
+ const importedFragment = fragmentDoc.importNode(fragmentBody, true);
210
+ fragmentDoc.replaceChild(importedFragment, fragmentDoc.documentElement);
211
+ const fragmentContext = {
212
+ identifier: `${baseIdentifier}-${itemNumber}`,
213
+ title: `${baseTitle} ${itemNumber}`,
214
+ lang,
215
+ itemBody: fragmentDoc,
216
+ };
217
+ return buildAssessmentItemXml(fragmentContext);
218
+ });
219
+ // Join multiple items with a comment separator for clarity
220
+ const separator = '\n\n<!-- Next Assessment Item -->\n\n';
221
+ return itemXmls.join(separator);
222
+ }
140
223
  function composeAndNormalizeItemBody(itemBody, xmlDoc) {
141
224
  const declarations = [];
142
225
  const seenIdentifiers = new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/interactions/composer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,wBAAwB,CAAC;AAkChC,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":"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"}
@@ -7,6 +7,7 @@ import { matchInteractionDescriptor } from '@qti-editor/interaction-match';
7
7
  import { orderInteractionDescriptor } from '@qti-editor/interaction-order';
8
8
  import { selectPointInteractionDescriptor } from '@qti-editor/interaction-select-point';
9
9
  import { textEntryInteractionDescriptor } from '@qti-editor/interaction-text-entry';
10
+ import { qtiItemDividerDescriptor } from '@qti-editor/qti-item-divider';
10
11
  const registeredDescriptors = [
11
12
  associateInteractionDescriptor,
12
13
  choiceInteractionDescriptor,
@@ -17,6 +18,7 @@ const registeredDescriptors = [
17
18
  orderInteractionDescriptor,
18
19
  selectPointInteractionDescriptor,
19
20
  textEntryInteractionDescriptor,
21
+ qtiItemDividerDescriptor,
20
22
  ];
21
23
  const metadataByTagName = new Map(registeredDescriptors.map(d => [d.tagName, d.composerMetadata]));
22
24
  const metadataByNodeTypeName = new Map(registeredDescriptors.map(d => [d.nodeTypeName.toLowerCase(), d.composerMetadata]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qti-editor/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "QTI semantics, composer registry, and XML export orchestration for QTI Editor",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,16 +25,17 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@qti-editor/interfaces": "1.1.0",
28
- "@qti-editor/interaction-associate": "1.1.1",
29
- "@qti-editor/interaction-choice": "1.1.1",
30
- "@qti-editor/interaction-extended-text": "1.1.1",
31
- "@qti-editor/interaction-hottext": "1.1.1",
32
- "@qti-editor/interaction-inline-choice": "1.1.0",
33
- "@qti-editor/interaction-match": "1.1.0",
34
- "@qti-editor/interaction-order": "0.5.0",
35
- "@qti-editor/interaction-select-point": "1.1.0",
36
- "@qti-editor/interaction-shared": "1.1.0",
37
- "@qti-editor/interaction-text-entry": "1.1.1"
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"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20.0.0",