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

@longsightgroup/qti3-writer

v0.13.3

Published

Framework-neutral QTI 3 assessment item XML writer for authoring applications.

Downloads

2,417

Readme

@longsightgroup/qti3-writer

Framework-neutral QTI 3 assessment item XML writer for authoring applications.

This package writes QTI-shaped authoring primitives to QTI 3 XML. It does not render UI or sanitize HTML. Trusted XHTML/QTI fragments must be prepared by the calling application before they are passed to the writer.

Install

npm install @longsightgroup/qti3-writer

Use

import { qti3TrustedXmlFragment, writeQti3AssessmentItemResult } from "@longsightgroup/qti3-writer";

const result = writeQti3AssessmentItemResult({
  interactionType: "choice",
  identifier: "item-1",
  title: "Choice item",
  promptHtml: qti3TrustedXmlFragment("Choose one."),
  responseCardinality: "single",
  choices: [
    { identifier: "A", text: "Alpha" },
    { identifier: "B", text: "Beta" },
  ],
  correctResponse: ["B"],
});

if (!result.ok) {
  throw new Error(result.diagnostics.map((diagnostic) => diagnostic.message).join("; "));
}

console.log(result.xml);

Choice items accept an optional maximumScore for writer-generated point scoring. It must be a finite non-negative number. With match_correct, the complete correct response earns this value; incorrect and unanswered responses earn zero. With map_response, each correct selected choice earns an equal share of the maximum. Incorrect selections earn zero with no deduction, so selecting all choices earns the maximum. For example, a 3-point item with two correct choices awards 1.5 for one correct choice and 3 for both. This is a defined additive policy, not a penalty policy.

The point value is encoded in QTI response processing, together with a MAXSCORE default and a positive SCORE normal-maximum. Zero-point items omit normal-maximum, whose QTI contract requires a positive value, and declare MAXSCORE as zero. Metadata does not rescale scores. Hosts aggregate the already-scored values. Omitting maximumScore preserves the existing one-point match or one-point-per-correct-selection mapping.

An explicit maximum cannot replace custom modalFeedback.responseProcessingXml, reuse a MAXSCORE response/feedback outcome, or make the complete answer unreachable through selection limits. These conflicts return diagnostics and no XML. Choice feedback uses the same point rules. This API constructs new scoring; it does not rewrite imported scoring programs or configure host defaults. Converted destinations must preserve the program or disclose its loss.

Choice items can map each selected choice to modal feedback, including a feedback identifier different from the choice identifier:

const result = writeQti3AssessmentItemResult({
  interactionType: "choice",
  identifier: "item-feedback",
  title: "Choice feedback",
  responseCardinality: "single",
  choices: [
    { identifier: "A", text: "Alpha" },
    { identifier: "B", text: "Beta" },
  ],
  correctResponse: ["B"],
  feedback: {
    entries: [
      { choiceIdentifier: "B", identifier: "RIGHT", text: "Correct." },
      { choiceIdentifier: "A", identifier: "A", text: "Try again." },
    ],
  },
});

if (!result.ok) {
  console.error(result.diagnostics);
}

Feedback works with single- and multiple-response choice items. For multiple-response items, each selected choice with an entry displays its feedback, and unselected choices display none. Invalid feedback configurations return typed diagnostics. Each entry needs exactly one text or contentHtml value with visible text; accessible image alt text and dynamic printed variables count. contentHtml is a caller-supplied trusted XML fragment.

Choice and inline-choice items can instead author responseFeedback for the whole response:

responseFeedback: {
  correct: { text: "That is the complete answer." },
  incorrect: {
    contentHtml: qti3TrustedXmlFragment(
      '<p>Review <a href="https://example.org/explanation">the explanation</a>.</p>',
    ),
  },
}

Every declared response must match its correct answer to select CORRECT. An attempted response that differs selects INCORRECT; an unanswered item selects neither. This distinction is independent of scoring: a partial-credit response with an extra distractor is still incorrect, and zero-point questions retain their answer feedback conditions. The generated processing preserves scoring and clears the feedback outcome on every run. At least one explanation is required; either explanation may be omitted. Dropdown slots must all have answer keys. The single identifier outcome defaults to RESPONSE_FEEDBACK; outcomeIdentifier can name it explicitly. Content uses the same validation as modal feedback. This model cannot be combined with feedback or modalFeedback, so it cannot silently replace imported custom processing. Hosts must separately authorize feedback display and enforce release timing; authoring these explanations grants no permission to reveal them during an exam.

