@ssml-builder-js/ssml-editor-react 2.8.1 → 2.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssml-builder-js/ssml-editor-react",
3
- "version": "2.8.1",
3
+ "version": "2.10.0",
4
4
  "description": "React-based SSML editor component",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,6 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@ssml-builder-js/ssml-core": "^2.8.1"
55
+ "@ssml-builder-js/ssml-core": "^2.10.0"
56
56
  }
57
57
  }
@@ -1,4 +1,4 @@
1
- import { Fragment, forwardRef, useImperativeHandle } from "react";
1
+ import { Fragment, forwardRef, useEffect, useImperativeHandle, useState } from "react";
2
2
  import type { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  import Editor from "@monaco-editor/react";
4
4
  import type { SsmlDocument } from "@ssml-builder-js/ssml-core";
@@ -40,6 +40,7 @@ import { InsertionPopover } from "./components/popovers/InsertionPopover";
40
40
  import { useSsmlEditorState } from "./hooks/useSsmlEditorState";
41
41
  import { useSsmlMonaco } from "./hooks/useSsmlMonaco";
42
42
  import { editorStyles as styles } from "./styles/editorStyles";
43
+ import { VisualSsmlEditor } from "./components/VisualSsmlEditor";
43
44
  const UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
44
45
  const TIMING_POPOVER_TAGS = new Set(["break", "mstts:silence", "mstts:audioduration"]);
45
46
  const PROSODY_POPOVER_TAGS = new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
@@ -637,6 +638,60 @@ const STYLE_CSS = `
637
638
  > .ssml-editor-help-settings-summary::before {
638
639
  content: "▾";
639
640
  }
641
+ [data-ssml-editor] .ssml-editor-visual-layout {
642
+ display: grid;
643
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
644
+ gap: 1rem;
645
+ }
646
+ [data-ssml-editor] .ssml-editor-visual-tree,
647
+ [data-ssml-editor] .ssml-editor-visual-form {
648
+ display: grid;
649
+ gap: 0.5rem;
650
+ align-content: start;
651
+ }
652
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
653
+ margin: 0;
654
+ padding-left: 1.25rem;
655
+ }
656
+ [data-ssml-editor] .ssml-editor-visual-tree button,
657
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb button,
658
+ [data-ssml-editor] .ssml-editor-visual-actions button {
659
+ padding: 0.35rem 0.5rem;
660
+ border: 1px solid var(--ssml-editor-control-border);
661
+ border-radius: 0.25rem;
662
+ color: var(--ssml-editor-color);
663
+ background: var(--ssml-editor-control-bg);
664
+ cursor: pointer;
665
+ }
666
+ [data-ssml-editor] .ssml-editor-visual-tree button[aria-current] {
667
+ border-color: var(--ssml-editor-active-border);
668
+ background: var(--ssml-editor-active-bg);
669
+ }
670
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb,
671
+ [data-ssml-editor] .ssml-editor-visual-actions {
672
+ display: flex;
673
+ flex-wrap: wrap;
674
+ gap: 0.4rem;
675
+ align-items: center;
676
+ }
677
+ [data-ssml-editor] .ssml-editor-visual-form textarea {
678
+ box-sizing: border-box;
679
+ width: 100%;
680
+ min-height: 6rem;
681
+ padding: 0.5rem;
682
+ color: var(--ssml-editor-color);
683
+ background: var(--ssml-editor-control-bg);
684
+ border: 1px solid var(--ssml-editor-control-border);
685
+ border-radius: 0.25rem;
686
+ font: inherit;
687
+ }
688
+ [data-ssml-editor] .ssml-editor-visual-errors {
689
+ padding: 0.5rem;
690
+ color: var(--ssml-editor-error);
691
+ background: var(--ssml-editor-error-bg);
692
+ border: 1px solid var(--ssml-editor-error);
693
+ border-radius: 0.25rem;
694
+ }
640
695
  `.trim();
641
696
 
642
697
  function injectEditorTheme(): void {
@@ -683,6 +738,8 @@ export interface SsmlEditorProps {
683
738
  minHeight?: string | number;
684
739
  /** Whether the Monaco editor is read-only. */
685
740
  readOnly?: boolean;
741
+ /** Selects the XML source editor or the structured visual editor. */
742
+ editMode?: SsmlEditorEditMode;
686
743
  /** Monaco theme mode. */
687
744
  theme?: SsmlEditorTheme;
688
745
  /** Monaco editor font size. */
@@ -723,6 +780,8 @@ export interface SsmlEditorProps {
723
780
  displayStyle?: CSSProperties;
724
781
  }
725
782
 
783
+ export type SsmlEditorEditMode = "code" | "visual";
784
+
726
785
  export interface SelectionInfo {
727
786
  selectedText: string;
728
787
  characterCount: number;
@@ -814,9 +873,12 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
814
873
  displayClassName,
815
874
  displayStyle,
816
875
  loadingFallback,
876
+ editMode: editModeProp = "code",
817
877
  }: SsmlEditorProps,
818
878
  ref,
819
879
  ): ReactElement {
880
+ const [editMode, setEditMode] = useState<SsmlEditorEditMode>(editModeProp);
881
+ useEffect(() => setEditMode(editModeProp), [editModeProp]);
820
882
  const resolvedEditorOptions: SsmlEditorOptions = {
821
883
  ...settings,
822
884
  ...editorOptions,
@@ -905,6 +967,7 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
905
967
 
906
968
  const {
907
969
  editorRef,
970
+ draftDocument,
908
971
  text,
909
972
  selectionOverlay,
910
973
  activeTags,
@@ -916,6 +979,7 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
916
979
  syntaxError,
917
980
  setSyntaxError,
918
981
  helpPanelId,
982
+ commit,
919
983
  handleTextChange,
920
984
  handleInsert,
921
985
  handleInsertBreak,
@@ -1210,6 +1274,27 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
1210
1274
  data-ssml-editor-toolbar-actions=""
1211
1275
  >
1212
1276
  {renderToolbarItems()}
1277
+ <span style={styles.toolbarSeparator} aria-hidden="true" />
1278
+ <button
1279
+ type="button"
1280
+ style={
1281
+ editMode === "visual" ? { ...toolbarButtonStyle, ...styles.toolbarButtonActive } : toolbarButtonStyle
1282
+ }
1283
+ aria-pressed={editMode === "visual"}
1284
+ onClick={() => setEditMode("visual")}
1285
+ >
1286
+ Visual
1287
+ </button>
1288
+ <button
1289
+ type="button"
1290
+ style={
1291
+ editMode === "code" ? { ...toolbarButtonStyle, ...styles.toolbarButtonActive } : toolbarButtonStyle
1292
+ }
1293
+ aria-pressed={editMode === "code"}
1294
+ onClick={() => setEditMode("code")}
1295
+ >
1296
+ Code
1297
+ </button>
1213
1298
  </div>
1214
1299
  </div>
1215
1300
  )}
@@ -1255,65 +1340,76 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
1255
1340
  )}
1256
1341
  </section>
1257
1342
  )}
