@ssml-builder-js/ssml-editor-elements 2.8.0 → 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/index.js CHANGED
@@ -100,6 +100,7 @@ var INTRINSICALLY_EMPTY_ELEMENTS = /* @__PURE__ */ new Set([
100
100
  "lexicon",
101
101
  "mark",
102
102
  "mstts:silence",
103
+ "mstts:audioduration",
103
104
  "mstts:viseme",
104
105
  "silence",
105
106
  "viseme"
@@ -758,6 +759,8 @@ function updateEditableText(document2, value) {
758
759
  // ../ssml-editor-react/src/constants/ssmlPresets.ts
759
760
  var ssmlPresets_exports = {};
760
761
  __export(ssmlPresets_exports, {
762
+ AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
763
+ AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
761
764
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
762
765
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
763
766
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -790,6 +793,103 @@ __export(ssmlPresets_exports, {
790
793
  getExpressAsStyleCategory: () => getExpressAsStyleCategory,
791
794
  resolveExpressAsStyles: () => resolveExpressAsStyles
792
795
  });
796
+
797
+ // ../ssml-editor-react/src/constants/azureVoiceStyleMap.generated.ts
798
+ var AZURE_VOICE_STYLE_MAP = {
799
+ "de-DE-ConradNeural": ["cheerful", "sad"],
800
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
801
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
802
+ "en-US-GuyNeural": [
803
+ "angry",
804
+ "cheerful",
805
+ "excited",
806
+ "friendly",
807
+ "hopeful",
808
+ "newscast",
809
+ "sad",
810
+ "shouting",
811
+ "terrified",
812
+ "unfriendly",
813
+ "whispering"
814
+ ],
815
+ "en-US-JennyMultilingualNeural": [
816
+ "cheerful",
817
+ "empathetic",
818
+ "excited",
819
+ "friendly",
820
+ "hopeful",
821
+ "sad",
822
+ "shouting",
823
+ "terrified",
824
+ "unfriendly",
825
+ "whispering"
826
+ ],
827
+ "en-US-JennyNeural": [
828
+ "assistant",
829
+ "chat",
830
+ "customerservice",
831
+ "newscast",
832
+ "cheerful",
833
+ "empathetic",
834
+ "excited",
835
+ "friendly",
836
+ "hopeful",
837
+ "sad",
838
+ "shouting",
839
+ "terrified",
840
+ "unfriendly",
841
+ "whispering"
842
+ ],
843
+ "es-ES-ElviraNeural": [],
844
+ "fil-PH-AngeloNeural": [],
845
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
846
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
847
+ "id-ID-GadisNeural": [],
848
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
849
+ "ja-JP-KeitaNeural": ["chat"],
850
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
851
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
852
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
853
+ "ms-MY-YasminNeural": [],
854
+ "pt-BR-FranciscaNeural": ["calm"],
855
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
856
+ "th-TH-PremwadeeNeural": [],
857
+ "vi-VN-HoaiMyNeural": [],
858
+ "zh-CN-XiaoxiaoNeural": [
859
+ "assistant",
860
+ "chat",
861
+ "customerservice",
862
+ "newscast",
863
+ "cheerful",
864
+ "empathetic",
865
+ "excited",
866
+ "friendly",
867
+ "hopeful",
868
+ "sad",
869
+ "terrified",
870
+ "whispering",
871
+ "poetry-reading",
872
+ "sports_commentary",
873
+ "sports_commentary_excited",
874
+ "story"
875
+ ],
876
+ "zh-CN-YunxiNeural": [
877
+ "narration-relaxed",
878
+ "embarrassed",
879
+ "fearful",
880
+ "sad",
881
+ "disgruntled",
882
+ "serious",
883
+ "angry",
884
+ "depressed",
885
+ "chat",
886
+ "cheerful",
887
+ "assistant"
888
+ ],
889
+ "zh-TW-HsiaoChenNeural": []
890
+ };
891
+
892
+ // ../ssml-editor-react/src/constants/ssmlPresets.ts
793
893
  var SSML_PRESETS = [
794
894
  {
795
895
  id: "basic",
@@ -889,7 +989,7 @@ function getExpressAsStyleCategory(style) {
889
989
  }
890
990
  return "other";
891
991
  }
892
- var VOICE_STYLE_MAP = {
992
+ var LEGACY_VOICE_STYLE_MAP = {
893
993
  "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
894
994
  "ja-JP-KeitaNeural": ["chat"],
895
995
  "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
@@ -975,6 +1075,7 @@ var VOICE_STYLE_MAP = {
975
1075
  "de-DE-ConradNeural": ["cheerful", "sad"],
976
1076
  "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
977
1077
  };
1078
+ var VOICE_STYLE_MAP = Object.keys(AZURE_VOICE_STYLE_MAP).length > 0 ? AZURE_VOICE_STYLE_MAP : LEGACY_VOICE_STYLE_MAP;
978
1079
  var VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map(
979
1080
  Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
980
1081
  );
@@ -1017,6 +1118,7 @@ var SAY_AS_PRESETS = [
1017
1118
  ];
1018
1119
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
1019
1120
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
1121
+ var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
1020
1122
  var SILENCE_TYPE_PRESETS = [
1021
1123
  "Leading",
1022
1124
  "Tailing",
@@ -1025,6 +1127,11 @@ var SILENCE_TYPE_PRESETS = [
1025
1127
  "Semicolon",
1026
1128
  "Enumerationcomma"
1027
1129
  ];
1130
+ var AUDIO_DURATION_DESCRIPTIONS = {
1131
+ "5s": { ja: "5\u79D2", en: "5 seconds" },
1132
+ "10s": { ja: "10\u79D2", en: "10 seconds" },
1133
+ "30s": { ja: "30\u79D2", en: "30 seconds" }
1134
+ };
1028
1135
  var PHONEME_ALPHABET_PRESETS = ["ipa", "sapi", "ups", "x-sampa"];
1029
1136
  var VISEME_TYPE_PRESETS = ["redlips_front", "FacialExpression"];
1030
1137
  var SSML_ATTRIBUTE_PRESETS = {
@@ -1068,6 +1175,14 @@ var SSML_ATTRIBUTE_PRESETS = {
1068
1175
  type: SILENCE_TYPE_PRESETS,
1069
1176
  value: SILENCE_VALUE_PRESETS
1070
1177
  },
1178
+ "mstts:audioduration": {
1179
+ value: AUDIO_DURATION_PRESETS
1180
+ },
1181
+ "mstts:backgroundaudio": {
1182
+ volume: PROSODY_VOLUME_PRESETS,
1183
+ fadein: AUDIO_DURATION_PRESETS,
1184
+ fadeout: AUDIO_DURATION_PRESETS
1185
+ },
1071
1186
  silence: {
1072
1187
  type: SILENCE_TYPE_PRESETS,
1073
1188
  value: SILENCE_VALUE_PRESETS
@@ -1555,6 +1670,28 @@ var SSML_COMPLETION_SNIPPETS = [
1555
1670
  label: "mstts:express-as",
1556
1671
  insertText: `<mstts:express-as style="cheerful">\${1:text}</mstts:express-as>`
1557
1672
  },
1673
+ {
1674
+ label: "mstts:audioduration",
1675
+ insertText: '<mstts:audioduration value="10s" />'
1676
+ },
1677
+ {
1678
+ label: "mstts:dialog",
1679
+ insertText: `<mstts:dialog>
1680
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
1681
+ </mstts:dialog>`
1682
+ },
1683
+ {
1684
+ label: "mstts:turn",
1685
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
1686
+ },
1687
+ {
1688
+ label: "mstts:backgroundaudio",
1689
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
1690
+ },
1691
+ {
1692
+ label: "mstts:ttsembedding",
1693
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
1694
+ },
1558
1695
  {
1559
1696
  label: "sub",
1560
1697
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -1822,6 +1959,13 @@ var SSML_HOVER_COPY = {
1822
1959
  parameters: {
1823
1960
  type: { title: "type", description: "\u30D3\u30BC\u30FC\u30E0\u30A4\u30D9\u30F3\u30C8\u306E\u5F62\u5F0F\u3002" }
1824
1961
  }
1962
+ },
1963
+ "mstts:audioduration": {
1964
+ title: "\u97F3\u58F0\u9577",
1965
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1966
+ parameters: {
1967
+ value: { title: "value", description: "\u76EE\u6A19\u6642\u9593\u3002\u4F8B: `10s`\u3001`5000ms`\u3001`00:00:10`\u3002" }
1968
+ }
1825
1969
  }
1826
1970
  }
1827
1971
  },
@@ -1979,6 +2123,13 @@ var SSML_HOVER_COPY = {
1979
2123
  parameters: {
1980
2124
  type: { title: "type", description: "The viseme event format." }
1981
2125
  }
2126
+ },
2127
+ "mstts:audioduration": {
2128
+ title: "Audio duration",
2129
+ description: "Sets the target duration of synthesized audio.",
2130
+ parameters: {
2131
+ value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." }
2132
+ }
1982
2133
  }
1983
2134
  }
1984
2135
  }
@@ -2275,6 +2426,74 @@ var SSML_TAG_DEFINITIONS = [
2275
2426
  values: VISEME_TYPE_PRESETS2
2276
2427
  }
2277
2428
  ]
2429
+ },
2430
+ {
2431
+ name: "mstts:audioduration",
2432
+ description: "Sets the target duration of synthesized audio.",
2433
+ parameters: [
2434
+ {
2435
+ name: "value",
2436
+ description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`.",
2437
+ example: "10s"
2438
+ }
2439
+ ]
2440
+ },
2441
+ {
2442
+ name: "mstts:dialog",
2443
+ description: "Groups multiple Azure dialog turns that can use different voices.",
2444
+ parameters: []
2445
+ },
2446
+ {
2447
+ name: "mstts:turn",
2448
+ description: "Adds one dialog turn using the required Azure voice name.",
2449
+ parameters: [
2450
+ {
2451
+ name: "voice",
2452
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
2453
+ example: "en-US-JennyNeural"
2454
+ }
2455
+ ]
2456
+ },
2457
+ {
2458
+ name: "mstts:backgroundaudio",
2459
+ description: "Plays background audio while speech is synthesized.",
2460
+ parameters: [
2461
+ {
2462
+ name: "src",
2463
+ description: "An absolute HTTP(S) URL for the background audio file.",
2464
+ example: "https://example.com/music.mp3"
2465
+ },
2466
+ {
2467
+ name: "volume",
2468
+ description: "The background audio volume, for example `-3dB` or `medium`.",
2469
+ example: "-3dB"
2470
+ },
2471
+ {
2472
+ name: "fadein",
2473
+ description: "The fade-in duration, for example `1s`.",
2474
+ example: "1s"
2475
+ },
2476
+ {
2477
+ name: "fadeout",
2478
+ description: "The fade-out duration, for example `500ms`.",
2479
+ example: "500ms"
2480
+ }
2481
+ ]
2482
+ },
2483
+ {
2484
+ name: "mstts:ttsembedding",
2485
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
2486
+ parameters: []
2487
+ },
2488
+ {
2489
+ name: "mstts:embedding",
2490
+ description: "Specifies embedding metadata for custom voice scenarios.",
2491
+ parameters: []
2492
+ },
2493
+ {
2494
+ name: "mstts:voiceconversion",
2495
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
2496
+ parameters: []
2278
2497
  }
2279
2498
  ];
2280
2499
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -2816,13 +3035,34 @@ var SSML_INSERTIONS = [
2816
3035
  suffix: "",
2817
3036
  mode: "insert"
2818
3037
  })
3038
+ },
3039
+ {
3040
+ id: "mstts:audioduration",
3041
+ icon: "\u25F7",
3042
+ tagName: "mstts:audioduration",
3043
+ selfClosing: true,
3044
+ labels: { ja: "\u97F3\u58F0\u9577", en: "Audio duration" },
3045
+ descriptions: {
3046
+ ja: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
3047
+ en: "Sets the target duration of synthesized audio."
3048
+ },
3049
+ parameterDescription: {
3050
+ ja: "\u76EE\u6A19\u6642\u9593\u3092\u9078\u629E\u3057\u307E\u3059\u3002",
3051
+ en: "Selects the target duration."
3052
+ },
3053
+ options: createInsertionOptions(AUDIO_DURATION_PRESETS, AUDIO_DURATION_DESCRIPTIONS),
3054
+ createTemplate: (value) => ({
3055
+ prefix: `<mstts:audioduration value="${value}"/>`,
3056
+ suffix: "",
3057
+ mode: "insert"
3058
+ })
2819
3059
  }
2820
3060
  ];
2821
3061
  var DEFAULT_INSERTION_GROUPS = [
2822
3062
  {
2823
3063
  id: "pauses",
2824
3064
  labels: { ja: "\u9593\u30FB\u7121\u97F3", en: "Pauses" },
2825
- insertionIds: ["break", "mstts:silence"]
3065
+ insertionIds: ["break", "mstts:silence", "mstts:audioduration"]
2826
3066
  },
2827
3067
  {
2828
3068
  id: "prosody",
@@ -3059,6 +3299,60 @@ section[data-ssml-editor] {
3059
3299
  border-radius: 0.25rem;
3060
3300
  overflow: visible;
3061
3301
  }
3302
+ [data-ssml-editor] .ssml-editor-visual {
3303
+ display: grid;
3304
+ gap: 0.75rem;
3305
+ min-height: 8rem;
3306
+ padding: 0.75rem;
3307
+ border: 1px solid var(--ssml-editor-control-border);
3308
+ border-radius: 0.25rem;
3309
+ }
3310
+ [data-ssml-editor] .ssml-editor-visual-layout {
3311
+ display: grid;
3312
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
3313
+ gap: 1rem;
3314
+ }
3315
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
3316
+ margin: 0;
3317
+ padding-left: 1.25rem;
3318
+ }
3319
+ [data-ssml-editor] .ssml-editor-visual button {
3320
+ padding: 0.35rem 0.5rem;
3321
+ border: 1px solid var(--ssml-editor-control-border);
3322
+ border-radius: 0.25rem;
3323
+ color: var(--ssml-editor-color);
3324
+ background: var(--ssml-editor-control-bg);
3325
+ cursor: pointer;
3326
+ }
3327
+ [data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
3328
+ border-color: var(--ssml-editor-active-border);
3329
+ background: var(--ssml-editor-active-bg);
3330
+ }
3331
+ [data-ssml-editor] .ssml-editor-visual textarea {
3332
+ box-sizing: border-box;
3333
+ width: 100%;
3334
+ min-height: 6rem;
3335
+ padding: 0.5rem;
3336
+ color: var(--ssml-editor-color);
3337
+ background: var(--ssml-editor-control-bg);
3338
+ border: 1px solid var(--ssml-editor-control-border);
3339
+ border-radius: 0.25rem;
3340
+ font: inherit;
3341
+ }
3342
+ [data-ssml-editor] .ssml-editor-visual-actions,
3343
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb {
3344
+ display: flex;
3345
+ flex-wrap: wrap;
3346
+ gap: 0.4rem;
3347
+ align-items: center;
3348
+ }
3349
+ [data-ssml-editor] .ssml-editor-visual-errors {
3350
+ padding: 0.5rem;
3351
+ color: #b91c1c;
3352
+ background: #fef2f2;
3353
+ border: 1px solid #b91c1c;
3354
+ border-radius: 0.25rem;
3355
+ }
3062
3356
  `.trim();
3063
3357
  function injectStyles() {
3064
3358
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
@@ -3072,6 +3366,55 @@ function injectStyles() {
3072
3366
  function isDarkTheme(theme) {
3073
3367
  return theme === "vs-dark" || theme.toLowerCase().includes("dark");
3074
3368
  }
3369
+ function isElement(node) {
3370
+ return typeof node !== "string" && node.type !== "text";
3371
+ }
3372
+ function visualElementName(element) {
3373
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
3374
+ }
3375
+ function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
3376
+ return nodes.flatMap((node, index) => {
3377
+ const currentPath = [...path, index];
3378
+ if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
3379
+ if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
3380
+ return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
3381
+ });
3382
+ }
3383
+ function updateVisualNodes(nodes, path, update) {
3384
+ if (path.length === 0) return nodes;
3385
+ const [index, ...rest] = path;
3386
+ return nodes.flatMap((node, nodeIndex) => {
3387
+ if (nodeIndex !== index) return [node];
3388
+ if (rest.length === 0) {
3389
+ const next = update(node);
3390
+ return Array.isArray(next) ? next : [next];
3391
+ }
3392
+ if (!isElement(node)) return [node];
3393
+ return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
3394
+ });
3395
+ }
3396
+ function updateVisualText(document2, path, value) {
3397
+ return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
3398
+ }
3399
+ function wrapVisualText(document2, path, start, end, type, attributes) {
3400
+ return {
3401
+ ...document2,
3402
+ children: updateVisualNodes(document2.children ?? [], path, (node) => {
3403
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
3404
+ if (!value || start === end) return node;
3405
+ if (type === "break") {
3406
+ return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
3407
+ (part) => typeof part === "string" ? part.length > 0 : true
3408
+ );
3409
+ }
3410
+ const selected = value.slice(start, end);
3411
+ const wrapper = { type, attributes, children: [selected] };
3412
+ return [value.slice(0, start), wrapper, value.slice(end)].filter(
3413
+ (part) => typeof part === "string" ? part.length > 0 : true
3414
+ );
3415
+ })
3416
+ };
3417
+ }
3075
3418
  function getMenuPosition(trigger, menu) {
3076
3419
  const bounds = trigger.getBoundingClientRect();
3077
3420
  const margin = 8;
@@ -3093,6 +3436,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3093
3436
  this.toolbarActions = null;
3094
3437
  this.display = null;
3095
3438
  this.editorContainer = null;
3439
+ this.visualContainer = null;
3096
3440
  this.helpPanel = null;
3097
3441
  this.openMenu = null;
3098
3442
  this.openMenuTrigger = null;
@@ -3106,6 +3450,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
3106
3450
  this.documentState = null;
3107
3451
  this.decorationsVisible = false;
3108
3452
  this.helpOpen = false;
3453
+ this.visualSelectedPath = null;
3454
+ this.visualSelection = { start: 0, end: 0 };
3109
3455
  this.handleDocumentPointerDown = (event) => {
3110
3456
  const target = event.target;
3111
3457
  if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
@@ -3147,6 +3493,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
3147
3493
  set locale(locale) {
3148
3494
  this.setAttribute("locale", locale);
3149
3495
  }
3496
+ get editMode() {
3497
+ return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
3498
+ }
3499
+ set editMode(mode) {
3500
+ this.setAttribute("edit-mode", mode);
3501
+ }
3150
3502
  prepareDocument(value) {
3151
3503
  try {
3152
3504
  this.documentState = (0, import_ssml_core2.parseSsml)(value);
@@ -3189,6 +3541,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3189
3541
  }
3190
3542
  }
3191
3543
  }
3544
+ this.renderVisualEditor();
3192
3545
  return;
3193
3546
  }
3194
3547
  if (name === "theme" && this.monaco) {
@@ -3206,6 +3559,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3206
3559
  this.renderHelp();
3207
3560
  return;
3208
3561
  }
3562
+ if (name === "edit-mode") {
3563
+ this.updateEditMode();
3564
+ }
3209
3565
  if (name === "show-decorations") {
3210
3566
  this.decorationsVisible = newValue !== null;
3211
3567
  this.updateDecorations();
@@ -3230,7 +3586,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3230
3586
  display.dataset.ssmlEditorDisplay = "";
3231
3587
  const editorContainer = document.createElement("div");
3232
3588
  editorContainer.className = "ssml-editor-editor";
3233
- display.append(editorContainer);
3589
+ const visualContainer = document.createElement("div");
3590
+ visualContainer.className = "ssml-editor-visual";
3591
+ display.append(editorContainer, visualContainer);
3234
3592
  root.append(toolbar, display);
3235
3593
  this.replaceChildren(root);
3236
3594
  this.root = root;
@@ -3238,8 +3596,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
3238
3596
  this.toolbarActions = toolbarActions;
3239
3597
  this.display = display;
3240
3598
  this.editorContainer = editorContainer;
3599
+ this.visualContainer = visualContainer;
3241
3600
  this.renderToolbar();
3242
3601
  this.renderHelp();
3602
+ this.updateEditMode();
3603
+ this.renderVisualEditor();
3243
3604
  }
3244
3605
  renderToolbar() {
3245
3606
  const toolbar = this.toolbar;
@@ -3283,10 +3644,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
3283
3644
  for (const id of toolbarIds) {
3284
3645
  const group = groupByButtonId.get(id);
3285
3646
  if (previousGroup !== void 0 && group !== previousGroup) {
3286
- const separator = document.createElement("span");
3287
- separator.className = "ssml-editor-toolbar-separator";
3288
- separator.setAttribute("aria-hidden", "true");
3289
- toolbarActions.append(separator);
3647
+ const separator2 = document.createElement("span");
3648
+ separator2.className = "ssml-editor-toolbar-separator";
3649
+ separator2.setAttribute("aria-hidden", "true");
3650
+ toolbarActions.append(separator2);
3290
3651
  }
3291
3652
  previousGroup = group;
3292
3653
  const insertion = insertionById.get(id);
@@ -3298,8 +3659,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
3298
3659
  toolbarActions.append(this.createActionButton(id));
3299
3660
  }
3300
3661
  }
3662
+ const separator = document.createElement("span");
3663
+ separator.className = "ssml-editor-toolbar-separator";
3664
+ separator.setAttribute("aria-hidden", "true");
3665
+ toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
3301
3666
  this.updateActiveButtons();
3302
3667
  }
3668
+ createModeButton(mode, label) {
3669
+ const button = document.createElement("button");
3670
+ button.type = "button";
3671
+ button.className = "ssml-editor-toolbar-button";
3672
+ button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
3673
+ button.setAttribute("aria-pressed", String(this.editMode === mode));
3674
+ button.textContent = label;
3675
+ button.addEventListener("click", () => {
3676
+ this.editMode = mode;
3677
+ });
3678
+ return button;
3679
+ }
3303
3680
  createActionButton(id) {
3304
3681
  const copy = EDITOR_COPY[this.locale];
3305
3682
  const labels = {
@@ -3679,6 +4056,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
3679
4056
  this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
3680
4057
  }
3681
4058
  }
4059
+ updateEditMode() {
4060
+ if (this.editorContainer && this.visualContainer) {
4061
+ const visual = this.editMode === "visual";
4062
+ this.editorContainer.hidden = visual;
4063
+ this.visualContainer.hidden = !visual;
4064
+ }
4065
+ for (const mode of ["visual", "code"]) {
4066
+ const button = this.toolbarActions?.querySelector(
4067
+ `[data-ssml-editor-button="edit-mode-${mode}"]`
4068
+ );
4069
+ button?.setAttribute("aria-pressed", String(this.editMode === mode));
4070
+ button?.toggleAttribute("data-active", this.editMode === mode);
4071
+ }
4072
+ this.renderVisualEditor();
4073
+ }
4074
+ renderVisualEditor() {
4075
+ const container = this.visualContainer;
4076
+ if (!container) return;
4077
+ container.replaceChildren();
4078
+ if (!this.documentState) {
4079
+ const error = document.createElement("p");
4080
+ error.className = "ssml-editor-visual-errors";
4081
+ error.textContent = "SSML syntax must be valid before visual editing is available.";
4082
+ container.append(error);
4083
+ return;
4084
+ }
4085
+ const documentState = this.documentState;
4086
+ const leaves = collectVisualTextLeaves(documentState.children ?? []);
4087
+ const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
4088
+ const breadcrumb = document.createElement("div");
4089
+ breadcrumb.className = "ssml-editor-visual-breadcrumb";
4090
+ breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
4091
+ const clear = document.createElement("button");
4092
+ clear.type = "button";
4093
+ clear.textContent = "Clear parent";
4094
+ clear.disabled = !this.visualSelectedPath;
4095
+ clear.addEventListener("click", () => {
4096
+ this.visualSelectedPath = null;
4097
+ this.renderVisualEditor();
4098
+ });
4099
+ breadcrumb.append(clear);
4100
+ container.append(breadcrumb);
4101
+ const diagnostics = (0, import_ssml_core2.validateAzureSsml)((0, import_ssml_core2.buildSsml)(documentState));
4102
+ if (diagnostics.length > 0) {
4103
+ const errors = document.createElement("div");
4104
+ errors.className = "ssml-editor-visual-errors";
4105
+ errors.setAttribute("role", "alert");
4106
+ for (const diagnostic of diagnostics) {
4107
+ const message = document.createElement("div");
4108
+ message.textContent = diagnostic.message;
4109
+ errors.append(message);
4110
+ }
4111
+ container.append(errors);
4112
+ }
4113
+ const layout = document.createElement("div");
4114
+ layout.className = "ssml-editor-visual-layout";
4115
+ const tree = document.createElement("nav");
4116
+ tree.className = "ssml-editor-visual-tree";
4117
+ tree.setAttribute("aria-label", "SSML structure tree");
4118
+ tree.append(document.createTextNode("Structure"));
4119
+ const treeList = document.createElement("ul");
4120
+ const renderTree = (nodes, parent, parentPath = []) => {
4121
+ nodes.forEach((node, index) => {
4122
+ if (!isElement(node)) return;
4123
+ const path = [...parentPath, index];
4124
+ const item = document.createElement("li");
4125
+ const button = document.createElement("button");
4126
+ button.type = "button";
4127
+ button.textContent = `<${visualElementName(node)}>`;
4128
+ button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
4129
+ button.addEventListener("click", () => {
4130
+ this.visualSelectedPath = path;
4131
+ this.renderVisualEditor();
4132
+ });
4133
+ item.append(button);
4134
+ if ((node.children ?? []).some(isElement)) {
4135
+ const childList = document.createElement("ul");
4136
+ renderTree(node.children ?? [], childList, path);
4137
+ item.append(childList);
4138
+ }
4139
+ parent.append(item);
4140
+ });
4141
+ };
4142
+ renderTree(documentState.children ?? [], treeList);
4143
+ tree.append(treeList);
4144
+ layout.append(tree);
4145
+ const form = document.createElement("div");
4146
+ form.className = "ssml-editor-visual-form";
4147
+ if (selectedLeaf) {
4148
+ const label = document.createElement("label");
4149
+ label.append(document.createTextNode("Text"));
4150
+ const textarea = document.createElement("textarea");
4151
+ textarea.value = selectedLeaf.value;
4152
+ textarea.readOnly = this.readonly;
4153
+ textarea.addEventListener("select", () => {
4154
+ this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
4155
+ });
4156
+ textarea.addEventListener("input", () => {
4157
+ if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
4158
+ });
4159
+ label.append(textarea);
4160
+ form.append(label);
4161
+ const actions = document.createElement("div");
4162
+ actions.className = "ssml-editor-visual-actions";
4163
+ const wrappers = [
4164
+ ["Rate", "prosody", { rate: "slow" }],
4165
+ ["Pitch", "prosody", { pitch: "high" }],
4166
+ ["Emotion", "mstts:express-as", { style: "cheerful" }],
4167
+ ["Pause", "break", { time: "500ms" }],
4168
+ ["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
4169
+ ];
4170
+ for (const [labelText, type, attributes] of wrappers) {
4171
+ const button = document.createElement("button");
4172
+ button.type = "button";
4173
+ button.textContent = labelText;
4174
+ button.disabled = this.readonly;
4175
+ button.addEventListener("click", () => {
4176
+ const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
4177
+ const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
4178
+ this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
4179
+ this.renderVisualEditor();
4180
+ });
4181
+ actions.append(button);
4182
+ }
4183
+ const preview = document.createElement("button");
4184
+ preview.type = "button";
4185
+ preview.textContent = "Preview selection";
4186
+ preview.addEventListener("click", () => {
4187
+ this.dispatchEvent(
4188
+ new CustomEvent("preview-selection", {
4189
+ detail: { ssml: (0, import_ssml_core2.buildSsml)({ ...documentState, children: [selectedLeaf.value] }) },
4190
+ bubbles: true,
4191
+ composed: true
4192
+ })
4193
+ );
4194
+ });
4195
+ actions.append(preview);
4196
+ form.append(actions);
4197
+ } else {
4198
+ form.textContent = "Select an element or text node to edit it.";
4199
+ }
4200
+ layout.append(form);
4201
+ container.append(layout);
4202
+ }
3682
4203
  updateActiveButtons() {
3683
4204
  const editor = this.editor;
3684
4205
  const model = this.model;
@@ -3706,6 +4227,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3706
4227
  return;
3707
4228
  }
3708
4229
  const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
4230
+ this.renderVisualEditor();
3709
4231
  const editor = monacoModule.editor.create(container, {
3710
4232
  model,
3711
4233
  theme: this.theme,
@@ -3804,7 +4326,8 @@ SsmlEditorElement.observedAttributes = [
3804
4326
  "locale",
3805
4327
  "show-toolbar",
3806
4328
  "show-toolbar-labels",
3807
- "show-decorations"
4329
+ "show-decorations",
4330
+ "edit-mode"
3808
4331
  ];
3809
4332
 
3810
4333
  // src/index.ts