ssml-builder-js 2.8.1 → 2.9.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
@@ -36,6 +36,7 @@ __export(react_exports, {
36
36
  SSML_HOVER_COPY: () => SSML_HOVER_COPY,
37
37
  SSML_INSERTIONS: () => SSML_INSERTIONS,
38
38
  SsmlEditor: () => SsmlEditor,
39
+ VisualSsmlEditor: () => VisualSsmlEditor,
39
40
  createSsmlEditorInsertionDefinition: () => createSsmlEditorInsertionDefinition,
40
41
  registerSsmlCodeLens: () => registerSsmlCodeLens,
41
42
  updateTagAttribute: () => updateTagAttribute
@@ -43,8 +44,8 @@ __export(react_exports, {
43
44
  module.exports = __toCommonJS(react_exports);
44
45
 
45
46
  // packages/ssml-editor-react/src/SsmlEditor.tsx
46
- var import_react3 = require("react");
47
- var import_react4 = __toESM(require("@monaco-editor/react"));
47
+ var import_react4 = require("react");
48
+ var import_react5 = __toESM(require("@monaco-editor/react"));
48
49
 
49
50
  // packages/ssml-editor-react/src/buttonVisibility.ts
50
51
  function isSsmlEditorButtonVisible(buttonVisibility, button) {
@@ -473,6 +474,11 @@ var SSML_ATTRIBUTE_PRESETS = {
473
474
  "mstts:audioduration": {
474
475
  value: AUDIO_DURATION_PRESETS
475
476
  },
477
+ "mstts:backgroundaudio": {
478
+ volume: PROSODY_VOLUME_PRESETS,
479
+ fadein: AUDIO_DURATION_PRESETS,
480
+ fadeout: AUDIO_DURATION_PRESETS
481
+ },
476
482
  silence: {
477
483
  type: SILENCE_TYPE_PRESETS,
478
484
  value: SILENCE_VALUE_PRESETS
@@ -1289,6 +1295,14 @@ var editorStyles = {
1289
1295
  display: "grid",
1290
1296
  gap: "0.5rem"
1291
1297
  },
1298
+ visual: {
1299
+ display: "grid",
1300
+ gap: "0.75rem",
1301
+ minHeight: "8rem",
1302
+ padding: "0.75rem",
1303
+ border: "1px solid var(--ssml-editor-control-border)",
1304
+ borderRadius: "0.25rem"
1305
+ },
1292
1306
  toolbarIconOnly: {
1293
1307
  justifyContent: "center",
1294
1308
  minWidth: "2.25rem",
@@ -1694,7 +1708,13 @@ var SSML_TAGS = {
1694
1708
  SILENCE: "silence",
1695
1709
  MSTTS_VISEME: "mstts:viseme",
1696
1710
  VISEME: "viseme",
1697
- MSTTS_AUDIO_DURATION: "mstts:audioduration"
1711
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
1712
+ MSTTS_DIALOG: "mstts:dialog",
1713
+ MSTTS_TURN: "mstts:turn",
1714
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
1715
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
1716
+ MSTTS_EMBEDDING: "mstts:embedding",
1717
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
1698
1718
  };
1699
1719
  var SSML_ATTRS = {
1700
1720
  VERSION: "version",
@@ -1703,6 +1723,7 @@ var SSML_ATTRS = {
1703
1723
  LANG: "lang",
1704
1724
  MSTTS_XMLNS: "xmlns:mstts",
1705
1725
  NAME: "name",
1726
+ VOICE: "voice",
1706
1727
  EFFECT: "effect",
1707
1728
  RATE: "rate",
1708
1729
  PITCH: "pitch",
@@ -1734,7 +1755,9 @@ var SSML_ATTRS = {
1734
1755
  MARK: "mark",
1735
1756
  URI: "uri",
1736
1757
  TYPE: "type",
1737
- VALUE: "value"
1758
+ VALUE: "value",
1759
+ FADE_IN: "fadein",
1760
+ FADE_OUT: "fadeout"
1738
1761
  };
1739
1762
  function escapeText(value) {
1740
1763
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1824,6 +1847,20 @@ function getAttributes(element) {
1824
1847
  case SSML_TAGS.MSTTS_AUDIO_DURATION:
1825
1848
  addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
1826
1849
  break;
1850
+ case SSML_TAGS.MSTTS_TURN:
1851
+ addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1852
+ break;
1853
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1854
+ addAttribute(attributes, SSML_ATTRS.SRC, element.src);
1855
+ addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
1856
+ addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
1857
+ addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1858
+ break;
1859
+ case SSML_TAGS.MSTTS_DIALOG:
1860
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1861
+ case SSML_TAGS.MSTTS_EMBEDDING:
1862
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1863
+ break;
1827
1864
  case SSML_TAGS.PARAGRAPH:
1828
1865
  case SSML_TAGS.SENTENCE:
1829
1866
  case SSML_TAGS.WORD:
@@ -2388,6 +2425,40 @@ function convertElement(node) {
2388
2425
  if (value !== void 0) element.value = value;
2389
2426
  return finishElement(element, node, attributes);
2390
2427
  }
2428
+ case SSML_TAGS.MSTTS_DIALOG: {
2429
+ const element = { type: SSML_TAGS.MSTTS_DIALOG };
2430
+ return finishElement(element, node, attributes);
2431
+ }
2432
+ case SSML_TAGS.MSTTS_TURN: {
2433
+ const element = { type: SSML_TAGS.MSTTS_TURN };
2434
+ const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2435
+ if (voice !== void 0) element.voice = voice;
2436
+ return finishElement(element, node, attributes);
2437
+ }
2438
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
2439
+ const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
2440
+ const src = readAttribute(attributes, SSML_ATTRS.SRC);
2441
+ const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
2442
+ const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
2443
+ const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
2444
+ if (src !== void 0) element.src = src;
2445
+ if (volume !== void 0) element.volume = volume;
2446
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
2447
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
2448
+ return finishElement(element, node, attributes);
2449
+ }
2450
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2451
+ const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2452
+ return finishElement(element, node, attributes);
2453
+ }
2454
+ case SSML_TAGS.MSTTS_EMBEDDING: {
2455
+ const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2456
+ return finishElement(element, node, attributes);
2457
+ }
2458
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2459
+ const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2460
+ return finishElement(element, node, attributes);
2461
+ }
2391
2462
  default: {
2392
2463
  const element = {
2393
2464
  name: node.name,
@@ -2508,6 +2579,644 @@ function validateSsml(xmlString) {
2508
2579
  };
2509
2580
  }
2510
2581
  }
2582
+ var AZURE_VOICE_DEFINITIONS = [
2583
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2584
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2585
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
2586
+ {
2587
+ name: "en-US-GuyNeural",
2588
+ locale: "en-US",
2589
+ styles: [
2590
+ "angry",
2591
+ "cheerful",
2592
+ "excited",
2593
+ "friendly",
2594
+ "hopeful",
2595
+ "newscast",
2596
+ "sad",
2597
+ "shouting",
2598
+ "terrified",
2599
+ "unfriendly",
2600
+ "whispering"
2601
+ ]
2602
+ },
2603
+ {
2604
+ name: "en-US-JennyMultilingualNeural",
2605
+ locale: "en-US",
2606
+ styles: [
2607
+ "cheerful",
2608
+ "empathetic",
2609
+ "excited",
2610
+ "friendly",
2611
+ "hopeful",
2612
+ "sad",
2613
+ "shouting",
2614
+ "terrified",
2615
+ "unfriendly",
2616
+ "whispering"
2617
+ ]
2618
+ },
2619
+ {
2620
+ name: "en-US-JennyNeural",
2621
+ locale: "en-US",
2622
+ styles: [
2623
+ "assistant",
2624
+ "chat",
2625
+ "customerservice",
2626
+ "newscast",
2627
+ "cheerful",
2628
+ "empathetic",
2629
+ "excited",
2630
+ "friendly",
2631
+ "hopeful",
2632
+ "sad",
2633
+ "shouting",
2634
+ "terrified",
2635
+ "unfriendly",
2636
+ "whispering"
2637
+ ]
2638
+ },
2639
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
2640
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
2641
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2642
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2643
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
2644
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
2645
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
2646
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
2647
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
2648
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
2649
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
2650
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
2651
+ {
2652
+ name: "ru-RU-SvetlanaNeural",
2653
+ locale: "ru-RU",
2654
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
2655
+ },
2656
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
2657
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
2658
+ {
2659
+ name: "zh-CN-XiaoxiaoNeural",
2660
+ locale: "zh-CN",
2661
+ styles: [
2662
+ "assistant",
2663
+ "chat",
2664
+ "customerservice",
2665
+ "newscast",
2666
+ "cheerful",
2667
+ "empathetic",
2668
+ "excited",
2669
+ "friendly",
2670
+ "hopeful",
2671
+ "sad",
2672
+ "terrified",
2673
+ "whispering",
2674
+ "poetry-reading",
2675
+ "sports_commentary",
2676
+ "sports_commentary_excited",
2677
+ "story"
2678
+ ]
2679
+ },
2680
+ {
2681
+ name: "zh-CN-YunxiNeural",
2682
+ locale: "zh-CN",
2683
+ styles: [
2684
+ "narration-relaxed",
2685
+ "embarrassed",
2686
+ "fearful",
2687
+ "sad",
2688
+ "disgruntled",
2689
+ "serious",
2690
+ "angry",
2691
+ "depressed",
2692
+ "chat",
2693
+ "cheerful",
2694
+ "assistant"
2695
+ ]
2696
+ },
2697
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
2698
+ ];
2699
+ var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
2700
+ var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
2701
+ "characters",
2702
+ "spell-out",
2703
+ "cardinal",
2704
+ "ordinal",
2705
+ "number",
2706
+ "date",
2707
+ "time",
2708
+ "telephone",
2709
+ "fraction",
2710
+ "address",
2711
+ "name",
2712
+ "currency",
2713
+ "number_digit"
2714
+ ]);
2715
+ var ALLOWED_ROLES = /* @__PURE__ */ new Set([
2716
+ "Girl",
2717
+ "Boy",
2718
+ "YoungAdultFemale",
2719
+ "YoungAdultMale",
2720
+ "OlderAdultFemale",
2721
+ "OlderAdultMale",
2722
+ "SeniorFemale",
2723
+ "SeniorMale"
2724
+ ]);
2725
+ var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
2726
+ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
2727
+ "Leading",
2728
+ "Tailing",
2729
+ "Sentenceboundary",
2730
+ "Comma",
2731
+ "Semicolon",
2732
+ "Enumerationcomma"
2733
+ ]);
2734
+ var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
2735
+ function decodeAttribute(value) {
2736
+ return value.replace(
2737
+ /&(?:amp|apos|gt|lt|quot);/gi,
2738
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
2739
+ );
2740
+ }
2741
+ function findTagEnd2(source, start) {
2742
+ let quote = "";
2743
+ for (let index = start; index < source.length; index += 1) {
2744
+ const character = source[index];
2745
+ if (quote) {
2746
+ if (character === quote) quote = "";
2747
+ } else if (character === '"' || character === "'") quote = character;
2748
+ else if (character === ">") return index;
2749
+ }
2750
+ return source.length - 1;
2751
+ }
2752
+ function tokenizeElements(source) {
2753
+ const tokens = [];
2754
+ const openElements = [];
2755
+ let index = 0;
2756
+ while (index < source.length) {
2757
+ const start = source.indexOf("<", index);
2758
+ if (start === -1) break;
2759
+ if (source.startsWith("<!--", start)) {
2760
+ const end2 = source.indexOf("-->", start + 4);
2761
+ index = end2 === -1 ? source.length : end2 + 3;
2762
+ continue;
2763
+ }
2764
+ if (source.startsWith("<![CDATA[", start)) {
2765
+ const end2 = source.indexOf("]]>", start + 9);
2766
+ index = end2 === -1 ? source.length : end2 + 3;
2767
+ continue;
2768
+ }
2769
+ if (source.startsWith("<?", start)) {
2770
+ const end2 = source.indexOf("?>", start + 2);
2771
+ index = end2 === -1 ? source.length : end2 + 2;
2772
+ continue;
2773
+ }
2774
+ const end = findTagEnd2(source, start + 1);
2775
+ const raw = source.slice(start, end + 1);
2776
+ if (raw.startsWith("</")) {
2777
+ openElements.pop();
2778
+ index = end + 1;
2779
+ continue;
2780
+ }
2781
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
2782
+ if (!nameMatch?.[1]) {
2783
+ index = end + 1;
2784
+ continue;
2785
+ }
2786
+ const attributes = /* @__PURE__ */ new Map();
2787
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
2788
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
2789
+ for (const match of attributeSource.matchAll(attributePattern)) {
2790
+ attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
2791
+ }
2792
+ const selfClosing = /\/\s*>$/.test(raw);
2793
+ const parent = openElements[openElements.length - 1];
2794
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2795
+ const tokenName = nameMatch[1];
2796
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2797
+ tokens.push({
2798
+ attributes,
2799
+ end,
2800
+ name: tokenName,
2801
+ parentName: parent?.name,
2802
+ parentVoiceName,
2803
+ selfClosing,
2804
+ start
2805
+ });
2806
+ if (!selfClosing) {
2807
+ openElements.push({
2808
+ name: tokenName,
2809
+ voiceName: tokenVoiceName
2810
+ });
2811
+ }
2812
+ index = end + 1;
2813
+ }
2814
+ return tokens;
2815
+ }
2816
+ function location(source, offset) {
2817
+ const before = source.slice(0, Math.max(0, offset));
2818
+ const line = before.split("\n").length;
2819
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2820
+ }
2821
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
2822
+ diagnostics.push({
2823
+ ...location(source, offset),
2824
+ message,
2825
+ severity,
2826
+ source: "ssml-static-validator",
2827
+ ...code2 ? { code: code2 } : {}
2828
+ });
2829
+ }
2830
+ function isSupportedProsodyRate(value) {
2831
+ const trimmed = value.trim();
2832
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
2833
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
2834
+ if (!multiplier) return false;
2835
+ const numericValue = Number(multiplier[1]);
2836
+ return numericValue >= 0.5 && numericValue <= 2;
2837
+ }
2838
+ function isValidAzureAudioDuration(value) {
2839
+ const trimmed = value.trim();
2840
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
2841
+ if (numeric) return Number(numeric[1]) > 0;
2842
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
2843
+ if (!clock) return false;
2844
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2845
+ }
2846
+ function attr(token, name) {
2847
+ return token.attributes.get(name.toLowerCase());
2848
+ }
2849
+ var DEFAULT_LANGUAGE_ALIASES = {
2850
+ "zh-CN": ["zh-Hans"],
2851
+ "zh-TW": ["zh-Hant"]
2852
+ };
2853
+ function canonicalLanguageTag(language) {
2854
+ const trimmed = language.trim();
2855
+ if (!trimmed) return "";
2856
+ try {
2857
+ return new Intl.Locale(trimmed).toString().toLowerCase();
2858
+ } catch {
2859
+ return trimmed.toLowerCase();
2860
+ }
2861
+ }
2862
+ function createLanguageNormalizer(options) {
2863
+ const aliases = /* @__PURE__ */ new Map();
2864
+ const addAliasGroup = (canonical, values) => {
2865
+ const normalizedCanonical = canonicalLanguageTag(canonical);
2866
+ if (!normalizedCanonical) return;
2867
+ aliases.set(normalizedCanonical, normalizedCanonical);
2868
+ for (const value of values) {
2869
+ const normalizedValue = canonicalLanguageTag(value);
2870
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
2871
+ }
2872
+ };
2873
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
2874
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
2875
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
2876
+ return (language) => {
2877
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
2878
+ const normalized = canonicalLanguageTag(customValue);
2879
+ return aliases.get(normalized) ?? normalized;
2880
+ };
2881
+ }
2882
+ function voiceLocalePrefix(voiceName) {
2883
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
2884
+ if (!match?.groups) return void 0;
2885
+ const tag = `${match.groups.language}-${match.groups.region}`;
2886
+ return {
2887
+ language: match.groups.language.toLowerCase(),
2888
+ region: match.groups.region.toLowerCase(),
2889
+ tag
2890
+ };
2891
+ }
2892
+ function definitionFromStyleMap(voiceName, styles) {
2893
+ return {
2894
+ name: voiceName,
2895
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
2896
+ styles
2897
+ };
2898
+ }
2899
+ function normalizeVoiceCatalog(options) {
2900
+ const definitions = /* @__PURE__ */ new Map();
2901
+ for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
2902
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
2903
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
2904
+ for (const definition of options.customVoiceDefinitions ?? [])
2905
+ definitions.set(definition.name.toLowerCase(), definition);
2906
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
2907
+ const key = voiceName.toLowerCase();
2908
+ const current = definitions.get(key);
2909
+ definitions.set(key, {
2910
+ ...current ?? definitionFromStyleMap(voiceName, styles),
2911
+ name: current?.name ?? voiceName,
2912
+ styles: styles.map((style) => style.toLowerCase())
2913
+ });
2914
+ }
2915
+ return definitions;
2916
+ }
2917
+ function diagnosticSeverity(policy) {
2918
+ if (policy === "ignore") return void 0;
2919
+ return policy === "error" ? "error" : "warning";
2920
+ }
2921
+ function languagePart(language) {
2922
+ try {
2923
+ return new Intl.Locale(language).language.toLowerCase();
2924
+ } catch {
2925
+ return language.split("-")[0]?.toLowerCase() ?? "";
2926
+ }
2927
+ }
2928
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
2929
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
2930
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
2931
+ const normalizedLanguage = normalizeLanguage(language);
2932
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
2933
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
2934
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
2935
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
2936
+ }
2937
+ function canonicalTagName(name) {
2938
+ const normalized = name.toLowerCase();
2939
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
2940
+ if (normalized === "sayas") return "say-as";
2941
+ return normalized;
2942
+ }
2943
+ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
2944
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
2945
+ return;
2946
+ const tagName = canonicalTagName(token.name);
2947
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
2948
+ const supportedTags = definition.supportedTags?.map(canonicalTagName);
2949
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
2950
+ addDiagnostic(
2951
+ diagnostics,
2952
+ source,
2953
+ token.start,
2954
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
2955
+ "error",
2956
+ "azure-unsupported-tag-for-voice"
2957
+ );
2958
+ }
2959
+ }
2960
+ function validateAudioSource(token, source, diagnostics, options, elementName2) {
2961
+ const src = attr(token, "src");
2962
+ if (!src) {
2963
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2964
+ return;
2965
+ }
2966
+ let parsed;
2967
+ try {
2968
+ parsed = new URL(src);
2969
+ } catch {
2970
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2971
+ return;
2972
+ }
2973
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2974
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2975
+ if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
2976
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2977
+ else if (!options.allowExternalAudio)
2978
+ addDiagnostic(
2979
+ diagnostics,
2980
+ source,
2981
+ token.start,
2982
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2983
+ );
2984
+ }
2985
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
2986
+ const name = token.name.toLowerCase();
2987
+ if (name === "voice" && !attr(token, "name")?.trim())
2988
+ addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
2989
+ if (name === "break") {
2990
+ const time = attr(token, "time");
2991
+ const strength = attr(token, "strength");
2992
+ if (!time && !strength)
2993
+ addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
2994
+ if (time && strength)
2995
+ addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
2996
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
2997
+ addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
2998
+ if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
2999
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
3000
+ }
3001
+ if (name === "prosody") {
3002
+ const rate = attr(token, "rate");
3003
+ const pitch = attr(token, "pitch");
3004
+ const volume = attr(token, "volume");
3005
+ if (rate && !isSupportedProsodyRate(rate))
3006
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
3007
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
3008
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
3009
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
3010
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
3011
+ }
3012
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
3013
+ const style = attr(token, "style");
3014
+ if (!style?.trim())
3015
+ addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
3016
+ const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
3017
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
3018
+ addDiagnostic(
3019
+ diagnostics,
3020
+ source,
3021
+ token.start,
3022
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
3023
+ );
3024
+ const role = attr(token, "role");
3025
+ if (role && !ALLOWED_ROLES.has(role))
3026
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
3027
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
3028
+ const supportedStyles = definition?.styles;
3029
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
3030
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
3031
+ addDiagnostic(
3032
+ diagnostics,
3033
+ source,
3034
+ token.start,
3035
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
3036
+ severity,
3037
+ "azure-unsupported-style"
3038
+ );
3039
+ if (style && voiceName && !definition && severity)
3040
+ addDiagnostic(
3041
+ diagnostics,
3042
+ source,
3043
+ token.start,
3044
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
3045
+ severity
3046
+ );
3047
+ }
3048
+ if (name === "say-as" || name === "sayas") {
3049
+ const interpretAs = attr(token, "interpret-as");
3050
+ if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
3051
+ addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
3052
+ }
3053
+ if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
3054
+ addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
3055
+ if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
3056
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
3057
+ if (name === "sub" && !attr(token, "alias")?.trim())
3058
+ addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
3059
+ if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
3060
+ addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
3061
+ if (name === "mark" && !attr(token, "name")?.trim())
3062
+ addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
3063
+ if (name === "bookmark" && !attr(token, "mark")?.trim())
3064
+ addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
3065
+ if (name === "lexicon") {
3066
+ const uri = attr(token, "uri");
3067
+ if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
3068
+ else {
3069
+ try {
3070
+ const parsed = new URL(uri);
3071
+ if (parsed.protocol !== "https:")
3072
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
3073
+ } catch {
3074
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
3075
+ }
3076
+ }
3077
+ }
3078
+ if (name === "mstts:silence") {
3079
+ const type = attr(token, "type");
3080
+ const value = attr(token, "value");
3081
+ if (!type || !ALLOWED_SILENCE_TYPES.has(type))
3082
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
3083
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
3084
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
3085
+ }
3086
+ if (name === "mstts:audioduration") {
3087
+ const value = attr(token, "value");
3088
+ if (!value || !isValidAzureAudioDuration(value))
3089
+ addDiagnostic(
3090
+ diagnostics,
3091
+ source,
3092
+ token.start,
3093
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
3094
+ );
3095
+ if (!token.selfClosing)
3096
+ addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
3097
+ }
3098
+ if (name === "mstts:viseme") {
3099
+ const type = attr(token, "type");
3100
+ if (!type || !ALLOWED_VISEME_TYPES.has(type))
3101
+ addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
3102
+ }
3103
+ if (name === "audio") {
3104
+ validateAudioSource(token, source, diagnostics, options, "audio");
3105
+ }
3106
+ if (name === "mstts:turn") {
3107
+ if (!attr(token, "voice")?.trim())
3108
+ addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
3109
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
3110
+ addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
3111
+ }
3112
+ if (name === "mstts:backgroundaudio") {
3113
+ validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
3114
+ const volume = attr(token, "volume");
3115
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
3116
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
3117
+ for (const [attribute, value] of [
3118
+ ["fadein", attr(token, "fadein")],
3119
+ ["fadeout", attr(token, "fadeout")]
3120
+ ]) {
3121
+ if (value && !isValidAzureAudioDuration(value))
3122
+ addDiagnostic(
3123
+ diagnostics,
3124
+ source,
3125
+ token.start,
3126
+ `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
3127
+ );
3128
+ }
3129
+ if (!token.selfClosing)
3130
+ addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3131
+ }
3132
+ }
3133
+ function validateAzureSsml(ssml, options = {}) {
3134
+ const diagnostics = [];
3135
+ if (typeof ssml !== "string") {
3136
+ return [
3137
+ {
3138
+ line: 1,
3139
+ column: 1,
3140
+ message: "SSML input must be a string",
3141
+ severity: "error",
3142
+ source: "ssml-static-validator"
3143
+ }
3144
+ ];
3145
+ }
3146
+ const maxLength = options.maxLength ?? 1e4;
3147
+ if (ssml.length > maxLength)
3148
+ addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
3149
+ try {
3150
+ parseSsml(ssml);
3151
+ } catch (error) {
3152
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
3153
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
3154
+ addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
3155
+ return diagnostics;
3156
+ }
3157
+ const tokens = tokenizeElements(ssml);
3158
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
3159
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
3160
+ if (!speak || voices.length === 0)
3161
+ addDiagnostic(
3162
+ diagnostics,
3163
+ ssml,
3164
+ speak?.start ?? 0,
3165
+ "Azure SSML requires at least one <voice> element under <speak>."
3166
+ );
3167
+ const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
3168
+ const voiceCatalog = normalizeVoiceCatalog(options);
3169
+ const normalizeLanguage = createLanguageNormalizer(options);
3170
+ const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
3171
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
3172
+ for (const token of voicesToValidate) {
3173
+ const name = attr(token, "name")?.trim();
3174
+ const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
3175
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
3176
+ if (name && !definition && policySeverity)
3177
+ addDiagnostic(
3178
+ diagnostics,
3179
+ ssml,
3180
+ token.start,
3181
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
3182
+ policySeverity,
3183
+ "azure-unknown-voice"
3184
+ );
3185
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
3186
+ addDiagnostic(
3187
+ diagnostics,
3188
+ ssml,
3189
+ token.start,
3190
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
3191
+ "warning",
3192
+ "azure-locale-mismatch"
3193
+ );
3194
+ }
3195
+ for (const token of tokens) {
3196
+ const tokenName = token.name.toLowerCase();
3197
+ const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
3198
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
3199
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
3200
+ validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
3201
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
3202
+ addDiagnostic(
3203
+ diagnostics,
3204
+ ssml,
3205
+ token.start,
3206
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
3207
+ "error",
3208
+ "azure-unsupported-model-for-voice"
3209
+ );
3210
+ }
3211
+ }
3212
+ return diagnostics;
3213
+ }
3214
+ var AZURE_VOICE_CATALOG_METADATA = {
3215
+ apiVersion: "2025-10-01",
3216
+ generatedAt: "2026-08-28T00:00:00.000Z",
3217
+ regions: [],
3218
+ voiceCount: AZURE_VOICE_DEFINITIONS.length
3219
+ };
2511
3220
 
2512
3221
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
2513
3222
  function getDocumentChildren(document2) {
@@ -3266,7 +3975,7 @@ function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol =
3266
3975
  }
3267
3976
 
3268
3977
  // packages/ssml-editor-react/src/ssmlContext.ts
3269
- function findTagEnd2(source, start, limit) {
3978
+ function findTagEnd3(source, start, limit) {
3270
3979
  let quote;
3271
3980
  for (let index = start + 1; index < limit; index += 1) {
3272
3981
  const character = source[index];
@@ -3336,7 +4045,7 @@ function findActiveSsmlTags(source, offset) {
3336
4045
  index = end;
3337
4046
  continue;
3338
4047
  }
3339
- const tagEnd = findTagEnd2(source, tagStart, source.length);
4048
+ const tagEnd = findTagEnd3(source, tagStart, source.length);
3340
4049
  const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
3341
4050
  const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3342
4051
  const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
@@ -3379,7 +4088,7 @@ function findSsmlVoiceContext(source, offset) {
3379
4088
  index = end === -1 || end + 2 > limit ? limit : end + 2;
3380
4089
  continue;
3381
4090
  }
3382
- const tagEnd = findTagEnd2(source, tagStart, limit);
4091
+ const tagEnd = findTagEnd3(source, tagStart, limit);
3383
4092
  if (tagEnd === -1) {
3384
4093
  break;
3385
4094
  }
@@ -4139,6 +4848,24 @@ var SSML_COMPLETION_SNIPPETS = [
4139
4848
  label: "mstts:audioduration",
4140
4849
  insertText: '<mstts:audioduration value="10s" />'
4141
4850
  },
4851
+ {
4852
+ label: "mstts:dialog",
4853
+ insertText: `<mstts:dialog>
4854
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
4855
+ </mstts:dialog>`
4856
+ },
4857
+ {
4858
+ label: "mstts:turn",
4859
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
4860
+ },
4861
+ {
4862
+ label: "mstts:backgroundaudio",
4863
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
4864
+ },
4865
+ {
4866
+ label: "mstts:ttsembedding",
4867
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
4868
+ },
4142
4869
  {
4143
4870
  label: "sub",
4144
4871
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -4317,7 +5044,7 @@ function registerSsmlCodeActions(monaco) {
4317
5044
 
4318
5045
  // packages/ssml-editor-react/src/ssmlCodeLens.ts
4319
5046
  var CODE_LENS_COMMAND = "ssml-editor.codeLens";
4320
- function findTagEnd3(source, start) {
5047
+ function findTagEnd4(source, start) {
4321
5048
  let quote;
4322
5049
  for (let index = start + 1; index < source.length; index += 1) {
4323
5050
  const character = source[index];
@@ -4357,7 +5084,7 @@ function getElementEnd(source, tagName, tagEnd) {
4357
5084
  index = index === -1 ? source.length : index + 3;
4358
5085
  continue;
4359
5086
  }
4360
- const nextEnd = findTagEnd3(source, nextStart);
5087
+ const nextEnd = findTagEnd4(source, nextStart);
4361
5088
  if (nextEnd === -1) {
4362
5089
  break;
4363
5090
  }
@@ -4409,7 +5136,7 @@ function registerSsmlCodeLens(monaco, editor, onOpenPopover) {
4409
5136
  index = index === -1 ? source.length : index + 3;
4410
5137
  continue;
4411
5138
  }
4412
- const tagEnd = findTagEnd3(source, tagStart);
5139
+ const tagEnd = findTagEnd4(source, tagStart);
4413
5140
  if (tagEnd === -1) {
4414
5141
  break;
4415
5142
  }
@@ -4806,6 +5533,63 @@ var SSML_TAG_DEFINITIONS = [
4806
5533
  example: "10s"
4807
5534
  }
4808
5535
  ]
5536
+ },
5537
+ {
5538
+ name: "mstts:dialog",
5539
+ description: "Groups multiple Azure dialog turns that can use different voices.",
5540
+ parameters: []
5541
+ },
5542
+ {
5543
+ name: "mstts:turn",
5544
+ description: "Adds one dialog turn using the required Azure voice name.",
5545
+ parameters: [
5546
+ {
5547
+ name: "voice",
5548
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
5549
+ example: "en-US-JennyNeural"
5550
+ }
5551
+ ]
5552
+ },
5553
+ {
5554
+ name: "mstts:backgroundaudio",
5555
+ description: "Plays background audio while speech is synthesized.",
5556
+ parameters: [
5557
+ {
5558
+ name: "src",
5559
+ description: "An absolute HTTP(S) URL for the background audio file.",
5560
+ example: "https://example.com/music.mp3"
5561
+ },
5562
+ {
5563
+ name: "volume",
5564
+ description: "The background audio volume, for example `-3dB` or `medium`.",
5565
+ example: "-3dB"
5566
+ },
5567
+ {
5568
+ name: "fadein",
5569
+ description: "The fade-in duration, for example `1s`.",
5570
+ example: "1s"
5571
+ },
5572
+ {
5573
+ name: "fadeout",
5574
+ description: "The fade-out duration, for example `500ms`.",
5575
+ example: "500ms"
5576
+ }
5577
+ ]
5578
+ },
5579
+ {
5580
+ name: "mstts:ttsembedding",
5581
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
5582
+ parameters: []
5583
+ },
5584
+ {
5585
+ name: "mstts:embedding",
5586
+ description: "Specifies embedding metadata for custom voice scenarios.",
5587
+ parameters: []
5588
+ },
5589
+ {
5590
+ name: "mstts:voiceconversion",
5591
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
5592
+ parameters: []
4809
5593
  }
4810
5594
  ];
4811
5595
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -4864,7 +5648,7 @@ function toRange(source, token) {
4864
5648
  function containsOffset(token, offset) {
4865
5649
  return offset >= token.start && offset < token.end;
4866
5650
  }
4867
- function findTagEnd4(source, start) {
5651
+ function findTagEnd5(source, start) {
4868
5652
  let quote;
4869
5653
  for (let index = start; index < source.length; index += 1) {
4870
5654
  const character = source[index];
@@ -4995,7 +5779,7 @@ function findTagAtOffset(source, offset) {
4995
5779
  searchStart = tokenEnd2;
4996
5780
  continue;
4997
5781
  }
4998
- const tagEnd = findTagEnd4(source, start + 1);
5782
+ const tagEnd = findTagEnd5(source, start + 1);
4999
5783
  const contentEnd = tagEnd ?? source.length;
5000
5784
  const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
5001
5785
  if (offset < tokenEnd) {
@@ -5410,6 +6194,140 @@ function useSsmlMonaco({
5410
6194
  return { editorRef, onMount };
5411
6195
  }
5412
6196
 
6197
+ // packages/ssml-editor-react/src/components/VisualSsmlEditor.tsx
6198
+ var import_react3 = require("react");
6199
+ function isElement(node) {
6200
+ return typeof node !== "string" && node.type !== "text";
6201
+ }
6202
+ function elementLabel(element) {
6203
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
6204
+ }
6205
+ function collectTextLeaves(nodes, path = [], ancestors = []) {
6206
+ return nodes.flatMap((node, index) => {
6207
+ const currentPath = [...path, index];
6208
+ if (typeof node === "string") return [{ ancestors, path: currentPath, value: node }];
6209
+ if (node.type === "text") return [{ ancestors, path: currentPath, value: node.value }];
6210
+ return collectTextLeaves(node.children ?? [], currentPath, [...ancestors, elementLabel(node)]);
6211
+ });
6212
+ }
6213
+ function updateNodesAtPath(nodes, path, update) {
6214
+ if (path.length === 0) return nodes;
6215
+ const [index, ...rest] = path;
6216
+ return nodes.flatMap((node, nodeIndex) => {
6217
+ if (nodeIndex !== index) return [node];
6218
+ if (rest.length === 0) {
6219
+ const next = update(node);
6220
+ return Array.isArray(next) ? next : [next];
6221
+ }
6222
+ if (!isElement(node)) return [node];
6223
+ return [{ ...node, children: updateNodesAtPath(node.children ?? [], rest, update) }];
6224
+ });
6225
+ }
6226
+ function updateTextAtPath(document2, path, value) {
6227
+ return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
6228
+ }
6229
+ function wrapTextAtPath(document2, path, start, end, tag, attributes) {
6230
+ return {
6231
+ ...document2,
6232
+ children: updateNodesAtPath(document2.children ?? [], path, (node) => {
6233
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
6234
+ if (!value || start === end) return node;
6235
+ if (tag === "break") {
6236
+ return [
6237
+ value.slice(0, start),
6238
+ { type: tag, attributes, children: [] },
6239
+ value.slice(start)
6240
+ ].filter((part) => typeof part === "string" ? part.length > 0 : true);
6241
+ }
6242
+ const selected = value.slice(start, end);
6243
+ const wrapped = { type: tag, attributes, children: [selected] };
6244
+ return [value.slice(0, start), wrapped, value.slice(end)].filter(
6245
+ (part) => typeof part === "string" ? part.length > 0 : true
6246
+ );
6247
+ })
6248
+ };
6249
+ }
6250
+ function TreeNode({
6251
+ node,
6252
+ path,
6253
+ selectedPath,
6254
+ onSelect
6255
+ }) {
6256
+ if (!isElement(node)) return null;
6257
+ const name = elementLabel(node);
6258
+ const isSelected = selectedPath?.join(".") === path.join(".");
6259
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`), (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
6260
+ TreeNode,
6261
+ {
6262
+ key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
6263
+ node: child,
6264
+ path: [...path, index],
6265
+ selectedPath,
6266
+ onSelect
6267
+ }
6268
+ ))));
6269
+ }
6270
+ function VisualSsmlEditor({
6271
+ document: document2,
6272
+ readOnly = false,
6273
+ onChange,
6274
+ onPreviewSelection
6275
+ }) {
6276
+ const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
6277
+ const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
6278
+ const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
6279
+ const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
6280
+ const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
6281
+ const commit = (nextDocument) => onChange?.(nextDocument);
6282
+ const applyWrapper = (tag, attributes) => {
6283
+ if (readOnly || !selectedLeaf) return;
6284
+ const start = selection.start === selection.end ? 0 : selection.start;
6285
+ const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
6286
+ commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
6287
+ };
6288
+ return /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual", "data-ssml-editor-visual": "" }, /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-breadcrumb" }, /* @__PURE__ */ React.createElement("legend", null, "SSML structure breadcrumb"), /* @__PURE__ */ React.createElement("span", null, "<speak>"), selectedLeaf?.ancestors.map((ancestor, index) => /* @__PURE__ */ React.createElement("span", { key: selectedLeaf?.ancestors.slice(0, index + 1).join("/") }, " / <", ancestor, ">")), selectedPath && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSelectedPath(null) }, "Clear parent")), diagnostics.length > 0 && /* @__PURE__ */ React.createElement("div", { role: "alert", "aria-invalid": "true", className: "ssml-editor-visual-errors" }, diagnostics.map((diagnostic) => /* @__PURE__ */ React.createElement("div", { key: `${diagnostic.code}-${diagnostic.message}` }, diagnostic.message))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-layout" }, /* @__PURE__ */ React.createElement("nav", { "aria-label": "SSML structure tree", className: "ssml-editor-visual-tree" }, /* @__PURE__ */ React.createElement("strong", null, "Structure"), /* @__PURE__ */ React.createElement("ul", null, document2.children?.map((node, index) => /* @__PURE__ */ React.createElement(
6289
+ TreeNode,
6290
+ {
6291
+ key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
6292
+ node,
6293
+ path: [index],
6294
+ selectedPath,
6295
+ onSelect: setSelectedPath
6296
+ }
6297
+ )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6298
+ "textarea",
6299
+ {
6300
+ value: selectedLeaf.value,
6301
+ readOnly,
6302
+ onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
6303
+ onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
6304
+ }
6305
+ )), /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-actions" }, /* @__PURE__ */ React.createElement("legend", null, "Apply SSML formatting"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { rate: "slow" }) }, "Rate"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { pitch: "high" }) }, "Pitch"), /* @__PURE__ */ React.createElement(
6306
+ "button",
6307
+ {
6308
+ type: "button",
6309
+ disabled: readOnly,
6310
+ onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
6311
+ },
6312
+ "Emotion"
6313
+ ), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
6314
+ "button",
6315
+ {
6316
+ type: "button",
6317
+ disabled: readOnly,
6318
+ onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
6319
+ },
6320
+ "Pronunciation"
6321
+ ), onPreviewSelection && /* @__PURE__ */ React.createElement(
6322
+ "button",
6323
+ {
6324
+ type: "button",
6325
+ onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
6326
+ },
6327
+ "Preview selection"
6328
+ ))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));
6329
+ }
6330
+
5413
6331
  // packages/ssml-editor-react/src/SsmlEditor.tsx
