ssml-builder-js 2.13.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.
@@ -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) {
@@ -1034,7 +1264,35 @@ function collectInheritedContext(nodes) {
1034
1264
  nodes.forEach(visit);
1035
1265
  return context;
1036
1266
  }
1037
- function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1267
+ function elementName(node) {
1268
+ return node.type === "custom" || node.type === "element" ? node.name : node.type;
1269
+ }
1270
+ function findSourceNodePath(nodes, targetOffset) {
1271
+ let textOffset = 0;
1272
+ let firstPath;
1273
+ let foundPath;
1274
+ const visit = (node, path) => {
1275
+ if (typeof node === "string" || node.type === "text") {
1276
+ const text = typeof node === "string" ? node : node.value;
1277
+ if (text && firstPath === void 0) firstPath = [...path];
1278
+ if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
1279
+ textOffset += text.length;
1280
+ return;
1281
+ }
1282
+ node.children?.forEach((child, index) => {
1283
+ const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
1284
+ visit(child, childPath);
1285
+ });
1286
+ };
1287
+ nodes.forEach((node, index) => {
1288
+ if (!foundPath) {
1289
+ if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
1290
+ else visit(node, ["speak", `${elementName(node)}[${index}]`]);
1291
+ }
1292
+ });
1293
+ return foundPath ?? firstPath;
1294
+ }
1295
+ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
1038
1296
  const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1039
1297
  const text = nodes.map(textFromNode).join("");
1040
1298
  const marks = [];
@@ -1049,7 +1307,24 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
1049
1307
  containedMarks: marks,
1050
1308
  hasBackgroundAudio: chunkNodes.some(
1051
1309
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1052
- )
1310
+ ),
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
+ }))
1053
1328
  };
1054
1329
  }
1055
1330
  function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
@@ -1059,11 +1334,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1059
1334
  throw new RangeError("maxLength must be a positive integer");
1060
1335
  }
1061
1336
  const document = parseSsml(ssml);
1337
+ const sourceMap = getSsmlSourceMap(ssml);
1062
1338
  const backgroundAudio = (document.children ?? []).find(
1063
1339
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1064
1340
  );
1065
1341
  if (ssml.length <= resolvedMaxLength) {
1066
- return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
1342
+ return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
1067
1343
  }
1068
1344
  const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
1069
1345
  const plainDocumentLength = documentWithChildren(document, []).length;
@@ -1087,7 +1363,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1087
1363
  }
1088
1364
  if (group.length > 0) chunks.push(group);
1089
1365
  if (chunks.length === 0) {
1090
- 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
+ );
1091
1376
  if (result.ssml.length > resolvedMaxLength) {
1092
1377
  throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1093
1378
  }
@@ -1101,7 +1386,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1101
1386
  chunkIndex,
1102
1387
  textStart,
1103
1388
  backgroundAudio,
1104
- resolvedOptions.replicateBackgroundAudio ?? false
1389
+ resolvedOptions.replicateBackgroundAudio ?? false,
1390
+ sourceMap,
1391
+ chunkIndex === chunks.length - 1
1105
1392
  );
1106
1393
  textStart = result.originalTextRange.end;
1107
1394
  return result;
@@ -1124,158 +1411,9 @@ function validateSsml(xmlString) {
1124
1411
  }
1125
1412
  }
1126
1413
 
