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.mjs CHANGED
@@ -43,12 +43,13 @@ import {
43
43
  resolveExpressAsStyles,
44
44
  updateEditableText,
45
45
  updateTagAttribute,
46
+ validateAzureSsml,
46
47
  validateSsml
47
- } from "./chunk-MWSDFGXW.mjs";
48
+ } from "./chunk-JNJVTEL6.mjs";
48
49
  import "./chunk-6S5ODO6A.mjs";
49
50
 
50
51
  // packages/ssml-editor-react/src/SsmlEditor.tsx
51
- import { Fragment, forwardRef, useImperativeHandle } from "react";
52
+ import { Fragment, forwardRef, useEffect as useEffect3, useImperativeHandle, useState as useState3 } from "react";
52
53
  import Editor from "@monaco-editor/react";
53
54
 
54
55
  // packages/ssml-editor-react/src/buttonVisibility.ts
@@ -141,6 +142,14 @@ var editorStyles = {
141
142
  display: "grid",
142
143
  gap: "0.5rem"
143
144
  },
145
+ visual: {
146
+ display: "grid",
147
+ gap: "0.75rem",
148
+ minHeight: "8rem",
149
+ padding: "0.75rem",
150
+ border: "1px solid var(--ssml-editor-control-border)",
151
+ borderRadius: "0.25rem"
152
+ },
144
153
  toolbarIconOnly: {
145
154
  justifyContent: "center",
146
155
  minWidth: "2.25rem",
@@ -1799,6 +1808,140 @@ function useSsmlMonaco({
1799
1808
  return { editorRef, onMount };
1800
1809
  }
1801
1810
 
1811
+ // packages/ssml-editor-react/src/components/VisualSsmlEditor.tsx
1812
+ import { useMemo, useState as useState2 } from "react";
1813
+ function isElement(node) {
1814
+ return typeof node !== "string" && node.type !== "text";
1815
+ }
1816
+ function elementLabel(element) {
1817
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
1818
+ }
1819
+ function collectTextLeaves(nodes, path = [], ancestors = []) {
1820
+ return nodes.flatMap((node, index) => {
1821
+ const currentPath = [...path, index];
1822
+ if (typeof node === "string") return [{ ancestors, path: currentPath, value: node }];
1823
+ if (node.type === "text") return [{ ancestors, path: currentPath, value: node.value }];
1824
+ return collectTextLeaves(node.children ?? [], currentPath, [...ancestors, elementLabel(node)]);
1825
+ });
1826
+ }
1827
+ function updateNodesAtPath(nodes, path, update) {
1828
+ if (path.length === 0) return nodes;
1829
+ const [index, ...rest] = path;
1830
+ return nodes.flatMap((node, nodeIndex) => {
1831
+ if (nodeIndex !== index) return [node];
1832
+ if (rest.length === 0) {
1833
+ const next = update(node);
1834
+ return Array.isArray(next) ? next : [next];
1835
+ }
1836
+ if (!isElement(node)) return [node];
1837
+ return [{ ...node, children: updateNodesAtPath(node.children ?? [], rest, update) }];
1838
+ });
1839
+ }
1840
+ function updateTextAtPath(document2, path, value) {
1841
+ return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
1842
+ }
1843
+ function wrapTextAtPath(document2, path, start, end, tag, attributes) {
1844
+ return {
1845
+ ...document2,
1846
+ children: updateNodesAtPath(document2.children ?? [], path, (node) => {
1847
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
1848
+ if (!value || start === end) return node;
1849
+ if (tag === "break") {
1850
+ return [
1851
+ value.slice(0, start),
1852
+ { type: tag, attributes, children: [] },
1853
+ value.slice(start)
1854
+ ].filter((part) => typeof part === "string" ? part.length > 0 : true);
1855
+ }
1856
+ const selected = value.slice(start, end);
1857
+ const wrapped = { type: tag, attributes, children: [selected] };
1858
+ return [value.slice(0, start), wrapped, value.slice(end)].filter(
1859
+ (part) => typeof part === "string" ? part.length > 0 : true
1860
+ );
1861
+ })
1862
+ };
1863
+ }
1864
+ function TreeNode({
1865
+ node,
1866
+ path,
1867
+ selectedPath,
1868
+ onSelect
1869
+ }) {
1870
+ if (!isElement(node)) return null;
1871
+ const name = elementLabel(node);
1872
+ const isSelected = selectedPath?.join(".") === path.join(".");
1873
+ 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(
1874
+ TreeNode,
1875
+ {
1876
+ key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
1877
+ node: child,
1878
+ path: [...path, index],
1879
+ selectedPath,
1880
+ onSelect
1881
+ }
1882
+ ))));
1883
+ }
1884
+ function VisualSsmlEditor({
1885
+ document: document2,
1886
+ readOnly = false,
1887
+ onChange,
1888
+ onPreviewSelection
1889
+ }) {
1890
+ const [selectedPath, setSelectedPath] = useState2(null);
1891
+ const [selection, setSelection] = useState2({ start: 0, end: 0 });
1892
+ const textLeaves = useMemo(() => collectTextLeaves(document2.children ?? []), [document2]);
1893
+ const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
1894
+ const diagnostics = useMemo(() => validateAzureSsml(buildSsml(document2)), [document2]);
1895
+ const commit = (nextDocument) => onChange?.(nextDocument);
1896
+ const applyWrapper = (tag, attributes) => {
1897
+ if (readOnly || !selectedLeaf) return;
1898
+ const start = selection.start === selection.end ? 0 : selection.start;
1899
+ const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
1900
+ commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
1901
+ };
1902
+ 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(
1903
+ TreeNode,
1904
+ {
1905
+ key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
1906
+ node,
1907
+ path: [index],
1908
+ selectedPath,
1909
+ onSelect: setSelectedPath
1910
+ }
1911
+ )))), /* @__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(
1912
+ "textarea",
1913
+ {
1914
+ value: selectedLeaf.value,
1915
+ readOnly,
1916
+ onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
1917
+ onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
1918
+ }
1919
+ )), /* @__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(
1920
+ "button",
1921
+ {
1922
+ type: "button",
1923
+ disabled: readOnly,
1924
+ onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
1925
+ },
1926
+ "Emotion"
1927
+ ), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
1928
+ "button",
1929
+ {
1930
+ type: "button",
1931
+ disabled: readOnly,
1932
+ onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
1933
+ },
1934
+ "Pronunciation"
1935
+ ), onPreviewSelection && /* @__PURE__ */ React.createElement(
1936
+ "button",
1937
+ {
1938
+ type: "button",
1939
+ onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
1940
+ },
1941
+ "Preview selection"
1942
+ ))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));
1943
+ }
1944
+
1802
1945
  // packages/ssml-editor-react/src/SsmlEditor.tsx
