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.
@@ -931,6 +931,236 @@ function buildPartialSsml(textOrOptions, context) {
931
931
  return serializePartialSsml(textOrOptions.text, textOrOptions);
932
932
  }
933
933
 
934
+ // packages/ssml-core/src/textNodes.ts
935
+ function decodeXmlText(value) {
936
+ return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
937
+ if (entity === "&") return "&";
938
+ if (entity === "'") return "'";
939
+ if (entity === ">") return ">";
940
+ if (entity === "&lt;") return "<";
941
+ if (entity === "&quot;") return '"';
942
+ const hexadecimal = entity.toLowerCase().startsWith("&#x");
943
+ const digits = entity.slice(hexadecimal ? 3 : 2, -1);
944
+ return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
945
+ });
946
+ }
947
+ function encodeXmlText(value) {
948
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
949
+ }
950
+ function decodeXmlAttribute(value) {
951
+ return decodeXmlText(value);
952
+ }
953
+ function findTagEnd(source, start) {
954
+ let quote = "";
955
+ for (let index = start; index < source.length; index += 1) {
956
+ const character = source[index];
957
+ if (quote) {
958
+ if (character === quote) quote = "";
959
+ } else if (character === '"' || character === "'") {
960
+ quote = character;
961
+ } else if (character === ">") {
962
+ return index;
963
+ }
964
+ }
965
+ return source.length - 1;
966
+ }
967
+ function readTagName(tag) {
968
+ const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
969
+ return match?.[1];
970
+ }
971
+ function readTagAttributes(tag, name) {
972
+ const attributes = {};
973
+ const nameStart = tag.indexOf(name);
974
+ const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
975
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
976
+ for (const match of attributeSource.matchAll(attributePattern)) {
977
+ attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
978
+ }
979
+ return attributes;
980
+ }
981
+ function collectTextNodes(source) {
982
+ const nodes = [];
983
+ const elements = [];
984
+ let index = 0;
985
+ const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
986
+ if (!rawText) return;
987
+ const path = elements.map((element) => element.name);
988
+ const parent = elements[elements.length - 1];
989
+ nodes.push({
990
+ context: {
991
+ ancestorTags: path.slice(0, -1),
992
+ parentAttributes: { ...parent?.attributes ?? {} },
993
+ parentTag: parent?.name ?? "",
994
+ path
995
+ },
996
+ decodedText: decodeXmlText(rawText),
997
+ end,
998
+ sourceEnd,
999
+ sourceStart,
1000
+ start
1001
+ });
1002
+ };
1003
+ while (index < source.length) {
1004
+ if (source[index] !== "<") {
1005
+ const nextTag = source.indexOf("<", index);
1006
+ const end2 = nextTag === -1 ? source.length : nextTag;
1007
+ addText(index, end2, source.slice(index, end2));
1008
+ index = end2;
1009
+ continue;
1010
+ }
1011
+ if (source.startsWith("<!--", index)) {
1012
+ const end2 = source.indexOf("-->", index + 4);
1013
+ index = end2 === -1 ? source.length : end2 + 3;
1014
+ continue;
1015
+ }
1016
+ if (source.startsWith("<![CDATA[", index)) {
1017
+ const contentStart = index + 9;
1018
+ const end2 = source.indexOf("]]>", contentStart);
1019
+ const contentEnd = end2 === -1 ? source.length : end2;
1020
+ addText(
1021
+ contentStart,
1022
+ contentEnd,
1023
+ source.slice(contentStart, contentEnd),
1024
+ index,
1025
+ end2 === -1 ? source.length : end2 + 3
1026
+ );
1027
+ index = end2 === -1 ? source.length : end2 + 3;
1028
+ continue;
1029
+ }
1030
+ if (source.startsWith("<?", index)) {
1031
+ const end2 = source.indexOf("?>", index + 2);
1032
+ index = end2 === -1 ? source.length : end2 + 2;
1033
+ continue;
1034
+ }
1035
+ if (source.startsWith("</", index)) {
1036
+ const end2 = findTagEnd(source, index + 2);
1037
+ elements.pop();
1038
+ index = end2 + 1;
1039
+ continue;
1040
+ }
1041
+ const end = findTagEnd(source, index + 1);
1042
+ const tag = source.slice(index, end + 1);
1043
+ const name = readTagName(tag);
1044
+ if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
1045
+ index = end + 1;
1046
+ }
1047
+ return nodes;
1048
+ }
1049
+ function collectSourceMap(source) {
1050
+ const segments = [];
1051
+ const markers = [];
1052
+ const elements = [];
1053
+ let textOffset = 0;
1054
+ let index = 0;
1055
+ const textParts = [];
1056
+ const addText = (value) => {
1057
+ if (!value) return;
1058
+ const parent = elements[elements.length - 1];
1059
+ if (parent) parent.nextChildIndex += 1;
1060
+ const sourceNodePath = parent?.path ?? ["speak"];
1061
+ const start = textOffset;
1062
+ textOffset += value.length;
1063
+ textParts.push(value);
1064
+ segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
1065
+ };
1066
+ while (index < source.length) {
1067
+ if (source[index] !== "<") {
1068
+ const end2 = source.indexOf("<", index);
1069
+ const textEnd = end2 === -1 ? source.length : end2;
1070
+ addText(decodeXmlText(source.slice(index, textEnd)));
1071
+ index = textEnd;
1072
+ continue;
1073
+ }
1074
+ if (source.startsWith("<!--", index)) {
1075
+ const end2 = source.indexOf("-->", index + 4);
1076
+ index = end2 === -1 ? source.length : end2 + 3;
1077
+ continue;
1078
+ }
1079
+ if (source.startsWith("<![CDATA[", index)) {
1080
+ const contentStart = index + 9;
1081
+ const end2 = source.indexOf("]]>", contentStart);
1082
+ const contentEnd = end2 === -1 ? source.length : end2;
1083
+ addText(source.slice(contentStart, contentEnd));
1084
+ index = end2 === -1 ? source.length : end2 + 3;
1085
+ continue;
1086
+ }
1087
+ if (source.startsWith("<?", index)) {
1088
+ const end2 = source.indexOf("?>", index + 2);
1089
+ index = end2 === -1 ? source.length : end2 + 2;
1090
+ continue;
1091
+ }
1092
+ const end = findTagEnd(source, index + 1);
1093
+ const rawTag = source.slice(index, end + 1);
1094
+ if (rawTag.startsWith("</")) {
1095
+ elements.pop();
1096
+ index = end + 1;
1097
+ continue;
1098
+ }
1099
+ const name = readTagName(rawTag);
1100
+ if (!name) {
1101
+ index = end + 1;
1102
+ continue;
1103
+ }
1104
+ const parent = elements[elements.length - 1];
1105
+ const childIndex = parent?.nextChildIndex ?? 0;
1106
+ if (parent) parent.nextChildIndex += 1;
1107
+ const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
1108
+ const attributes = readTagAttributes(rawTag, name);
1109
+ const normalizedName = name.toLowerCase();
1110
+ if (normalizedName === "mark" || normalizedName === "bookmark") {
1111
+ const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
1112
+ if (markerName) {
1113
+ markers.push({
1114
+ kind: normalizedName,
1115
+ name: markerName,
1116
+ originalTextRange: { start: textOffset, end: textOffset },
1117
+ sourceNodePath: [...path]
1118
+ });
1119
+ }
1120
+ }
1121
+ if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
1122
+ index = end + 1;
1123
+ }
1124
+ return { text: textParts.join(""), segments, markers };
1125
+ }
1126
+ function getSsmlSourceMap(ssml) {
1127
+ parseSsml(ssml);
1128
+ return collectSourceMap(ssml);
1129
+ }
1130
+ function extractSsmlText(ssml) {
1131
+ parseSsml(ssml);
1132
+ return collectTextNodes(ssml).map((node) => node.decodedText);
1133
+ }
1134
+ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1135
+ parseSsml(ssml);
1136
+ const nodes = collectTextNodes(ssml);
1137
+ const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
1138
+ const replacements = await Promise.all(
1139
+ nodes.map(async (node) => {
1140
+ const context = {
1141
+ ancestorTags: [...node.context.ancestorTags],
1142
+ parentAttributes: { ...node.context.parentAttributes },
1143
+ parentTag: node.context.parentTag,
1144
+ path: [...node.context.path]
1145
+ };
1146
+ const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
1147
+ if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
1148
+ const transformed = await transform(node.decodedText, context);
1149
+ if (typeof transformed !== "string") {
1150
+ throw new TypeError("SSML text node transform must return a string");
1151
+ }
1152
+ return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
1153
+ })
1154
+ );
1155
+ let result = "";
1156
+ let cursor = 0;
1157
+ nodes.forEach((node, nodeIndex) => {
1158
+ result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
1159
+ cursor = node.sourceEnd;
1160
+ });
1161
+ return result + ssml.slice(cursor);
1162
+ }
1163
+
934
1164
  // packages/ssml-core/src/split.ts
