npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pie-qti/qti2-to-pie

v0.1.1

Published

QTI 2.2 to PIE transformation plugin

Readme

@pie-qti/qti2-to-pie

QTI 2.2 to PIE transformation plugin with vendor extension support.

Installation

npm install @pie-qti/qti2-to-pie

Basic Usage

Transform QTI Items

import { Qti22ToPiePlugin } from '@pie-qti/qti2-to-pie';
import { TransformEngine } from '@pie-qti/transform-core';

// Create plugin instance
const plugin = new Qti22ToPiePlugin();

// Register with transform engine
const engine = new TransformEngine();
engine.registerPlugin(plugin);

// Transform QTI item to PIE
const result = await engine.transform({
  content: qtiItemXmlString,
  format: 'qti22'
});

console.log(result.items); // PIE items

Transform QTI Assessments

Transform QTI assessmentTest (multi-item tests) to PIE assessment format:

import { Qti22ToPiePlugin } from '@pie-qti/qti2-to-pie';

const qtiAssessmentTest = `<?xml version="1.0" encoding="UTF-8"?>
<assessmentTest xmlns="http://www.imsglobal.org/xsd/imsqti_v2p2"
  identifier="final-exam"
  title="Final Exam">
  <timeLimits maxTime="3600" allowLateSubmission="false"/>
  <testPart identifier="testPart1" navigationMode="linear" submissionMode="simultaneous">
    <assessmentSection identifier="section-1" title="Section 1" visible="true" fixed="false">
      <assessmentItemRef identifier="item-1" href="items/item-1.xml" required="true" fixed="false">
        <weight identifier="SCORE" value="2.0"/>
      </assessmentItemRef>
    </assessmentSection>
  </testPart>
  <outcomeProcessing>
    <setOutcomeValue identifier="SCORE">
      <sum><testVariables variableIdentifier="SCORE"/></sum>
    </setOutcomeValue>
  </outcomeProcessing>
</assessmentTest>`;

const plugin = new Qti22ToPiePlugin();
const result = await plugin.transform({ content: qtiAssessmentTest }, { logger: console });
const pieAssessment = result.items[0].content;

// PIE assessment with all features preserved:
// - sections with itemRefs
// - outcomeProcessingXml (raw QTI scoring logic)
// - branchRule and preCondition (adaptive navigation)
// - timeLimits, weights, item session controls
console.log(pieAssessment.sections);
console.log(pieAssessment.outcomeProcessingXml);

All assessment-level features are preserved through round-trips. See @pie-qti/pie-to-qti2 Assessment Transformations for complete documentation.

Vendor Extensions

This package supports vendor-specific QTI transformations through a flexible extension system. Vendor code lives in separate packages and registers with the plugin.

Quick Start with Vendor Extensions

import { Qti22ToPiePlugin } from '@pie-qti/qti2-to-pie';
import { amplifyDetector, amplifyTransformer } from '@your-org/qti-vendor-amplify';

const plugin = new Qti22ToPiePlugin();

// Register vendor extensions
plugin.registerVendorDetector(amplifyDetector);
plugin.registerVendorTransformer(amplifyTransformer);

// Now transforms will automatically detect and handle Amplify QTI

Extension Types

The plugin supports five types of vendor extensions:

1. VendorDetector - Identify vendor-specific QTI

import type { VendorDetector } from '@pie-qti/qti2-to-pie';

const myDetector: VendorDetector = {
  name: 'my-vendor-detector',
  detect(qtiXml, parsedDoc) {
    // Check for vendor patterns
    if (qtiXml.includes('my-vendor-identifier')) {
      return {
        vendor: 'my-vendor',
        confidence: 0.95,
        version: '1.0'
      };
    }
    return null;
  }
};

plugin.registerVendorDetector(myDetector);

2. VendorTransformer - Custom transformation logic

import type { VendorTransformer } from '@pie-qti/qti2-to-pie';

const myTransformer: VendorTransformer = {
  vendor: 'my-vendor',
  canHandle(qtiXml, vendorInfo, parsedDoc) {
    return vendorInfo.vendor === 'my-vendor';
  },
  async transform(qtiXml, vendorInfo, context, parsedDoc) {
    // Custom transformation logic
    return {
      items: [pieItem],
      format: 'pie',
      metadata: { /* ... */ }
    };
  }
};

plugin.registerVendorTransformer(myTransformer);

3. AssetResolver - Load external resources

import type { AssetResolver } from '@pie-qti/qti2-to-pie';