1127
- // packages/ssml-core/src/textNodes.ts
1128
- function decodeXmlText(value) {
1129
- return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
1130
- if (entity === "&amp;") return "&";
1131
- if (entity === "&apos;") return "'";
1132
- if (entity === "&gt;") return ">";
1133
- if (entity === "&lt;") return "<";
1134
- if (entity === "&quot;") return '"';
1135
- const hexadecimal = entity.toLowerCase().startsWith("&#x");
1136
- const digits = entity.slice(hexadecimal ? 3 : 2, -1);
1137
- return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
1138
- });
1139
- }
1140
- function encodeXmlText(value) {
1141
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1142
- }
1143
- function decodeXmlAttribute(value) {
1144
- return decodeXmlText(value);
1145
- }
1146
- function findTagEnd(source, start) {
1147
- let quote = "";
1148
- for (let index = start; index < source.length; index += 1) {
1149
- const character = source[index];
1150
- if (quote) {
1151
- if (character === quote) quote = "";
1152
- } else if (character === '"' || character === "'") {
1153
- quote = character;
1154
- } else if (character === ">") {
1155
- return index;
1156
- }
1157
- }
1158
- return source.length - 1;
1159
- }
1160
- function readTagName(tag) {
1161
- const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
1162
- return match?.[1];
1163
- }
1164
- function readTagAttributes(tag, name) {
1165
- const attributes = {};
1166
- const nameStart = tag.indexOf(name);
1167
- const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
1168
- const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
1169
- for (const match of attributeSource.matchAll(attributePattern)) {
1170
- attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
1171
- }
1172
- return attributes;
1173
- }
1174
- function collectTextNodes(source) {
1175
- const nodes = [];
1176
- const elements = [];
1177
- let index = 0;
1178
- const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
1179
- if (!rawText) return;
1180
- const path = elements.map((element) => element.name);
1181
- const parent = elements[elements.length - 1];
1182
- nodes.push({
1183
- context: {
1184
- ancestorTags: path.slice(0, -1),
1185
- parentAttributes: { ...parent?.attributes ?? {} },
1186
- parentTag: parent?.name ?? "",
1187
- path
1188
- },
1189
- decodedText: decodeXmlText(rawText),
1190
- end,
1191
- sourceEnd,
1192
- sourceStart,
1193
- start
1194
- });
1195
- };
1196
- while (index < source.length) {
1197
- if (source[index] !== "<") {
1198
- const nextTag = source.indexOf("<", index);
1199
- const end2 = nextTag === -1 ? source.length : nextTag;
1200
- addText(index, end2, source.slice(index, end2));
1201
- index = end2;
1202
- continue;
1203
- }
1204
- if (source.startsWith("<!--", index)) {
1205
- const end2 = source.indexOf("-->", index + 4);
1206
- index = end2 === -1 ? source.length : end2 + 3;
1207
- continue;
1208
- }
1209
- if (source.startsWith("<![CDATA[", index)) {
1210
- const contentStart = index + 9;
1211
- const end2 = source.indexOf("]]>", contentStart);
1212
- const contentEnd = end2 === -1 ? source.length : end2;
1213
- addText(
1214
- contentStart,
1215
- contentEnd,
1216
- source.slice(contentStart, contentEnd),
1217
- index,
1218
- end2 === -1 ? source.length : end2 + 3
1219
- );
1220
- index = end2 === -1 ? source.length : end2 + 3;
1221
- continue;
1222
- }
1223
- if (source.startsWith("<?", index)) {
1224
- const end2 = source.indexOf("?>", index + 2);
1225
- index = end2 === -1 ? source.length : end2 + 2;
1226
- continue;
1227
- }
1228
- if (source.startsWith("</", index)) {
1229
- const end2 = findTagEnd(source, index + 2);
1230
- elements.pop();
1231
- index = end2 + 1;
1232
- continue;
1233
- }
1234
- const end = findTagEnd(source, index + 1);
1235
- const tag = source.slice(index, end + 1);
1236
- const name = readTagName(tag);
1237
- if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
1238
- index = end + 1;
1239
- }
1240
- return nodes;
1241
- }
1242
- function extractSsmlText(ssml) {
1243
- parseSsml(ssml);
1244
- return collectTextNodes(ssml).map((node) => node.decodedText);
1245
- }
1246
- async function mapSsmlTextNodes(ssml, transform, options = {}) {
1247
- parseSsml(ssml);
1248
- const nodes = collectTextNodes(ssml);
1249
- const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
1250
- const replacements = await Promise.all(
1251
- nodes.map(async (node) => {
1252
- const context = {
1253
- ancestorTags: [...node.context.ancestorTags],
1254
- parentAttributes: { ...node.context.parentAttributes },
1255
- parentTag: node.context.parentTag,
1256
- path: [...node.context.path]
1257
- };
1258
- const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
1259
- if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
1260
- const transformed = await transform(node.decodedText, context);
1261
- if (typeof transformed !== "string") {
1262
- throw new TypeError("SSML text node transform must return a string");
1263
- }
1264
- return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
1265
- })
1266
- );
1267
- let result = "";
1268
- let cursor = 0;
1269
- nodes.forEach((node, nodeIndex) => {
1270
- result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
1271
- cursor = node.sourceEnd;
1272
- });
1273
- return result + ssml.slice(cursor);
1274
- }
1275
-
1276
1414
  // packages/ssml-core/src/migration.ts