935
1165
  var DEFAULT_MAX_LENGTH = 1e4;
936
1166
  function cloneElement(element, children) {
@@ -1062,7 +1292,7 @@ function findSourceNodePath(nodes, targetOffset) {
1062
1292
  });
1063
1293
  return foundPath ?? firstPath;
1064
1294
  }
1065
- function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1295
+ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
1066
1296
  const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1067
1297
  const text = nodes.map(textFromNode).join("");
1068
1298
  const marks = [];
@@ -1078,7 +1308,23 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
1078
1308
  hasBackgroundAudio: chunkNodes.some(
1079
1309
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1080
1310
  ),
1081
- sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
1311
+ sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
1312
+ sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
1313
+ const start = Math.max(segment.range.start, textStart);
1314
+ const end = Math.min(segment.range.end, textStart + text.length);
1315
+ return {
1316
+ text: segment.text.slice(start - segment.range.start, end - segment.range.start),
1317
+ range: { start, end },
1318
+ sourceNodePath: [...segment.sourceNodePath]
1319
+ };
1320
+ }),
1321
+ sourceMarkers: sourceMap.markers.filter(
1322
+ ({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
1323
+ ).map((marker) => ({
1324
+ ...marker,
1325
+ originalTextRange: { ...marker.originalTextRange },
1326
+ sourceNodePath: [...marker.sourceNodePath]
1327
+ }))
1082
1328
  };
1083
1329
  }
