@pie-players/pie-assessment-toolkit 0.3.46 → 0.3.48

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.
Files changed (86) hide show
  1. package/dist/attempt/AssessmentSession.js +2 -1
  2. package/dist/attempt/AssessmentSession.js.map +1 -1
  3. package/dist/attempt/adapters/activity-to-test-attempt-session.js +12 -8
  4. package/dist/attempt/adapters/activity-to-test-attempt-session.js.map +1 -1
  5. package/dist/components/ItemToolBar.custom-element.js +60 -21
  6. package/dist/components/PieAssessmentToolkit.custom-element.js +83 -28
  7. package/dist/components/SectionToolBar.custom-element.js +59 -21
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/policy/core/pnp-policy-inputs.js +1 -2
  11. package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
  12. package/dist/policy/core/provenance.js.map +1 -1
  13. package/dist/runtime/SectionRuntimeEngine.js +1 -1
  14. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  15. package/dist/runtime/core/cohort.js.map +1 -1
  16. package/dist/runtime/core/engine-input.js.map +1 -1
  17. package/dist/runtime/core/engine-readiness.js.map +1 -1
  18. package/dist/runtime/core/engine-resolver.js.map +1 -1
  19. package/dist/runtime/core/engine-transition.js +1 -3
  20. package/dist/runtime/core/engine-transition.js.map +1 -1
  21. package/dist/runtime/registration-events.js.map +1 -1
  22. package/dist/runtime/session-event-emitter-policy.js.map +1 -1
  23. package/dist/services/HighlightCoordinator.js +2 -1
  24. package/dist/services/HighlightCoordinator.js.map +1 -1
  25. package/dist/services/I18nService.d.ts +1 -1
  26. package/dist/services/I18nService.js.map +1 -1
  27. package/dist/services/SSMLExtractor.js.map +1 -1
  28. package/dist/services/TTSService.js +5 -2
  29. package/dist/services/TTSService.js.map +1 -1
  30. package/dist/services/ThemeProvider.js.map +1 -1
  31. package/dist/services/ToolRegistry.d.ts +1 -1
  32. package/dist/services/ToolRegistry.js.map +1 -1
  33. package/dist/services/ToolkitCoordinator.js +21 -11
  34. package/dist/services/ToolkitCoordinator.js.map +1 -1
  35. package/dist/services/createDefaultToolRegistry.js +4 -2
  36. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  37. package/dist/services/framework-error-bus.js.map +1 -1
  38. package/dist/services/framework-error.js.map +1 -1
  39. package/dist/services/interfaces.js.map +1 -1
  40. package/dist/services/section-controller-types.js.map +1 -1
  41. package/dist/services/tool-config-defaults.js +7 -1
  42. package/dist/services/tool-config-defaults.js.map +1 -1
  43. package/dist/services/tool-config-validation.js +3 -1
  44. package/dist/services/tool-config-validation.js.map +1 -1
  45. package/dist/services/tool-instance-id.js.map +1 -1
  46. package/dist/services/tool-providers/DesmosToolProvider.js.map +1 -1
  47. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
  48. package/dist/services/toolbar-items.js.map +1 -1
  49. package/dist/services/tools-config-normalizer.js +1 -2
  50. package/dist/services/tools-config-normalizer.js.map +1 -1
  51. package/dist/services/tts/browser-provider.js +60 -18
  52. package/dist/services/tts/browser-provider.js.map +1 -1
  53. package/dist/services/tts/catalog-span-alignment.js +8 -3
  54. package/dist/services/tts/catalog-span-alignment.js.map +1 -1
  55. package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -1
  56. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  57. package/dist/services/tts/generated-speech/index.js +1 -1
  58. package/dist/services/tts/generated-speech/index.js.map +1 -1
  59. package/dist/services/tts/generated-speech/math-speech-cache.js +3 -1
  60. package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -1
  61. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -1
  62. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -1
  63. package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -1
  64. package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -1
  65. package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -1
  66. package/dist/services/tts/math-aware-text-processing.js +6 -3
  67. package/dist/services/tts/math-aware-text-processing.js.map +1 -1
  68. package/dist/services/tts/math-speech.js +3 -1
  69. package/dist/services/tts/math-speech.js.map +1 -1
  70. package/dist/services/tts/visual-line-ranges.js.map +1 -1
  71. package/dist/services/tts-runtime-config.js +6 -2
  72. package/dist/services/tts-runtime-config.js.map +1 -1
  73. package/dist/tools/calculators/desmos-provider.js.map +1 -1
  74. package/dist/tools/default-tool-module-loaders.js.map +1 -1
  75. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  76. package/dist/tools/registrations/calculator.js +1 -1
  77. package/dist/tools/registrations/calculator.js.map +1 -1
  78. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  79. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  80. package/dist/tools/registrations/subject-specific-tools.js +1 -1
  81. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  82. package/dist/tools/registrations/tts.js +4 -2
  83. package/dist/tools/registrations/tts.js.map +1 -1
  84. package/dist/tools/types.js.map +1 -1
  85. package/dist/tools/variant-resolver.js.map +1 -1
  86. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"SSMLExtractor.js","sourceRoot":"","sources":["../../src/services/SSMLExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSMLExtractor\n *\n * Service for extracting embedded SSML from PIE item content and converting\n * it into QTI 3.0 accessibility catalogs.\n *\n * Authors can embed <speak> SSML tags directly in content for convenience.\n * The extractor automatically:\n * 1. Parses markup and finds all <speak> elements\n * 2. Extracts SSML content with language metadata\n * 3. Generates accessibility catalogs with unique IDs\n * 4. Cleans visual markup (removes SSML, adds data-catalog-idref)\n *\n * This enables proper pronunciation, emphasis, and pacing for TTS without\n * requiring authors to maintain separate catalog files.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tAccessibilityCatalog,\n\tConfigEntity,\n\tPieModel,\n} from \"@pie-players/pie-players-shared/types\";\n\n/**\n * Result from extracting SSML from markup\n */\ninterface MarkupExtractionResult {\n\tcatalogs: AccessibilityCatalog[];\n\tcleanedMarkup: string;\n}\n\n/**\n * Result from extracting SSML from entire item config\n */\nexport interface ExtractionResult {\n\tcatalogs: AccessibilityCatalog[];\n\tcleanedConfig: ConfigEntity;\n}\n\n/**\n * Allow-listed SSML elements (from the SSML 1.1 spec subset we forward to\n * TTS providers). Anything outside this list is unwrapped during\n * sanitization so we never leak author-supplied scripts, iframes, or\n * arbitrary HTML into the TTS pipeline.\n */\nconst SSML_ALLOWED_TAGS = new Set([\n\t\"speak\",\n\t\"break\",\n\t\"prosody\",\n\t\"emphasis\",\n\t\"say-as\",\n\t\"sub\",\n\t\"mark\",\n\t\"phoneme\",\n\t\"lang\",\n\t\"p\",\n\t\"s\",\n\t\"voice\",\n\t\"audio\",\n\t\"lexicon\",\n]);\n\n/**\n * Allow-listed attributes across SSML elements. Attribute allow-listing is\n * global (not per-tag) because the union is small and consistent across\n * providers.\n */\n/**\n * Tags whose content must be dropped entirely (rather than unwrapped to\n * preserve phrasing text). These are dangerous containers whose text\n * payload could be re-interpreted downstream.\n */\nconst SSML_DROP_FULLY_TAGS = new Set([\n\t\"script\",\n\t\"style\",\n\t\"iframe\",\n\t\"object\",\n\t\"embed\",\n\t\"foreignobject\",\n\t\"noscript\",\n\t\"template\",\n]);\n\nconst SSML_ALLOWED_ATTRS = new Set([\n\t\"alphabet\",\n\t\"ph\",\n\t\"time\",\n\t\"strength\",\n\t\"rate\",\n\t\"pitch\",\n\t\"volume\",\n\t\"name\",\n\t\"interpret-as\",\n\t\"format\",\n\t\"detail\",\n\t\"language\",\n\t\"xml:lang\",\n\t\"lang\",\n\t\"gender\",\n\t\"src\",\n]);\n\n/**\n * SSML elements that are empty in practice (no child content). Authors\n * typically write them as self-closing (`<break time=\"200ms\"/>`). HTML's\n * parser does NOT recognize them as void elements and silently drops the\n * `/`, then nests every subsequent sibling as a child — producing\n * malformed SSML like `<break>…rest of paragraph…</break>` when\n * round-tripped. Pre-processing self-closing forms to explicit\n * open+close pairs before HTML parsing avoids the reflow, and the\n * serializer re-emits them as self-closing so providers see valid SSML.\n *\n * `audio` is intentionally excluded — SSML <audio> may carry fallback\n * children (the spec's \"alternate content\"), so treating it as void\n * would lose author intent.\n */\nconst SSML_VOID_TAGS = new Set([\"break\", \"mark\", \"lexicon\"]);\n\n/**\n * Normalize self-closing forms of SSML void elements to explicit\n * open+close pairs BEFORE handing the string to an HTML parser. The\n * HTML5 parser ignores the `/` on non-void tags, so `<break/>` becomes\n * a plain `<break>` open tag that swallows every following sibling.\n * Rewriting to `<break></break>` preserves the empty-element shape\n * across the HTML round-trip.\n */\nfunction expandSelfClosingVoidTags(input: string): string {\n\tlet next = input;\n\tfor (const tag of SSML_VOID_TAGS) {\n\t\tconst selfClosing = new RegExp(`<${tag}\\\\b([^>/]*?)\\\\s*/>`, \"gi\");\n\t\tnext = next.replace(selfClosing, (_match, attrs: string) => {\n\t\t\tconst attrText = attrs ? attrs.replace(/\\s+$/, \"\") : \"\";\n\t\t\treturn `<${tag}${attrText}></${tag}>`;\n\t\t});\n\t}\n\treturn next;\n}\n\n/**\n * Hosts that must never appear inside an SSML `<audio src>` because they\n * would cause the server-side TTS provider to fetch a private / metadata\n * endpoint on behalf of the request. Mirrors the SchoolCity provider's\n * defense-in-depth check; the SSML-side block is cheap and belt-and-braces.\n */\nconst SSML_PRIVATE_AUDIO_HOSTS: RegExp[] = [\n\t/^localhost$/i,\n\t/^127(?:\\.\\d{1,3}){3}$/,\n\t/^0(?:\\.\\d{1,3}){3}$/,\n\t/^10(?:\\.\\d{1,3}){3}$/,\n\t/^192\\.168(?:\\.\\d{1,3}){2}$/,\n\t/^172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2}$/,\n\t/^169\\.254(?:\\.\\d{1,3}){2}$/,\n\t/^::1$/,\n\t/^fe80:/i,\n\t/^fc00:/i,\n\t/^fd[0-9a-f]{2}:/i,\n\t/^metadata\\.google\\.internal$/i,\n\t/^metadata\\.azure\\.internal$/i,\n\t/^metadata\\.packet\\.net$/i,\n];\n\nfunction isSafeSsmlAudioSrc(raw: string): boolean {\n\tif (typeof raw !== \"string\" || raw.length === 0) return false;\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn false;\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn false;\n\t}\n\tconst hostname = parsed.hostname.replace(/^\\[|\\]$/g, \"\").replace(/\\.$/, \"\");\n\treturn !SSML_PRIVATE_AUDIO_HOSTS.some((pattern) => pattern.test(hostname));\n}\n\n/**\n * Sanitize an SSML subtree in place: remove disallowed attributes and\n * replace disallowed elements with their text content (so phrasing order\n * is preserved but no scripts / iframes / unknown tags survive).\n */\nexport function sanitizeSsmlElement(element: Element): void {\n\t// Walk children snapshot first because we may replace nodes as we go.\n\tconst children = Array.from(element.children);\n\tfor (const child of children) {\n\t\tconst tagName = child.tagName.toLowerCase();\n\t\tif (SSML_DROP_FULLY_TAGS.has(tagName)) {\n\t\t\tchild.remove();\n\t\t\tcontinue;\n\t\t}\n\t\tsanitizeSsmlElement(child);\n\t\tif (!SSML_ALLOWED_TAGS.has(tagName)) {\n\t\t\tconst textNode = element.ownerDocument.createTextNode(\n\t\t\t\tchild.textContent ?? \"\",\n\t\t\t);\n\t\t\tchild.replaceWith(textNode);\n\t\t}\n\t}\n\t// Strip disallowed attributes on the surviving node.\n\tconst attrNames = Array.from(element.attributes).map((attr) => attr.name);\n\tfor (const attrName of attrNames) {\n\t\tif (!SSML_ALLOWED_ATTRS.has(attrName.toLowerCase())) {\n\t\t\telement.removeAttribute(attrName);\n\t\t}\n\t}\n\t// Belt-and-braces: even though `src` is allow-listed, SSML `<audio>`\n\t// sends its URL to a server-side TTS provider that will dereference\n\t// it. Refuse non-http(s) schemes and private / cloud-metadata hosts\n\t// at the SSML boundary so we never forward an SSRF primitive.\n\tif (element.tagName.toLowerCase() === \"audio\") {\n\t\tconst src = element.getAttribute(\"src\");\n\t\tif (src !== null && !isSafeSsmlAudioSrc(src)) {\n\t\t\telement.removeAttribute(\"src\");\n\t\t}\n\t}\n}\n\n/**\n * Serialize a sanitized `<speak>` element back to a minimal SSML string.\n * Uses a manual walker instead of `outerHTML` so that any namespace\n * quirks (e.g. `xmlns` attributes added by the HTML parser) are\n * normalised and disallowed descendants cannot reappear through quirks\n * in the serializer.\n */\nexport function serializeSsmlElement(element: Element): string {\n\tconst tagName = element.tagName.toLowerCase();\n\tconst attrs = Array.from(element.attributes)\n\t\t.filter((attr) => SSML_ALLOWED_ATTRS.has(attr.name.toLowerCase()))\n\t\t.map((attr) => ` ${attr.name}=\"${escapeXmlAttr(attr.value)}\"`)\n\t\t.join(\"\");\n\t// SSML void elements (e.g. <break>, <mark>) must be emitted as\n\t// self-closing so providers like AWS Polly accept them. Emitting\n\t// `<break></break>` parses fine on the SSML side but is wasteful;\n\t// emitting `<break>` (open without close) is invalid SSML.\n\tif (SSML_VOID_TAGS.has(tagName)) {\n\t\treturn `<${tagName}${attrs}/>`;\n\t}\n\tconst childPieces: string[] = [];\n\tfor (const node of Array.from(element.childNodes)) {\n\t\tif (node.nodeType === 3 /* text */) {\n\t\t\tchildPieces.push(escapeXmlText(node.nodeValue ?? \"\"));\n\t\t} else if (node.nodeType === 1 /* element */) {\n\t\t\tconst childElement = node as Element;\n\t\t\tconst childTag = childElement.tagName.toLowerCase();\n\t\t\tif (SSML_DROP_FULLY_TAGS.has(childTag)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (SSML_ALLOWED_TAGS.has(childTag)) {\n\t\t\t\tchildPieces.push(serializeSsmlElement(childElement));\n\t\t\t} else {\n\t\t\t\tchildPieces.push(escapeXmlText(childElement.textContent ?? \"\"));\n\t\t\t}\n\t\t}\n\t}\n\treturn `<${tagName}${attrs}>${childPieces.join(\"\")}</${tagName}>`;\n}\n\nexport function sanitizeSsmlString(input: string): string {\n\tconst trimmed = String(input || \"\").trim();\n\tif (!trimmed || !trimmed.includes(\"<\")) return input;\n\tif (typeof DOMParser === \"undefined\") return input;\n\tconst parser = new DOMParser();\n\tconst doc = parser.parseFromString(expandSelfClosingVoidTags(trimmed), \"text/html\");\n\tconst speakElement = doc.querySelector(\"speak\");\n\tif (speakElement) {\n\t\tsanitizeSsmlElement(speakElement);\n\t\treturn serializeSsmlElement(speakElement);\n\t}\n\tconst wrapper = doc.createElement(\"speak\");\n\tfor (const node of Array.from(doc.body.childNodes)) {\n\t\twrapper.appendChild(node);\n\t}\n\tsanitizeSsmlElement(wrapper);\n\treturn Array.from(wrapper.childNodes)\n\t\t.map((node) => {\n\t\t\tif (node.nodeType === 3 /* text */) {\n\t\t\t\treturn escapeXmlText(node.nodeValue ?? \"\");\n\t\t\t}\n\t\t\tif (node.nodeType === 1 /* element */) {\n\t\t\t\tconst child = node as Element;\n\t\t\t\tif (SSML_ALLOWED_TAGS.has(child.tagName.toLowerCase())) {\n\t\t\t\t\treturn serializeSsmlElement(child);\n\t\t\t\t}\n\t\t\t\treturn escapeXmlText(child.textContent ?? \"\");\n\t\t\t}\n\t\t\treturn \"\";\n\t\t})\n\t\t.join(\"\");\n}\n\nfunction escapeXmlText(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\");\n}\n\nfunction escapeXmlAttr(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\");\n}\n\n/**\n * SSMLExtractor Service\n *\n * Extracts embedded SSML from PIE content and generates accessibility catalogs.\n */\nexport class SSMLExtractor {\n\tprivate catalogCounter = 0;\n\n\t/**\n\t * Extract SSML from entire item config (markup + models)\n\t *\n\t * @param config Item configuration with potential embedded SSML\n\t * @returns Extracted catalogs and cleaned config\n\t */\n\textractFromItemConfig(config: ConfigEntity): ExtractionResult {\n\t\tconst allCatalogs: AccessibilityCatalog[] = [];\n\n\t\t// Shallow clone of config + a fresh models array. The per-model/choice\n\t\t// objects are replaced (not mutated in place) by the `.map` below, so the\n\t\t// caller's original config and its models are never mutated.\n\t\tconst cleanedConfig: ConfigEntity = {\n\t\t\t...config,\n\t\t\tmodels: config.models ? [...config.models] : [],\n\t\t};\n\n\t\t// 1. Extract from top-level markup (passages)\n\t\tif (config.markup) {\n\t\t\tconst markupResult = this.extractFromMarkup(config.markup, \"markup\");\n\t\t\tallCatalogs.push(...markupResult.catalogs);\n\t\t\tcleanedConfig.markup = markupResult.cleanedMarkup;\n\t\t}\n\n\t\t// 2. Extract from models (prompts, choices, etc.)\n\t\tif (config.models && config.models.length > 0) {\n\t\t\tcleanedConfig.models = config.models.map((model) => {\n\t\t\t\tconst cleanedModel = { ...model };\n\t\t\t\tconst modelCatalogs: AccessibilityCatalog[] = [];\n\n\t\t\t\t// Extract from prompt\n\t\t\t\tif (model.prompt && typeof model.prompt === \"string\") {\n\t\t\t\t\tconst promptResult = this.extractFromMarkup(\n\t\t\t\t\t\tmodel.prompt,\n\t\t\t\t\t\t`prompt-${model.id}`,\n\t\t\t\t\t);\n\t\t\t\t\tmodelCatalogs.push(...promptResult.catalogs);\n\t\t\t\t\tcleanedModel.prompt = promptResult.cleanedMarkup;\n\t\t\t\t}\n\n\t\t\t\t// Extract from choices\n\t\t\t\tif (model.choices && Array.isArray(model.choices)) {\n\t\t\t\t\tcleanedModel.choices = model.choices.map((choice) => {\n\t\t\t\t\t\tif (choice.label && typeof choice.label === \"string\") {\n\t\t\t\t\t\t\tconst choiceResult = this.extractFromMarkup(\n\t\t\t\t\t\t\t\tchoice.label,\n\t\t\t\t\t\t\t\t`choice-${model.id}-${choice.value}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tmodelCatalogs.push(...choiceResult.catalogs);\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...choice,\n\t\t\t\t\t\t\t\tlabel: choiceResult.cleanedMarkup,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn choice;\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tallCatalogs.push(...modelCatalogs);\n\t\t\t\treturn cleanedModel;\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tcatalogs: allCatalogs,\n\t\t\tcleanedConfig,\n\t\t};\n\t}\n\n\t/**\n\t * Extract SSML from markup string\n\t *\n\t * @param markup HTML markup potentially containing <speak> elements\n\t * @param idPrefix Prefix for generating catalog IDs\n\t * @returns Extracted catalogs and cleaned markup\n\t */\n\tprivate extractFromMarkup(\n\t\tmarkup: string,\n\t\tidPrefix: string,\n\t): MarkupExtractionResult {\n\t\tif (!markup || markup.trim() === \"\") {\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\n\t\t// Check if running in browser environment\n\t\tif (typeof window === \"undefined\" || typeof DOMParser === \"undefined\") {\n\t\t\tconsole.warn(\n\t\t\t\t\"[SSMLExtractor] DOMParser not available (SSR?), skipping extraction\",\n\t\t\t);\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\n\t\tconst catalogs: AccessibilityCatalog[] = [];\n\n\t\ttry {\n\t\t\tconst parser = new DOMParser();\n\t\t\tconst doc = parser.parseFromString(\n\t\t\t\texpandSelfClosingVoidTags(markup),\n\t\t\t\t\"text/html\",\n\t\t\t);\n\n\t\t\t// Find all <speak> elements\n\t\t\tconst speakElements = Array.from(doc.querySelectorAll(\"speak\"));\n\n\t\t\tif (speakElements.length === 0) {\n\t\t\t\t// No SSML found, return original markup\n\t\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t\t}\n\n\t\t\t// Process each <speak> element\n\t\t\tfor (const speakEl of speakElements) {\n\t\t\t\ttry {\n\t\t\t\t\t// Generate unique catalog ID\n\t\t\t\t\tconst catalogId = this.generateCatalogId(idPrefix);\n\n\t\t\t\t\t// Sanitize before serializing so the SSML forwarded to the TTS\n\t\t\t\t\t// provider only contains spec-allowed tags/attributes. This\n\t\t\t\t\t// closes an author-content -> TTS-server pipeline that would\n\t\t\t\t\t// otherwise accept arbitrary HTML.\n\t\t\t\t\tsanitizeSsmlElement(speakEl);\n\t\t\t\t\tconst ssmlContent = serializeSsmlElement(speakEl);\n\n\t\t\t\t\t// Extract language (check both xml:lang and lang attributes)\n\t\t\t\t\tconst language =\n\t\t\t\t\t\tspeakEl.getAttribute(\"xml:lang\") ||\n\t\t\t\t\t\tspeakEl.getAttribute(\"lang\") ||\n\t\t\t\t\t\t\"en-US\";\n\n\t\t\t\t\t// Get plain text for visual display\n\t\t\t\t\tconst plainText = speakEl.textContent || \"\";\n\n\t\t\t\t\t// Find parent element or create wrapper\n\t\t\t\t\tlet wrapper = speakEl.parentElement;\n\n\t\t\t\t\tif (!wrapper || wrapper.tagName === \"BODY\") {\n\t\t\t\t\t\t// SPEAK is at root level - create span wrapper\n\t\t\t\t\t\tconst span = doc.createElement(\"span\");\n\t\t\t\t\t\tspeakEl.parentNode?.insertBefore(span, speakEl);\n\t\t\t\t\t\tspan.appendChild(doc.createTextNode(plainText));\n\t\t\t\t\t\twrapper = span;\n\t\t\t\t\t\tspeakEl.remove();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Just remove <speak> - visual content should be in sibling element\n\t\t\t\t\t\tspeakEl.remove();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Tag the wrapper with the QTI-style catalog reference so the\n\t\t\t\t\t// runtime can resolve this region's spoken content. Same attribute\n\t\t\t\t\t// authored content uses (`data-catalog-idref`) — one canonical name.\n\t\t\t\t\tif (wrapper) {\n\t\t\t\t\t\twrapper.setAttribute(\"data-catalog-idref\", catalogId);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create catalog entry\n\t\t\t\t\tcatalogs.push({\n\t\t\t\t\t\tidentifier: catalogId,\n\t\t\t\t\t\tcards: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcatalog: \"spoken\",\n\t\t\t\t\t\t\t\tlanguage,\n\t\t\t\t\t\t\t\tcontent: ssmlContent,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\"[SSMLExtractor] Error processing <speak> element:\",\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t\t// Continue with other elements\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Return cleaned markup\n\t\t\tconst cleanedMarkup = doc.body.innerHTML;\n\n\t\t\treturn { catalogs, cleanedMarkup };\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[SSMLExtractor] Error parsing markup:\", error);\n\t\t\t// Return original markup if parsing fails\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\t}\n\n\t/**\n\t * Generate unique catalog ID\n\t *\n\t * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')\n\t * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')\n\t */\n\tprivate generateCatalogId(prefix: string): string {\n\t\tconst id = `auto-${prefix}-${this.catalogCounter}`;\n\t\tthis.catalogCounter++;\n\t\treturn id;\n\t}\n\n\t/**\n\t * Reset counter (useful for testing or new extraction contexts)\n\t */\n\treset(): void {\n\t\tthis.catalogCounter = 0;\n\t}\n}\n"]}