For any supported interaction, use modalFeedback to author item-level QTI feedback. Declare one or more identifier outcomes with single or multiple cardinality, then add entries referencing those outcomes. Entries can use showHide: "show" (the default) or "hide", an optional title, and either plain text or a trusted XHTML/QTI fragment. Set responseProcessingXml to trusted QTI rules when processing must assign feedback outcomes; these rules replace that interaction's default scoring rules, so include any required SCORE assignment. The choice-specific feedback helper and generic modalFeedback field cannot be used together. Both paths use the same content checks: blank markup is rejected, and a blank optional HTML field does not replace nonblank text. Feedback outcome names must differ from every response identifier declared by the item. Malformed XML and forbidden interactions retain their parser diagnostic codes and messages, with paths to the authored feedback entry. Custom and portable-custom items cannot provide both their own responseProcessingXml and modalFeedback.responseProcessingXml; that conflict returns a diagnostic.

Multiple-response choice items default to max-choices="0" (unlimited), whether feedback is present or absent. An explicit maxChoices value takes precedence.

The core parser retains both flattened text and structured feedback content. The player renders supported rich content, including printed variables, through its content renderer. Feedback titles are displayed and label their feedback groups. Parsing and rewriting an arbitrary source item is not a byte-for-byte XML round trip.

The stable application-facing API is writeQti3AssessmentItemResult(item). It returns typed diagnostics and should be used by production authoring systems. Use validateQti3AuthoringItem(item) when a UI or import pipeline needs diagnostics before writing XML.

writeQti3AssessmentItem(item) remains available as a convenience API for scripts and tests. It throws Qti3WriterError when writer invariants fail.

Direct builders do not require the redundant interactionType discriminant:

import { buildQti3TextEntryItem, qti3TrustedXmlFragment } from "@longsightgroup/qti3-writer";

const xml = buildQti3TextEntryItem({
  identifier: "item-2",
  title: "Text entry",
  bodyHtml: qti3TrustedXmlFragment(
    '<p>Answer: <qti-text-entry-interaction response-identifier="RESPONSE"/></p>',
  ),
  responses: [{ responseIdentifier: "RESPONSE", answers: [{ value: "deno" }] }],
});

Packages

The package writer emits item-bank QTI 3 packages as a manifest, file map, or deterministic ZIP:

import { writeQti3PackageZipResult } from "@longsightgroup/qti3-writer";

const result = writeQti3PackageZipResult({
  identifier: "package-1",
  title: "Example package",
  items: [
    {
      kind: "authoringItem",
      path: "items/item-1.xml",
      assets: [
        {
          path: "items/assets/prompt.png",
          data: new Uint8Array([137, 80, 78, 71]),
        },
      ],
      item: {
        interactionType: "choice",
        identifier: "item-1",
        title: "Choice item",
        responseCardinality: "single",
        choices: [
          { identifier: "A", text: "Alpha" },
          { identifier: "B", text: "Beta" },
        ],
        correctResponse: ["B"],
      },
    },
  ],
});

if (result.ok) {
  console.log(result.zip);
}

Use writeQti3PackageManifestResult(input) for only imsmanifest.xml, writeQti3PackageFilesResult(input) for an ordered file list containing imsmanifest.xml, item XML, and item-owned assets, and writeQti3PackageZipResult(input) for deterministic ZIP bytes. Package result APIs validate item XML through @longsightgroup/qti3-core and return typed diagnostics. Convenience APIs without Result throw Qti3WriterError.

Package writing currently targets item-bank packages. Assets are explicit: declare each asset on the item that owns the manifest resource. The writer does not rewrite URLs inside trusted item XML. @longsightgroup/qti3-core infers asset media types from file extensions when packages are parsed.

Manifests declare QTI Item Bank metadata with schema version 3.0.1 and include the required organizations element. An optional package title is stored in LOM metadata and restored by the core package importer. Content validation of a generated package does not certify the writer.

Trusted Fragments

bodyHtml, promptHtml, and rich choice content use Qti3TrustedXmlFragment. The writer escapes plain text fields and XML attributes, but it assembles trusted fragments as provided. Sanitization is the calling application's responsibility.