5414
6332
  var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
5415
6333
  var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
@@ -5876,6 +6794,60 @@ var STYLE_CSS = `
5876
6794
  > .ssml-editor-help-settings-summary::before {
5877
6795
  content: "\u25BE";
5878
6796
  }
6797
+ [data-ssml-editor] .ssml-editor-visual-layout {
6798
+ display: grid;
6799
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
6800
+ gap: 1rem;
6801
+ }
6802
+ [data-ssml-editor] .ssml-editor-visual-tree,
6803
+ [data-ssml-editor] .ssml-editor-visual-form {
6804
+ display: grid;
6805
+ gap: 0.5rem;
6806
+ align-content: start;
6807
+ }
6808
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
6809
+ margin: 0;
6810
+ padding-left: 1.25rem;
6811
+ }
6812
+ [data-ssml-editor] .ssml-editor-visual-tree button,
6813
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb button,
6814
+ [data-ssml-editor] .ssml-editor-visual-actions button {
6815
+ padding: 0.35rem 0.5rem;
6816
+ border: 1px solid var(--ssml-editor-control-border);
6817
+ border-radius: 0.25rem;
6818
+ color: var(--ssml-editor-color);
6819
+ background: var(--ssml-editor-control-bg);
6820
+ cursor: pointer;
6821
+ }
6822
+ [data-ssml-editor] .ssml-editor-visual-tree button[aria-current] {
6823
+ border-color: var(--ssml-editor-active-border);
6824
+ background: var(--ssml-editor-active-bg);
6825
+ }
6826
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb,
6827
+ [data-ssml-editor] .ssml-editor-visual-actions {
6828
+ display: flex;
6829
+ flex-wrap: wrap;
6830
+ gap: 0.4rem;
6831
+ align-items: center;
6832
+ }
6833
+ [data-ssml-editor] .ssml-editor-visual-form textarea {
6834
+ box-sizing: border-box;
6835
+ width: 100%;
6836
+ min-height: 6rem;
6837
+ padding: 0.5rem;
6838
+ color: var(--ssml-editor-color);
6839
+ background: var(--ssml-editor-control-bg);
6840
+ border: 1px solid var(--ssml-editor-control-border);
6841
+ border-radius: 0.25rem;
6842
+ font: inherit;
6843
+ }
6844
+ [data-ssml-editor] .ssml-editor-visual-errors {
6845
+ padding: 0.5rem;
6846
+ color: var(--ssml-editor-error);
6847
+ background: var(--ssml-editor-error-bg);
6848
+ border: 1px solid var(--ssml-editor-error);
6849
+ border-radius: 0.25rem;
6850
+ }
5879
6851
  `.trim();
