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.
@@ -925,6 +925,130 @@ function buildPartialSsml(textOrOptions, context) {
925
925
  }
926
926
  return serializePartialSsml(textOrOptions.text, textOrOptions);
927
927
  }
928
+ function decodeXmlText(value) {
929
+ return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
930
+ if (entity === "&") return "&";
931
+ if (entity === "'") return "'";
932
+ if (entity === ">") return ">";
933
+ if (entity === "&lt;") return "<";
934
+ if (entity === "&quot;") return '"';
935
+ const hexadecimal = entity.toLowerCase().startsWith("&#x");
936
+ const digits = entity.slice(hexadecimal ? 3 : 2, -1);
937
+ return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
938
+ });
939
+ }
940
+ function decodeXmlAttribute(value) {
941
+ return decodeXmlText(value);
942
+ }
943
+ function findTagEnd(source, start) {
944
+ let quote = "";
945
+ for (let index = start; index < source.length; index += 1) {
946
+ const character = source[index];
947
+ if (quote) {
948
+ if (character === quote) quote = "";
949
+ } else if (character === '"' || character === "'") {
950
+ quote = character;
951
+ } else if (character === ">") {
952
+ return index;
953
+ }
954
+ }
955
+ return source.length - 1;
956
+ }
957
+ function readTagName(tag) {
958
+ const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
959
+ return match?.[1];
960
+ }
961
+ function readTagAttributes(tag, name) {
962
+ const attributes = {};
963
+ const nameStart = tag.indexOf(name);
964
+ const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
965
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
966
+ for (const match of attributeSource.matchAll(attributePattern)) {
967
+ attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
968
+ }
969
+ return attributes;
970
+ }
971
+ function collectSourceMap(source) {
972
+ const segments = [];
973
+ const markers = [];
974
+ const elements = [];
975
+ let textOffset = 0;
976
+ let index = 0;
977
+ const textParts = [];
978
+ const addText = (value) => {
979
+ if (!value) return;
980
+ const parent = elements[elements.length - 1];
981
+ if (parent) parent.nextChildIndex += 1;
982
+ const sourceNodePath = parent?.path ?? ["speak"];
983
+ const start = textOffset;
984
+ textOffset += value.length;
985
+ textParts.push(value);
986
+ segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
987
+ };
988
+ while (index < source.length) {
989
+ if (source[index] !== "<") {
990
+ const end2 = source.indexOf("<", index);
991
+ const textEnd = end2 === -1 ? source.length : end2;
992
+ addText(decodeXmlText(source.slice(index, textEnd)));
993
+ index = textEnd;
994
+ continue;
995
+ }
996
+ if (source.startsWith("<!--", index)) {
997
+ const end2 = source.indexOf("-->", index + 4);
998
+ index = end2 === -1 ? source.length : end2 + 3;
999
+ continue;
1000
+ }
1001
+ if (source.startsWith("<![CDATA[", index)) {
1002
+ const contentStart = index + 9;
1003
+ const end2 = source.indexOf("]]>", contentStart);
1004
+ const contentEnd = end2 === -1 ? source.length : end2;
1005
+ addText(source.slice(contentStart, contentEnd));
1006
+ index = end2 === -1 ? source.length : end2 + 3;
1007
+ continue;
1008
+ }
1009
+ if (source.startsWith("<?", index)) {
1010
+ const end2 = source.indexOf("?>", index + 2);
1011
+ index = end2 === -1 ? source.length : end2 + 2;
1012
+ continue;
1013
+ }
1014
+ const end = findTagEnd(source, index + 1);
1015
+ const rawTag = source.slice(index, end + 1);
1016
+ if (rawTag.startsWith("</")) {
1017
+ elements.pop();
1018
+ index = end + 1;
1019
+ continue;
1020
+ }
1021
+ const name = readTagName(rawTag);
1022
+ if (!name) {
1023
+ index = end + 1;
1024
+ continue;
1025
+ }
1026
+ const parent = elements[elements.length - 1];
1027
+ const childIndex = parent?.nextChildIndex ?? 0;
1028
+ if (parent) parent.nextChildIndex += 1;
1029
+ const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
1030
+ const attributes = readTagAttributes(rawTag, name);
1031
+ const normalizedName = name.toLowerCase();
1032
+ if (normalizedName === "mark" || normalizedName === "bookmark") {
1033
+ const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
1034
+ if (markerName) {
1035
+ markers.push({
1036
+ kind: normalizedName,
1037
+ name: markerName,
1038
+ originalTextRange: { start: textOffset, end: textOffset },
1039
+ sourceNodePath: [...path]
1040
+ });
1041
+ }
1042
+ }
1043
+ if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
1044
+ index = end + 1;
1045
+ }
1046
+ return { text: textParts.join(""), segments, markers };
1047
+ }
1048
+ function getSsmlSourceMap(ssml) {
1049
+ parseSsml(ssml);
1050
+ return collectSourceMap(ssml);
1051
+ }
928
1052
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
929
1053
  function validateSsml(xmlString) {
930
1054
  try {
@@ -1066,34 +1190,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1066
1190
  const inFlight = /* @__PURE__ */ new Map();
1067
1191
  const waiters = [];
1068
1192
  let active = 0;
1069
- const acquire = async () => {
1193
+ const configuredSignal = options.signal ?? new AbortController().signal;
1194
+ const acquire = async (signal) => {
1195
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1070
1196
  if (active < concurrency) {
1071
1197
  active += 1;
1072
1198
  return;
1073
1199
  }
1074
- await new Promise((resolve) => waiters.push(resolve));
1200
+ await new Promise((resolve, reject) => {
1201
+ let waiter;
1202
+ const abortHandler = () => {
1203
+ const index = waiters.indexOf(waiter);
1204
+ if (index >= 0) waiters.splice(index, 1);
1205
+ signal.removeEventListener("abort", abortHandler);
1206
+ reject(new Error("URL validation was aborted."));
1207
+ };
1208
+ signal.addEventListener("abort", abortHandler, { once: true });
1209
+ waiter = () => {
1210
+ signal.removeEventListener("abort", abortHandler);
1211
+ resolve();
1212
+ };
1213
+ waiters.push(waiter);
1214
+ });
1075
1215
  active += 1;
1076
1216
  };
1077
1217
  const release = () => {
1078
1218
  active -= 1;
1079
1219
  waiters.shift()?.();
1080
1220
  };
1081
- const check = async (url, context) => {
1082
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1083
- const cached = cache.get(url);
1221
+ const check = async (url, context, signal = configuredSignal) => {
1222
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1223
+ const key = `${context.tag}:${context.attribute}:${url}`;
1224
+ const cached = cache.get(key);
1084
1225
  if (cached !== void 0) return cached;
1085
- const existing = inFlight.get(url);
1226
+ const existing = inFlight.get(key);
1086
1227
  if (existing) return existing;
1087
1228
  const promise = (async () => {
1088
- await acquire();
1229
+ await acquire(signal);
1089
1230
  try {
1090
- if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1091
- const validation = Promise.resolve(validator(url, context));
1231
+ if (signal.aborted) throw new Error("URL validation was aborted.");
1232
+ const validation = Promise.resolve(validator(url, context, signal));
1092
1233
  let timer;
1093
1234
  let abortHandler;
1094
1235
  const cancellation = new Promise((_resolve, reject) => {
1095
1236
  abortHandler = () => reject(new Error("URL validation was aborted."));
1096
- options.signal?.addEventListener("abort", abortHandler, { once: true });
1237
+ signal.addEventListener("abort", abortHandler, { once: true });
1097
1238
  if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1098
1239
  timer = setTimeout(
1099
1240
  () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
@@ -1103,24 +1244,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
1103
1244
  });
1104
1245
  try {
1105
1246
  const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1106
- cache.set(url, result);
1247
+ cache.set(key, result);
1107
1248
  return result;
1108
1249
  } finally {
1109
1250
  if (timer) clearTimeout(timer);
1110
- if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1251
+ if (abortHandler) signal.removeEventListener("abort", abortHandler);
1111
1252
  }
1112
1253
  } finally {
1113
1254
  release();
1114
1255
  }
1115
1256
  })();
1116
- inFlight.set(url, promise);
1257
+ inFlight.set(key, promise);
1117
1258
  try {
1118
1259
  return await promise;
1119
1260
  } finally {
1120
- inFlight.delete(url);
1261
+ inFlight.delete(key);
1121
1262
  }
1122
1263
  };
1123
- return (url, context) => check(url, context);
1264
+ return (url, context, signal) => check(url, context, signal ?? configuredSignal);
1124
1265
  }
1125
1266
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1126
1267
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
@@ -1766,6 +1907,7 @@ function validateAzureSsml(ssml, options = {}) {
1766
1907
  ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
1767
1908
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
1768
1909
  });
1910
+ const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
1769
1911
  let tokens;
1770
1912
  try {
1771
1913
  tokens = tokenizeElements(ssml);
@@ -1775,7 +1917,11 @@ function validateAzureSsml(ssml, options = {}) {
1775
1917
  const checks = tokens.flatMap(
1776
1918
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
1777
1919
  try {
1778
- const result = await boundedValidator(value, { tag: token.name, attribute });
1920
+ const result = await boundedValidator(
1921
+ value,
1922
+ { tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
1923
+ validationSignal
1924
+ );
1779
1925
  const valid = typeof result === "boolean" ? result : result.valid;
1780
1926
  if (!valid) {
1781
1927
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -1810,7 +1956,8 @@ export {
1810
1956
  buildSsml,
1811
1957
  parseSsml,
1812
1958
  buildPartialSsml,
1959
+ getSsmlSourceMap,
1813
1960
  validateSsml,
1814
1961
  validateAzureSsml
1815
1962
  };
1816
- //# sourceMappingURL=chunk-QFIBPCO4.mjs.map
1963
+ //# sourceMappingURL=chunk-WXFLUCLR.mjs.map