Welcome to @citolab/qti-convert, a tool for converting and transforming QTI. This package has scripts that can be executed from the command line and contains functions that can be integrated in JavaScript/TypeScript applications.
You can easily install the package using npm:
npm install @citolab/qti-convert
@citolab/qti-convert can be used directly from the command line for quick conversions. And for more advanced usage @citolab/qti-convert can be integrate within your JavaScript or TypeScript projects.
Command Line Tool.
The following commands can be run from the terminal:
npx -p=@citolab/qti-convert qti-convert-pkg yourpackage.zip
Should have a qti2.x zip file as input parameter. It will create a qti3 zip file in the same folder call yourpackage-qti3.zip
npx -p=@citolab/qti-convert qti-convert-folder yourfolder (e.g c:\users\you\qti-folder or /Users/you/qti-folder)
Should have the path to a folder as input parameter. This folder should contain the content of a qti.2x package It will convert all files inside the folder and copy the converted files to a new folder called: yourfolder-qti3
For test purposes it must sometimes be helpfull to remove large files from your qti-package. This works on both qti2x as qti3. It will create a new zip file called: {orginal-name}-stripped.zip
npx -p=@citolab/qti-convert qti-strip-media-pkg yourpackage.zip
This will remove audio and video by default. But you can specify filetype/file size yourself as well:
npx -p=@citolab/qti-convert qti-strip-media-pkg yourpackage.zip audio,.css,300kb
This will remove all audio file of any known extension, .css files and files larger than 300kb. audio,video,images are supported as type, for other files you should add the extension.
Not only the files are remove but the reference in the item/test and manifest will be removed as well. In the item and test if will be replace by an image placeholder that indicates that there is a file removed. css and xsd references will just be deleted just like references in the manifest.
npx -p=@citolab/qti-convert qti-create-assessment yourfolder (e.g c:\users\you\qti-folder or /Users/you/qti-folder)
I you have a directory with one or more items but no assessment test, this command will create a assessment test that contains all the items that are in the foldername. It will override an existing assessment that's callled test.xml.
Should have the path to a folder as input parameter. This folder should contain the content of a qti3 or qti2x package
npx -p=@citolab/qti-convert qti-create-manifest yourfolder (e.g c:\users\you\qti-folder or /Users/you/qti-folder)
This will create or update an existing manifest. It will look into the directory and search for all items, tests and resources. Also it will add the resources that are used in an item as a dependency. This folder should contain the content of a qti3 or qti2x package
This create a package.zip based on all resources in a manifest. So it you have an existing package and you want to remove some items, you can extract the package.zip, remove the manifest, re-generate a manifest using qti-create-manifest and then run this command. The resources used in only the items you deleted, wont be packaged in the new zip.
npx -p=@citolab/qti-convert qti-create-package yourpackage.zip
This create a package.zip per item, for all items in a folder. The package will be called: package_{item_title || item_identifer}.zip.
npx -p=@citolab/qti-convert qti-create-package-per-item yourpackage.zip
import { convertQti2toQti3 } from '@citolab/qti-convert/qti-convert';
const qti2Xml = '<qti-assessment-item ...>...</qti-assessment-item>';
convertQti2toQti3(qti2Xml).then(qti3Xml => {
console.log(qti3Xml);
});
The convertPackageStream function processes a zipped QTI package from a stream and converts all relevant QTI 2x files to QTI 3.0.
import { convertPackageStream } from '@citolab/qti-convert/qti-convert';
import { createReadStream, writeFileSync } from 'fs';
import * as unzipper from 'unzipper';
const inputZipStream = createReadStream('path/to/qti2.zip').pipe(unzipper.Parse({ forceStream: true }));
convertPackageStream(inputZipStream).then(outputBuffer => {
writeFileSync('path/to/qti3.zip', outputBuffer);
});
The convertPackageFile function reads a local QTI package file, converts it, and writes the converted package to a specified output file.
import { convertPackageFile } from '@citolab/qti-convert/qti-convert';
const inputFilePath = 'path/to/qti2-package.zip';
const outputFilePath = 'path/to/qti3-package.zip';
convertPackageFile(inputFilePath, outputFilePath).then(() => {
console.log('Package conversion complete!');
});
The convertPackageFolder function converts all QTI 2.x files in a directory to QTI 3.0 and saves them to an output directory.
import { convertPackageFolder } from '@citolab/qti-convert/qti-convert';
const inputFolder = 'path/to/qti2-folder';
const outputFolder = 'path/to/qti3-folder';
convertPackageFolder(inputFolder, outputFolder).then(() => {
console.log('Conversion complete!');
});
You can customize the conversion logic by providing custom conversion functions for manifest files, assessment files, and item files.
This is typically needed when a specific platform needs specific conversions.
import { convertPackageFolder } from '@citolab/qti-convert/qti-convert';
import * as cheerio from 'cheerio';
const customConvertManifest = async ($manifest: cheerio.CheerioAPI): Promise<cheerio.CheerioAPI> => {
// Base conversion:
convertManifestFile($manifest);
// Custom manifest conversion logic here
return $manifest;
};
const customConvertAssessment = async ($assessment: cheerio.CheerioAPI): Promise<cheerio.CheerioAPI> => {
// Base conversion:
if ($assessment('assessmentTest').length > 0) {
const modifiedContent = await convertQti2toQti3(cleanXMLString($assessment.xml()));
$assessment = cheerio.load(modifiedContent, { xmlMode: true, xml: true });
}
// Custom assessment conversion logic here
return $assessment;
};
const customConvertItem = async ($item: cheerio.CheerioAPI): Promise<cheerio.CheerioAPI> => {
// Base conversion:
if ($item('assessmentItem').length > 0) {
const modifiedContent = await convertQti2toQti3(cleanXMLString($item.xml()));
$item = cheerio.load(modifiedContent, { xmlMode: true, xml: true });
}
// Custom item conversion logic here
return $item;
};
convertPackageFolder(
'path/to/qti2-folder',
'path/to/qti3-folder',
customConvertManifest,
customConvertAssessment,
customConvertItem
).then(() => {
console.log('Custom conversion complete!');
});
To use the qtiTransform function, import it and pass a QTI XML string. The returned API allows you to chain various transformation methods. There are some built-in functions but you can also create your own functions and chain these
import { qtiTransform } from '@citolab/qti-convert/qti-transformer';
const qtiXml = '<qti-assessment-item ...>...</qti-assessment-item>';
const transformedXml = qtiTransform(qtiXml).stripStylesheets().objectToImg().customTypes().xml();
console.log(transformedXml);
Apply a synchronous function to the XML.
qtiTransform(xmlValue).fnCh($ => {
// Your custom synchronous transformation logic
});
Apply an asynchronous function to the XML.
await(xmlValue).fnChAsync(async $ => {
// Your custom asynchronous transformation logic
});
The build-in functions that can be chained are:
mathml(): QtiTransformAPI
: Convert MathML elements to web components.objectToVideo(): QtiTransformAPI
: Convert<object>
elements to<video>
elements..objectToAudio(): QtiTransformAPI
: Convert<object>
elements to<audio>
elements..objectToImg(): QtiTransformAPI
: Convert<object>
elements to<img>
elements.stripStylesheets(): QtiTransformAPI
: Remove all stylesheet references from the XML.changeAssetLocation(getNewUrl: (oldUrl: string) => string, srcAttribute?: string[], skipBase64 = true): QtiTransformAPI
: Helper function to change the asset location of media files. Url can be changed in the callback function. By default the following attributes are checked for references:['src', 'href', 'data', 'primary-path', 'fallback-path', 'template-location']
but that can be overriden. Also by default you won't get a callback for base64 urls.changeAssetLocationAsync(getNewUrl: (oldUrl: string) => Promise<string>, srcAttribute?: string[], skipBase64 = true): QtiTransformAPI
: Async function of changeAssetLocationconfigurePciAsync(baseUrl: string, getModuleResolutionConfig: (url: string) => Promise<ModuleResolutionConfig>): Promise<QtiTransformAPI>
: makes sure custom-interaction-type-identifier are unique per item, adds /modules/module_resolution.js and /modules/fallback_module_resolution.js to the qti-interaction-modules tag of the item qti and sets a baseUrl to be able to get the full path of the modules.upgradePci()
: The default qti3 upgrader doesn't handle pci's exported from TAO properly. This is tested only for PCI's that use the latest PCI standard and are exported to qti2.x with TAO.customTypes(): QtiTransformAPI
: Apply custom type transformations to the XML. Can be used override default web-components. E.g.<qti-choice-interaction class="type:custom">
will result in<qti-choice-interaction-custom>
so you can create your own web-component to render choice interactions.customInteraction(baseRef: string, baseItem: string)
Transforms qti-custom-interactions that contain an object tag. Object tag will be removed and attributes will be merged in the qti-custom-interactions tag.stripMaterialInfo(): QtiTransformAPI
: Remove unnecessary material information from the XMLqbCleanup(): QtiTransformAPI
: Clean-up for package created with the Quesify PlatformdepConvert(): QtiTransformAPI
: Converts qti from the Dutch Extension Profile. For now only dep-dialog to a html popover. With is basic support for these dialog.minChoicesToOne(): QtiTransformAPI
: Ensure the minimum number of choices is one.suffix(elements: string[], suffix: string)
: Add a suffix to specified elements.externalScored()
: Mark the XML as externally scored.
Other function to get the output of the transformer:
xml()
: returns the xml as stringbrower.htmlDoc()
: returns a DocumentFragment. Won't work on node.brower.xmlDoc()
: returns a XMLDocument. Won't work on node.
The @citolab/qti-convert/qti-loader module provides utilities for loading and processing QTI XML content. It includes functions to fetch IMS manifest files, retrieve assessment tests, and extract item information.
The testLoader
function fetches and processes the IMS manifest XML file from a given URI. It retrieves the assessment test and its associated items.
@citolab/qti-convert/qti-helper provides a set of helper functions for QTI padckage. E.g. removeMediaFromPackage is a helper to remove media files from a QTI-package
@citolab/qti-convert/qti-helper-node provides a set of helper functions for QTI padckages. It includes functions to recursively retrieve QTI resources, create or update IMS manifest files and generate QTI assessment tests. These only work in NodeJS and wont work in the browser.
createAssessmentTest create an assessment with all items in a folder. createOrCompleteManifest will create or update a manifest based on all resources in a folder.
import { createOrCompleteManifest, createAssessmentTest } from '@citolab/qti-convert/qti-helper';
const foldername = 'path/to/qti-folder';
async function processQtiFolder(foldername: string) {
try {
const manifest = await createOrCompleteManifest(foldername);
console.log('Manifest:', manifest);
const assessmentTest = await createAssessmentTest(foldername);
console.log('Assessment Test:', assessmentTest);
} catch (error) {
console.error('Error processing QTI folder:', error);
}
}
processQtiFolder(foldername);
Returns a list of all resources with its type.
import { getAllResourcesRecursively, QtiResource } from '@citolab/qti-convert/qti-helper';
const allResources: QtiResource[] = [];
const foldername = 'path/to/qti-folder';
getAllResourcesRecursively(allResources, foldername);
console.log(allResources);
This package is licensed under the GNU General Public License. See the LICENSE file for more information.
Contributions are welcome! Please open an issue or submit a pull request on GitHub.