5880
6852
  function injectEditorTheme() {
5881
6853
  if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
@@ -5907,7 +6879,7 @@ function ToolbarDecorationsSwitch({
5907
6879
  /* @__PURE__ */ React.createElement("span", { className: "ssml-editor-switch-thumb", style: editorStyles.toolbarSwitchThumb, "aria-hidden": "true" })
5908
6880
  ));
5909
6881
  }
5910
- var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6882
+ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
5911
6883
  document: document2,
5912
6884
  onChange,
5913
6885
  onSsmlChange,
@@ -5945,8 +6917,11 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5945
6917
  toolbarStyle,
5946
6918
  displayClassName,
5947
6919
  displayStyle,
5948
- loadingFallback
6920
+ loadingFallback,
6921
+ editMode: editModeProp = "code"
5949
6922
  }, ref) {
6923
+ const [editMode, setEditMode] = (0, import_react4.useState)(editModeProp);
6924
+ (0, import_react4.useEffect)(() => setEditMode(editModeProp), [editModeProp]);
5950
6925
  const resolvedEditorOptions = {
5951
6926
  ...settings,
5952
6927
  ...editorOptions,
@@ -6024,6 +6999,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6024
6999
  );
6025
7000
  const {
6026
7001
  editorRef,
7002
+ draftDocument,
6027
7003
  text,
6028
7004
  selectionOverlay,
6029
7005
  activeTags,
@@ -6035,6 +7011,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6035
7011
  syntaxError,
6036
7012
  setSyntaxError,
6037
7013
  helpPanelId,
7014
+ commit,
6038
7015
  handleTextChange,
6039
7016
  handleInsert,
6040
7017
  handleInsertBreak,
@@ -6082,7 +7059,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6082
7059
  enableCodeLens,
6083
7060
  onOpenPopover: handleCodeLensAction
6084
7061
  });
