ssml-builder-js 2.8.1 → 2.10.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/dist/elements.js CHANGED
@@ -75,7 +75,13 @@ var SSML_TAGS = {
75
75
  SILENCE: "silence",
76
76
  MSTTS_VISEME: "mstts:viseme",
77
77
  VISEME: "viseme",
78
- MSTTS_AUDIO_DURATION: "mstts:audioduration"
78
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
79
+ MSTTS_DIALOG: "mstts:dialog",
80
+ MSTTS_TURN: "mstts:turn",
81
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
82
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
83
+ MSTTS_EMBEDDING: "mstts:embedding",
84
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
79
85
  };
80
86
  var SSML_ATTRS = {
81
87
  VERSION: "version",
@@ -84,6 +90,8 @@ var SSML_ATTRS = {
84
90
  LANG: "lang",
85
91
  MSTTS_XMLNS: "xmlns:mstts",
86
92
  NAME: "name",
93
+ VOICE: "voice",
94
+ SPEAKER: "speaker",
87
95
  EFFECT: "effect",
88
96
  RATE: "rate",
89
97
  PITCH: "pitch",
@@ -114,8 +122,15 @@ var SSML_ATTRS = {
114
122
  ALIAS: "alias",
115
123
  MARK: "mark",
116
124
  URI: "uri",
125
+ ID: "id",
126
+ MODEL: "model",
127
+ PROFILE: "profile",
128
+ URL: "url",
129
+ SPEAKER_PROFILE_ID: "speakerProfileId",
117
130
  TYPE: "type",
118
- VALUE: "value"
131
+ VALUE: "value",
132
+ FADE_IN: "fadein",
133
+ FADE_OUT: "fadeout"
119
134
  };
120
135
  function escapeText(value) {
121
136
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -205,6 +220,30 @@ function getAttributes(element) {
205
220
  case SSML_TAGS.MSTTS_AUDIO_DURATION:
206
221
  addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
207
222
  break;
223
+ case SSML_TAGS.MSTTS_TURN:
224
+ addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
225
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
226
+ break;
227
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
228
+ addAttribute(attributes, SSML_ATTRS.SRC, element.src);
229
+ addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
230
+ addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
231
+ addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
232
+ break;
233
+ case SSML_TAGS.MSTTS_DIALOG:
234
+ break;
235
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING:
236
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
237
+ break;
238
+ case SSML_TAGS.MSTTS_EMBEDDING:
239
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
240
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
241
+ break;
242
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION:
243
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
244
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
245
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
246
+ break;
208
247
  case SSML_TAGS.PARAGRAPH:
209
248
  case SSML_TAGS.SENTENCE:
210
249
  case SSML_TAGS.WORD:
@@ -769,6 +808,54 @@ function convertElement(node) {
769
808
  if (value !== void 0) element.value = value;
770
809
  return finishElement(element, node, attributes);
771
810
  }
811
+ case SSML_TAGS.MSTTS_DIALOG: {
812
+ const element = { type: SSML_TAGS.MSTTS_DIALOG };
813
+ return finishElement(element, node, attributes);
814
+ }
815
+ case SSML_TAGS.MSTTS_TURN: {
816
+ const element = { type: SSML_TAGS.MSTTS_TURN };
817
+ const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
818
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
819
+ if (voice !== void 0) element.voice = voice;
820
+ if (speaker !== void 0) element.speaker = speaker;
821
+ return finishElement(element, node, attributes);
822
+ }
823
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
824
+ const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
825
+ const src = readAttribute(attributes, SSML_ATTRS.SRC);
826
+ const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
827
+ const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
828
+ const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
829
+ if (src !== void 0) element.src = src;
830
+ if (volume !== void 0) element.volume = volume;
831
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
832
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
833
+ return finishElement(element, node, attributes);
834
+ }
835
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
836
+ const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
837
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
838
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
839
+ return finishElement(element, node, attributes);
840
+ }
841
+ case SSML_TAGS.MSTTS_EMBEDDING: {
842
+ const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
843
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
844
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
845
+ if (id !== void 0) element.id = id;
846
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
847
+ return finishElement(element, node, attributes);
848
+ }
849
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
850
+ const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
851
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
852
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
853
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
854
+ if (url !== void 0) element.url = url;
855
+ if (profile !== void 0) element.profile = profile;
856
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
857
+ return finishElement(element, node, attributes);
858
+ }
772
859
  default: {
773
860
  const element = {
774
861
  name: node.name,
@@ -810,6 +897,683 @@ function parseSsml(xmlString) {
810
897
  }
811
898
  return document2;
812
899
  }
900
+ var AZURE_VOICE_DEFINITIONS = [
901
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
902
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
903
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
904
+ {
905
+ name: "en-US-GuyNeural",
906
+ locale: "en-US",
907
+ styles: [
908
+ "angry",
909
+ "cheerful",
910
+ "excited",
911
+ "friendly",
912
+ "hopeful",
913
+ "newscast",
914
+ "sad",
915
+ "shouting",
916
+ "terrified",
917
+ "unfriendly",
918
+ "whispering"
919
+ ]
920
+ },
921
+ {
922
+ name: "en-US-JennyMultilingualNeural",
923
+ locale: "en-US",
924
+ styles: [
925
+ "cheerful",
926
+ "empathetic",
927
+ "excited",
928
+ "friendly",
929
+ "hopeful",
930
+ "sad",
931
+ "shouting",
932
+ "terrified",
933
+ "unfriendly",
934
+ "whispering"
935
+ ]
936
+ },
937
+ {
938
+ name: "en-US-JennyNeural",
939
+ locale: "en-US",
940
+ styles: [
941
+ "assistant",
942
+ "chat",
943
+ "customerservice",
944
+ "newscast",
945
+ "cheerful",
946
+ "empathetic",
947
+ "excited",
948
+ "friendly",
949
+ "hopeful",
950
+ "sad",
951
+ "shouting",
952
+ "terrified",
953
+ "unfriendly",
954
+ "whispering"
955
+ ]
956
+ },
957
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
958
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
959
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
960
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
961
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
962
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
963
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
964
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
965
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
966
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
967
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
968
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
969
+ {
970
+ name: "ru-RU-SvetlanaNeural",
971
+ locale: "ru-RU",
972
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
973
+ },
974
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
975
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
976
+ {
977
+ name: "zh-CN-XiaoxiaoNeural",
978
+ locale: "zh-CN",
979
+ styles: [
980
+ "assistant",
981
+ "chat",
982
+ "customerservice",
983
+ "newscast",
984
+ "cheerful",
985
+ "empathetic",
986
+ "excited",
987
+ "friendly",
988
+ "hopeful",
989
+ "sad",
990
+ "terrified",
991
+ "whispering",
992
+ "poetry-reading",
993
+ "sports_commentary",
994
+ "sports_commentary_excited",
995
+ "story"
996
+ ]
997
+ },
998
+ {
999
+ name: "zh-CN-YunxiNeural",
1000
+ locale: "zh-CN",
1001
+ styles: [
1002
+ "narration-relaxed",
1003
+ "embarrassed",
1004
+ "fearful",
1005
+ "sad",
1006
+ "disgruntled",
1007
+ "serious",
1008
+ "angry",
1009
+ "depressed",
1010
+ "chat",
1011
+ "cheerful",
1012
+ "assistant"
1013
+ ]
1014
+ },
1015
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
1016
+ ];
1017
+ var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1018
+ var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1019
+ "characters",
1020
+ "spell-out",
1021
+ "cardinal",
1022
+ "ordinal",
1023
+ "number",
1024
+ "date",
1025
+ "time",
1026
+ "telephone",
1027
+ "fraction",
1028
+ "address",
1029
+ "name",
1030
+ "currency",
1031
+ "number_digit"
1032
+ ]);
1033
+ var ALLOWED_ROLES = /* @__PURE__ */ new Set([
1034
+ "Girl",
1035
+ "Boy",
1036
+ "YoungAdultFemale",
1037
+ "YoungAdultMale",
1038
+ "OlderAdultFemale",
1039
+ "OlderAdultMale",
1040
+ "SeniorFemale",
1041
+ "SeniorMale"
1042
+ ]);
1043
+ var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
1044
+ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1045
+ "Leading",
1046
+ "Tailing",
1047
+ "Sentenceboundary",
1048
+ "Comma",
1049
+ "Semicolon",
1050
+ "Enumerationcomma"
1051
+ ]);
1052
+ var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1053
+ function decodeAttribute(value) {
1054
+ return value.replace(
1055
+ /&(?:amp|apos|gt|lt|quot);/gi,
1056
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
1057
+ );
1058
+ }
1059
+ function findTagEnd2(source, start) {
1060
+ let quote = "";
1061
+ for (let index = start; index < source.length; index += 1) {
1062
+ const character = source[index];
1063
+ if (quote) {
1064
+ if (character === quote) quote = "";
1065
+ } else if (character === '"' || character === "'") quote = character;
1066
+ else if (character === ">") return index;
1067
+ }
1068
+ return source.length - 1;
1069
+ }
1070
+ function tokenizeElements(source) {
1071
+ const tokens = [];
1072
+ const openElements = [];
1073
+ let index = 0;
1074
+ while (index < source.length) {
1075
+ const start = source.indexOf("<", index);
1076
+ if (start === -1) break;
1077
+ if (source.startsWith("<!--", start)) {
1078
+ const end2 = source.indexOf("-->", start + 4);
1079
+ index = end2 === -1 ? source.length : end2 + 3;
1080
+ continue;
1081
+ }
1082
+ if (source.startsWith("<![CDATA[", start)) {
1083
+ const end2 = source.indexOf("]]>", start + 9);
1084
+ index = end2 === -1 ? source.length : end2 + 3;
1085
+ continue;
1086
+ }
1087
+ if (source.startsWith("<?", start)) {
1088
+ const end2 = source.indexOf("?>", start + 2);
1089
+ index = end2 === -1 ? source.length : end2 + 2;
1090
+ continue;
1091
+ }
1092
+ const end = findTagEnd2(source, start + 1);
1093
+ const raw = source.slice(start, end + 1);
1094
+ if (raw.startsWith("</")) {
1095
+ openElements.pop();
1096
+ index = end + 1;
1097
+ continue;
1098
+ }
1099
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
1100
+ if (!nameMatch?.[1]) {
1101
+ index = end + 1;
1102
+ continue;
1103
+ }
1104
+ const attributes = /* @__PURE__ */ new Map();
1105
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
1106
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
1107
+ for (const match of attributeSource.matchAll(attributePattern)) {
1108
+ attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
1109
+ }
1110
+ const selfClosing = /\/\s*>$/.test(raw);
1111
+ const parent = openElements[openElements.length - 1];
1112
+ const childElementIndex = parent?.childElementCount;
1113
+ if (parent) parent.childElementCount += 1;
1114
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1115
+ const tokenName = nameMatch[1];
1116
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1117
+ tokens.push({
1118
+ attributes,
1119
+ childElementIndex,
1120
+ end,
1121
+ name: tokenName,
1122
+ parentName: parent?.name,
1123
+ parentVoiceName,
1124
+ selfClosing,
1125
+ start
1126
+ });
1127
+ if (!selfClosing) {
1128
+ openElements.push({
1129
+ childElementCount: 0,
1130
+ name: tokenName,
1131
+ voiceName: tokenVoiceName
1132
+ });
1133
+ }
1134
+ index = end + 1;
1135
+ }
1136
+ return tokens;
1137
+ }
1138
+ function location(source, offset) {
1139
+ const before = source.slice(0, Math.max(0, offset));
1140
+ const line = before.split("\n").length;
1141
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
1142
+ }
1143
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
1144
+ diagnostics.push({
1145
+ ...location(source, offset),
1146
+ message,
1147
+ severity,
1148
+ source: "ssml-static-validator",
1149
+ ...code2 ? { code: code2 } : {}
1150
+ });
1151
+ }
1152
+ function isSupportedProsodyRate(value) {
1153
+ const trimmed = value.trim();
1154
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
1155
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
1156
+ if (!multiplier) return false;
1157
+ const numericValue = Number(multiplier[1]);
1158
+ return numericValue >= 0.5 && numericValue <= 2;
1159
+ }
1160
+ function isValidAzureAudioDuration(value) {
1161
+ const trimmed = value.trim();
1162
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
1163
+ if (numeric) return Number(numeric[1]) > 0;
1164
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
1165
+ if (!clock) return false;
1166
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1167
+ }
1168
+ function isValidAzureBackgroundAudioDuration(value) {
1169
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1170
+ if (!match) return false;
1171
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1172
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1173
+ }
1174
+ function attr(token, name) {
1175
+ return token.attributes.get(name.toLowerCase());
1176
+ }
1177
+ var DEFAULT_LANGUAGE_ALIASES = {
1178
+ "zh-CN": ["zh-Hans"],
1179
+ "zh-TW": ["zh-Hant"]
1180
+ };
1181
+ function canonicalLanguageTag(language) {
1182
+ const trimmed = language.trim();
1183
+ if (!trimmed) return "";
1184
+ try {
1185
+ return new Intl.Locale(trimmed).toString().toLowerCase();
1186
+ } catch {
1187
+ return trimmed.toLowerCase();
1188
+ }
1189
+ }
1190
+ function createLanguageNormalizer(options) {
1191
+ const aliases = /* @__PURE__ */ new Map();
1192
+ const addAliasGroup = (canonical, values) => {
1193
+ const normalizedCanonical = canonicalLanguageTag(canonical);
1194
+ if (!normalizedCanonical) return;
1195
+ aliases.set(normalizedCanonical, normalizedCanonical);
1196
+ for (const value of values) {
1197
+ const normalizedValue = canonicalLanguageTag(value);
1198
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
1199
+ }
1200
+ };
1201
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
1202
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
1203
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
1204
+ return (language) => {
1205
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
1206
+ const normalized = canonicalLanguageTag(customValue);
1207
+ return aliases.get(normalized) ?? normalized;
1208
+ };
1209
+ }
1210
+ function voiceLocalePrefix(voiceName) {
1211
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
1212
+ if (!match?.groups) return void 0;
1213
+ const tag = `${match.groups.language}-${match.groups.region}`;
1214
+ return {
1215
+ language: match.groups.language.toLowerCase(),
1216
+ region: match.groups.region.toLowerCase(),
1217
+ tag
1218
+ };
1219
+ }
1220
+ function definitionFromStyleMap(voiceName, styles) {
1221
+ return {
1222
+ name: voiceName,
1223
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
1224
+ styles
1225
+ };
1226
+ }
1227
+ function normalizeVoiceCatalog(options) {
1228
+ const definitions = /* @__PURE__ */ new Map();
1229
+ for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
1230
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
1231
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
1232
+ for (const definition of options.customVoiceDefinitions ?? [])
1233
+ definitions.set(definition.name.toLowerCase(), definition);
1234
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
1235
+ const key = voiceName.toLowerCase();
1236
+ const current = definitions.get(key);
1237
+ definitions.set(key, {
1238
+ ...current ?? definitionFromStyleMap(voiceName, styles),
1239
+ name: current?.name ?? voiceName,
1240
+ styles: styles.map((style) => style.toLowerCase())
1241
+ });
1242
+ }
1243
+ return definitions;
1244
+ }
1245
+ function diagnosticSeverity(policy) {
1246
+ if (policy === "ignore") return void 0;
1247
+ return policy === "error" ? "error" : "warning";
1248
+ }
1249
+ function languagePart(language) {
1250
+ try {
1251
+ return new Intl.Locale(language).language.toLowerCase();
1252
+ } catch {
1253
+ return language.split("-")[0]?.toLowerCase() ?? "";
1254
+ }
1255
+ }
1256
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
1257
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
1258
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
1259
+ const normalizedLanguage = normalizeLanguage(language);
1260
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
1261
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
1262
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
1263
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
1264
+ }
1265
+ function canonicalTagName(name) {
1266
+ const normalized = name.toLowerCase();
1267
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
1268
+ if (normalized === "sayas") return "say-as";
1269
+ return normalized;
1270
+ }
1271
+ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
1272
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
1273
+ return;
1274
+ const tagName = canonicalTagName(token.name);
1275
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
1276
+ const supportedTags = definition.supportedTags?.map(canonicalTagName);
1277
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
1278
+ addDiagnostic(
1279
+ diagnostics,
1280
+ source,
1281
+ token.start,
1282
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
1283
+ "error",
1284
+ "azure-unsupported-tag-for-voice"
1285
+ );
1286
+ }
1287
+ }
1288
+ function validateAudioSource(token, source, diagnostics, options, elementName2) {
1289
+ const src = attr(token, "src");
1290
+ if (!src) {
1291
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
1292
+ return;
1293
+ }
1294
+ let parsed;
1295
+ try {
1296
+ parsed = new URL(src);
1297
+ } catch {
1298
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1299
+ return;
1300
+ }
1301
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1302
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1303
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1304
+ try {
1305
+ return new URL(allowedOrigin).origin === parsed.origin;
1306
+ } catch {
1307
+ return allowedOrigin === parsed.origin;
1308
+ }
1309
+ }) ?? false;
1310
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1311
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1312
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1313
+ addDiagnostic(
1314
+ diagnostics,
1315
+ source,
1316
+ token.start,
1317
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
1318
+ );
1319
+ }
1320
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1321
+ const name = token.name.toLowerCase();
1322
+ if (name === "voice" && !attr(token, "name")?.trim())
1323
+ addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1324
+ if (name === "break") {
1325
+ const time = attr(token, "time");
1326
+ const strength = attr(token, "strength");
1327
+ if (!time && !strength)
1328
+ addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
1329
+ if (time && strength)
1330
+ addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
1331
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
1332
+ addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
1333
+ if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
1334
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
1335
+ }
1336
+ if (name === "prosody") {
1337
+ const rate = attr(token, "rate");
1338
+ const pitch = attr(token, "pitch");
1339
+ const volume = attr(token, "volume");
1340
+ if (rate && !isSupportedProsodyRate(rate))
1341
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
1342
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
1343
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
1344
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
1345
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
1346
+ }
1347
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
1348
+ const style = attr(token, "style");
1349
+ if (!style?.trim())
1350
+ addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
1351
+ const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
1352
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
1353
+ addDiagnostic(
1354
+ diagnostics,
1355
+ source,
1356
+ token.start,
1357
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
1358
+ );
1359
+ const role = attr(token, "role");
1360
+ if (role && !ALLOWED_ROLES.has(role))
1361
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
1362
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
1363
+ const supportedStyles = definition?.styles;
1364
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
1365
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
1366
+ addDiagnostic(
1367
+ diagnostics,
1368
+ source,
1369
+ token.start,
1370
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
1371
+ severity,
1372
+ "azure-unsupported-style"
1373
+ );
1374
+ if (style && voiceName && !definition && severity)
1375
+ addDiagnostic(
1376
+ diagnostics,
1377
+ source,
1378
+ token.start,
1379
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
1380
+ severity
1381
+ );
1382
+ }
1383
+ if (name === "say-as" || name === "sayas") {
1384
+ const interpretAs = attr(token, "interpret-as");
1385
+ if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
1386
+ addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
1387
+ }
1388
+ if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
1389
+ addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
1390
+ if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
1391
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
1392
+ if (name === "sub" && !attr(token, "alias")?.trim())
1393
+ addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
1394
+ if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
1395
+ addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
1396
+ if (name === "mark" && !attr(token, "name")?.trim())
1397
+ addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
1398
+ if (name === "bookmark" && !attr(token, "mark")?.trim())
1399
+ addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
1400
+ if (name === "lexicon") {
1401
+ const uri = attr(token, "uri");
1402
+ if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
1403
+ else {
1404
+ try {
1405
+ const parsed = new URL(uri);
1406
+ if (parsed.protocol !== "https:")
1407
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
1408
+ } catch {
1409
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
1410
+ }
1411
+ }
1412
+ }
1413
+ if (name === "mstts:silence") {
1414
+ const type = attr(token, "type");
1415
+ const value = attr(token, "value");
1416
+ if (!type || !ALLOWED_SILENCE_TYPES.has(type))
1417
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
1418
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
1419
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
1420
+ }
1421
+ if (name === "mstts:audioduration") {
1422
+ const value = attr(token, "value");
1423
+ if (!value || !isValidAzureAudioDuration(value))
1424
+ addDiagnostic(
1425
+ diagnostics,
1426
+ source,
1427
+ token.start,
1428
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
1429
+ );
1430
+ if (!token.selfClosing)
1431
+ addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
1432
+ }
1433
+ if (name === "mstts:viseme") {
1434
+ const type = attr(token, "type");
1435
+ if (!type || !ALLOWED_VISEME_TYPES.has(type))
1436
+ addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
1437
+ }
1438
+ if (name === "audio") {
1439
+ validateAudioSource(token, source, diagnostics, options, "audio");
1440
+ }
1441
+ if (name === "mstts:turn") {
1442
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
1443
+ addDiagnostic(
1444
+ diagnostics,
1445
+ source,
1446
+ token.start,
1447
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
1448
+ );
1449
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
1450
+ addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1451
+ }
1452
+ if (name === "mstts:backgroundaudio") {
1453
+ validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1454
+ const volume = attr(token, "volume");
1455
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1456
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1457
+ for (const [attribute, value] of [
1458
+ ["fadein", attr(token, "fadein")],
1459
+ ["fadeout", attr(token, "fadeout")]
1460
+ ]) {
1461
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1462
+ addDiagnostic(
1463
+ diagnostics,
1464
+ source,
1465
+ token.start,
1466
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1467
+ );
1468
+ }
1469
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
1470
+ addDiagnostic(
1471
+ diagnostics,
1472
+ source,
1473
+ token.start,
1474
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
1475
+ );
1476
+ if (!token.selfClosing)
1477
+ addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1478
+ }
1479
+ }
1480
+ function validateAzureSsml(ssml, options = {}) {
1481
+ const diagnostics = [];
1482
+ if (typeof ssml !== "string") {
1483
+ return [
1484
+ {
1485
+ line: 1,
1486
+ column: 1,
1487
+ message: "SSML input must be a string",
1488
+ severity: "error",
1489
+ source: "ssml-static-validator"
1490
+ }
1491
+ ];
1492
+ }
1493
+ const maxLength = options.maxLength ?? 1e4;
1494
+ if (ssml.length > maxLength)
1495
+ addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
1496
+ try {
1497
+ parseSsml(ssml);
1498
+ } catch (error) {
1499
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
1500
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
1501
+ addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
1502
+ return diagnostics;
1503
+ }
1504
+ const tokens = tokenizeElements(ssml);
1505
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1506
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
1507
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
1508
+ for (const [index, token] of backgroundAudioTokens.entries()) {
1509
+ if (index > 0)
1510
+ addDiagnostic(
1511
+ diagnostics,
1512
+ ssml,
1513
+ token.start,
1514
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
1515
+ );
1516
+ }
1517
+ if (!speak || voices.length === 0)
1518
+ addDiagnostic(
1519
+ diagnostics,
1520
+ ssml,
1521
+ speak?.start ?? 0,
1522
+ "Azure SSML requires at least one <voice> element under <speak>."
1523
+ );
1524
+ const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
1525
+ const voiceCatalog = normalizeVoiceCatalog(options);
1526
+ const normalizeLanguage = createLanguageNormalizer(options);
1527
+ const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
1528
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
1529
+ for (const token of voicesToValidate) {
1530
+ const name = attr(token, "name")?.trim();
1531
+ const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1532
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
1533
+ if (name && !definition && policySeverity)
1534
+ addDiagnostic(
1535
+ diagnostics,
1536
+ ssml,
1537
+ token.start,
1538
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
1539
+ policySeverity,
1540
+ "azure-unknown-voice"
1541
+ );
1542
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
1543
+ addDiagnostic(
1544
+ diagnostics,
1545
+ ssml,
1546
+ token.start,
1547
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
1548
+ "warning",
1549
+ "azure-locale-mismatch"
1550
+ );
1551
+ }
1552
+ for (const token of tokens) {
1553
+ const tokenName = token.name.toLowerCase();
1554
+ const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
1555
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
1556
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
1557
+ validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
1558
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
1559
+ addDiagnostic(
1560
+ diagnostics,
1561
+ ssml,
1562
+ token.start,
1563
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
1564
+ "error",
1565
+ "azure-unsupported-model-for-voice"
1566
+ );
1567
+ }
1568
+ }
1569
+ return diagnostics;
1570
+ }
1571
+ var AZURE_VOICE_CATALOG_METADATA = {
1572
+ apiVersion: "2025-10-01",
1573
+ generatedAt: "2026-08-28T00:00:00.000Z",
1574
+ regions: [],
1575
+ voiceCount: AZURE_VOICE_DEFINITIONS.length
1576
+ };
813
1577
 
