ssml-builder-js 2.14.0 → 2.16.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 +10 -4
- package/dist/{chunk-UZSCXPC5.mjs → chunk-F2EMU3HM.mjs} +2 -2
- package/dist/{chunk-QFIBPCO4.mjs → chunk-HI74FTKY.mjs} +197 -21
- package/dist/chunk-HI74FTKY.mjs.map +1 -0
- package/dist/{chunk-AQ55MOPU.mjs → chunk-NKLZGITR.mjs} +354 -177
- package/dist/chunk-NKLZGITR.mjs.map +1 -0
- package/dist/core.d.mts +67 -17
- package/dist/core.d.ts +67 -17
- package/dist/core.js +353 -174
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +5 -1
- package/dist/elements.js +70 -20
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-CUXRwSw0.d.mts → index.d-DR5Qz43p.d.mts} +37 -2
- package/dist/{index.d-CUXRwSw0.d.ts → index.d-DR5Qz43p.d.ts} +37 -2
- package/dist/index.d.mts +162 -19
- package/dist/index.d.ts +162 -19
- package/dist/index.js +2060 -1128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +752 -174
- 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 +147 -32
- 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-F2EMU3HM.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) {
|
|
@@ -1062,7 +1292,7 @@ function findSourceNodePath(nodes, targetOffset) {
|
|
|
1062
1292
|
});
|
|
1063
1293
|
return foundPath ?? firstPath;
|
|
1064
1294
|
}
|
|
1065
|
-
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1295
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1066
1296
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1067
1297
|
const text = nodes.map(textFromNode).join("");
|
|
1068
1298
|
const marks = [];
|
|
@@ -1078,7 +1308,23 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1078
1308
|
hasBackgroundAudio: chunkNodes.some(
|
|
1079
1309
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1080
1310
|
),
|
|
1081
|
-
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
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
|
+
}))
|
|
1082
1328
|
};
|
|
1083
1329
|
}
|
|
1084
1330
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1088,11 +1334,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1088
1334
|
throw new RangeError("maxLength must be a positive integer");
|
|
1089
1335
|
}
|
|
1090
1336
|
const document = parseSsml(ssml);
|
|
1337
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1091
1338
|
const backgroundAudio = (document.children ?? []).find(
|
|
1092
1339
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1093
1340
|
);
|
|
1094
1341
|
if (ssml.length <= resolvedMaxLength) {
|
|
1095
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1342
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1096
1343
|
}
|
|
1097
1344
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1098
1345
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1116,7 +1363,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1116
1363
|
}
|
|
1117
1364
|
if (group.length > 0) chunks.push(group);
|
|
1118
1365
|
if (chunks.length === 0) {
|
|
1119
|
-
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
|
+
);
|
|
1120
1376
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1121
1377
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1122
1378
|
}
|
|
@@ -1130,7 +1386,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1130
1386
|
chunkIndex,
|
|
1131
1387
|
textStart,
|
|
1132
1388
|
backgroundAudio,
|
|
1133
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1389
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1390
|
+
sourceMap,
|
|
1391
|
+
chunkIndex === chunks.length - 1
|
|
1134
1392
|
);
|
|
1135
1393
|
textStart = result.originalTextRange.end;
|
|
1136
1394
|
return result;
|
|
@@ -1153,155 +1411,6 @@ function validateSsml(xmlString) {
|
|
|
1153
1411
|
}
|
|
1154
1412
|
}
|
|
1155
1413
|
|
|
1156
|
-
// packages/ssml-core/src/textNodes.ts
|
|
1157
|
-
function decodeXmlText(value) {
|
|
1158
|
-
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1159
|
-
if (entity === "&") return "&";
|
|
1160
|
-
if (entity === "'") return "'";
|
|
1161
|
-
if (entity === ">") return ">";
|
|
1162
|
-
if (entity === "<") return "<";
|
|
1163
|
-
if (entity === """) return '"';
|
|
1164
|
-
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1165
|
-
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1166
|
-
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
function encodeXmlText(value) {
|
|
1170
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1171
|
-
}
|
|
1172
|
-
function decodeXmlAttribute(value) {
|
|
1173
|
-
return decodeXmlText(value);
|
|
1174
|
-
}
|
|
1175
|
-
function findTagEnd(source, start) {
|
|
1176
|
-
let quote = "";
|
|
1177
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1178
|
-
const character = source[index];
|
|
1179
|
-
if (quote) {
|
|
1180
|
-
if (character === quote) quote = "";
|
|
1181
|
-
} else if (character === '"' || character === "'") {
|
|
1182
|
-
quote = character;
|
|
1183
|
-
} else if (character === ">") {
|
|
1184
|
-
return index;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
return source.length - 1;
|
|
1188
|
-
}
|
|
1189
|
-
function readTagName(tag) {
|
|
1190
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1191
|
-
return match?.[1];
|
|
1192
|
-
}
|
|
1193
|
-
function readTagAttributes(tag, name) {
|
|
1194
|
-
const attributes = {};
|
|
1195
|
-
const nameStart = tag.indexOf(name);
|
|
1196
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1197
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1198
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1199
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1200
|
-
}
|
|
1201
|
-
return attributes;
|
|
1202
|
-
}
|
|
1203
|
-
function collectTextNodes(source) {
|
|
1204
|
-
const nodes = [];
|
|
1205
|
-
const elements = [];
|
|
1206
|
-
let index = 0;
|
|
1207
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1208
|
-
if (!rawText) return;
|
|
1209
|
-
const path = elements.map((element) => element.name);
|
|
1210
|
-
const parent = elements[elements.length - 1];
|
|
1211
|
-
nodes.push({
|
|
1212
|
-
context: {
|
|
1213
|
-
ancestorTags: path.slice(0, -1),
|
|
1214
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1215
|
-
parentTag: parent?.name ?? "",
|
|
1216
|
-
path
|
|
1217
|
-
},
|
|
1218
|
-
decodedText: decodeXmlText(rawText),
|
|
1219
|
-
end,
|
|
1220
|
-
sourceEnd,
|
|
1221
|
-
sourceStart,
|
|
1222
|
-
start
|
|
1223
|
-
});
|
|
1224
|
-
};
|
|
1225
|
-
while (index < source.length) {
|
|
1226
|
-
if (source[index] !== "<") {
|
|
1227
|
-
const nextTag = source.indexOf("<", index);
|
|
1228
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1229
|
-
addText(index, end2, source.slice(index, end2));
|
|
1230
|
-
index = end2;
|
|
1231
|
-
continue;
|
|
1232
|
-
}
|
|
1233
|
-
if (source.startsWith("<!--", index)) {
|
|
1234
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1235
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1239
|
-
const contentStart = index + 9;
|
|
1240
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1241
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1242
|
-
addText(
|
|
1243
|
-
contentStart,
|
|
1244
|
-
contentEnd,
|
|
1245
|
-
source.slice(contentStart, contentEnd),
|
|
1246
|
-
index,
|
|
1247
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1248
|
-
);
|
|
1249
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
if (source.startsWith("<?", index)) {
|
|
1253
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1254
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
if (source.startsWith("</", index)) {
|
|
1258
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1259
|
-
elements.pop();
|
|
1260
|
-
index = end2 + 1;
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1263
|
-
const end = findTagEnd(source, index + 1);
|
|
1264
|
-
const tag = source.slice(index, end + 1);
|
|
1265
|
-
const name = readTagName(tag);
|
|
1266
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1267
|
-
index = end + 1;
|
|
1268
|
-
}
|
|
1269
|
-
return nodes;
|
|
1270
|
-
}
|
|
1271
|
-
function extractSsmlText(ssml) {
|
|
1272
|
-
parseSsml(ssml);
|
|
1273
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1274
|
-
}
|
|
1275
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1276
|
-
parseSsml(ssml);
|
|
1277
|
-
const nodes = collectTextNodes(ssml);
|
|
1278
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1279
|
-
const replacements = await Promise.all(
|
|
1280
|
-
nodes.map(async (node) => {
|
|
1281
|
-
const context = {
|
|
1282
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1283
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1284
|
-
parentTag: node.context.parentTag,
|
|
1285
|
-
path: [...node.context.path]
|
|
1286
|
-
};
|
|
1287
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1288
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1289
|
-
const transformed = await transform(node.decodedText, context);
|
|
1290
|
-
if (typeof transformed !== "string") {
|
|
1291
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1292
|
-
}
|
|
1293
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1294
|
-
})
|
|
1295
|
-
);
|
|
1296
|
-
let result = "";
|
|
1297
|
-
let cursor = 0;
|
|
1298
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1299
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1300
|
-
cursor = node.sourceEnd;
|
|
1301
|
-
});
|
|
1302
|
-
return result + ssml.slice(cursor);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1414
|
// packages/ssml-core/src/migration.ts
|
|
1306
1415
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1307
1416
|
function elementName2(element) {
|
|
@@ -1722,34 +1831,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1722
1831
|
const inFlight = /* @__PURE__ */ new Map();
|
|
1723
1832
|
const waiters = [];
|
|
1724
1833
|
let active = 0;
|
|
1725
|
-
const
|
|
1834
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1835
|
+
const acquire = async (signal) => {
|
|
1836
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1726
1837
|
if (active < concurrency) {
|
|
1727
1838
|
active += 1;
|
|
1728
1839
|
return;
|
|
1729
1840
|
}
|
|
1730
|
-
await new Promise((resolve) =>
|
|
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
|
+
});
|
|
1731
1856
|
active += 1;
|
|
1732
1857
|
};
|
|
1733
1858
|
const release = () => {
|
|
1734
1859
|
active -= 1;
|
|
1735
1860
|
waiters.shift()?.();
|
|
1736
1861
|
};
|
|
1737
|
-
const check = async (url, context) => {
|
|
1738
|
-
if (
|
|
1739
|
-
const
|
|
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);
|
|
1740
1866
|
if (cached !== void 0) return cached;
|
|
1741
|
-
const existing = inFlight.get(
|
|
1867
|
+
const existing = inFlight.get(key);
|
|
1742
1868
|
if (existing) return existing;
|
|
1743
1869
|
const promise = (async () => {
|
|
1744
|
-
await acquire();
|
|
1870
|
+
await acquire(signal);
|
|
1745
1871
|
try {
|
|
1746
|
-
if (
|
|
1747
|
-
const validation = Promise.resolve(validator(url, context));
|
|
1872
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1873
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1748
1874
|
let timer;
|
|
1749
1875
|
let abortHandler;
|
|
1750
1876
|
const cancellation = new Promise((_resolve, reject) => {
|
|
1751
1877
|
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1752
|
-
|
|
1878
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1753
1879
|
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1754
1880
|
timer = setTimeout(
|
|
1755
1881
|
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
@@ -1759,24 +1885,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1759
1885
|
});
|
|
1760
1886
|
try {
|
|
1761
1887
|
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1762
|
-
cache.set(
|
|
1888
|
+
cache.set(key, result);
|
|
1763
1889
|
return result;
|
|
1764
1890
|
} finally {
|
|
1765
1891
|
if (timer) clearTimeout(timer);
|
|
1766
|
-
if (abortHandler)
|
|
1892
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1767
1893
|
}
|
|
1768
1894
|
} finally {
|
|
1769
1895
|
release();
|
|
1770
1896
|
}
|
|
1771
1897
|
})();
|
|
1772
|
-
inFlight.set(
|
|
1898
|
+
inFlight.set(key, promise);
|
|
1773
1899
|
try {
|
|
1774
1900
|
return await promise;
|
|
1775
1901
|
} finally {
|
|
1776
|
-
inFlight.delete(
|
|
1902
|
+
inFlight.delete(key);
|
|
1777
1903
|
}
|
|
1778
1904
|
};
|
|
1779
|
-
return (url, context) => check(url, context);
|
|
1905
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1780
1906
|
}
|
|
1781
1907
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1782
1908
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
@@ -1889,6 +2015,7 @@ function tokenizeElements(source) {
|
|
|
1889
2015
|
if (parent) parent.childElementCount += 1;
|
|
1890
2016
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1891
2017
|
const tokenName = nameMatch[1];
|
|
2018
|
+
const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
|
|
1892
2019
|
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1893
2020
|
tokens.push({
|
|
1894
2021
|
attributes,
|
|
@@ -1899,12 +2026,14 @@ function tokenizeElements(source) {
|
|
|
1899
2026
|
parentName: parent?.name,
|
|
1900
2027
|
parentVoiceName,
|
|
1901
2028
|
selfClosing,
|
|
1902
|
-
start
|
|
2029
|
+
start,
|
|
2030
|
+
path
|
|
1903
2031
|
});
|
|
1904
2032
|
if (!selfClosing) {
|
|
1905
2033
|
openElements.push({
|
|
1906
2034
|
childElementCount: 0,
|
|
1907
2035
|
name: tokenName,
|
|
2036
|
+
path,
|
|
1908
2037
|
voiceName: tokenVoiceName
|
|
1909
2038
|
});
|
|
1910
2039
|
}
|
|
@@ -1917,13 +2046,14 @@ function location(source, offset) {
|
|
|
1917
2046
|
const line = before.split("\n").length;
|
|
1918
2047
|
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
1919
2048
|
}
|
|
1920
|
-
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
|
|
2049
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
|
|
1921
2050
|
diagnostics.push({
|
|
1922
2051
|
...location(source, offset),
|
|
1923
2052
|
message,
|
|
1924
2053
|
severity,
|
|
1925
2054
|
source: "ssml-static-validator",
|
|
1926
|
-
...code ? { code } : {}
|
|
2055
|
+
...code ? { code } : {},
|
|
2056
|
+
...metadata
|
|
1927
2057
|
});
|
|
1928
2058
|
}
|
|
1929
2059
|
function isSupportedProsodyRate(value) {
|
|
@@ -2396,9 +2526,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
|
|
|
2396
2526
|
for (const token of tokens) {
|
|
2397
2527
|
const tokenName = token.name.toLowerCase();
|
|
2398
2528
|
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
2529
|
+
const tokenDiagnosticStart = diagnostics.length;
|
|
2399
2530
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2400
2531
|
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2401
2532
|
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2533
|
+
annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
|
|
2402
2534
|
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2403
2535
|
addDiagnostic(
|
|
2404
2536
|
diagnostics,
|
|
@@ -2420,18 +2552,37 @@ function urlAttributes(token) {
|
|
|
2420
2552
|
return value === void 0 ? [] : [{ attribute, value }];
|
|
2421
2553
|
});
|
|
2422
2554
|
}
|
|
2555
|
+
function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
|
|
2556
|
+
const attributes = [...token.attributes.keys()];
|
|
2557
|
+
for (const diagnostic of diagnostics.slice(startIndex)) {
|
|
2558
|
+
const attributeName = attributes.find(
|
|
2559
|
+
(attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
|
|
2560
|
+
);
|
|
2561
|
+
const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
|
|
2562
|
+
Object.assign(diagnostic, {
|
|
2563
|
+
range: { start: token.start, end: token.end + 1 },
|
|
2564
|
+
tagName: token.name,
|
|
2565
|
+
...attributeName ? { attributeName } : {},
|
|
2566
|
+
...voiceName ? { voiceName } : {},
|
|
2567
|
+
...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
|
|
2568
|
+
nodePath,
|
|
2569
|
+
targetNodePath: [...token.path]
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2423
2573
|
function validateAzureSsml(ssml, options = {}) {
|
|
2424
2574
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2425
2575
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2426
2576
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2427
2577
|
const runnerOptions = options.urlValidation ?? {};
|
|
2428
|
-
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2578
|
+
const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
|
|
2429
2579
|
...runnerOptions,
|
|
2430
2580
|
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2431
2581
|
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2432
2582
|
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2433
2583
|
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2434
2584
|
});
|
|
2585
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2435
2586
|
let tokens;
|
|
2436
2587
|
try {
|
|
2437
2588
|
tokens = tokenizeElements(ssml);
|
|
@@ -2441,30 +2592,54 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2441
2592
|
const checks = tokens.flatMap(
|
|
2442
2593
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2443
2594
|
try {
|
|
2444
|
-
const result = await boundedValidator(
|
|
2595
|
+
const result = await boundedValidator(
|
|
2596
|
+
value,
|
|
2597
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2598
|
+
validationSignal
|
|
2599
|
+
);
|
|
2445
2600
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2446
2601
|
if (!valid) {
|
|
2447
2602
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
2603
|
+
const diagnosticStart = diagnostics.length;
|
|
2448
2604
|
addDiagnostic(
|
|
2449
2605
|
diagnostics,
|
|
2450
2606
|
ssml,
|
|
2451
2607
|
token.start,
|
|
2452
2608
|
`<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
|
|
2453
2609
|
);
|
|
2610
|
+
annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
|
|
2454
2611
|
}
|
|
2455
2612
|
} catch (error) {
|
|
2456
2613
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2614
|
+
const diagnosticStart = diagnostics.length;
|
|
2457
2615
|
addDiagnostic(
|
|
2458
2616
|
diagnostics,
|
|
2459
2617
|
ssml,
|
|
2460
2618
|
token.start,
|
|
2461
2619
|
`<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
|
|
2462
2620
|
);
|
|
2621
|
+
annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
|
|
2463
2622
|
}
|
|
2464
2623
|
})
|
|
2465
2624
|
);
|
|
2466
2625
|
return Promise.all(checks).then(() => diagnostics);
|
|
2467
2626
|
}
|
|
2627
|
+
async function validateAzureSsmlChunks(chunks, options = {}) {
|
|
2628
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2629
|
+
const sharedOptions = validator ? {
|
|
2630
|
+
...options,
|
|
2631
|
+
urlValidatorRunner: options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
|
|
2632
|
+
...options.urlValidation ?? {},
|
|
2633
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2634
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2635
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2636
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2637
|
+
})
|
|
2638
|
+
} : options;
|
|
2639
|
+
return Promise.all(
|
|
2640
|
+
chunks.map((chunk, chunkIndex) => Promise.resolve(validateAzureSsml(chunk, { ...sharedOptions, chunkIndex })))
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2468
2643
|
|
|
2469
2644
|
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2470
2645
|
var AZURE_VOICE_CATALOG_METADATA = {
|
|
@@ -2487,10 +2662,11 @@ export {
|
|
|
2487
2662
|
buildSsml,
|
|
2488
2663
|
parseSsml,
|
|
2489
2664
|
buildPartialSsml,
|
|
2490
|
-
|
|
2491
|
-
validateSsml,
|
|
2665
|
+
getSsmlSourceMap,
|
|
2492
2666
|
extractSsmlText,
|
|
2493
2667
|
mapSsmlTextNodes,
|
|
2668
|
+
splitSsmlDocument,
|
|
2669
|
+
validateSsml,
|
|
2494
2670
|
extractSsmlTranslatableText,
|
|
2495
2671
|
fromPlainTextToSsml,
|
|
2496
2672
|
validateSsmlStructureIntegrity,
|
|
@@ -2499,7 +2675,8 @@ export {
|
|
|
2499
2675
|
normalizeAzureLanguage,
|
|
2500
2676
|
areAzureLanguagesEquivalent,
|
|
2501
2677
|
validateAzureSsml,
|
|
2678
|
+
validateAzureSsmlChunks,
|
|
2502
2679
|
getAzureVoiceCatalogMetadata,
|
|
2503
2680
|
getBuiltInVoiceCatalogMetadata
|
|
2504
2681
|
};
|
|
2505
|
-
//# sourceMappingURL=chunk-
|
|
2682
|
+
//# sourceMappingURL=chunk-NKLZGITR.mjs.map
|