6085
- (0, import_react3.useImperativeHandle)(
7062
+ (0, import_react4.useImperativeHandle)(
6086
7063
  ref,
6087
7064
  () => ({
6088
7065
  getSelectedSsml: getSelectedSsml2,
@@ -6265,7 +7242,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6265
7242
  previousRenderedGroupId = groupId;
6266
7243
  hasRenderedToolbarItem = true;
6267
7244
  renderedItems.push(
6268
- /* @__PURE__ */ React.createElement(import_react3.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
7245
+ /* @__PURE__ */ React.createElement(import_react4.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
6269
7246
  );
6270
7247
  }
6271
7248
  return renderedItems;
@@ -6294,11 +7271,40 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6294
7271
  "aria-label": copy.toolbarAriaLabel,
6295
7272
  "data-ssml-editor-toolbar-actions": ""
6296
7273
  },
6297
- renderToolbarItems()
7274
+ renderToolbarItems(),
7275
+ /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }),
7276
+ /* @__PURE__ */ React.createElement(
7277
+ "button",
7278
+ {
7279
+ type: "button",
7280
+ style: editMode === "visual" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
7281
+ "aria-pressed": editMode === "visual",
7282
+ onClick: () => setEditMode("visual")
7283
+ },
7284
+ "Visual"
7285
+ ),
7286
+ /* @__PURE__ */ React.createElement(
7287
+ "button",
7288
+ {
7289
+ type: "button",
7290
+ style: editMode === "code" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
7291
+ "aria-pressed": editMode === "code",
7292
+ onClick: () => setEditMode("code")
7293
+ },
7294
+ "Code"
7295
+ )
6298
7296
  )
6299
7297
  ),