1803
1946
  var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
1804
1947
  var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
@@ -2265,6 +2408,60 @@ var STYLE_CSS = `
2265
2408
  > .ssml-editor-help-settings-summary::before {
2266
2409
  content: "\u25BE";
2267
2410
  }
2411
+ [data-ssml-editor] .ssml-editor-visual-layout {
2412
+ display: grid;
2413
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
2414
+ gap: 1rem;
2415
+ }
2416
+ [data-ssml-editor] .ssml-editor-visual-tree,
2417
+ [data-ssml-editor] .ssml-editor-visual-form {
2418
+ display: grid;
2419
+ gap: 0.5rem;
2420
+ align-content: start;
2421
+ }
2422
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
2423
+ margin: 0;
2424
+ padding-left: 1.25rem;
2425
+ }
2426
+ [data-ssml-editor] .ssml-editor-visual-tree button,
2427
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb button,
2428
+ [data-ssml-editor] .ssml-editor-visual-actions button {
2429
+ padding: 0.35rem 0.5rem;
2430
+ border: 1px solid var(--ssml-editor-control-border);
2431
+ border-radius: 0.25rem;
2432
+ color: var(--ssml-editor-color);
2433
+ background: var(--ssml-editor-control-bg);
2434
+ cursor: pointer;
2435
+ }
2436
+ [data-ssml-editor] .ssml-editor-visual-tree button[aria-current] {
2437
+ border-color: var(--ssml-editor-active-border);
2438
+ background: var(--ssml-editor-active-bg);
2439
+ }
2440
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb,
2441
+ [data-ssml-editor] .ssml-editor-visual-actions {
2442
+ display: flex;
2443
+ flex-wrap: wrap;
2444
+ gap: 0.4rem;
2445
+ align-items: center;
2446
+ }
2447
+ [data-ssml-editor] .ssml-editor-visual-form textarea {
2448
+ box-sizing: border-box;
2449
+ width: 100%;
2450
+ min-height: 6rem;
2451
+ padding: 0.5rem;
2452
+ color: var(--ssml-editor-color);
2453
+ background: var(--ssml-editor-control-bg);
2454
+ border: 1px solid var(--ssml-editor-control-border);
2455
+ border-radius: 0.25rem;
2456
+ font: inherit;
2457
+ }
2458
+ [data-ssml-editor] .ssml-editor-visual-errors {
2459
+ padding: 0.5rem;
2460
+ color: var(--ssml-editor-error);
2461
+ background: var(--ssml-editor-error-bg);
2462
+ border: 1px solid var(--ssml-editor-error);
2463
+ border-radius: 0.25rem;
2464
+ }
2268
2465
  `.trim();
