@longsightgroup/qti3-migrator 0.9.7 → 0.9.8

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.
Files changed (55) hide show
  1. package/README.md +39 -1
  2. package/dist/assets.d.ts +10 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +19 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/diagnostics.d.ts +2 -1
  7. package/dist/diagnostics.d.ts.map +1 -1
  8. package/dist/diagnostics.js +7 -0
  9. package/dist/diagnostics.js.map +1 -1
  10. package/dist/index.d.ts +4 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +11 -119
  13. package/dist/index.js.map +1 -1
  14. package/dist/item-finalize.d.ts +12 -0
  15. package/dist/item-finalize.d.ts.map +1 -0
  16. package/dist/item-finalize.js +93 -0
  17. package/dist/item-finalize.js.map +1 -0
  18. package/dist/item-hrefs.d.ts +2 -0
  19. package/dist/item-hrefs.d.ts.map +1 -0
  20. package/dist/item-hrefs.js +19 -0
  21. package/dist/item-hrefs.js.map +1 -0
  22. package/dist/migrate-items.d.ts +6 -0
  23. package/dist/migrate-items.d.ts.map +1 -0
  24. package/dist/migrate-items.js +31 -0
  25. package/dist/migrate-items.js.map +1 -0
  26. package/dist/package-result.d.ts +10 -1
  27. package/dist/package-result.d.ts.map +1 -1
  28. package/dist/package-result.js +32 -0
  29. package/dist/package-result.js.map +1 -1
  30. package/dist/paths.d.ts +4 -0
  31. package/dist/paths.d.ts.map +1 -0
  32. package/dist/paths.js +16 -0
  33. package/dist/paths.js.map +1 -0
  34. package/dist/resource-migration.d.ts +4 -0
  35. package/dist/resource-migration.d.ts.map +1 -0
  36. package/dist/resource-migration.js +128 -0
  37. package/dist/resource-migration.js.map +1 -0
  38. package/dist/source.d.ts +5 -0
  39. package/dist/source.d.ts.map +1 -1
  40. package/dist/source.js +26 -6
  41. package/dist/source.js.map +1 -1
  42. package/dist/types.d.ts +33 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +3 -3
  45. package/src/assets.ts +23 -0
  46. package/src/diagnostics.ts +12 -0
  47. package/src/index.ts +23 -172
  48. package/src/item-finalize.ts +133 -0
  49. package/src/item-hrefs.ts +23 -0
  50. package/src/migrate-items.ts +39 -0
  51. package/src/package-result.ts +46 -0
  52. package/src/paths.ts +33 -0
  53. package/src/resource-migration.ts +197 -0
  54. package/src/source.ts +40 -6
  55. package/src/types.ts +39 -0
package/src/index.ts CHANGED
@@ -1,42 +1,30 @@
1
- import {
2
- detectPackageMediaType,
3
- parseQtiXml,
4
- validateAssessmentItem,
5
- type QtiDiagnostic,
6
- } from "@longsightgroup/qti3-core";
7
- import {
8
- qti3TrustedXmlFragment,
9
- writeQti3AssessmentItemResult,
10
- type Qti3AuthoringItem,
11
- } from "@longsightgroup/qti3-writer";
12
- import { diagnostic, hasErrors } from "./diagnostics.js";
1
+ import { diagnostic } from "./diagnostics.js";
13
2
  import { migrationPathIdentifier } from "./identifiers.js";
3
+ import { finalizeItemResult, finalizeItemResults } from "./item-finalize.js";
4
+ import { migrateItemXml } from "./migrate-items.js";
5
+ import { migrationPackageAssets } from "./assets.js";
14
6
  import { resolveOptions } from "./options.js";
15
7
  import { migrationResultToPackage } from "./package-result.js";
16
- import { migrateQti12Xml } from "./qti12-item.js";
17
- import { itemTitleFromXml, migrateQti2ItemXml } from "./qti2-item.js";
8
+ import { migrateQtiResourceToQti3 } from "./resource-migration.js";
18
9
  import {
19
10
  detectMigrationSource,
20
11
  detectQtiMigrationSource,
21
12
  parseLegacyManifest,
22
13
  readMigrationSource,
23
- type MigrationEntry,
24
14
  type MigrationSource,
25
15
  } from "./source.js";