6300
- /* @__PURE__ */ React.createElement("div", { className: displayClassName, style: { ...editorStyles.display, ...displayStyle }, "data-ssml-editor-display": "" }, isHelpOpen && isSsmlEditorButtonVisible(buttonVisibility, "help") && /* @__PURE__ */ React.createElement("section", { id: helpPanelId, style: editorStyles.helpPanel, "aria-label": copy.helpHeading }, /* @__PURE__ */ React.createElement("h3", { style: editorStyles.helpHeading }, copy.helpHeading), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpDescription }, copy.helpDescription), helpInsertions.length > 0 && /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpList }, helpInsertions.map((insertion) => /* @__PURE__ */ React.createElement("li", { key: insertion.id, style: editorStyles.helpItem }, /* @__PURE__ */ React.createElement("details", { className: "ssml-editor-help-settings-accordion", style: editorStyles.helpSettingsAccordion }, /* @__PURE__ */ React.createElement("summary", { className: "ssml-editor-help-settings-summary", style: editorStyles.helpSettingsSummary }, /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpIcon, "aria-hidden": "true" }, insertion.icon), /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpSettingsSummaryContent }, /* @__PURE__ */ React.createElement("strong", null, insertion.labels[language]), " ", insertion.tagName && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("code", null, `<${insertion.tagName}${insertion.selfClosing ? "/>" : ">"}`), " \u2014", " "), insertion.descriptions[language])), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpSettingsDescription }, /* @__PURE__ */ React.createElement("strong", null, copy.parameters, ":"), " ", insertion.parameterDescription[language]), /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpSettingsList }, insertion.options.map((option) => /* @__PURE__ */ React.createElement("li", { key: option.value }, /* @__PURE__ */ React.createElement("strong", null, option.labels[language]), option.descriptions && /* @__PURE__ */ React.createElement(React.Fragment, null, " \u2014 ", option.descriptions[language]))))))))), /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
6301
- import_react4.default,
7298
+ /* @__PURE__ */ React.createElement("div", { className: displayClassName, style: { ...editorStyles.display, ...displayStyle }, "data-ssml-editor-display": "" }, isHelpOpen && isSsmlEditorButtonVisible(buttonVisibility, "help") && /* @__PURE__ */ React.createElement("section", { id: helpPanelId, style: editorStyles.helpPanel, "aria-label": copy.helpHeading }, /* @__PURE__ */ React.createElement("h3", { style: editorStyles.helpHeading }, copy.helpHeading), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpDescription }, copy.helpDescription), helpInsertions.length > 0 && /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpList }, helpInsertions.map((insertion) => /* @__PURE__ */ React.createElement("li", { key: insertion.id, style: editorStyles.helpItem }, /* @__PURE__ */ React.createElement("details", { className: "ssml-editor-help-settings-accordion", style: editorStyles.helpSettingsAccordion }, /* @__PURE__ */ React.createElement("summary", { className: "ssml-editor-help-settings-summary", style: editorStyles.helpSettingsSummary }, /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpIcon, "aria-hidden": "true" }, insertion.icon), /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpSettingsSummaryContent }, /* @__PURE__ */ React.createElement("strong", null, insertion.labels[language]), " ", insertion.tagName && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("code", null, `<${insertion.tagName}${insertion.selfClosing ? "/>" : ">"}`), " \u2014", " "), insertion.descriptions[language])), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpSettingsDescription }, /* @__PURE__ */ React.createElement("strong", null, copy.parameters, ":"), " ", insertion.parameterDescription[language]), /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpSettingsList }, insertion.options.map((option) => /* @__PURE__ */ React.createElement("li", { key: option.value }, /* @__PURE__ */ React.createElement("strong", null, option.labels[language]), option.descriptions && /* @__PURE__ */ React.createElement(React.Fragment, null, " \u2014 ", option.descriptions[language]))))))))), editMode === "visual" ? /* @__PURE__ */ React.createElement("div", { style: editorStyles.visual }, /* @__PURE__ */ React.createElement(
7299
+ VisualSsmlEditor,
7300
+ {
7301
+ document: draftDocument,
7302
+ readOnly: isReadOnly,
7303
+ onChange: commit,
7304
+ onPreviewSelection
7305
+ }
7306
+ )) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
7307
+ import_react5.default,
6302
7308
  {
6303
7309
  height: editorHeight,
6304
7310
  language: "xml",
@@ -6359,6 +7365,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6359
7365
  SSML_HOVER_COPY,
6360
7366
  SSML_INSERTIONS,
6361
7367
  SsmlEditor,
7368
+ VisualSsmlEditor,
6362
7369
  createSsmlEditorInsertionDefinition,
6363
7370
  registerSsmlCodeLens,
6364
7371
  updateTagAttribute