Support Matrix

The exported qti3WriterInteractionSupport array is the authoritative support matrix for interactions the writer can currently write and validate:

| Interaction | QTI element | Writer support | | ----------------- | ----------------------------------- | ---------------------------------------------------------------------- | | Choice | qti-choice-interaction | Writes and validates | | Order | qti-order-interaction | Writes and validates ordered cardinality and choice references | | Inline choice | qti-inline-choice-interaction | Replaces empty QTI placeholders and validates slot references | | Hottext | qti-hottext-interaction | Replaces empty QTI placeholders and validates choice references | | Gap match | qti-gap-match-interaction | Writes and validates gap choices, targets, and directed pairs | | Extended text | qti-extended-text-interaction | Writes constructed-response interactions and rubric blocks | | Upload | qti-upload-interaction | Writes file response declarations and application upload metadata | | Media | qti-media-interaction | Writes audio, video, and object media with playback metadata | | Associate | qti-associate-interaction | Writes and validates pair responses and associable choices | | Text entry | qti-text-entry-interaction | Writes declarations and validates trusted body interaction references | | Match | qti-match-interaction | Writes and validates | | Hotspot | qti-hotspot-interaction | Writes and validates accessible object metadata and references | | Graphic order | qti-graphic-order-interaction | Writes and validates object metadata, hotspots, and ordered responses | | Select point | qti-select-point-interaction | Writes and validates point responses, area mappings, and object data | | Position object | qti-position-object-interaction | Writes and validates stage/movable objects, point responses, targets | | Slider | qti-slider-interaction | Writes and validates numeric bounds, responses, mappings, presentation | | Custom | qti-custom-interaction | Writes trusted legacy custom markup and response processing fragments | | Portable custom | qti-portable-custom-interaction | Writes launch metadata, modules, trusted markup, response processing | | Drawing | qti-drawing-interaction | Writes file responses with accessible canvas object metadata | | End attempt | qti-end-attempt-interaction | Writes boolean end-attempt responses and button metadata | | Graphic associate | qti-graphic-associate-interaction | Writes and validates object metadata, hotspots, and pair responses | | Graphic gap match | qti-graphic-gap-match-interaction | Writes and validates hotspot targets |

The writer test suite round-trips every supported builder through @longsightgroup/qti3-core parsing and validateAssessmentItem() with zero diagnostics.

For order items, an explicit correctOrder must include every choice by default. Partial correct orders are accepted only when minChoices or maxChoices explicitly configures subset ordering.

Inline choice items use trusted bodyHtml with empty QTI-shaped placeholders. The writer replaces each <qti-inline-choice-interaction response-identifier="..."/> placeholder with the generated interaction for the matching slot. This keeps editor markers out of the public API while still letting applications control the surrounding inline prose. Default all_or_nothing scoring writes the slot count as the score when every inline choice is correct, so a two-slot item awards SCORE = 2. Use map_response scoring when the application needs per-slot partial credit or custom normalization.

Hottext items use trusted bodyHtml inside the generated qti-hottext-interaction. Each generated hottext choice is placed by an empty <qti-hottext identifier="..."/> placeholder in that body fragment. When maxChoices is omitted, the writer emits cardinality="multiple"; set maxChoices: 1 for single-select hottext items.

Gap match items use trusted bodyHtml inside the generated qti-gap-match-interaction. Targets are QTI-shaped <qti-gap identifier="..."/> elements in that body fragment and must match the structured targets list. Gap styling, including input-width classes, belongs on those trusted qti-gap elements. Each gap target can have at most one associated choice; repeated target identifiers in correctResponse are rejected. The writer supports text and image gap choices and defaults to map_response scoring for authoring systems that need per-pair partial credit.

Extended text items write a single qti-extended-text-interaction after optional trusted bodyHtml. The writer supports prompt, rubric, expected length/lines, min/max strings, placeholder text, pattern-mask attributes, format="plain", format="preformatted", and format="xhtml". The writer currently restricts Extended Text to cardinality="single" and base-type="string" and reports diagnostics for other response shapes. Core parsing and the player additionally support numeric values, records, and response collections.

Upload items write a single qti-upload-interaction with a cardinality="single" / base-type="file" response declaration. Application constraints such as maximum file size, allowed file types, and multiple-file UI behavior are emitted as data-max-size, data-file-types, and data-multiple attributes for runtimes that understand them. When correctResponse is provided, the writer emits a correct-response filename and the match_correct response-processing template; otherwise upload items remain manually scored/unscored.

