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/elements.mjs CHANGED
@@ -34,8 +34,9 @@ import {
34
34
  parseSsml,
35
35
  registerSsmlCompletionProvider,
36
36
  resolveExpressAsStyles,
37
- updateEditableText
38
- } from "./chunk-MWSDFGXW.mjs";
37
+ updateEditableText,
38
+ validateAzureSsml
39
+ } from "./chunk-JNJVTEL6.mjs";
39
40
  import "./chunk-6S5ODO6A.mjs";
40
41
 
41
42
  // packages/ssml-editor-react/src/ssmlInsertions.ts
@@ -492,6 +493,60 @@ section[data-ssml-editor] {
492
493
  border-radius: 0.25rem;
493
494
  overflow: visible;
494
495
  }
496
+ [data-ssml-editor] .ssml-editor-visual {
497
+ display: grid;
498
+ gap: 0.75rem;
499
+ min-height: 8rem;
500
+ padding: 0.75rem;
501
+ border: 1px solid var(--ssml-editor-control-border);
502
+ border-radius: 0.25rem;
503
+ }
504
+ [data-ssml-editor] .ssml-editor-visual-layout {
505
+ display: grid;
506
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
507
+ gap: 1rem;
508
+ }
509
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
510
+ margin: 0;
511
+ padding-left: 1.25rem;
512
+ }
513
+ [data-ssml-editor] .ssml-editor-visual button {
514
+ padding: 0.35rem 0.5rem;
515
+ border: 1px solid var(--ssml-editor-control-border);
516
+ border-radius: 0.25rem;
517
+ color: var(--ssml-editor-color);
518
+ background: var(--ssml-editor-control-bg);
519
+ cursor: pointer;
520
+ }
521
+ [data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
522
+ border-color: var(--ssml-editor-active-border);
523
+ background: var(--ssml-editor-active-bg);
524
+ }
525
+ [data-ssml-editor] .ssml-editor-visual textarea {
526
+ box-sizing: border-box;
527
+ width: 100%;
528
+ min-height: 6rem;
529
+ padding: 0.5rem;
530
+ color: var(--ssml-editor-color);
531
+ background: var(--ssml-editor-control-bg);
532
+ border: 1px solid var(--ssml-editor-control-border);
533
+ border-radius: 0.25rem;
534
+ font: inherit;
535
+ }
536
+ [data-ssml-editor] .ssml-editor-visual-actions,
537
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb {
538
+ display: flex;
539
+ flex-wrap: wrap;
540
+ gap: 0.4rem;
541
+ align-items: center;
542
+ }
543
+ [data-ssml-editor] .ssml-editor-visual-errors {
544
+ padding: 0.5rem;
545
+ color: #b91c1c;
546
+ background: #fef2f2;
547
+ border: 1px solid #b91c1c;
548
+ border-radius: 0.25rem;
549
+ }
495
550
  `.trim();
496
551
  function injectStyles() {
497
552
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
@@ -505,6 +560,55 @@ function injectStyles() {
505
560
  function isDarkTheme(theme) {
506
561
  return theme === "vs-dark" || theme.toLowerCase().includes("dark");
507
562
  }
563
+ function isElement(node) {
564
+ return typeof node !== "string" && node.type !== "text";
565
+ }
566
+ function visualElementName(element) {
567
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
568
+ }
569
+ function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
570
+ return nodes.flatMap((node, index) => {
571
+ const currentPath = [...path, index];
572
+ if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
573
+ if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
574
+ return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
575
+ });
576
+ }
577
+ function updateVisualNodes(nodes, path, update) {
578
+ if (path.length === 0) return nodes;
579
+ const [index, ...rest] = path;
580
+ return nodes.flatMap((node, nodeIndex) => {
581
+ if (nodeIndex !== index) return [node];
582
+ if (rest.length === 0) {
583
+ const next = update(node);
584
+ return Array.isArray(next) ? next : [next];
585
+ }
586
+ if (!isElement(node)) return [node];
587
+ return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
588
+ });
589
+ }
590
+ function updateVisualText(document2, path, value) {
591
+ return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
592
+ }
593
+ function wrapVisualText(document2, path, start, end, type, attributes) {
594
+ return {
595
+ ...document2,
596
+ children: updateVisualNodes(document2.children ?? [], path, (node) => {
597
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
598
+ if (!value || start === end) return node;
599
+ if (type === "break") {
600
+ return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
601
+ (part) => typeof part === "string" ? part.length > 0 : true
602
+ );
603
+ }
604
+ const selected = value.slice(start, end);
605
+ const wrapper = { type, attributes, children: [selected] };
606
+ return [value.slice(0, start), wrapper, value.slice(end)].filter(
607
+ (part) => typeof part === "string" ? part.length > 0 : true
608
+ );
609
+ })
610
+ };
611
+ }
508
612
  function getMenuPosition(trigger, menu) {
509
613
  const bounds = trigger.getBoundingClientRect();
510
614
  const margin = 8;
@@ -526,6 +630,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
526
630
  this.toolbarActions = null;
527
631
  this.display = null;
528
632
  this.editorContainer = null;
633
+ this.visualContainer = null;
529
634
  this.helpPanel = null;
530
635
  this.openMenu = null;
531
636
  this.openMenuTrigger = null;
@@ -539,6 +644,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
539
644
  this.documentState = null;
540
645
  this.decorationsVisible = false;
541
646
  this.helpOpen = false;
647
+ this.visualSelectedPath = null;
648
+ this.visualSelection = { start: 0, end: 0 };
542
649
  this.handleDocumentPointerDown = (event) => {
543
650
  const target = event.target;
544
651
  if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
@@ -580,6 +687,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
580
687
  set locale(locale) {
581
688
  this.setAttribute("locale", locale);
582
689
  }
690
+ get editMode() {
691
+ return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
692
+ }
693
+ set editMode(mode) {
694
+ this.setAttribute("edit-mode", mode);
695
+ }
583
696
  prepareDocument(value) {
584
697
  try {
585
698
  this.documentState = parseSsml(value);
@@ -622,6 +735,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
622
735
  }
623
736
  }
624
737
  }
738
+ this.renderVisualEditor();
625
739
  return;
626
740
  }
627
741
  if (name === "theme" && this.monaco) {
@@ -639,6 +753,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
639
753
  this.renderHelp();
640
754
  return;
641
755
  }
756
+ if (name === "edit-mode") {
757
+ this.updateEditMode();
758
+ }
642
759
  if (name === "show-decorations") {
643
760
  this.decorationsVisible = newValue !== null;
644
761
  this.updateDecorations();
@@ -663,7 +780,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
663
780
  display.dataset.ssmlEditorDisplay = "";
664
781
  const editorContainer = document.createElement("div");
665
782
  editorContainer.className = "ssml-editor-editor";
666
- display.append(editorContainer);
783
+ const visualContainer = document.createElement("div");
784
+ visualContainer.className = "ssml-editor-visual";
785
+ display.append(editorContainer, visualContainer);
667
786
  root.append(toolbar, display);
668
787
  this.replaceChildren(root);
669
788
  this.root = root;
@@ -671,8 +790,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
671
790
  this.toolbarActions = toolbarActions;
672
791
  this.display = display;
673
792
  this.editorContainer = editorContainer;
793
+ this.visualContainer = visualContainer;
674
794
  this.renderToolbar();
675
795
  this.renderHelp();
796
+ this.updateEditMode();
797
+ this.renderVisualEditor();
676
798
  }
677
799
  renderToolbar() {
678
800
  const toolbar = this.toolbar;
@@ -716,10 +838,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
716
838
  for (const id of toolbarIds) {
717
839
  const group = groupByButtonId.get(id);
718
840
  if (previousGroup !== void 0 && group !== previousGroup) {
719
- const separator = document.createElement("span");
720
- separator.className = "ssml-editor-toolbar-separator";
721
- separator.setAttribute("aria-hidden", "true");
722
- toolbarActions.append(separator);
841
+ const separator2 = document.createElement("span");
842
+ separator2.className = "ssml-editor-toolbar-separator";
843
+ separator2.setAttribute("aria-hidden", "true");
844
+ toolbarActions.append(separator2);
723
845
  }
724
846
  previousGroup = group;
725
847
  const insertion = insertionById.get(id);
@@ -731,8 +853,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
731
853
  toolbarActions.append(this.createActionButton(id));
732
854
  }
733
855
  }
856
+ const separator = document.createElement("span");
857
+ separator.className = "ssml-editor-toolbar-separator";
858
+ separator.setAttribute("aria-hidden", "true");
859
+ toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
734
860
  this.updateActiveButtons();
735
861
  }
862
+ createModeButton(mode, label) {
863
+ const button = document.createElement("button");
864
+ button.type = "button";
865
+ button.className = "ssml-editor-toolbar-button";
866
+ button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
867
+ button.setAttribute("aria-pressed", String(this.editMode === mode));
868
+ button.textContent = label;
869
+ button.addEventListener("click", () => {
870
+ this.editMode = mode;
871
+ });
872
+ return button;
873
+ }
736
874
  createActionButton(id) {
737
875
  const copy = EDITOR_COPY[this.locale];
738
876
  const labels = {
@@ -1112,6 +1250,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
1112
1250
  this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
1113
1251
  }
1114
1252
  }
1253
+ updateEditMode() {
1254
+ if (this.editorContainer && this.visualContainer) {
1255
+ const visual = this.editMode === "visual";
1256
+ this.editorContainer.hidden = visual;
1257
+ this.visualContainer.hidden = !visual;
1258
+ }
1259
+ for (const mode of ["visual", "code"]) {
1260
+ const button = this.toolbarActions?.querySelector(
1261
+ `[data-ssml-editor-button="edit-mode-${mode}"]`
1262
+ );
1263
+ button?.setAttribute("aria-pressed", String(this.editMode === mode));
1264
+ button?.toggleAttribute("data-active", this.editMode === mode);
1265
+ }
1266
+ this.renderVisualEditor();
1267
+ }
1268
+ renderVisualEditor() {
1269
+ const container = this.visualContainer;
1270
+ if (!container) return;
1271
+ container.replaceChildren();
1272
+ if (!this.documentState) {
1273
+ const error = document.createElement("p");
1274
+ error.className = "ssml-editor-visual-errors";
1275
+ error.textContent = "SSML syntax must be valid before visual editing is available.";
1276
+ container.append(error);
1277
+ return;
1278
+ }
1279
+ const documentState = this.documentState;
1280
+ const leaves = collectVisualTextLeaves(documentState.children ?? []);
1281
+ const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
1282
+ const breadcrumb = document.createElement("div");
1283
+ breadcrumb.className = "ssml-editor-visual-breadcrumb";
1284
+ breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
1285
+ const clear = document.createElement("button");
1286
+ clear.type = "button";
1287
+ clear.textContent = "Clear parent";
1288
+ clear.disabled = !this.visualSelectedPath;
1289
+ clear.addEventListener("click", () => {
1290
+ this.visualSelectedPath = null;
1291
+ this.renderVisualEditor();
1292
+ });
1293
+ breadcrumb.append(clear);
1294
+ container.append(breadcrumb);
1295
+ const diagnostics = validateAzureSsml(buildSsml(documentState));
1296
+ if (diagnostics.length > 0) {
1297
+ const errors = document.createElement("div");
1298
+ errors.className = "ssml-editor-visual-errors";
1299
+ errors.setAttribute("role", "alert");
1300
+ for (const diagnostic of diagnostics) {
1301
+ const message = document.createElement("div");
1302
+ message.textContent = diagnostic.message;
1303
+ errors.append(message);
1304
+ }
1305
+ container.append(errors);
1306
+ }
1307
+ const layout = document.createElement("div");
1308
+ layout.className = "ssml-editor-visual-layout";
1309
+ const tree = document.createElement("nav");
1310
+ tree.className = "ssml-editor-visual-tree";
1311
+ tree.setAttribute("aria-label", "SSML structure tree");
1312
+ tree.append(document.createTextNode("Structure"));
1313
+ const treeList = document.createElement("ul");
1314
+ const renderTree = (nodes, parent, parentPath = []) => {
1315
+ nodes.forEach((node, index) => {
1316
+ if (!isElement(node)) return;
1317
+ const path = [...parentPath, index];
1318
+ const item = document.createElement("li");
1319
+ const button = document.createElement("button");
1320
+ button.type = "button";
1321
+ button.textContent = `<${visualElementName(node)}>`;
1322
+ button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
1323
+ button.addEventListener("click", () => {
1324
+ this.visualSelectedPath = path;
1325
+ this.renderVisualEditor();
1326
+ });
1327
+ item.append(button);
1328
+ if ((node.children ?? []).some(isElement)) {
1329
+ const childList = document.createElement("ul");
1330
+ renderTree(node.children ?? [], childList, path);
1331
+ item.append(childList);
1332
+ }
1333
+ parent.append(item);
1334
+ });
1335
+ };
1336
+ renderTree(documentState.children ?? [], treeList);
1337
+ tree.append(treeList);
1338
+ layout.append(tree);
1339
+ const form = document.createElement("div");
1340
+ form.className = "ssml-editor-visual-form";
1341
+ if (selectedLeaf) {
1342
+ const label = document.createElement("label");
1343
+ label.append(document.createTextNode("Text"));
1344
+ const textarea = document.createElement("textarea");
1345
+ textarea.value = selectedLeaf.value;
1346
+ textarea.readOnly = this.readonly;
1347
+ textarea.addEventListener("select", () => {
1348
+ this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
1349
+ });
1350
+ textarea.addEventListener("input", () => {
1351
+ if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
1352
+ });
1353
+ label.append(textarea);
1354
+ form.append(label);
1355
+ const actions = document.createElement("div");
1356
+ actions.className = "ssml-editor-visual-actions";
1357
+ const wrappers = [
1358
+ ["Rate", "prosody", { rate: "slow" }],
1359
+ ["Pitch", "prosody", { pitch: "high" }],
1360
+ ["Emotion", "mstts:express-as", { style: "cheerful" }],
1361
+ ["Pause", "break", { time: "500ms" }],
1362
+ ["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
1363
+ ];
1364
+ for (const [labelText, type, attributes] of wrappers) {
1365
+ const button = document.createElement("button");
1366
+ button.type = "button";
1367
+ button.textContent = labelText;
1368
+ button.disabled = this.readonly;
1369
+ button.addEventListener("click", () => {
1370
+ const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
1371
+ const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
1372
+ this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
1373
+ this.renderVisualEditor();
1374
+ });
1375
+ actions.append(button);
1376
+ }
1377
+ const preview = document.createElement("button");
1378
+ preview.type = "button";
1379
+ preview.textContent = "Preview selection";
1380
+ preview.addEventListener("click", () => {
1381
+ this.dispatchEvent(
1382
+ new CustomEvent("preview-selection", {
1383
+ detail: { ssml: buildSsml({ ...documentState, children: [selectedLeaf.value] }) },
1384
+ bubbles: true,
1385
+ composed: true
1386
+ })
1387
+ );
1388
+ });
1389
+ actions.append(preview);
1390
+ form.append(actions);
1391
+ } else {
1392
+ form.textContent = "Select an element or text node to edit it.";
1393
+ }
1394
+ layout.append(form);
1395
+ container.append(layout);
1396
+ }
1115
1397
  updateActiveButtons() {
1116
1398
  const editor = this.editor;
1117
1399
  const model = this.model;
@@ -1139,6 +1421,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
1139
1421
  return;
1140
1422
  }
1141
1423
  const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
1424
+ this.renderVisualEditor();
1142
1425
  const editor = monacoModule.editor.create(container, {
1143
1426
  model,
1144
1427
  theme: this.theme,
@@ -1237,7 +1520,8 @@ SsmlEditorElement.observedAttributes = [
1237
1520
  "locale",
1238
1521
  "show-toolbar",
1239
1522
  "show-toolbar-labels",
1240
- "show-decorations"
1523
+ "show-decorations",
1524
+ "edit-mode"
1241
1525
  ];
1242
1526
 
1243
1527
  // packages/ssml-editor-elements/src/index.ts