ssml-builder-js 2.16.0 → 2.18.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
@@ -42,13 +42,13 @@ import {
42
42
  resolveExpressAsStyles,
43
43
  updateEditableText,
44
44
  updateTagAttribute
45
- } from "./chunk-F2EMU3HM.mjs";
45
+ } from "./chunk-SLZ7PE6W.mjs";
46
46
  import {
47
47
  buildPartialSsml,
48
48
  buildSsml,
49
49
  validateAzureSsml,
50
50
  validateSsml
51
- } from "./chunk-HI74FTKY.mjs";
51
+ } from "./chunk-5AZWURHW.mjs";
52
52
  import "./chunk-6S5ODO6A.mjs";
53
53
 
54
54
  // packages/ssml-editor-react/src/SsmlEditor.tsx
@@ -369,6 +369,8 @@ function InsertionPopover({
369
369
  toolbarButtonStyle,
370
370
  emptyOptionsMessage,
371
371
  isReadOnly,
372
+ disabled = false,
373
+ disabledReason,
372
374
  isOpen,
373
375
  menuPosition,
374
376
  menuRef,
@@ -386,7 +388,7 @@ function InsertionPopover({
386
388
  role: "menuitem",
387
389
  style: editorStyles.toolbarOption,
388
390
  title: option.descriptions?.[language] ?? insertion.descriptions[language],
389
- disabled: isReadOnly,
391
+ disabled: isReadOnly || disabled,
390
392
  onMouseDown: (event) => event.preventDefault(),
391
393
  onClick: () => {
392
394
  if (!isReadOnly) {
@@ -415,11 +417,12 @@ function InsertionPopover({
415
417
  {
416
418
  type: "button",
417
419
  style: toolbarButtonStyle,
418
- title: getInsertionTitle(insertion, language),
420
+ title: disabledReason ?? getInsertionTitle(insertion, language),
419
421
  "aria-label": insertion.labels[language],
420
422
  "aria-haspopup": "menu",
421
423
  "aria-expanded": isOpen,
422
424
  "aria-controls": isOpen ? `ssml-editor-popover-${insertion.id}` : void 0,
425
+ disabled: isReadOnly || disabled,
423
426
  onClick: (event) => onToggle(event.currentTarget)
424
427
  },
425
428
  showToolbarIcons && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarIcon, "aria-hidden": "true" }, insertion.icon),
@@ -1969,6 +1972,21 @@ function filteredVoices(voiceCatalog, locale, region, style) {
1969
1972
  return true;
1970
1973
  });
1971
1974
  }
1975
+ function voiceForPath(document2, path, voiceCatalog) {
1976
+ if (!path || !voiceCatalog) return void 0;
1977
+ const ancestors = getElementAncestors(document2.children ?? [], path);
1978
+ const node = getNodeAtPath(document2.children ?? [], path);
1979
+ const candidates = [...ancestors, ...node && isElement(node) ? [node] : []].reverse();
1980
+ const voiceName = candidates.find((element) => element.type === "voice")?.name ?? candidates.find((element) => element.type === "mstts:turn")?.voice;
1981
+ return voiceName ? voiceCatalog.find((voice) => voice.name.toLowerCase() === voiceName.toLowerCase()) : void 0;
1982
+ }
1983
+ function isVisualTagSupported(tagName, voice) {
1984
+ if (!voice) return true;
1985
+ const tag = tagName.toLowerCase();
1986
+ const unsupported = new Set((voice.unsupportedTags ?? []).map((candidate) => candidate.toLowerCase()));
1987
+ const supported = voice.supportedTags?.map((candidate) => candidate.toLowerCase());
1988
+ return !unsupported.has(tag) && (supported === void 0 || supported.includes(tag));
1989
+ }
1972
1990
  function VoiceCapabilityMatrix({
1973
1991
  voice,
1974
1992
  locale
@@ -2043,10 +2061,12 @@ function VisualElementInspector({
2043
2061
  voiceLocale,
2044
2062
  voiceRegion,
2045
2063
  voiceStyle,
2046
- diagnostics
2064
+ diagnostics,
2065
+ voiceDefinition
2047
2066
  }) {
2067
+ const tagSupported = isVisualTagSupported(elementLabel(element), voiceDefinition);
2048
2068
  if (customInspector) {
2049
- return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly, onChange: commit, locale }));
2069
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly: readOnly || !tagSupported, onChange: commit, locale }));
2050
2070
  }
2051
2071
  const fields = getElementFields(element);
2052
2072
  const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
@@ -2054,7 +2074,7 @@ function VisualElementInspector({
2054
2074
  (voice) => voice.name === (element.type === "voice" ? element.name : void 0)
2055
2075
  );
2056
2076
  const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
2057
- return /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`, " ", /* @__PURE__ */ React.createElement(WarningBadge, { messages: elementWarnings(diagnostics, element) })), fields.length === 0 ? /* @__PURE__ */ React.createElement("p", null, "This element is preserved in the visual tree. Edit its attributes in Code mode.") : fields.map((field) => {
2077
+ return /* @__PURE__ */ React.createElement("fieldset", { disabled: readOnly || !tagSupported, style: !tagSupported ? { border: "1px solid #c0392b" } : void 0 }, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`, " ", /* @__PURE__ */ React.createElement(WarningBadge, { messages: elementWarnings(diagnostics, element) })), fields.length === 0 ? /* @__PURE__ */ React.createElement("p", null, "This element is preserved in the visual tree. Edit its attributes in Code mode.") : fields.map((field) => {
2058
2078
  const value = element[field.key];
2059
2079
  const inputValue = value === void 0 ? "" : String(value);
2060
2080
  const inputId = `ssml-visual-${field.key}`;
@@ -2103,6 +2123,7 @@ function VisualElementInspector({
2103
2123
  {
2104
2124
  id: inputId,
2105
2125
  value: inputValue,
2126
+ disabled: readOnly || !tagSupported,
2106
2127
  readOnly,
2107
2128
  onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
2108
2129
  }
@@ -2111,6 +2132,7 @@ function VisualElementInspector({
2111
2132
  {
2112
2133
  id: inputId,
2113
2134
  value: inputValue,
2135
+ disabled: readOnly || !tagSupported,
2114
2136
  readOnly,
2115
2137
  onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
2116
2138
  }
@@ -2174,18 +2196,27 @@ function TreeNode({
2174
2196
  }) {
2175
2197
  if (!isElement(node)) return null;
2176
2198
  const name = elementLabel(node);
2199
+ const messages = getWarnings(node);
2177
2200
  const isSelected = selectedPath?.join(".") === path.join(".");
2178
- return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`, /* @__PURE__ */ React.createElement(WarningBadge, { messages: getWarnings(node) })), (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
2179
- TreeNode,
2201
+ return /* @__PURE__ */ React.createElement(
2202
+ "li",
2180
2203
  {
2181
- key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
2182
- node: child,
2183
- path: [...path, index],
2184
- selectedPath,
2185
- onSelect,
2186
- getWarnings
2187
- }
2188
- ))));
2204
+ "data-ssml-diagnostic": messages.length > 0 ? "error" : void 0,
2205
+ style: messages.length > 0 ? { border: "1px solid #c0392b", borderRadius: "0.25rem" } : void 0
2206
+ },
2207
+ /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`, /* @__PURE__ */ React.createElement(WarningBadge, { messages: getWarnings(node) })),
2208
+ (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
2209
+ TreeNode,
2210
+ {
2211
+ key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
2212
+ node: child,
2213
+ path: [...path, index],
2214
+ selectedPath,
2215
+ onSelect,
2216
+ getWarnings
2217
+ }
2218
+ )))
2219
+ );
2189
2220
  }
2190
2221
  function VisualSsmlEditor({
2191
2222
  document: document2,
@@ -2200,13 +2231,16 @@ function VisualSsmlEditor({
2200
2231
  voiceRegion,
2201
2232
  voiceStyle,
2202
2233
  voiceModel,
2203
- model
2234
+ model,
2235
+ voiceCatalogMetadata
2204
2236
  }) {
2205
2237
  const [selectedPath, setSelectedPath] = useState2(null);
2206
2238
  const [selection, setSelection] = useState2({ start: 0, end: 0 });
2207
2239
  const textLeaves = useMemo(() => collectTextLeaves(document2.children ?? []), [document2]);
2208
2240
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
2209
2241
  const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
2242
+ const activeVoice = voiceForPath(document2, selectedPath ?? selectedLeaf?.path, voiceCatalog);
2243
+ const staleCatalog = voiceCatalogMetadata?.expiresAt ? Date.parse(voiceCatalogMetadata.expiresAt) <= Date.now() : false;
2210
2244
  const selectedCustomInspector = selectedElement && isElement(selectedElement) ? customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type] : void 0;
2211
2245
  const validationOptions = useMemo(
2212
2246
  () => ({
@@ -2236,7 +2270,7 @@ function VisualSsmlEditor({
2236
2270
  const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
2237
2271
  commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
2238
2272
  };
2239
- 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(
2273
+ 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))), staleCatalog && /* @__PURE__ */ React.createElement("div", { role: "status", className: "ssml-editor-visual-catalog-warning" }, locale === "ja" ? "\u97F3\u58F0\u30AB\u30BF\u30ED\u30B0\u304C\u53E4\u304F\u306A\u3063\u3066\u3044\u307E\u3059\u3002\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "The voice catalog is stale. Refresh it before synthesizing."), /* @__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(
2240
2274
  TreeNode,
2241
2275
  {
2242
2276
  key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
@@ -2250,14 +2284,14 @@ function VisualSsmlEditor({
2250
2284
  document: document2,
2251
2285
  element: selectedElement,
2252
2286
  path: selectedPath ?? [],
2253
- readOnly,
2287
+ readOnly: readOnly || !isVisualTagSupported(elementLabel(selectedElement), activeVoice),
2254
2288
  onChange: commit,
2255
2289
  locale
2256
2290
  }) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, localizedElementLabel(selectedElement, locale)), /* @__PURE__ */ React.createElement("p", null, SSML_ELEMENT_COPY[locale][elementLabel(selectedElement)]?.description), /* @__PURE__ */ React.createElement(
2257
2291
  "button",
2258
2292
  {
2259
2293
  type: "button",
2260
- disabled: readOnly,
2294
+ disabled: readOnly || !isVisualTagSupported("mstts:dialog", activeVoice),
2261
2295
  onClick: () => commit(addDialogTurn(document2, selectedPath ?? []))
2262
2296
  },
2263
2297
  locale === "ja" ? "\u30BF\u30FC\u30F3\u3092\u8FFD\u52A0" : "Add turn"
@@ -2339,7 +2373,8 @@ function VisualSsmlEditor({
2339
2373
  voiceLocale,
2340
2374
  voiceRegion,
2341
2375
  voiceStyle,
2342
- diagnostics
2376
+ diagnostics,
2377
+ voiceDefinition: activeVoice
2343
2378
  }
2344
2379
  ) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
2345
2380
  "textarea",
@@ -2349,19 +2384,43 @@ function VisualSsmlEditor({
2349
2384
  onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
2350
2385
  onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
2351
2386
  }
2352
- )), /* @__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(
2387
+ )), /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-actions" }, /* @__PURE__ */ React.createElement("legend", null, "Apply SSML formatting"), /* @__PURE__ */ React.createElement(
2353
2388
  "button",
2354
2389
  {
2355
2390
  type: "button",
2356
- disabled: readOnly,
2391
+ disabled: readOnly || !isVisualTagSupported("prosody", activeVoice),
2392
+ onClick: () => applyWrapper("prosody", { rate: "slow" })
2393
+ },
2394
+ "Rate"
2395
+ ), /* @__PURE__ */ React.createElement(
2396
+ "button",
2397
+ {
2398
+ type: "button",
2399
+ disabled: readOnly || !isVisualTagSupported("prosody", activeVoice),
2400
+ onClick: () => applyWrapper("prosody", { pitch: "high" })
2401
+ },
2402
+ "Pitch"
2403
+ ), /* @__PURE__ */ React.createElement(
2404
+ "button",
2405
+ {
2406
+ type: "button",
2407
+ disabled: readOnly || !isVisualTagSupported("mstts:express-as", activeVoice),
2357
2408
  onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
2358
2409
  },
2359
2410
  "Emotion"
2360
- ), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
2411
+ ), /* @__PURE__ */ React.createElement(
2361
2412
  "button",
2362
2413
  {
2363
2414
  type: "button",
2364
- disabled: readOnly,
2415
+ disabled: readOnly || !isVisualTagSupported("break", activeVoice),
2416
+ onClick: () => applyWrapper("break", { time: "500ms" })
2417
+ },
2418
+ "Pause"
2419
+ ), /* @__PURE__ */ React.createElement(
2420
+ "button",
2421
+ {
2422
+ type: "button",
2423
+ disabled: readOnly || !isVisualTagSupported("phoneme", activeVoice),
2365
2424
  onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
2366
2425
  },
2367
2426
  "Pronunciation"
@@ -2380,6 +2439,23 @@ var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
2380
2439
  var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
2381
2440
  var PROSODY_POPOVER_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
2382
2441
  var TEXT_POPOVER_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
2442
+ function isVoiceTagSupported(tagName, voice) {
2443
+ if (!tagName || !voice) return true;
2444
+ const tag = tagName.toLowerCase();
2445
+ const unsupported = new Set((voice.unsupportedTags ?? []).map((candidate) => candidate.toLowerCase()));
2446
+ const supported = voice.supportedTags?.map((candidate) => candidate.toLowerCase());
2447
+ return !unsupported.has(tag) && (supported === void 0 || supported.includes(tag));
2448
+ }
2449
+ function findFirstVoiceName(nodes) {
2450
+ for (const node of nodes) {
2451
+ if (typeof node === "string" || node.type === "text") continue;
2452
+ if (node.type === "voice" && node.name) return node.name;
2453
+ if (node.type === "mstts:turn" && node.voice) return node.voice;
2454
+ const nested = findFirstVoiceName(node.children ?? []);
2455
+ if (nested) return nested;
2456
+ }
2457
+ return void 0;
2458
+ }
2383
2459
  function localizedText(value) {
2384
2460
  return { ja: value, en: value };
2385
2461
  }
@@ -2965,6 +3041,7 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
2965
3041
  voiceStyle,
2966
3042
  voiceModel,
2967
3043
  model,
3044
+ voiceCatalogMetadata,
2968
3045
  emotionStyles,
2969
3046
  className,
2970
3047
  style,
@@ -3128,6 +3205,10 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
3128
3205
  const isProsodyPopover = insertion.tagName !== void 0 && PROSODY_POPOVER_TAGS.has(insertion.tagName);
3129
3206
  const isTextPopover = insertion.tagName !== void 0 && TEXT_POPOVER_TAGS.has(insertion.tagName);
3130
3207
  const insertionButtonStyle = insertion.tagName && activeTags.has(insertion.tagName.toLowerCase()) ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle;
3208
+ const activeVoiceName = popoverVoiceName ?? findFirstVoiceName(draftDocument.children ?? []);
3209
+ const activeVoice = voiceCatalog?.find((voice) => voice.name.toLowerCase() === activeVoiceName?.toLowerCase());
3210
+ const insertionDisabled = !isVoiceTagSupported(insertion.tagName, activeVoice);
3211
+ const disabledReason = insertionDisabled ? language === "ja" ? `\u9078\u629E\u4E2D\u306E\u97F3\u58F0\u306F <${insertion.tagName}> \u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\u3002` : `The selected voice does not support <${insertion.tagName}>.` : void 0;
3131
3212
  const props = {
3132
3213
  language,
3133
3214
  isDarkTheme,
@@ -3136,6 +3217,8 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
3136
3217
  toolbarButtonStyle: insertionButtonStyle,
3137
3218
  emptyOptionsMessage: copy.noAvailableOptions,
3138
3219
  isReadOnly,
3220
+ disabled: insertionDisabled,
3221
+ disabledReason,
3139
3222
  openPopoverId,
3140
3223
  menuPosition: popoverPosition,
3141
3224
  menuRef: setPopoverMenuRef,
@@ -3364,6 +3447,7 @@ var SsmlEditor = forwardRef(function SsmlEditor2({
3364
3447
  voiceLocale,
3365
3448
  voiceRegion,
3366
3449
  voiceStyle,
3450
+ voiceCatalogMetadata,
3367
3451
  voiceModel,
3368
3452
  model
3369
3453
  }