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.
- package/README.md +12 -2
- package/dist/{chunk-LCTE26LS.mjs → chunk-BDY2Q2JL.mjs} +2 -2
- package/dist/{chunk-25LOR4AJ.mjs → chunk-FXUM45ZY.mjs} +402 -169
- package/dist/chunk-FXUM45ZY.mjs.map +1 -0
- package/dist/{chunk-CZ2F3TET.mjs → chunk-WXFLUCLR.mjs} +227 -9
- package/dist/chunk-WXFLUCLR.mjs.map +1 -0
- package/dist/core.d.mts +65 -17
- package/dist/core.d.ts +65 -17
- package/dist/core.js +401 -166
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +5 -1
- package/dist/elements.js +101 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-Xbr6ZWnK.d.mts → index.d-8BvkB9gz.d.mts} +40 -2
- package/dist/{index.d-Xbr6ZWnK.d.ts → index.d-8BvkB9gz.d.ts} +40 -2
- package/dist/index.d.mts +170 -18
- package/dist/index.d.ts +170 -18
- package/dist/index.js +1512 -489
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +612 -47
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +15 -2
- package/dist/react.d.ts +15 -2
- package/dist/react.js +204 -23
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +105 -17
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-25LOR4AJ.mjs.map +0 -1
- package/dist/chunk-CZ2F3TET.mjs.map +0 -1
- /package/dist/{chunk-LCTE26LS.mjs.map → chunk-BDY2Q2JL.mjs.map} +0 -0
|
@@ -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 === "<") return "<";
|
|
934
|
+
if (entity === """) 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 {
|
|
@@ -1059,6 +1183,86 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1059
1183
|
},
|
|
1060
1184
|
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
1061
1185
|
];
|
|
1186
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1187
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1188
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1189
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1190
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1191
|
+
const waiters = [];
|
|
1192
|
+
let active = 0;
|
|
1193
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1194
|
+
const acquire = async (signal) => {
|
|
1195
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1196
|
+
if (active < concurrency) {
|
|
1197
|
+
active += 1;
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
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
|
+
});
|
|
1215
|
+
active += 1;
|
|
1216
|
+
};
|
|
1217
|
+
const release = () => {
|
|
1218
|
+
active -= 1;
|
|
1219
|
+
waiters.shift()?.();
|
|
1220
|
+
};
|
|
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);
|
|
1225
|
+
if (cached !== void 0) return cached;
|
|
1226
|
+
const existing = inFlight.get(key);
|
|
1227
|
+
if (existing) return existing;
|
|
1228
|
+
const promise = (async () => {
|
|
1229
|
+
await acquire(signal);
|
|
1230
|
+
try {
|
|
1231
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1232
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1233
|
+
let timer;
|
|
1234
|
+
let abortHandler;
|
|
1235
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1236
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1237
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1238
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1239
|
+
timer = setTimeout(
|
|
1240
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1241
|
+
options.timeoutMs
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
try {
|
|
1246
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1247
|
+
cache.set(key, result);
|
|
1248
|
+
return result;
|
|
1249
|
+
} finally {
|
|
1250
|
+
if (timer) clearTimeout(timer);
|
|
1251
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1252
|
+
}
|
|
1253
|
+
} finally {
|
|
1254
|
+
release();
|
|
1255
|
+
}
|
|
1256
|
+
})();
|
|
1257
|
+
inFlight.set(key, promise);
|
|
1258
|
+
try {
|
|
1259
|
+
return await promise;
|
|
1260
|
+
} finally {
|
|
1261
|
+
inFlight.delete(key);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1265
|
+
}
|
|
1062
1266
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1063
1267
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1064
1268
|
"characters",
|
|
@@ -1343,23 +1547,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
1343
1547
|
);
|
|
1344
1548
|
}
|
|
1345
1549
|
}
|
|
1346
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
1550
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
1347
1551
|
const src = attr(token, "src");
|
|
1348
1552
|
if (!src) {
|
|
1349
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
1553
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
1350
1554
|
return;
|
|
1351
1555
|
}
|
|
1352
1556
|
let parsed;
|
|
1353
1557
|
try {
|
|
1354
1558
|
parsed = new URL(src);
|
|
1355
1559
|
} catch {
|
|
1356
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
1560
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
1357
1561
|
return;
|
|
1358
1562
|
}
|
|
1359
1563
|
if (parsed.username || parsed.password)
|
|
1360
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
1564
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
1361
1565
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1362
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
1566
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
1363
1567
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1364
1568
|
try {
|
|
1365
1569
|
const configured = new URL(allowedOrigin);
|
|
@@ -1371,13 +1575,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
1371
1575
|
}
|
|
1372
1576
|
}) ?? false;
|
|
1373
1577
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
1374
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
1578
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
1375
1579
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
1376
1580
|
addDiagnostic(
|
|
1377
1581
|
diagnostics,
|
|
1378
1582
|
source,
|
|
1379
1583
|
token.start,
|
|
1380
|
-
`<${
|
|
1584
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1381
1585
|
);
|
|
1382
1586
|
}
|
|
1383
1587
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -1695,6 +1899,15 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1695
1899
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
1696
1900
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1697
1901
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
1902
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
1903
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
1904
|
+
...runnerOptions,
|
|
1905
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1906
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1907
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
1908
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1909
|
+
});
|
|
1910
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
1698
1911
|
let tokens;
|
|
1699
1912
|
try {
|
|
1700
1913
|
tokens = tokenizeElements(ssml);
|
|
@@ -1704,7 +1917,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1704
1917
|
const checks = tokens.flatMap(
|
|
1705
1918
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
1706
1919
|
try {
|
|
1707
|
-
const result = await
|
|
1920
|
+
const result = await boundedValidator(
|
|
1921
|
+
value,
|
|
1922
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
1923
|
+
validationSignal
|
|
1924
|
+
);
|
|
1708
1925
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
1709
1926
|
if (!valid) {
|
|
1710
1927
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -1739,7 +1956,8 @@ export {
|
|
|
1739
1956
|
buildSsml,
|
|
1740
1957
|
parseSsml,
|
|
1741
1958
|
buildPartialSsml,
|
|
1959
|
+
getSsmlSourceMap,
|
|
1742
1960
|
validateSsml,
|
|
1743
1961
|
validateAzureSsml
|
|
1744
1962
|
};
|
|
1745
|
-
//# sourceMappingURL=chunk-
|
|
1963
|
+
//# sourceMappingURL=chunk-WXFLUCLR.mjs.map
|