@qti-editor/core 1.4.0 → 1.4.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.
@@ -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;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"}
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;AAoKD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B7C"}
@@ -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?.trim() || 'item-1');
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?.trim() || 'item';
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: perItem?.identifier?.trim() || `${baseIdentifier}-${itemNumber}`,
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?.trim() || itemContext.identifier?.trim() || 'item-1';
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?.trim() || 'item';
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qti-editor/core",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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.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"
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",