ssml-builder-js 2.13.0 → 2.14.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.js CHANGED
@@ -30,6 +30,7 @@ __export(core_exports, {
30
30
  areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
31
31
  buildPartialSsml: () => buildPartialSsml,
32
32
  buildSsml: () => buildSsml,
33
+ createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
33
34
  extractSsmlText: () => extractSsmlText,
34
35
  extractSsmlTranslatableText: () => extractSsmlTranslatableText,
35
36
  fromPlainTextToSsml: () => fromPlainTextToSsml,
@@ -1076,6 +1077,34 @@ function collectInheritedContext(nodes) {
1076
1077
  nodes.forEach(visit);
1077
1078
  return context;
1078
1079
  }
1080
+ function elementName(node) {
1081
+ return node.type === "custom" || node.type === "element" ? node.name : node.type;
1082
+ }
1083
+ function findSourceNodePath(nodes, targetOffset) {
1084
+ let textOffset = 0;
1085
+ let firstPath;
1086
+ let foundPath;
1087
+ const visit = (node, path) => {
1088
+ if (typeof node === "string" || node.type === "text") {
1089
+ const text = typeof node === "string" ? node : node.value;
1090
+ if (text && firstPath === void 0) firstPath = [...path];
1091
+ if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
1092
+ textOffset += text.length;
1093
+ return;
1094
+ }
1095
+ node.children?.forEach((child, index) => {
1096
+ const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
1097
+ visit(child, childPath);
1098
+ });
1099
+ };
1100
+ nodes.forEach((node, index) => {
1101
+ if (!foundPath) {
1102
+ if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
1103
+ else visit(node, ["speak", `${elementName(node)}[${index}]`]);
1104
+ }
1105
+ });
1106
+ return foundPath ?? firstPath;
1107
+ }
1079
1108
  function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1080
1109
  const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1081
1110
  const text = nodes.map(textFromNode).join("");
@@ -1091,7 +1120,8 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
1091
1120
  containedMarks: marks,
1092
1121
  hasBackgroundAudio: chunkNodes.some(
1093
1122
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1094
- )
1123
+ ),
1124
+ sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
1095
1125
  };
1096
1126
  }
1097
1127
  function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
@@ -1317,7 +1347,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1317
1347
 
1318
1348
  // packages/ssml-core/src/migration.ts
1319
1349
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1320
- function elementName(element) {
1350
+ function elementName2(element) {
1321
1351
  switch (element.type) {
1322
1352
  case "custom":
1323
1353
  case "element":
@@ -1470,7 +1500,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
1470
1500
  }
1471
1501
  return;
1472
1502
  }
1473
- const tag = elementName(node);
1503
+ const tag = elementName2(node);
1474
1504
  if (skipTags.has(tag.toLowerCase())) return;
1475
1505
  visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
1476
1506
  });
@@ -1516,7 +1546,7 @@ function serializeDocument2(document) {
1516
1546
  const serialize = (node) => {
1517
1547
  if (typeof node === "string") return node.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1518
1548
  if (node.type === "text") return serialize(node.value);
1519
- const tag = elementName(node);
1549
+ const tag = elementName2(node);
1520
1550
  const nodeAttributes = elementAttributes(node);
1521
1551
  const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, "&quot;")}"`).join("");
1522
1552
  const children = childrenOf(node).map(serialize).join("");
@@ -1530,7 +1560,7 @@ function flatten(document) {
1530
1560
  nodes.forEach((node, index) => {
1531
1561
  if (typeof node === "string" || node.type === "text") return;
1532
1562
  const currentPath = `${path}/${index}`;
1533
- result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
1563
+ result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
1534
1564
  visit(childrenOf(node), currentPath);
1535
1565
  });
1536
1566
  };
@@ -1728,6 +1758,69 @@ var AZURE_VOICE_DEFINITIONS = [
1728
1758
  ];
1729
1759
 
1730
1760
  // packages/ssml-core/src/azureValidation.ts