Media items write a single qti-media-interaction with a cardinality="single" / base-type="integer" response declaration for play count. The writer supports audio, video, and object media sources, video captions, transcript companion materials, autostart/loop flags, min/max play bounds, coords, labels, dimensions, and media shared-vocabulary player controls.

Slider items write a single numeric qti-slider-interaction. The writer infers base-type as integer when bounds, step, correct response, and mapping keys are whole numbers; otherwise it uses float. Slider scoring defaults to map_response when mappings are present and match_correct when they are not.

Custom interaction items write deprecated legacy qti-custom-interaction items for applications that still need that QTI shape. The writer validates response declaration shape and XML attribute names, but it treats widget markup and custom response processing as trusted fragments supplied by the application. Portable custom items write qti-portable-custom-interaction launch metadata, optional qti-interaction-modules, and trusted qti-interaction-markup. The writer requires a custom interaction type identifier plus either a module attribute or at least one interaction module. Drawing items write a single qti-drawing-interaction with a cardinality="single" / base-type="file" response declaration and an accessible canvas object. End attempt items write a single qti-end-attempt-interaction with a cardinality="single" / base-type="boolean" response declaration and a required button title.

Graphic interactions render generated long-description markup immediately before the interaction element. Graphic order defaults correctOrder to the declared hotspot order when omitted. For graphic order items, an explicit correctOrder must include every hotspot by default. Partial correct orders are accepted only when minChoices or maxChoices explicitly configures subset ordering. Select point and position object write map_response_point scoring and require at least one area mapping target. When maxChoices is omitted, these point interactions emit cardinality="single"; set maxChoices above 1 for multi-point responses.

Graphic Gap Match requires hotspot targets, emitted as qti-associable-hotspot elements on the graphic. The writer diagnoses targetType: "inlineGap" as unsupported for this interaction. Use Gap Match for inline qti-gap targets in text.

Use itemBodyHtml to preserve surrounding item content and the position of the rendered interaction body. Supply trusted XML containing exactly one empty <qti-interaction-placeholder/>. The shared item assembler replaces that placeholder with the renderer's body; bodyHtml and promptHtml retain their existing meanings. Malformed placement templates return invalid_item_body_template diagnostics.

Use buildQti3RubricBlock({ view, use, placement, content }) to create a validated rubric fragment for an item's bodyHtml or itemBodyHtml. The extended-text rubricHtml convenience still creates a scorer/scoring rubric. Nested interactions and rubrics are rejected, including in fixed-test instructions; test instructions also reject template content. Rubric-local stylesheets and catalogs are currently unsupported. Fixed-test rubrics and test feedback are interchange only; core test execution rejects them rather than delivering a test without its instructions or feedback.

writeQti3FixedAssessmentTest accepts trusted static instructions on each part and section. Section instructions serialize as a candidate/instructions rubric inside that section, after any ordering rule and before its item references. They add no response or item reference. Use successive sections to retain instructions between question groups; shuffling stays within each section. parseQtiTestRubrics and createCandidateTestRubricDelivery preserve this owner for host delivery. The host must display the instructions when learners enter that section; the core branching executor still rejects tests containing rubrics. Invalid rubric content returns diagnostics.

Instruction-only sections can precede, separate or follow question sections. The fixed-ordering API retains those scopes with empty item-reference arrays when generating and restoring an attempt order; it does not create scored placeholder items. A test still needs at least one question reference. Hosts must display instruction-only scopes explicitly, including ending instructions; flattening only question references would omit their authored placement.

Fixed-test time limits and session controls preserved by interchange are not execution support. The core execution classifier rejects authored limits or controls that its test runtime cannot enforce.

Inline-choice items support an explicit finite non-negative maximumScore with all_or_nothing matching. Every dropdown must be answered correctly to earn that item maximum; wrong or unanswered slots score zero. Omission retains the existing one point per slot for a fully correct item. Explicit zero emits MAXSCORE zero without a positive normal-maximum attribute. Option mappings, custom modal processing and conflicting MAXSCORE identifiers return diagnostics rather than being replaced or scaled. A one-slot dropdown and single-answer choice can thus carry the same explicit matching points.