const myAssetResolver: AssetResolver = {
  name: 'my-asset-resolver',
  canResolve(assetType, assetUrl) {
    return assetUrl.startsWith('https://my-vendor-cdn.com/');
  },
  async resolve(assetType, assetUrl, baseDir) {
    // Fetch and return asset
    const response = await fetch(assetUrl);
    const buffer = await response.arrayBuffer();
    return {
      url: assetUrl,
      buffer: Buffer.from(buffer),
      mimeType: response.headers.get('content-type') || 'application/octet-stream'
    };
  }
};

plugin.registerAssetResolver(myAssetResolver);

4. CssClassExtractor - Parse vendor CSS classes

import type { CssClassExtractor } from '@pie-qti/qti2-to-pie';

const myClassExtractor: CssClassExtractor = {
  vendor: 'my-vendor',
  extract(element) {
    const classes = element.getAttribute('class')?.split(/\s+/) || [];
    return {
      behavioral: classes.filter(c => c.startsWith('behavior-')),
      styling: classes.filter(c => c.startsWith('style-')),
      semantic: classes.filter(c => c.startsWith('semantic-')),
      unknown: classes.filter(c => !c.startsWith('behavior-') &&
                                   !c.startsWith('style-') &&
                                   !c.startsWith('semantic-'))
    };
  }
};

plugin.registerCssClassExtractor(myClassExtractor);

5. MetadataExtractor - Extract vendor metadata

import type { MetadataExtractor } from '@pie-qti/qti2-to-pie';

const myMetadataExtractor: MetadataExtractor = {
  vendor: 'my-vendor',
  extract(qtiXml, parsedDoc, vendorInfo) {
    // Extract vendor-specific metadata
    return {
      vendorId: vendorInfo.metadata?.id,
      customField: parsedDoc.getAttribute('data-custom')
    };
  }
};

plugin.registerMetadataExtractor(myMetadataExtractor);

Helper Utilities

The package provides generic utilities for common vendor customization tasks:

import {
  extractCssClasses,
  extractCustomAttributes,
  preserveVendorClasses,
  preserveVendorAttributes,
  applyBehavioralClasses
} from '@pie-qti/qti2-to-pie';

// Extract and categorize CSS classes
const classes = extractCssClasses(element);
console.log(classes.behavioral); // ['labels-none', 'input-medium']

// Extract custom attributes
const attrs = extractCustomAttributes(element, 'vendor-');
console.log(attrs.vendor); // Vendor-specific attributes
console.log(attrs.data);   // data-* attributes

// Preserve in PIE model for round-trip
preserveVendorClasses(pieModel, classes);
preserveVendorAttributes(pieModel, attrs);

// Apply behavioral classes to PIE configuration
applyBehavioralClasses(pieModel, classes.behavioral);

Complete Documentation

Supported QTI Versions

  • QTI 2.2

Supported Interaction Types

Standard QTI interactions:

  • choiceInteraction - Multiple choice
  • extendedTextInteraction - Extended text response
  • orderInteraction - Ordering/ranking
  • matchInteraction - Matching
  • textEntryInteraction - Fill in the blank
  • inlineChoiceInteraction - Inline dropdown
  • gapMatchInteraction - Drag in the blank
  • hotspotInteraction - Hotspot
  • graphicGapMatchInteraction - Image cloze
  • selectPointInteraction - Point selection
  • hottextInteraction - Selectable text
  • associateInteraction - Association (mapped to categorize)

Custom interactions (via vendor extensions):

  • Add your own by implementing VendorTransformer

Round-Trip Compatibility

This plugin is designed to work seamlessly with @pie-qti/pie-to-qti2 for bidirectional transformation:

PIE Item ──→ @pie-qti/pie-to-qti2 ──→ QTI XML ──→ @pie-qti/qti2-to-pie ──→ PIE Item

Identifier Preservation

The transformation pipeline preserves stable identifiers across round-trips:

// Original PIE item
const pieItem = {
  id: 'item-internal-456',      // Internal/working ID
  baseId: 'item-stable-123',    // Stable/public ID for round-trips
  uuid: '...',
  config: { /* ... */ }
};

// After PIE → QTI → PIE round-trip
const roundTripped = await transformPieThenQti(pieItem);
console.log(roundTripped.id);     // 'item-stable-123' (restored from baseId)
console.log(roundTripped.baseId); // 'item-stable-123' (preserved)

