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.mjs CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  areAzureLanguagesEquivalent,
3
3
  buildPartialSsml,
4
4
  buildSsml,
5
+ createAzureUrlValidatorRunner,
5
6
  extractSsmlText,
6
7
  extractSsmlTranslatableText,
7
8
  fromPlainTextToSsml,
@@ -15,12 +16,13 @@ import {
15
16
  validateAzureSsml,
16
17
  validateSsml,
17
18
  validateSsmlStructureIntegrity
18
- } from "./chunk-25LOR4AJ.mjs";
19
+ } from "./chunk-AQ55MOPU.mjs";
19
20
  import "./chunk-6S5ODO6A.mjs";
20
21
  export {
21
22
  areAzureLanguagesEquivalent,
22
23
  buildPartialSsml,
23
24
  buildSsml,
25
+ createAzureUrlValidatorRunner,
24
26
  extractSsmlText,
25
27
  extractSsmlTranslatableText,
26
28
  fromPlainTextToSsml,
package/dist/elements.js CHANGED
@@ -1017,6 +1017,69 @@ var AZURE_VOICE_DEFINITIONS = [
1017
1017
  },
1018
1018
  { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
1019
1019
  ];
1020
+ function createAzureUrlValidatorRunner(validator, options = {}) {
1021
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
1022
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
1023
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
1024
+ const inFlight = /* @__PURE__ */ new Map();
1025
+ const waiters = [];
1026
+ let active = 0;
1027
+ const acquire = async () => {
1028
+ if (active < concurrency) {
1029
+ active += 1;
1030
+ return;
1031
+ }
1032
+ await new Promise((resolve) => waiters.push(resolve));
1033
+ active += 1;
1034
+ };
1035
+ const release = () => {
1036
+ active -= 1;
1037
+ waiters.shift()?.();
1038
+ };
1039
+ const check = async (url, context) => {
1040
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1041
+ const cached = cache.get(url);
1042
+ if (cached !== void 0) return cached;
1043
+ const existing = inFlight.get(url);
1044
+ if (existing) return existing;
1045
+ const promise = (async () => {
1046
+ await acquire();
1047
+ try {
1048
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1049
+ const validation = Promise.resolve(validator(url, context));
1050
+ let timer;
1051
+ let abortHandler;
1052
+ const cancellation = new Promise((_resolve, reject) => {
1053
+ abortHandler = () => reject(new Error("URL validation was aborted."));
1054
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
1055
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1056
+ timer = setTimeout(
1057
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
1058
+ options.timeoutMs
1059
+ );
1060
+ }
1061
+ });
1062
+ try {
1063
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1064
+ cache.set(url, result);
1065
+ return result;
1066
+ } finally {
1067
+ if (timer) clearTimeout(timer);
1068
+ if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1069
+ }
1070
+ } finally {
1071
+ release();
1072
+ }
1073
+ })();
1074
+ inFlight.set(url, promise);
1075
+ try {
1076
+ return await promise;
1077
+ } finally {
1078
+ inFlight.delete(url);
1079
+ }
1080
+ };
1081
+ return (url, context) => check(url, context);
1082
+ }
1020
1083
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1021
1084
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1022
1085
  "characters",
@@ -1301,23 +1364,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
1301
1364
  );
1302
1365
  }
1303
1366
  }
1304
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
1367
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
1305
1368
  const src = attr(token, "src");
1306
1369
  if (!src) {
1307
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
1370
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
1308
1371
  return;
1309
1372
  }
1310
1373
  let parsed;
1311
1374
  try {
1312
1375
  parsed = new URL(src);
1313
1376
  } catch {
1314
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1377
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
1315
1378
  return;
1316
1379
  }
1317
1380
  if (parsed.username || parsed.password)
1318
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1381
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
1319
1382
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1320
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1383
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
1321
1384
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1322
1385
  try {
1323
1386
  const configured = new URL(allowedOrigin);
@@ -1329,13 +1392,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1329
1392
  }
1330
1393
  }) ?? false;
1331
1394
  if (options.allowedAudioOrigins && !isAllowedOrigin)
1332
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1395
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
1333
1396
  else if (!isAllowedOrigin && !options.allowExternalAudio)
1334
1397
  addDiagnostic(
1335
1398
  diagnostics,
1336
1399
  source,
1337
1400
  token.start,
1338
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
1401
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
1339
1402
  );
1340
1403
  }
1341
1404
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -1653,6 +1716,14 @@ function validateAzureSsml(ssml, options = {}) {
1653
1716
  const diagnostics = validateAzureSsmlStatic(ssml, options);
1654
1717
  const validator = options.urlValidator ?? options.customUrlValidator;
1655
1718
  if (!validator || typeof ssml !== "string") return diagnostics;
1719
+ const runnerOptions = options.urlValidation ?? {};
1720
+ const boundedValidator = createAzureUrlValidatorRunner(validator, {
1721
+ ...runnerOptions,
1722
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
1723
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
1724
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
1725
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
1726
+ });
1656
1727
  let tokens;
1657
1728
  try {
1658
1729
  tokens = tokenizeElements(ssml);
@@ -1662,7 +1733,7 @@ function validateAzureSsml(ssml, options = {}) {
1662
1733
  const checks = tokens.flatMap(
1663
1734
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
1664
1735
  try {
1665
- const result = await validator(value, { tag: token.name, attribute });
1736
+ const result = await boundedValidator(value, { tag: token.name, attribute });
1666
1737
  const valid = typeof result === "boolean" ? result : result.valid;
1667
1738
  if (!valid) {
1668
1739
  const reason = typeof result === "boolean" ? void 0 : result.reason;