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.
- package/README.md +6 -4
- package/dist/{chunk-UZSCXPC5.mjs → chunk-BDY2Q2JL.mjs} +2 -2
- package/dist/{chunk-AQ55MOPU.mjs → chunk-FXUM45ZY.mjs} +305 -173
- package/dist/chunk-FXUM45ZY.mjs.map +1 -0
- package/dist/{chunk-QFIBPCO4.mjs → chunk-WXFLUCLR.mjs} +164 -17
- package/dist/chunk-WXFLUCLR.mjs.map +1 -0
- package/dist/core.d.mts +48 -17
- package/dist/core.d.ts +48 -17
- package/dist/core.js +303 -170
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +38 -16
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-CUXRwSw0.d.mts → index.d-8BvkB9gz.d.mts} +22 -2
- package/dist/{index.d-CUXRwSw0.d.ts → index.d-8BvkB9gz.d.ts} +22 -2
- package/dist/index.d.mts +114 -18
- package/dist/index.d.ts +114 -18
- package/dist/index.js +1783 -1281
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -86
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +11 -2
- package/dist/react.d.ts +11 -2
- package/dist/react.js +115 -28
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +79 -14
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-AQ55MOPU.mjs.map +0 -1
- package/dist/chunk-QFIBPCO4.mjs.map +0 -1
- /package/dist/{chunk-UZSCXPC5.mjs.map → chunk-BDY2Q2JL.mjs.map} +0 -0
package/dist/core.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(core_exports, {
|
|
|
36
36
|
fromPlainTextToSsml: () => fromPlainTextToSsml,
|
|
37
37
|
getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
|
|
38
38
|
getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
|
|
39
|
+
getSsmlSourceMap: () => getSsmlSourceMap,
|
|
39
40
|
isValidAzureAudioDuration: () => isValidAzureAudioDuration,
|
|
40
41
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
41
42
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
@@ -974,6 +975,236 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
974
975
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
975
976
|
}
|
|
976
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
|
+
|
|
977
1208
|
// packages/ssml-core/src/split.ts
|
|
978
1209
|
var DEFAULT_MAX_LENGTH = 1e4;
|
|
979
1210
|
function cloneElement(element, children) {
|
|
@@ -1105,7 +1336,7 @@ function findSourceNodePath(nodes, targetOffset) {
|
|
|
1105
1336
|
});
|
|
1106
1337
|
return foundPath ?? firstPath;
|
|
1107
1338
|
}
|
|
1108
|
-
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1339
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1109
1340
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1110
1341
|
const text = nodes.map(textFromNode).join("");
|
|
1111
1342
|
const marks = [];
|
|
@@ -1121,7 +1352,23 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1121
1352
|
hasBackgroundAudio: chunkNodes.some(
|
|
1122
1353
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1123
1354
|
),
|
|
1124
|
-
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
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
|
+
}))
|
|
1125
1372
|
};
|
|
1126
1373
|
}
|
|
1127
1374
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1131,11 +1378,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1131
1378
|
throw new RangeError("maxLength must be a positive integer");
|
|
1132
1379
|
}
|
|
1133
1380
|
const document = parseSsml(ssml);
|
|
1381
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1134
1382
|
const backgroundAudio = (document.children ?? []).find(
|
|
1135
1383
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1136
1384
|
);
|
|
1137
1385
|
if (ssml.length <= resolvedMaxLength) {
|
|
1138
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1386
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1139
1387
|
}
|
|
1140
1388
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1141
1389
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1159,7 +1407,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1159
1407
|
}
|
|
1160
1408
|
if (group.length > 0) chunks.push(group);
|
|
1161
1409
|
if (chunks.length === 0) {
|
|
1162
|
-
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
|
+
);
|
|
1163
1420
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1164
1421
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1165
1422
|
}
|
|
@@ -1173,7 +1430,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1173
1430
|
chunkIndex,
|
|
1174
1431
|
textStart,
|
|
1175
1432
|
backgroundAudio,
|
|
1176
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1433
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1434
|
+
sourceMap,
|
|
1435
|
+
chunkIndex === chunks.length - 1
|
|
1177
1436
|
);
|
|
1178
1437
|
textStart = result.originalTextRange.end;
|
|
1179
1438
|
return result;
|
|
@@ -1196,155 +1455,6 @@ function validateSsml(xmlString) {
|
|
|
1196
1455
|
}
|
|
1197
1456
|
}
|
|
1198
1457
|
|
|
1199
|
-
// packages/ssml-core/src/textNodes.ts
|
|
1200
|
-
function decodeXmlText(value) {
|
|
1201
|
-
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1202
|
-
if (entity === "&") return "&";
|
|
1203
|
-
if (entity === "'") return "'";
|
|
1204
|
-
if (entity === ">") return ">";
|
|
1205
|
-
if (entity === "<") return "<";
|
|
1206
|
-
if (entity === """) return '"';
|
|
1207
|
-
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1208
|
-
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1209
|
-
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
function encodeXmlText(value) {
|
|
1213
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1214
|
-
}
|
|
1215
|
-
function decodeXmlAttribute(value) {
|
|
1216
|
-
return decodeXmlText(value);
|
|
1217
|
-
}
|
|
1218
|
-
function findTagEnd(source, start) {
|
|
1219
|
-
let quote = "";
|
|
1220
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1221
|
-
const character = source[index];
|
|
1222
|
-
if (quote) {
|
|
1223
|
-
if (character === quote) quote = "";
|
|
1224
|
-
} else if (character === '"' || character === "'") {
|
|
1225
|
-
quote = character;
|
|
1226
|
-
} else if (character === ">") {
|
|
1227
|
-
return index;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return source.length - 1;
|
|
1231
|
-
}
|
|
1232
|
-
function readTagName(tag) {
|
|
1233
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1234
|
-
return match?.[1];
|
|
1235
|
-
}
|
|
1236
|
-
function readTagAttributes(tag, name) {
|
|
1237
|
-
const attributes = {};
|
|
1238
|
-
const nameStart = tag.indexOf(name);
|
|
1239
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1240
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1241
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1242
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1243
|
-
}
|
|
1244
|
-
return attributes;
|
|
1245
|
-
}
|
|
1246
|
-
function collectTextNodes(source) {
|
|
1247
|
-
const nodes = [];
|
|
1248
|
-
const elements = [];
|
|
1249
|
-
let index = 0;
|
|
1250
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1251
|
-
if (!rawText) return;
|
|
1252
|
-
const path = elements.map((element) => element.name);
|
|
1253
|
-
const parent = elements[elements.length - 1];
|
|
1254
|
-
nodes.push({
|
|
1255
|
-
context: {
|
|
1256
|
-
ancestorTags: path.slice(0, -1),
|
|
1257
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1258
|
-
parentTag: parent?.name ?? "",
|
|
1259
|
-
path
|
|
1260
|
-
},
|
|
1261
|
-
decodedText: decodeXmlText(rawText),
|
|
1262
|
-
end,
|
|
1263
|
-
sourceEnd,
|
|
1264
|
-
sourceStart,
|
|
1265
|
-
start
|
|
1266
|
-
});
|
|
1267
|
-
};
|
|
1268
|
-
while (index < source.length) {
|
|
1269
|
-
if (source[index] !== "<") {
|
|
1270
|
-
const nextTag = source.indexOf("<", index);
|
|
1271
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1272
|
-
addText(index, end2, source.slice(index, end2));
|
|
1273
|
-
index = end2;
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
if (source.startsWith("<!--", index)) {
|
|
1277
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1278
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1282
|
-
const contentStart = index + 9;
|
|
1283
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1284
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1285
|
-
addText(
|
|
1286
|
-
contentStart,
|
|
1287
|
-
contentEnd,
|
|
1288
|
-
source.slice(contentStart, contentEnd),
|
|
1289
|
-
index,
|
|
1290
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1291
|
-
);
|
|
1292
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
if (source.startsWith("<?", index)) {
|
|
1296
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1297
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
if (source.startsWith("</", index)) {
|
|
1301
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1302
|
-
elements.pop();
|
|
1303
|
-
index = end2 + 1;
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
const end = findTagEnd(source, index + 1);
|
|
1307
|
-
const tag = source.slice(index, end + 1);
|
|
1308
|
-
const name = readTagName(tag);
|
|
1309
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1310
|
-
index = end + 1;
|
|
1311
|
-
}
|
|
1312
|
-
return nodes;
|
|
1313
|
-
}
|
|
1314
|
-
function extractSsmlText(ssml) {
|
|
1315
|
-
parseSsml(ssml);
|
|
1316
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1317
|
-
}
|
|
1318
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1319
|
-
parseSsml(ssml);
|
|
1320
|
-
const nodes = collectTextNodes(ssml);
|
|
1321
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1322
|
-
const replacements = await Promise.all(
|
|
1323
|
-
nodes.map(async (node) => {
|
|
1324
|
-
const context = {
|
|
1325
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1326
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1327
|
-
parentTag: node.context.parentTag,
|
|
1328
|
-
path: [...node.context.path]
|
|
1329
|
-
};
|
|
1330
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1331
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1332
|
-
const transformed = await transform(node.decodedText, context);
|
|
1333
|
-
if (typeof transformed !== "string") {
|
|
1334
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1335
|
-
}
|
|
1336
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1337
|
-
})
|
|
1338
|
-
);
|
|
1339
|
-
let result = "";
|
|
1340
|
-
let cursor = 0;
|
|
1341
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1342
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1343
|
-
cursor = node.sourceEnd;
|
|
1344
|
-
});
|
|
1345
|
-
return result + ssml.slice(cursor);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
1458
|
// packages/ssml-core/src/migration.ts
|
|
1349
1459
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1350
1460
|
function elementName2(element) {
|
|
@@ -1765,34 +1875,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1765
1875
|
const inFlight = /* @__PURE__ */ new Map();
|
|
1766
1876
|
const waiters = [];
|
|
1767
1877
|
let active = 0;
|
|
1768
|
-
const
|
|
1878
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1879
|
+
const acquire = async (signal) => {
|
|
1880
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1769
1881
|
if (active < concurrency) {
|
|
1770
1882
|
active += 1;
|
|
1771
1883
|
return;
|
|
1772
1884
|
}
|
|
1773
|
-
await new Promise((resolve) =>
|
|
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
|
+
});
|
|
1774
1900
|
active += 1;
|
|
1775
1901
|
};
|
|
1776
1902
|
const release = () => {
|
|
1777
1903
|
active -= 1;
|
|
1778
1904
|
waiters.shift()?.();
|
|
1779
1905
|
};
|
|
1780
|
-
const check = async (url, context) => {
|
|
1781
|
-
if (
|
|
1782
|
-
const
|
|
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);
|
|
1783
1910
|
if (cached !== void 0) return cached;
|
|
1784
|
-
const existing = inFlight.get(
|
|
1911
|
+
const existing = inFlight.get(key);
|
|
1785
1912
|
if (existing) return existing;
|
|
1786
1913
|
const promise = (async () => {
|
|
1787
|
-
await acquire();
|
|
1914
|
+
await acquire(signal);
|
|
1788
1915
|
try {
|
|
1789
|
-
if (
|
|
1790
|
-
const validation = Promise.resolve(validator(url, context));
|
|
1916
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1917
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1791
1918
|
let timer;
|
|
1792
1919
|
let abortHandler;
|
|
1793
1920
|
const cancellation = new Promise((_resolve, reject) => {
|
|
1794
1921
|
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1795
|
-
|
|
1922
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1796
1923
|
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1797
1924
|
timer = setTimeout(
|
|
1798
1925
|
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
@@ -1802,24 +1929,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1802
1929
|
});
|
|
1803
1930
|
try {
|
|
1804
1931
|
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1805
|
-
cache.set(
|
|
1932
|
+
cache.set(key, result);
|
|
1806
1933
|
return result;
|
|
1807
1934
|
} finally {
|
|
1808
1935
|
if (timer) clearTimeout(timer);
|
|
1809
|
-
if (abortHandler)
|
|
1936
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1810
1937
|
}
|
|
1811
1938
|
} finally {
|
|
1812
1939
|
release();
|
|
1813
1940
|
}
|
|
1814
1941
|
})();
|
|
1815
|
-
inFlight.set(
|
|
1942
|
+
inFlight.set(key, promise);
|
|
1816
1943
|
try {
|
|
1817
1944
|
return await promise;
|
|
1818
1945
|
} finally {
|
|
1819
|
-
inFlight.delete(
|
|
1946
|
+
inFlight.delete(key);
|
|
1820
1947
|
}
|
|
1821
1948
|
};
|
|
1822
|
-
return (url, context) => check(url, context);
|
|
1949
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1823
1950
|
}
|
|
1824
1951
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1825
1952
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
@@ -2475,6 +2602,7 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2475
2602
|
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2476
2603
|
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2477
2604
|
});
|
|
2605
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2478
2606
|
let tokens;
|
|
2479
2607
|
try {
|
|
2480
2608
|
tokens = tokenizeElements(ssml);
|
|
@@ -2484,7 +2612,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2484
2612
|
const checks = tokens.flatMap(
|
|
2485
2613
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2486
2614
|
try {
|
|
2487
|
-
const result = await boundedValidator(
|
|
2615
|
+
const result = await boundedValidator(
|
|
2616
|
+
value,
|
|
2617
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2618
|
+
validationSignal
|
|
2619
|
+
);
|
|
2488
2620
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2489
2621
|
if (!valid) {
|
|
2490
2622
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2536,6 +2668,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2536
2668
|
fromPlainTextToSsml,
|
|
2537
2669
|
getAzureVoiceCatalogMetadata,
|
|
2538
2670
|
getBuiltInVoiceCatalogMetadata,
|
|
2671
|
+
getSsmlSourceMap,
|
|
2539
2672
|
isValidAzureAudioDuration,
|
|
2540
2673
|
mapSsmlTextNodes,
|
|
2541
2674
|
normalizeAzureLanguage,
|