ssml-builder-js 2.14.0 → 2.15.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
@@ -36,6 +36,7 @@ __export(core_exports, {
36
36
  fromPlainTextToSsml: () => fromPlainTextToSsml,
37
37
  getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
38
38
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
39
+ getSsmlSourceMap: () => getSsmlSourceMap,
39
40
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
40
41
  mapSsmlTextNodes: () => mapSsmlTextNodes,
41
42
  normalizeAzureLanguage: () => normalizeAzureLanguage,
@@ -974,6 +975,236 @@ function buildPartialSsml(textOrOptions, context) {
974
975
  return serializePartialSsml(textOrOptions.text, textOrOptions);
975
976
  }
976
977
 
978
+ // packages/ssml-core/src/textNodes.ts
979
+ function decodeXmlText(value) {
980
+ return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
981
+ if (entity === "&") return "&";
982
+ if (entity === "'") return "'";
983
+ if (entity === ">") return ">";
984
+ if (entity === "&lt;") return "<";
985
+ if (entity === "&quot;") return '"';
986
+ const hexadecimal = entity.toLowerCase().startsWith("&#x");
987
+ const digits = entity.slice(hexadecimal ? 3 : 2, -1);
988
+ return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
989
+ });
990
+ }
991
+ function encodeXmlText(value) {
992
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
993
+ }
994
+ function decodeXmlAttribute(value) {
995
+ return decodeXmlText(value);
996
+ }
997
+ function findTagEnd(source, start) {
998
+ let quote = "";
999
+ for (let index = start; index < source.length; index += 1) {
1000
+ const character = source[index];
1001
+ if (quote) {
1002
+ if (character === quote) quote = "";
1003
+ } else if (character === '"' || character === "'") {
1004
+ quote = character;
1005
+ } else if (character === ">") {
1006
+ return index;
1007
+ }
1008
+ }
1009
+ return source.length - 1;
1010
+ }
1011
+ function readTagName(tag) {
1012
+ const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
1013
+ return match?.[1];
1014
+ }
1015
+ function readTagAttributes(tag, name) {
1016
+ const attributes = {};
1017
+ const nameStart = tag.indexOf(name);
1018
+ const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
1019
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
1020
+ for (const match of attributeSource.matchAll(attributePattern)) {
1021
+ attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
1022
+ }
1023
+ return attributes;
1024
+ }
1025
+ function collectTextNodes(source) {
1026
+ const nodes = [];
1027
+ const elements = [];
1028
+ let index = 0;
1029
+ const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
1030
+ if (!rawText) return;
1031
+ const path = elements.map((element) => element.name);
1032
+ const parent = elements[elements.length - 1];
1033
+ nodes.push({
1034
+ context: {
1035
+ ancestorTags: path.slice(0, -1),
1036
+ parentAttributes: { ...parent?.attributes ?? {} },
1037
+ parentTag: parent?.name ?? "",
1038
+ path
1039
+ },
1040
+ decodedText: decodeXmlText(rawText),
1041
+ end,
1042
+ sourceEnd,
1043
+ sourceStart,
1044
+ start
1045
+ });
1046
+ };
1047
+ while (index < source.length) {
1048
+ if (source[index] !== "<") {
1049
+ const nextTag = source.indexOf("<", index);
1050
+ const end2 = nextTag === -1 ? source.length : nextTag;
1051
+ addText(index, end2, source.slice(index, end2));
1052
+ index = end2;
1053
+ continue;
1054
+ }
1055
+ if (source.startsWith("<!--", index)) {
1056
+ const end2 = source.indexOf("-->", index + 4);
1057
+ index = end2 === -1 ? source.length : end2 + 3;
1058
+ continue;
1059
+ }
1060
+ if (source.startsWith("<![CDATA[", index)) {
1061
+ const contentStart = index + 9;
1062
+ const end2 = source.indexOf("]]>", contentStart);
1063
+ const contentEnd = end2 === -1 ? source.length : end2;
1064
+ addText(
1065
+ contentStart,
1066
+ contentEnd,
1067
+ source.slice(contentStart, contentEnd),
1068
+ index,
1069
+ end2 === -1 ? source.length : end2 + 3
1070
+ );
1071
+ index = end2 === -1 ? source.length : end2 + 3;
1072
+ continue;
1073
+ }
1074
+ if (source.startsWith("<?", index)) {
1075
+ const end2 = source.indexOf("?>", index + 2);
1076
+ index = end2 === -1 ? source.length : end2 + 2;
1077
+ continue;
1078
+ }
1079
+ if (source.startsWith("</", index)) {
1080
+ const end2 = findTagEnd(source, index + 2);
1081
+ elements.pop();
1082
+ index = end2 + 1;
1083
+ continue;
1084
+ }
1085
+ const end = findTagEnd(source, index + 1);
1086
+ const tag = source.slice(index, end + 1);
1087
+ const name = readTagName(tag);
1088
+ if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
1089
+ index = end + 1;
1090
+ }
1091
+ return nodes;
1092
+ }
1093
+ function collectSourceMap(source) {
1094
+ const segments = [];
1095
+ const markers = [];
1096
+ const elements = [];
1097
+ let textOffset = 0;
1098
+ let index = 0;
1099
+ const textParts = [];
1100
+ const addText = (value) => {
1101
+ if (!value) return;
1102
+ const parent = elements[elements.length - 1];
1103
+ if (parent) parent.nextChildIndex += 1;
1104
+ const sourceNodePath = parent?.path ?? ["speak"];
1105
+ const start = textOffset;
1106
+ textOffset += value.length;
1107
+ textParts.push(value);
1108
+ segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
1109
+ };
1110
+ while (index < source.length) {
1111
+ if (source[index] !== "<") {
1112
+ const end2 = source.indexOf("<", index);
1113
+ const textEnd = end2 === -1 ? source.length : end2;
1114
+ addText(decodeXmlText(source.slice(index, textEnd)));
1115
+ index = textEnd;
1116
+ continue;
1117
+ }
1118
+ if (source.startsWith("<!--", index)) {
1119
+ const end2 = source.indexOf("-->", index + 4);
1120
+ index = end2 === -1 ? source.length : end2 + 3;
1121
+ continue;
1122
+ }
1123
+ if (source.startsWith("<![CDATA[", index)) {
1124
+ const contentStart = index + 9;
1125
+ const end2 = source.indexOf("]]>", contentStart);
1126
+ const contentEnd = end2 === -1 ? source.length : end2;
1127
+ addText(source.slice(contentStart, contentEnd));
1128
+ index = end2 === -1 ? source.length : end2 + 3;
1129
+ continue;
1130
+ }
1131
+ if (source.startsWith("<?", index)) {
1132
+ const end2 = source.indexOf("?>", index + 2);
1133
+ index = end2 === -1 ? source.length : end2 + 2;
1134
+ continue;
1135
+ }
1136
+ const end = findTagEnd(source, index + 1);
1137
+ const rawTag = source.slice(index, end + 1);
1138
+ if (rawTag.startsWith("</")) {
1139
+ elements.pop();
1140
+ index = end + 1;
1141
+ continue;
1142
+ }
1143
+ const name = readTagName(rawTag);
1144
+ if (!name) {
1145
+ index = end + 1;
1146
+ continue;
1147
+ }
1148
+ const parent = elements[elements.length - 1];
1149
+ const childIndex = parent?.nextChildIndex ?? 0;
1150
+ if (parent) parent.nextChildIndex += 1;
1151
+ const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
1152
+ const attributes = readTagAttributes(rawTag, name);
1153
+ const normalizedName = name.toLowerCase();
1154
+ if (normalizedName === "mark" || normalizedName === "bookmark") {
1155
+ const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
1156
+ if (markerName) {
1157
+ markers.push({
1158
+ kind: normalizedName,
1159
+ name: markerName,
1160
+ originalTextRange: { start: textOffset, end: textOffset },
1161
+ sourceNodePath: [...path]
1162
+ });
1163
+ }
1164
+ }
1165
+ if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
1166
+ index = end + 1;
1167
+ }
1168
+ return { text: textParts.join(""), segments, markers };
1169
+ }
1170
+ function getSsmlSourceMap(ssml) {
1171
+ parseSsml(ssml);
1172
+ return collectSourceMap(ssml);
1173
+ }
1174
+ function extractSsmlText(ssml) {
1175
+ parseSsml(ssml);
1176
+ return collectTextNodes(ssml).map((node) => node.decodedText);
1177
+ }
1178
+ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1179
+ parseSsml(ssml);
1180
+ const nodes = collectTextNodes(ssml);
1181
+ const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
1182
+ const replacements = await Promise.all(
1183
+ nodes.map(async (node) => {
1184
+ const context = {
1185
+ ancestorTags: [...node.context.ancestorTags],
1186
+ parentAttributes: { ...node.context.parentAttributes },
1187
+ parentTag: node.context.parentTag,
1188
+ path: [...node.context.path]
1189
+ };
1190
+ const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
1191
+ if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
1192
+ const transformed = await transform(node.decodedText, context);
1193
+ if (typeof transformed !== "string") {
1194
+ throw new TypeError("SSML text node transform must return a string");
1195
+ }
1196
+ return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
1197
+ })
1198
+ );
1199
+ let result = "";
1200
+ let cursor = 0;
1201
+ nodes.forEach((node, nodeIndex) => {
1202
+ result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
1203
+ cursor = node.sourceEnd;
1204
+ });
1205
+ return result + ssml.slice(cursor);
1206
+ }
1207
+
977
1208
  // packages/ssml-core/src/split.ts
