ssml-builder-js 2.10.0 → 2.12.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/dist/core.d.mts CHANGED
@@ -188,6 +188,14 @@ interface BuildPartialSsmlOptions extends SsmlPartialContext {
188
188
  declare function buildPartialSsml(text: string, context?: SsmlPartialContext): string;
189
189
  declare function buildPartialSsml(options: BuildPartialSsmlOptions): string;
190
190
 
191
+ /**
192
+ * Splits SSML into independently synthesizable documents without breaking XML
193
+ * elements. Parent elements such as `<voice>` and `<prosody>` are copied into
194
+ * every block. Paragraph and sentence elements therefore remain the natural
195
+ * split points, while oversized text is split at word/character boundaries.
196
+ */
197
+ declare function splitSsmlDocument(ssml: string, maxLength?: number): string[];
198
+
191
199
  declare function parseSsml(xmlString: string): SsmlDocument;
192
200
 
193
201
  interface SsmlValidationError {
@@ -253,9 +261,9 @@ declare function extractSsmlTranslatableText(ssml: string, options?: ExtractSsml
253
261
  declare function fromPlainTextToSsml(text: string, options?: FromPlainTextToSsmlOptions): string;
254
262
  declare function validateSsmlStructureIntegrity(originalSsml: string, translatedSsml: string): SsmlStructureIntegrityResult;
255
263
 
256
- type SsmlDiagnosticSeverity = "error" | "warning";
264
+ type SsmlDiagnosticSeverity = "error" | "warning" | "info";
257
265
  type SsmlDiagnosticSource = "ssml-static-validator";
258
- type AzureDiagnosticCode = "azure-unknown-voice" | "azure-unsupported-style" | "azure-locale-mismatch" | "azure-unsupported-tag-for-voice" | "azure-unsupported-model-for-voice";
266
+ type AzureDiagnosticCode = "azure-unknown-voice" | "azure-unsupported-style" | "azure-locale-mismatch" | "azure-unsupported-tag-for-voice" | "azure-unsupported-model-for-voice" | "azure-preview-voice" | "azure-deprecated-voice" | "azure-preview-tag" | "azure-deprecated-tag";
259
267
  interface SsmlDiagnostic {
260
268
  code?: AzureDiagnosticCode;
261
269
  line: number;
@@ -272,6 +280,8 @@ interface AzureVoiceDefinition {
272
280
  supportedTags?: readonly string[];
273
281
  unsupportedTags?: readonly string[];
274
282
  models?: readonly string[];
283
+ regions?: readonly string[];
284
+ status?: "ga" | "preview" | "deprecated";
275
285
  }
276
286
  /** Alias retained for consumers that prefer the metadata terminology. */
277
287
  interface AzureVoiceMetadata extends AzureVoiceDefinition {
@@ -284,9 +294,17 @@ interface AzureValidationOptions {
284
294
  customVoiceDefinitions?: readonly AzureVoiceDefinition[];
285
295
  languageAliases?: Record<string, string | readonly string[]>;
286
296
  maxLength?: number;
297
+ /** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
298
+ maxXmlDepth?: number;
287
299
  /** Optional model identifier used to validate a voice's model compatibility. */
288
300
  model?: string;
289
301
  normalizeLanguage?: (lang: string) => string;
302
+ /** Overrides the built-in preview tag list for this validation run. */
303
+ previewTags?: readonly string[];
304
+ /** Adds tags that should be reported as deprecated for this validation run. */
305
+ deprecatedTags?: readonly string[];
306
+ /** Explicit tag lifecycle metadata. This takes precedence over previewTags/deprecatedTags. */
307
+ tagStatuses?: Readonly<Record<string, "ga" | "preview" | "deprecated">>;
290
308
  unknownVoicePolicy?: "error" | "warn" | "ignore";
291
309
  unsupportedStylePolicy?: "error" | "warn" | "ignore";
292
310
  validateNestedVoices?: boolean;
@@ -316,4 +334,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
316
334
  /** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
317
335
  declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
318
336
 
319
- export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
337
+ export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
package/dist/core.d.ts CHANGED
@@ -188,6 +188,14 @@ interface BuildPartialSsmlOptions extends SsmlPartialContext {
188
188
  declare function buildPartialSsml(text: string, context?: SsmlPartialContext): string;
189
189
  declare function buildPartialSsml(options: BuildPartialSsmlOptions): string;
190
190
 
191
+ /**
192
+ * Splits SSML into independently synthesizable documents without breaking XML
193
+ * elements. Parent elements such as `<voice>` and `<prosody>` are copied into
194
+ * every block. Paragraph and sentence elements therefore remain the natural
195
+ * split points, while oversized text is split at word/character boundaries.
196
+ */
197
+ declare function splitSsmlDocument(ssml: string, maxLength?: number): string[];
198
+
191
199
  declare function parseSsml(xmlString: string): SsmlDocument;
192
200
 
193
201
  interface SsmlValidationError {
@@ -253,9 +261,9 @@ declare function extractSsmlTranslatableText(ssml: string, options?: ExtractSsml
253
261
  declare function fromPlainTextToSsml(text: string, options?: FromPlainTextToSsmlOptions): string;
254
262
  declare function validateSsmlStructureIntegrity(originalSsml: string, translatedSsml: string): SsmlStructureIntegrityResult;
255
263
 
256
- type SsmlDiagnosticSeverity = "error" | "warning";
264
+ type SsmlDiagnosticSeverity = "error" | "warning" | "info";
257
265
  type SsmlDiagnosticSource = "ssml-static-validator";
258
- type AzureDiagnosticCode = "azure-unknown-voice" | "azure-unsupported-style" | "azure-locale-mismatch" | "azure-unsupported-tag-for-voice" | "azure-unsupported-model-for-voice";
266
+ type AzureDiagnosticCode = "azure-unknown-voice" | "azure-unsupported-style" | "azure-locale-mismatch" | "azure-unsupported-tag-for-voice" | "azure-unsupported-model-for-voice" | "azure-preview-voice" | "azure-deprecated-voice" | "azure-preview-tag" | "azure-deprecated-tag";
259
267
  interface SsmlDiagnostic {
260
268
  code?: AzureDiagnosticCode;
261
269
  line: number;
@@ -272,6 +280,8 @@ interface AzureVoiceDefinition {
272
280
  supportedTags?: readonly string[];
273
281
  unsupportedTags?: readonly string[];
274
282
  models?: readonly string[];
283
+ regions?: readonly string[];
284
+ status?: "ga" | "preview" | "deprecated";
275
285
  }
276
286
  /** Alias retained for consumers that prefer the metadata terminology. */
277
287
  interface AzureVoiceMetadata extends AzureVoiceDefinition {
@@ -284,9 +294,17 @@ interface AzureValidationOptions {
284
294
  customVoiceDefinitions?: readonly AzureVoiceDefinition[];
285
295
  languageAliases?: Record<string, string | readonly string[]>;
286
296
  maxLength?: number;
297
+ /** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
298
+ maxXmlDepth?: number;
287
299
  /** Optional model identifier used to validate a voice's model compatibility. */
288
300
  model?: string;
289
301
  normalizeLanguage?: (lang: string) => string;
302
+ /** Overrides the built-in preview tag list for this validation run. */
303
+ previewTags?: readonly string[];
304
+ /** Adds tags that should be reported as deprecated for this validation run. */
305
+ deprecatedTags?: readonly string[];
306
+ /** Explicit tag lifecycle metadata. This takes precedence over previewTags/deprecatedTags. */
307
+ tagStatuses?: Readonly<Record<string, "ga" | "preview" | "deprecated">>;
290
308
  unknownVoicePolicy?: "error" | "warn" | "ignore";
291
309
  unsupportedStylePolicy?: "error" | "warn" | "ignore";
292
310
  validateNestedVoices?: boolean;
@@ -316,4 +334,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
316
334
  /** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
317
335
  declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
318
336
 
319
- export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
337
+ export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
package/dist/core.js CHANGED
@@ -39,6 +39,7 @@ __export(core_exports, {
39
39
  mapSsmlTextNodes: () => mapSsmlTextNodes,
40
40
  normalizeAzureLanguage: () => normalizeAzureLanguage,
41
41
  parseSsml: () => parseSsml,
42
+ splitSsmlDocument: () => splitSsmlDocument,
42
43
  validateAzureSsml: () => validateAzureSsml,
43
44
  validateSsml: () => validateSsml,
44
45
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
@@ -972,6 +973,104 @@ function buildPartialSsml(textOrOptions, context) {
972
973
  return serializePartialSsml(textOrOptions.text, textOrOptions);
973
974
  }
974
975
 
976
+ // packages/ssml-core/src/split.ts
977
+ var DEFAULT_MAX_LENGTH = 1e4;
978
+ function cloneElement(element, children) {
979
+ return { ...element, children };
980
+ }
981
+ function documentWithChildren(document, children) {
982
+ return buildSsml({ ...document, children });
983
+ }
984
+ function wrapWithContext(node, context) {
985
+ return context.reduceRight((current, parent) => cloneElement(parent, [current]), node);
986
+ }
987
+ function documentWithNode(document, node, context) {
988
+ return documentWithChildren(document, [wrapWithContext(node, context)]);
989
+ }
990
+ function splitTextNode(text, document, context, maxLength) {
991
+ if (!text) return [text];
992
+ const parts = [];
993
+ let start = 0;
994
+ while (start < text.length) {
995
+ let end = start + 1;
996
+ let bestEnd = end;
997
+ while (end <= text.length) {
998
+ if (documentWithNode(document, text.slice(start, end), context).length > maxLength) break;
999
+ bestEnd = end;
1000
+ end += 1;
1001
+ }
1002
+ if (bestEnd === start) {
1003
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1004
+ }
1005
+ const segment = text.slice(start, bestEnd);
1006
+ const boundary = Math.max(segment.lastIndexOf(" "), segment.lastIndexOf("\n"), segment.lastIndexOf(" "));
1007
+ const splitEnd = boundary > 0 ? start + boundary + 1 : bestEnd;
1008
+ parts.push(text.slice(start, splitEnd));
1009
+ start = splitEnd;
1010
+ }
1011
+ return parts;
1012
+ }
1013
+ function splitNode(document, node, maxLength, context = []) {
1014
+ if (typeof node === "string" || node.type === "text") {
1015
+ const value = typeof node === "string" ? node : node.value;
1016
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1017
+ return splitTextNode(value, document, context, maxLength);
1018
+ }
1019
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1020
+ const children = node.children ?? [];
1021
+ if (children.length === 0) {
1022
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1023
+ }
1024
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength, [...context, node]));
1025
+ const parts = [];
1026
+ let group = [];
1027
+ const flush = () => {
1028
+ if (group.length > 0) {
1029
+ parts.push(cloneElement(node, group));
1030
+ group = [];
1031
+ }
1032
+ };
1033
+ for (const child of splitChildren) {
1034
+ const candidate = cloneElement(node, [...group, child]);
1035
+ if (documentWithNode(document, candidate, context).length <= maxLength) {
1036
+ group.push(child);
1037
+ continue;
1038
+ }
1039
+ flush();
1040
+ if (documentWithNode(document, cloneElement(node, [child]), context).length > maxLength) {
1041
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1042
+ }
1043
+ group.push(child);
1044
+ }
1045
+ flush();
1046
+ return parts;
1047
+ }
1048
+ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH) {
1049
+ if (!Number.isInteger(maxLength) || maxLength <= 0) {
1050
+ throw new RangeError("maxLength must be a positive integer");
1051
+ }
1052
+ const document = parseSsml(ssml);
1053
+ if (ssml.length <= maxLength) return [ssml];
1054
+ const children = document.children ?? [];
1055
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength));
1056
+ const chunks = [];
1057
+ let group = [];
1058
+ for (const child of splitChildren) {
1059
+ const candidate = [...group, child];
1060
+ if (documentWithChildren(document, candidate).length <= maxLength) {
1061
+ group = candidate;
1062
+ continue;
1063
+ }
1064
+ if (group.length > 0) chunks.push(group);
1065
+ group = [child];
1066
+ if (documentWithChildren(document, group).length > maxLength) {
1067
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1068
+ }
1069
+ }
1070
+ if (group.length > 0) chunks.push(group);
1071
+ return chunks.map((chunk) => documentWithChildren(document, chunk));
1072
+ }
1073
+
975
1074
  // packages/ssml-core/src/validation.ts
976
1075
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
977
1076
  function validateSsml(xmlString) {
@@ -1487,6 +1586,9 @@ var AZURE_VOICE_DEFINITIONS = [
1487
1586
  },
1488
1587
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
1489
1588
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1589
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
1590
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
1591
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
1490
1592
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1491
1593
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1492
1594
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -1583,6 +1685,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1583
1685
  "Enumerationcomma"
1584
1686
  ]);
1585
1687
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1688
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
1689
+ function featureStatusForTag(name, options) {
1690
+ const tagName = canonicalTagName(name);
1691
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
1692
+ ([candidate]) => canonicalTagName(candidate) === tagName
1693
+ )?.[1];
1694
+ if (configured) return configured;
1695
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
1696
+ return "preview";
1697
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
1698
+ return void 0;
1699
+ }
1586
1700
  function decodeAttribute(value) {
1587
1701
  return value.replace(
1588
1702
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -1651,6 +1765,7 @@ function tokenizeElements(source) {
1651
1765
  attributes,
1652
1766
  childElementIndex,
1653
1767
  end,
1768
+ depth: openElements.length + 1,
1654
1769
  name: tokenName,
1655
1770
  parentName: parent?.name,
1656
1771
  parentVoiceName,
@@ -1699,9 +1814,9 @@ function isValidAzureAudioDuration(value) {
1699
1814
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1700
1815
  }
1701
1816
  function isValidAzureBackgroundAudioDuration(value) {
1702
- const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1817
+ const match = /^(\d+)$/.exec(value.trim());
1703
1818
  if (!match) return false;
1704
- const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1819
+ const milliseconds = Number(match[1]);
1705
1820
  return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1706
1821
  }
1707
1822
  function attr(token, name) {
@@ -1841,13 +1956,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1841
1956
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1842
1957
  return;
1843
1958
  }
1959
+ if (parsed.username || parsed.password)
1960
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1844
1961
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1845
1962
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1846
1963
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1847
1964
  try {
1848
- return new URL(allowedOrigin).origin === parsed.origin;
1965
+ const configured = new URL(allowedOrigin);
1966
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
1967
+ return false;
1968
+ return configured.origin === parsed.origin;
1849
1969
  } catch {
1850
- return allowedOrigin === parsed.origin;
1970
+ return false;
1851
1971
  }
1852
1972
  }) ?? false;
1853
1973
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -1862,6 +1982,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1862
1982
  }
1863
1983
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1864
1984
  const name = token.name.toLowerCase();
1985
+ const tagStatus = featureStatusForTag(token.name, options);
1986
+ if (tagStatus === "preview")
1987
+ addDiagnostic(
1988
+ diagnostics,
1989
+ source,
1990
+ token.start,
1991
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
1992
+ "warning",
1993
+ "azure-preview-tag"
1994
+ );
1995
+ if (tagStatus === "deprecated")
1996
+ addDiagnostic(
1997
+ diagnostics,
1998
+ source,
1999
+ token.start,
2000
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
2001
+ "info",
2002
+ "azure-deprecated-tag"
2003
+ );
1865
2004
  if (name === "voice" && !attr(token, "name")?.trim())
1866
2005
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1867
2006
  if (name === "break") {
@@ -2036,6 +2175,9 @@ function validateAzureSsml(ssml, options = {}) {
2036
2175
  const maxLength = options.maxLength ?? 1e4;
2037
2176
  if (ssml.length > maxLength)
2038
2177
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
2178
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
2179
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
2180
+ }
2039
2181
  try {
2040
2182
  parseSsml(ssml);
2041
2183
  } catch (error) {
@@ -2045,6 +2187,18 @@ function validateAzureSsml(ssml, options = {}) {
2045
2187
  return diagnostics;
2046
2188
  }
2047
2189
  const tokens = tokenizeElements(ssml);
2190
+ if (options.maxXmlDepth !== void 0) {
2191
+ for (const token of tokens) {
2192
+ if (token.depth > options.maxXmlDepth) {
2193
+ addDiagnostic(
2194
+ diagnostics,
2195
+ ssml,
2196
+ token.start,
2197
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
2198
+ );
2199
+ }
2200
+ }
2201
+ }
2048
2202
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2049
2203
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2050
2204
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -2073,6 +2227,24 @@ function validateAzureSsml(ssml, options = {}) {
2073
2227
  const name = attr(token, "name")?.trim();
2074
2228
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
2075
2229
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2230
+ if (name && definition?.status === "preview")
2231
+ addDiagnostic(
2232
+ diagnostics,
2233
+ ssml,
2234
+ token.start,
2235
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
2236
+ "warning",
2237
+ "azure-preview-voice"
2238
+ );
2239
+ if (name && definition?.status === "deprecated")
2240
+ addDiagnostic(
2241
+ diagnostics,
2242
+ ssml,
2243
+ token.start,
2244
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
2245
+ "info",
2246
+ "azure-deprecated-voice"
2247
+ );
2076
2248
  if (name && !definition && policySeverity)
2077
2249
  addDiagnostic(
2078
2250
  diagnostics,
@@ -2142,6 +2314,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2142
2314
  mapSsmlTextNodes,
2143
2315
  normalizeAzureLanguage,
2144
2316
  parseSsml,
2317
+ splitSsmlDocument,
2145
2318
  validateAzureSsml,
2146
2319
  validateSsml,
2147
2320
  validateSsmlStructureIntegrity