814
1578
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
815
1579
  function getDocumentChildren(document2) {
@@ -1948,6 +2712,11 @@ var SSML_ATTRIBUTE_PRESETS = {
1948
2712
  "mstts:audioduration": {
1949
2713
  value: AUDIO_DURATION_PRESETS
1950
2714
  },
2715
+ "mstts:backgroundaudio": {
2716
+ volume: PROSODY_VOLUME_PRESETS,
2717
+ fadein: AUDIO_DURATION_PRESETS,
2718
+ fadeout: AUDIO_DURATION_PRESETS
2719
+ },
1951
2720
  silence: {
1952
2721
  type: SILENCE_TYPE_PRESETS,
1953
2722
  value: SILENCE_VALUE_PRESETS
@@ -2284,7 +3053,7 @@ var SILENCE_VALUE_DESCRIPTIONS = {
2284
3053
  };
2285
3054
 
2286
3055
  // packages/ssml-editor-react/src/ssmlContext.ts
2287
- function findTagEnd2(source, start, limit) {
3056
+ function findTagEnd3(source, start, limit) {
2288
3057
  let quote;
2289
3058
  for (let index = start + 1; index < limit; index += 1) {
2290
3059
  const character = source[index];
@@ -2334,7 +3103,7 @@ function findActiveSsmlTags(source, offset) {
2334
3103
  index = end;
2335
3104
  continue;
2336
3105
  }
2337
- const tagEnd = findTagEnd2(source, tagStart, source.length);
3106
+ const tagEnd = findTagEnd3(source, tagStart, source.length);
2338
3107
  const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
2339
3108
  const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
2340
3109
  const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
@@ -2377,7 +3146,7 @@ function findSsmlVoiceContext(source, offset) {
2377
3146
  index = end === -1 || end + 2 > limit ? limit : end + 2;
2378
3147
  continue;
2379
3148
  }
2380
- const tagEnd = findTagEnd2(source, tagStart, limit);
3149
+ const tagEnd = findTagEnd3(source, tagStart, limit);
2381
3150
  if (tagEnd === -1) {
2382
3151
  break;
2383
3152
  }
@@ -2439,6 +3208,24 @@ var SSML_COMPLETION_SNIPPETS = [
2439
3208
  label: "mstts:audioduration",
2440
3209
  insertText: '<mstts:audioduration value="10s" />'
2441
3210
  },
3211
+ {
3212
+ label: "mstts:dialog",
3213
+ insertText: `<mstts:dialog>
3214
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
3215
+ </mstts:dialog>`
3216
+ },
3217
+ {
3218
+ label: "mstts:turn",
3219
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
3220
+ },
3221
+ {
3222
+ label: "mstts:backgroundaudio",
3223
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
3224
+ },
3225
+ {
3226
+ label: "mstts:ttsembedding",
3227
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
3228
+ },
2442
3229
  {
2443
3230
  label: "sub",
2444
3231
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -3184,6 +3971,63 @@ var SSML_TAG_DEFINITIONS = [
3184
3971
  example: "10s"
3185
3972
  }
3186
3973
  ]
3974
+ },
3975
+ {
3976
+ name: "mstts:dialog",
3977
+ description: "Groups multiple Azure dialog turns that can use different voices.",
3978
+ parameters: []
3979
+ },
3980
+ {
3981
+ name: "mstts:turn",
3982
+ description: "Adds one dialog turn using the required Azure voice name.",
3983
+ parameters: [
3984
+ {
3985
+ name: "voice",
3986
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
3987
+ example: "en-US-JennyNeural"
3988
+ }
3989
+ ]
3990
+ },
3991
+ {
3992
+ name: "mstts:backgroundaudio",
3993
+ description: "Plays background audio while speech is synthesized.",
3994
+ parameters: [
3995
+ {
3996
+ name: "src",
3997
+ description: "An absolute HTTP(S) URL for the background audio file.",
3998
+ example: "https://example.com/music.mp3"
3999
+ },
4000
+ {
4001
+ name: "volume",
4002
+ description: "The background audio volume, for example `-3dB` or `medium`.",
4003
+ example: "-3dB"
4004
+ },
4005
+ {
4006
+ name: "fadein",
4007
+ description: "The fade-in duration, for example `1s`.",
4008
+ example: "1s"
4009
+ },
4010
+ {
4011
+ name: "fadeout",
4012
+ description: "The fade-out duration, for example `500ms`.",
4013
+ example: "500ms"
4014
+ }
4015
+ ]
4016
+ },
4017
+ {
4018
+ name: "mstts:ttsembedding",
4019
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
4020
+ parameters: []
4021
+ },
4022
+ {
4023
+ name: "mstts:embedding",
4024
+ description: "Specifies embedding metadata for custom voice scenarios.",
4025
+ parameters: []
4026
+ },
4027
+ {
4028
+ name: "mstts:voiceconversion",
4029
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
4030
+ parameters: []
3187
4031
  }
3188
4032
  ];
3189
4033
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -3242,7 +4086,7 @@ function toRange(source, token) {
3242
4086
  function containsOffset(token, offset) {
3243
4087
  return offset >= token.start && offset < token.end;
3244
4088
  }
3245
- function findTagEnd3(source, start) {
4089
+ function findTagEnd4(source, start) {
3246
4090
  let quote;
3247
4091
  for (let index = start; index < source.length; index += 1) {
3248
4092
  const character = source[index];
@@ -3373,7 +4217,7 @@ function findTagAtOffset(source, offset) {
3373
4217
  searchStart = tokenEnd2;
3374
4218
  continue;
3375
4219
  }
3376
- const tagEnd = findTagEnd3(source, start + 1);
4220
+ const tagEnd = findTagEnd4(source, start + 1);
3377
4221
  const contentEnd = tagEnd ?? source.length;
3378
4222
  const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
3379
4223
  if (offset < tokenEnd) {
@@ -3989,6 +4833,60 @@ section[data-ssml-editor] {
3989
4833
  border-radius: 0.25rem;
3990
4834
  overflow: visible;
3991
4835
  }
4836
+ [data-ssml-editor] .ssml-editor-visual {
4837
+ display: grid;
4838
+ gap: 0.75rem;
4839
+ min-height: 8rem;
4840
+ padding: 0.75rem;
4841
+ border: 1px solid var(--ssml-editor-control-border);
4842
+ border-radius: 0.25rem;
4843
+ }
4844
+ [data-ssml-editor] .ssml-editor-visual-layout {
4845
+ display: grid;
4846
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
4847
+ gap: 1rem;
4848
+ }
4849
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
4850
+ margin: 0;
4851
+ padding-left: 1.25rem;
4852
+ }
4853
+ [data-ssml-editor] .ssml-editor-visual button {
4854
+ padding: 0.35rem 0.5rem;
4855
+ border: 1px solid var(--ssml-editor-control-border);
4856
+ border-radius: 0.25rem;
4857
+ color: var(--ssml-editor-color);
4858
+ background: var(--ssml-editor-control-bg);
4859
+ cursor: pointer;
4860
+ }
4861
+ [data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
4862
+ border-color: var(--ssml-editor-active-border);
4863
+ background: var(--ssml-editor-active-bg);
4864
+ }
4865
+ [data-ssml-editor] .ssml-editor-visual textarea {
4866
+ box-sizing: border-box;
4867
+ width: 100%;
4868
+ min-height: 6rem;
4869
+ padding: 0.5rem;
4870
+ color: var(--ssml-editor-color);
4871
+ background: var(--ssml-editor-control-bg);
4872
+ border: 1px solid var(--ssml-editor-control-border);
4873
+ border-radius: 0.25rem;
4874
+ font: inherit;
4875
+ }
4876
+ [data-ssml-editor] .ssml-editor-visual-actions,
4877
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb {
4878
+ display: flex;
4879
+ flex-wrap: wrap;
4880
+ gap: 0.4rem;
4881
+ align-items: center;
4882
+ }
4883
+ [data-ssml-editor] .ssml-editor-visual-errors {
4884
+ padding: 0.5rem;
4885
+ color: #b91c1c;
4886
+ background: #fef2f2;
4887
+ border: 1px solid #b91c1c;
4888
+ border-radius: 0.25rem;
4889
+ }
3992
4890
  `.trim();
3993
4891
  function injectStyles() {
3994
4892
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
@@ -4002,6 +4900,55 @@ function injectStyles() {
4002
4900
  function isDarkTheme(theme) {
4003
4901
  return theme === "vs-dark" || theme.toLowerCase().includes("dark");
4004
4902
  }
4903
+ function isElement(node) {
4904
+ return typeof node !== "string" && node.type !== "text";
4905
+ }
4906
+ function visualElementName(element) {
4907
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
4908
+ }
4909
+ function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
4910
+ return nodes.flatMap((node, index) => {
4911
+ const currentPath = [...path, index];
4912
+ if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
4913
+ if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
4914
+ return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
4915
+ });
4916
+ }
4917
+ function updateVisualNodes(nodes, path, update) {
4918
+ if (path.length === 0) return nodes;
4919
+ const [index, ...rest] = path;
4920
+ return nodes.flatMap((node, nodeIndex) => {
4921
+ if (nodeIndex !== index) return [node];
4922
+ if (rest.length === 0) {
4923
+ const next = update(node);
4924
+ return Array.isArray(next) ? next : [next];
4925
+ }
4926
+ if (!isElement(node)) return [node];
4927
+ return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
4928
+ });
4929
+ }
4930
+ function updateVisualText(document2, path, value) {
4931
+ return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
4932
+ }
4933
+ function wrapVisualText(document2, path, start, end, type, attributes) {
4934
+ return {
4935
+ ...document2,
4936
+ children: updateVisualNodes(document2.children ?? [], path, (node) => {
4937
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
4938
+ if (!value || start === end) return node;
4939
+ if (type === "break") {
4940
+ return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
4941
+ (part) => typeof part === "string" ? part.length > 0 : true
4942
+ );
4943
+ }
4944
+ const selected = value.slice(start, end);
4945
+ const wrapper = { type, attributes, children: [selected] };
4946
+ return [value.slice(0, start), wrapper, value.slice(end)].filter(
4947
+ (part) => typeof part === "string" ? part.length > 0 : true
4948
+ );
4949
+ })
4950
+ };
4951
+ }
4005
4952
  function getMenuPosition(trigger, menu) {
4006
4953
  const bounds = trigger.getBoundingClientRect();
4007
4954
  const margin = 8;
@@ -4023,6 +4970,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
4023
4970
  this.toolbarActions = null;
4024
4971
  this.display = null;
4025
4972
  this.editorContainer = null;
4973
+ this.visualContainer = null;
4026
4974
  this.helpPanel = null;
4027
4975
  this.openMenu = null;
4028
4976
  this.openMenuTrigger = null;
@@ -4036,6 +4984,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
4036
4984
  this.documentState = null;
4037
4985
  this.decorationsVisible = false;
4038
4986
  this.helpOpen = false;
4987
+ this.visualSelectedPath = null;
4988
+ this.visualSelection = { start: 0, end: 0 };
4039
4989
  this.handleDocumentPointerDown = (event) => {
4040
4990
  const target = event.target;
4041
4991
  if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
@@ -4077,6 +5027,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
4077
5027
  set locale(locale) {
4078
5028
  this.setAttribute("locale", locale);
4079
5029
  }
5030
+ get editMode() {
5031
+ return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
5032
+ }
5033
+ set editMode(mode) {
5034
+ this.setAttribute("edit-mode", mode);
5035
+ }
4080
5036
  prepareDocument(value) {
4081
5037
  try {
4082
5038
  this.documentState = parseSsml(value);
@@ -4119,6 +5075,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
4119
5075
  }
4120
5076
  }
4121
5077
  }
5078
+ this.renderVisualEditor();
4122
5079
  return;
4123
5080
  }
4124
5081
  if (name === "theme" && this.monaco) {
@@ -4136,6 +5093,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
4136
5093
  this.renderHelp();
4137
5094
  return;
4138
5095
  }
5096
+ if (name === "edit-mode") {
5097
+ this.updateEditMode();
5098
+ }
4139
5099
  if (name === "show-decorations") {
4140
5100
  this.decorationsVisible = newValue !== null;
4141
5101
  this.updateDecorations();
@@ -4160,7 +5120,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
4160
5120
  display.dataset.ssmlEditorDisplay = "";
4161
5121
  const editorContainer = document.createElement("div");
4162
5122
  editorContainer.className = "ssml-editor-editor";
4163
- display.append(editorContainer);
5123
+ const visualContainer = document.createElement("div");
5124
+ visualContainer.className = "ssml-editor-visual";
5125
+ display.append(editorContainer, visualContainer);
4164
5126
  root.append(toolbar, display);
4165
5127
  this.replaceChildren(root);
4166
5128
  this.root = root;
@@ -4168,8 +5130,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
4168
5130
  this.toolbarActions = toolbarActions;
4169
5131
  this.display = display;
4170
5132
  this.editorContainer = editorContainer;
5133
+ this.visualContainer = visualContainer;
4171
5134
  this.renderToolbar();
4172
5135
  this.renderHelp();
5136
+ this.updateEditMode();
5137
+ this.renderVisualEditor();
4173
5138
  }
4174
5139
  renderToolbar() {
4175
5140
  const toolbar = this.toolbar;
@@ -4213,10 +5178,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
4213
5178
  for (const id of toolbarIds) {
4214
5179
  const group = groupByButtonId.get(id);
4215
5180
  if (previousGroup !== void 0 && group !== previousGroup) {
4216
- const separator = document.createElement("span");
4217
- separator.className = "ssml-editor-toolbar-separator";
4218
- separator.setAttribute("aria-hidden", "true");
4219
- toolbarActions.append(separator);
5181
+ const separator2 = document.createElement("span");
5182
+ separator2.className = "ssml-editor-toolbar-separator";
5183
+ separator2.setAttribute("aria-hidden", "true");
5184
+ toolbarActions.append(separator2);
4220
5185
  }
4221
5186
  previousGroup = group;
4222
5187
  const insertion = insertionById.get(id);
@@ -4228,8 +5193,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
4228
5193
  toolbarActions.append(this.createActionButton(id));
4229
5194
  }
4230
5195
  }
5196
+ const separator = document.createElement("span");
5197
+ separator.className = "ssml-editor-toolbar-separator";
5198
+ separator.setAttribute("aria-hidden", "true");
5199
+ toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
4231
5200
  this.updateActiveButtons();
4232
5201
  }
5202
+ createModeButton(mode, label) {
5203
+ const button = document.createElement("button");
5204
+ button.type = "button";
5205
+ button.className = "ssml-editor-toolbar-button";
5206
+ button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
5207
+ button.setAttribute("aria-pressed", String(this.editMode === mode));
5208
+ button.textContent = label;
5209
+ button.addEventListener("click", () => {
5210
+ this.editMode = mode;
5211
+ });
5212
+ return button;
5213
+ }
4233
5214
  createActionButton(id) {
4234
5215
  const copy = EDITOR_COPY[this.locale];
4235
5216
  const labels = {
@@ -4609,6 +5590,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
4609
5590
  this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
4610
5591
  }
4611
5592
  }
5593
+ updateEditMode() {
5594
+ if (this.editorContainer && this.visualContainer) {
5595
+ const visual = this.editMode === "visual";
5596
+ this.editorContainer.hidden = visual;
5597
+ this.visualContainer.hidden = !visual;
5598
+ }
5599
+ for (const mode of ["visual", "code"]) {
5600
+ const button = this.toolbarActions?.querySelector(
5601
+ `[data-ssml-editor-button="edit-mode-${mode}"]`
5602
+ );
5603
+ button?.setAttribute("aria-pressed", String(this.editMode === mode));
5604
+ button?.toggleAttribute("data-active", this.editMode === mode);
5605
+ }
5606
+ this.renderVisualEditor();
5607
+ }
5608
+ renderVisualEditor() {
5609
+ const container = this.visualContainer;
5610
+ if (!container) return;
5611
+ container.replaceChildren();
5612
+ if (!this.documentState) {
5613
+ const error = document.createElement("p");
5614
+ error.className = "ssml-editor-visual-errors";
5615
+ error.textContent = "SSML syntax must be valid before visual editing is available.";
5616
+ container.append(error);
5617
+ return;
5618
+ }
5619
+ const documentState = this.documentState;
5620
+ const leaves = collectVisualTextLeaves(documentState.children ?? []);
5621
+ const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
5622
+ const breadcrumb = document.createElement("div");
5623
+ breadcrumb.className = "ssml-editor-visual-breadcrumb";
5624
+ breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
5625
+ const clear = document.createElement("button");
5626
+ clear.type = "button";
5627
+ clear.textContent = "Clear parent";
5628
+ clear.disabled = !this.visualSelectedPath;
5629
+ clear.addEventListener("click", () => {
5630
+ this.visualSelectedPath = null;
5631
+ this.renderVisualEditor();
5632
+ });
5633
+ breadcrumb.append(clear);
5634
+ container.append(breadcrumb);
5635
+ const diagnostics = validateAzureSsml(buildSsml(documentState));
5636
+ if (diagnostics.length > 0) {
5637
+ const errors = document.createElement("div");
5638
+ errors.className = "ssml-editor-visual-errors";
5639
+ errors.setAttribute("role", "alert");
5640
+ for (const diagnostic of diagnostics) {
5641
+ const message = document.createElement("div");
5642
+ message.textContent = diagnostic.message;
5643
+ errors.append(message);
5644
+ }
5645
+ container.append(errors);
5646
+ }
5647
+ const layout = document.createElement("div");
5648
+ layout.className = "ssml-editor-visual-layout";
5649
+ const tree = document.createElement("nav");
5650
+ tree.className = "ssml-editor-visual-tree";
5651
+ tree.setAttribute("aria-label", "SSML structure tree");
5652
+ tree.append(document.createTextNode("Structure"));
5653
+ const treeList = document.createElement("ul");
5654
+ const renderTree = (nodes, parent, parentPath = []) => {
5655
+ nodes.forEach((node, index) => {
5656
+ if (!isElement(node)) return;
5657
+ const path = [...parentPath, index];
5658
+ const item = document.createElement("li");
5659
+ const button = document.createElement("button");
5660
+ button.type = "button";
5661
+ button.textContent = `<${visualElementName(node)}>`;
5662
+ button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
5663
+ button.addEventListener("click", () => {
5664
+ this.visualSelectedPath = path;
5665
+ this.renderVisualEditor();
5666
+ });
5667
+ item.append(button);
5668
+ if ((node.children ?? []).some(isElement)) {
5669
+ const childList = document.createElement("ul");
5670
+ renderTree(node.children ?? [], childList, path);
5671
+ item.append(childList);
5672
+ }
5673
+ parent.append(item);
5674
+ });
5675
+ };
5676
+ renderTree(documentState.children ?? [], treeList);
5677
+ tree.append(treeList);
5678
+ layout.append(tree);
5679
+ const form = document.createElement("div");
5680
+ form.className = "ssml-editor-visual-form";
5681
+ if (selectedLeaf) {
5682
+ const label = document.createElement("label");
5683
+ label.append(document.createTextNode("Text"));
5684
+ const textarea = document.createElement("textarea");
5685
+ textarea.value = selectedLeaf.value;
5686
+ textarea.readOnly = this.readonly;
5687
+ textarea.addEventListener("select", () => {
5688
+ this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
5689
+ });
5690
+ textarea.addEventListener("input", () => {
5691
+ if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
5692
+ });
5693
+ label.append(textarea);
5694
+ form.append(label);
5695
+ const actions = document.createElement("div");
5696
+ actions.className = "ssml-editor-visual-actions";
5697
+ const wrappers = [
5698
+ ["Rate", "prosody", { rate: "slow" }],
5699
+ ["Pitch", "prosody", { pitch: "high" }],
5700
+ ["Emotion", "mstts:express-as", { style: "cheerful" }],
5701
+ ["Pause", "break", { time: "500ms" }],
5702
+ ["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
5703
+ ];
5704
+ for (const [labelText, type, attributes] of wrappers) {
5705
+ const button = document.createElement("button");
5706
+ button.type = "button";
5707
+ button.textContent = labelText;
5708
+ button.disabled = this.readonly;
5709
+ button.addEventListener("click", () => {
5710
+ const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
5711
+ const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
5712
+ this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
5713
+ this.renderVisualEditor();
5714
+ });
5715
+ actions.append(button);
5716
+ }
5717
+ const preview = document.createElement("button");
5718
+ preview.type = "button";
5719
+ preview.textContent = "Preview selection";
5720
+ preview.addEventListener("click", () => {
5721
+ this.dispatchEvent(
5722
+ new CustomEvent("preview-selection", {
5723
+ detail: { ssml: buildSsml({ ...documentState, children: [selectedLeaf.value] }) },
5724
+ bubbles: true,
5725
+ composed: true
5726
+ })
5727
+ );
5728
+ });
5729
+ actions.append(preview);
5730
+ form.append(actions);
5731
+ } else {
5732
+ form.textContent = "Select an element or text node to edit it.";
5733
+ }
5734
+ layout.append(form);
5735
+ container.append(layout);
5736
+ }
4612
5737
  updateActiveButtons() {
4613
5738
  const editor = this.editor;
4614
5739
  const model = this.model;
@@ -4636,6 +5761,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
4636
5761
  return;
4637
5762
  }
4638
5763
  const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
5764
+ this.renderVisualEditor();
4639
5765
  const editor = monacoModule.editor.create(container, {
4640
5766
  model,
4641
5767
  theme: this.theme,
@@ -4734,7 +5860,8 @@ SsmlEditorElement.observedAttributes = [
4734
5860
  "locale",
4735
5861
  "show-toolbar",
4736
5862
  "show-toolbar-labels",
4737
- "show-decorations"
5863
+ "show-decorations",
5864
+ "edit-mode"
4738
5865
  ];
4739
5866
 
4740
5867
  // packages/ssml-editor-elements/src/index.ts