978
1209
  var DEFAULT_MAX_LENGTH = 1e4;
979
1210
  function cloneElement(element, children) {
@@ -1105,7 +1336,7 @@ function findSourceNodePath(nodes, targetOffset) {
1105
1336
  });
1106
1337
  return foundPath ?? firstPath;
1107
1338
  }
1108
- function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1339
+ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
1109
1340
  const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1110
1341
  const text = nodes.map(textFromNode).join("");
1111
1342
  const marks = [];
@@ -1121,7 +1352,23 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
1121
1352
  hasBackgroundAudio: chunkNodes.some(
1122
1353
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1123
1354
  ),
1124
- sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
1355
+ sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
1356
+ sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
1357
+ const start = Math.max(segment.range.start, textStart);
1358
+ const end = Math.min(segment.range.end, textStart + text.length);
1359
+ return {
1360
+ text: segment.text.slice(start - segment.range.start, end - segment.range.start),
1361
+ range: { start, end },
1362
+ sourceNodePath: [...segment.sourceNodePath]
1363
+ };
1364
+ }),
1365
+ sourceMarkers: sourceMap.markers.filter(
1366
+ ({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
1367
+ ).map((marker) => ({
1368
+ ...marker,
1369
+ originalTextRange: { ...marker.originalTextRange },
1370
+ sourceNodePath: [...marker.sourceNodePath]
1371
+ }))
1125
1372
  };
1126
1373
  }