1084
1330
  function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
@@ -1088,11 +1334,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1088
1334
  throw new RangeError("maxLength must be a positive integer");
1089
1335
  }
1090
1336
  const document = parseSsml(ssml);
1337
+ const sourceMap = getSsmlSourceMap(ssml);
1091
1338
  const backgroundAudio = (document.children ?? []).find(
1092
1339
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1093
1340
  );
1094
1341
  if (ssml.length <= resolvedMaxLength) {
1095
- return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
1342
+ return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
1096
1343
  }
1097
1344
  const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
1098
1345
  const plainDocumentLength = documentWithChildren(document, []).length;
@@ -1116,7 +1363,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1116
1363
  }
1117
1364
  if (group.length > 0) chunks.push(group);
1118
1365
  if (chunks.length === 0) {
1119
- const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
1366
+ const result = createChunk(
1367
+ document,
1368
+ [],
1369
+ 0,
1370
+ 0,
1371
+ backgroundAudio,
1372
+ resolvedOptions.replicateBackgroundAudio ?? false,
1373
+ sourceMap,
1374
+ true
1375
+ );
1120
1376
  if (result.ssml.length > resolvedMaxLength) {
1121
1377
  throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1122
1378
  }
@@ -1130,7 +1386,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1130
1386
  chunkIndex,
1131
1387
  textStart,
1132
1388
  backgroundAudio,
1133
- resolvedOptions.replicateBackgroundAudio ?? false
1389
+ resolvedOptions.replicateBackgroundAudio ?? false,
1390
+ sourceMap,
1391
+ chunkIndex === chunks.length - 1
1134
1392
  );