26
16
  import type {
27
- QtiMigrationAsset,
28
17
  QtiMigrationDetectionResult,
29
18
  QtiMigrationDiagnostic,
30
19
  QtiMigrationItemResult,
31
20
  QtiMigrationOptions,
32
- QtiPackageMigrationResult,
33
21
  QtiMigrationPart,
34
22
  QtiMigrationResult,
35
- QtiMigrationSourceFormat,
36
23
  QtiMigrationSourceInput,
24
+ QtiPackageMigrationResult,
37
25
  } from "./types.js";
38
26
 
39
- export { detectQtiMigrationSource };
27
+ export { detectQtiMigrationSource, migrateQtiResourceToQti3 };
40
28
  export type {
41
29
  QtiMigrationAsset,
42
30
  QtiMigrationDetectionResult,
@@ -46,11 +34,15 @@ export type {
46
34
  QtiMigrationOptions,
47
35
  QtiPackageMigrationResult,
48
36
  QtiMigrationPart,
37
+ QtiMigrationResourceEntry,
38
+ QtiMigrationResourceInput,
49
39
  QtiMigrationRepairPolicy,
50
40
  QtiMigrationResult,
51
41
  QtiMigrationSourceFormat,
52
42
  QtiMigrationSourceInput,
53
43
  QtiMigrationUnsupportedPolicy,
44
+ QtiResourceMigrationResult,
45
+ QtiResourceMigrationStatus,
54
46
  } from "./types.js";
55
47
 
56
48
  export async function migrateQtiToQti3(
@@ -219,10 +211,13 @@ function migratePackageSource(
219
211
  });
220
212
  continue;
221
213
  }
222
- const migratedItems = migrateItemXml(entry.text, href, sourceFormat, options);
214
+ const migratedItems = finalizeItemResults(
215
+ migrateItemXml(entry.text, href, sourceFormat, options),
216
+ href,
217
+ options,
218
+ );
223
219
  for (const item of migratedItems) {
224
- const finalized = finalizeItemResult(item, href, options);
225
- items.push({ ...finalized, assetHrefs: assetHrefsByItemHref.get(href) ?? [] });
220
+ items.push({ ...item, assetHrefs: assetHrefsByItemHref.get(href) ?? [] });
226
221
  }
227
222
  }
228
223
  return {
@@ -230,7 +225,7 @@ function migratePackageSource(
230
225
  sourceFormat,
231
226
  parts: [partFromItems("PART_1", manifest.title, items)],
232
227
  items,
233
- assets: packageAssets(source.entries),
228
+ assets: migrationPackageAssets(source.entries),
234
229
  diagnostics,
235
230
  };
236
231
  }
