@conform-ed/qti-xml 0.0.19 → 0.0.21
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/dist/cc-qti/convert-to-v3.d.ts +46 -0
- package/dist/cc-qti/index.d.ts +2 -0
- package/dist/cc-qti/normalize-questestinterop.d.ts +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +805 -0
- package/package.json +2 -2
- package/src/cc-qti/convert-to-v3.ts +460 -0
- package/src/cc-qti/index.ts +2 -0
- package/src/cc-qti/normalize-questestinterop.ts +544 -0
- package/src/index.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -5384,6 +5384,808 @@ function serializeQtiDocument(version, key, document) {
|
|
|
5384
5384
|
throw new Error(`Serialization is not implemented for ${version} ${String(key)}.`);
|
|
5385
5385
|
}
|
|
5386
5386
|
}
|
|
5387
|
+
// src/cc-qti/normalize-questestinterop.ts
|
|
5388
|
+
import {
|
|
5389
|
+
QtiQuestestinteropProfileDocumentSchema,
|
|
5390
|
+
QtiQuestestinteropRawDocumentSchema
|
|
5391
|
+
} from "@conform-ed/contracts/common-cartridge/v1_4";
|
|
5392
|
+
var ccQtiNamespace = "http://www.imsglobal.org/xsd/ims_qtiasiv1p2";
|
|
5393
|
+
function elements(node, localName) {
|
|
5394
|
+
return node.children.filter((child) => child.type === "element" && (localName === undefined || child.localName === localName));
|
|
5395
|
+
}
|
|
5396
|
+
function firstElement(node, localName) {
|
|
5397
|
+
return elements(node, localName)[0];
|
|
5398
|
+
}
|
|
5399
|
+
function attr2(node, name) {
|
|
5400
|
+
const value = node.attributes[name];
|
|
5401
|
+
return value === undefined ? undefined : value;
|
|
5402
|
+
}
|
|
5403
|
+
function textOf(node) {
|
|
5404
|
+
const parts = [];
|
|
5405
|
+
const walk = (children) => {
|
|
5406
|
+
for (const child of children) {
|
|
5407
|
+
if (child.type === "text") {
|
|
5408
|
+
parts.push(child.value);
|
|
5409
|
+
} else {
|
|
5410
|
+
walk(child.children);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
};
|
|
5414
|
+
walk(node.children);
|
|
5415
|
+
return parts.join("").trim();
|
|
5416
|
+
}
|
|
5417
|
+
function put(target, key, value) {
|
|
5418
|
+
if (value !== undefined) {
|
|
5419
|
+
target[key] = value;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
function mapMatText(node) {
|
|
5423
|
+
const out = { kind: "mattext", value: textOf(node) };
|
|
5424
|
+
put(out, "texttype", attr2(node, "texttype"));
|
|
5425
|
+
put(out, "charset", attr2(node, "charset"));
|
|
5426
|
+
put(out, "label", attr2(node, "label"));
|
|
5427
|
+
put(out, "uri", attr2(node, "uri"));
|
|
5428
|
+
put(out, "width", attr2(node, "width"));
|
|
5429
|
+
put(out, "height", attr2(node, "height"));
|
|
5430
|
+
put(out, "x0", attr2(node, "x0"));
|
|
5431
|
+
put(out, "y0", attr2(node, "y0"));
|
|
5432
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5433
|
+
put(out, "xmlSpace", attr2(node, "xml:space"));
|
|
5434
|
+
return out;
|
|
5435
|
+
}
|
|
5436
|
+
function mapMaterialChild(node) {
|
|
5437
|
+
switch (node.localName) {
|
|
5438
|
+
case "mattext":
|
|
5439
|
+
return mapMatText(node);
|
|
5440
|
+
case "matref":
|
|
5441
|
+
return { kind: "matref", linkrefid: attr2(node, "linkrefid") ?? "" };
|
|
5442
|
+
case "matbreak":
|
|
5443
|
+
return { kind: "matbreak" };
|
|
5444
|
+
default:
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
function mapAltMaterial(node) {
|
|
5449
|
+
const out = {
|
|
5450
|
+
kind: "altmaterial",
|
|
5451
|
+
children: elements(node).map(mapMaterialChild).filter((child) => child !== undefined)
|
|
5452
|
+
};
|
|
5453
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5454
|
+
return out;
|
|
5455
|
+
}
|
|
5456
|
+
function mapMaterial(node) {
|
|
5457
|
+
const out = {
|
|
5458
|
+
kind: "material",
|
|
5459
|
+
children: elements(node).filter((child) => child.localName !== "altmaterial").map(mapMaterialChild).filter((child) => child !== undefined)
|
|
5460
|
+
};
|
|
5461
|
+
put(out, "label", attr2(node, "label"));
|
|
5462
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5463
|
+
const altmaterial = elements(node, "altmaterial");
|
|
5464
|
+
if (altmaterial.length > 0) {
|
|
5465
|
+
out["altmaterial"] = altmaterial.map(mapAltMaterial);
|
|
5466
|
+
}
|
|
5467
|
+
return out;
|
|
5468
|
+
}
|
|
5469
|
+
function mapMaterialRef(node) {
|
|
5470
|
+
return { kind: "material_ref", linkrefid: attr2(node, "linkrefid") ?? "" };
|
|
5471
|
+
}
|
|
5472
|
+
function mapFlowMat(node) {
|
|
5473
|
+
const out = {
|
|
5474
|
+
kind: "flow_mat",
|
|
5475
|
+
children: elements(node).map((child) => {
|
|
5476
|
+
if (child.localName === "flow_mat")
|
|
5477
|
+
return mapFlowMat(child);
|
|
5478
|
+
if (child.localName === "material")
|
|
5479
|
+
return mapMaterial(child);
|
|
5480
|
+
if (child.localName === "material_ref")
|
|
5481
|
+
return mapMaterialRef(child);
|
|
5482
|
+
return;
|
|
5483
|
+
}).filter((child) => child !== undefined)
|
|
5484
|
+
};
|
|
5485
|
+
put(out, "class", attr2(node, "class"));
|
|
5486
|
+
return out;
|
|
5487
|
+
}
|
|
5488
|
+
function mapResponseLabel(node) {
|
|
5489
|
+
const out = { kind: "response_label", ident: attr2(node, "ident") ?? "" };
|
|
5490
|
+
put(out, "labelrefid", attr2(node, "labelrefid"));
|
|
5491
|
+
put(out, "rshuffle", attr2(node, "rshuffle"));
|
|
5492
|
+
put(out, "match_group", attr2(node, "match_group"));
|
|
5493
|
+
put(out, "match_max", attr2(node, "match_max"));
|
|
5494
|
+
const children = elements(node).map((child) => {
|
|
5495
|
+
if (child.localName === "material")
|
|
5496
|
+
return mapMaterial(child);
|
|
5497
|
+
if (child.localName === "material_ref")
|
|
5498
|
+
return mapMaterialRef(child);
|
|
5499
|
+
if (child.localName === "flow_mat")
|
|
5500
|
+
return mapFlowMat(child);
|
|
5501
|
+
return;
|
|
5502
|
+
}).filter((child) => child !== undefined);
|
|
5503
|
+
if (children.length > 0) {
|
|
5504
|
+
out["children"] = children;
|
|
5505
|
+
}
|
|
5506
|
+
return out;
|
|
5507
|
+
}
|
|
5508
|
+
function mapFlowLabel(node) {
|
|
5509
|
+
const out = {
|
|
5510
|
+
kind: "flow_label",
|
|
5511
|
+
children: elements(node).map((child) => {
|
|
5512
|
+
if (child.localName === "flow_label")
|
|
5513
|
+
return mapFlowLabel(child);
|
|
5514
|
+
if (child.localName === "response_label")
|
|
5515
|
+
return mapResponseLabel(child);
|
|
5516
|
+
return;
|
|
5517
|
+
}).filter((child) => child !== undefined)
|
|
5518
|
+
};
|
|
5519
|
+
put(out, "class", attr2(node, "class"));
|
|
5520
|
+
return out;
|
|
5521
|
+
}
|
|
5522
|
+
function mapRenderChildren(node) {
|
|
5523
|
+
return elements(node).map((child) => {
|
|
5524
|
+
if (child.localName === "material")
|
|
5525
|
+
return mapMaterial(child);
|
|
5526
|
+
if (child.localName === "material_ref")
|
|
5527
|
+
return mapMaterialRef(child);
|
|
5528
|
+
if (child.localName === "response_label")
|
|
5529
|
+
return mapResponseLabel(child);
|
|
5530
|
+
if (child.localName === "flow_label")
|
|
5531
|
+
return mapFlowLabel(child);
|
|
5532
|
+
return;
|
|
5533
|
+
}).filter((child) => child !== undefined);
|
|
5534
|
+
}
|
|
5535
|
+
function mapRender(node) {
|
|
5536
|
+
if (node.localName === "render_fib") {
|
|
5537
|
+
const out2 = { kind: "render_fib" };
|
|
5538
|
+
put(out2, "encoding", attr2(node, "encoding"));
|
|
5539
|
+
put(out2, "charset", attr2(node, "charset"));
|
|
5540
|
+
put(out2, "rows", attr2(node, "rows"));
|
|
5541
|
+
put(out2, "columns", attr2(node, "columns"));
|
|
5542
|
+
put(out2, "maxchars", attr2(node, "maxchars"));
|
|
5543
|
+
put(out2, "minnumber", attr2(node, "minnumber"));
|
|
5544
|
+
put(out2, "maxnumber", attr2(node, "maxnumber"));
|
|
5545
|
+
put(out2, "prompt", attr2(node, "prompt"));
|
|
5546
|
+
put(out2, "fibtype", attr2(node, "fibtype"));
|
|
5547
|
+
const children2 = mapRenderChildren(node);
|
|
5548
|
+
if (children2.length > 0)
|
|
5549
|
+
out2["children"] = children2;
|
|
5550
|
+
return out2;
|
|
5551
|
+
}
|
|
5552
|
+
const out = { kind: "render_choice" };
|
|
5553
|
+
put(out, "shuffle", attr2(node, "shuffle"));
|
|
5554
|
+
put(out, "minnumber", attr2(node, "minnumber"));
|
|
5555
|
+
put(out, "maxnumber", attr2(node, "maxnumber"));
|
|
5556
|
+
const children = mapRenderChildren(node);
|
|
5557
|
+
if (children.length > 0)
|
|
5558
|
+
out["children"] = children;
|
|
5559
|
+
return out;
|
|
5560
|
+
}
|
|
5561
|
+
function mapLeadingTrailing(node) {
|
|
5562
|
+
if (node.localName === "material")
|
|
5563
|
+
return mapMaterial(node);
|
|
5564
|
+
if (node.localName === "material_ref")
|
|
5565
|
+
return mapMaterialRef(node);
|
|
5566
|
+
return;
|
|
5567
|
+
}
|
|
5568
|
+
function mapResponse(node) {
|
|
5569
|
+
const kind = node.localName === "response_str" ? "response_str" : "response_lid";
|
|
5570
|
+
const out = { kind, ident: attr2(node, "ident") ?? "" };
|
|
5571
|
+
put(out, "rcardinality", attr2(node, "rcardinality"));
|
|
5572
|
+
put(out, "rtiming", attr2(node, "rtiming"));
|
|
5573
|
+
const renderNode = elements(node).find((child) => child.localName.startsWith("render_"));
|
|
5574
|
+
out["render"] = renderNode ? mapRender(renderNode) : { kind: "render_choice" };
|
|
5575
|
+
const childEls = elements(node);
|
|
5576
|
+
const leadingIndex = childEls.findIndex((c) => c.localName === "material" || c.localName === "material_ref");
|
|
5577
|
+
const renderIndex = childEls.findIndex((c) => c.localName.startsWith("render_"));
|
|
5578
|
+
if (leadingIndex !== -1 && (renderIndex === -1 || leadingIndex < renderIndex)) {
|
|
5579
|
+
const leading = mapLeadingTrailing(childEls[leadingIndex]);
|
|
5580
|
+
if (leading)
|
|
5581
|
+
out["leading"] = leading;
|
|
5582
|
+
}
|
|
5583
|
+
const trailingEls = childEls.filter((c, i) => (c.localName === "material" || c.localName === "material_ref") && i > renderIndex && renderIndex !== -1);
|
|
5584
|
+
if (trailingEls.length > 0) {
|
|
5585
|
+
const trailing = mapLeadingTrailing(trailingEls[0]);
|
|
5586
|
+
if (trailing)
|
|
5587
|
+
out["trailing"] = trailing;
|
|
5588
|
+
}
|
|
5589
|
+
return out;
|
|
5590
|
+
}
|
|
5591
|
+
function mapFlow(node) {
|
|
5592
|
+
const out = {
|
|
5593
|
+
kind: "flow",
|
|
5594
|
+
children: elements(node).map((child) => {
|
|
5595
|
+
if (child.localName === "flow")
|
|
5596
|
+
return mapFlow(child);
|
|
5597
|
+
if (child.localName === "material")
|
|
5598
|
+
return mapMaterial(child);
|
|
5599
|
+
if (child.localName === "material_ref")
|
|
5600
|
+
return mapMaterialRef(child);
|
|
5601
|
+
if (child.localName === "response_lid" || child.localName === "response_str")
|
|
5602
|
+
return mapResponse(child);
|
|
5603
|
+
return;
|
|
5604
|
+
}).filter((child) => child !== undefined)
|
|
5605
|
+
};
|
|
5606
|
+
put(out, "class", attr2(node, "class"));
|
|
5607
|
+
return out;
|
|
5608
|
+
}
|
|
5609
|
+
function mapPresentation(node) {
|
|
5610
|
+
const flowNode = firstElement(node, "flow");
|
|
5611
|
+
const common = {};
|
|
5612
|
+
put(common, "label", attr2(node, "label"));
|
|
5613
|
+
put(common, "xmlLang", attr2(node, "xml:lang"));
|
|
5614
|
+
put(common, "x0", attr2(node, "x0"));
|
|
5615
|
+
put(common, "y0", attr2(node, "y0"));
|
|
5616
|
+
put(common, "width", attr2(node, "width"));
|
|
5617
|
+
put(common, "height", attr2(node, "height"));
|
|
5618
|
+
if (flowNode) {
|
|
5619
|
+
return { flow: mapFlow(flowNode), ...common };
|
|
5620
|
+
}
|
|
5621
|
+
return {
|
|
5622
|
+
children: elements(node).map((child) => {
|
|
5623
|
+
if (child.localName === "material")
|
|
5624
|
+
return mapMaterial(child);
|
|
5625
|
+
if (child.localName === "response_lid" || child.localName === "response_str")
|
|
5626
|
+
return mapResponse(child);
|
|
5627
|
+
return;
|
|
5628
|
+
}).filter((child) => child !== undefined),
|
|
5629
|
+
...common
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5632
|
+
function mapConditionNode(node) {
|
|
5633
|
+
switch (node.localName) {
|
|
5634
|
+
case "varequal":
|
|
5635
|
+
return {
|
|
5636
|
+
kind: "varequal",
|
|
5637
|
+
value: textOf(node),
|
|
5638
|
+
respident: attr2(node, "respident") ?? "",
|
|
5639
|
+
...attr2(node, "case") !== undefined ? { case: attr2(node, "case") } : {}
|
|
5640
|
+
};
|
|
5641
|
+
case "varsubstring":
|
|
5642
|
+
return {
|
|
5643
|
+
kind: "varsubstring",
|
|
5644
|
+
value: textOf(node),
|
|
5645
|
+
respident: attr2(node, "respident") ?? "",
|
|
5646
|
+
...attr2(node, "case") !== undefined ? { case: attr2(node, "case") } : {}
|
|
5647
|
+
};
|
|
5648
|
+
case "and":
|
|
5649
|
+
return { kind: "and", tests: mapConditionTests(node) };
|
|
5650
|
+
case "or":
|
|
5651
|
+
return { kind: "or", tests: mapConditionTests(node) };
|
|
5652
|
+
case "not":
|
|
5653
|
+
return { kind: "not", tests: mapConditionTests(node) };
|
|
5654
|
+
case "other":
|
|
5655
|
+
return { kind: "other" };
|
|
5656
|
+
default:
|
|
5657
|
+
return;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
function mapConditionTests(node) {
|
|
5661
|
+
return elements(node).map(mapConditionNode).filter((child) => child !== undefined);
|
|
5662
|
+
}
|
|
5663
|
+
function mapRespCondition(node) {
|
|
5664
|
+
const conditionvar = firstElement(node, "conditionvar");
|
|
5665
|
+
const out = {
|
|
5666
|
+
conditionvar: { tests: conditionvar ? mapConditionTests(conditionvar) : [] }
|
|
5667
|
+
};
|
|
5668
|
+
put(out, "title", attr2(node, "title"));
|
|
5669
|
+
put(out, "continue", attr2(node, "continue"));
|
|
5670
|
+
const setvars = elements(node, "setvar").map((sv) => {
|
|
5671
|
+
const entry = { value: textOf(sv) };
|
|
5672
|
+
put(entry, "varname", attr2(sv, "varname"));
|
|
5673
|
+
put(entry, "action", attr2(sv, "action"));
|
|
5674
|
+
return entry;
|
|
5675
|
+
});
|
|
5676
|
+
if (setvars.length > 0)
|
|
5677
|
+
out["setvar"] = setvars;
|
|
5678
|
+
const feedbacks = elements(node, "displayfeedback").map((df) => {
|
|
5679
|
+
const entry = {
|
|
5680
|
+
feedbacktype: attr2(df, "feedbacktype") ?? "Response",
|
|
5681
|
+
linkrefid: attr2(df, "linkrefid") ?? ""
|
|
5682
|
+
};
|
|
5683
|
+
const value = textOf(df);
|
|
5684
|
+
if (value.length > 0)
|
|
5685
|
+
entry["value"] = value;
|
|
5686
|
+
return entry;
|
|
5687
|
+
});
|
|
5688
|
+
if (feedbacks.length > 0)
|
|
5689
|
+
out["displayfeedback"] = feedbacks;
|
|
5690
|
+
return out;
|
|
5691
|
+
}
|
|
5692
|
+
function mapResprocessing(node) {
|
|
5693
|
+
const outcomesNode = firstElement(node, "outcomes");
|
|
5694
|
+
const decvarNode = outcomesNode ? firstElement(outcomesNode, "decvar") : undefined;
|
|
5695
|
+
const decvar = {
|
|
5696
|
+
value: attr2(decvarNode ?? node, "defaultval") ?? "",
|
|
5697
|
+
varname: attr2(decvarNode ?? node, "varname") ?? "SCORE"
|
|
5698
|
+
};
|
|
5699
|
+
put(decvar, "vartype", attr2(decvarNode ?? node, "vartype"));
|
|
5700
|
+
put(decvar, "minvalue", attr2(decvarNode ?? node, "minvalue"));
|
|
5701
|
+
put(decvar, "maxvalue", attr2(decvarNode ?? node, "maxvalue"));
|
|
5702
|
+
return {
|
|
5703
|
+
outcomes: { decvar },
|
|
5704
|
+
respcondition: elements(node, "respcondition").map(mapRespCondition)
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
function mapFeedbackMaterialList(node) {
|
|
5708
|
+
const flowMats = elements(node, "flow_mat");
|
|
5709
|
+
if (flowMats.length > 0) {
|
|
5710
|
+
return { flow_mat: flowMats.map(mapFlowMat) };
|
|
5711
|
+
}
|
|
5712
|
+
return { material: elements(node, "material").map(mapMaterial) };
|
|
5713
|
+
}
|
|
5714
|
+
function mapSolution(node) {
|
|
5715
|
+
const out = {
|
|
5716
|
+
kind: "solution",
|
|
5717
|
+
solutionmaterial: elements(node, "solutionmaterial").map(mapFeedbackMaterialList)
|
|
5718
|
+
};
|
|
5719
|
+
put(out, "feedbackstyle", attr2(node, "feedbackstyle"));
|
|
5720
|
+
return out;
|
|
5721
|
+
}
|
|
5722
|
+
function mapHint(node) {
|
|
5723
|
+
const out = { kind: "hint", hintmaterial: elements(node, "hintmaterial").map(mapFeedbackMaterialList) };
|
|
5724
|
+
put(out, "feedbackstyle", attr2(node, "feedbackstyle"));
|
|
5725
|
+
return out;
|
|
5726
|
+
}
|
|
5727
|
+
function mapItemFeedback(node) {
|
|
5728
|
+
const out = {
|
|
5729
|
+
ident: attr2(node, "ident") ?? "",
|
|
5730
|
+
children: elements(node).map((child) => {
|
|
5731
|
+
if (child.localName === "flow_mat")
|
|
5732
|
+
return mapFlowMat(child);
|
|
5733
|
+
if (child.localName === "material")
|
|
5734
|
+
return mapMaterial(child);
|
|
5735
|
+
if (child.localName === "solution")
|
|
5736
|
+
return mapSolution(child);
|
|
5737
|
+
if (child.localName === "hint")
|
|
5738
|
+
return mapHint(child);
|
|
5739
|
+
return;
|
|
5740
|
+
}).filter((child) => child !== undefined)
|
|
5741
|
+
};
|
|
5742
|
+
put(out, "title", attr2(node, "title"));
|
|
5743
|
+
return out;
|
|
5744
|
+
}
|
|
5745
|
+
function mapQtimetadata(node) {
|
|
5746
|
+
return {
|
|
5747
|
+
qtimetadatafield: elements(node, "qtimetadatafield").map((field) => {
|
|
5748
|
+
const labelNode = firstElement(field, "fieldlabel");
|
|
5749
|
+
const entryNode = firstElement(field, "fieldentry");
|
|
5750
|
+
const out = {
|
|
5751
|
+
fieldlabel: labelNode ? textOf(labelNode) : "",
|
|
5752
|
+
fieldentry: entryNode ? textOf(entryNode) : ""
|
|
5753
|
+
};
|
|
5754
|
+
put(out, "xmlLang", attr2(field, "xml:lang"));
|
|
5755
|
+
return out;
|
|
5756
|
+
})
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
5759
|
+
function mapItem(node) {
|
|
5760
|
+
const out = { ident: attr2(node, "ident") ?? "" };
|
|
5761
|
+
put(out, "title", attr2(node, "title"));
|
|
5762
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5763
|
+
const itemmetadata = firstElement(node, "itemmetadata");
|
|
5764
|
+
if (itemmetadata) {
|
|
5765
|
+
out["itemmetadata"] = {
|
|
5766
|
+
qtimetadata: elements(itemmetadata, "qtimetadata").map(mapQtimetadata)
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
const presentation = firstElement(node, "presentation");
|
|
5770
|
+
if (presentation) {
|
|
5771
|
+
out["presentation"] = mapPresentation(presentation);
|
|
5772
|
+
}
|
|
5773
|
+
const resprocessing = elements(node, "resprocessing");
|
|
5774
|
+
if (resprocessing.length > 0) {
|
|
5775
|
+
out["resprocessing"] = resprocessing.map(mapResprocessing);
|
|
5776
|
+
}
|
|
5777
|
+
const itemfeedback = elements(node, "itemfeedback");
|
|
5778
|
+
if (itemfeedback.length > 0) {
|
|
5779
|
+
out["itemfeedback"] = itemfeedback.map(mapItemFeedback);
|
|
5780
|
+
}
|
|
5781
|
+
return out;
|
|
5782
|
+
}
|
|
5783
|
+
function mapSection(node) {
|
|
5784
|
+
const out = {
|
|
5785
|
+
ident: attr2(node, "ident") ?? "",
|
|
5786
|
+
item: elements(node, "item").map(mapItem)
|
|
5787
|
+
};
|
|
5788
|
+
put(out, "title", attr2(node, "title"));
|
|
5789
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5790
|
+
return out;
|
|
5791
|
+
}
|
|
5792
|
+
function mapAssessment(node) {
|
|
5793
|
+
const out = {
|
|
5794
|
+
ident: attr2(node, "ident") ?? "",
|
|
5795
|
+
title: attr2(node, "title") ?? ""
|
|
5796
|
+
};
|
|
5797
|
+
put(out, "xmlLang", attr2(node, "xml:lang"));
|
|
5798
|
+
const qtimetadata = firstElement(node, "qtimetadata");
|
|
5799
|
+
if (qtimetadata)
|
|
5800
|
+
out["qtimetadata"] = mapQtimetadata(qtimetadata);
|
|
5801
|
+
const section = firstElement(node, "section");
|
|
5802
|
+
out["section"] = section ? mapSection(section) : { ident: "root_section", item: [] };
|
|
5803
|
+
return out;
|
|
5804
|
+
}
|
|
5805
|
+
function mapObjectbank(node) {
|
|
5806
|
+
const out = {
|
|
5807
|
+
ident: attr2(node, "ident") ?? "",
|
|
5808
|
+
item: elements(node, "item").map(mapItem)
|
|
5809
|
+
};
|
|
5810
|
+
const qtimetadata = firstElement(node, "qtimetadata");
|
|
5811
|
+
if (qtimetadata)
|
|
5812
|
+
out["qtimetadata"] = mapQtimetadata(qtimetadata);
|
|
5813
|
+
return out;
|
|
5814
|
+
}
|
|
5815
|
+
function normalizeQuestestinterop(xml, options) {
|
|
5816
|
+
const profile = options?.profile ?? true;
|
|
5817
|
+
let root;
|
|
5818
|
+
try {
|
|
5819
|
+
root = parseXmlDocument(xml);
|
|
5820
|
+
} catch (error) {
|
|
5821
|
+
return { status: "invalid", issues: [error instanceof Error ? error.message : "Invalid XML."] };
|
|
5822
|
+
}
|
|
5823
|
+
if (root.localName !== "questestinterop") {
|
|
5824
|
+
return { status: "invalid", issues: [`Expected <questestinterop> root, found <${root.localName}>.`] };
|
|
5825
|
+
}
|
|
5826
|
+
const assessment = firstElement(root, "assessment");
|
|
5827
|
+
const objectbank = firstElement(root, "objectbank");
|
|
5828
|
+
let candidate;
|
|
5829
|
+
if (assessment) {
|
|
5830
|
+
candidate = { questestinterop: { assessment: mapAssessment(assessment) } };
|
|
5831
|
+
} else if (objectbank) {
|
|
5832
|
+
candidate = { questestinterop: { objectbank: mapObjectbank(objectbank) } };
|
|
5833
|
+
} else {
|
|
5834
|
+
return { status: "invalid", issues: ["<questestinterop> must contain an <assessment> or <objectbank>."] };
|
|
5835
|
+
}
|
|
5836
|
+
const schema = profile ? QtiQuestestinteropProfileDocumentSchema : QtiQuestestinteropRawDocumentSchema;
|
|
5837
|
+
const parsed = schema.safeParse(candidate);
|
|
5838
|
+
if (!parsed.success) {
|
|
5839
|
+
return {
|
|
5840
|
+
status: "invalid",
|
|
5841
|
+
issues: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
5842
|
+
};
|
|
5843
|
+
}
|
|
5844
|
+
return {
|
|
5845
|
+
status: "valid",
|
|
5846
|
+
document: parsed.data
|
|
5847
|
+
};
|
|
5848
|
+
}
|
|
5849
|
+
// src/cc-qti/convert-to-v3.ts
|
|
5850
|
+
var MATCH_CORRECT = "https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct";
|
|
5851
|
+
var MAP_RESPONSE = "https://www.imsglobal.org/question/qti_v3p0/rptemplates/map_response";
|
|
5852
|
+
function asNode3(value) {
|
|
5853
|
+
return value ?? {};
|
|
5854
|
+
}
|
|
5855
|
+
function arr(value) {
|
|
5856
|
+
return Array.isArray(value) ? value : [];
|
|
5857
|
+
}
|
|
5858
|
+
function strOf(node, key) {
|
|
5859
|
+
const value = node[key];
|
|
5860
|
+
return typeof value === "string" ? value : undefined;
|
|
5861
|
+
}
|
|
5862
|
+
var QTI_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9._-]*$/u;
|
|
5863
|
+
function sanitizeIdentifier(raw) {
|
|
5864
|
+
if (raw.length > 0 && QTI_IDENTIFIER.test(raw))
|
|
5865
|
+
return raw;
|
|
5866
|
+
const replaced = raw.replace(/[^A-Za-z0-9._-]/gu, "_");
|
|
5867
|
+
const prefixed = /^[A-Za-z_]/u.test(replaced) ? replaced : `id_${replaced}`;
|
|
5868
|
+
return prefixed.length > 0 ? prefixed : "_";
|
|
5869
|
+
}
|
|
5870
|
+
function assignUniqueIdentifiers(rawIds) {
|
|
5871
|
+
const used = new Set;
|
|
5872
|
+
return rawIds.map((rawId) => {
|
|
5873
|
+
const base = sanitizeIdentifier(rawId);
|
|
5874
|
+
let candidate = base;
|
|
5875
|
+
let suffix = 2;
|
|
5876
|
+
while (used.has(candidate)) {
|
|
5877
|
+
candidate = `${base}-${suffix}`;
|
|
5878
|
+
suffix += 1;
|
|
5879
|
+
}
|
|
5880
|
+
used.add(candidate);
|
|
5881
|
+
return candidate;
|
|
5882
|
+
});
|
|
5883
|
+
}
|
|
5884
|
+
function htmlToFragments(html) {
|
|
5885
|
+
try {
|
|
5886
|
+
const root = parseXmlDocument(`<div xmlns="${asiNamespace}">${html}</div>`);
|
|
5887
|
+
return mapXmlChildren(root);
|
|
5888
|
+
} catch {
|
|
5889
|
+
return [html];
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
function mapXmlElement(element) {
|
|
5893
|
+
const children = mapXmlChildren(element);
|
|
5894
|
+
const out = { kind: "xml", namespace: asiNamespace, name: element.localName };
|
|
5895
|
+
if (Object.keys(element.attributes).length > 0) {
|
|
5896
|
+
out["attributes"] = element.attributes;
|
|
5897
|
+
}
|
|
5898
|
+
if (children.length > 0) {
|
|
5899
|
+
out["children"] = children;
|
|
5900
|
+
}
|
|
5901
|
+
return out;
|
|
5902
|
+
}
|
|
5903
|
+
function mapXmlChildren(element) {
|
|
5904
|
+
const fragments2 = [];
|
|
5905
|
+
for (const child of element.children) {
|
|
5906
|
+
if (child.type === "text") {
|
|
5907
|
+
if (child.value.trim().length > 0)
|
|
5908
|
+
fragments2.push(child.value);
|
|
5909
|
+
} else {
|
|
5910
|
+
fragments2.push(mapXmlElement(child));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
return fragments2;
|
|
5914
|
+
}
|
|
5915
|
+
function mattextFragments(mattext) {
|
|
5916
|
+
const value = strOf(mattext, "value") ?? "";
|
|
5917
|
+
const texttype = strOf(mattext, "texttype");
|
|
5918
|
+
if (texttype && texttype.toLowerCase().includes("html")) {
|
|
5919
|
+
return htmlToFragments(value);
|
|
5920
|
+
}
|
|
5921
|
+
return value.length > 0 ? [value] : [];
|
|
5922
|
+
}
|
|
5923
|
+
function materialFragments(material) {
|
|
5924
|
+
return arr(material["children"]).flatMap((child) => {
|
|
5925
|
+
const kind = strOf(child, "kind");
|
|
5926
|
+
if (kind === "mattext")
|
|
5927
|
+
return mattextFragments(child);
|
|
5928
|
+
if (kind === "matbreak")
|
|
5929
|
+
return [{ kind: "xml", namespace: asiNamespace, name: "br" }];
|
|
5930
|
+
return [];
|
|
5931
|
+
});
|
|
5932
|
+
}
|
|
5933
|
+
function stemContent(materials) {
|
|
5934
|
+
const content = [];
|
|
5935
|
+
for (const material of materials) {
|
|
5936
|
+
const fragments2 = materialFragments(material);
|
|
5937
|
+
if (fragments2.length === 0)
|
|
5938
|
+
continue;
|
|
5939
|
+
if (fragments2.every((fragment) => typeof fragment === "string")) {
|
|
5940
|
+
content.push({ kind: "xml", namespace: asiNamespace, name: "p", children: fragments2 });
|
|
5941
|
+
} else {
|
|
5942
|
+
content.push(...fragments2);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
return content;
|
|
5946
|
+
}
|
|
5947
|
+
function collectPresentation(presentation) {
|
|
5948
|
+
const stemMaterials = [];
|
|
5949
|
+
let response;
|
|
5950
|
+
const visit = (node) => {
|
|
5951
|
+
const kind = strOf(node, "kind");
|
|
5952
|
+
if (kind === "response_lid" || kind === "response_str") {
|
|
5953
|
+
if (!response)
|
|
5954
|
+
response = node;
|
|
5955
|
+
const leading = node["leading"];
|
|
5956
|
+
if (leading && strOf(asNode3(leading), "kind") === "material")
|
|
5957
|
+
stemMaterials.push(asNode3(leading));
|
|
5958
|
+
return;
|
|
5959
|
+
}
|
|
5960
|
+
if (kind === "material") {
|
|
5961
|
+
stemMaterials.push(node);
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
if (kind === "flow") {
|
|
5965
|
+
for (const child of arr(node["children"]))
|
|
5966
|
+
visit(child);
|
|
5967
|
+
}
|
|
5968
|
+
};
|
|
5969
|
+
const flow = presentation["flow"];
|
|
5970
|
+
if (flow) {
|
|
5971
|
+
visit(asNode3(flow));
|
|
5972
|
+
} else {
|
|
5973
|
+
for (const child of arr(presentation["children"]))
|
|
5974
|
+
visit(child);
|
|
5975
|
+
}
|
|
5976
|
+
return { stemMaterials, response };
|
|
5977
|
+
}
|
|
5978
|
+
function renderChoiceLabels(response) {
|
|
5979
|
+
const render = asNode3(response["render"]);
|
|
5980
|
+
return arr(render["children"]).filter((child) => strOf(child, "kind") === "response_label");
|
|
5981
|
+
}
|
|
5982
|
+
function choiceContent(label) {
|
|
5983
|
+
const materials = arr(label["children"]).filter((child) => strOf(child, "kind") === "material");
|
|
5984
|
+
const fragments2 = materials.flatMap(materialFragments);
|
|
5985
|
+
return fragments2.length > 0 ? fragments2 : [strOf(label, "ident") ?? ""];
|
|
5986
|
+
}
|
|
5987
|
+
function collectVarequals(item) {
|
|
5988
|
+
const result = [];
|
|
5989
|
+
const walk = (node) => {
|
|
5990
|
+
const kind = strOf(node, "kind");
|
|
5991
|
+
if (kind === "varequal" || kind === "varsubstring") {
|
|
5992
|
+
result.push({
|
|
5993
|
+
respident: strOf(node, "respident") ?? "",
|
|
5994
|
+
value: strOf(node, "value") ?? "",
|
|
5995
|
+
caseSensitive: strOf(node, "case") === "Yes"
|
|
5996
|
+
});
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
if (kind === "and" || kind === "or" || kind === "not") {
|
|
6000
|
+
for (const test of arr(node["tests"]))
|
|
6001
|
+
walk(test);
|
|
6002
|
+
}
|
|
6003
|
+
};
|
|
6004
|
+
for (const resprocessing of arr(item["resprocessing"])) {
|
|
6005
|
+
for (const respcondition of arr(resprocessing["respcondition"])) {
|
|
6006
|
+
for (const test of arr(asNode3(respcondition["conditionvar"])["tests"]))
|
|
6007
|
+
walk(test);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
return result;
|
|
6011
|
+
}
|
|
6012
|
+
function findCcProfile(item) {
|
|
6013
|
+
for (const metadata of arr(asNode3(item["itemmetadata"])["qtimetadata"])) {
|
|
6014
|
+
for (const field of arr(metadata["qtimetadatafield"])) {
|
|
6015
|
+
if (strOf(field, "fieldlabel") === "cc_profile")
|
|
6016
|
+
return strOf(field, "fieldentry");
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
function scoreOutcomeDeclaration() {
|
|
6022
|
+
return {
|
|
6023
|
+
identifier: "SCORE",
|
|
6024
|
+
cardinality: "single",
|
|
6025
|
+
baseType: "float",
|
|
6026
|
+
defaultValue: { values: [{ value: "0" }] }
|
|
6027
|
+
};
|
|
6028
|
+
}
|
|
6029
|
+
function buildChoiceItem(item, response, stem) {
|
|
6030
|
+
const rawResponseId = strOf(response, "ident") ?? "RESPONSE";
|
|
6031
|
+
const responseId = sanitizeIdentifier(rawResponseId);
|
|
6032
|
+
const rcardinality = strOf(response, "rcardinality") ?? "Single";
|
|
6033
|
+
const cardinality = rcardinality === "Multiple" ? "multiple" : rcardinality === "Ordered" ? "ordered" : "single";
|
|
6034
|
+
const labels = renderChoiceLabels(response);
|
|
6035
|
+
const correct = collectVarequals(item).filter((entry) => entry.respident === rawResponseId).map((entry) => sanitizeIdentifier(entry.value));
|
|
6036
|
+
const interaction = {
|
|
6037
|
+
kind: "choiceInteraction",
|
|
6038
|
+
responseIdentifier: responseId,
|
|
6039
|
+
shuffle: strOf(asNode3(response["render"]), "shuffle") === "Yes",
|
|
6040
|
+
maxChoices: cardinality === "single" ? 1 : 0,
|
|
6041
|
+
simpleChoices: labels.map((label) => ({
|
|
6042
|
+
kind: "simpleChoice",
|
|
6043
|
+
identifier: sanitizeIdentifier(strOf(label, "ident") ?? ""),
|
|
6044
|
+
content: choiceContent(label)
|
|
6045
|
+
}))
|
|
6046
|
+
};
|
|
6047
|
+
const responseDeclaration = {
|
|
6048
|
+
identifier: responseId,
|
|
6049
|
+
cardinality,
|
|
6050
|
+
baseType: "identifier",
|
|
6051
|
+
correctResponse: { values: correct.map((value) => ({ value })) }
|
|
6052
|
+
};
|
|
6053
|
+
return {
|
|
6054
|
+
interactionKind: "choice",
|
|
6055
|
+
document: {
|
|
6056
|
+
responseDeclarations: [responseDeclaration],
|
|
6057
|
+
outcomeDeclarations: [scoreOutcomeDeclaration()],
|
|
6058
|
+
itemBody: { content: [...stemContent(stem), interaction] },
|
|
6059
|
+
responseProcessing: { template: MATCH_CORRECT }
|
|
6060
|
+
}
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
function buildTextEntryItem(item, response, stem) {
|
|
6064
|
+
const rawResponseId = strOf(response, "ident") ?? "RESPONSE";
|
|
6065
|
+
const responseId = sanitizeIdentifier(rawResponseId);
|
|
6066
|
+
const correct = collectVarequals(item).filter((entry) => entry.respident === rawResponseId);
|
|
6067
|
+
const responseDeclaration = {
|
|
6068
|
+
identifier: responseId,
|
|
6069
|
+
cardinality: "single",
|
|
6070
|
+
baseType: "string",
|
|
6071
|
+
...correct[0] ? { correctResponse: { values: [{ value: correct[0].value }] } } : {},
|
|
6072
|
+
mapping: {
|
|
6073
|
+
defaultValue: 0,
|
|
6074
|
+
mapEntries: correct.map((entry) => ({
|
|
6075
|
+
mapKey: entry.value,
|
|
6076
|
+
mappedValue: 1,
|
|
6077
|
+
caseSensitive: entry.caseSensitive
|
|
6078
|
+
}))
|
|
6079
|
+
}
|
|
6080
|
+
};
|
|
6081
|
+
const interaction = { kind: "textEntryInteraction", responseIdentifier: responseId };
|
|
6082
|
+
return {
|
|
6083
|
+
interactionKind: "textEntry",
|
|
6084
|
+
document: {
|
|
6085
|
+
responseDeclarations: [responseDeclaration],
|
|
6086
|
+
outcomeDeclarations: [scoreOutcomeDeclaration()],
|
|
6087
|
+
itemBody: { content: [...stemContent(stem), interaction] },
|
|
6088
|
+
responseProcessing: { template: MAP_RESPONSE }
|
|
6089
|
+
}
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
function buildExtendedTextItem(response, stem) {
|
|
6093
|
+
const responseId = sanitizeIdentifier(strOf(response, "ident") ?? "RESPONSE");
|
|
6094
|
+
return {
|
|
6095
|
+
interactionKind: "extendedText",
|
|
6096
|
+
document: {
|
|
6097
|
+
responseDeclarations: [{ identifier: responseId, cardinality: "single", baseType: "string" }],
|
|
6098
|
+
outcomeDeclarations: [scoreOutcomeDeclaration()],
|
|
6099
|
+
itemBody: {
|
|
6100
|
+
content: [...stemContent(stem), { kind: "extendedTextInteraction", responseIdentifier: responseId }]
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
};
|
|
6104
|
+
}
|
|
6105
|
+
function convertItem(item, identifier) {
|
|
6106
|
+
const title = strOf(item, "title") ?? identifier;
|
|
6107
|
+
const ccProfile = findCcProfile(item);
|
|
6108
|
+
const presentation = asNode3(item["presentation"]);
|
|
6109
|
+
const { stemMaterials, response } = collectPresentation(presentation);
|
|
6110
|
+
let built;
|
|
6111
|
+
if (!response) {
|
|
6112
|
+
built = buildExtendedTextItem({ ident: "RESPONSE" }, stemMaterials);
|
|
6113
|
+
} else {
|
|
6114
|
+
const responseKind = strOf(response, "kind");
|
|
6115
|
+
const renderKind = strOf(asNode3(response["render"]), "kind");
|
|
6116
|
+
const hasCorrect = collectVarequals(item).length > 0;
|
|
6117
|
+
if (responseKind === "response_lid" && renderKind === "render_choice") {
|
|
6118
|
+
built = buildChoiceItem(item, response, stemMaterials);
|
|
6119
|
+
} else if (responseKind === "response_str" && hasCorrect) {
|
|
6120
|
+
built = buildTextEntryItem(item, response, stemMaterials);
|
|
6121
|
+
} else {
|
|
6122
|
+
built = buildExtendedTextItem(response, stemMaterials);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
const assessmentItem = {
|
|
6126
|
+
identifier,
|
|
6127
|
+
title,
|
|
6128
|
+
timeDependent: false,
|
|
6129
|
+
...built.document
|
|
6130
|
+
};
|
|
6131
|
+
const xml = serializeQtiDocument("3.0.1", "qtiAssessmentItemDocument", { assessmentItem });
|
|
6132
|
+
return { identifier, title, ccProfile, interactionKind: built.interactionKind, xml };
|
|
6133
|
+
}
|
|
6134
|
+
function buildTest(assessment, items) {
|
|
6135
|
+
const identifier = strOf(assessment, "ident") ?? "assessment";
|
|
6136
|
+
const title = strOf(assessment, "title") ?? identifier;
|
|
6137
|
+
const section = asNode3(assessment["section"]);
|
|
6138
|
+
const sectionId = strOf(section, "ident") ?? "root_section";
|
|
6139
|
+
const assessmentTest = {
|
|
6140
|
+
identifier,
|
|
6141
|
+
title,
|
|
6142
|
+
outcomeDeclarations: [{ identifier: "SCORE", cardinality: "single", baseType: "float" }],
|
|
6143
|
+
testParts: [
|
|
6144
|
+
{
|
|
6145
|
+
identifier: "testpart-1",
|
|
6146
|
+
navigationMode: "nonlinear",
|
|
6147
|
+
submissionMode: "individual",
|
|
6148
|
+
children: [
|
|
6149
|
+
{
|
|
6150
|
+
identifier: sectionId,
|
|
6151
|
+
title: strOf(section, "title") ?? title,
|
|
6152
|
+
visible: true,
|
|
6153
|
+
children: items.map((item) => ({
|
|
6154
|
+
identifier: `ref-${item.identifier}`,
|
|
6155
|
+
href: `${item.identifier}.xml`
|
|
6156
|
+
}))
|
|
6157
|
+
}
|
|
6158
|
+
]
|
|
6159
|
+
}
|
|
6160
|
+
]
|
|
6161
|
+
};
|
|
6162
|
+
return {
|
|
6163
|
+
identifier,
|
|
6164
|
+
title,
|
|
6165
|
+
itemIdentifiers: items.map((item) => item.identifier),
|
|
6166
|
+
xml: serializeQtiDocument("3.0.1", "qtiAssessmentTestDocument", { assessmentTest })
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
function convertItems(rawItems) {
|
|
6170
|
+
const identifiers = assignUniqueIdentifiers(rawItems.map((item) => strOf(item, "ident") ?? "item"));
|
|
6171
|
+
return rawItems.map((item, index) => convertItem(item, identifiers[index]));
|
|
6172
|
+
}
|
|
6173
|
+
function convertCcQtiV1ToV3(xml, options) {
|
|
6174
|
+
const normalized = normalizeQuestestinterop(xml, { profile: options?.profile ?? false });
|
|
6175
|
+
if (normalized.status === "invalid") {
|
|
6176
|
+
return { status: "invalid", issues: normalized.issues };
|
|
6177
|
+
}
|
|
6178
|
+
const root = normalized.document.questestinterop;
|
|
6179
|
+
if ("assessment" in root) {
|
|
6180
|
+
const assessment = asNode3(root["assessment"]);
|
|
6181
|
+
const section = asNode3(assessment["section"]);
|
|
6182
|
+
const items2 = convertItems(arr(section["item"]));
|
|
6183
|
+
return { status: "converted", source: "assessment", items: items2, test: buildTest(assessment, items2) };
|
|
6184
|
+
}
|
|
6185
|
+
const objectbank = asNode3(root["objectbank"]);
|
|
6186
|
+
const items = convertItems(arr(objectbank["item"]));
|
|
6187
|
+
return { status: "converted", source: "objectbank", items };
|
|
6188
|
+
}
|
|
5387
6189
|
export {
|
|
5388
6190
|
validateQtiXmlFile,
|
|
5389
6191
|
validateQtiXmlContent,
|
|
@@ -5405,10 +6207,13 @@ export {
|
|
|
5405
6207
|
serializeQtiAccessForAllPnp,
|
|
5406
6208
|
selectQtiSchema,
|
|
5407
6209
|
parseXmlDocument,
|
|
6210
|
+
normalizeQuestestinterop,
|
|
5408
6211
|
normalizeQtiDocument,
|
|
5409
6212
|
isSerializationImplemented,
|
|
5410
6213
|
isNormalizationImplemented,
|
|
5411
6214
|
detectQtiRoot,
|
|
6215
|
+
convertCcQtiV1ToV3,
|
|
6216
|
+
ccQtiNamespace,
|
|
5412
6217
|
buildQtiExampleInventory,
|
|
5413
6218
|
asiNamespace
|
|
5414
6219
|
};
|