1135
1393
  textStart = result.originalTextRange.end;
1136
1394
  return result;
@@ -1153,155 +1411,6 @@ function validateSsml(xmlString) {
1153
1411
  }
1154
1412
  }
1155
1413
 
1156
- // packages/ssml-core/src/textNodes.ts
1157
- function decodeXmlText(value) {
1158
- return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
1159
- if (entity === "&amp;") return "&";
1160
- if (entity === "&apos;") return "'";
1161
- if (entity === "&gt;") return ">";
1162
- if (entity === "&lt;") return "<";
1163
- if (entity === "&quot;") return '"';
1164
- const hexadecimal = entity.toLowerCase().startsWith("&#x");
1165
- const digits = entity.slice(hexadecimal ? 3 : 2, -1);
1166
- return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
1167
- });
1168
- }
1169
- function encodeXmlText(value) {
1170
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1171
- }
1172
- function decodeXmlAttribute(value) {
1173
- return decodeXmlText(value);
1174
- }
1175
- function findTagEnd(source, start) {
1176
- let quote = "";
1177
- for (let index = start; index < source.length; index += 1) {
1178
- const character = source[index];
1179
- if (quote) {
1180
- if (character === quote) quote = "";
1181
- } else if (character === '"' || character === "'") {
1182
- quote = character;
1183
- } else if (character === ">") {
1184
- return index;
1185
- }
1186
- }
1187
- return source.length - 1;
1188
- }
1189
- function readTagName(tag) {
1190
- const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
1191
- return match?.[1];
1192
- }
1193
- function readTagAttributes(tag, name) {
1194
- const attributes = {};
1195
- const nameStart = tag.indexOf(name);
1196
- const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
1197
- const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
1198
- for (const match of attributeSource.matchAll(attributePattern)) {
1199
- attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
1200
- }
1201
- return attributes;
1202
- }
1203
- function collectTextNodes(source) {
1204
- const nodes = [];
1205
- const elements = [];
1206
- let index = 0;
1207
- const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
1208
- if (!rawText) return;
1209
- const path = elements.map((element) => element.name);
1210
- const parent = elements[elements.length - 1];
1211
- nodes.push({
1212
- context: {
1213
- ancestorTags: path.slice(0, -1),
1214
- parentAttributes: { ...parent?.attributes ?? {} },
1215
- parentTag: parent?.name ?? "",
1216
- path
1217
- },
1218
- decodedText: decodeXmlText(rawText),
1219
- end,
1220
- sourceEnd,
1221
- sourceStart,
1222
- start
1223
- });
1224
- };
1225
- while (index < source.length) {
1226
- if (source[index] !== "<") {
1227
- const nextTag = source.indexOf("<", index);
1228
- const end2 = nextTag === -1 ? source.length : nextTag;
1229
- addText(index, end2, source.slice(index, end2));
1230
- index = end2;
1231
- continue;
1232
- }
1233
- if (source.startsWith("<!--", index)) {
1234
- const end2 = source.indexOf("-->", index + 4);
1235
- index = end2 === -1 ? source.length : end2 + 3;
1236
- continue;
1237
- }
1238
- if (source.startsWith("<![CDATA[", index)) {
1239
- const contentStart = index + 9;
1240
- const end2 = source.indexOf("]]>", contentStart);
1241
- const contentEnd = end2 === -1 ? source.length : end2;
1242
- addText(
1243
- contentStart,
1244
- contentEnd,
1245
- source.slice(contentStart, contentEnd),
1246
- index,
1247
- end2 === -1 ? source.length : end2 + 3
1248
- );
1249
- index = end2 === -1 ? source.length : end2 + 3;
1250
- continue;
1251
- }
1252
- if (source.startsWith("<?", index)) {
1253
- const end2 = source.indexOf("?>", index + 2);
1254
- index = end2 === -1 ? source.length : end2 + 2;
1255
- continue;
1256
- }
1257
- if (source.startsWith("</", index)) {
1258
- const end2 = findTagEnd(source, index + 2);
1259
- elements.pop();
1260
- index = end2 + 1;
1261
- continue;
1262
- }
1263
- const end = findTagEnd(source, index + 1);
1264
- const tag = source.slice(index, end + 1);
1265
- const name = readTagName(tag);
1266
- if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
1267
- index = end + 1;
1268
- }
1269
- return nodes;
1270
- }
1271
- function extractSsmlText(ssml) {
1272
- parseSsml(ssml);
1273
- return collectTextNodes(ssml).map((node) => node.decodedText);
1274
- }
1275
- async function mapSsmlTextNodes(ssml, transform, options = {}) {
1276
- parseSsml(ssml);
1277
- const nodes = collectTextNodes(ssml);
1278
- const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
1279
- const replacements = await Promise.all(
1280
- nodes.map(async (node) => {
1281
- const context = {
1282
- ancestorTags: [...node.context.ancestorTags],
1283
- parentAttributes: { ...node.context.parentAttributes },
1284
- parentTag: node.context.parentTag,
1285
- path: [...node.context.path]
1286
- };
1287
- const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
1288
- if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
1289
- const transformed = await transform(node.decodedText, context);
1290
- if (typeof transformed !== "string") {
1291
- throw new TypeError("SSML text node transform must return a string");
1292
- }
1293
- return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
1294
- })
1295
- );
1296
- let result = "";
1297
- let cursor = 0;
1298
- nodes.forEach((node, nodeIndex) => {
1299
- result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
1300
- cursor = node.sourceEnd;
1301
- });
1302
- return result + ssml.slice(cursor);
1303
- }
1304
-
1305
1414
  // packages/ssml-core/src/migration.ts