Key guarantees:

  • baseId is preserved bidirectionally via QTI <qti-metadata-field name="externalId">
  • When transforming QTI → PIE, baseId becomes the PIE item's id if present
  • When transforming PIE → QTI → PIE, the stable baseId is maintained
  • Vendor-specific identifiers are preserved through custom metadata

Output Format Consistency

Both transformation directions use a consistent wrapped output format:

interface TransformOutput {
  items: Array<{
    content: any;           // PIE object or QTI XML string
    format: 'pie' | 'qti22'; // Explicit format tag
  }>;
  format: 'pie' | 'qti22';   // Primary output format
  metadata: TransformMetadata;
  warnings?: TransformWarning[];
  errors?: TransformError[];
}

// QTI → PIE
const qtiToPieResult = await qti22ToPiePlugin.transform({ content: qtiXml });
console.log(qtiToPieResult.items[0].format); // 'pie'
const pieItem = qtiToPieResult.items[0].content; // PIE object

// PIE → QTI
const pieToQtiResult = await pieToQti2Plugin.transform({ content: pieItem });
console.log(pieToQtiResult.items[0].format); // 'qti22'
const qtiXml = pieToQtiResult.items[0].content; // QTI XML string

Lossless Round-Trip via PIE Extension

The plugin supports lossless round-trip transformation (PIE → QTI → PIE) via the PIE namespace extension:

import { hasPieExtension, extractPieExtension } from '@pie-qti/qti2-to-pie';

// Check if QTI has PIE extension
if (hasPieExtension(qtiXml)) {
  // Extract original PIE model
  const { sourceModel, metadata } = extractPieExtension(qtiXml);

  // Returns exact original PIE item with all properties preserved
  return { items: [{ content: sourceModel, format: 'pie' }], ... };
}

When PIE extension is present:

  • Original PIE model is extracted verbatim
  • No QTI → PIE transformation occurs
  • 100% lossless round-trip guaranteed
  • Vendor-specific configuration preserved

When PIE extension is absent:

  • Standard QTI → PIE transformation applied
  • baseId extracted from metadata
  • Best-effort semantic mapping
  • Some PIE-specific features may be approximated

Metadata Preservation

The transformation pipeline preserves metadata across round-trips:

// Metadata preserved during PIE → QTI
{
  sourceSystemId: 'pie',           // Identifies origin system
  externalId: pieItem.baseId,      // Stable identifier
  itemType: 'MC',                  // PIE item type
  // Custom vendor metadata...
}

// Extracted during QTI → PIE
const baseId = extractBaseId(qtiElement); // Reads externalId with sourceSystemId=pie
const pieItem = {
  id: baseId || qtiIdentifier,
  baseId: baseId,                   // Preserved for round-trips
  metadata: {
    searchMetaData: {
      itemType: 'MC',
      source: 'qti22'
    }
  }
};

Best Practices for Round-Trip Scenarios

  1. Always use baseId for items that need stable identifiers:

    const pieItem = {
      id: 'item-work-456',
      baseId: 'item-stable-123',  // This persists across round-trips
      // ...
    };
  2. Check for PIE extension when transforming QTI that may have originated from PIE:

    if (hasPieExtension(qtiXml)) {
      // Lossless path - original PIE preserved
    } else {
      // Lossy path - best-effort QTI → PIE transformation
    }
  3. Use consistent output format when building pipelines:

    // Access transformed content consistently
    const content = result.items[0].content;
    const format = result.items[0].format;
  4. Preserve vendor metadata for vendor-specific round-trips:

    plugin.registerMetadataExtractor({
      vendor: 'my-vendor',
      extract(qtiXml, parsedDoc, vendorInfo) {
        return { /* vendor-specific metadata */ };
      }
    });

Round-Trip Testing

The test suite includes comprehensive round-trip validation:

# Run round-trip tests
bun test tests/integration/passage-roundtrip.test.ts

# Tests verify:
# - baseId preservation
# - Identifier mapping
# - Metadata preservation
# - Passage reference integrity
# - Lossless PIE extension extraction

Architecture

QTI XML Input
    ↓
Vendor Detection (VendorDetector)
    ↓
├─ Vendor Transformer (if detected)
│   ├─ Asset Resolution (AssetResolver)
│   ├─ CSS Class Extraction (CssClassExtractor)
│   ├─ Metadata Extraction (MetadataExtractor)
│   └─ Custom Transform Logic
│
└─ Standard Transformer (fallback)
    └─ Standard QTI → PIE
    ↓
PIE Output

License

ISC