ssml-builder-js 2.13.0 → 2.14.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 +8 -0
- package/dist/{chunk-25LOR4AJ.mjs → chunk-AQ55MOPU.mjs} +115 -14
- package/dist/chunk-AQ55MOPU.mjs.map +1 -0
- package/dist/{chunk-CZ2F3TET.mjs → chunk-QFIBPCO4.mjs} +80 -9
- package/dist/chunk-QFIBPCO4.mjs.map +1 -0
- package/dist/{chunk-LCTE26LS.mjs → chunk-UZSCXPC5.mjs} +2 -2
- package/dist/core.d.mts +18 -1
- package/dist/core.d.ts +18 -1
- package/dist/core.js +115 -13
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +79 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-Xbr6ZWnK.d.mts → index.d-CUXRwSw0.d.mts} +19 -1
- package/dist/{index.d-Xbr6ZWnK.d.ts → index.d-CUXRwSw0.d.ts} +19 -1
- package/dist/index.d.mts +66 -10
- package/dist/index.d.ts +66 -10
- package/dist/index.js +557 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -17
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +5 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.js +106 -12
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +29 -6
- 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-UZSCXPC5.mjs.map} +0 -0
package/dist/core.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(core_exports, {
|
|
|
30
30
|
areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
|
|
31
31
|
buildPartialSsml: () => buildPartialSsml,
|
|
32
32
|
buildSsml: () => buildSsml,
|
|
33
|
+
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
33
34
|
extractSsmlText: () => extractSsmlText,
|
|
34
35
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
35
36
|
fromPlainTextToSsml: () => fromPlainTextToSsml,
|
|
@@ -1076,6 +1077,34 @@ function collectInheritedContext(nodes) {
|
|
|
1076
1077
|
nodes.forEach(visit);
|
|
1077
1078
|
return context;
|
|
1078
1079
|
}
|
|
1080
|
+
function elementName(node) {
|
|
1081
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1082
|
+
}
|
|
1083
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1084
|
+
let textOffset = 0;
|
|
1085
|
+
let firstPath;
|
|
1086
|
+
let foundPath;
|
|
1087
|
+
const visit = (node, path) => {
|
|
1088
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1089
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1090
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1091
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1092
|
+
textOffset += text.length;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
node.children?.forEach((child, index) => {
|
|
1096
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1097
|
+
visit(child, childPath);
|
|
1098
|
+
});
|
|
1099
|
+
};
|
|
1100
|
+
nodes.forEach((node, index) => {
|
|
1101
|
+
if (!foundPath) {
|
|
1102
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1103
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
return foundPath ?? firstPath;
|
|
1107
|
+
}
|
|
1079
1108
|
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1080
1109
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1081
1110
|
const text = nodes.map(textFromNode).join("");
|
|
@@ -1091,7 +1120,8 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1091
1120
|
containedMarks: marks,
|
|
1092
1121
|
hasBackgroundAudio: chunkNodes.some(
|
|
1093
1122
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1094
|
-
)
|
|
1123
|
+
),
|
|
1124
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
1095
1125
|
};
|
|
1096
1126
|
}
|
|
1097
1127
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1317,7 +1347,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1317
1347
|
|
|
1318
1348
|
// packages/ssml-core/src/migration.ts
|
|
1319
1349
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1320
|
-
function
|
|
1350
|
+
function elementName2(element) {
|
|
1321
1351
|
switch (element.type) {
|
|
1322
1352
|
case "custom":
|
|
1323
1353
|
case "element":
|
|
@@ -1470,7 +1500,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1470
1500
|
}
|
|
1471
1501
|
return;
|
|
1472
1502
|
}
|
|
1473
|
-
const tag =
|
|
1503
|
+
const tag = elementName2(node);
|
|
1474
1504
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1475
1505
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1476
1506
|
});
|
|
@@ -1516,7 +1546,7 @@ function serializeDocument2(document) {
|
|
|
1516
1546
|
const serialize = (node) => {
|
|
1517
1547
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1518
1548
|
if (node.type === "text") return serialize(node.value);
|
|
1519
|
-
const tag =
|
|
1549
|
+
const tag = elementName2(node);
|
|
1520
1550
|
const nodeAttributes = elementAttributes(node);
|
|
1521
1551
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1522
1552
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1530,7 +1560,7 @@ function flatten(document) {
|
|
|
1530
1560
|
nodes.forEach((node, index) => {
|
|
1531
1561
|
if (typeof node === "string" || node.type === "text") return;
|
|
1532
1562
|
const currentPath = `${path}/${index}`;
|
|
1533
|
-
result.push({ name:
|
|
1563
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1534
1564
|
visit(childrenOf(node), currentPath);
|
|
1535
1565
|
});
|
|
1536
1566
|
};
|
|
@@ -1728,6 +1758,69 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1728
1758
|
];
|
|
1729
1759
|
|
|
1730
1760
|
// packages/ssml-core/src/azureValidation.ts
|
|
1761
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1762
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1763
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1764
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1765
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1766
|
+
const waiters = [];
|
|
1767
|
+
let active = 0;
|
|
1768
|
+
const acquire = async () => {
|
|
1769
|
+
if (active < concurrency) {
|
|
1770
|
+
active += 1;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
1774
|
+
active += 1;
|
|
1775
|
+
};
|
|
1776
|
+
const release = () => {
|
|
1777
|
+
active -= 1;
|
|
1778
|
+
waiters.shift()?.();
|
|
1779
|
+
};
|
|
1780
|
+
const check = async (url, context) => {
|
|
1781
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1782
|
+
const cached = cache.get(url);
|
|
1783
|
+
if (cached !== void 0) return cached;
|
|
1784
|
+
const existing = inFlight.get(url);
|
|
1785
|
+
if (existing) return existing;
|
|
1786
|
+
const promise = (async () => {
|
|
1787
|
+
await acquire();
|
|
1788
|
+
try {
|
|
1789
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1790
|
+
const validation = Promise.resolve(validator(url, context));
|
|
1791
|
+
let timer;
|
|
1792
|
+
let abortHandler;
|
|
1793
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1794
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1795
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1796
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1797
|
+
timer = setTimeout(
|
|
1798
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1799
|
+
options.timeoutMs
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
try {
|
|
1804
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1805
|
+
cache.set(url, result);
|
|
1806
|
+
return result;
|
|
1807
|
+
} finally {
|
|
1808
|
+
if (timer) clearTimeout(timer);
|
|
1809
|
+
if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
|
|
1810
|
+
}
|
|
1811
|
+
} finally {
|
|
1812
|
+
release();
|
|
1813
|
+
}
|
|
1814
|
+
})();
|
|
1815
|
+
inFlight.set(url, promise);
|
|
1816
|
+
try {
|
|
1817
|
+
return await promise;
|
|
1818
|
+
} finally {
|
|
1819
|
+
inFlight.delete(url);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
return (url, context) => check(url, context);
|
|
1823
|
+
}
|
|
1731
1824
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1732
1825
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1733
1826
|
"characters",
|
|
@@ -2022,23 +2115,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
2022
2115
|
);
|
|
2023
2116
|
}
|
|
2024
2117
|
}
|
|
2025
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2118
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
2026
2119
|
const src = attr(token, "src");
|
|
2027
2120
|
if (!src) {
|
|
2028
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2121
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
2029
2122
|
return;
|
|
2030
2123
|
}
|
|
2031
2124
|
let parsed;
|
|
2032
2125
|
try {
|
|
2033
2126
|
parsed = new URL(src);
|
|
2034
2127
|
} catch {
|
|
2035
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2128
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
2036
2129
|
return;
|
|
2037
2130
|
}
|
|
2038
2131
|
if (parsed.username || parsed.password)
|
|
2039
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2132
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
2040
2133
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2041
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2134
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2042
2135
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2043
2136
|
try {
|
|
2044
2137
|
const configured = new URL(allowedOrigin);
|
|
@@ -2050,13 +2143,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2050
2143
|
}
|
|
2051
2144
|
}) ?? false;
|
|
2052
2145
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2053
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2146
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2054
2147
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2055
2148
|
addDiagnostic(
|
|
2056
2149
|
diagnostics,
|
|
2057
2150
|
source,
|
|
2058
2151
|
token.start,
|
|
2059
|
-
`<${
|
|
2152
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2060
2153
|
);
|
|
2061
2154
|
}
|
|
2062
2155
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2374,6 +2467,14 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2374
2467
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2375
2468
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2376
2469
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2470
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2471
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2472
|
+
...runnerOptions,
|
|
2473
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2474
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2475
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2476
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2477
|
+
});
|
|
2377
2478
|
let tokens;
|
|
2378
2479
|
try {
|
|
2379
2480
|
tokens = tokenizeElements(ssml);
|
|
@@ -2383,7 +2484,7 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2383
2484
|
const checks = tokens.flatMap(
|
|
2384
2485
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2385
2486
|
try {
|
|
2386
|
-
const result = await
|
|
2487
|
+
const result = await boundedValidator(value, { tag: token.name, attribute });
|
|
2387
2488
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2388
2489
|
if (!valid) {
|
|
2389
2490
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2429,6 +2530,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2429
2530
|
areAzureLanguagesEquivalent,
|
|
2430
2531
|
buildPartialSsml,
|
|
2431
2532
|
buildSsml,
|
|
2533
|
+
createAzureUrlValidatorRunner,
|
|
2432
2534
|
extractSsmlText,
|
|
2433
2535
|
extractSsmlTranslatableText,
|
|
2434
2536
|
fromPlainTextToSsml,
|