@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.
- package/README.md +39 -1
- package/dist/assets.d.ts +10 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +19 -0
- package/dist/assets.js.map +1 -0
- package/dist/diagnostics.d.ts +2 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +7 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -119
- package/dist/index.js.map +1 -1
- package/dist/item-finalize.d.ts +12 -0
- package/dist/item-finalize.d.ts.map +1 -0
- package/dist/item-finalize.js +93 -0
- package/dist/item-finalize.js.map +1 -0
- package/dist/item-hrefs.d.ts +2 -0
- package/dist/item-hrefs.d.ts.map +1 -0
- package/dist/item-hrefs.js +19 -0
- package/dist/item-hrefs.js.map +1 -0
- package/dist/migrate-items.d.ts +6 -0
- package/dist/migrate-items.d.ts.map +1 -0
- package/dist/migrate-items.js +31 -0
- package/dist/migrate-items.js.map +1 -0
- package/dist/package-result.d.ts +10 -1
- package/dist/package-result.d.ts.map +1 -1
- package/dist/package-result.js +32 -0
- package/dist/package-result.js.map +1 -1
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +16 -0
- package/dist/paths.js.map +1 -0
- package/dist/resource-migration.d.ts +4 -0
- package/dist/resource-migration.d.ts.map +1 -0
- package/dist/resource-migration.js +128 -0
- package/dist/resource-migration.js.map +1 -0
- package/dist/source.d.ts +5 -0
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +26 -6
- package/dist/source.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/assets.ts +23 -0
- package/src/diagnostics.ts +12 -0
- package/src/index.ts +23 -172
- package/src/item-finalize.ts +133 -0
- package/src/item-hrefs.ts +23 -0
- package/src/migrate-items.ts +39 -0
- package/src/package-result.ts +46 -0
- package/src/paths.ts +33 -0
- package/src/resource-migration.ts +197 -0
- package/src/source.ts +40 -6
- 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 {
|
|
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 =
|
|
214
|
+
const migratedItems = finalizeItemResults(
|
|
215
|
+
migrateItemXml(entry.text, href, sourceFormat, options),
|
|
216
|
+
href,
|
|
217
|
+
options,
|
|
218
|
+
);
|
|
223
219
|
for (const item of migratedItems) {
|
|
224
|
-
|
|
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:
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
const href = source.filename ?? "item.xml";
|
|
241
|
+
const migratedItems = finalizeItemResults(
|
|
242
|
+
migrateItemXml(xml, href, sourceFormat, options),
|
|
243
|
+
href,
|
|
249
244
|
options,
|
|
250
|
-
)
|
|
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
|
+
}
|
package/src/package-result.ts
CHANGED
|
@@ -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
|
+
}
|