1127
1374
  function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
@@ -1131,11 +1378,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1131
1378
  throw new RangeError("maxLength must be a positive integer");
1132
1379
  }
1133
1380
  const document = parseSsml(ssml);
1381
+ const sourceMap = getSsmlSourceMap(ssml);
1134
1382
  const backgroundAudio = (document.children ?? []).find(
1135
1383
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1136
1384
  );
1137
1385
  if (ssml.length <= resolvedMaxLength) {
1138
- return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
1386
+ return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
1139
1387
  }
1140
1388
  const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
1141
1389
  const plainDocumentLength = documentWithChildren(document, []).length;
@@ -1159,7 +1407,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1159
1407
  }
1160
1408
  if (group.length > 0) chunks.push(group);
1161
1409
  if (chunks.length === 0) {
1162
- const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
1410
+ const result = createChunk(
1411
+ document,
1412
+ [],
1413
+ 0,
1414
+ 0,
1415
+ backgroundAudio,
1416
+ resolvedOptions.replicateBackgroundAudio ?? false,
1417
+ sourceMap,
1418
+ true
1419
+ );
1163
1420
  if (result.ssml.length > resolvedMaxLength) {
1164
1421
  throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1165
1422
  }
@@ -1173,7 +1430,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1173
1430
  chunkIndex,
1174
1431
  textStart,
1175
1432
  backgroundAudio,
1176
- resolvedOptions.replicateBackgroundAudio ?? false
1433
+ resolvedOptions.replicateBackgroundAudio ?? false,
1434
+ sourceMap,
1435
+ chunkIndex === chunks.length - 1
1177
1436
  );