2269
2466
  function injectEditorTheme() {
2270
2467
  if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
@@ -2334,8 +2531,11 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
2334
2531
  toolbarStyle,
2335
2532
  displayClassName,
2336
2533
  displayStyle,
2337
- loadingFallback
2534
+ loadingFallback,
2535
+ editMode: editModeProp = "code"
2338
2536
  }, ref) {
2537
+ const [editMode, setEditMode] = useState3(editModeProp);
2538
+ useEffect3(() => setEditMode(editModeProp), [editModeProp]);
2339
2539
  const resolvedEditorOptions = {
2340
2540
  ...settings,
2341
2541
  ...editorOptions,
@@ -2413,6 +2613,7 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
2413
2613
  );
2414
2614
  const {
2415
2615
  editorRef,
2616
+ draftDocument,
2416
2617
  text,
2417
2618
  selectionOverlay,
2418
2619
  activeTags,
@@ -2424,6 +2625,7 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
2424
2625
  syntaxError,
2425
2626
  setSyntaxError,
2426
2627
  helpPanelId,
2628
+ commit,
2427
2629
  handleTextChange,
2428
2630
  handleInsert,
2429
2631
  handleInsertBreak,
@@ -2683,10 +2885,39 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
2683
2885
  "aria-label": copy.toolbarAriaLabel,
2684
2886
  "data-ssml-editor-toolbar-actions": ""
2685
2887
  },
2686
- renderToolbarItems()
2888
+ renderToolbarItems(),
2889
+ /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }),
2890
+ /* @__PURE__ */ React.createElement(
2891
+ "button",
2892
+ {
2893
+ type: "button",
2894
+ style: editMode === "visual" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
2895
+ "aria-pressed": editMode === "visual",
2896
+ onClick: () => setEditMode("visual")
2897
+ },
2898
+ "Visual"
2899
+ ),
2900
+ /* @__PURE__ */ React.createElement(
2901
+ "button",
2902
+ {
2903
+ type: "button",
2904
+ style: editMode === "code" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
2905
+ "aria-pressed": editMode === "code",
2906
+ onClick: () => setEditMode("code")
2907
+ },
2908
+ "Code"
2909
+ )
2687
2910
  )
2688
2911
  ),
2689
- /* @__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(
2912
+ /* @__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(
2913
+ VisualSsmlEditor,
2914
+ {
2915
+ document: draftDocument,
2916
+ readOnly: isReadOnly,
2917
+ onChange: commit,
2918
+ onPreviewSelection
2919
+ }
2920
+ )) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
2690
2921
  Editor,
2691
2922
  {
2692
2923
  height: editorHeight,
@@ -2747,6 +2978,7 @@ export {
2747
2978
  SSML_HOVER_COPY,
2748
2979
  SSML_INSERTIONS,
2749
2980
  SsmlEditor,
2981
+ VisualSsmlEditor,
2750
2982
  createSsmlEditorInsertionDefinition,
2751
2983
  registerSsmlCodeLens,
2752
2984
  updateTagAttribute