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
package/dist/core.js
CHANGED
|
@@ -30,11 +30,13 @@ __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,
|
|
36
37
|
getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
|
|
37
38
|
getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
|
|
39
|
+
getSsmlSourceMap: () => getSsmlSourceMap,
|
|
38
40
|
isValidAzureAudioDuration: () => isValidAzureAudioDuration,
|
|
39
41
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
40
42
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
@@ -973,6 +975,236 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
973
975
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
974
976
|
}
|
|
975
977
|
|
|
978
|
+
// packages/ssml-core/src/textNodes.ts
|
|
979
|
+
function decodeXmlText(value) {
|
|
980
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
981
|
+
if (entity === "&") return "&";
|
|
982
|
+
if (entity === "'") return "'";
|
|
983
|
+
if (entity === ">") return ">";
|
|
984
|
+
if (entity === "<") return "<";
|
|
985
|
+
if (entity === """) return '"';
|
|
986
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
987
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
988
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function encodeXmlText(value) {
|
|
992
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
993
|
+
}
|
|
994
|
+
function decodeXmlAttribute(value) {
|
|
995
|
+
return decodeXmlText(value);
|
|
996
|
+
}
|
|
997
|
+
function findTagEnd(source, start) {
|
|
998
|
+
let quote = "";
|
|
999
|
+
for (let index = start; index < source.length; index += 1) {
|
|
1000
|
+
const character = source[index];
|
|
1001
|
+
if (quote) {
|
|
1002
|
+
if (character === quote) quote = "";
|
|
1003
|
+
} else if (character === '"' || character === "'") {
|
|
1004
|
+
quote = character;
|
|
1005
|
+
} else if (character === ">") {
|
|
1006
|
+
return index;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return source.length - 1;
|
|
1010
|
+
}
|
|
1011
|
+
function readTagName(tag) {
|
|
1012
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1013
|
+
return match?.[1];
|
|
1014
|
+
}
|
|
1015
|
+
function readTagAttributes(tag, name) {
|
|
1016
|
+
const attributes = {};
|
|
1017
|
+
const nameStart = tag.indexOf(name);
|
|
1018
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1019
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1020
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1021
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1022
|
+
}
|
|
1023
|
+
return attributes;
|
|
1024
|
+
}
|
|
1025
|
+
function collectTextNodes(source) {
|
|
1026
|
+
const nodes = [];
|
|
1027
|
+
const elements = [];
|
|
1028
|
+
let index = 0;
|
|
1029
|
+
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1030
|
+
if (!rawText) return;
|
|
1031
|
+
const path = elements.map((element) => element.name);
|
|
1032
|
+
const parent = elements[elements.length - 1];
|
|
1033
|
+
nodes.push({
|
|
1034
|
+
context: {
|
|
1035
|
+
ancestorTags: path.slice(0, -1),
|
|
1036
|
+
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1037
|
+
parentTag: parent?.name ?? "",
|
|
1038
|
+
path
|
|
1039
|
+
},
|
|
1040
|
+
decodedText: decodeXmlText(rawText),
|
|
1041
|
+
end,
|
|
1042
|
+
sourceEnd,
|
|
1043
|
+
sourceStart,
|
|
1044
|
+
start
|
|
1045
|
+
});
|
|
1046
|
+
};
|
|
1047
|
+
while (index < source.length) {
|
|
1048
|
+
if (source[index] !== "<") {
|
|
1049
|
+
const nextTag = source.indexOf("<", index);
|
|
1050
|
+
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1051
|
+
addText(index, end2, source.slice(index, end2));
|
|
1052
|
+
index = end2;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (source.startsWith("<!--", index)) {
|
|
1056
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1057
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1061
|
+
const contentStart = index + 9;
|
|
1062
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1063
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1064
|
+
addText(
|
|
1065
|
+
contentStart,
|
|
1066
|
+
contentEnd,
|
|
1067
|
+
source.slice(contentStart, contentEnd),
|
|
1068
|
+
index,
|
|
1069
|
+
end2 === -1 ? source.length : end2 + 3
|
|
1070
|
+
);
|
|
1071
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (source.startsWith("<?", index)) {
|
|
1075
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1076
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (source.startsWith("</", index)) {
|
|
1080
|
+
const end2 = findTagEnd(source, index + 2);
|
|
1081
|
+
elements.pop();
|
|
1082
|
+
index = end2 + 1;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const end = findTagEnd(source, index + 1);
|
|
1086
|
+
const tag = source.slice(index, end + 1);
|
|
1087
|
+
const name = readTagName(tag);
|
|
1088
|
+
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1089
|
+
index = end + 1;
|
|
1090
|
+
}
|
|
1091
|
+
return nodes;
|
|
1092
|
+
}
|
|
1093
|
+
function collectSourceMap(source) {
|
|
1094
|
+
const segments = [];
|
|
1095
|
+
const markers = [];
|
|
1096
|
+
const elements = [];
|
|
1097
|
+
let textOffset = 0;
|
|
1098
|
+
let index = 0;
|
|
1099
|
+
const textParts = [];
|
|
1100
|
+
const addText = (value) => {
|
|
1101
|
+
if (!value) return;
|
|
1102
|
+
const parent = elements[elements.length - 1];
|
|
1103
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1104
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
1105
|
+
const start = textOffset;
|
|
1106
|
+
textOffset += value.length;
|
|
1107
|
+
textParts.push(value);
|
|
1108
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
1109
|
+
};
|
|
1110
|
+
while (index < source.length) {
|
|
1111
|
+
if (source[index] !== "<") {
|
|
1112
|
+
const end2 = source.indexOf("<", index);
|
|
1113
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
1114
|
+
addText(decodeXmlText(source.slice(index, textEnd)));
|
|
1115
|
+
index = textEnd;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (source.startsWith("<!--", index)) {
|
|
1119
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1120
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1124
|
+
const contentStart = index + 9;
|
|
1125
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1126
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1127
|
+
addText(source.slice(contentStart, contentEnd));
|
|
1128
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (source.startsWith("<?", index)) {
|
|
1132
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1133
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const end = findTagEnd(source, index + 1);
|
|
1137
|
+
const rawTag = source.slice(index, end + 1);
|
|
1138
|
+
if (rawTag.startsWith("</")) {
|
|
1139
|
+
elements.pop();
|
|
1140
|
+
index = end + 1;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const name = readTagName(rawTag);
|
|
1144
|
+
if (!name) {
|
|
1145
|
+
index = end + 1;
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const parent = elements[elements.length - 1];
|
|
1149
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
1150
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1151
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
1152
|
+
const attributes = readTagAttributes(rawTag, name);
|
|
1153
|
+
const normalizedName = name.toLowerCase();
|
|
1154
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
1155
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
1156
|
+
if (markerName) {
|
|
1157
|
+
markers.push({
|
|
1158
|
+
kind: normalizedName,
|
|
1159
|
+
name: markerName,
|
|
1160
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
1161
|
+
sourceNodePath: [...path]
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
1166
|
+
index = end + 1;
|
|
1167
|
+
}
|
|
1168
|
+
return { text: textParts.join(""), segments, markers };
|
|
1169
|
+
}
|
|
1170
|
+
function getSsmlSourceMap(ssml) {
|
|
1171
|
+
parseSsml(ssml);
|
|
1172
|
+
return collectSourceMap(ssml);
|
|
1173
|
+
}
|
|
1174
|
+
function extractSsmlText(ssml) {
|
|
1175
|
+
parseSsml(ssml);
|
|
1176
|
+
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1177
|
+
}
|
|
1178
|
+
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1179
|
+
parseSsml(ssml);
|
|
1180
|
+
const nodes = collectTextNodes(ssml);
|
|
1181
|
+
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1182
|
+
const replacements = await Promise.all(
|
|
1183
|
+
nodes.map(async (node) => {
|
|
1184
|
+
const context = {
|
|
1185
|
+
ancestorTags: [...node.context.ancestorTags],
|
|
1186
|
+
parentAttributes: { ...node.context.parentAttributes },
|
|
1187
|
+
parentTag: node.context.parentTag,
|
|
1188
|
+
path: [...node.context.path]
|
|
1189
|
+
};
|
|
1190
|
+
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1191
|
+
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1192
|
+
const transformed = await transform(node.decodedText, context);
|
|
1193
|
+
if (typeof transformed !== "string") {
|
|
1194
|
+
throw new TypeError("SSML text node transform must return a string");
|
|
1195
|
+
}
|
|
1196
|
+
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
let result = "";
|
|
1200
|
+
let cursor = 0;
|
|
1201
|
+
nodes.forEach((node, nodeIndex) => {
|
|
1202
|
+
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1203
|
+
cursor = node.sourceEnd;
|
|
1204
|
+
});
|
|
1205
|
+
return result + ssml.slice(cursor);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
976
1208
|
// packages/ssml-core/src/split.ts
|
|
977
1209
|
var DEFAULT_MAX_LENGTH = 1e4;
|
|
978
1210
|
function cloneElement(element, children) {
|
|
@@ -1076,7 +1308,35 @@ function collectInheritedContext(nodes) {
|
|
|
1076
1308
|
nodes.forEach(visit);
|
|
1077
1309
|
return context;
|
|
1078
1310
|
}
|
|
1079
|
-
function
|
|
1311
|
+
function elementName(node) {
|
|
1312
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1313
|
+
}
|
|
1314
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1315
|
+
let textOffset = 0;
|
|
1316
|
+
let firstPath;
|
|
1317
|
+
let foundPath;
|
|
1318
|
+
const visit = (node, path) => {
|
|
1319
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1320
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1321
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1322
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1323
|
+
textOffset += text.length;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
node.children?.forEach((child, index) => {
|
|
1327
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1328
|
+
visit(child, childPath);
|
|
1329
|
+
});
|
|
1330
|
+
};
|
|
1331
|
+
nodes.forEach((node, index) => {
|
|
1332
|
+
if (!foundPath) {
|
|
1333
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1334
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
return foundPath ?? firstPath;
|
|
1338
|
+
}
|
|
1339
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1080
1340
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1081
1341
|
const text = nodes.map(textFromNode).join("");
|
|
1082
1342
|
const marks = [];
|
|
@@ -1091,7 +1351,24 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1091
1351
|
containedMarks: marks,
|
|
1092
1352
|
hasBackgroundAudio: chunkNodes.some(
|
|
1093
1353
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1094
|
-
)
|
|
1354
|
+
),
|
|
1355
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
|
|
1356
|
+
sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
|
|
1357
|
+
const start = Math.max(segment.range.start, textStart);
|
|
1358
|
+
const end = Math.min(segment.range.end, textStart + text.length);
|
|
1359
|
+
return {
|
|
1360
|
+
text: segment.text.slice(start - segment.range.start, end - segment.range.start),
|
|
1361
|
+
range: { start, end },
|
|
1362
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
1363
|
+
};
|
|
1364
|
+
}),
|
|
1365
|
+
sourceMarkers: sourceMap.markers.filter(
|
|
1366
|
+
({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
|
|
1367
|
+
).map((marker) => ({
|
|
1368
|
+
...marker,
|
|
1369
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
1370
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
1371
|
+
}))
|
|
1095
1372
|
};
|
|
1096
1373
|
}
|
|
1097
1374
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1101,11 +1378,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1101
1378
|
throw new RangeError("maxLength must be a positive integer");
|
|
1102
1379
|
}
|
|
1103
1380
|
const document = parseSsml(ssml);
|
|
1381
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1104
1382
|
const backgroundAudio = (document.children ?? []).find(
|
|
1105
1383
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1106
1384
|
);
|
|
1107
1385
|
if (ssml.length <= resolvedMaxLength) {
|
|
1108
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1386
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1109
1387
|
}
|
|
1110
1388
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1111
1389
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1129,7 +1407,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1129
1407
|
}
|
|
1130
1408
|
if (group.length > 0) chunks.push(group);
|
|
1131
1409
|
if (chunks.length === 0) {
|
|
1132
|
-
const result = createChunk(
|
|
1410
|
+
const result = createChunk(
|
|
1411
|
+
document,
|
|
1412
|
+
[],
|
|
1413
|
+
0,
|
|
1414
|
+
0,
|
|
1415
|
+
backgroundAudio,
|
|
1416
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1417
|
+
sourceMap,
|
|
1418
|
+
true
|
|
1419
|
+
);
|
|
1133
1420
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1134
1421
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1135
1422
|
}
|
|
@@ -1143,7 +1430,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1143
1430
|
chunkIndex,
|
|
1144
1431
|
textStart,
|
|
1145
1432
|
backgroundAudio,
|
|
1146
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1433
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1434
|
+
sourceMap,
|
|
1435
|
+
chunkIndex === chunks.length - 1
|
|
1147
1436
|
);
|
|
1148
1437
|
textStart = result.originalTextRange.end;
|
|
1149
1438
|
return result;
|
|
@@ -1166,158 +1455,9 @@ function validateSsml(xmlString) {
|
|
|
1166
1455
|
}
|
|
1167
1456
|
}
|
|
1168
1457
|
|
|
1169
|
-
// packages/ssml-core/src/textNodes.ts
|
|
1170
|
-
function decodeXmlText(value) {
|
|
1171
|
-
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1172
|
-
if (entity === "&") return "&";
|
|
1173
|
-
if (entity === "'") return "'";
|
|
1174
|
-
if (entity === ">") return ">";
|
|
1175
|
-
if (entity === "<") return "<";
|
|
1176
|
-
if (entity === """) return '"';
|
|
1177
|
-
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1178
|
-
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1179
|
-
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
function encodeXmlText(value) {
|
|
1183
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1184
|
-
}
|
|
1185
|
-
function decodeXmlAttribute(value) {
|
|
1186
|
-
return decodeXmlText(value);
|
|
1187
|
-
}
|
|
1188
|
-
function findTagEnd(source, start) {
|
|
1189
|
-
let quote = "";
|
|
1190
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1191
|
-
const character = source[index];
|
|
1192
|
-
if (quote) {
|
|
1193
|
-
if (character === quote) quote = "";
|
|
1194
|
-
} else if (character === '"' || character === "'") {
|
|
1195
|
-
quote = character;
|
|
1196
|
-
} else if (character === ">") {
|
|
1197
|
-
return index;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
return source.length - 1;
|
|
1201
|
-
}
|
|
1202
|
-
function readTagName(tag) {
|
|
1203
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1204
|
-
return match?.[1];
|
|
1205
|
-
}
|
|
1206
|
-
function readTagAttributes(tag, name) {
|
|
1207
|
-
const attributes = {};
|
|
1208
|
-
const nameStart = tag.indexOf(name);
|
|
1209
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1210
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1211
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1212
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1213
|
-
}
|
|
1214
|
-
return attributes;
|
|
1215
|
-
}
|
|
1216
|
-
function collectTextNodes(source) {
|
|
1217
|
-
const nodes = [];
|
|
1218
|
-
const elements = [];
|
|
1219
|
-
let index = 0;
|
|
1220
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1221
|
-
if (!rawText) return;
|
|
1222
|
-
const path = elements.map((element) => element.name);
|
|
1223
|
-
const parent = elements[elements.length - 1];
|
|
1224
|
-
nodes.push({
|
|
1225
|
-
context: {
|
|
1226
|
-
ancestorTags: path.slice(0, -1),
|
|
1227
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1228
|
-
parentTag: parent?.name ?? "",
|
|
1229
|
-
path
|
|
1230
|
-
},
|
|
1231
|
-
decodedText: decodeXmlText(rawText),
|
|
1232
|
-
end,
|
|
1233
|
-
sourceEnd,
|
|
1234
|
-
sourceStart,
|
|
1235
|
-
start
|
|
1236
|
-
});
|
|
1237
|
-
};
|
|
1238
|
-
while (index < source.length) {
|
|
1239
|
-
if (source[index] !== "<") {
|
|
1240
|
-
const nextTag = source.indexOf("<", index);
|
|
1241
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1242
|
-
addText(index, end2, source.slice(index, end2));
|
|
1243
|
-
index = end2;
|
|
1244
|
-
continue;
|
|
1245
|
-
}
|
|
1246
|
-
if (source.startsWith("<!--", index)) {
|
|
1247
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1248
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1249
|
-
continue;
|
|
1250
|
-
}
|
|
1251
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1252
|
-
const contentStart = index + 9;
|
|
1253
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1254
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1255
|
-
addText(
|
|
1256
|
-
contentStart,
|
|
1257
|
-
contentEnd,
|
|
1258
|
-
source.slice(contentStart, contentEnd),
|
|
1259
|
-
index,
|
|
1260
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1261
|
-
);
|
|
1262
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
if (source.startsWith("<?", index)) {
|
|
1266
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1267
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
if (source.startsWith("</", index)) {
|
|
1271
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1272
|
-
elements.pop();
|
|
1273
|
-
index = end2 + 1;
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
const end = findTagEnd(source, index + 1);
|
|
1277
|
-
const tag = source.slice(index, end + 1);
|
|
1278
|
-
const name = readTagName(tag);
|
|
1279
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1280
|
-
index = end + 1;
|
|
1281
|
-
}
|
|
1282
|
-
return nodes;
|
|
1283
|
-
}
|
|
1284
|
-
function extractSsmlText(ssml) {
|
|
1285
|
-
parseSsml(ssml);
|
|
1286
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1287
|
-
}
|
|
1288
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1289
|
-
parseSsml(ssml);
|
|
1290
|
-
const nodes = collectTextNodes(ssml);
|
|
1291
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1292
|
-
const replacements = await Promise.all(
|
|
1293
|
-
nodes.map(async (node) => {
|
|
1294
|
-
const context = {
|
|
1295
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1296
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1297
|
-
parentTag: node.context.parentTag,
|
|
1298
|
-
path: [...node.context.path]
|
|
1299
|
-
};
|
|
1300
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1301
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1302
|
-
const transformed = await transform(node.decodedText, context);
|
|
1303
|
-
if (typeof transformed !== "string") {
|
|
1304
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1305
|
-
}
|
|
1306
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1307
|
-
})
|
|
1308
|
-
);
|
|
1309
|
-
let result = "";
|
|
1310
|
-
let cursor = 0;
|
|
1311
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1312
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1313
|
-
cursor = node.sourceEnd;
|
|
1314
|
-
});
|
|
1315
|
-
return result + ssml.slice(cursor);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1458
|
// packages/ssml-core/src/migration.ts
|
|
1319
1459
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1320
|
-
function
|
|
1460
|
+
function elementName2(element) {
|
|
1321
1461
|
switch (element.type) {
|
|
1322
1462
|
case "custom":
|
|
1323
1463
|
case "element":
|
|
@@ -1470,7 +1610,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1470
1610
|
}
|
|
1471
1611
|
return;
|
|
1472
1612
|
}
|
|
1473
|
-
const tag =
|
|
1613
|
+
const tag = elementName2(node);
|
|
1474
1614
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1475
1615
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1476
1616
|
});
|
|
@@ -1516,7 +1656,7 @@ function serializeDocument2(document) {
|
|
|
1516
1656
|
const serialize = (node) => {
|
|
1517
1657
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1518
1658
|
if (node.type === "text") return serialize(node.value);
|
|
1519
|
-
const tag =
|
|
1659
|
+
const tag = elementName2(node);
|
|
1520
1660
|
const nodeAttributes = elementAttributes(node);
|
|
1521
1661
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1522
1662
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1530,7 +1670,7 @@ function flatten(document) {
|
|
|
1530
1670
|
nodes.forEach((node, index) => {
|
|
1531
1671
|
if (typeof node === "string" || node.type === "text") return;
|
|
1532
1672
|
const currentPath = `${path}/${index}`;
|
|
1533
|
-
result.push({ name:
|
|
1673
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1534
1674
|
visit(childrenOf(node), currentPath);
|
|
1535
1675
|
});
|
|
1536
1676
|
};
|
|
@@ -1728,6 +1868,86 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1728
1868
|
];
|
|
1729
1869
|
|
|
1730
1870
|
// packages/ssml-core/src/azureValidation.ts
|
|
1871
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1872
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1873
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1874
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1875
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1876
|
+
const waiters = [];
|
|
1877
|
+
let active = 0;
|
|
1878
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1879
|
+
const acquire = async (signal) => {
|
|
1880
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1881
|
+
if (active < concurrency) {
|
|
1882
|
+
active += 1;
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
await new Promise((resolve, reject) => {
|
|
1886
|
+
let waiter;
|
|
1887
|
+
const abortHandler = () => {
|
|
1888
|
+
const index = waiters.indexOf(waiter);
|
|
1889
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
1890
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1891
|
+
reject(new Error("URL validation was aborted."));
|
|
1892
|
+
};
|
|
1893
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1894
|
+
waiter = () => {
|
|
1895
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1896
|
+
resolve();
|
|
1897
|
+
};
|
|
1898
|
+
waiters.push(waiter);
|
|
1899
|
+
});
|
|
1900
|
+
active += 1;
|
|
1901
|
+
};
|
|
1902
|
+
const release = () => {
|
|
1903
|
+
active -= 1;
|
|
1904
|
+
waiters.shift()?.();
|
|
1905
|
+
};
|
|
1906
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
1907
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1908
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
1909
|
+
const cached = cache.get(key);
|
|
1910
|
+
if (cached !== void 0) return cached;
|
|
1911
|
+
const existing = inFlight.get(key);
|
|
1912
|
+
if (existing) return existing;
|
|
1913
|
+
const promise = (async () => {
|
|
1914
|
+
await acquire(signal);
|
|
1915
|
+
try {
|
|
1916
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1917
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1918
|
+
let timer;
|
|
1919
|
+
let abortHandler;
|
|
1920
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1921
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1922
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1923
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1924
|
+
timer = setTimeout(
|
|
1925
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1926
|
+
options.timeoutMs
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
try {
|
|
1931
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1932
|
+
cache.set(key, result);
|
|
1933
|
+
return result;
|
|
1934
|
+
} finally {
|
|
1935
|
+
if (timer) clearTimeout(timer);
|
|
1936
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1937
|
+
}
|
|
1938
|
+
} finally {
|
|
1939
|
+
release();
|
|
1940
|
+
}
|
|
1941
|
+
})();
|
|
1942
|
+
inFlight.set(key, promise);
|
|
1943
|
+
try {
|
|
1944
|
+
return await promise;
|
|
1945
|
+
} finally {
|
|
1946
|
+
inFlight.delete(key);
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1950
|
+
}
|
|
1731
1951
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1732
1952
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1733
1953
|
"characters",
|
|
@@ -2022,23 +2242,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
2022
2242
|
);
|
|
2023
2243
|
}
|
|
2024
2244
|
}
|
|
2025
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2245
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
2026
2246
|
const src = attr(token, "src");
|
|
2027
2247
|
if (!src) {
|
|
2028
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2248
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
2029
2249
|
return;
|
|
2030
2250
|
}
|
|
2031
2251
|
let parsed;
|
|
2032
2252
|
try {
|
|
2033
2253
|
parsed = new URL(src);
|
|
2034
2254
|
} catch {
|
|
2035
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2255
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
2036
2256
|
return;
|
|
2037
2257
|
}
|
|
2038
2258
|
if (parsed.username || parsed.password)
|
|
2039
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2259
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
2040
2260
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2041
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2261
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2042
2262
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2043
2263
|
try {
|
|
2044
2264
|
const configured = new URL(allowedOrigin);
|
|
@@ -2050,13 +2270,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2050
2270
|
}
|
|
2051
2271
|
}) ?? false;
|
|
2052
2272
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2053
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2273
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2054
2274
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2055
2275
|
addDiagnostic(
|
|
2056
2276
|
diagnostics,
|
|
2057
2277
|
source,
|
|
2058
2278
|
token.start,
|
|
2059
|
-
`<${
|
|
2279
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2060
2280
|
);
|
|
2061
2281
|
}
|
|
2062
2282
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2374,6 +2594,15 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2374
2594
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2375
2595
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2376
2596
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2597
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2598
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2599
|
+
...runnerOptions,
|
|
2600
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2601
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2602
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2603
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2604
|
+
});
|
|
2605
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2377
2606
|
let tokens;
|
|
2378
2607
|
try {
|
|
2379
2608
|
tokens = tokenizeElements(ssml);
|
|
@@ -2383,7 +2612,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2383
2612
|
const checks = tokens.flatMap(
|
|
2384
2613
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2385
2614
|
try {
|
|
2386
|
-
const result = await
|
|
2615
|
+
const result = await boundedValidator(
|
|
2616
|
+
value,
|
|
2617
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2618
|
+
validationSignal
|
|
2619
|
+
);
|
|
2387
2620
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2388
2621
|
if (!valid) {
|
|
2389
2622
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2429,11 +2662,13 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2429
2662
|
areAzureLanguagesEquivalent,
|
|
2430
2663
|
buildPartialSsml,
|
|
2431
2664
|
buildSsml,
|
|
2665
|
+
createAzureUrlValidatorRunner,
|
|
2432
2666
|
extractSsmlText,
|
|
2433
2667
|
extractSsmlTranslatableText,
|
|
2434
2668
|
fromPlainTextToSsml,
|
|
2435
2669
|
getAzureVoiceCatalogMetadata,
|
|
2436
2670
|
getBuiltInVoiceCatalogMetadata,
|
|
2671
|
+
getSsmlSourceMap,
|
|
2437
2672
|
isValidAzureAudioDuration,
|
|
2438
2673
|
mapSsmlTextNodes,
|
|
2439
2674
|
normalizeAzureLanguage,
|