1178
1437
  textStart = result.originalTextRange.end;
1179
1438
  return result;
@@ -1196,155 +1455,6 @@ function validateSsml(xmlString) {
1196
1455
  }
1197
1456
  }
1198
1457
 
1199
- // packages/ssml-core/src/textNodes.ts
1200
- function decodeXmlText(value) {
1201
- return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
1202
- if (entity === "&amp;") return "&";
1203
- if (entity === "&apos;") return "'";
1204
- if (entity === "&gt;") return ">";
1205
- if (entity === "&lt;") return "<";
1206
- if (entity === "&quot;") return '"';
1207
- const hexadecimal = entity.toLowerCase().startsWith("&#x");
1208
- const digits = entity.slice(hexadecimal ? 3 : 2, -1);
1209
- return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
1210
- });
1211
- }
1212
- function encodeXmlText(value) {
1213
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1214
- }
1215
- function decodeXmlAttribute(value) {
1216
- return decodeXmlText(value);
1217
- }
1218
- function findTagEnd(source, start) {
1219
- let quote = "";
1220
- for (let index = start; index < source.length; index += 1) {
1221
- const character = source[index];
1222
- if (quote) {
1223
- if (character === quote) quote = "";
1224
- } else if (character === '"' || character === "'") {
1225
- quote = character;
1226
- } else if (character === ">") {
1227
- return index;
1228
- }
1229
- }
1230
- return source.length - 1;
1231
- }
1232
- function readTagName(tag) {
1233
- const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
1234
- return match?.[1];
1235
- }
1236
- function readTagAttributes(tag, name) {
1237
- const attributes = {};
1238
- const nameStart = tag.indexOf(name);
1239
- const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
1240
- const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
1241
- for (const match of attributeSource.matchAll(attributePattern)) {
1242
- attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
1243
- }
1244
- return attributes;
1245
- }
1246
- function collectTextNodes(source) {
1247
- const nodes = [];
1248
- const elements = [];
1249
- let index = 0;
1250
- const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
1251
- if (!rawText) return;
1252
- const path = elements.map((element) => element.name);
1253
- const parent = elements[elements.length - 1];
1254
- nodes.push({
1255
- context: {
1256
- ancestorTags: path.slice(0, -1),
1257
- parentAttributes: { ...parent?.attributes ?? {} },
1258
- parentTag: parent?.name ?? "",
1259
- path
1260
- },
1261
- decodedText: decodeXmlText(rawText),
1262
- end,
1263
- sourceEnd,
1264
- sourceStart,
1265
- start
1266
- });
1267
- };
1268
- while (index < source.length) {
1269
- if (source[index] !== "<") {
1270
- const nextTag = source.indexOf("<", index);
1271
- const end2 = nextTag === -1 ? source.length : nextTag;
1272
- addText(index, end2, source.slice(index, end2));
1273
- index = end2;
1274
- continue;
1275
- }
1276
- if (source.startsWith("<!--", index)) {
1277
- const end2 = source.indexOf("-->", index + 4);
1278
- index = end2 === -1 ? source.length : end2 + 3;
1279
- continue;
1280
- }
1281
- if (source.startsWith("<![CDATA[", index)) {
1282
- const contentStart = index + 9;
1283
- const end2 = source.indexOf("]]>", contentStart);
1284
- const contentEnd = end2 === -1 ? source.length : end2;
1285
- addText(
1286
- contentStart,
1287
- contentEnd,
1288
- source.slice(contentStart, contentEnd),
1289
- index,
1290
- end2 === -1 ? source.length : end2 + 3
1291
- );
1292
- index = end2 === -1 ? source.length : end2 + 3;
1293
- continue;
1294
- }
1295
- if (source.startsWith("<?", index)) {
1296
- const end2 = source.indexOf("?>", index + 2);
1297
- index = end2 === -1 ? source.length : end2 + 2;
1298
- continue;
1299
- }
1300
- if (source.startsWith("</", index)) {
1301
- const end2 = findTagEnd(source, index + 2);
1302
- elements.pop();
1303
- index = end2 + 1;
1304
- continue;
1305
- }
1306
- const end = findTagEnd(source, index + 1);
1307
- const tag = source.slice(index, end + 1);
1308
- const name = readTagName(tag);
1309
- if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
1310
- index = end + 1;
1311
- }
1312
- return nodes;
1313
- }
1314
- function extractSsmlText(ssml) {
1315
- parseSsml(ssml);
1316
- return collectTextNodes(ssml).map((node) => node.decodedText);
1317
- }
1318
- async function mapSsmlTextNodes(ssml, transform, options = {}) {
1319
- parseSsml(ssml);
1320
- const nodes = collectTextNodes(ssml);
1321
- const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
1322
- const replacements = await Promise.all(
1323
- nodes.map(async (node) => {
1324
- const context = {
1325
- ancestorTags: [...node.context.ancestorTags],
1326
- parentAttributes: { ...node.context.parentAttributes },
1327
- parentTag: node.context.parentTag,
1328
- path: [...node.context.path]
1329
- };
1330
- const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
1331
- if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
1332
- const transformed = await transform(node.decodedText, context);
1333
- if (typeof transformed !== "string") {
1334
- throw new TypeError("SSML text node transform must return a string");
1335
- }
1336
- return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
1337
- })
1338
- );
1339
- let result = "";
1340
- let cursor = 0;
1341
- nodes.forEach((node, nodeIndex) => {
1342
- result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
1343
- cursor = node.sourceEnd;
1344
- });
1345
- return result + ssml.slice(cursor);
1346
- }
1347
-
1348
1458
  // packages/ssml-core/src/migration.ts