1761
+ function createAzureUrlValidatorRunner(validator, options = {}) {
1762
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
1763
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
1764
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
1765
+ const inFlight = /* @__PURE__ */ new Map();
1766
+ const waiters = [];
1767
+ let active = 0;
1768
+ const acquire = async () => {
1769
+ if (active < concurrency) {
1770
+ active += 1;
1771
+ return;
1772
+ }
1773
+ await new Promise((resolve) => waiters.push(resolve));
1774
+ active += 1;
1775
+ };
1776
+ const release = () => {
1777
+ active -= 1;
1778
+ waiters.shift()?.();
1779
+ };
1780
+ const check = async (url, context) => {
1781
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1782
+ const cached = cache.get(url);
1783
+ if (cached !== void 0) return cached;
1784
+ const existing = inFlight.get(url);
1785
+ if (existing) return existing;
1786
+ const promise = (async () => {
1787
+ await acquire();
1788
+ try {
1789
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1790
+ const validation = Promise.resolve(validator(url, context));
1791
+ let timer;
1792
+ let abortHandler;
1793
+ const cancellation = new Promise((_resolve, reject) => {
1794
+ abortHandler = () => reject(new Error("URL validation was aborted."));
1795
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
1796
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1797
+ timer = setTimeout(
1798
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
1799
+ options.timeoutMs
1800
+ );
1801
+ }
1802
+ });
1803
+ try {
1804
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1805
+ cache.set(url, result);
1806
+ return result;
1807
+ } finally {
1808
+ if (timer) clearTimeout(timer);
1809
+ if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1810
+ }
1811
+ } finally {
1812
+ release();
1813
+ }
1814
+ })();
1815
+ inFlight.set(url, promise);
1816
+ try {
1817
+ return await promise;
1818
+ } finally {
1819
+ inFlight.delete(url);
1820
+ }
1821
+ };
1822
+ return (url, context) => check(url, context);
1823
+ }
1731
1824
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1732
1825
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1733
1826
  "characters",
@@ -2022,23 +2115,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
2022
2115
  );
2023
2116
  }
2024
2117
  }
2025
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
2118
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
2026
2119
  const src = attr(token, "src");
2027
2120
  if (!src) {
2028
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2121
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
2029
2122
  return;
2030
2123
  }
2031
2124
  let parsed;
2032
2125
  try {
2033
2126
  parsed = new URL(src);
2034
2127
  } catch {
2035
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2128
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
2036
2129
  return;
2037
2130
  }
2038
2131
  if (parsed.username || parsed.password)
2039
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
2132
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
2040
2133
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2041
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2134
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
2042
2135
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
2043
2136
  try {
2044
2137
  const configured = new URL(allowedOrigin);
@@ -2050,13 +2143,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2050
2143
  }
2051
2144
  }) ?? false;
2052
2145
  if (options.allowedAudioOrigins && !isAllowedOrigin)
2053
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2146
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
2054
2147
  else if (!isAllowedOrigin && !options.allowExternalAudio)
2055
2148
  addDiagnostic(
2056
2149
  diagnostics,
2057
2150
  source,
2058
2151
  token.start,
2059
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2152
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2060
2153
  );
2061
2154
  }
2062
2155
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -2374,6 +2467,14 @@ function validateAzureSsml(ssml, options = {}) {
2374
2467
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2375
2468
  const validator = options.urlValidator ?? options.customUrlValidator;
2376
2469
  if (!validator || typeof ssml !== "string") return diagnostics;
2470
+ const runnerOptions = options.urlValidation ?? {};
2471
+ const boundedValidator = createAzureUrlValidatorRunner(validator, {
2472
+ ...runnerOptions,
2473
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2474
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2475
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2476
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2477
+ });
2377
2478
  let tokens;
2378
2479
  try {
2379
2480
  tokens = tokenizeElements(ssml);
@@ -2383,7 +2484,7 @@ function validateAzureSsml(ssml, options = {}) {
2383
2484
  const checks = tokens.flatMap(
2384
2485
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2385
2486
  try {
2386
- const result = await validator(value, { tag: token.name, attribute });
2487
+ const result = await boundedValidator(value, { tag: token.name, attribute });
2387
2488
  const valid = typeof result === "boolean" ? result : result.valid;
2388
2489
  if (!valid) {
2389
2490
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -2429,6 +2530,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2429
2530
  areAzureLanguagesEquivalent,
2430
2531
  buildPartialSsml,
2431
2532
  buildSsml,
2533
+ createAzureUrlValidatorRunner,
2432
2534
  extractSsmlText,
2433
2535
  extractSsmlTranslatableText,
2434
2536
  fromPlainTextToSsml,