1
+ {"version":3,"file":"SSMLExtractor.js","sourceRoot":"","sources":["../../src/services/SSMLExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSMLExtractor\n *\n * Service for extracting embedded SSML from PIE item content and converting\n * it into QTI 3.0 accessibility catalogs.\n *\n * Authors can embed <speak> SSML tags directly in content for convenience.\n * The extractor automatically:\n * 1. Parses markup and finds all <speak> elements\n * 2. Extracts SSML content with language metadata\n * 3. Generates accessibility catalogs with unique IDs\n * 4. Cleans visual markup (removes SSML, adds data-catalog-idref)\n *\n * This enables proper pronunciation, emphasis, and pacing for TTS without\n * requiring authors to maintain separate catalog files.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tAccessibilityCatalog,\n\tConfigEntity,\n\tPieModel,\n} from \"@pie-players/pie-players-shared/types\";\n\n/**\n * Result from extracting SSML from markup\n */\ninterface MarkupExtractionResult {\n\tcatalogs: AccessibilityCatalog[];\n\tcleanedMarkup: string;\n}\n\n/**\n * Result from extracting SSML from entire item config\n */\nexport interface ExtractionResult {\n\tcatalogs: AccessibilityCatalog[];\n\tcleanedConfig: ConfigEntity;\n}\n\n/**\n * Allow-listed SSML elements (from the SSML 1.1 spec subset we forward to\n * TTS providers). Anything outside this list is unwrapped during\n * sanitization so we never leak author-supplied scripts, iframes, or\n * arbitrary HTML into the TTS pipeline.\n */\nconst SSML_ALLOWED_TAGS = new Set([\n\t\"speak\",\n\t\"break\",\n\t\"prosody\",\n\t\"emphasis\",\n\t\"say-as\",\n\t\"sub\",\n\t\"mark\",\n\t\"phoneme\",\n\t\"lang\",\n\t\"p\",\n\t\"s\",\n\t\"voice\",\n\t\"audio\",\n\t\"lexicon\",\n]);\n\n/**\n * Allow-listed attributes across SSML elements. Attribute allow-listing is\n * global (not per-tag) because the union is small and consistent across\n * providers.\n */\n/**\n * Tags whose content must be dropped entirely (rather than unwrapped to\n * preserve phrasing text). These are dangerous containers whose text\n * payload could be re-interpreted downstream.\n */\nconst SSML_DROP_FULLY_TAGS = new Set([\n\t\"script\",\n\t\"style\",\n\t\"iframe\",\n\t\"object\",\n\t\"embed\",\n\t\"foreignobject\",\n\t\"noscript\",\n\t\"template\",\n]);\n\nconst SSML_ALLOWED_ATTRS = new Set([\n\t\"alphabet\",\n\t\"ph\",\n\t\"time\",\n\t\"strength\",\n\t\"rate\",\n\t\"pitch\",\n\t\"volume\",\n\t\"name\",\n\t\"interpret-as\",\n\t\"format\",\n\t\"detail\",\n\t\"language\",\n\t\"xml:lang\",\n\t\"lang\",\n\t\"gender\",\n\t\"src\",\n]);\n\n/**\n * SSML elements that are empty in practice (no child content). Authors\n * typically write them as self-closing (`<break time=\"200ms\"/>`). HTML's\n * parser does NOT recognize them as void elements and silently drops the\n * `/`, then nests every subsequent sibling as a child — producing\n * malformed SSML like `<break>…rest of paragraph…</break>` when\n * round-tripped. Pre-processing self-closing forms to explicit\n * open+close pairs before HTML parsing avoids the reflow, and the\n * serializer re-emits them as self-closing so providers see valid SSML.\n *\n * `audio` is intentionally excluded — SSML <audio> may carry fallback\n * children (the spec's \"alternate content\"), so treating it as void\n * would lose author intent.\n */\nconst SSML_VOID_TAGS = new Set([\"break\", \"mark\", \"lexicon\"]);\n\n/**\n * Normalize self-closing forms of SSML void elements to explicit\n * open+close pairs BEFORE handing the string to an HTML parser. The\n * HTML5 parser ignores the `/` on non-void tags, so `<break/>` becomes\n * a plain `<break>` open tag that swallows every following sibling.\n * Rewriting to `<break></break>` preserves the empty-element shape\n * across the HTML round-trip.\n */\nfunction expandSelfClosingVoidTags(input: string): string {\n\tlet next = input;\n\tfor (const tag of SSML_VOID_TAGS) {\n\t\tconst selfClosing = new RegExp(`<${tag}\\\\b([^>/]*?)\\\\s*/>`, \"gi\");\n\t\tnext = next.replace(selfClosing, (_match, attrs: string) => {\n\t\t\tconst attrText = attrs ? attrs.replace(/\\s+$/, \"\") : \"\";\n\t\t\treturn `<${tag}${attrText}></${tag}>`;\n\t\t});\n\t}\n\treturn next;\n}\n\n/**\n * Hosts that must never appear inside an SSML `<audio src>` because they\n * would cause the server-side TTS provider to fetch a private / metadata\n * endpoint on behalf of the request. Mirrors the SchoolCity provider's\n * defense-in-depth check; the SSML-side block is cheap and belt-and-braces.\n */\nconst SSML_PRIVATE_AUDIO_HOSTS: RegExp[] = [\n\t/^localhost$/i,\n\t/^127(?:\\.\\d{1,3}){3}$/,\n\t/^0(?:\\.\\d{1,3}){3}$/,\n\t/^10(?:\\.\\d{1,3}){3}$/,\n\t/^192\\.168(?:\\.\\d{1,3}){2}$/,\n\t/^172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2}$/,\n\t/^169\\.254(?:\\.\\d{1,3}){2}$/,\n\t/^::1$/,\n\t/^fe80:/i,\n\t/^fc00:/i,\n\t/^fd[0-9a-f]{2}:/i,\n\t/^metadata\\.google\\.internal$/i,\n\t/^metadata\\.azure\\.internal$/i,\n\t/^metadata\\.packet\\.net$/i,\n];\n\nfunction isSafeSsmlAudioSrc(raw: string): boolean {\n\tif (typeof raw !== \"string\" || raw.length === 0) return false;\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn false;\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn false;\n\t}\n\tconst hostname = parsed.hostname.replace(/^\\[|\\]$/g, \"\").replace(/\\.$/, \"\");\n\treturn !SSML_PRIVATE_AUDIO_HOSTS.some((pattern) => pattern.test(hostname));\n}\n\n/**\n * Sanitize an SSML subtree in place: remove disallowed attributes and\n * replace disallowed elements with their text content (so phrasing order\n * is preserved but no scripts / iframes / unknown tags survive).\n */\nexport function sanitizeSsmlElement(element: Element): void {\n\t// Walk children snapshot first because we may replace nodes as we go.\n\tconst children = Array.from(element.children);\n\tfor (const child of children) {\n\t\tconst tagName = child.tagName.toLowerCase();\n\t\tif (SSML_DROP_FULLY_TAGS.has(tagName)) {\n\t\t\tchild.remove();\n\t\t\tcontinue;\n\t\t}\n\t\tsanitizeSsmlElement(child);\n\t\tif (!SSML_ALLOWED_TAGS.has(tagName)) {\n\t\t\tconst textNode = element.ownerDocument.createTextNode(\n\t\t\t\tchild.textContent ?? \"\",\n\t\t\t);\n\t\t\tchild.replaceWith(textNode);\n\t\t}\n\t}\n\t// Strip disallowed attributes on the surviving node.\n\tconst attrNames = Array.from(element.attributes).map((attr) => attr.name);\n\tfor (const attrName of attrNames) {\n\t\tif (!SSML_ALLOWED_ATTRS.has(attrName.toLowerCase())) {\n\t\t\telement.removeAttribute(attrName);\n\t\t}\n\t}\n\t// Belt-and-braces: even though `src` is allow-listed, SSML `<audio>`\n\t// sends its URL to a server-side TTS provider that will dereference\n\t// it. Refuse non-http(s) schemes and private / cloud-metadata hosts\n\t// at the SSML boundary so we never forward an SSRF primitive.\n\tif (element.tagName.toLowerCase() === \"audio\") {\n\t\tconst src = element.getAttribute(\"src\");\n\t\tif (src !== null && !isSafeSsmlAudioSrc(src)) {\n\t\t\telement.removeAttribute(\"src\");\n\t\t}\n\t}\n}\n\n/**\n * Serialize a sanitized `<speak>` element back to a minimal SSML string.\n * Uses a manual walker instead of `outerHTML` so that any namespace\n * quirks (e.g. `xmlns` attributes added by the HTML parser) are\n * normalised and disallowed descendants cannot reappear through quirks\n * in the serializer.\n */\nexport function serializeSsmlElement(element: Element): string {\n\tconst tagName = element.tagName.toLowerCase();\n\tconst attrs = Array.from(element.attributes)\n\t\t.filter((attr) => SSML_ALLOWED_ATTRS.has(attr.name.toLowerCase()))\n\t\t.map((attr) => ` ${attr.name}=\"${escapeXmlAttr(attr.value)}\"`)\n\t\t.join(\"\");\n\t// SSML void elements (e.g. <break>, <mark>) must be emitted as\n\t// self-closing so providers like AWS Polly accept them. Emitting\n\t// `<break></break>` parses fine on the SSML side but is wasteful;\n\t// emitting `<break>` (open without close) is invalid SSML.\n\tif (SSML_VOID_TAGS.has(tagName)) {\n\t\treturn `<${tagName}${attrs}/>`;\n\t}\n\tconst childPieces: string[] = [];\n\tfor (const node of Array.from(element.childNodes)) {\n\t\tif (node.nodeType === 3 /* text */) {\n\t\t\tchildPieces.push(escapeXmlText(node.nodeValue ?? \"\"));\n\t\t} else if (node.nodeType === 1 /* element */) {\n\t\t\tconst childElement = node as Element;\n\t\t\tconst childTag = childElement.tagName.toLowerCase();\n\t\t\tif (SSML_DROP_FULLY_TAGS.has(childTag)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (SSML_ALLOWED_TAGS.has(childTag)) {\n\t\t\t\tchildPieces.push(serializeSsmlElement(childElement));\n\t\t\t} else {\n\t\t\t\tchildPieces.push(escapeXmlText(childElement.textContent ?? \"\"));\n\t\t\t}\n\t\t}\n\t}\n\treturn `<${tagName}${attrs}>${childPieces.join(\"\")}</${tagName}>`;\n}\n\nexport function sanitizeSsmlString(input: string): string {\n\tconst trimmed = String(input || \"\").trim();\n\tif (!trimmed || !trimmed.includes(\"<\")) return input;\n\tif (typeof DOMParser === \"undefined\") return input;\n\tconst parser = new DOMParser();\n\tconst doc = parser.parseFromString(\n\t\texpandSelfClosingVoidTags(trimmed),\n\t\t\"text/html\",\n\t);\n\tconst speakElement = doc.querySelector(\"speak\");\n\tif (speakElement) {\n\t\tsanitizeSsmlElement(speakElement);\n\t\treturn serializeSsmlElement(speakElement);\n\t}\n\tconst wrapper = doc.createElement(\"speak\");\n\tfor (const node of Array.from(doc.body.childNodes)) {\n\t\twrapper.appendChild(node);\n\t}\n\tsanitizeSsmlElement(wrapper);\n\treturn Array.from(wrapper.childNodes)\n\t\t.map((node) => {\n\t\t\tif (node.nodeType === 3 /* text */) {\n\t\t\t\treturn escapeXmlText(node.nodeValue ?? \"\");\n\t\t\t}\n\t\t\tif (node.nodeType === 1 /* element */) {\n\t\t\t\tconst child = node as Element;\n\t\t\t\tif (SSML_ALLOWED_TAGS.has(child.tagName.toLowerCase())) {\n\t\t\t\t\treturn serializeSsmlElement(child);\n\t\t\t\t}\n\t\t\t\treturn escapeXmlText(child.textContent ?? \"\");\n\t\t\t}\n\t\t\treturn \"\";\n\t\t})\n\t\t.join(\"\");\n}\n\nfunction escapeXmlText(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\");\n}\n\nfunction escapeXmlAttr(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\");\n}\n\n/**\n * SSMLExtractor Service\n *\n * Extracts embedded SSML from PIE content and generates accessibility catalogs.\n */\nexport class SSMLExtractor {\n\tprivate catalogCounter = 0;\n\n\t/**\n\t * Extract SSML from entire item config (markup + models)\n\t *\n\t * @param config Item configuration with potential embedded SSML\n\t * @returns Extracted catalogs and cleaned config\n\t */\n\textractFromItemConfig(config: ConfigEntity): ExtractionResult {\n\t\tconst allCatalogs: AccessibilityCatalog[] = [];\n\n\t\t// Shallow clone of config + a fresh models array. The per-model/choice\n\t\t// objects are replaced (not mutated in place) by the `.map` below, so the\n\t\t// caller's original config and its models are never mutated.\n\t\tconst cleanedConfig: ConfigEntity = {\n\t\t\t...config,\n\t\t\tmodels: config.models ? [...config.models] : [],\n\t\t};\n\n\t\t// 1. Extract from top-level markup (passages)\n\t\tif (config.markup) {\n\t\t\tconst markupResult = this.extractFromMarkup(config.markup, \"markup\");\n\t\t\tallCatalogs.push(...markupResult.catalogs);\n\t\t\tcleanedConfig.markup = markupResult.cleanedMarkup;\n\t\t}\n\n\t\t// 2. Extract from models (prompts, choices, etc.)\n\t\tif (config.models && config.models.length > 0) {\n\t\t\tcleanedConfig.models = config.models.map((model) => {\n\t\t\t\tconst cleanedModel = { ...model };\n\t\t\t\tconst modelCatalogs: AccessibilityCatalog[] = [];\n\n\t\t\t\t// Extract from prompt\n\t\t\t\tif (model.prompt && typeof model.prompt === \"string\") {\n\t\t\t\t\tconst promptResult = this.extractFromMarkup(\n\t\t\t\t\t\tmodel.prompt,\n\t\t\t\t\t\t`prompt-${model.id}`,\n\t\t\t\t\t);\n\t\t\t\t\tmodelCatalogs.push(...promptResult.catalogs);\n\t\t\t\t\tcleanedModel.prompt = promptResult.cleanedMarkup;\n\t\t\t\t}\n\n\t\t\t\t// Extract from choices\n\t\t\t\tif (model.choices && Array.isArray(model.choices)) {\n\t\t\t\t\tcleanedModel.choices = model.choices.map((choice) => {\n\t\t\t\t\t\tif (choice.label && typeof choice.label === \"string\") {\n\t\t\t\t\t\t\tconst choiceResult = this.extractFromMarkup(\n\t\t\t\t\t\t\t\tchoice.label,\n\t\t\t\t\t\t\t\t`choice-${model.id}-${choice.value}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tmodelCatalogs.push(...choiceResult.catalogs);\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...choice,\n\t\t\t\t\t\t\t\tlabel: choiceResult.cleanedMarkup,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn choice;\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tallCatalogs.push(...modelCatalogs);\n\t\t\t\treturn cleanedModel;\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tcatalogs: allCatalogs,\n\t\t\tcleanedConfig,\n\t\t};\n\t}\n\n\t/**\n\t * Extract SSML from markup string\n\t *\n\t * @param markup HTML markup potentially containing <speak> elements\n\t * @param idPrefix Prefix for generating catalog IDs\n\t * @returns Extracted catalogs and cleaned markup\n\t */\n\tprivate extractFromMarkup(\n\t\tmarkup: string,\n\t\tidPrefix: string,\n\t): MarkupExtractionResult {\n\t\tif (!markup || markup.trim() === \"\") {\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\n\t\t// Check if running in browser environment\n\t\tif (typeof window === \"undefined\" || typeof DOMParser === \"undefined\") {\n\t\t\tconsole.warn(\n\t\t\t\t\"[SSMLExtractor] DOMParser not available (SSR?), skipping extraction\",\n\t\t\t);\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\n\t\tconst catalogs: AccessibilityCatalog[] = [];\n\n\t\ttry {\n\t\t\tconst parser = new DOMParser();\n\t\t\tconst doc = parser.parseFromString(\n\t\t\t\texpandSelfClosingVoidTags(markup),\n\t\t\t\t\"text/html\",\n\t\t\t);\n\n\t\t\t// Find all <speak> elements\n\t\t\tconst speakElements = Array.from(doc.querySelectorAll(\"speak\"));\n\n\t\t\tif (speakElements.length === 0) {\n\t\t\t\t// No SSML found, return original markup\n\t\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t\t}\n\n\t\t\t// Process each <speak> element\n\t\t\tfor (const speakEl of speakElements) {\n\t\t\t\ttry {\n\t\t\t\t\t// Generate unique catalog ID\n\t\t\t\t\tconst catalogId = this.generateCatalogId(idPrefix);\n\n\t\t\t\t\t// Sanitize before serializing so the SSML forwarded to the TTS\n\t\t\t\t\t// provider only contains spec-allowed tags/attributes. This\n\t\t\t\t\t// closes an author-content -> TTS-server pipeline that would\n\t\t\t\t\t// otherwise accept arbitrary HTML.\n\t\t\t\t\tsanitizeSsmlElement(speakEl);\n\t\t\t\t\tconst ssmlContent = serializeSsmlElement(speakEl);\n\n\t\t\t\t\t// Extract language (check both xml:lang and lang attributes)\n\t\t\t\t\tconst language =\n\t\t\t\t\t\tspeakEl.getAttribute(\"xml:lang\") ||\n\t\t\t\t\t\tspeakEl.getAttribute(\"lang\") ||\n\t\t\t\t\t\t\"en-US\";\n\n\t\t\t\t\t// Get plain text for visual display\n\t\t\t\t\tconst plainText = speakEl.textContent || \"\";\n\n\t\t\t\t\t// Find parent element or create wrapper\n\t\t\t\t\tlet wrapper = speakEl.parentElement;\n\n\t\t\t\t\tif (!wrapper || wrapper.tagName === \"BODY\") {\n\t\t\t\t\t\t// SPEAK is at root level - create span wrapper\n\t\t\t\t\t\tconst span = doc.createElement(\"span\");\n\t\t\t\t\t\tspeakEl.parentNode?.insertBefore(span, speakEl);\n\t\t\t\t\t\tspan.appendChild(doc.createTextNode(plainText));\n\t\t\t\t\t\twrapper = span;\n\t\t\t\t\t\tspeakEl.remove();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Just remove <speak> - visual content should be in sibling element\n\t\t\t\t\t\tspeakEl.remove();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Tag the wrapper with the QTI-style catalog reference so the\n\t\t\t\t\t// runtime can resolve this region's spoken content. Same attribute\n\t\t\t\t\t// authored content uses (`data-catalog-idref`) — one canonical name.\n\t\t\t\t\tif (wrapper) {\n\t\t\t\t\t\twrapper.setAttribute(\"data-catalog-idref\", catalogId);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create catalog entry\n\t\t\t\t\tcatalogs.push({\n\t\t\t\t\t\tidentifier: catalogId,\n\t\t\t\t\t\tcards: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcatalog: \"spoken\",\n\t\t\t\t\t\t\t\tlanguage,\n\t\t\t\t\t\t\t\tcontent: ssmlContent,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\"[SSMLExtractor] Error processing <speak> element:\",\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t\t// Continue with other elements\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Return cleaned markup\n\t\t\tconst cleanedMarkup = doc.body.innerHTML;\n\n\t\t\treturn { catalogs, cleanedMarkup };\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[SSMLExtractor] Error parsing markup:\", error);\n\t\t\t// Return original markup if parsing fails\n\t\t\treturn { catalogs: [], cleanedMarkup: markup };\n\t\t}\n\t}\n\n\t/**\n\t * Generate unique catalog ID\n\t *\n\t * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')\n\t * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')\n\t */\n\tprivate generateCatalogId(prefix: string): string {\n\t\tconst id = `auto-${prefix}-${this.catalogCounter}`;\n\t\tthis.catalogCounter++;\n\t\treturn id;\n\t}\n\n\t/**\n\t * Reset counter (useful for testing or new extraction contexts)\n\t */\n\treset(): void {\n\t\tthis.catalogCounter = 0;\n\t}\n}\n"]}
@@ -45,7 +45,10 @@ const sameRange = (left, right) => left === right ||
45
45
  const sameRenderableHighlightTarget = (left, right) => {
46
46
  if (left === right)
47
47
  return true;
48
- if (!left || !right || left.type !== right.type || left.quality !== right.quality) {
48
+ if (!left ||
49
+ !right ||
50
+ left.type !== right.type ||
51
+ left.quality !== right.quality) {
49
52
  return false;
50
53
  }
51
54
  if (left.type === "range" && right.type === "range") {
@@ -1094,7 +1097,7 @@ export class TTSService {
1094
1097
  // emits playback chunks. The aggregate `contentToSpeak` stays plain text
1095
1098
  // (`plan.plainSpeechText`) so seek/structural-pause planning is unaffected.
1096
1099
  const playbackFormat = this.resolveGeneratedPlaybackFormat();
1097
- const { plan, containsMathMarkup, visibleText: collectedVisibleText } = await buildGeneratedSpeechFromRoot({
1100
+ const { plan, containsMathMarkup, visibleText: collectedVisibleText, } = await buildGeneratedSpeechFromRoot({
1098
1101
  contentRoot: contentElement,
1099
1102
  language,
1100
1103
  mathSpeech: this.getMathSpeechOptions(),