ssml-builder-js 2.1.0 → 2.2.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/react.js CHANGED
@@ -58,7 +58,7 @@ __export(ssmlPresets_exports, {
58
58
  EMPHASIS_LEVEL_DESCRIPTIONS: () => EMPHASIS_LEVEL_DESCRIPTIONS,
59
59
  EMPHASIS_LEVEL_PRESETS: () => EMPHASIS_LEVEL_PRESETS,
60
60
  EXPRESS_AS_ROLE_PRESETS: () => EXPRESS_AS_ROLE_PRESETS,
61
- EXPRESS_AS_STYLES_BY_VOICE: () => EXPRESS_AS_STYLES_BY_VOICE,
61
+ EXPRESS_AS_STYLE_CATEGORIES: () => EXPRESS_AS_STYLE_CATEGORIES,
62
62
  EXPRESS_AS_STYLE_DESCRIPTIONS: () => EXPRESS_AS_STYLE_DESCRIPTIONS,
63
63
  EXPRESS_AS_STYLE_PRESETS: () => EXPRESS_AS_STYLE_PRESETS,
64
64
  LANGUAGE_DESCRIPTIONS: () => LANGUAGE_DESCRIPTIONS,
@@ -80,6 +80,8 @@ __export(ssmlPresets_exports, {
80
80
  SSML_PRESETS: () => SSML_PRESETS,
81
81
  SSML_PRESET_EXAMPLES: () => SSML_PRESET_EXAMPLES,
82
82
  VISEME_TYPE_PRESETS: () => VISEME_TYPE_PRESETS,
83
+ VOICE_STYLE_MAP: () => VOICE_STYLE_MAP,
84
+ getExpressAsStyleCategory: () => getExpressAsStyleCategory,
83
85
  resolveExpressAsStyles: () => resolveExpressAsStyles
84
86
  });
85
87
  var SSML_PRESETS = [
@@ -124,21 +126,90 @@ var EXPRESS_AS_STYLE_PRESETS = [
124
126
  "shouting",
125
127
  "terrified",
126
128
  "unfriendly",
127
- "whispering"
129
+ "whispering",
130
+ "empathetic",
131
+ "relieved",
132
+ "fearful",
133
+ "depressed",
134
+ "disgruntled",
135
+ "embarrassed",
136
+ "narration-relaxed",
137
+ "poetry-reading",
138
+ "sports_commentary",
139
+ "sports_commentary_excited",
140
+ "story"
128
141
  ];
129
- var EXPRESS_AS_STYLES_BY_VOICE = {
130
- "ja-JP-NanamiNeural": ["chat", "cheerful", "customerservice"],
131
- "ja-JP-KeitaNeural": [],
132
- "en-US-JennyNeural": [
142
+ var EXPRESS_AS_STYLE_CATEGORIES = {
143
+ emotions: [
144
+ "cheerful",
145
+ "sad",
133
146
  "angry",
147
+ "calm",
148
+ "fearful",
149
+ "depressed",
150
+ "disgruntled",
151
+ "embarrassed",
152
+ "empathetic",
153
+ "envious",
154
+ "excited",
155
+ "friendly",
156
+ "gentle",
157
+ "hopeful",
158
+ "relieved",
159
+ "serious",
160
+ "shouting",
161
+ "terrified",
162
+ "unfriendly",
163
+ "whispering"
164
+ ],
165
+ scenarios: ["chat", "customerservice", "assistant", "livecommercial", "poetry-reading", "story"],
166
+ media: [
167
+ "newscast",
168
+ "newscast-casual",
169
+ "newscast-formal",
170
+ "narration-professional",
171
+ "narration-relaxed",
172
+ "documentary-narration",
173
+ "advertisement_upbeat",
174
+ "sports_commentary",
175
+ "sports_commentary_excited"
176
+ ]
177
+ };
178
+ function getExpressAsStyleCategory(style) {
179
+ for (const [category, styles] of Object.entries(EXPRESS_AS_STYLE_CATEGORIES)) {
180
+ if (styles.includes(style)) {
181
+ return category;
182
+ }
183
+ }
184
+ return "other";
185
+ }
186
+ var VOICE_STYLE_MAP = {
187
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
188
+ "ja-JP-KeitaNeural": ["chat"],
189
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
190
+ "en-US-JennyMultilingualNeural": [
191
+ "cheerful",
192
+ "empathetic",
193
+ "excited",
194
+ "friendly",
195
+ "hopeful",
196
+ "sad",
197
+ "shouting",
198
+ "terrified",
199
+ "unfriendly",
200
+ "whispering"
201
+ ],
202
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
203
+ "en-US-JennyNeural": [
134
204
  "assistant",
135
205
  "chat",
136
- "cheerful",
137
206
  "customerservice",
207
+ "newscast",
208
+ "cheerful",
209
+ "empathetic",
138
210
  "excited",
139
211
  "friendly",
140
212
  "hopeful",
141
- "newscast",
142
213
  "sad",
143
214
  "shouting",
144
215
  "terrified",
@@ -157,18 +228,58 @@ var EXPRESS_AS_STYLES_BY_VOICE = {
157
228
  "terrified",
158
229
  "unfriendly",
159
230
  "whispering"
160
- ]
231
+ ],
232
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
233
+ "zh-CN-XiaoxiaoNeural": [
234
+ "assistant",
235
+ "chat",
236
+ "customerservice",
237
+ "newscast",
238
+ "cheerful",
239
+ "empathetic",
240
+ "excited",
241
+ "friendly",
242
+ "hopeful",
243
+ "sad",
244
+ "terrified",
245
+ "whispering",
246
+ "poetry-reading",
247
+ "sports_commentary",
248
+ "sports_commentary_excited",
249
+ "story"
250
+ ],
251
+ "zh-CN-YunxiNeural": [
252
+ "narration-relaxed",
253
+ "embarrassed",
254
+ "fearful",
255
+ "sad",
256
+ "disgruntled",
257
+ "serious",
258
+ "angry",
259
+ "depressed",
260
+ "chat",
261
+ "cheerful",
262
+ "assistant"
263
+ ],
264
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
265
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
266
+ "pt-BR-FranciscaNeural": ["calm"],
267
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
268
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
269
+ "de-DE-ConradNeural": ["cheerful", "sad"],
270
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
161
271
  };
162
- var EXPRESS_AS_STYLES_BY_NORMALIZED_VOICE = new Map(
163
- Object.entries(EXPRESS_AS_STYLES_BY_VOICE).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
272
+ var VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map(
273
+ Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
164
274
  );
165
275
  function resolveExpressAsStyles(voiceName, candidates = EXPRESS_AS_STYLE_PRESETS) {
166
- if (voiceName === void 0) {
276
+ const normalizedVoiceName = voiceName?.trim().toLowerCase();
277
+ if (!normalizedVoiceName) {
167
278
  return candidates;
168
279
  }
169
- const supportedStyles = EXPRESS_AS_STYLES_BY_NORMALIZED_VOICE.get(voiceName.trim().toLowerCase());
280
+ const supportedStyles = VOICE_STYLE_MAP_BY_NORMALIZED_NAME.get(normalizedVoiceName);
170
281
  if (supportedStyles === void 0) {
171
- return candidates;
282
+ return [];
172
283
  }
173
284
  const supportedStyleSet = new Set(supportedStyles);
174
285
  return candidates.filter((style) => supportedStyleSet.has(style));
@@ -418,6 +529,30 @@ var EXPRESS_AS_STYLE_DESCRIPTIONS = {
418
529
  ja: "\u8208\u596E\u3057\u305F\u30B9\u30BF\u30A4\u30EB",
419
530
  en: "Uses an excited style."
420
531
  },
532
+ empathetic: {
533
+ ja: "\u5171\u611F\u3092\u793A\u3059\u30B9\u30BF\u30A4\u30EB",
534
+ en: "Uses an empathetic style."
535
+ },
536
+ relieved: {
537
+ ja: "\u5B89\u5FC3\u3057\u305F\u30B9\u30BF\u30A4\u30EB",
538
+ en: "Uses a relieved style."
539
+ },
540
+ fearful: {
541
+ ja: "\u6050\u308C\u3092\u611F\u3058\u3055\u305B\u308B\u30B9\u30BF\u30A4\u30EB",
542
+ en: "Uses a fearful style."
543
+ },
544
+ depressed: {
545
+ ja: "\u843D\u3061\u8FBC\u3093\u3060\u30B9\u30BF\u30A4\u30EB",
546
+ en: "Uses a depressed style."
547
+ },
548
+ disgruntled: {
549
+ ja: "\u4E0D\u6E80\u3092\u611F\u3058\u3055\u305B\u308B\u30B9\u30BF\u30A4\u30EB",
550
+ en: "Uses a disgruntled style."
551
+ },
552
+ embarrassed: {
553
+ ja: "\u6065\u305A\u304B\u3057\u305D\u3046\u306A\u30B9\u30BF\u30A4\u30EB",
554
+ en: "Uses an embarrassed style."
555
+ },
421
556
  serious: {
422
557
  ja: "\u771F\u5263\u306A\u30B9\u30BF\u30A4\u30EB",
423
558
  en: "Uses a serious style."
@@ -457,6 +592,26 @@ var EXPRESS_AS_STYLE_DESCRIPTIONS = {
457
592
  whispering: {
458
593
  ja: "\u3055\u3055\u3084\u304F\u3088\u3046\u306A\u30B9\u30BF\u30A4\u30EB",
459
594
  en: "Uses a whispering style."
595
+ },
596
+ "narration-relaxed": {
597
+ ja: "\u30EA\u30E9\u30C3\u30AF\u30B9\u3057\u305F\u30CA\u30EC\u30FC\u30B7\u30E7\u30F3\u5411\u3051\u306E\u30B9\u30BF\u30A4\u30EB",
598
+ en: "Uses a relaxed narration style."
599
+ },
600
+ "poetry-reading": {
601
+ ja: "\u8A69\u306E\u6717\u8AAD\u5411\u3051\u306E\u30B9\u30BF\u30A4\u30EB",
602
+ en: "Uses a poetry-reading style."
603
+ },
604
+ sports_commentary: {
605
+ ja: "\u30B9\u30DD\u30FC\u30C4\u5B9F\u6CC1\u5411\u3051\u306E\u30B9\u30BF\u30A4\u30EB",
606
+ en: "Uses a sports commentary style."
607
+ },
608
+ sports_commentary_excited: {
609
+ ja: "\u8208\u596E\u3057\u305F\u30B9\u30DD\u30FC\u30C4\u5B9F\u6CC1\u5411\u3051\u306E\u30B9\u30BF\u30A4\u30EB",
610
+ en: "Uses an excited sports commentary style."
611
+ },
612
+ story: {
613
+ ja: "\u7269\u8A9E\u306E\u6717\u8AAD\u5411\u3051\u306E\u30B9\u30BF\u30A4\u30EB",
614
+ en: "Uses a storytelling style."
460
615
  }
461
616
  };
462
617
  var SAY_AS_DESCRIPTIONS = {
@@ -568,7 +723,12 @@ var EDITOR_COPY = {
568
723
  selectionCountSuffix: "\u6587\u5B57",
569
724
  previewSelection: "\u9078\u629E\u90E8\u5206\u3092\u8A66\u8074",
570
725
  previewSelectionTitle: "\u9078\u629E\u90E8\u5206\u306ESSML\u3092\u8A66\u8074",
571
- noAvailableOptions: "\u5229\u7528\u53EF\u80FD\u306A\u9078\u629E\u80A2\u304C\u3042\u308A\u307E\u305B\u3093\u3002"
726
+ noAvailableOptions: "\u5229\u7528\u53EF\u80FD\u306A\u9078\u629E\u80A2\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
727
+ styleNotSupported: "\u3053\u306E\u97F3\u58F0\u306F\u30B9\u30BF\u30A4\u30EB\u6307\u5B9A\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093",
728
+ categoryEmotions: "\u611F\u60C5\u30FB\u30C8\u30FC\u30F3",
729
+ categoryScenarios: "\u4F1A\u8A71\u30FB\u30B7\u30CA\u30EA\u30AA",
730
+ categoryMedia: "\u30E1\u30C7\u30A3\u30A2\u30FB\u30CA\u30EC\u30FC\u30B7\u30E7\u30F3",
731
+ categoryOther: "\u305D\u306E\u4ED6"
572
732
  },
573
733
  en: {
574
734
  editorAriaLabel: "SSML editor",
@@ -594,7 +754,12 @@ var EDITOR_COPY = {
594
754
  selectionCountSuffix: " characters",
595
755
  previewSelection: "Preview selection",
596
756
  previewSelectionTitle: "Preview the selected SSML",
597
- noAvailableOptions: "No options are available."
757
+ noAvailableOptions: "No options are available.",
758
+ styleNotSupported: "This voice does not support style selection.",
759
+ categoryEmotions: "Emotions / Tone",
760
+ categoryScenarios: "Conversations / Scenarios",
761
+ categoryMedia: "Media / Broadcast",
762
+ categoryOther: "Other"
598
763
  }
599
764
  };
600
765
  var INLINE_BADGE_COPY = {
@@ -991,6 +1156,10 @@ var editorStyles = {
991
1156
  font: "inherit",
992
1157
  cursor: "pointer"
993
1158
  },
1159
+ toolbarButtonActive: {
1160
+ border: "1px solid var(--ssml-editor-active-border)",
1161
+ backgroundColor: "var(--ssml-editor-active-bg)"
1162
+ },
994
1163
  display: {
995
1164
  display: "grid",
996
1165
  gap: "0.5rem"
@@ -1036,12 +1205,35 @@ var editorStyles = {
1036
1205
  whiteSpace: "nowrap",
1037
1206
  cursor: "pointer"
1038
1207
  },
1208
+ toolbarOptionGroup: {
1209
+ display: "grid",
1210
+ gap: "0.125rem",
1211
+ margin: 0,
1212
+ padding: 0,
1213
+ border: 0
1214
+ },
1215
+ toolbarOptionGroupLabel: {
1216
+ padding: "0.375rem 0.5rem 0.125rem",
1217
+ color: "var(--ssml-editor-color)",
1218
+ fontSize: "0.875rem",
1219
+ fontWeight: 600,
1220
+ whiteSpace: "nowrap"
1221
+ },
1039
1222
  toolbarEmpty: {
1040
1223
  margin: 0,
1041
1224
  padding: "0.375rem 0.5rem",
1042
1225
  color: "var(--ssml-editor-color)",
1043
1226
  whiteSpace: "nowrap"
1044
1227
  },
1228
+ toolbarEmptySelect: {
1229
+ padding: "0.375rem 0.5rem",
1230
+ border: "1px solid var(--ssml-editor-control-border)",
1231
+ borderRadius: "0.125rem",
1232
+ color: "var(--ssml-editor-color)",
1233
+ backgroundColor: "var(--ssml-editor-control-bg)",
1234
+ font: "inherit",
1235
+ textAlign: "left"
1236
+ },
1045
1237
  helpPanel: {
1046
1238
  display: "grid",
1047
1239
  gap: "0.5rem",
@@ -1193,8 +1385,29 @@ function InsertionPopover({
1193
1385
  menuRef,
1194
1386
  onToggle,
1195
1387
  onClose,
1196
- onApply
1388
+ onApply,
1389
+ optionGroups
1197
1390
  }) {
1391
+ const hasOptions = optionGroups ? optionGroups.some((group) => group.options.length > 0) : insertion.options.length > 0;
1392
+ const renderOption = (option) => /* @__PURE__ */ React.createElement(
1393
+ "button",
1394
+ {
1395
+ key: option.value,
1396
+ type: "button",
1397
+ role: "menuitem",
1398
+ style: editorStyles.toolbarOption,
1399
+ title: option.descriptions?.[language] ?? insertion.descriptions[language],
1400
+ disabled: isReadOnly,
1401
+ onMouseDown: (event) => event.preventDefault(),
1402
+ onClick: () => {
1403
+ if (!isReadOnly) {
1404
+ onApply(insertion, option);
1405
+ }
1406
+ onClose();
1407
+ }
1408
+ },
1409
+ option.labels[language]
1410
+ );
1198
1411
  const menu = isOpen && menuPosition && typeof document !== "undefined" ? /* @__PURE__ */ React.createElement(
1199
1412
  "div",
1200
1413
  {
@@ -1206,25 +1419,7 @@ function InsertionPopover({
1206
1419
  role: "menu",
1207
1420
  "aria-label": insertion.labels[language]
1208
1421
  },
1209
- insertion.options.length === 0 ? /* @__PURE__ */ React.createElement("p", { role: "status", style: editorStyles.toolbarEmpty }, emptyOptionsMessage) : insertion.options.map((option) => /* @__PURE__ */ React.createElement(
1210
- "button",
1211
- {
1212
- key: option.value,
1213
- type: "button",
1214
- role: "menuitem",
1215
- style: editorStyles.toolbarOption,
1216
- title: option.descriptions?.[language] ?? insertion.descriptions[language],
1217
- disabled: isReadOnly,
1218
- onMouseDown: (event) => event.preventDefault(),
1219
- onClick: () => {
1220
- if (!isReadOnly) {
1221
- onApply(insertion, option);
1222
- }
1223
- onClose();
1224
- }
1225
- },
1226
- option.labels[language]
1227
- ))
1422
+ !hasOptions ? /* @__PURE__ */ React.createElement("div", { role: "presentation" }, /* @__PURE__ */ React.createElement("select", { disabled: true, value: "", style: editorStyles.toolbarEmptySelect }, /* @__PURE__ */ React.createElement("option", { value: "", disabled: true }, emptyOptionsMessage))) : optionGroups ? optionGroups.map((group) => /* @__PURE__ */ React.createElement("fieldset", { key: group.label, style: editorStyles.toolbarOptionGroup }, /* @__PURE__ */ React.createElement("legend", { style: editorStyles.toolbarOptionGroupLabel }, group.label), group.options.map(renderOption))) : insertion.options.map(renderOption)
1228
1423
  ) : null;
1229
1424
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: editorStyles.toolbarDropdown }, /* @__PURE__ */ React.createElement(
1230
1425
  "button",
@@ -1249,6 +1444,8 @@ function InsertionPopovers({
1249
1444
  insertions,
1250
1445
  openPopoverId,
1251
1446
  onToggle,
1447
+ emptyOptionsMessages,
1448
+ optionGroups,
1252
1449
  ...props
1253
1450
  }) {
1254
1451
  return /* @__PURE__ */ React.createElement(React.Fragment, null, insertions.map((insertion) => /* @__PURE__ */ React.createElement(
@@ -1257,6 +1454,8 @@ function InsertionPopovers({
1257
1454
  key: insertion.id,
1258
1455
  ...props,
1259
1456
  insertion,
1457
+ emptyOptionsMessage: emptyOptionsMessages?.[insertion.id] ?? props.emptyOptionsMessage,
1458
+ optionGroups: optionGroups?.[insertion.id],
1260
1459
  isOpen: openPopoverId === insertion.id,
1261
1460
  onToggle: (trigger) => onToggle(insertion.id, trigger)
1262
1461
  }
@@ -1264,23 +1463,57 @@ function InsertionPopovers({
1264
1463
  }
1265
1464
 
1266
1465
  // packages/ssml-editor-react/src/components/popovers/ProsodyPopovers.tsx
1267
- function ProsodyPopovers({ insertions, voiceName, ...props }) {
1466
+ var CATEGORY_LABEL_KEYS = {
1467
+ emotions: "categoryEmotions",
1468
+ scenarios: "categoryScenarios",
1469
+ media: "categoryMedia",
1470
+ other: "categoryOther"
1471
+ };
1472
+ var CATEGORY_ORDER = ["emotions", "scenarios", "media", "other"];
1473
+ function isExpressAsInsertion(insertion) {
1474
+ return insertion.id === "emotion" && insertion.tagName?.toLowerCase() === "mstts:express-as";
1475
+ }
1476
+ function createOptionGroups(options, language) {
1477
+ const t = (key) => EDITOR_COPY[language][key];
1478
+ return CATEGORY_ORDER.map((category) => ({
1479
+ label: t(CATEGORY_LABEL_KEYS[category]),
1480
+ options: options.filter((option) => getExpressAsStyleCategory(option.value) === category)
1481
+ })).filter((group) => group.options.length > 0);
1482
+ }
1483
+ function ProsodyPopovers({ insertions, voiceName, language, ...props }) {
1484
+ const emptyOptionsMessages = {};
1268
1485
  const filteredInsertions = insertions.map((insertion) => {
1269
- if (insertion.id !== "emotion" || insertion.tagName?.toLowerCase() !== "mstts:express-as") {
1486
+ if (!isExpressAsInsertion(insertion)) {
1270
1487
  return insertion;
1271
1488
  }
1272
- const styles = new Set(
1489
+ const availableStyles = new Set(
1273
1490
  resolveExpressAsStyles(
1274
1491
  voiceName,
1275
1492
  insertion.options.map((option) => option.value)
1276
1493
  )
1277
1494
  );
1495
+ if (availableStyles.size === 0) {
1496
+ emptyOptionsMessages[insertion.id] = EDITOR_COPY[language].styleNotSupported;
1497
+ }
1498
+ const availableOptions = insertion.options.filter((option) => availableStyles.has(option.value));
1278
1499
  return {
1279
1500
  ...insertion,
1280
- options: insertion.options.filter((option) => styles.has(option.value))
1501
+ options: availableOptions
1281
1502
  };
1282
1503
  });
1283
- return /* @__PURE__ */ React.createElement(InsertionPopovers, { ...props, insertions: filteredInsertions });
1504
+ const optionGroups = Object.fromEntries(
1505
+ filteredInsertions.filter(isExpressAsInsertion).map((insertion) => [insertion.id, createOptionGroups(insertion.options, language)])
1506
+ );
1507
+ return /* @__PURE__ */ React.createElement(
1508
+ InsertionPopovers,
1509
+ {
1510
+ ...props,
1511
+ emptyOptionsMessages,
1512
+ language,
1513
+ insertions: filteredInsertions,
1514
+ optionGroups
1515
+ }
1516
+ );
1284
1517
  }
1285
1518
 
1286
1519
  // packages/ssml-editor-react/src/components/popovers/TextPopovers.tsx
@@ -2648,148 +2881,7 @@ function formatXmlFragment(xml) {
2648
2881
  return preserveTrailingLineBreak(unwrapFormattedFragment(formatted) ?? xml, trailingLineBreaks);
2649
2882
  }
2650
2883
 
2651
- // packages/ssml-editor-react/src/ssmlInsertion.ts
2652
- function isLineStart(value, offset) {
2653
- return offset === 0 || value[offset - 1] === "\n" || value[offset - 1] === "\r";
2654
- }
2655
- function isLineEnd(value, offset) {
2656
- return offset === value.length || value[offset] === "\n" || value[offset] === "\r";
2657
- }
2658
- function getLineBreakAt(value, offset) {
2659
- if (value.startsWith("\r\n", offset)) {
2660
- return "\r\n";
2661
- }
2662
- if (value[offset] === "\n" || value[offset] === "\r") {
2663
- return value[offset];
2664
- }
2665
- return "";
2666
- }
2667
- function startsWithLineBreak(value) {
2668
- return value.startsWith("\n") || value.startsWith("\r");
2669
- }
2670
- function endsWithLineBreak(value) {
2671
- return value.endsWith("\n") || value.endsWith("\r");
2672
- }
2673
- function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol = "\n", selectedText = startOffset === endOffset ? "" : source.slice(startOffset, endOffset)) {
2674
- if (template.mode === SSML_INSERTION_MODES.wrap) {
2675
- const trailingLineBreak2 = getLineBreakAt(source, endOffset) === "" ? eol : "";
2676
- return {
2677
- replacement: `${template.prefix}${selectedText}${template.suffix}${trailingLineBreak2}`,
2678
- selectionOffset: template.prefix.length
2679
- };
2680
- }
2681
- const followingLineBreak = getLineBreakAt(source, endOffset);
2682
- const leadingLineBreak = !isLineStart(source, startOffset) && !startsWithLineBreak(template.prefix) ? eol : "";
2683
- const needsTrailingLineBreak = selectedText.length > 0 ? !selectedText.startsWith("\n") && !selectedText.startsWith("\r") : !isLineEnd(source, endOffset) || followingLineBreak === "";
2684
- const trailingLineBreak = needsTrailingLineBreak && !endsWithLineBreak(template.prefix) ? eol : "";
2685
- const insertionPrefix = `${leadingLineBreak}${template.prefix}`;
2686
- return {
2687
- replacement: `${insertionPrefix}${trailingLineBreak}${selectedText}`,
2688
- selectionOffset: insertionPrefix.length + trailingLineBreak.length + (selectedText.length === 0 && trailingLineBreak === "" ? followingLineBreak.length : 0)
2689
- };
2690
- }
2691
-
2692
- // packages/ssml-editor-react/src/ssmlContext.ts
2693
- function findTagEnd2(source, start, limit) {
2694
- let quote;
2695
- for (let index = start + 1; index < limit; index += 1) {
2696
- const character = source[index];
2697
- if (quote !== void 0) {
2698
- if (character === quote) {
2699
- quote = void 0;
2700
- }
2701
- continue;
2702
- }
2703
- if (character === '"' || character === "'") {
2704
- quote = character;
2705
- continue;
2706
- }
2707
- if (character === ">") {
2708
- return index;
2709
- }
2710
- }
2711
- return -1;
2712
- }
2713
- function getVoiceName(tag) {
2714
- return tag.match(/\bname\s*=\s*(["'])([\s\S]*?)\1/i)?.[2];
2715
- }
2716
- function closeElement(stack, name) {
2717
- for (let index = stack.length - 1; index >= 0; index -= 1) {
2718
- if (stack[index]?.name === name) {
2719
- stack.splice(index);
2720
- return;
2721
- }
2722
- }
2723
- }
2724
- function findSsmlVoiceContext(source, offset) {
2725
- const limit = Math.max(0, Math.min(offset, source.length));
2726
- const stack = [];
2727
- let index = 0;
2728
- while (index < limit) {
2729
- const tagStart = source.indexOf("<", index);
2730
- if (tagStart === -1 || tagStart >= limit) {
2731
- break;
2732
- }
2733
- if (source.startsWith("<!--", tagStart)) {
2734
- const end = source.indexOf("-->", tagStart + 4);
2735
- index = end === -1 || end + 3 > limit ? limit : end + 3;
2736
- continue;
2737
- }
2738
- if (source.startsWith("<![CDATA[", tagStart)) {
2739
- const end = source.indexOf("]]>", tagStart + 9);
2740
- index = end === -1 || end + 3 > limit ? limit : end + 3;
2741
- continue;
2742
- }
2743
- if (source.startsWith("<?", tagStart)) {
2744
- const end = source.indexOf("?>", tagStart + 2);
2745
- index = end === -1 || end + 2 > limit ? limit : end + 2;
2746
- continue;
2747
- }
2748
- const tagEnd = findTagEnd2(source, tagStart, limit);
2749
- if (tagEnd === -1) {
2750
- break;
2751
- }
2752
- const tag = source.slice(tagStart, tagEnd + 1);
2753
- if (tag.startsWith("<!")) {
2754
- index = tagEnd + 1;
2755
- continue;
2756
- }
2757
- const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
2758
- if (closingMatch?.[1]) {
2759
- closeElement(stack, closingMatch[1].toLowerCase());
2760
- index = tagEnd + 1;
2761
- continue;
2762
- }
2763
- const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
2764
- if (openingMatch?.[1] && !/\/\s*>$/.test(tag)) {
2765
- const name = openingMatch[1].toLowerCase();
2766
- stack.push({
2767
- name,
2768
- ...name === "voice" ? { voiceName: getVoiceName(tag) } : {}
2769
- });
2770
- }
2771
- index = tagEnd + 1;
2772
- }
2773
- for (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex -= 1) {
2774
- const element = stack[stackIndex];
2775
- if (element?.name === "voice") {
2776
- return element.voiceName === void 0 ? {} : { voiceName: element.voiceName };
2777
- }
2778
- }
2779
- return void 0;
2780
- }
2781
-
2782
- // packages/ssml-editor-react/src/hooks/useSsmlEditorState.ts
2783
- var TIMING_INSERTION_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence"]);
2784
- var PROSODY_INSERTION_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
2785
- var TEXT_INSERTION_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
2786
- var EMPTY_SELECTION_OVERLAY = {
2787
- selectedText: "",
2788
- characterCount: 0,
2789
- hasSelection: false,
2790
- position: null,
2791
- placement: "above"
2792
- };
2884
+ // packages/ssml-editor-react/src/editableSsml.ts
2793
2885
  function isSsmlElement(node) {
2794
2886
  return typeof node !== "string" && node.type !== "text";
2795
2887
  }
@@ -2884,48 +2976,6 @@ function preserveEmptyPairElements(nodes, startTags, startTagIndex) {
2884
2976
  function getDocumentChildren2(document2) {
2885
2977
  return document2.children ?? (document2.content === void 0 ? [] : [document2.content]);
2886
2978
  }
2887
- function getPartialContext(document2) {
2888
- const children = getDocumentChildren2(document2);
2889
- const voice = findFirstElement(children, isVoice);
2890
- const prosody = findFirstElement(children, isProsody);
2891
- const context = {
2892
- version: document2.version,
2893
- lang: document2.lang
2894
- };
2895
- if (voice) {
2896
- context.voice = {
2897
- name: voice.name,
2898
- effect: voice.effect,
2899
- attributes: voice.attributes
2900
- };
2901
- }
2902
- if (prosody) {
2903
- context.prosody = {
2904
- rate: prosody.rate,
2905
- pitch: prosody.pitch,
2906
- volume: prosody.volume,
2907
- contour: prosody.contour,
2908
- range: prosody.range,
2909
- attributes: prosody.attributes
2910
- };
2911
- }
2912
- return context;
2913
- }
2914
- function findFirstElement(nodes, predicate) {
2915
- for (const node of nodes) {
2916
- if (!isSsmlElement(node)) {
2917
- continue;
2918
- }
2919
- if (predicate(node)) {
2920
- return node;
2921
- }
2922
- const child = findFirstElement(node.children ?? [], predicate);
2923
- if (child) {
2924
- return child;
2925
- }
2926
- }
2927
- return void 0;
2928
- }
2929
2979
  function findFirstElementPath(nodes, predicate, ancestors = []) {
2930
2980
  for (const node of nodes) {
2931
2981
  if (!isSsmlElement(node)) {
@@ -3006,13 +3056,10 @@ function getEditableRegion(document2) {
3006
3056
  ...voice ? { voiceName: voice.name } : {}
3007
3057
  };
3008
3058
  }
3009
- function getEditableChildren(document2) {
3010
- return getEditableRegion(document2).children;
3011
- }
3012
3059
  function getEditableText(document2) {
3013
- return serializeEditableText(getEditableChildren(document2), document2.lang);
3060
+ return serializeEditableText(getEditableRegion(document2).children, document2.lang);
3014
3061
  }
3015
- function updateText(document2, value) {
3062
+ function updateEditableText(document2, value) {
3016
3063
  const nextChildren = parseEditableText(value, document2.lang);
3017
3064
  const editableChildren = nextChildren.length > 0 ? nextChildren : [value];
3018
3065
  const children = getDocumentChildren2(document2);
@@ -3032,6 +3079,241 @@ function updateText(document2, value) {
3032
3079
  }
3033
3080
  return withChildren(document2, editableChildren);
3034
3081
  }
3082
+
3083
+ // packages/ssml-editor-react/src/ssmlInsertion.ts
3084
+ function isLineStart(value, offset) {
3085
+ return offset === 0 || value[offset - 1] === "\n" || value[offset - 1] === "\r";
3086
+ }
3087
+ function isLineEnd(value, offset) {
3088
+ return offset === value.length || value[offset] === "\n" || value[offset] === "\r";
3089
+ }
3090
+ function getLineBreakAt(value, offset) {
3091
+ if (value.startsWith("\r\n", offset)) {
3092
+ return "\r\n";
3093
+ }
3094
+ if (value[offset] === "\n" || value[offset] === "\r") {
3095
+ return value[offset];
3096
+ }
3097
+ return "";
3098
+ }
3099
+ function startsWithLineBreak(value) {
3100
+ return value.startsWith("\n") || value.startsWith("\r");
3101
+ }
3102
+ function endsWithLineBreak(value) {
3103
+ return value.endsWith("\n") || value.endsWith("\r");
3104
+ }
3105
+ function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol = "\n", selectedText = startOffset === endOffset ? "" : source.slice(startOffset, endOffset)) {
3106
+ if (template.mode === SSML_INSERTION_MODES.wrap) {
3107
+ const trailingLineBreak2 = getLineBreakAt(source, endOffset) === "" ? eol : "";
3108
+ return {
3109
+ replacement: `${template.prefix}${selectedText}${template.suffix}${trailingLineBreak2}`,
3110
+ selectionOffset: template.prefix.length
3111
+ };
3112
+ }
3113
+ const followingLineBreak = getLineBreakAt(source, endOffset);
3114
+ const leadingLineBreak = !isLineStart(source, startOffset) && !startsWithLineBreak(template.prefix) ? eol : "";
3115
+ const needsTrailingLineBreak = selectedText.length > 0 ? !selectedText.startsWith("\n") && !selectedText.startsWith("\r") : !isLineEnd(source, endOffset) || followingLineBreak === "";
3116
+ const trailingLineBreak = needsTrailingLineBreak && !endsWithLineBreak(template.prefix) ? eol : "";
3117
+ const insertionPrefix = `${leadingLineBreak}${template.prefix}`;
3118
+ return {
3119
+ replacement: `${insertionPrefix}${trailingLineBreak}${selectedText}`,
3120
+ selectionOffset: insertionPrefix.length + trailingLineBreak.length + (selectedText.length === 0 && trailingLineBreak === "" ? followingLineBreak.length : 0)
3121
+ };
3122
+ }
3123
+
3124
+ // packages/ssml-editor-react/src/ssmlContext.ts
3125
+ function findTagEnd2(source, start, limit) {
3126
+ let quote;
3127
+ for (let index = start + 1; index < limit; index += 1) {
3128
+ const character = source[index];
3129
+ if (quote !== void 0) {
3130
+ if (character === quote) {
3131
+ quote = void 0;
3132
+ }
3133
+ continue;
3134
+ }
3135
+ if (character === '"' || character === "'") {
3136
+ quote = character;
3137
+ continue;
3138
+ }
3139
+ if (character === ">") {
3140
+ return index;
3141
+ }
3142
+ }
3143
+ return -1;
3144
+ }
3145
+ function getVoiceName(tag) {
3146
+ return tag.match(/\bname\s*=\s*(["'])([\s\S]*?)\1/i)?.[2];
3147
+ }
3148
+ function closeElement(stack, name) {
3149
+ for (let index = stack.length - 1; index >= 0; index -= 1) {
3150
+ if (stack[index]?.name === name) {
3151
+ stack.splice(index);
3152
+ return;
3153
+ }
3154
+ }
3155
+ }
3156
+ function findActiveSsmlTags(source, offset) {
3157
+ const limit = Math.max(0, Math.min(offset, source.length));
3158
+ const stack = [];
3159
+ let index = 0;
3160
+ while (index < source.length) {
3161
+ const tagStart = source.indexOf("<", index);
3162
+ if (tagStart === -1 || tagStart > limit) {
3163
+ break;
3164
+ }
3165
+ const nonContentEnd = source.startsWith("<!--", tagStart) ? source.indexOf("-->", tagStart + 4) : source.startsWith("<![CDATA[", tagStart) ? source.indexOf("]]>", tagStart + 9) : source.startsWith("<?", tagStart) ? source.indexOf("?>", tagStart + 2) : void 0;
3166
+ if (nonContentEnd !== void 0) {
3167
+ const delimiterLength = source.startsWith("<?", tagStart) ? 2 : 3;
3168
+ const end = nonContentEnd === -1 ? source.length : nonContentEnd + delimiterLength;
3169
+ if (limit < end) {
3170
+ break;
3171
+ }
3172
+ index = end;
3173
+ continue;
3174
+ }
3175
+ const tagEnd = findTagEnd2(source, tagStart, source.length);
3176
+ const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
3177
+ const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3178
+ const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3179
+ if (tagEnd === -1 || limit <= tagEnd) {
3180
+ if (openingMatch?.[1]) {
3181
+ stack.push({ name: openingMatch[1].toLowerCase() });
3182
+ }
3183
+ break;
3184
+ }
3185
+ if (closingMatch?.[1]) {
3186
+ closeElement(stack, closingMatch[1].toLowerCase());
3187
+ } else if (openingMatch?.[1] && !/\/\s*>$/.test(tag)) {
3188
+ stack.push({ name: openingMatch[1].toLowerCase() });
3189
+ }
3190
+ index = tagEnd + 1;
3191
+ }
3192
+ return new Set(stack.map(({ name }) => name));
3193
+ }
3194
+ function findSsmlVoiceContext(source, offset) {
3195
+ const limit = Math.max(0, Math.min(offset, source.length));
3196
+ const stack = [];
3197
+ let index = 0;
3198
+ while (index < limit) {
3199
+ const tagStart = source.indexOf("<", index);
3200
+ if (tagStart === -1 || tagStart >= limit) {
3201
+ break;
3202
+ }
3203
+ if (source.startsWith("<!--", tagStart)) {
3204
+ const end = source.indexOf("-->", tagStart + 4);
3205
+ index = end === -1 || end + 3 > limit ? limit : end + 3;
3206
+ continue;
3207
+ }
3208
+ if (source.startsWith("<![CDATA[", tagStart)) {
3209
+ const end = source.indexOf("]]>", tagStart + 9);
3210
+ index = end === -1 || end + 3 > limit ? limit : end + 3;
3211
+ continue;
3212
+ }
3213
+ if (source.startsWith("<?", tagStart)) {
3214
+ const end = source.indexOf("?>", tagStart + 2);
3215
+ index = end === -1 || end + 2 > limit ? limit : end + 2;
3216
+ continue;
3217
+ }
3218
+ const tagEnd = findTagEnd2(source, tagStart, limit);
3219
+ if (tagEnd === -1) {
3220
+ break;
3221
+ }
3222
+ const tag = source.slice(tagStart, tagEnd + 1);
3223
+ if (tag.startsWith("<!")) {
3224
+ index = tagEnd + 1;
3225
+ continue;
3226
+ }
3227
+ const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3228
+ if (closingMatch?.[1]) {
3229
+ closeElement(stack, closingMatch[1].toLowerCase());
3230
+ index = tagEnd + 1;
3231
+ continue;
3232
+ }
3233
+ const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3234
+ if (openingMatch?.[1] && !/\/\s*>$/.test(tag)) {
3235
+ const name = openingMatch[1].toLowerCase();
3236
+ stack.push({
3237
+ name,
3238
+ ...name === "voice" ? { voiceName: getVoiceName(tag) } : {}
3239
+ });
3240
+ }
3241
+ index = tagEnd + 1;
3242
+ }
3243
+ for (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex -= 1) {
3244
+ const element = stack[stackIndex];
3245
+ if (element?.name === "voice") {
3246
+ return element.voiceName === void 0 ? {} : { voiceName: element.voiceName };
3247
+ }
3248
+ }
3249
+ return void 0;
3250
+ }
3251
+
3252
+ // packages/ssml-editor-react/src/hooks/useSsmlEditorState.ts
3253
+ var TIMING_INSERTION_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence"]);
3254
+ var PROSODY_INSERTION_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
3255
+ var TEXT_INSERTION_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
3256
+ var EMPTY_SELECTION_OVERLAY = {
3257
+ selectedText: "",
3258
+ characterCount: 0,
3259
+ hasSelection: false,
3260
+ position: null,
3261
+ placement: "above"
3262
+ };
3263
+ function isSsmlElement2(node) {
3264
+ return typeof node !== "string" && node.type !== "text";
3265
+ }
3266
+ function isVoice2(element) {
3267
+ return element.type === "voice";
3268
+ }
3269
+ function isProsody2(element) {
3270
+ return element.type === "prosody";
3271
+ }
3272
+ function getDocumentChildren3(document2) {
3273
+ return document2.children ?? (document2.content === void 0 ? [] : [document2.content]);
3274
+ }
3275
+ function getPartialContext(document2) {
3276
+ const children = getDocumentChildren3(document2);
3277
+ const voice = findFirstElement(children, isVoice2);
3278
+ const prosody = findFirstElement(children, isProsody2);
3279
+ const context = {
3280
+ version: document2.version,
3281
+ lang: document2.lang
3282
+ };
3283
+ if (voice) {
3284
+ context.voice = {
3285
+ name: voice.name,
3286
+ effect: voice.effect,
3287
+ attributes: voice.attributes
3288
+ };
3289
+ }
3290
+ if (prosody) {
3291
+ context.prosody = {
3292
+ rate: prosody.rate,
3293
+ pitch: prosody.pitch,
3294
+ volume: prosody.volume,
3295
+ contour: prosody.contour,
3296
+ range: prosody.range,
3297
+ attributes: prosody.attributes
3298
+ };
3299
+ }
3300
+ return context;
3301
+ }
3302
+ function findFirstElement(nodes, predicate) {
3303
+ for (const node of nodes) {
3304
+ if (!isSsmlElement2(node)) {
3305
+ continue;
3306
+ }
3307
+ if (predicate(node)) {
3308
+ return node;
3309
+ }
3310
+ const child = findFirstElement(node.children ?? [], predicate);
3311
+ if (child) {
3312
+ return child;
3313
+ }
3314
+ }
3315
+ return void 0;
3316
+ }
3035
3317
  function getSelectionInfo(editor) {
3036
3318
  const model = editor.getModel();
3037
3319
  const selection = editor.getSelection();
@@ -3167,6 +3449,7 @@ function useSsmlEditorState({
3167
3449
  const [decorationsVisible, setDecorationsVisible] = (0, import_react.useState)(showDecorations);
3168
3450
  const [isHelpOpen, setIsHelpOpen] = (0, import_react.useState)(false);
3169
3451
  const [syntaxError, setSyntaxError] = (0, import_react.useState)(null);
3452
+ const [activeTags, setActiveTags] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
3170
3453
  const [openPopoverId, setOpenPopoverId] = (0, import_react.useState)(null);
3171
3454
  const [popoverVoiceName, setPopoverVoiceName] = (0, import_react.useState)(void 0);
3172
3455
  const [popoverPosition, setPopoverPosition] = (0, import_react.useState)(null);
@@ -3269,6 +3552,14 @@ function useSsmlEditorState({
3269
3552
  });
3270
3553
  }
3271
3554
  }, []);
3555
+ const updateActiveTags = (0, import_react.useCallback)((nextTags) => {
3556
+ setActiveTags((currentTags) => {
3557
+ if (currentTags.size === nextTags.size && [...currentTags].every((tag) => nextTags.has(tag))) {
3558
+ return currentTags;
3559
+ }
3560
+ return new Set(nextTags);
3561
+ });
3562
+ }, []);
3272
3563
  const commit = (0, import_react.useCallback)(
3273
3564
  (nextDocument) => {
3274
3565
  draftDocumentRef.current = nextDocument;
@@ -3280,7 +3571,7 @@ function useSsmlEditorState({
3280
3571
  );
3281
3572
  const handleTextChange = (0, import_react.useCallback)(
3282
3573
  (value) => {
3283
- commit(updateText(draftDocumentRef.current, value));
3574
+ commit(updateEditableText(draftDocumentRef.current, value));
3284
3575
  if (editorRef.current) {
3285
3576
  refreshSelectionOverlay(editorRef.current, false);
3286
3577
  }
@@ -3324,7 +3615,7 @@ function useSsmlEditorState({
3324
3615
  }, [commit]);
3325
3616
  const handleFormat = (0, import_react.useCallback)(() => {
3326
3617
  const value = editorRef.current?.getValue() ?? getEditableText(draftDocumentRef.current);
3327
- commit(updateText(draftDocumentRef.current, formatXmlFragment(value)));
3618
+ commit(updateEditableText(draftDocumentRef.current, formatXmlFragment(value)));
3328
3619
  }, [commit]);
3329
3620
  const handleUndo = (0, import_react.useCallback)(() => {
3330
3621
  editorRef.current?.trigger("toolbar", "undo", null);
@@ -3354,7 +3645,7 @@ function useSsmlEditorState({
3354
3645
  );
3355
3646
  const getFullSsml = (0, import_react.useCallback)(
3356
3647
  () => buildSsml(
3357
- editorRef.current ? updateText(draftDocumentRef.current, editorRef.current.getValue()) : draftDocumentRef.current
3648
+ editorRef.current ? updateEditableText(draftDocumentRef.current, editorRef.current.getValue()) : draftDocumentRef.current
3358
3649
  ),
3359
3650
  []
3360
3651
  );
@@ -3364,6 +3655,7 @@ function useSsmlEditorState({
3364
3655
  draftDocument,
3365
3656
  text: getEditableText(draftDocument),
3366
3657
  selectionOverlay,
3658
+ activeTags,
3367
3659
  isDarkTheme: resolvedTheme === "dark" || resolvedTheme === "system" && isDark,
3368
3660
  decorationsVisible,
3369
3661
  setDecorationsVisible,
@@ -3385,6 +3677,7 @@ function useSsmlEditorState({
3385
3677
  previewSelection,
3386
3678
  canPreviewSelection: onPreviewSelection !== void 0,
3387
3679
  refreshSelectionOverlay,
3680
+ updateActiveTags,
3388
3681
  getSelectedSsml: getSelectedSsmlValue,
3389
3682
  getCurrentLineSsml: getCurrentLineSsmlValue,
3390
3683
  getFullSsml,
@@ -3618,7 +3911,8 @@ function registerSsmlCompletionProvider(monaco, options = {}) {
3618
3911
  ...attributeValues?.map((value2) => ({
3619
3912
  label: value2,
3620
3913
  kind: monaco.languages.CompletionItemKind.Value,
3621
- insertText: value2
3914
+ insertText: value2,
3915
+ range
3622
3916
  })) ?? []
3623
3917
  ]
3624
3918
  };
@@ -4434,6 +4728,7 @@ function useSsmlMonaco({
4434
4728
  decorationsVisible,
4435
4729
  getOuterVoiceName,
4436
4730
  onSelectionOverlayChange,
4731
+ onActiveTagsChange,
4437
4732
  onSyntaxErrorChange
4438
4733
  }) {
4439
4734
  const internalEditorRef = (0, import_react2.useRef)(null);
@@ -4443,6 +4738,7 @@ function useSsmlMonaco({
4443
4738
  const codeActionProviderRef = (0, import_react2.useRef)(null);
4444
4739
  const releaseHoverProviderRef = (0, import_react2.useRef)(null);
4445
4740
  const selectionChangeRef = (0, import_react2.useRef)(null);
4741
+ const cursorPositionChangeRef = (0, import_react2.useRef)(null);
4446
4742
  const diagnosticsChangeRef = (0, import_react2.useRef)(null);
4447
4743
  const diagnosticsTimeoutRef = (0, import_react2.useRef)(null);
4448
4744
  const selectionLayoutDisposablesRef = (0, import_react2.useRef)([]);
@@ -4451,11 +4747,13 @@ function useSsmlMonaco({
4451
4747
  const decorationsVisibleRef = (0, import_react2.useRef)(decorationsVisible);
4452
4748
  const getOuterVoiceNameRef = (0, import_react2.useRef)(getOuterVoiceName);
4453
4749
  const onSelectionOverlayChangeRef = (0, import_react2.useRef)(onSelectionOverlayChange);
4750
+ const onActiveTagsChangeRef = (0, import_react2.useRef)(onActiveTagsChange);
4454
4751
  const onSyntaxErrorChangeRef = (0, import_react2.useRef)(onSyntaxErrorChange);
4455
4752
  languageRef.current = language;
4456
4753
  decorationsVisibleRef.current = decorationsVisible;
4457
4754
  getOuterVoiceNameRef.current = getOuterVoiceName;
4458
4755
  onSelectionOverlayChangeRef.current = onSelectionOverlayChange;
4756
+ onActiveTagsChangeRef.current = onActiveTagsChange;
4459
4757
  onSyntaxErrorChangeRef.current = onSyntaxErrorChange;
4460
4758
  const runSsmlDiagnostics = (0, import_react2.useCallback)((editor, monaco) => {
4461
4759
  const model = editor.getModel();
@@ -4485,6 +4783,15 @@ function useSsmlMonaco({
4485
4783
  diagnosticsChangeRef.current?.dispose();
4486
4784
  diagnosticsChangeRef.current = null;
4487
4785
  }, []);
4786
+ const syncActiveTags = (0, import_react2.useCallback)((editor) => {
4787
+ const model = editor.getModel();
4788
+ const position = editor.getPosition();
4789
+ if (!model || !position) {
4790
+ onActiveTagsChangeRef.current(/* @__PURE__ */ new Set());
4791
+ return;
4792
+ }
4793
+ onActiveTagsChangeRef.current(findActiveSsmlTags(model.getValue(), model.getOffsetAt(position)));
4794
+ }, []);
4488
4795
  const disposeMonacoResources = (0, import_react2.useCallback)(() => {
4489
4796
  clearSsmlDiagnosticsResources();
4490
4797
  const model = editorRef.current?.getModel();
@@ -4494,6 +4801,8 @@ function useSsmlMonaco({
4494
4801
  }
4495
4802
  selectionChangeRef.current?.dispose();
4496
4803
  selectionChangeRef.current = null;
4804
+ cursorPositionChangeRef.current?.dispose();
4805
+ cursorPositionChangeRef.current = null;
4497
4806
  completionProviderRef.current?.dispose();
4498
4807
  completionProviderRef.current = null;
4499
4808
  codeActionProviderRef.current?.dispose();
@@ -4515,6 +4824,9 @@ function useSsmlMonaco({
4515
4824
  selectionChangeRef.current = editor.onDidChangeCursorSelection(() => {
4516
4825
  onSelectionOverlayChangeRef.current(editor, true);
4517
4826
  });
4827
+ cursorPositionChangeRef.current = editor.onDidChangeCursorPosition(() => {
4828
+ syncActiveTags(editor);
4829
+ });
4518
4830
  diagnosticsChangeRef.current = editor.onDidChangeModelContent(() => {
4519
4831
  const model2 = editor.getModel();
4520
4832
  if (model2) {
@@ -4526,6 +4838,7 @@ function useSsmlMonaco({
4526
4838
  inlineDecorationIdsRef.current
4527
4839
  );
4528
4840
  }
4841
+ syncActiveTags(editor);
4529
4842
  scheduleSsmlDiagnostics(editor, monaco);
4530
4843
  });
4531
4844
  selectionLayoutDisposablesRef.current = [
@@ -4551,8 +4864,9 @@ function useSsmlMonaco({
4551
4864
  );
4552
4865
  }
4553
4866
  onSelectionOverlayChangeRef.current(editor, true);
4867
+ syncActiveTags(editor);
4554
4868
  },
4555
- [disposeMonacoResources, editorRef, runSsmlDiagnostics, scheduleSsmlDiagnostics]
4869
+ [disposeMonacoResources, editorRef, runSsmlDiagnostics, scheduleSsmlDiagnostics, syncActiveTags]
4556
4870
  );
4557
4871
  (0, import_react2.useEffect)(() => {
4558
4872
  const monaco = monacoRef.current;
@@ -4940,6 +5254,8 @@ var STYLE_CSS = `
4940
5254
  --ssml-editor-border: #d1d5db;
4941
5255
  --ssml-editor-control-bg: #f9fafb;
4942
5256
  --ssml-editor-control-border: #9ca3af;
5257
+ --ssml-editor-active-bg: #dbeafe;
5258
+ --ssml-editor-active-border: #2563eb;
4943
5259
  --ssml-editor-preview-bg: #f3f4f6;
4944
5260
  --ssml-editor-error: #b91c1c;
4945
5261
  --ssml-editor-error-bg: #fef2f2;
@@ -4957,6 +5273,8 @@ var STYLE_CSS = `
4957
5273
  --ssml-editor-border: #374151;
4958
5274
  --ssml-editor-control-bg: #111827;
4959
5275
  --ssml-editor-control-border: #4b5563;
5276
+ --ssml-editor-active-bg: #1e3a8a;
5277
+ --ssml-editor-active-border: #60a5fa;
4960
5278
  --ssml-editor-preview-bg: #111827;
4961
5279
  --ssml-editor-error: #fca5a5;
4962
5280
  --ssml-editor-error-bg: #450a0a;
@@ -5176,6 +5494,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5176
5494
  editorRef,
5177
5495
  text,
5178
5496
  selectionOverlay,
5497
+ activeTags,
5179
5498
  isDarkTheme,
5180
5499
  decorationsVisible,
5181
5500
  setDecorationsVisible,
@@ -5196,6 +5515,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5196
5515
  previewSelection,
5197
5516
  canPreviewSelection,
5198
5517
  refreshSelectionOverlay,
5518
+ updateActiveTags,
5199
5519
  getSelectedSsml: getSelectedSsml2,
5200
5520
  getCurrentLineSsml: getCurrentLineSsml2,
5201
5521
  getFullSsml,
@@ -5224,6 +5544,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5224
5544
  decorationsVisible,
5225
5545
  getOuterVoiceName,
5226
5546
  onSelectionOverlayChange: refreshSelectionOverlay,
5547
+ onActiveTagsChange: updateActiveTags,
5227
5548
  onSyntaxErrorChange: setSyntaxError
5228
5549
  });
5229
5550
  (0, import_react3.useImperativeHandle)(
@@ -5239,12 +5560,13 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5239
5560
  const isTimingPopover = insertion.tagName !== void 0 && TIMING_POPOVER_TAGS.has(insertion.tagName);
5240
5561
  const isProsodyPopover = insertion.tagName !== void 0 && PROSODY_POPOVER_TAGS.has(insertion.tagName);
5241
5562
  const isTextPopover = insertion.tagName !== void 0 && TEXT_POPOVER_TAGS.has(insertion.tagName);
5563
+ const insertionButtonStyle = insertion.tagName && activeTags.has(insertion.tagName.toLowerCase()) ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle;
5242
5564
  const props = {
5243
5565
  language,
5244
5566
  isDarkTheme,
5245
5567
  showToolbarIcons,
5246
5568
  showToolbarText,
5247
- toolbarButtonStyle,
5569
+ toolbarButtonStyle: insertionButtonStyle,
5248
5570
  emptyOptionsMessage: copy.noAvailableOptions,
5249
5571
  isReadOnly,
5250
5572
  openPopoverId,