@examplary/qti 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,8 +20,10 @@ const item = new QtiItem({
20
20
  title: "Sample Question",
21
21
  });
22
22
 
23
- item.addResponseDeclaration({ identifier: "RESPONSE" });
24
- item.addCorrectResponse("RESPONSE", ["4"]);
23
+ item.addResponseDeclaration({
24
+ identifier: "RESPONSE",
25
+ correctResponse: ["4"],
26
+ });
25
27
 
26
28
  item.addItemBodyFromHtml("<p>What is 2 + 2?</p>");
27
29
  item.addInteraction(
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export * from "./ims/ims-manifest";
2
2
  export * from "./ims/ims-package";
3
3
  export * from "./qti/qti-element";
4
4
  export * from "./qti/qti-test";
5
+ export * from "./qti/qti-test-part";
6
+ export * from "./qti/qti-assessment-section";
5
7
  export * from "./qti/qti-item";
6
8
  export * from "./qti/interactions";
7
9
  export * from "./qti/types";
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ __exportStar(require("./ims/ims-manifest"), exports);
18
18
  __exportStar(require("./ims/ims-package"), exports);
19
19
  __exportStar(require("./qti/qti-element"), exports);
20
20
  __exportStar(require("./qti/qti-test"), exports);
21
+ __exportStar(require("./qti/qti-test-part"), exports);
22
+ __exportStar(require("./qti/qti-assessment-section"), exports);
21
23
  __exportStar(require("./qti/qti-item"), exports);
22
24
  __exportStar(require("./qti/interactions"), exports);
23
25
  __exportStar(require("./qti/types"), exports);
@@ -13,6 +13,7 @@ import { InlineChoiceInteraction } from "./inline-choice-interaction";
13
13
  import { MatchInteraction } from "./match-interaction";
14
14
  import { MediaInteraction } from "./media-interaction";
15
15
  import { OrderInteraction } from "./order-interaction";
16
+ import { PortableCustomInteraction } from "./portable-custom-interaction";
16
17
  import { PositionObjectInteraction } from "./position-object-interaction";
17
18
  import { SelectPointInteraction } from "./select-point-interaction";
18
19
  import { SliderInteraction } from "./slider-interaction";
@@ -34,6 +35,7 @@ export * from "./interaction";
34
35
  export * from "./match-interaction";
35
36
  export * from "./media-interaction";
36
37
  export * from "./order-interaction";
38
+ export * from "./portable-custom-interaction";
37
39
  export * from "./position-object-interaction";
38
40
  export * from "./select-point-interaction";
39
41
  export * from "./slider-interaction";
@@ -55,6 +57,7 @@ export declare const qtiInteractionTypes: {
55
57
  MatchInteraction: typeof MatchInteraction;
56
58
  MediaInteraction: typeof MediaInteraction;
57
59
  OrderInteraction: typeof OrderInteraction;
60
+ PortableCustomInteraction: typeof PortableCustomInteraction;
58
61
  PositionObjectInteraction: typeof PositionObjectInteraction;
59
62
  SelectPointInteraction: typeof SelectPointInteraction;
60
63
  SliderInteraction: typeof SliderInteraction;
@@ -30,6 +30,7 @@ const inline_choice_interaction_1 = require("./inline-choice-interaction");
30
30
  const match_interaction_1 = require("./match-interaction");
31
31
  const media_interaction_1 = require("./media-interaction");
32
32
  const order_interaction_1 = require("./order-interaction");
33
+ const portable_custom_interaction_1 = require("./portable-custom-interaction");
33
34
  const position_object_interaction_1 = require("./position-object-interaction");
34
35
  const select_point_interaction_1 = require("./select-point-interaction");
35
36
  const slider_interaction_1 = require("./slider-interaction");
@@ -51,6 +52,7 @@ __exportStar(require("./interaction"), exports);
51
52
  __exportStar(require("./match-interaction"), exports);
52
53
  __exportStar(require("./media-interaction"), exports);
53
54
  __exportStar(require("./order-interaction"), exports);
55
+ __exportStar(require("./portable-custom-interaction"), exports);
54
56
  __exportStar(require("./position-object-interaction"), exports);
55
57
  __exportStar(require("./select-point-interaction"), exports);
56
58
  __exportStar(require("./slider-interaction"), exports);
@@ -72,6 +74,7 @@ exports.qtiInteractionTypes = {
72
74
  MatchInteraction: match_interaction_1.MatchInteraction,
73
75
  MediaInteraction: media_interaction_1.MediaInteraction,
74
76
  OrderInteraction: order_interaction_1.OrderInteraction,
77
+ PortableCustomInteraction: portable_custom_interaction_1.PortableCustomInteraction,
75
78
  PositionObjectInteraction: position_object_interaction_1.PositionObjectInteraction,
76
79
  SelectPointInteraction: select_point_interaction_1.SelectPointInteraction,
77
80
  SliderInteraction: slider_interaction_1.SliderInteraction,
@@ -0,0 +1,35 @@
1
+ import { QtiPromptInteraction, QtiPromptInteractionOptions } from "./interaction";
2
+ export type PortableCustomInteractionOptions = QtiPromptInteractionOptions & {
3
+ module: string;
4
+ customInteractionTypeIdentifier: string;
5
+ dataExamplarySettings?: string;
6
+ class?: string;
7
+ modules?: {
8
+ id: string;
9
+ primaryPath: string;
10
+ fallbackPath?: string;
11
+ }[];
12
+ markup?: string;
13
+ templateVariables?: {
14
+ templateIdentifier: string;
15
+ }[];
16
+ contextVariables?: {
17
+ identifier: string;
18
+ }[];
19
+ };
20
+ /**
21
+ * A custom QTI portable custom interaction (PCI), allowing you to load arbitrary
22
+ * HTML and JavaScript for rendering the interaction.
23
+ *
24
+ * @see https://www.imsglobal.org/spec/qti/v3p0/impl#h.98xaka8g51za
25
+ *
26
+ * @example
27
+ * const interaction = new PortableCustomInteraction({
28
+ * responseIdentifier: "RESPONSE",
29
+ * module: "single-line-text",
30
+ * customInteractionTypeIdentifier: "urn:fdc:examplary.ai:pci:single-line-text",
31
+ * });
32
+ */
33
+ export declare class PortableCustomInteraction extends QtiPromptInteraction {
34
+ constructor(options: PortableCustomInteractionOptions);
35
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PortableCustomInteraction = void 0;
4
+ const xmlbuilder2_1 = require("xmlbuilder2");
5
+ const interaction_1 = require("./interaction");
6
+ const html_1 = require("../../utils/html");
7
+ /**
8
+ * A custom QTI portable custom interaction (PCI), allowing you to load arbitrary
9
+ * HTML and JavaScript for rendering the interaction.
10
+ *
11
+ * @see https://www.imsglobal.org/spec/qti/v3p0/impl#h.98xaka8g51za
12
+ *
13
+ * @example
14
+ * const interaction = new PortableCustomInteraction({
15
+ * responseIdentifier: "RESPONSE",
16
+ * module: "single-line-text",
17
+ * customInteractionTypeIdentifier: "urn:fdc:examplary.ai:pci:single-line-text",
18
+ * });
19
+ */
20
+ class PortableCustomInteraction extends interaction_1.QtiPromptInteraction {
21
+ constructor(options) {
22
+ super(options);
23
+ this.item = (0, xmlbuilder2_1.fragment)().ele("qti-portable-custom-interaction", {
24
+ "response-identifier": options.responseIdentifier,
25
+ label: options.label,
26
+ module: options.module,
27
+ "custom-interaction-type-identifier": options.customInteractionTypeIdentifier,
28
+ "data-examplary-settings": options.dataExamplarySettings,
29
+ class: options.class,
30
+ });
31
+ // Modules
32
+ if (options.modules?.length) {
33
+ const modules = this.item.ele("qti-interaction-modules");
34
+ for (const module of options.modules) {
35
+ modules.ele("qti-interaction-module", {
36
+ id: module.id,
37
+ "primary-path": module.primaryPath,
38
+ "fallback-path": module.fallbackPath,
39
+ });
40
+ }
41
+ }
42
+ // Markup
43
+ (0, html_1.appendHtmlFragment)(options.markup || "", this.item.ele("qti-interaction-markup"));
44
+ // Template variables
45
+ if (options.templateVariables?.length) {
46
+ for (const variable of options.templateVariables) {
47
+ this.item.ele("qti-template-variable", {
48
+ "template-identifier": variable.templateIdentifier,
49
+ });
50
+ }
51
+ }
52
+ // Context variables
53
+ if (options.contextVariables?.length) {
54
+ for (const variable of options.contextVariables) {
55
+ this.item.ele("qti-context-variable", {
56
+ identifier: variable.identifier,
57
+ });
58
+ }
59
+ }
60
+ }
61
+ }
62
+ exports.PortableCustomInteraction = PortableCustomInteraction;
@@ -0,0 +1,26 @@
1
+ export type QtiAssessmentSectionOptions = {
2
+ identifier: string;
3
+ title: string;
4
+ visible: boolean;
5
+ class?: string;
6
+ fixed?: boolean;
7
+ required?: boolean;
8
+ keepTogether?: boolean;
9
+ };
10
+ export type QtiItemReference = {
11
+ itemIdentifier: string;
12
+ href: string;
13
+ };
14
+ export declare class QtiAssessmentSection {
15
+ identifier: string;
16
+ title: string;
17
+ visible: boolean;
18
+ class?: string;
19
+ fixed?: boolean;
20
+ required?: boolean;
21
+ keepTogether?: boolean;
22
+ protected itemReferences: QtiItemReference[];
23
+ constructor(options: QtiAssessmentSectionOptions);
24
+ addItemReference(itemIdentifier: string, href: string): void;
25
+ getItemReferences(): QtiItemReference[];
26
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QtiAssessmentSection = void 0;
4
+ class QtiAssessmentSection {
5
+ identifier;
6
+ title;
7
+ visible;
8
+ class;
9
+ fixed;
10
+ required;
11
+ keepTogether;
12
+ itemReferences = [];
13
+ constructor(options) {
14
+ this.identifier = options.identifier;
15
+ this.title = options.title;
16
+ this.visible = options.visible;
17
+ this.class = options.class;
18
+ this.fixed = options.fixed ?? false;
19
+ this.required = options.required ?? false;
20
+ this.keepTogether = options.keepTogether ?? true;
21
+ }
22
+ addItemReference(itemIdentifier, href) {
23
+ this.itemReferences.push({ itemIdentifier, href });
24
+ }
25
+ getItemReferences() {
26
+ return this.itemReferences;
27
+ }
28
+ }
29
+ exports.QtiAssessmentSection = QtiAssessmentSection;
@@ -2,10 +2,18 @@ import { XMLBuilder } from "xmlbuilder2/lib/interfaces";
2
2
  export type NamespacedElementContent = string | {
3
3
  [key: string]: NamespacedElementContent;
4
4
  } | NamespacedElementContent[];
5
+ export type NamespacedElement = {
6
+ namespace: string;
7
+ elementName: string;
8
+ content: NamespacedElementContent;
9
+ attributes?: Record<string, string | undefined>;
10
+ };
5
11
  export declare abstract class QtiElement {
6
- protected abstract getRootElement(): XMLBuilder;
7
- buildXml(): string;
12
+ protected namespaces: Record<string, string>;
13
+ protected namespaceElements: NamespacedElement[];
14
+ abstract buildXml(): string;
8
15
  registerNamespace(prefix: string, uri: string): void;
9
- addNamespacedElement(namespace: string, elementName: string, content: NamespacedElementContent, attributes?: Record<string, string>): void;
16
+ addNamespacedElement(namespace: string, elementName: string, content: NamespacedElementContent, attributes?: Record<string, string | undefined>): void;
17
+ protected appendNamespacesAndElements(element: XMLBuilder): void;
10
18
  private appendContent;
11
19
  }
@@ -2,15 +2,27 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QtiElement = void 0;
4
4
  class QtiElement {
5
- buildXml() {
6
- return this.getRootElement().end({ prettyPrint: true });
7
- }
5
+ namespaces = {};
6
+ namespaceElements = [];
8
7
  registerNamespace(prefix, uri) {
9
- this.getRootElement().att(`xmlns:${prefix}`, uri);
8
+ this.namespaces[prefix] = uri;
10
9
  }
11
10
  addNamespacedElement(namespace, elementName, content, attributes) {
12
- const element = this.getRootElement().ele(`${namespace}:${elementName}`, attributes);
13
- this.appendContent(element, namespace, content);
11
+ this.namespaceElements.push({
12
+ namespace,
13
+ elementName,
14
+ content,
15
+ attributes,
16
+ });
17
+ }
18
+ appendNamespacesAndElements(element) {
19
+ for (const [prefix, uri] of Object.entries(this.namespaces)) {
20
+ element.att(`xmlns:${prefix}`, uri);
21
+ }
22
+ for (const nsElement of this.namespaceElements) {
23
+ const child = element.ele(`${nsElement.namespace}:${nsElement.elementName}`, nsElement.attributes);
24
+ this.appendContent(child, nsElement.namespace, nsElement.content);
25
+ }
14
26
  }
15
27
  appendContent(element, namespace, content) {
16
28
  if (typeof content === "string") {
@@ -1,4 +1,3 @@
1
- import { XMLBuilder } from "xmlbuilder2/lib/interfaces";
2
1
  import { QtiInteraction } from "./interactions/interaction";
3
2
  import { QtiElement } from "./qti-element";
4
3
  import { QtiTest } from "./qti-test";
@@ -25,6 +24,7 @@ export type ResponseDeclaration = {
25
24
  identifier: string;
26
25
  cardinality?: QtiCardinality;
27
26
  baseType?: QtiBaseType;
27
+ correctResponse?: string[];
28
28
  };
29
29
  export type OutcomeDeclaration = {
30
30
  identifier: string;
@@ -38,6 +38,11 @@ export declare enum ResponseProcessingTemplate {
38
38
  MapResponse = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response.xml",
39
39
  MapResponsePoint = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response_point.xml"
40
40
  }
41
+ export type ItemBodyElement = {
42
+ html: string;
43
+ } | {
44
+ interaction: QtiInteraction;
45
+ };
41
46
  export declare class QtiItem extends QtiElement {
42
47
  identifier: string;
43
48
  adaptive?: boolean;
@@ -47,19 +52,17 @@ export declare class QtiItem extends QtiElement {
47
52
  label?: string;
48
53
  toolName?: string;
49
54
  toolVersion?: string;
50
- private item;
51
- private itemBody?;
52
- private responseDeclarations;
55
+ protected responseDeclarations: Map<string, ResponseDeclaration>;
56
+ protected responseProcessing: ResponseProcessingTemplate | null;
57
+ protected outcomeDeclarations: Map<string, OutcomeDeclaration>;
58
+ protected itemBodyElements: ItemBodyElement[];
53
59
  constructor(options?: QtiItemOptions);
60
+ buildXml(): string;
54
61
  addToPackage(pkg: ImsPackage): Promise<void>;
55
62
  addToTest(test: QtiTest): void;
56
- private getOrCreateItemBody;
57
63
  addItemBodyFromHtml(html: string): void;
58
64
  addInteraction(interaction: QtiInteraction): void;
59
- addPciInteraction(interaction: PciInteraction, externalModules?: boolean): void;
60
65
  addResponseDeclaration(responseDeclaration?: ResponseDeclaration): void;
61
- addCorrectResponse(identifier: string, values: string[]): void;
62
66
  addResponseProcessing(template: ResponseProcessingTemplate): void;
63
67
  addOutcomeDeclaration(outcomeDeclaration?: OutcomeDeclaration): void;
64
- protected getRootElement(): XMLBuilder;
65
68
  }
@@ -20,9 +20,10 @@ class QtiItem extends qti_element_1.QtiElement {
20
20
  label;
21
21
  toolName;
22
22
  toolVersion;
23
- item;
24
- itemBody;
25
23
  responseDeclarations = new Map();
24
+ responseProcessing = null;
25
+ outcomeDeclarations = new Map();
26
+ itemBodyElements = [];
26
27
  constructor(options) {
27
28
  super();
28
29
  this.identifier = options?.identifier || "item-" + Date.now();
@@ -31,7 +32,11 @@ class QtiItem extends qti_element_1.QtiElement {
31
32
  this.title = options?.title;
32
33
  this.language = options?.language;
33
34
  this.label = options?.label;
34
- this.item = (0, index_js_1.create)({ version: "1.0", encoding: "UTF-8" }).ele("qti-assessment-item", {
35
+ this.toolName = options?.toolName;
36
+ this.toolVersion = options?.toolVersion;
37
+ }
38
+ buildXml() {
39
+ const item = (0, index_js_1.create)({ version: "1.0", encoding: "UTF-8" }).ele("qti-assessment-item", {
35
40
  xmlns: "http://www.imsglobal.org/xsd/imsqtiasi_v3p0",
36
41
  "xmlns:m": "http://www.w3.org/1998/Math/MathML",
37
42
  "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
@@ -46,6 +51,54 @@ class QtiItem extends qti_element_1.QtiElement {
46
51
  toolVersion: this.toolVersion || "1.0.0",
47
52
  "xml:lang": this.language,
48
53
  });
54
+ // Extensions
55
+ this.appendNamespacesAndElements(item);
56
+ // Response declaration
57
+ // Note: some implementations expect the response to be defined before the body
58
+ for (const responseDeclaration of this.responseDeclarations.values()) {
59
+ const response = item.ele("qti-response-declaration", {
60
+ identifier: responseDeclaration.identifier,
61
+ cardinality: responseDeclaration.cardinality || "single",
62
+ "base-type": responseDeclaration.baseType || "string",
63
+ });
64
+ if (responseDeclaration.correctResponse?.length) {
65
+ const correctResponse = response.ele("qti-correct-response");
66
+ for (const value of responseDeclaration.correctResponse) {
67
+ correctResponse.ele("qti-value").txt(value);
68
+ }
69
+ }
70
+ }
71
+ // Outcome declaration
72
+ for (const outcomeDeclaration of this.outcomeDeclarations.values()) {
73
+ const outcome = item.ele("qti-outcome-declaration", {
74
+ "base-type": outcomeDeclaration.baseType,
75
+ cardinality: outcomeDeclaration.cardinality,
76
+ identifier: outcomeDeclaration.identifier,
77
+ });
78
+ if (outcomeDeclaration.defaultValue !== undefined) {
79
+ outcome
80
+ .ele("qti-default-value")
81
+ .ele("qti-value")
82
+ .txt(outcomeDeclaration.defaultValue.toString());
83
+ }
84
+ }
85
+ // Item body
86
+ const itemBody = item.ele("qti-item-body");
87
+ for (const element of this.itemBodyElements) {
88
+ if ("html" in element) {
89
+ (0, html_1.appendHtmlFragment)(element.html, itemBody);
90
+ }
91
+ if ("interaction" in element) {
92
+ itemBody.import(element.interaction.getXmlBuilder());
93
+ }
94
+ }
95
+ // Response processing
96
+ if (this.responseProcessing) {
97
+ item.ele("qti-response-processing", {
98
+ template: this.responseProcessing,
99
+ });
100
+ }
101
+ return item.end({ prettyPrint: true });
49
102
  }
50
103
  async addToPackage(pkg) {
51
104
  await pkg.addResource({
@@ -61,78 +114,26 @@ class QtiItem extends qti_element_1.QtiElement {
61
114
  addToTest(test) {
62
115
  test.addItemReference(this.identifier, `item-${this.identifier}.xml`);
63
116
  }
64
- getOrCreateItemBody() {
65
- if (!this.itemBody) {
66
- this.itemBody = this.item.ele("qti-item-body");
67
- }
68
- return this.itemBody;
69
- }
70
117
  addItemBodyFromHtml(html) {
71
- const content = `${this.title ? `<h1>${this.title}</h1>` : ""}${html}`;
72
- (0, html_1.appendHtmlFragment)(content, this.getOrCreateItemBody());
118
+ this.itemBodyElements.push({ html });
73
119
  }
74
120
  addInteraction(interaction) {
75
- this.getOrCreateItemBody().import(interaction.getXmlBuilder());
76
- }
77
- addPciInteraction(interaction, externalModules = false) {
78
- const pci = this.getOrCreateItemBody().ele("qti-portable-custom-interaction", {
79
- "response-identifier": interaction.responseIdentifier,
80
- module: interaction.module,
81
- "custom-interaction-type-identifier": interaction.customInteractionTypeIdentifier,
82
- "data-examplary-settings": interaction.dataExamplarySettings,
83
- class: interaction.class,
84
- });
85
- if (externalModules) {
86
- // Use external PCI runtime and HTTP hosted module
87
- const modules = pci.ele("qti-interaction-modules");
88
- modules.ele("qti-interaction-module", {
89
- id: "examplaryPciRuntime",
90
- "primary-path": "https://unpkg.com/@examplary/pci-runtime@latest/public/dist/runtime",
91
- });
92
- modules.ele("qti-interaction-module", {
93
- id: "single-line-text",
94
- "primary-path": `https://api.examplary.ai/question-types/${interaction.module}/export/qti3-pci`,
95
- });
96
- }
97
- pci.ele("qti-interaction-markup"); // empty markup
121
+ this.itemBodyElements.push({ interaction });
98
122
  }
99
123
  addResponseDeclaration(responseDeclaration = {
100
124
  identifier: "RESPONSE",
101
125
  cardinality: "single",
102
126
  baseType: "string",
103
127
  }) {
104
- // TODO: some implementations expect the response to be defined before the body, so we should enforce order
105
- if (this.responseDeclarations.has(responseDeclaration.identifier)) {
106
- throw new Error(`Response declaration with identifier ${responseDeclaration.identifier} already exists.`);
107
- }
108
- const element = this.item.ele("qti-response-declaration", {
109
- identifier: responseDeclaration.identifier,
110
- cardinality: responseDeclaration.cardinality || "single",
111
- "base-type": responseDeclaration.baseType || "string",
112
- });
113
- this.responseDeclarations.set(responseDeclaration.identifier, {
114
- element,
115
- responseDeclaration,
116
- });
117
- }
118
- addCorrectResponse(identifier, values) {
119
- const responseDecl = this.responseDeclarations.get(identifier);
120
- if (!responseDecl) {
121
- throw new Error(`Response declaration with identifier ${identifier} does not exist.`);
122
- }
123
- if (values.length > 1 &&
124
- responseDecl.responseDeclaration.cardinality === "single") {
125
- throw new Error(`Cannot add multiple correct responses to a single cardinality response declaration (${identifier}).`);
126
- }
127
- const correctResponse = responseDecl.element.ele("qti-correct-response");
128
- for (const value of values) {
129
- correctResponse.ele("qti-value").txt(value);
128
+ if (responseDeclaration.correctResponse &&
129
+ responseDeclaration.correctResponse?.length > 1 &&
130
+ responseDeclaration.cardinality === "single") {
131
+ throw new Error(`Cannot add multiple correct responses to a single cardinality response declaration (${responseDeclaration.identifier}).`);
130
132
  }
133
+ this.responseDeclarations.set(responseDeclaration.identifier, responseDeclaration);
131
134
  }
132
135
  addResponseProcessing(template) {
133
- this.item.ele("qti-response-processing", {
134
- template,
135
- });
136
+ this.responseProcessing = template;
136
137
  }
137
138
  addOutcomeDeclaration(outcomeDeclaration = {
138
139
  identifier: "SCORE",
@@ -140,20 +141,7 @@ class QtiItem extends qti_element_1.QtiElement {
140
141
  baseType: "float",
141
142
  defaultValue: 0,
142
143
  }) {
143
- const outcome = this.item.ele("qti-outcome-declaration", {
144
- "base-type": outcomeDeclaration.baseType,
145
- cardinality: outcomeDeclaration.cardinality,
146
- identifier: outcomeDeclaration.identifier,
147
- });
148
- if (outcomeDeclaration.defaultValue !== undefined) {
149
- outcome
150
- .ele("qti-default-value")
151
- .ele("qti-value")
152
- .txt(outcomeDeclaration.defaultValue.toString());
153
- }
154
- }
155
- getRootElement() {
156
- return this.item;
144
+ this.outcomeDeclarations.set(outcomeDeclaration.identifier, outcomeDeclaration);
157
145
  }
158
146
  }
159
147
  exports.QtiItem = QtiItem;
@@ -0,0 +1,19 @@
1
+ import { QtiAssessmentSection } from "./qti-assessment-section";
2
+ export type QtiTestPartOptions = {
3
+ identifier: string;
4
+ title?: string;
5
+ class?: string;
6
+ navigationMode?: "linear" | "nonlinear";
7
+ submissionMode?: "individual" | "simultaneous";
8
+ };
9
+ export declare class QtiTestPart {
10
+ identifier: string;
11
+ title?: string;
12
+ class?: string;
13
+ navigationMode: "linear" | "nonlinear";
14
+ submissionMode: "individual" | "simultaneous";
15
+ protected sections: QtiAssessmentSection[];
16
+ constructor(options: QtiTestPartOptions);
17
+ addSection(section: QtiAssessmentSection): void;
18
+ getSections(): QtiAssessmentSection[];
19
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QtiTestPart = void 0;
4
+ class QtiTestPart {
5
+ identifier;
6
+ title;
7
+ class;
8
+ navigationMode;
9
+ submissionMode;
10
+ sections = [];
11
+ constructor(options) {
12
+ this.identifier = options.identifier;
13
+ this.title = options.title;
14
+ this.class = options.class;
15
+ this.navigationMode = options.navigationMode ?? "linear";
16
+ this.submissionMode = options.submissionMode ?? "simultaneous";
17
+ }
18
+ addSection(section) {
19
+ this.sections.push(section);
20
+ }
21
+ getSections() {
22
+ return this.sections;
23
+ }
24
+ }
25
+ exports.QtiTestPart = QtiTestPart;
@@ -1,5 +1,6 @@
1
- import { XMLBuilder } from "xmlbuilder2/lib/interfaces";
2
1
  import { QtiElement } from "./qti-element";
2
+ import { OutcomeDeclaration } from "./qti-item";
3
+ import { QtiTestPart } from "./qti-test-part";
3
4
  import { ImsPackage } from "../ims/ims-package";
4
5
  type QtiTestOptions = {
5
6
  identifier?: string;
@@ -7,6 +8,7 @@ type QtiTestOptions = {
7
8
  language?: string;
8
9
  toolName?: string;
9
10
  toolVersion?: string;
11
+ addDefaultOutcomes?: boolean;
10
12
  };
11
13
  export declare class QtiTest extends QtiElement {
12
14
  identifier: string;
@@ -14,14 +16,16 @@ export declare class QtiTest extends QtiElement {
14
16
  language?: string;
15
17
  toolName?: string;
16
18
  toolVersion?: string;
17
- private test;
18
- private part;
19
- private section;
20
- private outcomeProcessing;
19
+ protected outcomeDeclarations: Map<string, OutcomeDeclaration>;
20
+ protected testParts: QtiTestPart[];
21
21
  constructor(options?: QtiTestOptions);
22
+ buildXml(): string;
22
23
  addToPackage(pkg: ImsPackage): Promise<void>;
24
+ addTestPart(testPart: QtiTestPart): void;
25
+ /**
26
+ * Convenience method to add an item reference to the first section of the first test part.
27
+ */
23
28
  addItemReference(itemIdentifier: string, href: string): void;
24
- private addOutcomeDeclaration;
25
- protected getRootElement(): XMLBuilder;
29
+ addOutcomeDeclaration(outcomeDeclaration: OutcomeDeclaration): void;
26
30
  }
27
31
  export {};
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QtiTest = void 0;
4
4
  const index_js_1 = require("xmlbuilder2/lib/index.js");
5
+ const qti_assessment_section_1 = require("./qti-assessment-section");
5
6
  const qti_element_1 = require("./qti-element");
7
+ const qti_test_part_1 = require("./qti-test-part");
6
8
  const ims_manifest_1 = require("../ims/ims-manifest");
7
9
  class QtiTest extends qti_element_1.QtiElement {
8
10
  identifier;
@@ -10,10 +12,8 @@ class QtiTest extends qti_element_1.QtiElement {
10
12
  language;
11
13
  toolName;
12
14
  toolVersion;
13
- test;
14
- part; // currently, we only support one part per test
15
- section; // currently, we only support one section per part
16
- outcomeProcessing;
15
+ outcomeDeclarations = new Map();
16
+ testParts = [];
17
17
  constructor(options) {
18
18
  super();
19
19
  this.identifier = options?.identifier || "test-" + Date.now();
@@ -21,7 +21,23 @@ class QtiTest extends qti_element_1.QtiElement {
21
21
  this.language = options?.language;
22
22
  this.toolName = options?.toolName || "Examplary QTI Module";
23
23
  this.toolVersion = options?.toolVersion || "1.0.0";
24
- this.test = (0, index_js_1.create)({ version: "1.0", encoding: "UTF-8" }).ele("qti-assessment-test", {
24
+ if (options?.addDefaultOutcomes !== false) {
25
+ this.addOutcomeDeclaration({
26
+ identifier: "SCORE",
27
+ cardinality: "single",
28
+ baseType: "float",
29
+ defaultValue: 0,
30
+ });
31
+ this.addOutcomeDeclaration({
32
+ identifier: "MAX_SCORE",
33
+ cardinality: "single",
34
+ baseType: "float",
35
+ defaultValue: 0,
36
+ });
37
+ }
38
+ }
39
+ buildXml() {
40
+ const test = (0, index_js_1.create)({ version: "1.0", encoding: "UTF-8" }).ele("qti-assessment-test", {
25
41
  xmlns: "http://www.imsglobal.org/xsd/imsqtiasi_v3p0",
26
42
  "xmlns:m": "http://www.w3.org/1998/Math/MathML",
27
43
  "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
@@ -33,19 +49,64 @@ class QtiTest extends qti_element_1.QtiElement {
33
49
  toolVersion: this.toolVersion,
34
50
  "xml:lang": this.language,
35
51
  });
36
- this.part = this.test.ele("qti-test-part", {
37
- identifier: "TEST-PART",
38
- "navigation-mode": "linear",
39
- "submission-mode": "simultaneous",
40
- });
41
- this.section = this.part.ele("qti-assessment-section", {
42
- identifier: "TEST-SECTION",
43
- title: "Section 1",
44
- visible: "true",
45
- });
46
- this.outcomeProcessing = this.test.ele("qti-outcome-processing");
47
- this.addOutcomeDeclaration("SCORE");
48
- this.addOutcomeDeclaration("MAX_SCORE");
52
+ // Extensions
53
+ this.appendNamespacesAndElements(test);
54
+ // Outcome declarations
55
+ for (const outcomeDeclaration of this.outcomeDeclarations.values()) {
56
+ const outcome = test.ele("qti-outcome-declaration", {
57
+ "base-type": outcomeDeclaration.baseType,
58
+ cardinality: outcomeDeclaration.cardinality,
59
+ identifier: outcomeDeclaration.identifier,
60
+ });
61
+ if (outcomeDeclaration.defaultValue !== undefined) {
62
+ outcome
63
+ .ele("qti-default-value")
64
+ .ele("qti-value")
65
+ .txt(outcomeDeclaration.defaultValue.toString());
66
+ }
67
+ }
68
+ // Parts
69
+ for (const testPart of this.testParts) {
70
+ const part = test.ele("qti-test-part", {
71
+ identifier: testPart.identifier,
72
+ title: testPart.title,
73
+ "navigation-mode": testPart.navigationMode,
74
+ "submission-mode": testPart.submissionMode,
75
+ class: testPart.class,
76
+ });
77
+ // Sections
78
+ for (const section of testPart.getSections()) {
79
+ const sec = part.ele("qti-assessment-section", {
80
+ identifier: section.identifier,
81
+ title: section.title,
82
+ class: section.class,
83
+ visible: section.visible ? "true" : "false",
84
+ fixed: section.fixed ? "true" : "false",
85
+ required: section.required ? "true" : "false",
86
+ keepTogether: section.keepTogether ? "true" : "false",
87
+ });
88
+ // Item references
89
+ for (const itemRef of section.getItemReferences()) {
90
+ sec.ele("qti-assessment-item-ref", {
91
+ identifier: itemRef.itemIdentifier,
92
+ href: itemRef.href,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ // Outcome processing
98
+ const outcomeProcessing = test.ele("qti-outcome-processing");
99
+ for (const outcomeDeclaration of this.outcomeDeclarations.values()) {
100
+ outcomeProcessing
101
+ .ele("qti-set-outcome-value", {
102
+ identifier: outcomeDeclaration.identifier,
103
+ })
104
+ .ele("qti-sum")
105
+ .ele("qti-test-variables", {
106
+ "variable-identifier": outcomeDeclaration.identifier,
107
+ });
108
+ }
109
+ return test.end({ prettyPrint: true });
49
110
  }
50
111
  async addToPackage(pkg) {
51
112
  await pkg.addResource({
@@ -58,33 +119,27 @@ class QtiTest extends qti_element_1.QtiElement {
58
119
  },
59
120
  ]);
60
121
  }
61
- addItemReference(itemIdentifier, href) {
62
- this.section.ele("qti-assessment-item-ref", {
63
- identifier: itemIdentifier,
64
- href: href,
65
- });
122
+ addTestPart(testPart) {
123
+ this.testParts.push(testPart);
66
124
  }
67
- addOutcomeDeclaration(identifier) {
68
- this.test
69
- .ele("qti-outcome-declaration", {
70
- identifier,
71
- cardinality: "single",
72
- baseType: "float",
73
- })
74
- .ele("qti-default-value")
75
- .ele("qti-value")
76
- .txt("0");
77
- this.outcomeProcessing
78
- .ele("qti-set-outcome-value", {
79
- identifier,
80
- })
81
- .ele("qti-sum")
82
- .ele("qti-test-variables", {
83
- "variable-identifier": identifier,
84
- });
125
+ /**
126
+ * Convenience method to add an item reference to the first section of the first test part.
127
+ */
128
+ addItemReference(itemIdentifier, href) {
129
+ if (!this.testParts.length) {
130
+ this.addTestPart(new qti_test_part_1.QtiTestPart({ identifier: "PART-1" }));
131
+ }
132
+ if (!this.testParts[0].getSections().length) {
133
+ this.testParts[0].addSection(new qti_assessment_section_1.QtiAssessmentSection({
134
+ identifier: "SECTION-1",
135
+ title: "Section 1",
136
+ visible: true,
137
+ }));
138
+ }
139
+ this.testParts[0].getSections()[0].addItemReference(itemIdentifier, href);
85
140
  }
86
- getRootElement() {
87
- return this.test;
141
+ addOutcomeDeclaration(outcomeDeclaration) {
142
+ this.outcomeDeclarations.set(outcomeDeclaration.identifier, outcomeDeclaration);
88
143
  }
89
144
  }
90
145
  exports.QtiTest = QtiTest;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@examplary/qti",
3
3
  "description": "Utilities to generate QTI 3.0 assessment packages.",
4
4
  "packageManager": "yarn@4.5.3",
5
- "version": "1.0.1",
5
+ "version": "1.3.0",
6
6
  "main": "dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "start": "yarn build --watch",
13
- "test": "vitest run",
13
+ "test": "vitest",
14
14
  "release": "semantic-release -e semantic-release-monorepo",
15
15
  "build": "tsc"
16
16
  },