@@ -242,12 +237,12 @@ function migrateItemSource(
242
237
  ): QtiMigrationResult {
243
238
  const sourceFormat = detection.sourceFormat!;
244
239
  const xml = source.xml ?? "";
245
- const migratedItems = migrateItemXml(
246
- xml,
247
- source.filename ?? "item.xml",
248
- sourceFormat,
240
+ const href = source.filename ?? "item.xml";
241
+ const migratedItems = finalizeItemResults(
242
+ migrateItemXml(xml, href, sourceFormat, options),
243
+ href,
249
244
  options,
250
- ).map((item) => finalizeItemResult(item, source.filename ?? `${item.identifier}.xml`, options));
245
+ );
251
246
  return {
252
247
  title: migratedItems[0]?.title ?? source.filename ?? "Imported QTI Item",
253
248
  sourceFormat,
@@ -258,142 +253,6 @@ function migrateItemSource(
258
253
  };
259
254
  }
260
255
 
261
- function migrateItemXml(
262
- xml: string,
263
- path: string,
264
- sourceFormat: QtiMigrationSourceFormat,
265
- options: ReturnType<typeof resolveOptions>,
266
- ): readonly PendingMigrationItem[] {
267
- if (sourceFormat === "qti12") {
268
- return migrateQti12Xml(xml, path, options).map((result, index) => ({
269
- identifier: result.authoringItem?.identifier ?? `ITEM_${index + 1}`,
270
- title: result.authoringItem?.title ?? itemTitleFromXmlSafe(xml),
271
- authoringItem: result.authoringItem,
272
- diagnostics: result.diagnostics,
273
- }));
274
- }
275
- const result = migrateQti2ItemXml(xml, path, sourceFormat, options);
276
- return [
277
- {
278
- identifier: result.authoringItem?.identifier ?? migrationPathIdentifier(path),
279
- title: result.authoringItem?.title ?? itemTitleFromXmlSafe(xml),
280
- authoringItem: result.authoringItem,
281
- diagnostics: result.diagnostics,
282
- },
283
- ];
284
- }
285
-
286
- interface PendingMigrationItem {
287
- readonly identifier: string;
288
- readonly title: string;
289
- readonly authoringItem?: Qti3AuthoringItem | undefined;
290
- readonly diagnostics: readonly QtiMigrationDiagnostic[];
291
- }
292
-
293
- function finalizeItemResult(
294
- pending: PendingMigrationItem,
295
- href: string,
296
- options: ReturnType<typeof resolveOptions>,
297
- ): QtiMigrationItemResult {
298
- const diagnostics = [...pending.diagnostics];
299
- if (pending.authoringItem && !hasErrors(diagnostics)) {
300
- const writer = writeQti3AssessmentItemResult(pending.authoringItem);
301
- if (writer.ok) {
302
- diagnostics.push(...validateWrittenXml(writer.xml));
303
- return {
304
- identifier: pending.identifier,
305
- title: pending.title,
306
- href,
307
- authoringItem: pending.authoringItem,
308
- xml: writer.xml,
309
- diagnostics,
310
- };
311
- }
312
- diagnostics.push({
313
- code: "writer_diagnostics",
314
- severity: "error",
315
- message: "QTI 3 writer rejected migrated authoring item.",
316
- writerDiagnostics: writer.diagnostics,
317
- });
318
- }
319
-
320
- if (options.unsupportedPolicy === "stub") {
321
- const stub = stubItem(pending.identifier, pending.title);
322
- const writer = writeQti3AssessmentItemResult(stub);
323
- return {
324
- identifier: pending.identifier,
325
- title: pending.title,
326
- href,
327
- authoringItem: stub,
328
- xml: writer.ok ? writer.xml : undefined,
329
- diagnostics: [
330
- ...diagnostics,
331
- diagnostic(
332
- "unsupported_item_stubbed",
333
- "warning",
334
- "Unsupported item migrated as an extended-text review stub.",
335
- ),
336
- ],
337
- };
338
- }
339
-
340
- return {
341
- identifier: pending.identifier,
342
- title: pending.title,
343
- href,
344
- authoringItem: options.unsupportedPolicy === "skip" ? undefined : pending.authoringItem,
345
- diagnostics,
346
- };
347
- }
348
-
349
- function validateWrittenXml(xml: string): QtiMigrationDiagnostic[] {
350
- const parsed = parseQtiXml(xml);
351
- const diagnostics: QtiMigrationDiagnostic[] = [];
352
- if (!parsed.ok || !parsed.document) {
353
- diagnostics.push(...coreDiagnostics("core_parse", parsed.diagnostics));
354
- return diagnostics;
355
- }
356
- const validation = validateAssessmentItem(parsed.document);
357
- if (!validation.ok) diagnostics.push(...coreDiagnostics("core_validate", validation.diagnostics));
358
- return diagnostics;
359
- }
360
-
361
- function coreDiagnostics(
362
- prefix: string,
363
- diagnostics: readonly QtiDiagnostic[],
364
- ): QtiMigrationDiagnostic[] {
365
- return diagnostics.map((entry) =>
366
- diagnostic(
367
- `${prefix}.${entry.code}`,
368
- entry.severity === "error" ? "error" : "warning",
369
- entry.message,
370
- ),
371
- );
372
- }
373
-
374
- function stubItem(identifier: string, title: string): Qti3AuthoringItem {
375
- return {
376
- interactionType: "extendedText",
377
- identifier,
378
- title,
379
- bodyHtml: qti3TrustedXmlFragment("<p>This item requires manual migration review.</p>"),
380
- responseIdentifier: "RESPONSE",
381
- responseBaseType: "string",
382
- responseCardinality: "single",
383
- expectedLines: 6,
384
- };
385
- }
386
-
387
- function packageAssets(entries: readonly MigrationEntry[]): QtiMigrationAsset[] {
388
- return entries
389
- .filter((entry) => !entry.path.toLowerCase().endsWith(".xml"))
390
- .map((entry) => ({
391
- path: entry.path,
392
- data: entry.bytes,
393
- mediaType: detectPackageMediaType(entry.path),
394
- }));
395
- }
396
-
397
256
  function partFromItems(
398
257
  identifier: string,
399
258
  title: string,
@@ -401,11 +260,3 @@ function partFromItems(
401
260
  ): QtiMigrationPart {
402
261
  return { identifier, title, itemHrefs: items.map((item) => item.href) };
403
262
  }
404
-
405
- function itemTitleFromXmlSafe(xml: string): string {
406
- try {
407
- return itemTitleFromXml(xml);
408
- } catch {
409
- return "Imported Item";
410
- }
411
- }
@@ -0,0 +1,133 @@
1
+ import { parseQtiXml, validateAssessmentItem, type QtiDiagnostic } from "@longsightgroup/qti3-core";
2
+ import {
3
+ qti3TrustedXmlFragment,
4
+ writeQti3AssessmentItemResult,
5
+ type Qti3AuthoringItem,
6
+ } from "@longsightgroup/qti3-writer";
7
+
8
+ import { diagnostic, hasErrors } from "./diagnostics.js";
9
+ import { uniqueSiblingItemHref } from "./item-hrefs.js";
10
+ import type { QtiMigrationDiagnostic, QtiMigrationItemResult } from "./types.js";
11
+ import type { ResolvedQtiMigrationOptions } from "./types.js";
12
+
13
+ export interface PendingMigrationItem {
14
+ readonly identifier: string;
15
+ readonly title: string;
16
+ readonly authoringItem?: Qti3AuthoringItem | undefined;
17
+ readonly diagnostics: readonly QtiMigrationDiagnostic[];
18
+ }
19
+
20
+ export function finalizeItemResults(
21
+ pendingItems: readonly PendingMigrationItem[],
22
+ sourceHref: string,
23
+ options: ResolvedQtiMigrationOptions,
24
+ ): readonly QtiMigrationItemResult[] {
25
+ if (pendingItems.length <= 1) {
26
+ return pendingItems.map((item) => finalizeItemResult(item, sourceHref, options));
27
+ }
28
+
29
+ const identifiers = new Set<string>();
30
+ const paths = new Set<string>();
31
+ return pendingItems.map((item, index) => {
32
+ const identifier = item.identifier.trim();
33
+ const uniqueIdentifier = identifier && !identifiers.has(identifier);
34
+ if (uniqueIdentifier) identifiers.add(identifier);
35
+ const suffix = uniqueIdentifier ? identifier : `ITEM_${index + 1}`;
36
+ const href = uniqueSiblingItemHref(sourceHref, suffix, paths);
37
+ return finalizeItemResult(item, href, options);
38
+ });
39
+ }
40
+
41
+ export function finalizeItemResult(
42
+ pending: PendingMigrationItem,
43
+ href: string,
44
+ options: ResolvedQtiMigrationOptions,
45
+ ): QtiMigrationItemResult {
46
+ const diagnostics = [...pending.diagnostics];
47
+ if (pending.authoringItem && !hasErrors(diagnostics)) {
48
+ const writer = writeQti3AssessmentItemResult(pending.authoringItem);
49
+ if (writer.ok) {
50
+ diagnostics.push(...validateWrittenXml(writer.xml));
51
+ return {
52
+ identifier: pending.identifier,
53
+ title: pending.title,
54
+ href,
55
+ authoringItem: pending.authoringItem,
56
+ xml: writer.xml,
57
+ diagnostics,
58
+ };
59
+ }
60
+ diagnostics.push({
61
+ code: "writer_diagnostics",
62
+ severity: "error",
63
+ message: "QTI 3 writer rejected migrated authoring item.",
64
+ writerDiagnostics: writer.diagnostics,
65
+ });
66
+ }
67
+
68
+ if (options.unsupportedPolicy === "stub") {
69
+ const stub = stubItem(pending.identifier, pending.title);
70
+ const writer = writeQti3AssessmentItemResult(stub);
71
+ return {
72
+ identifier: pending.identifier,
73
+ title: pending.title,
74
+ href,
75
+ authoringItem: stub,
76
+ xml: writer.ok ? writer.xml : undefined,
77
+ diagnostics: [
78
+ ...diagnostics,
79
+ diagnostic(
80
+ "unsupported_item_stubbed",
81
+ "warning",
82
+ "Unsupported item migrated as an extended-text review stub.",
83
+ ),
84
+ ],
85
+ };
86
+ }
87
+
88
+ return {
89
+ identifier: pending.identifier,
90
+ title: pending.title,
91
+ href,
92
+ authoringItem: options.unsupportedPolicy === "skip" ? undefined : pending.authoringItem,
93
+ diagnostics,
94
+ };
95
+ }
96
+
97
+ function validateWrittenXml(xml: string): QtiMigrationDiagnostic[] {
98
+ const parsed = parseQtiXml(xml);
99
+ const diagnostics: QtiMigrationDiagnostic[] = [];
100
+ if (!parsed.ok || !parsed.document) {
101
+ diagnostics.push(...coreDiagnostics("core_parse", parsed.diagnostics));
102
+ return diagnostics;
103
+ }
104
+ const validation = validateAssessmentItem(parsed.document);
105
+ if (!validation.ok) diagnostics.push(...coreDiagnostics("core_validate", validation.diagnostics));
106
+ return diagnostics;
107
+ }
108
+
109
+ function coreDiagnostics(
110
+ prefix: string,
111
+ diagnostics: readonly QtiDiagnostic[],
112
+ ): QtiMigrationDiagnostic[] {
113
+ return diagnostics.map((entry) =>
114
+ diagnostic(
115
+ `${prefix}.${entry.code}`,
116
+ entry.severity === "error" ? "error" : "warning",
117
+ entry.message,
118
+ ),
119
+ );
120
+ }
121
+
122
+ function stubItem(identifier: string, title: string): Qti3AuthoringItem {
123
+ return {
124
+ interactionType: "extendedText",
125
+ identifier,
126
+ title,
127
+ bodyHtml: qti3TrustedXmlFragment("<p>This item requires manual migration review.</p>"),
128
+ responseIdentifier: "RESPONSE",
129
+ responseBaseType: "string",
130
+ responseCardinality: "single",
131
+ expectedLines: 6,
132
+ };
133
+ }
@@ -0,0 +1,23 @@
1
+ import { migrationPathIdentifier } from "./identifiers.js";
2
+
3
+ export function uniqueSiblingItemHref(
4
+ sourceHref: string,
5
+ suffix: string,
6
+ usedPaths: Set<string>,
7
+ ): string {
8
+ const slashIndex = sourceHref.lastIndexOf("/");
9
+ const directory = slashIndex >= 0 ? sourceHref.slice(0, slashIndex + 1) : "";
10
+ const fileName = slashIndex >= 0 ? sourceHref.slice(slashIndex + 1) : sourceHref;
11
+ const dotIndex = fileName.toLowerCase().endsWith(".xml")
12
+ ? fileName.length - 4
13
+ : fileName.lastIndexOf(".");
14
+ const baseName = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName || "item";
15
+ const extension = dotIndex > 0 ? fileName.slice(dotIndex) : ".xml";
16
+ const safeSuffix = migrationPathIdentifier(suffix);
17
+ let candidate = `${directory}${baseName}_${safeSuffix}${extension}`;
18
+ for (let index = 2; usedPaths.has(candidate); index += 1) {
19
+ candidate = `${directory}${baseName}_${safeSuffix}_${index}${extension}`;
20
+ }
21
+ usedPaths.add(candidate);
22
+ return candidate;
23
+ }
@@ -0,0 +1,39 @@
1
+ import { migrationPathIdentifier } from "./identifiers.js";
2
+ import type { PendingMigrationItem } from "./item-finalize.js";
3
+ import { migrateQti12Xml } from "./qti12-item.js";
4
+ import { itemTitleFromXml, migrateQti2ItemXml } from "./qti2-item.js";
5
+ import type { QtiMigrationSourceFormat } from "./types.js";
6
+ import type { ResolvedQtiMigrationOptions } from "./types.js";
7
+
8
+ export function migrateItemXml(
9
+ xml: string,
10
+ path: string,
11
+ sourceFormat: QtiMigrationSourceFormat,
12
+ options: ResolvedQtiMigrationOptions,
13
+ ): readonly PendingMigrationItem[] {
14
+ if (sourceFormat === "qti12") {
15
+ return migrateQti12Xml(xml, path, options).map((result, index) => ({
16
+ identifier: result.authoringItem?.identifier ?? `ITEM_${index + 1}`,
17
+ title: result.authoringItem?.title ?? itemTitleFromXmlSafe(xml),
18
+ authoringItem: result.authoringItem,
19
+ diagnostics: result.diagnostics,
20
+ }));
21
+ }
22
+ const result = migrateQti2ItemXml(xml, path, sourceFormat, options);
23
+ return [
24
+ {
25
+ identifier: result.authoringItem?.identifier ?? migrationPathIdentifier(path),
26
+ title: result.authoringItem?.title ?? itemTitleFromXmlSafe(xml),
27
+ authoringItem: result.authoringItem,
28
+ diagnostics: result.diagnostics,
29
+ },
30
+ ];
31
+ }
32
+
33
+ export function itemTitleFromXmlSafe(xml: string): string {
34
+ try {
35
+ return itemTitleFromXml(xml);
36
+ } catch {
37
+ return "Imported Item";
38
+ }
39
+ }
@@ -1,14 +1,25 @@
1
+ import { detectPackageMediaType } from "@longsightgroup/qti3-core";
1
2
  import type { Qti3PackageAsset, Qti3PackageItem } from "@longsightgroup/qti3-writer";
3
+ import { writeQti3PackageFilesResult } from "@longsightgroup/qti3-writer";
2
4
 
3
5
  import { diagnostic, hasErrors } from "./diagnostics.js";
4
6
  import { migrationPackageIdentifier } from "./identifiers.js";
5
7
  import type {
6
8
  QtiMigrationAsset,
7
9
  QtiMigrationDiagnostic,
10
+ QtiMigrationResourceEntry,
8
11
  QtiMigrationResult,
9
12
  QtiPackageMigrationResult,
10
13
  } from "./types.js";
11
14
 
15
+ export type MigrationPackageEntriesResult =
16
+ | {
17
+ readonly ok: true;
18
+ readonly entries: readonly QtiMigrationResourceEntry[];
19
+ readonly diagnostics: readonly QtiMigrationDiagnostic[];
20
+ }
21
+ | { readonly ok: false; readonly diagnostics: readonly QtiMigrationDiagnostic[] };
22
+
12
23
  export function migrationResultToPackage(result: QtiMigrationResult): QtiPackageMigrationResult {
13
24
  const itemDiagnostics = result.items.flatMap((item) => item.diagnostics);
14
25
  const assetDiagnostics: QtiMigrationDiagnostic[] = [];
@@ -34,6 +45,41 @@ export function migrationResultToPackage(result: QtiMigrationResult): QtiPackage
34
45
  };
35
46
  }
36
47
 
48
+ export function migrationResultToPackageEntries(
49
+ result: QtiMigrationResult,
50
+ ): MigrationPackageEntriesResult {
51
+ const packageResult = migrationResultToPackage(result);
52
+ if (!packageResult.ok) {
53
+ return { ok: false, diagnostics: packageResult.diagnostics };
54
+ }
55
+
56
+ const files = writeQti3PackageFilesResult(packageResult.package);
57
+ if (!files.ok) {
58
+ return {
59
+ ok: false,
60
+ diagnostics: [
61
+ ...packageResult.diagnostics,
62
+ {
63
+ code: "writer_diagnostics",
64
+ severity: "error",
65
+ message: "QTI 3 writer rejected migrated package files.",
66
+ writerDiagnostics: files.diagnostics,
67
+ },
68
+ ],
69
+ };
70
+ }
71
+
72
+ return {
73
+ ok: true,
74
+ entries: files.files.map((file) => ({
75
+ path: file.path,
76
+ data: file.data,
77
+ mediaType: detectPackageMediaType(file.path),
78
+ })),
79
+ diagnostics: packageResult.diagnostics,
80
+ };
81
+ }
82
+
37
83
  function buildPackageItems(
38
84
  result: QtiMigrationResult,
39
85
  assetsByPath: ReadonlyMap<string, Qti3PackageAsset>,
package/src/paths.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { normalizePackagePath, type QtiDiagnostic } from "@longsightgroup/qti3-core";
2
+
3
+ import { diagnostic } from "./diagnostics.js";
4
+ import type { QtiMigrationDiagnostic } from "./types.js";
5
+
6
+ /** Normalize a host resource path and map core package-path diagnostics into migrator codes. */
7
+ export function normalizeMigrationResourcePath(
8
+ path: string,
9
+ context: string,
10
+ diagnostics: QtiMigrationDiagnostic[],
11
+ ): string | undefined {
12
+ const coreDiagnostics: QtiDiagnostic[] = [];
13
+ const normalized = normalizePackagePath(path.replaceAll("\\", "/"), context, coreDiagnostics);
14
+ diagnostics.push(
15
+ ...coreDiagnostics.map((entry) =>
16
+ diagnostic(
17
+ `resource_${entry.code}`,
18
+ entry.severity === "error" ? "error" : "warning",
19
+ entry.message,
20
+ {
21
+ path: entry.path,
22
+ },
23
+ ),
24
+ ),
25
+ );
26
+ if (normalized === "") {
27
+ diagnostics.push(
28
+ diagnostic("resource_path_empty", "error", `${context} must not be empty.`, { path }),
29
+ );
30
+ return undefined;
31
+ }
32
+ return normalized;
33
+ }