1349
1459
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1350
1460
  function elementName2(element) {
@@ -1765,34 +1875,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1765
1875
  const inFlight = /* @__PURE__ */ new Map();
1766
1876
  const waiters = [];
1767
1877
  let active = 0;
1768
- const acquire = async () => {
1878
+ const configuredSignal = options.signal ?? new AbortController().signal;
1879
+ const acquire = async (signal) => {
1880
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1769
1881
  if (active < concurrency) {
1770
1882
  active += 1;
1771
1883
  return;
1772
1884
  }
1773
- await new Promise((resolve) => waiters.push(resolve));
1885
+ await new Promise((resolve, reject) => {
1886
+ let waiter;
1887
+ const abortHandler = () => {
1888
+ const index = waiters.indexOf(waiter);
1889
+ if (index >= 0) waiters.splice(index, 1);
1890
+ signal.removeEventListener("abort", abortHandler);
1891
+ reject(new Error("URL validation was aborted."));
1892
+ };
1893
+ signal.addEventListener("abort", abortHandler, { once: true });
1894
+ waiter = () => {
1895
+ signal.removeEventListener("abort", abortHandler);
1896
+ resolve();
1897
+ };
1898
+ waiters.push(waiter);
1899
+ });
1774
1900
  active += 1;
1775
1901
  };
1776
1902
  const release = () => {
1777
1903
  active -= 1;
1778
1904
  waiters.shift()?.();
1779
1905
  };
1780
- const check = async (url, context) => {
1781
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1782
- const cached = cache.get(url);
1906
+ const check = async (url, context, signal = configuredSignal) => {
1907
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1908
+ const key = `${context.tag}:${context.attribute}:${url}`;
1909
+ const cached = cache.get(key);
1783
1910
  if (cached !== void 0) return cached;
1784
- const existing = inFlight.get(url);
1911
+ const existing = inFlight.get(key);
1785
1912
  if (existing) return existing;
1786
1913
  const promise = (async () => {
1787
- await acquire();
1914
+ await acquire(signal);
1788
1915
  try {
1789
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1790
- const validation = Promise.resolve(validator(url, context));
1916
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1917
+ const validation = Promise.resolve(validator(url, context, signal));
1791
1918
  let timer;
1792
1919
  let abortHandler;
1793
1920
  const cancellation = new Promise((_resolve, reject) => {
1794
1921
  abortHandler = () => reject(new Error("URL validation was aborted."));
1795
- options.signal?.addEventListener("abort", abortHandler, { once: true });
1922
+ signal.addEventListener("abort", abortHandler, { once: true });
1796
1923
  if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1797
1924
  timer = setTimeout(
1798
1925
  () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
@@ -1802,24 +1929,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1802
1929
  });
1803
1930
  try {
1804
1931
  const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1805
- cache.set(url, result);
1932
+ cache.set(key, result);
1806
1933
  return result;
1807
1934
  } finally {
1808
1935
  if (timer) clearTimeout(timer);
1809
- if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1936
+ if (abortHandler) signal.removeEventListener("abort", abortHandler);
1810
1937
  }
1811
1938
  } finally {
1812
1939
  release();
1813
1940
  }
1814
1941
  })();
1815
- inFlight.set(url, promise);
1942
+ inFlight.set(key, promise);
1816
1943
  try {
1817
1944
  return await promise;
1818
1945
  } finally {
1819
- inFlight.delete(url);
1946
+ inFlight.delete(key);
1820
1947
  }
1821
1948
  };
1822
- return (url, context) => check(url, context);
1949
+ return (url, context, signal) => check(url, context, signal ?? configuredSignal);
1823
1950
  }
1824
1951
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1825
1952
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
@@ -2475,6 +2602,7 @@ function validateAzureSsml(ssml, options = {}) {
2475
2602
  ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2476
2603
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2477
2604
  });
2605
+ const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
2478
2606
  let tokens;
2479
2607
  try {
2480
2608
  tokens = tokenizeElements(ssml);
@@ -2484,7 +2612,11 @@ function validateAzureSsml(ssml, options = {}) {
2484
2612
  const checks = tokens.flatMap(
2485
2613
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2486
2614
  try {
2487
- const result = await boundedValidator(value, { tag: token.name, attribute });
2615
+ const result = await boundedValidator(
2616
+ value,
2617
+ { tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
2618
+ validationSignal
2619
+ );
2488
2620
  const valid = typeof result === "boolean" ? result : result.valid;
2489
2621
  if (!valid) {
2490
2622
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -2536,6 +2668,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2536
2668
  fromPlainTextToSsml,
2537
2669
  getAzureVoiceCatalogMetadata,
2538
2670
  getBuiltInVoiceCatalogMetadata,
2671
+ getSsmlSourceMap,
2539
2672
  isValidAzureAudioDuration,
2540
2673
  mapSsmlTextNodes,
2541
2674
  normalizeAzureLanguage,