1306
1415
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1307
1416
  function elementName2(element) {
@@ -1722,34 +1831,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1722
1831
  const inFlight = /* @__PURE__ */ new Map();
1723
1832
  const waiters = [];
1724
1833
  let active = 0;
1725
- const acquire = async () => {
1834
+ const configuredSignal = options.signal ?? new AbortController().signal;
1835
+ const acquire = async (signal) => {
1836
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1726
1837
  if (active < concurrency) {
1727
1838
  active += 1;
1728
1839
  return;
1729
1840
  }
1730
- await new Promise((resolve) => waiters.push(resolve));
1841
+ await new Promise((resolve, reject) => {
1842
+ let waiter;
1843
+ const abortHandler = () => {
1844
+ const index = waiters.indexOf(waiter);
1845
+ if (index >= 0) waiters.splice(index, 1);
1846
+ signal.removeEventListener("abort", abortHandler);
1847
+ reject(new Error("URL validation was aborted."));
1848
+ };
1849
+ signal.addEventListener("abort", abortHandler, { once: true });
1850
+ waiter = () => {
1851
+ signal.removeEventListener("abort", abortHandler);
1852
+ resolve();
1853
+ };
1854
+ waiters.push(waiter);
1855
+ });
1731
1856
  active += 1;
1732
1857
  };
1733
1858
  const release = () => {
1734
1859
  active -= 1;
1735
1860
  waiters.shift()?.();
1736
1861
  };
1737
- const check = async (url, context) => {
1738
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1739
- const cached = cache.get(url);
1862
+ const check = async (url, context, signal = configuredSignal) => {
1863
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1864
+ const key = `${context.tag}:${context.attribute}:${url}`;
1865
+ const cached = cache.get(key);
1740
1866
  if (cached !== void 0) return cached;
1741
- const existing = inFlight.get(url);
1867
+ const existing = inFlight.get(key);
1742
1868
  if (existing) return existing;
1743
1869
  const promise = (async () => {
1744
- await acquire();
1870
+ await acquire(signal);
1745
1871
  try {
1746
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1747
- const validation = Promise.resolve(validator(url, context));
1872
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1873
+ const validation = Promise.resolve(validator(url, context, signal));
1748
1874
  let timer;
1749
1875
  let abortHandler;
1750
1876
  const cancellation = new Promise((_resolve, reject) => {
1751
1877
  abortHandler = () => reject(new Error("URL validation was aborted."));
1752
- options.signal?.addEventListener("abort", abortHandler, { once: true });
1878
+ signal.addEventListener("abort", abortHandler, { once: true });
1753
1879
  if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1754
1880
  timer = setTimeout(
1755
1881
  () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
@@ -1759,24 +1885,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1759
1885
  });
1760
1886
  try {
1761
1887
  const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1762
- cache.set(url, result);
1888
+ cache.set(key, result);
1763
1889
  return result;
1764
1890
  } finally {
1765
1891
  if (timer) clearTimeout(timer);
1766
- if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1892
+ if (abortHandler) signal.removeEventListener("abort", abortHandler);
1767
1893
  }
1768
1894
  } finally {
1769
1895
  release();
1770
1896
  }
1771
1897
  })();
1772
- inFlight.set(url, promise);
1898
+ inFlight.set(key, promise);
1773
1899
  try {
1774
1900
  return await promise;
1775
1901
  } finally {
1776
- inFlight.delete(url);
1902
+ inFlight.delete(key);
1777
1903
  }
1778
1904
  };
1779
- return (url, context) => check(url, context);
1905
+ return (url, context, signal) => check(url, context, signal ?? configuredSignal);
1780
1906
  }
1781
1907
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1782
1908
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
@@ -1889,6 +2015,7 @@ function tokenizeElements(source) {
1889
2015
  if (parent) parent.childElementCount += 1;
1890
2016
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1891
2017
  const tokenName = nameMatch[1];
2018
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
1892
2019
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1893
2020
  tokens.push({
1894
2021
  attributes,
@@ -1899,12 +2026,14 @@ function tokenizeElements(source) {
1899
2026
  parentName: parent?.name,
1900
2027
  parentVoiceName,
1901
2028
  selfClosing,
1902
- start
2029
+ start,
2030
+ path
1903
2031
  });
1904
2032
  if (!selfClosing) {
1905
2033
  openElements.push({
1906
2034
  childElementCount: 0,
1907
2035
  name: tokenName,
2036
+ path,
1908
2037
  voiceName: tokenVoiceName
1909
2038
  });
1910
2039
  }
@@ -1917,13 +2046,14 @@ function location(source, offset) {
1917
2046
  const line = before.split("\n").length;
1918
2047
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
1919
2048
  }
1920
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
2049
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
1921
2050
  diagnostics.push({
1922
2051
  ...location(source, offset),
1923
2052
  message,
1924
2053
  severity,
1925
2054
  source: "ssml-static-validator",
1926
- ...code ? { code } : {}
2055
+ ...code ? { code } : {},
2056
+ ...metadata
1927
2057
  });
1928
2058
  }
1929
2059
  function isSupportedProsodyRate(value) {
@@ -2396,9 +2526,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
2396
2526
  for (const token of tokens) {
2397
2527
  const tokenName = token.name.toLowerCase();
2398
2528
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2529
+ const tokenDiagnosticStart = diagnostics.length;
2399
2530
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2400
2531
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2401
2532
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2533
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
2402
2534
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2403
2535
  addDiagnostic(
2404
2536
  diagnostics,
@@ -2420,18 +2552,37 @@ function urlAttributes(token) {
2420
2552
  return value === void 0 ? [] : [{ attribute, value }];
2421
2553
  });
2422
2554
  }
2555
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
2556
+ const attributes = [...token.attributes.keys()];
2557
+ for (const diagnostic of diagnostics.slice(startIndex)) {
2558
+ const attributeName = attributes.find(
2559
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
2560
+ );
2561
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
2562
+ Object.assign(diagnostic, {
2563
+ range: { start: token.start, end: token.end + 1 },
2564
+ tagName: token.name,
2565
+ ...attributeName ? { attributeName } : {},
2566
+ ...voiceName ? { voiceName } : {},
2567
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
2568
+ nodePath,
2569
+ targetNodePath: [...token.path]
2570
+ });
2571
+ }
2572
+ }
2423
2573
  function validateAzureSsml(ssml, options = {}) {
2424
2574
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2425
2575
  const validator = options.urlValidator ?? options.customUrlValidator;
2426
2576
  if (!validator || typeof ssml !== "string") return diagnostics;
2427
2577
  const runnerOptions = options.urlValidation ?? {};
2428
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
2578
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2429
2579
  ...runnerOptions,
2430
2580
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2431
2581
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2432
2582
  ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2433
2583
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2434
2584
  });
2585
+ const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
2435
2586
  let tokens;
2436
2587
  try {
2437
2588
  tokens = tokenizeElements(ssml);
@@ -2441,30 +2592,54 @@ function validateAzureSsml(ssml, options = {}) {
2441
2592
  const checks = tokens.flatMap(
2442
2593
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2443
2594
  try {
2444
- const result = await boundedValidator(value, { tag: token.name, attribute });
2595
+ const result = await boundedValidator(
2596
+ value,
2597
+ { tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
2598
+ validationSignal
2599
+ );
2445
2600
  const valid = typeof result === "boolean" ? result : result.valid;
2446
2601
  if (!valid) {
2447
2602
  const reason = typeof result === "boolean" ? void 0 : result.reason;
2603
+ const diagnosticStart = diagnostics.length;
2448
2604
  addDiagnostic(
2449
2605
  diagnostics,
2450
2606
  ssml,
2451
2607
  token.start,
2452
2608
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2453
2609
  );
2610
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2454
2611
  }
2455
2612
  } catch (error) {
2456
2613
  const reason = error instanceof Error ? error.message : String(error);
2614
+ const diagnosticStart = diagnostics.length;
2457
2615
  addDiagnostic(
2458
2616
  diagnostics,
2459
2617
  ssml,
2460
2618
  token.start,
2461
2619
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2462
2620
  );
2621
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2463
2622
  }
2464
2623
  })
2465
2624
  );
2466
2625
  return Promise.all(checks).then(() => diagnostics);
2467
2626
  }
2627
+ async function validateAzureSsmlChunks(chunks, options = {}) {
2628
+ const validator = options.urlValidator ?? options.customUrlValidator;
2629
+ const sharedOptions = validator ? {
2630
+ ...options,
2631
+ urlValidatorRunner: options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2632
+ ...options.urlValidation ?? {},
2633
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2634
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2635
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2636
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2637
+ })
2638
+ } : options;
2639
+ return Promise.all(
2640
+ chunks.map((chunk, chunkIndex) => Promise.resolve(validateAzureSsml(chunk, { ...sharedOptions, chunkIndex })))
2641
+ );
2642
+ }
2468
2643
 
2469
2644
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2470
2645
  var AZURE_VOICE_CATALOG_METADATA = {
@@ -2487,10 +2662,11 @@ export {
2487
2662
  buildSsml,
2488
2663
  parseSsml,
2489
2664
  buildPartialSsml,
2490
- splitSsmlDocument,
2491
- validateSsml,
2665
+ getSsmlSourceMap,
2492
2666
  extractSsmlText,
2493
2667
  mapSsmlTextNodes,
2668
+ splitSsmlDocument,
2669
+ validateSsml,
2494
2670
  extractSsmlTranslatableText,
2495
2671
  fromPlainTextToSsml,
2496
2672
  validateSsmlStructureIntegrity,
@@ -2499,7 +2675,8 @@ export {
2499
2675
  normalizeAzureLanguage,
2500
2676
  areAzureLanguagesEquivalent,
2501
2677
  validateAzureSsml,
2678
+ validateAzureSsmlChunks,
2502
2679
  getAzureVoiceCatalogMetadata,
2503
2680
  getBuiltInVoiceCatalogMetadata
2504
2681
  };
2505
- //# sourceMappingURL=chunk-AQ55MOPU.mjs.map
2682
+ //# sourceMappingURL=chunk-NKLZGITR.mjs.map