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
|
@@ -931,6 +931,236 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
931
931
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
// packages/ssml-core/src/textNodes.ts
|
|
935
|
+
function decodeXmlText(value) {
|
|
936
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
937
|
+
if (entity === "&") return "&";
|
|
938
|
+
if (entity === "'") return "'";
|
|
939
|
+
if (entity === ">") return ">";
|
|
940
|
+
if (entity === "<") return "<";
|
|
941
|
+
if (entity === """) return '"';
|
|
942
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
943
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
944
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function encodeXmlText(value) {
|
|
948
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
949
|
+
}
|
|
950
|
+
function decodeXmlAttribute(value) {
|
|
951
|
+
return decodeXmlText(value);
|
|
952
|
+
}
|
|
953
|
+
function findTagEnd(source, start) {
|
|
954
|
+
let quote = "";
|
|
955
|
+
for (let index = start; index < source.length; index += 1) {
|
|
956
|
+
const character = source[index];
|
|
957
|
+
if (quote) {
|
|
958
|
+
if (character === quote) quote = "";
|
|
959
|
+
} else if (character === '"' || character === "'") {
|
|
960
|
+
quote = character;
|
|
961
|
+
} else if (character === ">") {
|
|
962
|
+
return index;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return source.length - 1;
|
|
966
|
+
}
|
|
967
|
+
function readTagName(tag) {
|
|
968
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
969
|
+
return match?.[1];
|
|
970
|
+
}
|
|
971
|
+
function readTagAttributes(tag, name) {
|
|
972
|
+
const attributes = {};
|
|
973
|
+
const nameStart = tag.indexOf(name);
|
|
974
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
975
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
976
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
977
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
978
|
+
}
|
|
979
|
+
return attributes;
|
|
980
|
+
}
|
|
981
|
+
function collectTextNodes(source) {
|
|
982
|
+
const nodes = [];
|
|
983
|
+
const elements = [];
|
|
984
|
+
let index = 0;
|
|
985
|
+
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
986
|
+
if (!rawText) return;
|
|
987
|
+
const path = elements.map((element) => element.name);
|
|
988
|
+
const parent = elements[elements.length - 1];
|
|
989
|
+
nodes.push({
|
|
990
|
+
context: {
|
|
991
|
+
ancestorTags: path.slice(0, -1),
|
|
992
|
+
parentAttributes: { ...parent?.attributes ?? {} },
|
|
993
|
+
parentTag: parent?.name ?? "",
|
|
994
|
+
path
|
|
995
|
+
},
|
|
996
|
+
decodedText: decodeXmlText(rawText),
|
|
997
|
+
end,
|
|
998
|
+
sourceEnd,
|
|
999
|
+
sourceStart,
|
|
1000
|
+
start
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
while (index < source.length) {
|
|
1004
|
+
if (source[index] !== "<") {
|
|
1005
|
+
const nextTag = source.indexOf("<", index);
|
|
1006
|
+
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1007
|
+
addText(index, end2, source.slice(index, end2));
|
|
1008
|
+
index = end2;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (source.startsWith("<!--", index)) {
|
|
1012
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1013
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1017
|
+
const contentStart = index + 9;
|
|
1018
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1019
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1020
|
+
addText(
|
|
1021
|
+
contentStart,
|
|
1022
|
+
contentEnd,
|
|
1023
|
+
source.slice(contentStart, contentEnd),
|
|
1024
|
+
index,
|
|
1025
|
+
end2 === -1 ? source.length : end2 + 3
|
|
1026
|
+
);
|
|
1027
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (source.startsWith("<?", index)) {
|
|
1031
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1032
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (source.startsWith("</", index)) {
|
|
1036
|
+
const end2 = findTagEnd(source, index + 2);
|
|
1037
|
+
elements.pop();
|
|
1038
|
+
index = end2 + 1;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const end = findTagEnd(source, index + 1);
|
|
1042
|
+
const tag = source.slice(index, end + 1);
|
|
1043
|
+
const name = readTagName(tag);
|
|
1044
|
+
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1045
|
+
index = end + 1;
|
|
1046
|
+
}
|
|
1047
|
+
return nodes;
|
|
1048
|
+
}
|
|
1049
|
+
function collectSourceMap(source) {
|
|
1050
|
+
const segments = [];
|
|
1051
|
+
const markers = [];
|
|
1052
|
+
const elements = [];
|
|
1053
|
+
let textOffset = 0;
|
|
1054
|
+
let index = 0;
|
|
1055
|
+
const textParts = [];
|
|
1056
|
+
const addText = (value) => {
|
|
1057
|
+
if (!value) return;
|
|
1058
|
+
const parent = elements[elements.length - 1];
|
|
1059
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1060
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
1061
|
+
const start = textOffset;
|
|
1062
|
+
textOffset += value.length;
|
|
1063
|
+
textParts.push(value);
|
|
1064
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
1065
|
+
};
|
|
1066
|
+
while (index < source.length) {
|
|
1067
|
+
if (source[index] !== "<") {
|
|
1068
|
+
const end2 = source.indexOf("<", index);
|
|
1069
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
1070
|
+
addText(decodeXmlText(source.slice(index, textEnd)));
|
|
1071
|
+
index = textEnd;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (source.startsWith("<!--", index)) {
|
|
1075
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1076
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1080
|
+
const contentStart = index + 9;
|
|
1081
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1082
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1083
|
+
addText(source.slice(contentStart, contentEnd));
|
|
1084
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (source.startsWith("<?", index)) {
|
|
1088
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1089
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const end = findTagEnd(source, index + 1);
|
|
1093
|
+
const rawTag = source.slice(index, end + 1);
|
|
1094
|
+
if (rawTag.startsWith("</")) {
|
|
1095
|
+
elements.pop();
|
|
1096
|
+
index = end + 1;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const name = readTagName(rawTag);
|
|
1100
|
+
if (!name) {
|
|
1101
|
+
index = end + 1;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const parent = elements[elements.length - 1];
|
|
1105
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
1106
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1107
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
1108
|
+
const attributes = readTagAttributes(rawTag, name);
|
|
1109
|
+
const normalizedName = name.toLowerCase();
|
|
1110
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
1111
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
1112
|
+
if (markerName) {
|
|
1113
|
+
markers.push({
|
|
1114
|
+
kind: normalizedName,
|
|
1115
|
+
name: markerName,
|
|
1116
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
1117
|
+
sourceNodePath: [...path]
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
1122
|
+
index = end + 1;
|
|
1123
|
+
}
|
|
1124
|
+
return { text: textParts.join(""), segments, markers };
|
|
1125
|
+
}
|
|
1126
|
+
function getSsmlSourceMap(ssml) {
|
|
1127
|
+
parseSsml(ssml);
|
|
1128
|
+
return collectSourceMap(ssml);
|
|
1129
|
+
}
|
|
1130
|
+
function extractSsmlText(ssml) {
|
|
1131
|
+
parseSsml(ssml);
|
|
1132
|
+
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1133
|
+
}
|
|
1134
|
+
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1135
|
+
parseSsml(ssml);
|
|
1136
|
+
const nodes = collectTextNodes(ssml);
|
|
1137
|
+
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1138
|
+
const replacements = await Promise.all(
|
|
1139
|
+
nodes.map(async (node) => {
|
|
1140
|
+
const context = {
|
|
1141
|
+
ancestorTags: [...node.context.ancestorTags],
|
|
1142
|
+
parentAttributes: { ...node.context.parentAttributes },
|
|
1143
|
+
parentTag: node.context.parentTag,
|
|
1144
|
+
path: [...node.context.path]
|
|
1145
|
+
};
|
|
1146
|
+
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1147
|
+
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1148
|
+
const transformed = await transform(node.decodedText, context);
|
|
1149
|
+
if (typeof transformed !== "string") {
|
|
1150
|
+
throw new TypeError("SSML text node transform must return a string");
|
|
1151
|
+
}
|
|
1152
|
+
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1153
|
+
})
|
|
1154
|
+
);
|
|
1155
|
+
let result = "";
|
|
1156
|
+
let cursor = 0;
|
|
1157
|
+
nodes.forEach((node, nodeIndex) => {
|
|
1158
|
+
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1159
|
+
cursor = node.sourceEnd;
|
|
1160
|
+
});
|
|
1161
|
+
return result + ssml.slice(cursor);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
934
1164
|
// packages/ssml-core/src/split.ts
|
|
935
1165
|
var DEFAULT_MAX_LENGTH = 1e4;
|
|
936
1166
|
function cloneElement(element, children) {
|
|
@@ -1034,7 +1264,35 @@ function collectInheritedContext(nodes) {
|
|
|
1034
1264
|
nodes.forEach(visit);
|
|
1035
1265
|
return context;
|
|
1036
1266
|
}
|
|
1037
|
-
function
|
|
1267
|
+
function elementName(node) {
|
|
1268
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1269
|
+
}
|
|
1270
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1271
|
+
let textOffset = 0;
|
|
1272
|
+
let firstPath;
|
|
1273
|
+
let foundPath;
|
|
1274
|
+
const visit = (node, path) => {
|
|
1275
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1276
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1277
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1278
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1279
|
+
textOffset += text.length;
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
node.children?.forEach((child, index) => {
|
|
1283
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1284
|
+
visit(child, childPath);
|
|
1285
|
+
});
|
|
1286
|
+
};
|
|
1287
|
+
nodes.forEach((node, index) => {
|
|
1288
|
+
if (!foundPath) {
|
|
1289
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1290
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
return foundPath ?? firstPath;
|
|
1294
|
+
}
|
|
1295
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1038
1296
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1039
1297
|
const text = nodes.map(textFromNode).join("");
|
|
1040
1298
|
const marks = [];
|
|
@@ -1049,7 +1307,24 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1049
1307
|
containedMarks: marks,
|
|
1050
1308
|
hasBackgroundAudio: chunkNodes.some(
|
|
1051
1309
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1052
|
-
)
|
|
1310
|
+
),
|
|
1311
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
|
|
1312
|
+
sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
|
|
1313
|
+
const start = Math.max(segment.range.start, textStart);
|
|
1314
|
+
const end = Math.min(segment.range.end, textStart + text.length);
|
|
1315
|
+
return {
|
|
1316
|
+
text: segment.text.slice(start - segment.range.start, end - segment.range.start),
|
|
1317
|
+
range: { start, end },
|
|
1318
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
1319
|
+
};
|
|
1320
|
+
}),
|
|
1321
|
+
sourceMarkers: sourceMap.markers.filter(
|
|
1322
|
+
({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
|
|
1323
|
+
).map((marker) => ({
|
|
1324
|
+
...marker,
|
|
1325
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
1326
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
1327
|
+
}))
|
|
1053
1328
|
};
|
|
1054
1329
|
}
|
|
1055
1330
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1059,11 +1334,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1059
1334
|
throw new RangeError("maxLength must be a positive integer");
|
|
1060
1335
|
}
|
|
1061
1336
|
const document = parseSsml(ssml);
|
|
1337
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1062
1338
|
const backgroundAudio = (document.children ?? []).find(
|
|
1063
1339
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1064
1340
|
);
|
|
1065
1341
|
if (ssml.length <= resolvedMaxLength) {
|
|
1066
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1342
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1067
1343
|
}
|
|
1068
1344
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1069
1345
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1087,7 +1363,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1087
1363
|
}
|
|
1088
1364
|
if (group.length > 0) chunks.push(group);
|
|
1089
1365
|
if (chunks.length === 0) {
|
|
1090
|
-
const result = createChunk(
|
|
1366
|
+
const result = createChunk(
|
|
1367
|
+
document,
|
|
1368
|
+
[],
|
|
1369
|
+
0,
|
|
1370
|
+
0,
|
|
1371
|
+
backgroundAudio,
|
|
1372
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1373
|
+
sourceMap,
|
|
1374
|
+
true
|
|
1375
|
+
);
|
|
1091
1376
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1092
1377
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1093
1378
|
}
|
|
@@ -1101,7 +1386,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1101
1386
|
chunkIndex,
|
|
1102
1387
|
textStart,
|
|
1103
1388
|
backgroundAudio,
|
|
1104
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1389
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1390
|
+
sourceMap,
|
|
1391
|
+
chunkIndex === chunks.length - 1
|
|
1105
1392
|
);
|
|
1106
1393
|
textStart = result.originalTextRange.end;
|
|
1107
1394
|
return result;
|
|
@@ -1124,158 +1411,9 @@ function validateSsml(xmlString) {
|
|
|
1124
1411
|
}
|
|
1125
1412
|
}
|
|
1126
1413
|
|
|
1127
|
-
// packages/ssml-core/src/textNodes.ts
|
|
1128
|
-
function decodeXmlText(value) {
|
|
1129
|
-
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1130
|
-
if (entity === "&") return "&";
|
|
1131
|
-
if (entity === "'") return "'";
|
|
1132
|
-
if (entity === ">") return ">";
|
|
1133
|
-
if (entity === "<") return "<";
|
|
1134
|
-
if (entity === """) return '"';
|
|
1135
|
-
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1136
|
-
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1137
|
-
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
function encodeXmlText(value) {
|
|
1141
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1142
|
-
}
|
|
1143
|
-
function decodeXmlAttribute(value) {
|
|
1144
|
-
return decodeXmlText(value);
|
|
1145
|
-
}
|
|
1146
|
-
function findTagEnd(source, start) {
|
|
1147
|
-
let quote = "";
|
|
1148
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1149
|
-
const character = source[index];
|
|
1150
|
-
if (quote) {
|
|
1151
|
-
if (character === quote) quote = "";
|
|
1152
|
-
} else if (character === '"' || character === "'") {
|
|
1153
|
-
quote = character;
|
|
1154
|
-
} else if (character === ">") {
|
|
1155
|
-
return index;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
return source.length - 1;
|
|
1159
|
-
}
|
|
1160
|
-
function readTagName(tag) {
|
|
1161
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1162
|
-
return match?.[1];
|
|
1163
|
-
}
|
|
1164
|
-
function readTagAttributes(tag, name) {
|
|
1165
|
-
const attributes = {};
|
|
1166
|
-
const nameStart = tag.indexOf(name);
|
|
1167
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1168
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1169
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1170
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1171
|
-
}
|
|
1172
|
-
return attributes;
|
|
1173
|
-
}
|
|
1174
|
-
function collectTextNodes(source) {
|
|
1175
|
-
const nodes = [];
|
|
1176
|
-
const elements = [];
|
|
1177
|
-
let index = 0;
|
|
1178
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1179
|
-
if (!rawText) return;
|
|
1180
|
-
const path = elements.map((element) => element.name);
|
|
1181
|
-
const parent = elements[elements.length - 1];
|
|
1182
|
-
nodes.push({
|
|
1183
|
-
context: {
|
|
1184
|
-
ancestorTags: path.slice(0, -1),
|
|
1185
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1186
|
-
parentTag: parent?.name ?? "",
|
|
1187
|
-
path
|
|
1188
|
-
},
|
|
1189
|
-
decodedText: decodeXmlText(rawText),
|
|
1190
|
-
end,
|
|
1191
|
-
sourceEnd,
|
|
1192
|
-
sourceStart,
|
|
1193
|
-
start
|
|
1194
|
-
});
|
|
1195
|
-
};
|
|
1196
|
-
while (index < source.length) {
|
|
1197
|
-
if (source[index] !== "<") {
|
|
1198
|
-
const nextTag = source.indexOf("<", index);
|
|
1199
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1200
|
-
addText(index, end2, source.slice(index, end2));
|
|
1201
|
-
index = end2;
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
if (source.startsWith("<!--", index)) {
|
|
1205
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1206
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1207
|
-
continue;
|
|
1208
|
-
}
|
|
1209
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1210
|
-
const contentStart = index + 9;
|
|
1211
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1212
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1213
|
-
addText(
|
|
1214
|
-
contentStart,
|
|
1215
|
-
contentEnd,
|
|
1216
|
-
source.slice(contentStart, contentEnd),
|
|
1217
|
-
index,
|
|
1218
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1219
|
-
);
|
|
1220
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1221
|
-
continue;
|
|
1222
|
-
}
|
|
1223
|
-
if (source.startsWith("<?", index)) {
|
|
1224
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1225
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1226
|
-
continue;
|
|
1227
|
-
}
|
|
1228
|
-
if (source.startsWith("</", index)) {
|
|
1229
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1230
|
-
elements.pop();
|
|
1231
|
-
index = end2 + 1;
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
const end = findTagEnd(source, index + 1);
|
|
1235
|
-
const tag = source.slice(index, end + 1);
|
|
1236
|
-
const name = readTagName(tag);
|
|
1237
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1238
|
-
index = end + 1;
|
|
1239
|
-
}
|
|
1240
|
-
return nodes;
|
|
1241
|
-
}
|
|
1242
|
-
function extractSsmlText(ssml) {
|
|
1243
|
-
parseSsml(ssml);
|
|
1244
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1245
|
-
}
|
|
1246
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1247
|
-
parseSsml(ssml);
|
|
1248
|
-
const nodes = collectTextNodes(ssml);
|
|
1249
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1250
|
-
const replacements = await Promise.all(
|
|
1251
|
-
nodes.map(async (node) => {
|
|
1252
|
-
const context = {
|
|
1253
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1254
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1255
|
-
parentTag: node.context.parentTag,
|
|
1256
|
-
path: [...node.context.path]
|
|
1257
|
-
};
|
|
1258
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1259
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1260
|
-
const transformed = await transform(node.decodedText, context);
|
|
1261
|
-
if (typeof transformed !== "string") {
|
|
1262
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1263
|
-
}
|
|
1264
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1265
|
-
})
|
|
1266
|
-
);
|
|
1267
|
-
let result = "";
|
|
1268
|
-
let cursor = 0;
|
|
1269
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1270
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1271
|
-
cursor = node.sourceEnd;
|
|
1272
|
-
});
|
|
1273
|
-
return result + ssml.slice(cursor);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
1414
|
// packages/ssml-core/src/migration.ts
|
|
1277
1415
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1278
|
-
function
|
|
1416
|
+
function elementName2(element) {
|
|
1279
1417
|
switch (element.type) {
|
|
1280
1418
|
case "custom":
|
|
1281
1419
|
case "element":
|
|
@@ -1428,7 +1566,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1428
1566
|
}
|
|
1429
1567
|
return;
|
|
1430
1568
|
}
|
|
1431
|
-
const tag =
|
|
1569
|
+
const tag = elementName2(node);
|
|
1432
1570
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1433
1571
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1434
1572
|
});
|
|
@@ -1474,7 +1612,7 @@ function serializeDocument2(document) {
|
|
|
1474
1612
|
const serialize = (node) => {
|
|
1475
1613
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1476
1614
|
if (node.type === "text") return serialize(node.value);
|
|
1477
|
-
const tag =
|
|
1615
|
+
const tag = elementName2(node);
|
|
1478
1616
|
const nodeAttributes = elementAttributes(node);
|
|
1479
1617
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1480
1618
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1488,7 +1626,7 @@ function flatten(document) {
|
|
|
1488
1626
|
nodes.forEach((node, index) => {
|
|
1489
1627
|
if (typeof node === "string" || node.type === "text") return;
|
|
1490
1628
|
const currentPath = `${path}/${index}`;
|
|
1491
|
-
result.push({ name:
|
|
1629
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1492
1630
|
visit(childrenOf(node), currentPath);
|
|
1493
1631
|
});
|
|
1494
1632
|
};
|
|
@@ -1686,6 +1824,86 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1686
1824
|
];
|
|
1687
1825
|
|
|
1688
1826
|
// packages/ssml-core/src/azureValidation.ts
|
|
1827
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1828
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1829
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1830
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1831
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1832
|
+
const waiters = [];
|
|
1833
|
+
let active = 0;
|
|
1834
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1835
|
+
const acquire = async (signal) => {
|
|
1836
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1837
|
+
if (active < concurrency) {
|
|
1838
|
+
active += 1;
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
await new Promise((resolve, reject) => {
|
|
1842
|
+
let waiter;
|
|
1843
|
+
const abortHandler = () => {
|
|
1844
|
+
const index = waiters.indexOf(waiter);
|
|
1845
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
1846
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1847
|
+
reject(new Error("URL validation was aborted."));
|
|
1848
|
+
};
|
|
1849
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1850
|
+
waiter = () => {
|
|
1851
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1852
|
+
resolve();
|
|
1853
|
+
};
|
|
1854
|
+
waiters.push(waiter);
|
|
1855
|
+
});
|
|
1856
|
+
active += 1;
|
|
1857
|
+
};
|
|
1858
|
+
const release = () => {
|
|
1859
|
+
active -= 1;
|
|
1860
|
+
waiters.shift()?.();
|
|
1861
|
+
};
|
|
1862
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
1863
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1864
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
1865
|
+
const cached = cache.get(key);
|
|
1866
|
+
if (cached !== void 0) return cached;
|
|
1867
|
+
const existing = inFlight.get(key);
|
|
1868
|
+
if (existing) return existing;
|
|
1869
|
+
const promise = (async () => {
|
|
1870
|
+
await acquire(signal);
|
|
1871
|
+
try {
|
|
1872
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1873
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1874
|
+
let timer;
|
|
1875
|
+
let abortHandler;
|
|
1876
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1877
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1878
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1879
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1880
|
+
timer = setTimeout(
|
|
1881
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1882
|
+
options.timeoutMs
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
try {
|
|
1887
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1888
|
+
cache.set(key, result);
|
|
1889
|
+
return result;
|
|
1890
|
+
} finally {
|
|
1891
|
+
if (timer) clearTimeout(timer);
|
|
1892
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1893
|
+
}
|
|
1894
|
+
} finally {
|
|
1895
|
+
release();
|
|
1896
|
+
}
|
|
1897
|
+
})();
|
|
1898
|
+
inFlight.set(key, promise);
|
|
1899
|
+
try {
|
|
1900
|
+
return await promise;
|
|
1901
|
+
} finally {
|
|
1902
|
+
inFlight.delete(key);
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1906
|
+
}
|
|
1689
1907
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1690
1908
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1691
1909
|
"characters",
|
|
@@ -1980,23 +2198,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
1980
2198
|
);
|
|
1981
2199
|
}
|
|
1982
2200
|
}
|
|
1983
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2201
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
1984
2202
|
const src = attr(token, "src");
|
|
1985
2203
|
if (!src) {
|
|
1986
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2204
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
1987
2205
|
return;
|
|
1988
2206
|
}
|
|
1989
2207
|
let parsed;
|
|
1990
2208
|
try {
|
|
1991
2209
|
parsed = new URL(src);
|
|
1992
2210
|
} catch {
|
|
1993
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2211
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
1994
2212
|
return;
|
|
1995
2213
|
}
|
|
1996
2214
|
if (parsed.username || parsed.password)
|
|
1997
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2215
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
1998
2216
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1999
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2217
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2000
2218
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2001
2219
|
try {
|
|
2002
2220
|
const configured = new URL(allowedOrigin);
|
|
@@ -2008,13 +2226,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2008
2226
|
}
|
|
2009
2227
|
}) ?? false;
|
|
2010
2228
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2011
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2229
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2012
2230
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2013
2231
|
addDiagnostic(
|
|
2014
2232
|
diagnostics,
|
|
2015
2233
|
source,
|
|
2016
2234
|
token.start,
|
|
2017
|
-
`<${
|
|
2235
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2018
2236
|
);
|
|
2019
2237
|
}
|
|
2020
2238
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2332,6 +2550,15 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2332
2550
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2333
2551
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2334
2552
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2553
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2554
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2555
|
+
...runnerOptions,
|
|
2556
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2557
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2558
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2559
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2560
|
+
});
|
|
2561
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2335
2562
|
let tokens;
|
|
2336
2563
|
try {
|
|
2337
2564
|
tokens = tokenizeElements(ssml);
|
|
@@ -2341,7 +2568,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2341
2568
|
const checks = tokens.flatMap(
|
|
2342
2569
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2343
2570
|
try {
|
|
2344
|
-
const result = await
|
|
2571
|
+
const result = await boundedValidator(
|
|
2572
|
+
value,
|
|
2573
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2574
|
+
validationSignal
|
|
2575
|
+
);
|
|
2345
2576
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2346
2577
|
if (!valid) {
|
|
2347
2578
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2387,13 +2618,15 @@ export {
|
|
|
2387
2618
|
buildSsml,
|
|
2388
2619
|
parseSsml,
|
|
2389
2620
|
buildPartialSsml,
|
|
2390
|
-
|
|
2391
|
-
validateSsml,
|
|
2621
|
+
getSsmlSourceMap,
|
|
2392
2622
|
extractSsmlText,
|
|
2393
2623
|
mapSsmlTextNodes,
|
|
2624
|
+
splitSsmlDocument,
|
|
2625
|
+
validateSsml,
|
|
2394
2626
|
extractSsmlTranslatableText,
|
|
2395
2627
|
fromPlainTextToSsml,
|
|
2396
2628
|
validateSsmlStructureIntegrity,
|
|
2629
|
+
createAzureUrlValidatorRunner,
|
|
2397
2630
|
isValidAzureAudioDuration,
|
|
2398
2631
|
normalizeAzureLanguage,
|
|
2399
2632
|
areAzureLanguagesEquivalent,
|
|
@@ -2401,4 +2634,4 @@ export {
|
|
|
2401
2634
|
getAzureVoiceCatalogMetadata,
|
|
2402
2635
|
getBuiltInVoiceCatalogMetadata
|
|
2403
2636
|
};
|
|
2404
|
-
//# sourceMappingURL=chunk-
|
|
2637
|
+
//# sourceMappingURL=chunk-FXUM45ZY.mjs.map
|