1258
- <div style={{ ...styles.editor, minHeight: editorMinHeight }}>
1259
- <Editor
1260
- height={editorHeight}
1261
- language="xml"
1262
- theme={isDarkTheme ? "vs-dark" : "light"}
1263
- options={{
1264
- automaticLayout: resolvedEditorOptions.automaticLayout ?? true,
1265
- autoClosingBrackets: "never",
1266
- fontSize: resolvedEditorOptions.fontSize,
1267
- hover: { enabled: "on" },
1268
- inlayHints: { enabled: decorationsVisible ? "on" : "off" },
1269
- lineNumbers: resolvedEditorOptions.lineNumbers,
1270
- minimap: {
1271
- enabled: resolvedEditorOptions.minimap ?? true,
1272
- },
1273
- readOnly: isReadOnly,
1274
- wordWrap: resolvedEditorOptions.wordWrap,
1275
- }}
1276
- value={text}
1277
- loading={loadingFallback}
1278
- onMount={onMount}
1279
- onChange={(value) => handleTextChange(value ?? "")}
1280
- />
1281
- {selectionOverlay.hasSelection && selectionOverlay.position && (
1282
- <div
1283
- role="toolbar"
1284
- aria-label={copy.selectionActions}
1285
- data-ssml-editor-selection-actions=""
1286
- style={{
1287
- ...styles.selectionActions,
1288
- top: selectionOverlay.position.top,
1289
- left: selectionOverlay.position.left,
1290
- transform:
1291
- selectionOverlay.placement === "above"
1292
- ? "translateY(calc(-100% - 0.5rem))"
1293
- : `translateY(calc(${selectionOverlay.position.height}px + 0.5rem))`,
1343
+ {editMode === "visual" ? (
1344
+ <div style={styles.visual}>
1345
+ <VisualSsmlEditor
1346
+ document={draftDocument}
1347
+ readOnly={isReadOnly}
1348
+ onChange={commit}
1349
+ onPreviewSelection={onPreviewSelection}
1350
+ />
1351
+ </div>
1352
+ ) : (
1353
+ <div style={{ ...styles.editor, minHeight: editorMinHeight }}>
1354
+ <Editor
1355
+ height={editorHeight}
1356
+ language="xml"
1357
+ theme={isDarkTheme ? "vs-dark" : "light"}
1358
+ options={{
1359
+ automaticLayout: resolvedEditorOptions.automaticLayout ?? true,
1360
+ autoClosingBrackets: "never",
1361
+ fontSize: resolvedEditorOptions.fontSize,
1362
+ hover: { enabled: "on" },
1363
+ inlayHints: { enabled: decorationsVisible ? "on" : "off" },
1364
+ lineNumbers: resolvedEditorOptions.lineNumbers,
1365
+ minimap: {
1366
+ enabled: resolvedEditorOptions.minimap ?? true,
1367
+ },
1368
+ readOnly: isReadOnly,
1369
+ wordWrap: resolvedEditorOptions.wordWrap,
1294
1370
  }}
1295
- onMouseDown={(event) => event.preventDefault()}
1296
- >
1297
- <span style={styles.selectionCount} aria-live="polite">
1298
- {selectionOverlay.characterCount}
1299
- {copy.selectionCountSuffix}
1300
- </span>
1301
- <span style={styles.selectionActionsDivider} aria-hidden="true" />
1302
- <button
1303
- type="button"
1304
- style={styles.selectionActionButton}
1305
- title={copy.previewSelectionTitle}
1306
- disabled={!canPreviewSelection}
1307
- onClick={previewSelection}
1371
+ value={text}
1372
+ loading={loadingFallback}
1373
+ onMount={onMount}
1374
+ onChange={(value) => handleTextChange(value ?? "")}
1375
+ />
1376
+ {selectionOverlay.hasSelection && selectionOverlay.position && (
1377
+ <div
1378
+ role="toolbar"
1379
+ aria-label={copy.selectionActions}
1380
+ data-ssml-editor-selection-actions=""
1381
+ style={{
1382
+ ...styles.selectionActions,
1383
+ top: selectionOverlay.position.top,
1384
+ left: selectionOverlay.position.left,
1385
+ transform:
1386
+ selectionOverlay.placement === "above"
1387
+ ? "translateY(calc(-100% - 0.5rem))"
1388
+ : `translateY(calc(${selectionOverlay.position.height}px + 0.5rem))`,
1389
+ }}
1390
+ onMouseDown={(event) => event.preventDefault()}
1308
1391
  >
1309
- <span style={styles.selectionActionIcon} aria-hidden="true">
1310
-
1392
+ <span style={styles.selectionCount} aria-live="polite">
1393
+ {selectionOverlay.characterCount}
1394
+ {copy.selectionCountSuffix}
1311
1395
  </span>
1312
- {copy.previewSelection}
1313
- </button>
1314
- </div>
1315
- )}
1316
- </div>
1396
+ <span style={styles.selectionActionsDivider} aria-hidden="true" />
1397
+ <button
1398
+ type="button"
1399
+ style={styles.selectionActionButton}
1400
+ title={copy.previewSelectionTitle}
1401
+ disabled={!canPreviewSelection}
1402
+ onClick={previewSelection}
1403
+ >
1404
+ <span style={styles.selectionActionIcon} aria-hidden="true">
1405
+
1406
+ </span>
1407
+ {copy.previewSelection}
1408
+ </button>
1409
+ </div>
1410
+ )}
1411
+ </div>
1412
+ )}
1317
1413
  {syntaxError && (
1318
1414
  <p style={styles.error} role="alert">
1319
1415
  {copy.syntaxError}: {syntaxError.message}
@@ -0,0 +1,240 @@
1
+ import { useMemo, useState } from "react";
2
+ import type { ReactElement } from "react";
3
+ import {
4
+ buildSsml,
5
+ validateAzureSsml,
6
+ type SsmlDocument,
7
+ type SsmlElement,
8
+ type SsmlNode,
9
+ } from "@ssml-builder-js/ssml-core";
10
+
11
+ export interface VisualSsmlEditorProps {
12
+ document: SsmlDocument;
13
+ readOnly?: boolean;
14
+ onChange?: (document: SsmlDocument) => void;
15
+ onPreviewSelection?: (ssml: string) => void;
16
+ }
17
+
18
+ interface TextLeaf {
19
+ path: number[];
20
+ value: string;
21
+ ancestors: string[];
22
+ }
23
+
24
+ function isElement(node: SsmlNode): node is SsmlElement {
25
+ return typeof node !== "string" && node.type !== "text";
26
+ }
27
+
28
+ function elementLabel(element: SsmlElement): string {
29
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
30
+ }
31
+
32
+ function collectTextLeaves(nodes: SsmlNode[], path: number[] = [], ancestors: string[] = []): TextLeaf[] {
33
+ return nodes.flatMap((node, index) => {
34
+ const currentPath = [...path, index];
35
+ if (typeof node === "string") return [{ ancestors, path: currentPath, value: node }];
36
+ if (node.type === "text") return [{ ancestors, path: currentPath, value: node.value }];
37
+ return collectTextLeaves(node.children ?? [], currentPath, [...ancestors, elementLabel(node)]);
38
+ });
39
+ }
40
+
41
+ function updateNodesAtPath(
42
+ nodes: SsmlNode[],
43
+ path: number[],
44
+ update: (node: SsmlNode) => SsmlNode | SsmlNode[],
45
+ ): SsmlNode[] {
46
+ if (path.length === 0) return nodes;
47
+ const [index, ...rest] = path;
48
+ return nodes.flatMap((node, nodeIndex) => {
49
+ if (nodeIndex !== index) return [node];
50
+ if (rest.length === 0) {
51
+ const next = update(node);
52
+ return Array.isArray(next) ? next : [next];
53
+ }
54
+ if (!isElement(node)) return [node];
55
+ return [{ ...node, children: updateNodesAtPath(node.children ?? [], rest, update) }];
56
+ });
57
+ }
58
+
59
+ function updateTextAtPath(document: SsmlDocument, path: number[], value: string): SsmlDocument {
60
+ return { ...document, children: updateNodesAtPath(document.children ?? [], path, () => value) };
61
+ }
62
+
63
+ function wrapTextAtPath(
64
+ document: SsmlDocument,
65
+ path: number[],
66
+ start: number,
67
+ end: number,
68
+ tag: SsmlElement["type"],
69
+ attributes: Record<string, string>,
70
+ ): SsmlDocument {
71
+ return {
72
+ ...document,
73
+ children: updateNodesAtPath(document.children ?? [], path, (node) => {
74
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
75
+ if (!value || start === end) return node;
76
+ if (tag === "break") {
77
+ return [
78
+ value.slice(0, start),
79
+ { type: tag, attributes, children: [] } as SsmlElement,
80
+ value.slice(start),
81
+ ].filter((part) => (typeof part === "string" ? part.length > 0 : true));
82
+ }
83
+ const selected = value.slice(start, end);
84
+ const wrapped = { type: tag, attributes, children: [selected] } as SsmlElement;
85
+ return [value.slice(0, start), wrapped, value.slice(end)].filter((part) =>
86
+ typeof part === "string" ? part.length > 0 : true,
87
+ );
88
+ }),
89
+ };
90
+ }
91
+
92
+ function TreeNode({
93
+ node,
94
+ path,
95
+ selectedPath,
96
+ onSelect,
97
+ }: {
98
+ node: SsmlNode;
99
+ path: number[];
100
+ selectedPath: number[] | null;
101
+ onSelect: (path: number[]) => void;
102
+ }): ReactElement | null {
103
+ if (!isElement(node)) return null;
104
+ const name = elementLabel(node);
105
+ const isSelected = selectedPath?.join(".") === path.join(".");
106
+ return (
107
+ <li>
108
+ <button type="button" aria-current={isSelected ? "true" : undefined} onClick={() => onSelect(path)}>
109
+ {`<${name}>`}
110
+ </button>
111
+ {(node.children ?? []).length > 0 && (
112
+ <ul>
113
+ {node.children?.map((child, index) => (
114
+ <TreeNode
115
+ key={`${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`}
116
+ node={child}
117
+ path={[...path, index]}
118
+ selectedPath={selectedPath}
119
+ onSelect={onSelect}
120
+ />
121
+ ))}
122
+ </ul>
123
+ )}
124
+ </li>
125
+ );
126
+ }
127
+
128
+ export function VisualSsmlEditor({
129
+ document,
130
+ readOnly = false,
131
+ onChange,
132
+ onPreviewSelection,
133
+ }: VisualSsmlEditorProps): ReactElement {
134
+ const [selectedPath, setSelectedPath] = useState<number[] | null>(null);
135
+ const [selection, setSelection] = useState({ start: 0, end: 0 });
136
+ const textLeaves = useMemo(() => collectTextLeaves(document.children ?? []), [document]);
137
+ const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
138
+ const diagnostics = useMemo(() => validateAzureSsml(buildSsml(document)), [document]);
139
+ const commit = (nextDocument: SsmlDocument): void => onChange?.(nextDocument);
140
+
141
+ const applyWrapper = (tag: SsmlElement["type"], attributes: Record<string, string>): void => {
142
+ if (readOnly || !selectedLeaf) return;
143
+ const start = selection.start === selection.end ? 0 : selection.start;
144
+ const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
145
+ commit(wrapTextAtPath(document, selectedLeaf.path, start, end, tag, attributes));
146
+ };
147
+
148
+ return (
149
+ <div className="ssml-editor-visual" data-ssml-editor-visual="">
150
+ <fieldset className="ssml-editor-visual-breadcrumb">
151
+ <legend>SSML structure breadcrumb</legend>
152
+ <span>&lt;speak&gt;</span>
153
+ {selectedLeaf?.ancestors.map((ancestor, index) => (
154
+ <span key={selectedLeaf?.ancestors.slice(0, index + 1).join("/")}> / &lt;{ancestor}&gt;</span>
155
+ ))}
156
+ {selectedPath && (
157
+ <button type="button" onClick={() => setSelectedPath(null)}>
158
+ Clear parent
159
+ </button>
160
+ )}
161
+ </fieldset>
162
+ {diagnostics.length > 0 && (
163
+ <div role="alert" aria-invalid="true" className="ssml-editor-visual-errors">
164
+ {diagnostics.map((diagnostic) => (
165
+ <div key={`${diagnostic.code}-${diagnostic.message}`}>{diagnostic.message}</div>
166
+ ))}
167
+ </div>
168
+ )}
169
+ <div className="ssml-editor-visual-layout">
170
+ <nav aria-label="SSML structure tree" className="ssml-editor-visual-tree">
171
+ <strong>Structure</strong>
172
+ <ul>
173
+ {document.children?.map((node, index) => (
174
+ <TreeNode
175
+ key={isElement(node) ? elementLabel(node) : JSON.stringify(node)}
176
+ node={node}
177
+ path={[index]}
178
+ selectedPath={selectedPath}
179
+ onSelect={setSelectedPath}
180
+ />
181
+ ))}
182
+ </ul>
183
+ </nav>
184
+ <div className="ssml-editor-visual-form">
185
+ {selectedLeaf ? (
186
+ <>
187
+ <label>
188
+ Text
189
+ <textarea
190
+ value={selectedLeaf.value}
191
+ readOnly={readOnly}
192
+ onSelect={(event) =>
193
+ setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd })
194
+ }
195
+ onChange={(event) => commit(updateTextAtPath(document, selectedLeaf.path, event.target.value))}
196
+ />
197
+ </label>
198
+ <fieldset className="ssml-editor-visual-actions">
199
+ <legend>Apply SSML formatting</legend>
200
+ <button type="button" disabled={readOnly} onClick={() => applyWrapper("prosody", { rate: "slow" })}>
201
+ Rate
202
+ </button>
203
+ <button type="button" disabled={readOnly} onClick={() => applyWrapper("prosody", { pitch: "high" })}>
204
+ Pitch
205
+ </button>
206
+ <button
207
+ type="button"
208
+ disabled={readOnly}
209
+ onClick={() => applyWrapper("mstts:express-as", { style: "cheerful" })}
210
+ >
211
+ Emotion
212
+ </button>
213
+ <button type="button" disabled={readOnly} onClick={() => applyWrapper("break", { time: "500ms" })}>
214
+ Pause
215
+ </button>
216
+ <button
217
+ type="button"
218
+ disabled={readOnly}
219
+ onClick={() => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })}
220
+ >
221
+ Pronunciation
222
+ </button>
223
+ {onPreviewSelection && (
224
+ <button
225
+ type="button"
226
+ onClick={() => onPreviewSelection(buildSsml({ ...document, children: [selectedLeaf.value] }))}
227
+ >
228
+ Preview selection
229
+ </button>
230
+ )}
231
+ </fieldset>
232
+ </>
233
+ ) : (
234
+ <p>Select an element or text node to edit it.</p>
235
+ )}
236
+ </div>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
@@ -322,6 +322,11 @@ export const SSML_ATTRIBUTE_PRESETS = {
322
322
  "mstts:audioduration": {
323
323
  value: AUDIO_DURATION_PRESETS,
324
324
  },
325
+ "mstts:backgroundaudio": {
326
+ volume: PROSODY_VOLUME_PRESETS,
327
+ fadein: AUDIO_DURATION_PRESETS,
328
+ fadeout: AUDIO_DURATION_PRESETS,
329
+ },
325
330
  silence: {
326
331
  type: SILENCE_TYPE_PRESETS,
327
332
  value: SILENCE_VALUE_PRESETS,
package/src/index.tsx CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  export { SsmlEditor } from "./SsmlEditor";
6
+ export { VisualSsmlEditor } from "./components/VisualSsmlEditor";
7
+ export type { VisualSsmlEditorProps } from "./components/VisualSsmlEditor";
6
8
  export type {
7
9
  SsmlEditorButton,
8
10
  SsmlEditorButtonVisibility,
@@ -21,6 +23,7 @@ export type {
21
23
  SsmlEditorOptions,
22
24
  SsmlEditorRef,
23
25
  SsmlEditorProps,
26
+ SsmlEditorEditMode,
24
27
  SsmlEditorToolbarGroup,
25
28
  SelectionInfo,
26
29
  SsmlEditorTheme,
@@ -46,6 +46,22 @@ const SSML_COMPLETION_SNIPPETS = [
46
46
  label: "mstts:audioduration",
47
47
  insertText: '<mstts:audioduration value="10s" />',
48
48
  },
49
+ {
50
+ label: "mstts:dialog",
51
+ insertText: `<mstts:dialog>\n <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>\n</mstts:dialog>`,
52
+ },
53
+ {
54
+ label: "mstts:turn",
55
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`,
56
+ },
57
+ {
58
+ label: "mstts:backgroundaudio",
59
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`,
60
+ },
61
+ {
62
+ label: "mstts:ttsembedding",
63
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`,
64
+ },
49
65
  {
50
66
  label: "sub",
51
67
  insertText: `<sub alias="\${1:読み}">\${2:漢字}</sub>`,
package/src/ssmlHover.ts CHANGED
@@ -339,6 +339,63 @@ const SSML_TAG_DEFINITIONS: readonly SsmlTagDefinition[] = [
339
339
  },
340
340
  ],
341
341
  },
342
+ {
343
+ name: "mstts:dialog",
344
+ description: "Groups multiple Azure dialog turns that can use different voices.",
345
+ parameters: [],
346
+ },
347
+ {
348
+ name: "mstts:turn",
349
+ description: "Adds one dialog turn using the required Azure voice name.",
350
+ parameters: [
351
+ {
352
+ name: "voice",
353
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
354
+ example: "en-US-JennyNeural",
355
+ },
356
+ ],
357
+ },
358
+ {
359
+ name: "mstts:backgroundaudio",
360
+ description: "Plays background audio while speech is synthesized.",
361
+ parameters: [
362
+ {
363
+ name: "src",
364
+ description: "An absolute HTTP(S) URL for the background audio file.",
365
+ example: "https://example.com/music.mp3",
366
+ },
367
+ {
368
+ name: "volume",
369
+ description: "The background audio volume, for example `-3dB` or `medium`.",
370
+ example: "-3dB",
371
+ },
372
+ {
373
+ name: "fadein",
374
+ description: "The fade-in duration, for example `1s`.",
375
+ example: "1s",
376
+ },
377
+ {
378
+ name: "fadeout",
379
+ description: "The fade-out duration, for example `500ms`.",
380
+ example: "500ms",
381
+ },
382
+ ],
383
+ },
384
+ {
385
+ name: "mstts:ttsembedding",
386
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
387
+ parameters: [],
388
+ },
389
+ {
390
+ name: "mstts:embedding",
391
+ description: "Specifies embedding metadata for custom voice scenarios.",
392
+ parameters: [],
393
+ },
394
+ {
395
+ name: "mstts:voiceconversion",
396
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
397
+ parameters: [],
398
+ },
342
399
  ] as const;
343
400
 
344
401
  export { SSML_TAG_DEFINITIONS };
@@ -82,6 +82,14 @@ export const editorStyles: Record<string, CSSProperties> = {
82
82
  display: "grid",
83
83
  gap: "0.5rem",
84
84
  },
85
+ visual: {
86
+ display: "grid",
87
+ gap: "0.75rem",
88
+ minHeight: "8rem",
89
+ padding: "0.75rem",
90
+ border: "1px solid var(--ssml-editor-control-border)",
91
+ borderRadius: "0.25rem",
92
+ },
85
93
  toolbarIconOnly: {
86
94
  justifyContent: "center",
87
95
  minWidth: "2.25rem",