@longsightgroup/qti3-core 0.1.1 → 0.2.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
@@ -38,6 +38,7 @@ if (parsed.document) {
38
38
  - Validate item-level QTI behavior and emit structured diagnostics.
39
39
  - Score supported response-processing patterns without a DOM.
40
40
  - Serialize and restore attempt state through `qti3.attempt-state.v1`.
41
+ - Preserve QTI 3 Portable Custom Interaction metadata and opaque PCI interaction state.
41
42
  - Publish support metadata for current and deprecated item interactions.
42
43
 
43
44
  See the main repository README for the support matrix and release notes:
@@ -0,0 +1,32 @@
1
+ import type { QtiAssessmentItem, QtiCatalog, QtiCatalogFileHref, QtiCatalogHtmlContent, QtiDocument, QtiSourceLocation } from "./types.js";
2
+ export interface QtiCatalogSupportResolutionOptions {
3
+ supports?: string | readonly string[] | undefined;
4
+ languages?: string | readonly string[] | undefined;
5
+ includeDefaultFallback?: boolean | undefined;
6
+ }
7
+ export interface QtiCatalogSupportResolution {
8
+ itemIdentifier: string;
9
+ references: QtiResolvedCatalogReference[];
10
+ }
11
+ export interface QtiResolvedCatalogReference {
12
+ idref: string;
13
+ catalog?: QtiCatalog | undefined;
14
+ matches: QtiResolvedCatalogSupport[];
15
+ source?: QtiSourceLocation | undefined;
16
+ }
17
+ export interface QtiResolvedCatalogSupport {
18
+ catalogId: string;
19
+ support: string;
20
+ default: boolean;
21
+ fileHrefs: QtiCatalogFileHref[];
22
+ attributes: Record<string, string>;
23
+ cardAttributes: Record<string, string>;
24
+ catalogAttributes: Record<string, string>;
25
+ language?: string | undefined;
26
+ htmlContent?: QtiCatalogHtmlContent | undefined;
27
+ source?: QtiSourceLocation | undefined;
28
+ cardSource?: QtiSourceLocation | undefined;
29
+ catalogSource?: QtiSourceLocation | undefined;
30
+ }
31
+ export declare function createCatalogSupportResolution(model: QtiDocument | QtiAssessmentItem, options?: QtiCatalogSupportResolutionOptions): QtiCatalogSupportResolution;
32
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EAGV,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAClD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACnD,sBAAsB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,2BAA2B;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,2BAA2B,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,WAAW,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAChD,MAAM,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACvC,UAAU,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC/C;AAYD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,WAAW,GAAG,iBAAiB,EACtC,OAAO,GAAE,kCAAuC,GAC/C,2BAA2B,CAqB7B"}
@@ -0,0 +1,126 @@
1
+ export function createCatalogSupportResolution(model, options = {}) {
2
+ const item = "item" in model ? model.item : model;
3
+ const supportFilter = stringFilter(options.supports);
4
+ const languages = stringList(options.languages).map((language) => language.toLowerCase());
5
+ const catalogById = new Map(item.catalogInfo?.catalogs.map((catalog) => [catalog.id, catalog]) ?? []);
6
+ return {
7
+ itemIdentifier: item.identifier,
8
+ references: item.catalogReferences.map((reference) => {
9
+ const catalog = catalogById.get(reference.idref);
10
+ const resolved = {
11
+ idref: reference.idref,
12
+ matches: catalog ? matchingCatalogSupports(catalog, supportFilter, languages, options) : [],
13
+ };
14
+ if (catalog)
15
+ resolved.catalog = catalog;
16
+ if (reference.source)
17
+ resolved.source = reference.source;
18
+ return resolved;
19
+ }),
20
+ };
21
+ }
22
+ function matchingCatalogSupports(catalog, supportFilter, languages, options) {
23
+ return catalog.cards.flatMap((card) => {
24
+ if (supportFilter && !supportFilter.has(card.support.toLowerCase()))
25
+ return [];
26
+ return selectedCandidates(card, languages, options).map((candidate) => resolvedSupport(catalog, candidate));
27
+ });
28
+ }
29
+ function selectedCandidates(card, languages, options) {
30
+ const candidates = catalogCandidates(card);
31
+ if (languages.length === 0) {
32
+ const defaults = candidates.filter((candidate) => candidate.default);
33
+ return defaults.length > 0 ? defaults : candidates;
34
+ }
35
+ const languageMatches = candidates
36
+ .map((candidate, index) => ({
37
+ candidate,
38
+ index,
39
+ rank: languageMatchRank(candidate.language, languages),
40
+ }))
41
+ .filter((entry) => Number.isInteger(entry.rank))
42
+ .sort((a, b) => a.rank - b.rank || a.index - b.index);
43
+ if (languageMatches.length > 0)
44
+ return languageMatches.map((entry) => entry.candidate);
45
+ if (options.includeDefaultFallback === false)
46
+ return [];
47
+ const defaults = candidates.filter((candidate) => candidate.default);
48
+ return defaults.length > 0 ? defaults : candidates.filter((candidate) => !candidate.language);
49
+ }
50
+ function catalogCandidates(card) {
51
+ if (card.entries.length > 0) {
52
+ return card.entries.map((entry) => entryCandidate(card, entry));
53
+ }
54
+ return [
55
+ {
56
+ card,
57
+ default: true,
58
+ fileHrefs: card.fileHrefs,
59
+ attributes: card.attributes,
60
+ htmlContent: card.htmlContent,
61
+ source: card.source,
62
+ },
63
+ ];
64
+ }
65
+ function entryCandidate(card, entry) {
66
+ const candidate = {
67
+ card,
68
+ default: entry.default,
69
+ fileHrefs: entry.fileHrefs,
70
+ attributes: entry.attributes,
71
+ };
72
+ if (entry.language)
73
+ candidate.language = entry.language;
74
+ if (entry.htmlContent)
75
+ candidate.htmlContent = entry.htmlContent;
76
+ if (entry.source)
77
+ candidate.source = entry.source;
78
+ return candidate;
79
+ }
80
+ function resolvedSupport(catalog, candidate) {
81
+ const resolved = {
82
+ catalogId: catalog.id,
83
+ support: candidate.card.support,
84
+ default: candidate.default,
85
+ fileHrefs: candidate.fileHrefs,
86
+ attributes: candidate.attributes,
87
+ cardAttributes: candidate.card.attributes,
88
+ catalogAttributes: catalog.attributes,
89
+ };
90
+ if (candidate.language)
91
+ resolved.language = candidate.language;
92
+ if (candidate.htmlContent)
93
+ resolved.htmlContent = candidate.htmlContent;
94
+ if (candidate.source)
95
+ resolved.source = candidate.source;
96
+ if (candidate.card.source)
97
+ resolved.cardSource = candidate.card.source;
98
+ if (catalog.source)
99
+ resolved.catalogSource = catalog.source;
100
+ return resolved;
101
+ }
102
+ function languageMatchRank(language, requestedLanguages) {
103
+ if (!language)
104
+ return undefined;
105
+ const normalizedLanguage = language.toLowerCase();
106
+ const primaryLanguage = normalizedLanguage.split("-")[0] ?? normalizedLanguage;
107
+ for (const [index, requestedLanguage] of requestedLanguages.entries()) {
108
+ if (normalizedLanguage === requestedLanguage)
109
+ return index * 2;
110
+ const requestedPrimary = requestedLanguage.split("-")[0] ?? requestedLanguage;
111
+ if (primaryLanguage === requestedPrimary)
112
+ return index * 2 + 1;
113
+ }
114
+ return undefined;
115
+ }
116
+ function stringFilter(value) {
117
+ const values = stringList(value).map((entry) => entry.toLowerCase());
118
+ return values.length > 0 ? new Set(values) : undefined;
119
+ }
120
+ function stringList(value) {
121
+ if (value === undefined)
122
+ return [];
123
+ const entries = Array.isArray(value) ? value : [value];
124
+ return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
125
+ }
126
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ export { createCatalogSupportResolution, type QtiCatalogSupportResolution, type QtiCatalogSupportResolutionOptions, type QtiResolvedCatalogReference, type QtiResolvedCatalogSupport, } from "./catalog.js";
1
2
  export { parseQtiXml } from "./parser.js";
3
+ export { createTextToSpeechTraversal, parseQtiDataSsml, validateQtiDataSsmlMetadata, type QtiDataSsml, type QtiDataSsmlBreak, type QtiDataSsmlBreakStrength, type QtiDataSsmlParseResult, type QtiDataSsmlPhoneme, type QtiDataSsmlProsody, type QtiDataSsmlSayAs, type QtiDataSsmlSub, type QtiTextToSpeechSegment, type QtiTextToSpeechSegmentKind, type QtiTextToSpeechTraversal, } from "./tts.js";
2
4
  export { assertQtiAttemptStateV1, createItemSession, isQtiAttemptStateV1, visibleModalFeedback, type QtiCustomOperatorContext, type QtiCustomOperatorHandler, type QtiCustomOperatorRegistry, type QtiItemSession, type QtiItemSessionOptions, } from "./session.js";
3
5
  export { deprecatedInteractionSupport, elementSupport, getInteractionSupport, interactionNameToType, interactionSupport, processingSupport, } from "./support.js";
4
6
  export { validateAssessmentItem } from "./validation.js";
5
- export type { QtiAssessmentItem, QtiAttemptStatus, QtiAttemptStateV1, QtiChoice, QtiChoiceRole, QtiContentNode, QtiDiagnostic, QtiDocument, QtiElementSupport, QtiInteractionElementSupport, QtiInteraction, QtiInteractionType, QtiModalFeedback, QtiObjectAsset, QtiParseResult, QtiProcessingElementSupport, QtiResponseBranch, QtiScoreResult, QtiSupportStatus, QtiTemplateDeclaration, QtiTemplateBranch, QtiTemplateProcessing, QtiTemplateRule, QtiValidationResult, QtiValue, } from "./types.js";
7
+ export type { QtiAssessmentItem, QtiAttemptStatus, QtiAttemptStateV1, QtiCatalog, QtiCatalogCard, QtiCatalogCardEntry, QtiCatalogFileHref, QtiCatalogHtmlContent, QtiCatalogInfo, QtiCatalogReference, QtiChoice, QtiChoiceRole, QtiContentNode, QtiDiagnostic, QtiDocument, QtiElementSupport, QtiInteractionElementSupport, QtiInteraction, QtiInteractionType, QtiMediaSource, QtiMediaTrack, QtiModalFeedback, QtiObjectAsset, QtiParseResult, QtiProcessingElementSupport, QtiPortableCustomDefinition, QtiPortableCustomInteractionModule, QtiPortableCustomInteractionModules, QtiPortableCustomStateValue, QtiPortableCustomVariableBinding, QtiResponseBranch, QtiScoreResult, QtiSupportStatus, QtiTemplateDeclaration, QtiTemplateBranch, QtiTemplateProcessing, QtiTemplateRule, QtiValidationResult, QtiValue, } from "./types.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,4BAA4B,EAC5B,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,4BAA4B,EAC5B,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,QAAQ,GACT,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,2BAA2B,EAC3B,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,GAC9B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,4BAA4B,EAC5B,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,4BAA4B,EAC5B,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,2BAA2B,EAC3B,kCAAkC,EAClC,mCAAmC,EACnC,2BAA2B,EAC3B,gCAAgC,EAChC,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,QAAQ,GACT,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ export { createCatalogSupportResolution, } from "./catalog.js";
1
2
  export { parseQtiXml } from "./parser.js";
3
+ export { createTextToSpeechTraversal, parseQtiDataSsml, validateQtiDataSsmlMetadata, } from "./tts.js";
2
4
  export { assertQtiAttemptStateV1, createItemSession, isQtiAttemptStateV1, visibleModalFeedback, } from "./session.js";
3
5
  export { deprecatedInteractionSupport, elementSupport, getInteractionSupport, interactionNameToType, interactionSupport, processingSupport, } from "./support.js";
4
6
  export { validateAssessmentItem } from "./validation.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GAMrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,4BAA4B,EAC5B,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,GAK/B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,2BAA2B,GAY5B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GAMrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,4BAA4B,EAC5B,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAsBV,cAAc,EAcf,MAAM,YAAY,CAAC;AAiBpB,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CA0CvD"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAuBV,cAAc,EAkBf,MAAM,YAAY,CAAC;AAiBpB,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CA0CvD"}
package/dist/parser.js CHANGED
@@ -78,6 +78,7 @@ function parseAssessmentItem(node, diagnostics) {
78
78
  return {
79
79
  identifier,
80
80
  title: node.attributes.title,
81
+ language: node.attributes["xml:lang"] ?? node.attributes.lang,
81
82
  adaptive: node.attributes.adaptive === "true",
82
83
  prompt: prompt ? textContent(prompt) : undefined,
83
84
  responseDeclarations,
@@ -351,7 +352,11 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
351
352
  }
352
353
  const objectNode = interactionType === "positionObject"
353
354
  ? positionObjectInteractionObject(node)
354
- : descendants(node, (child) => child.localName === "object" || child.localName === "img")[0];
355
+ : interactionType === "media"
356
+ ? mediaInteractionObject(node)
357
+ : interactionType === "drawing"
358
+ ? drawingInteractionObject(node)
359
+ : descendants(node, (child) => child.localName === "object" || child.localName === "img")[0];
355
360
  return {
356
361
  type: interactionType ?? "custom",
357
362
  qtiName: node.localName,
@@ -359,11 +364,16 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
359
364
  responseCardinality: responseDeclaration?.cardinality,
360
365
  responseBaseType: responseDeclaration?.baseType,
361
366
  prompt: prompt ? textContent(prompt) : undefined,
367
+ promptAttributes: prompt?.attributes,
368
+ promptSource: prompt?.source,
362
369
  contextText: inlineInteractionContext(node, interactionType),
363
370
  object: parseObjectAsset(objectNode),
364
371
  positionObjectStage: interactionType === "positionObject"
365
372
  ? parseObjectAsset(positionObjectStageObject(node))
366
373
  : undefined,
374
+ portableCustom: interactionType === "portableCustom"
375
+ ? parsePortableCustomDefinition(node, diagnostics)
376
+ : undefined,
367
377
  choices: parseChoices(node),
368
378
  hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
369
379
  gapMatchSegments: interactionType === "gapMatch" || interactionType === "graphicGapMatch"
@@ -378,9 +388,147 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
378
388
  source: node.source,
379
389
  };
380
390
  }
391
+ function parsePortableCustomDefinition(node, diagnostics) {
392
+ const markup = firstChildElement(node, "qti-interaction-markup", diagnostics);
393
+ const modules = firstChildElement(node, "qti-interaction-modules", diagnostics);
394
+ return {
395
+ responseIdentifier: node.attributes["response-identifier"],
396
+ customInteractionTypeIdentifier: node.attributes["custom-interaction-type-identifier"],
397
+ module: node.attributes.module,
398
+ interactionModules: modules ? parsePortableCustomInteractionModules(modules) : undefined,
399
+ interactionMarkup: markup ? parsePortableCustomMarkupChildren(markup, diagnostics) : [],
400
+ interactionMarkupRaw: markup ? serializeXmlContent(markup) : undefined,
401
+ templateVariables: childElements(node, "qti-template-variable").map((variable) => parsePortableCustomVariableBinding(variable, "template")),
402
+ contextVariables: childElements(node, "qti-context-variable").map((variable) => parsePortableCustomVariableBinding(variable, "context")),
403
+ stylesheets: childElements(node, "qti-stylesheet").map(parseStylesheet),
404
+ catalogInfo: parseCatalogInfo(childElements(node, "qti-catalog-info")[0]),
405
+ dataAttributes: Object.fromEntries(Object.entries(node.attributes).filter(([name]) => name.startsWith("data-"))),
406
+ attributes: node.attributes,
407
+ source: node.source,
408
+ };
409
+ }
410
+ function firstChildElement(node, name, diagnostics) {
411
+ const matches = childElements(node, name);
412
+ if (matches.length > 1) {
413
+ diagnostics.push({
414
+ code: "interaction.portableCustom.child.duplicate",
415
+ severity: "error",
416
+ message: `${node.localName} allows at most one ${name} child.`,
417
+ path: matches[1]?.source?.path ?? node.source?.path,
418
+ source: matches[1]?.source ?? node.source,
419
+ });
420
+ }
421
+ return matches[0];
422
+ }
423
+ function parsePortableCustomMarkupChildren(node, diagnostics) {
424
+ const content = [];
425
+ for (const entry of node.content) {
426
+ if (typeof entry === "string") {
427
+ if (entry.length > 0)
428
+ content.push({ kind: "text", text: entry, source: node.source });
429
+ continue;
430
+ }
431
+ content.push(parsePortableCustomMarkupNode(entry, diagnostics));
432
+ }
433
+ return content;
434
+ }
435
+ function parsePortableCustomMarkupNode(node, diagnostics) {
436
+ if (isInteractionElement(node)) {
437
+ diagnostics.push({
438
+ code: "interaction.portableCustom.markupInteraction",
439
+ severity: "error",
440
+ message: `qti-interaction-markup must not contain nested QTI interaction ${node.localName}.`,
441
+ path: node.source?.path,
442
+ source: node.source,
443
+ });
444
+ }
445
+ if (node.localName === "qti-printed-variable") {
446
+ return {
447
+ kind: "printedVariable",
448
+ identifier: node.attributes.identifier ?? "",
449
+ format: node.attributes.format,
450
+ attributes: node.attributes,
451
+ source: node.source,
452
+ };
453
+ }
454
+ if (node.localName === "qti-feedback-block" || node.localName === "qti-feedback-inline") {
455
+ return {
456
+ kind: "feedback",
457
+ feedbackType: node.localName === "qti-feedback-block" ? "block" : "inline",
458
+ identifier: node.attributes.identifier ?? "",
459
+ outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
460
+ showHide: node.attributes["show-hide"] === "hide" ? "hide" : "show",
461
+ attributes: node.attributes,
462
+ children: parsePortableCustomMarkupChildren(node, diagnostics),
463
+ source: node.source,
464
+ };
465
+ }
466
+ return {
467
+ kind: "element",
468
+ qtiName: node.localName,
469
+ attributes: node.attributes,
470
+ children: parsePortableCustomMarkupChildren(node, diagnostics),
471
+ source: node.source,
472
+ };
473
+ }
474
+ function parsePortableCustomInteractionModules(node) {
475
+ return {
476
+ primaryConfiguration: node.attributes["primary-configuration"],
477
+ secondaryConfiguration: node.attributes["secondary-configuration"] ?? node.attributes["fallback-configuration"],
478
+ modules: childElements(node, "qti-interaction-module").map(parsePortableCustomInteractionModule),
479
+ attributes: node.attributes,
480
+ source: node.source,
481
+ };
482
+ }
483
+ function parsePortableCustomInteractionModule(node) {
484
+ return {
485
+ id: node.attributes.id,
486
+ primaryPath: node.attributes["primary-path"],
487
+ fallbackPath: node.attributes["fallback-path"],
488
+ attributes: node.attributes,
489
+ source: node.source,
490
+ };
491
+ }
492
+ function parsePortableCustomVariableBinding(node, kind) {
493
+ return {
494
+ kind,
495
+ identifier: node.attributes.identifier ?? node.attributes["template-identifier"],
496
+ variableIdentifier: node.attributes["variable-identifier"],
497
+ attributes: node.attributes,
498
+ source: node.source,
499
+ };
500
+ }
501
+ function serializeXmlContent(node) {
502
+ return node.content
503
+ .map((entry) => (typeof entry === "string" ? escapeXmlText(entry) : serializeXmlNode(entry)))
504
+ .join("");
505
+ }
506
+ function serializeXmlNode(node) {
507
+ const attributes = Object.entries(node.attributes)
508
+ .map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
509
+ .join("");
510
+ if (node.content.length === 0)
511
+ return `<${node.name}${attributes}/>`;
512
+ return `<${node.name}${attributes}>${serializeXmlContent(node)}</${node.name}>`;
513
+ }
514
+ function escapeXmlText(value) {
515
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
516
+ }
517
+ function escapeXmlAttribute(value) {
518
+ return escapeXmlText(value).replaceAll('"', "&quot;");
519
+ }
381
520
  function positionObjectInteractionObject(node) {
382
521
  return childElements(node).find((child) => child.localName === "object" || child.localName === "img");
383
522
  }
523
+ function mediaInteractionObject(node) {
524
+ return childElements(node).find((child) => child.localName === "audio" ||
525
+ child.localName === "video" ||
526
+ child.localName === "object" ||
527
+ child.localName === "img");
528
+ }
529
+ function drawingInteractionObject(node) {
530
+ return childElements(node).find((child) => child.localName === "object" || child.localName === "img" || child.localName === "picture");
531
+ }
384
532
  function positionObjectStageObject(node) {
385
533
  const ancestorStage = nearestAncestor(node, "qti-position-object-stage");
386
534
  const stage = ancestorStage ?? childElements(node, "qti-position-object-stage")[0];
@@ -478,17 +626,148 @@ function normalizeInlineContext(value) {
478
626
  function parseObjectAsset(node) {
479
627
  if (!node)
480
628
  return undefined;
481
- const data = node.attributes.data ?? node.attributes.src;
629
+ const pictureImage = node.localName === "picture" ? childElements(node, "img")[0] : undefined;
630
+ const data = node.attributes.data ?? node.attributes.src ?? pictureImage?.attributes.src;
631
+ const sources = parseMediaSources(node);
632
+ const tracks = parseMediaTracks(node);
633
+ const inferredSvgDimensions = inlineSvgDimensions(data);
482
634
  return {
483
635
  data,
484
- type: node.attributes.type ?? assetTypeFromData(data),
485
- width: node.attributes.width,
486
- height: node.attributes.height,
487
- text: textContent(node),
636
+ type: node.attributes.type ??
637
+ pictureImage?.attributes.type ??
638
+ assetTypeFromData(data) ??
639
+ firstSourceType(sources),
640
+ width: node.attributes.width ?? pictureImage?.attributes.width ?? inferredSvgDimensions?.width,
641
+ height: node.attributes.height ?? pictureImage?.attributes.height ?? inferredSvgDimensions?.height,
642
+ sources,
643
+ tracks,
644
+ text: textContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
488
645
  attributes: node.attributes,
489
646
  source: node.source,
490
647
  };
491
648
  }
649
+ function inlineSvgDimensions(data) {
650
+ const svgText = decodeInlineSvgData(data);
651
+ if (!svgText)
652
+ return undefined;
653
+ const tree = parseXmlTree(svgText);
654
+ const svg = tree.root;
655
+ if (!svg || svg.localName !== "svg")
656
+ return undefined;
657
+ const width = svgLength(svg.attributes.width);
658
+ const height = svgLength(svg.attributes.height);
659
+ if (width !== undefined && height !== undefined) {
660
+ return { width: formatDimension(width), height: formatDimension(height) };
661
+ }
662
+ const viewBox = svgViewBoxDimensions(svg.attributes.viewBox);
663
+ if (!viewBox)
664
+ return undefined;
665
+ const inferredWidth = width ?? viewBox.width;
666
+ const inferredHeight = height ?? viewBox.height;
667
+ if (inferredWidth <= 0 || inferredHeight <= 0)
668
+ return undefined;
669
+ return { width: formatDimension(inferredWidth), height: formatDimension(inferredHeight) };
670
+ }
671
+ function decodeInlineSvgData(data) {
672
+ if (!data)
673
+ return undefined;
674
+ const comma = data.indexOf(",");
675
+ if (comma < 0)
676
+ return undefined;
677
+ const metadata = data.slice(0, comma);
678
+ if (!/^data:image\/svg\+xml(?:[;,]|$)/i.test(metadata))
679
+ return undefined;
680
+ const payload = data.slice(comma + 1);
681
+ if (/;base64(?:[;,]|$)/i.test(metadata))
682
+ return decodeBase64Ascii(payload);
683
+ try {
684
+ return decodeURIComponent(payload);
685
+ }
686
+ catch {
687
+ return payload;
688
+ }
689
+ }
690
+ function decodeBase64Ascii(value) {
691
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
692
+ const cleaned = value.replace(/\s+/g, "").replace(/=+$/, "");
693
+ let bits = 0;
694
+ let buffer = 0;
695
+ let output = "";
696
+ for (const char of cleaned) {
697
+ const digit = alphabet.indexOf(char);
698
+ if (digit < 0)
699
+ return undefined;
700
+ buffer = (buffer << 6) | digit;
701
+ bits += 6;
702
+ if (bits < 8)
703
+ continue;
704
+ bits -= 8;
705
+ output += String.fromCharCode((buffer >> bits) & 0xff);
706
+ }
707
+ return output;
708
+ }
709
+ function svgLength(value) {
710
+ if (!value || value.trim().endsWith("%"))
711
+ return undefined;
712
+ const match = value.trim().match(/^(\d+(?:\.\d+)?|\.\d+)(?:px)?$/i);
713
+ if (!match?.[1])
714
+ return undefined;
715
+ const parsed = Number(match[1]);
716
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
717
+ }
718
+ function svgViewBoxDimensions(value) {
719
+ const parts = value
720
+ ?.trim()
721
+ .split(/[\s,]+/)
722
+ .map((part) => Number(part));
723
+ if (!parts || parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
724
+ return undefined;
725
+ }
726
+ const width = parts[2];
727
+ const height = parts[3];
728
+ if (width === undefined || height === undefined || width <= 0 || height <= 0)
729
+ return undefined;
730
+ return { width, height };
731
+ }
732
+ function formatDimension(value) {
733
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(4)));
734
+ }
735
+ function parseMediaSources(node) {
736
+ return childElements(node, "source").map((source) => {
737
+ const src = source.attributes.src ?? firstSrcsetCandidate(source.attributes.srcset);
738
+ return {
739
+ src,
740
+ type: source.attributes.type ?? assetTypeFromData(src),
741
+ attributes: source.attributes,
742
+ source: source.source,
743
+ };
744
+ });
745
+ }
746
+ function parseMediaTracks(node) {
747
+ return childElements(node, "track").map((track) => ({
748
+ kind: track.attributes.kind,
749
+ src: track.attributes.src,
750
+ srclang: track.attributes.srclang,
751
+ label: track.attributes.label,
752
+ default: track.attributes.default !== undefined,
753
+ attributes: track.attributes,
754
+ source: track.source,
755
+ }));
756
+ }
757
+ function firstSourceType(sources) {
758
+ const explicitType = sources.find((source) => source.type)?.type;
759
+ if (explicitType)
760
+ return explicitType;
761
+ return sources.find((source) => source.src)?.src
762
+ ? assetTypeFromData(sources.find((source) => source.src)?.src)
763
+ : undefined;
764
+ }
765
+ function firstSrcsetCandidate(srcset) {
766
+ return srcset
767
+ ?.split(",")
768
+ .map((candidate) => candidate.trim().split(/\s+/)[0])
769
+ .find((candidate) => candidate && candidate.length > 0);
770
+ }
492
771
  function assetTypeFromData(data) {
493
772
  if (!data)
494
773
  return undefined;
@@ -496,8 +775,16 @@ function assetTypeFromData(data) {
496
775
  return "image/svg+xml";
497
776
  if (data.startsWith("data:image/"))
498
777
  return "image/*";
778
+ if (data.startsWith("data:audio/"))
779
+ return "audio/*";
780
+ if (data.startsWith("data:video/"))
781
+ return "video/*";
499
782
  if (/\.(svg|png|jpg|jpeg|gif|webp)(?:[?#].*)?$/i.test(data))
500
783
  return "image/*";
784
+ if (/\.(aac|flac|m4a|mp3|oga|ogg|opus|wav)(?:[?#].*)?$/i.test(data))
785
+ return "audio/*";
786
+ if (/\.(m4v|mov|mp4|ogv|webm)(?:[?#].*)?$/i.test(data))
787
+ return "video/*";
501
788
  return undefined;
502
789
  }
503
790
  function parseChoices(node) {