@longsightgroup/qti3-core 0.1.2 → 0.2.1

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, QtiMediaSource, QtiMediaTrack, 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,cAAc,EACd,aAAa,EACb,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,EAuBV,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,
@@ -363,11 +364,16 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
363
364
  responseCardinality: responseDeclaration?.cardinality,
364
365
  responseBaseType: responseDeclaration?.baseType,
365
366
  prompt: prompt ? textContent(prompt) : undefined,
367
+ promptAttributes: prompt?.attributes,
368
+ promptSource: prompt?.source,
366
369
  contextText: inlineInteractionContext(node, interactionType),
367
370
  object: parseObjectAsset(objectNode),
368
371
  positionObjectStage: interactionType === "positionObject"
369
372
  ? parseObjectAsset(positionObjectStageObject(node))
370
373
  : undefined,
374
+ portableCustom: interactionType === "portableCustom"
375
+ ? parsePortableCustomDefinition(node, diagnostics)
376
+ : undefined,
371
377
  choices: parseChoices(node),
372
378
  hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
373
379
  gapMatchSegments: interactionType === "gapMatch" || interactionType === "graphicGapMatch"
@@ -382,6 +388,135 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
382
388
  source: node.source,
383
389
  };
384
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
+ }
385
520
  function positionObjectInteractionObject(node) {
386
521
  return childElements(node).find((child) => child.localName === "object" || child.localName === "img");
387
522
  }
@@ -495,21 +630,108 @@ function parseObjectAsset(node) {
495
630
  const data = node.attributes.data ?? node.attributes.src ?? pictureImage?.attributes.src;
496
631
  const sources = parseMediaSources(node);
497
632
  const tracks = parseMediaTracks(node);
633
+ const inferredSvgDimensions = inlineSvgDimensions(data);
498
634
  return {
499
635
  data,
500
636
  type: node.attributes.type ??
501
637
  pictureImage?.attributes.type ??
502
638
  assetTypeFromData(data) ??
503
639
  firstSourceType(sources),
504
- width: node.attributes.width ?? pictureImage?.attributes.width,
505
- height: node.attributes.height ?? pictureImage?.attributes.height,
640
+ width: node.attributes.width ?? pictureImage?.attributes.width ?? inferredSvgDimensions?.width,
641
+ height: node.attributes.height ?? pictureImage?.attributes.height ?? inferredSvgDimensions?.height,
506
642
  sources,
507
643
  tracks,
508
- text: textContent(node) || pictureImage?.attributes.alt || "",
644
+ text: textContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
509
645
  attributes: node.attributes,
510
646
  source: node.source,
511
647
  };
512
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
+ }
513
735
  function parseMediaSources(node) {
514
736
  return childElements(node, "source").map((source) => {
515
737
  const src = source.attributes.src ?? firstSrcsetCandidate(source.attributes.srcset);