1277
1415
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1278
- function elementName(element) {
1416
+ function elementName2(element) {
1279
1417
  switch (element.type) {
1280
1418
  case "custom":
1281
1419
  case "element":
@@ -1428,7 +1566,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
1428
1566
  }
1429
1567
  return;
1430
1568
  }
1431
- const tag = elementName(node);
1569
+ const tag = elementName2(node);
1432
1570
  if (skipTags.has(tag.toLowerCase())) return;
1433
1571
  visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
1434
1572
  });
@@ -1474,7 +1612,7 @@ function serializeDocument2(document) {
1474
1612
  const serialize = (node) => {
1475
1613
  if (typeof node === "string") return node.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1476
1614
  if (node.type === "text") return serialize(node.value);
1477
- const tag = elementName(node);
1615
+ const tag = elementName2(node);
1478
1616
  const nodeAttributes = elementAttributes(node);
1479
1617
  const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, "&quot;")}"`).join("");
1480
1618
  const children = childrenOf(node).map(serialize).join("");
@@ -1488,7 +1626,7 @@ function flatten(document) {
1488
1626
  nodes.forEach((node, index) => {
1489
1627
  if (typeof node === "string" || node.type === "text") return;
1490
1628
  const currentPath = `${path}/${index}`;
1491
- result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
1629
+ result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
1492
1630
  visit(childrenOf(node), currentPath);
1493
1631
  });
1494
1632
  };
@@ -1686,6 +1824,86 @@ var AZURE_VOICE_DEFINITIONS = [
1686
1824
  ];
1687
1825
 
1688
1826
  // packages/ssml-core/src/azureValidation.ts
1827
+ function createAzureUrlValidatorRunner(validator, options = {}) {
1828
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
1829
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
1830
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
1831
+ const inFlight = /* @__PURE__ */ new Map();
1832
+ const waiters = [];
1833
+ let active = 0;
1834
+ const configuredSignal = options.signal ?? new AbortController().signal;
1835
+ const acquire = async (signal) => {
1836
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1837
+ if (active < concurrency) {
1838
+ active += 1;
1839
+ return;
1840
+ }
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
+ });
1856
+ active += 1;
1857
+ };
1858
+ const release = () => {
1859
+ active -= 1;
1860
+ waiters.shift()?.();
1861
+ };
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);
1866
+ if (cached !== void 0) return cached;
1867
+ const existing = inFlight.get(key);
1868
+ if (existing) return existing;
1869
+ const promise = (async () => {
1870
+ await acquire(signal);
1871
+ try {
1872
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1873
+ const validation = Promise.resolve(validator(url, context, signal));
1874
+ let timer;
1875
+ let abortHandler;
1876
+ const cancellation = new Promise((_resolve, reject) => {
1877
+ abortHandler = () => reject(new Error("URL validation was aborted."));
1878
+ signal.addEventListener("abort", abortHandler, { once: true });
1879
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1880
+ timer = setTimeout(
1881
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
1882
+ options.timeoutMs
1883
+ );
1884
+ }
1885
+ });
1886
+ try {
1887
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1888
+ cache.set(key, result);
1889
+ return result;
1890
+ } finally {
1891
+ if (timer) clearTimeout(timer);
1892
+ if (abortHandler) signal.removeEventListener("abort", abortHandler);
1893
+ }
1894
+ } finally {
1895
+ release();
1896
+ }
1897
+ })();
1898
+ inFlight.set(key, promise);
1899
+ try {
1900
+ return await promise;
1901
+ } finally {
1902
+ inFlight.delete(key);
1903
+ }
1904
+ };
1905
+ return (url, context, signal) => check(url, context, signal ?? configuredSignal);
1906
+ }
1689
1907
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1690
1908
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1691
1909
  "characters",
