ssml-builder-js 2.14.0 → 2.16.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.mjs CHANGED
@@ -8,15 +8,17 @@ import {
8
8
  fromPlainTextToSsml,
9
9
  getAzureVoiceCatalogMetadata,
10
10
  getBuiltInVoiceCatalogMetadata,
11
+ getSsmlSourceMap,
11
12
  isValidAzureAudioDuration,
12
13
  mapSsmlTextNodes,
13
14
  normalizeAzureLanguage,
14
15
  parseSsml,
15
16
  splitSsmlDocument,
16
17
  validateAzureSsml,
18
+ validateAzureSsmlChunks,
17
19
  validateSsml,
18
20
  validateSsmlStructureIntegrity
19
- } from "./chunk-AQ55MOPU.mjs";
21
+ } from "./chunk-NKLZGITR.mjs";
20
22
  import "./chunk-6S5ODO6A.mjs";
21
23
  export {
22
24
  areAzureLanguagesEquivalent,
@@ -28,12 +30,14 @@ export {
28
30
  fromPlainTextToSsml,
29
31
  getAzureVoiceCatalogMetadata,
30
32
  getBuiltInVoiceCatalogMetadata,
33
+ getSsmlSourceMap,
31
34
  isValidAzureAudioDuration,
32
35
  mapSsmlTextNodes,
33
36
  normalizeAzureLanguage,
34
37
  parseSsml,
35
38
  splitSsmlDocument,
36
39
  validateAzureSsml,
40
+ validateAzureSsmlChunks,
37
41
  validateSsml,
38
42
  validateSsmlStructureIntegrity
39
43
  };
package/dist/elements.js CHANGED
@@ -1024,34 +1024,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1024
1024
  const inFlight = /* @__PURE__ */ new Map();
1025
1025
  const waiters = [];
1026
1026
  let active = 0;
1027
- const acquire = async () => {
1027
+ const configuredSignal = options.signal ?? new AbortController().signal;
1028
+ const acquire = async (signal) => {
1029
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1028
1030
  if (active < concurrency) {
1029
1031
  active += 1;
1030
1032
  return;
1031
1033
  }
1032
- await new Promise((resolve) => waiters.push(resolve));
1034
+ await new Promise((resolve, reject) => {
1035
+ let waiter;
1036
+ const abortHandler = () => {
1037
+ const index = waiters.indexOf(waiter);
1038
+ if (index >= 0) waiters.splice(index, 1);
1039
+ signal.removeEventListener("abort", abortHandler);
1040
+ reject(new Error("URL validation was aborted."));
1041
+ };
1042
+ signal.addEventListener("abort", abortHandler, { once: true });
1043
+ waiter = () => {
1044
+ signal.removeEventListener("abort", abortHandler);
1045
+ resolve();
1046
+ };
1047
+ waiters.push(waiter);
1048
+ });
1033
1049
  active += 1;
1034
1050
  };
1035
1051
  const release = () => {
1036
1052
  active -= 1;
1037
1053
  waiters.shift()?.();
1038
1054
  };
1039
- const check = async (url, context) => {
1040
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1041
- const cached = cache.get(url);
1055
+ const check = async (url, context, signal = configuredSignal) => {
1056
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1057
+ const key = `${context.tag}:${context.attribute}:${url}`;
1058
+ const cached = cache.get(key);
1042
1059
  if (cached !== void 0) return cached;
1043
- const existing = inFlight.get(url);
1060
+ const existing = inFlight.get(key);
1044
1061
  if (existing) return existing;
1045
1062
  const promise = (async () => {
1046
- await acquire();
1063
+ await acquire(signal);
1047
1064
  try {
1048
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1049
- const validation = Promise.resolve(validator(url, context));
1065
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1066
+ const validation = Promise.resolve(validator(url, context, signal));
1050
1067
  let timer;
1051
1068
  let abortHandler;
1052
1069
  const cancellation = new Promise((_resolve, reject) => {
1053
1070
  abortHandler = () => reject(new Error("URL validation was aborted."));
1054
- options.signal?.addEventListener("abort", abortHandler, { once: true });
1071
+ signal.addEventListener("abort", abortHandler, { once: true });
1055
1072
  if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1056
1073
  timer = setTimeout(
1057
1074
  () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
@@ -1061,24 +1078,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1061
1078
  });
1062
1079
  try {
1063
1080
  const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1064
- cache.set(url, result);
1081
+ cache.set(key, result);
1065
1082
  return result;
1066
1083
  } finally {
1067
1084
  if (timer) clearTimeout(timer);
1068
- if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1085
+ if (abortHandler) signal.removeEventListener("abort", abortHandler);
1069
1086
  }
1070
1087
  } finally {
1071
1088
  release();
1072
1089
  }
1073
1090
  })();
1074
- inFlight.set(url, promise);
1091
+ inFlight.set(key, promise);
1075
1092
  try {
1076
1093
  return await promise;
1077
1094
  } finally {
1078
- inFlight.delete(url);
1095
+ inFlight.delete(key);
1079
1096
  }
1080
1097
  };
1081
- return (url, context) => check(url, context);
1098
+ return (url, context, signal) => check(url, context, signal ?? configuredSignal);
1082
1099
  }
1083
1100
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1084
1101
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
@@ -1191,6 +1208,7 @@ function tokenizeElements(source) {
1191
1208
  if (parent) parent.childElementCount += 1;
1192
1209
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1193
1210
  const tokenName = nameMatch[1];
1211
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
1194
1212
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1195
1213
  tokens.push({
1196
1214
  attributes,
@@ -1201,12 +1219,14 @@ function tokenizeElements(source) {
1201
1219
  parentName: parent?.name,
1202
1220
  parentVoiceName,
1203
1221
  selfClosing,
1204
- start
1222
+ start,
1223
+ path
1205
1224
  });
1206
1225
  if (!selfClosing) {
1207
1226
  openElements.push({
1208
1227
  childElementCount: 0,
1209
1228
  name: tokenName,
1229
+ path,
1210
1230
  voiceName: tokenVoiceName
1211
1231
  });
1212
1232
  }
@@ -1219,13 +1239,14 @@ function location(source, offset) {
1219
1239
  const line = before.split("\n").length;
1220
1240
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
1221
1241
  }
1222
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
1242
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2, metadata = {}) {
1223
1243
  diagnostics.push({
1224
1244
  ...location(source, offset),
1225
1245
  message,
1226
1246
  severity,
1227
1247
  source: "ssml-static-validator",
1228
- ...code2 ? { code: code2 } : {}
1248
+ ...code2 ? { code: code2 } : {},
1249
+ ...metadata
1229
1250
  });
1230
1251
  }
1231
1252
  function isSupportedProsodyRate(value) {
@@ -1688,9 +1709,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
1688
1709
  for (const token of tokens) {
1689
1710
  const tokenName = token.name.toLowerCase();
1690
1711
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
1712
+ const tokenDiagnosticStart = diagnostics.length;
1691
1713
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
1692
1714
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
1693
1715
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
1716
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
1694
1717
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
1695
1718
  addDiagnostic(
1696
1719
  diagnostics,
@@ -1712,18 +1735,37 @@ function urlAttributes(token) {
1712
1735
  return value === void 0 ? [] : [{ attribute, value }];
1713
1736
  });
1714
1737
  }
1738
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
1739
+ const attributes = [...token.attributes.keys()];
1740
+ for (const diagnostic of diagnostics.slice(startIndex)) {
1741
+ const attributeName = attributes.find(
1742
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
1743
+ );
1744
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
1745
+ Object.assign(diagnostic, {
1746
+ range: { start: token.start, end: token.end + 1 },
1747
+ tagName: token.name,
1748
+ ...attributeName ? { attributeName } : {},
1749
+ ...voiceName ? { voiceName } : {},
1750
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
1751
+ nodePath,
1752
+ targetNodePath: [...token.path]
1753
+ });
1754
+ }
1755
+ }
1715
1756
  function validateAzureSsml(ssml, options = {}) {
1716
1757
  const diagnostics = validateAzureSsmlStatic(ssml, options);
1717
1758
  const validator = options.urlValidator ?? options.customUrlValidator;
1718
1759
  if (!validator || typeof ssml !== "string") return diagnostics;
1719
1760
  const runnerOptions = options.urlValidation ?? {};
1720
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
1761
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
1721
1762
  ...runnerOptions,
1722
1763
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
1723
1764
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
1724
1765
  ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
1725
1766
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
1726
1767
  });
1768
+ const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
1727
1769
  let tokens;
1728
1770
  try {
1729
1771
  tokens = tokenizeElements(ssml);
@@ -1733,25 +1775,33 @@ function validateAzureSsml(ssml, options = {}) {
1733
1775
  const checks = tokens.flatMap(
1734
1776
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
1735
1777
  try {
1736
- const result = await boundedValidator(value, { tag: token.name, attribute });
1778
+ const result = await boundedValidator(
1779
+ value,
1780
+ { tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
1781
+ validationSignal
1782
+ );
1737
1783
  const valid = typeof result === "boolean" ? result : result.valid;
1738
1784
  if (!valid) {
1739
1785
  const reason = typeof result === "boolean" ? void 0 : result.reason;
1786
+ const diagnosticStart = diagnostics.length;
1740
1787
  addDiagnostic(
1741
1788
  diagnostics,
1742
1789
  ssml,
1743
1790
  token.start,
1744
1791
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
1745
1792
  );
1793
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
1746
1794
  }
1747
1795
  } catch (error) {
1748
1796
  const reason = error instanceof Error ? error.message : String(error);
1797
+ const diagnosticStart = diagnostics.length;
1749
1798
  addDiagnostic(
1750
1799
  diagnostics,
1751
1800
  ssml,
1752
1801
  token.start,
1753
1802
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
1754
1803
  );
1804
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
1755
1805
  }
1756
1806
  })
1757
1807
  );