@@ -1980,23 +2198,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
1980
2198
  );
1981
2199
  }
1982
2200
  }
1983
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
2201
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
1984
2202
  const src = attr(token, "src");
1985
2203
  if (!src) {
1986
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2204
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
1987
2205
  return;
1988
2206
  }
1989
2207
  let parsed;
1990
2208
  try {
1991
2209
  parsed = new URL(src);
1992
2210
  } catch {
1993
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2211
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
1994
2212
  return;
1995
2213
  }
1996
2214
  if (parsed.username || parsed.password)
1997
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
2215
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
1998
2216
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1999
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2217
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
2000
2218
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
2001
2219
  try {
2002
2220
  const configured = new URL(allowedOrigin);
@@ -2008,13 +2226,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2008
2226
  }
2009
2227
  }) ?? false;
2010
2228
  if (options.allowedAudioOrigins && !isAllowedOrigin)
2011
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2229
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
2012
2230
  else if (!isAllowedOrigin && !options.allowExternalAudio)
2013
2231
  addDiagnostic(
2014
2232
  diagnostics,
2015
2233
  source,
2016
2234
  token.start,
2017
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2235
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2018
2236
  );
2019
2237
  }
2020
2238
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -2332,6 +2550,15 @@ function validateAzureSsml(ssml, options = {}) {
2332
2550
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2333
2551
  const validator = options.urlValidator ?? options.customUrlValidator;
2334
2552
  if (!validator || typeof ssml !== "string") return diagnostics;
2553
+ const runnerOptions = options.urlValidation ?? {};
2554
+ const boundedValidator = createAzureUrlValidatorRunner(validator, {
2555
+ ...runnerOptions,
2556
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2557
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2558
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2559
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2560
+ });
2561
+ const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
2335
2562
  let tokens;
2336
2563
  try {
2337
2564
  tokens = tokenizeElements(ssml);
@@ -2341,7 +2568,11 @@ function validateAzureSsml(ssml, options = {}) {
2341
2568
  const checks = tokens.flatMap(
2342
2569
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2343
2570
  try {
2344
- const result = await validator(value, { tag: token.name, attribute });
2571
+ const result = await boundedValidator(
2572
+ value,
2573
+ { tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
2574
+ validationSignal
2575
+ );
2345
2576
  const valid = typeof result === "boolean" ? result : result.valid;
2346
2577
  if (!valid) {
2347
2578
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -2387,13 +2618,15 @@ export {
2387
2618
  buildSsml,
2388
2619
  parseSsml,
2389
2620
  buildPartialSsml,
2390
- splitSsmlDocument,
2391
- validateSsml,
2621
+ getSsmlSourceMap,
2392
2622
  extractSsmlText,
2393
2623
  mapSsmlTextNodes,
2624
+ splitSsmlDocument,
2625
+ validateSsml,
2394
2626
  extractSsmlTranslatableText,
2395
2627
  fromPlainTextToSsml,
2396
2628
  validateSsmlStructureIntegrity,
2629
+ createAzureUrlValidatorRunner,
2397
2630
  isValidAzureAudioDuration,
2398
2631
  normalizeAzureLanguage,
2399
2632
  areAzureLanguagesEquivalent,
@@ -2401,4 +2634,4 @@ export {
2401
2634
  getAzureVoiceCatalogMetadata,
2402
2635
  getBuiltInVoiceCatalogMetadata
2403
2636
  };
2404
- //# sourceMappingURL=chunk-25LOR4AJ.mjs.map
2637
+ //# sourceMappingURL=chunk-FXUM45ZY.mjs.map