@ssml-builder-js/ssml-editor-react 2.4.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/CHANGELOG.md +91 -0
- package/dist/index.d.mts +288 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +5423 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5385 -0
- package/dist/index.mjs.map +1 -0
- package/e2e/monaco-editor.spec.ts +126 -0
- package/package.json +57 -0
- package/src/SsmlEditor.tsx +1302 -0
- package/src/buttonVisibility.ts +36 -0
- package/src/clearSsmlDocument.ts +57 -0
- package/src/components/popovers/InsertionPopover.tsx +136 -0
- package/src/components/popovers/InsertionPopovers.tsx +47 -0
- package/src/components/popovers/ProsodyPopovers.tsx +78 -0
- package/src/components/popovers/TextPopovers.tsx +8 -0
- package/src/components/popovers/TimingPopovers.tsx +8 -0
- package/src/constants/ssmlPresets.ts +660 -0
- package/src/constants/ui.ts +11 -0
- package/src/editableSsml.ts +251 -0
- package/src/formatXml.ts +600 -0
- package/src/hooks/useSsmlEditorState.ts +704 -0
- package/src/hooks/useSsmlMonaco.ts +393 -0
- package/src/index.tsx +50 -0
- package/src/locales.ts +435 -0
- package/src/ssmlCodeAction.ts +144 -0
- package/src/ssmlCodeLens.ts +226 -0
- package/src/ssmlCompletion.ts +129 -0
- package/src/ssmlContext.ts +196 -0
- package/src/ssmlDiagnostics.ts +191 -0
- package/src/ssmlHover.ts +703 -0
- package/src/ssmlInsertion.ts +75 -0
- package/src/ssmlInsertions.ts +471 -0
- package/src/styles/editorStyles.ts +282 -0
- package/test/format-xml-edge-cases.test.ts +107 -0
- package/test/index.test.ts +597 -0
- package/test/randomized-editor-invariants.test.ts +520 -0
- package/test/ui-components.test.tsx +854 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.mjs +10 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @ts-expect-error The Node strip-types test runner requires the explicit TypeScript extension.
|
|
2
|
+
import { SSML_INSERTION_MODES } from "./constants/ui.ts";
|
|
3
|
+
|
|
4
|
+
export type SsmlInsertionMode = (typeof SSML_INSERTION_MODES)[keyof typeof SSML_INSERTION_MODES];
|
|
5
|
+
|
|
6
|
+
export interface SsmlInsertionTemplate {
|
|
7
|
+
prefix: string;
|
|
8
|
+
suffix: string;
|
|
9
|
+
mode: SsmlInsertionMode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SsmlInsertionEdit {
|
|
13
|
+
replacement: string;
|
|
14
|
+
selectionOffset: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isLineStart(value: string, offset: number): boolean {
|
|
18
|
+
return offset === 0 || value[offset - 1] === "\n" || value[offset - 1] === "\r";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isLineEnd(value: string, offset: number): boolean {
|
|
22
|
+
return offset === value.length || value[offset] === "\n" || value[offset] === "\r";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getLineBreakAt(value: string, offset: number): string {
|
|
26
|
+
if (value.startsWith("\r\n", offset)) {
|
|
27
|
+
return "\r\n";
|
|
28
|
+
}
|
|
29
|
+
if (value[offset] === "\n" || value[offset] === "\r") {
|
|
30
|
+
return value[offset];
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function startsWithLineBreak(value: string): boolean {
|
|
36
|
+
return value.startsWith("\n") || value.startsWith("\r");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function endsWithLineBreak(value: string): boolean {
|
|
40
|
+
return value.endsWith("\n") || value.endsWith("\r");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createSsmlInsertionEdit(
|
|
44
|
+
source: string,
|
|
45
|
+
startOffset: number,
|
|
46
|
+
endOffset: number,
|
|
47
|
+
template: SsmlInsertionTemplate,
|
|
48
|
+
eol = "\n",
|
|
49
|
+
selectedText = startOffset === endOffset ? "" : source.slice(startOffset, endOffset),
|
|
50
|
+
): SsmlInsertionEdit {
|
|
51
|
+
if (template.mode === SSML_INSERTION_MODES.wrap) {
|
|
52
|
+
const trailingLineBreak = getLineBreakAt(source, endOffset) === "" ? eol : "";
|
|
53
|
+
return {
|
|
54
|
+
replacement: `${template.prefix}${selectedText}${template.suffix}${trailingLineBreak}`,
|
|
55
|
+
selectionOffset: template.prefix.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const followingLineBreak = getLineBreakAt(source, endOffset);
|
|
60
|
+
const leadingLineBreak = !isLineStart(source, startOffset) && !startsWithLineBreak(template.prefix) ? eol : "";
|
|
61
|
+
const needsTrailingLineBreak =
|
|
62
|
+
selectedText.length > 0
|
|
63
|
+
? !selectedText.startsWith("\n") && !selectedText.startsWith("\r")
|
|
64
|
+
: !isLineEnd(source, endOffset) || followingLineBreak === "";
|
|
65
|
+
const trailingLineBreak = needsTrailingLineBreak && !endsWithLineBreak(template.prefix) ? eol : "";
|
|
66
|
+
const insertionPrefix = `${leadingLineBreak}${template.prefix}`;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
replacement: `${insertionPrefix}${trailingLineBreak}${selectedText}`,
|
|
70
|
+
selectionOffset:
|
|
71
|
+
insertionPrefix.length +
|
|
72
|
+
trailingLineBreak.length +
|
|
73
|
+
(selectedText.length === 0 && trailingLineBreak === "" ? followingLineBreak.length : 0),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type { SsmlEditorLocalizedText } from "./locales";
|
|
2
|
+
import {
|
|
3
|
+
BREAK_TIME_DESCRIPTIONS,
|
|
4
|
+
BREAK_TIME_PRESETS,
|
|
5
|
+
EMPHASIS_LEVEL_DESCRIPTIONS,
|
|
6
|
+
EMPHASIS_LEVEL_PRESETS,
|
|
7
|
+
EXPRESS_AS_STYLE_DESCRIPTIONS,
|
|
8
|
+
EXPRESS_AS_STYLE_PRESETS,
|
|
9
|
+
LANGUAGE_DESCRIPTIONS,
|
|
10
|
+
LANGUAGE_PRESETS,
|
|
11
|
+
PROSODY_PITCH_DESCRIPTIONS,
|
|
12
|
+
PROSODY_PITCH_PRESETS,
|
|
13
|
+
PROSODY_RATE_DESCRIPTIONS,
|
|
14
|
+
PROSODY_RATE_PRESETS,
|
|
15
|
+
PROSODY_VOLUME_DESCRIPTIONS,
|
|
16
|
+
PROSODY_VOLUME_PRESETS,
|
|
17
|
+
SAY_AS_DESCRIPTIONS,
|
|
18
|
+
SAY_AS_PRESETS,
|
|
19
|
+
SILENCE_VALUE_DESCRIPTIONS,
|
|
20
|
+
SILENCE_VALUE_PRESETS,
|
|
21
|
+
} from "./constants/ssmlPresets";
|
|
22
|
+
|
|
23
|
+
export type SsmlEditorInsertionMode = "insert" | "wrap";
|
|
24
|
+
|
|
25
|
+
export interface SsmlEditorInsertionOption {
|
|
26
|
+
value: string;
|
|
27
|
+
labels: SsmlEditorLocalizedText;
|
|
28
|
+
descriptions?: SsmlEditorLocalizedText;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SsmlEditorInsertionTemplate {
|
|
32
|
+
prefix: string;
|
|
33
|
+
suffix: string;
|
|
34
|
+
mode: SsmlEditorInsertionMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SsmlEditorInsertionDefinition {
|
|
38
|
+
id: string;
|
|
39
|
+
icon: string;
|
|
40
|
+
tagName?: string;
|
|
41
|
+
selfClosing?: boolean;
|
|
42
|
+
labels: SsmlEditorLocalizedText;
|
|
43
|
+
titles?: SsmlEditorLocalizedText;
|
|
44
|
+
descriptions: SsmlEditorLocalizedText;
|
|
45
|
+
parameterDescription: SsmlEditorLocalizedText;
|
|
46
|
+
options: readonly SsmlEditorInsertionOption[];
|
|
47
|
+
createTemplate: (value: string) => SsmlEditorInsertionTemplate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SsmlEditorCustomInsertionDefinition {
|
|
51
|
+
id: string;
|
|
52
|
+
tagName: string;
|
|
53
|
+
attribute?: string;
|
|
54
|
+
icon?: string;
|
|
55
|
+
labels: SsmlEditorLocalizedText;
|
|
56
|
+
titles?: SsmlEditorLocalizedText;
|
|
57
|
+
descriptions?: SsmlEditorLocalizedText;
|
|
58
|
+
parameterDescription?: SsmlEditorLocalizedText;
|
|
59
|
+
options: readonly SsmlEditorInsertionOption[];
|
|
60
|
+
mode?: SsmlEditorInsertionMode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SsmlEditorCustomInsertion = SsmlEditorInsertionDefinition | SsmlEditorCustomInsertionDefinition;
|
|
64
|
+
|
|
65
|
+
export type SsmlEditorCustomInsertionCollection =
|
|
66
|
+
| readonly SsmlEditorCustomInsertion[]
|
|
67
|
+
| Readonly<Record<string, SsmlEditorCustomInsertion | undefined>>;
|
|
68
|
+
|
|
69
|
+
export interface SsmlEditorInsertionGroup {
|
|
70
|
+
id: string;
|
|
71
|
+
labels: SsmlEditorLocalizedText;
|
|
72
|
+
insertionIds: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SsmlEditorToolbarGroup {
|
|
76
|
+
id: string;
|
|
77
|
+
buttonIds: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function localizedText(value: string): SsmlEditorLocalizedText {
|
|
81
|
+
return { ja: value, en: value };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function escapeXmlAttribute(value: string): string {
|
|
85
|
+
return value
|
|
86
|
+
.replace(/&/g, "&")
|
|
87
|
+
.replace(/"/g, """)
|
|
88
|
+
.replace(/</g, "<")
|
|
89
|
+
.replace(/>/g, ">")
|
|
90
|
+
.replace(/'/g, "'");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createSsmlEditorInsertionDefinition(
|
|
94
|
+
definition: SsmlEditorCustomInsertionDefinition,
|
|
95
|
+
): SsmlEditorInsertionDefinition {
|
|
96
|
+
const mode = definition.mode ?? "wrap";
|
|
97
|
+
const attribute = definition.attribute ? ` ${definition.attribute}="` : "";
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: definition.id,
|
|
101
|
+
icon: definition.icon ?? "+",
|
|
102
|
+
tagName: definition.tagName,
|
|
103
|
+
selfClosing: mode === "insert",
|
|
104
|
+
labels: definition.labels,
|
|
105
|
+
titles: definition.titles,
|
|
106
|
+
descriptions: definition.descriptions ?? localizedText(`Inserts the <${definition.tagName}> element.`),
|
|
107
|
+
parameterDescription:
|
|
108
|
+
definition.parameterDescription ?? localizedText(`Selects the value for the <${definition.tagName}> element.`),
|
|
109
|
+
options: definition.options,
|
|
110
|
+
createTemplate: (value) => {
|
|
111
|
+
const attributeValue = definition.attribute ? `${attribute}${escapeXmlAttribute(value)}"` : "";
|
|
112
|
+
if (mode === "wrap") {
|
|
113
|
+
return {
|
|
114
|
+
prefix: `<${definition.tagName}${attributeValue}>`,
|
|
115
|
+
suffix: `</${definition.tagName}>`,
|
|
116
|
+
mode,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
prefix: `<${definition.tagName}${attributeValue}/>`,
|
|
122
|
+
suffix: "",
|
|
123
|
+
mode,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createInsertionOptions(
|
|
130
|
+
values: readonly string[],
|
|
131
|
+
descriptions?: Readonly<Record<string, SsmlEditorLocalizedText>>,
|
|
132
|
+
): readonly SsmlEditorInsertionOption[] {
|
|
133
|
+
return values.map((value) => ({
|
|
134
|
+
value,
|
|
135
|
+
labels: { ja: value, en: value },
|
|
136
|
+
...(descriptions?.[value] ? { descriptions: descriptions[value] } : {}),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const SSML_INSERTIONS = [
|
|
141
|
+
{
|
|
142
|
+
id: "break",
|
|
143
|
+
icon: "⏸",
|
|
144
|
+
tagName: "break",
|
|
145
|
+
selfClosing: true,
|
|
146
|
+
labels: { ja: "間", en: "Break" },
|
|
147
|
+
descriptions: {
|
|
148
|
+
ja: "指定した時間だけ無音の間を挿入します。",
|
|
149
|
+
en: "Inserts a silent pause for the selected duration.",
|
|
150
|
+
},
|
|
151
|
+
parameterDescription: {
|
|
152
|
+
ja: "無音にする時間を選択します。",
|
|
153
|
+
en: "Selects the duration of the silent pause.",
|
|
154
|
+
},
|
|
155
|
+
options: createInsertionOptions(BREAK_TIME_PRESETS, BREAK_TIME_DESCRIPTIONS),
|
|
156
|
+
createTemplate: (value) => ({
|
|
157
|
+
prefix: `<break time="${value}"/>`,
|
|
158
|
+
suffix: "",
|
|
159
|
+
mode: "insert",
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "emphasis",
|
|
164
|
+
icon: "✦",
|
|
165
|
+
tagName: "emphasis",
|
|
166
|
+
labels: { ja: "強調", en: "Emphasis" },
|
|
167
|
+
descriptions: {
|
|
168
|
+
ja: "選択範囲の強調レベルを変更します。",
|
|
169
|
+
en: "Changes the emphasis level of the selected text.",
|
|
170
|
+
},
|
|
171
|
+
parameterDescription: {
|
|
172
|
+
ja: "選択範囲の強調レベルを選択します。",
|
|
173
|
+
en: "Selects the emphasis level for the selected text.",
|
|
174
|
+
},
|
|
175
|
+
options: createInsertionOptions(EMPHASIS_LEVEL_PRESETS, EMPHASIS_LEVEL_DESCRIPTIONS),
|
|
176
|
+
createTemplate: (value) => ({
|
|
177
|
+
prefix: `<emphasis level="${value}">`,
|
|
178
|
+
suffix: "</emphasis>",
|
|
179
|
+
mode: "wrap",
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "rate",
|
|
184
|
+
icon: "↕",
|
|
185
|
+
tagName: "prosody",
|
|
186
|
+
labels: { ja: "速度", en: "Rate" },
|
|
187
|
+
descriptions: {
|
|
188
|
+
ja: "選択範囲の読み上げ速度を変更します。",
|
|
189
|
+
en: "Changes the speech rate of the selected text.",
|
|
190
|
+
},
|
|
191
|
+
parameterDescription: {
|
|
192
|
+
ja: "選択範囲の読み上げ速度を選択します。",
|
|
193
|
+
en: "Selects the speech rate for the selected text.",
|
|
194
|
+
},
|
|
195
|
+
options: createInsertionOptions(PROSODY_RATE_PRESETS, PROSODY_RATE_DESCRIPTIONS),
|
|
196
|
+
createTemplate: (value) => ({
|
|
197
|
+
prefix: `<prosody rate="${value}">`,
|
|
198
|
+
suffix: "</prosody>",
|
|
199
|
+
mode: "wrap",
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "pitch",
|
|
204
|
+
icon: "↗",
|
|
205
|
+
tagName: "prosody",
|
|
206
|
+
labels: { ja: "高さ", en: "Pitch" },
|
|
207
|
+
descriptions: {
|
|
208
|
+
ja: "選択範囲の声の高さを変更します。",
|
|
209
|
+
en: "Changes the pitch of the selected text.",
|
|
210
|
+
},
|
|
211
|
+
parameterDescription: {
|
|
212
|
+
ja: "選択範囲の声の高さを半音単位で選択します。",
|
|
213
|
+
en: "Selects the pitch adjustment in semitone steps.",
|
|
214
|
+
},
|
|
215
|
+
options: createInsertionOptions(PROSODY_PITCH_PRESETS, PROSODY_PITCH_DESCRIPTIONS),
|
|
216
|
+
createTemplate: (value) => ({
|
|
217
|
+
prefix: `<prosody pitch="${value}">`,
|
|
218
|
+
suffix: "</prosody>",
|
|
219
|
+
mode: "wrap",
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "volume",
|
|
224
|
+
icon: "🔊",
|
|
225
|
+
tagName: "prosody",
|
|
226
|
+
labels: { ja: "音量", en: "Volume" },
|
|
227
|
+
descriptions: {
|
|
228
|
+
ja: "選択範囲の音量を変更します。",
|
|
229
|
+
en: "Changes the volume of the selected text.",
|
|
230
|
+
},
|
|
231
|
+
parameterDescription: {
|
|
232
|
+
ja: "選択範囲の音量レベルを選択します。",
|
|
233
|
+
en: "Selects the volume level for the selected text.",
|
|
234
|
+
},
|
|
235
|
+
options: createInsertionOptions(PROSODY_VOLUME_PRESETS, PROSODY_VOLUME_DESCRIPTIONS),
|
|
236
|
+
createTemplate: (value) => ({
|
|
237
|
+
prefix: `<prosody volume="${value}">`,
|
|
238
|
+
suffix: "</prosody>",
|
|
239
|
+
mode: "wrap",
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "emotion",
|
|
244
|
+
icon: "☺",
|
|
245
|
+
tagName: "mstts:express-as",
|
|
246
|
+
labels: { ja: "感情", en: "Emotion" },
|
|
247
|
+
descriptions: {
|
|
248
|
+
ja: "選択範囲に音声の感情スタイルを適用します。",
|
|
249
|
+
en: "Applies a voice emotion style to the selected text.",
|
|
250
|
+
},
|
|
251
|
+
parameterDescription: {
|
|
252
|
+
ja: "選択範囲に適用する音声の感情スタイルを選択します。",
|
|
253
|
+
en: "Selects the voice emotion style to apply.",
|
|
254
|
+
},
|
|
255
|
+
options: createInsertionOptions(EXPRESS_AS_STYLE_PRESETS, EXPRESS_AS_STYLE_DESCRIPTIONS),
|
|
256
|
+
createTemplate: (value) => ({
|
|
257
|
+
prefix: `<mstts:express-as style="${value}">`,
|
|
258
|
+
suffix: "</mstts:express-as>",
|
|
259
|
+
mode: "wrap",
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "say-as",
|
|
264
|
+
icon: "Aa",
|
|
265
|
+
tagName: "say-as",
|
|
266
|
+
labels: { ja: "読み上げ", en: "Say as" },
|
|
267
|
+
descriptions: {
|
|
268
|
+
ja: "数字や日付などの読み上げ方を指定します。",
|
|
269
|
+
en: "Specifies how values such as numbers or dates are spoken.",
|
|
270
|
+
},
|
|
271
|
+
parameterDescription: {
|
|
272
|
+
ja: "選択範囲の読み上げ方を選択します。",
|
|
273
|
+
en: "Selects how the selected text is spoken.",
|
|
274
|
+
},
|
|
275
|
+
options: createInsertionOptions(SAY_AS_PRESETS, SAY_AS_DESCRIPTIONS),
|
|
276
|
+
createTemplate: (value) => ({
|
|
277
|
+
prefix: `<say-as interpret-as="${value}">`,
|
|
278
|
+
suffix: "</say-as>",
|
|
279
|
+
mode: "wrap",
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "lang",
|
|
284
|
+
icon: "文",
|
|
285
|
+
tagName: "lang",
|
|
286
|
+
labels: { ja: "言語", en: "Language" },
|
|
287
|
+
descriptions: {
|
|
288
|
+
ja: "選択範囲の読み上げ言語を変更します。",
|
|
289
|
+
en: "Changes the speaking language of the selected text.",
|
|
290
|
+
},
|
|
291
|
+
parameterDescription: {
|
|
292
|
+
ja: "選択範囲に適用する BCP-47 言語タグを選択します。",
|
|
293
|
+
en: "Selects the BCP-47 language tag for the selected text.",
|
|
294
|
+
},
|
|
295
|
+
options: createInsertionOptions(LANGUAGE_PRESETS, LANGUAGE_DESCRIPTIONS),
|
|
296
|
+
createTemplate: (value) => ({
|
|
297
|
+
prefix: `<lang xml:lang="${value}">`,
|
|
298
|
+
suffix: "</lang>",
|
|
299
|
+
mode: "wrap",
|
|
300
|
+
}),
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: "mstts:silence",
|
|
304
|
+
icon: "⏳",
|
|
305
|
+
tagName: "mstts:silence",
|
|
306
|
+
selfClosing: true,
|
|
307
|
+
labels: { ja: "無音", en: "Silence" },
|
|
308
|
+
descriptions: {
|
|
309
|
+
ja: "無音時間を挿入します。",
|
|
310
|
+
en: "Inserts a silence interval.",
|
|
311
|
+
},
|
|
312
|
+
parameterDescription: {
|
|
313
|
+
ja: "無音にする時間を選択します。",
|
|
314
|
+
en: "Selects the silence duration.",
|
|
315
|
+
},
|
|
316
|
+
options: createInsertionOptions(SILENCE_VALUE_PRESETS, SILENCE_VALUE_DESCRIPTIONS),
|
|
317
|
+
createTemplate: (value) => ({
|
|
318
|
+
prefix: `<mstts:silence type="Leading" value="${value}"/>`,
|
|
319
|
+
suffix: "",
|
|
320
|
+
mode: "insert",
|
|
321
|
+
}),
|
|
322
|
+
},
|
|
323
|
+
] satisfies readonly SsmlEditorInsertionDefinition[];
|
|
324
|
+
|
|
325
|
+
export const DEFAULT_INSERTION_GROUPS = [
|
|
326
|
+
{
|
|
327
|
+
id: "pauses",
|
|
328
|
+
labels: { ja: "間・無音", en: "Pauses" },
|
|
329
|
+
insertionIds: ["break", "mstts:silence"],
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "prosody",
|
|
333
|
+
labels: { ja: "声の調整", en: "Voice" },
|
|
334
|
+
insertionIds: ["rate", "pitch", "volume"],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "expression",
|
|
338
|
+
labels: { ja: "表現", en: "Expression" },
|
|
339
|
+
insertionIds: ["emphasis", "emotion"],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "pronunciation",
|
|
343
|
+
labels: { ja: "読み上げ", en: "Pronunciation" },
|
|
344
|
+
insertionIds: ["say-as", "lang"],
|
|
345
|
+
},
|
|
346
|
+
] satisfies readonly SsmlEditorInsertionGroup[];
|
|
347
|
+
|
|
348
|
+
export function createDefaultToolbarGroups(
|
|
349
|
+
insertionGroups: readonly SsmlEditorInsertionGroup[],
|
|
350
|
+
): readonly SsmlEditorToolbarGroup[] {
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
id: "history",
|
|
354
|
+
buttonIds: ["undo", "redo"],
|
|
355
|
+
},
|
|
356
|
+
...insertionGroups.map(({ id, insertionIds }) => ({
|
|
357
|
+
id,
|
|
358
|
+
buttonIds: insertionIds,
|
|
359
|
+
})),
|
|
360
|
+
{
|
|
361
|
+
id: "document",
|
|
362
|
+
buttonIds: ["clearAll", "format", "decorations"],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "help",
|
|
366
|
+
buttonIds: ["help"],
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getInsertionCollection(
|
|
372
|
+
collection: SsmlEditorCustomInsertionCollection | undefined,
|
|
373
|
+
): readonly SsmlEditorCustomInsertion[] {
|
|
374
|
+
if (!collection) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return Array.isArray(collection)
|
|
379
|
+
? collection
|
|
380
|
+
: Object.values(collection).filter((insertion): insertion is SsmlEditorCustomInsertion => insertion !== undefined);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function getConfiguredInsertions(
|
|
384
|
+
emotionStyles: readonly string[] | undefined,
|
|
385
|
+
customInsertions: SsmlEditorCustomInsertionCollection | undefined,
|
|
386
|
+
additionalInsertions: SsmlEditorCustomInsertionCollection | undefined,
|
|
387
|
+
): readonly SsmlEditorInsertionDefinition[] {
|
|
388
|
+
const insertions = new Map<string, SsmlEditorInsertionDefinition>();
|
|
389
|
+
for (const insertion of SSML_INSERTIONS) {
|
|
390
|
+
if (insertion.id === "emotion" && emotionStyles !== undefined) {
|
|
391
|
+
insertions.set(insertion.id, {
|
|
392
|
+
...insertion,
|
|
393
|
+
options: createInsertionOptions(emotionStyles),
|
|
394
|
+
});
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
insertions.set(insertion.id, insertion);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const normalizeInsertion = (insertion: SsmlEditorCustomInsertion): SsmlEditorInsertionDefinition =>
|
|
401
|
+
"createTemplate" in insertion ? insertion : createSsmlEditorInsertionDefinition(insertion);
|
|
402
|
+
|
|
403
|
+
for (const insertion of getInsertionCollection(additionalInsertions)) {
|
|
404
|
+
const normalized = normalizeInsertion(insertion);
|
|
405
|
+
if (!insertions.has(normalized.id)) {
|
|
406
|
+
insertions.set(normalized.id, normalized);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (const insertion of getInsertionCollection(customInsertions)) {
|
|
411
|
+
const normalized = normalizeInsertion(insertion);
|
|
412
|
+
insertions.set(normalized.id, normalized);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return [...insertions.values()];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function orderInsertions(
|
|
419
|
+
insertions: readonly SsmlEditorInsertionDefinition[],
|
|
420
|
+
insertionOrder: readonly string[] | undefined,
|
|
421
|
+
): readonly SsmlEditorInsertionDefinition[] {
|
|
422
|
+
if (!insertionOrder || insertionOrder.length === 0) {
|
|
423
|
+
return insertions;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const insertionsById = new Map(insertions.map((insertion) => [insertion.id, insertion]));
|
|
427
|
+
const ordered: SsmlEditorInsertionDefinition[] = [];
|
|
428
|
+
const included = new Set<string>();
|
|
429
|
+
for (const id of insertionOrder) {
|
|
430
|
+
const insertion = insertionsById.get(id);
|
|
431
|
+
if (insertion && !included.has(id)) {
|
|
432
|
+
ordered.push(insertion);
|
|
433
|
+
included.add(id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const insertion of insertions) {
|
|
438
|
+
if (!included.has(insertion.id)) {
|
|
439
|
+
ordered.push(insertion);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return ordered;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function orderToolbarButtons(
|
|
447
|
+
buttonIds: readonly string[],
|
|
448
|
+
toolbarOrder: readonly string[] | undefined,
|
|
449
|
+
): readonly string[] {
|
|
450
|
+
if (!toolbarOrder || toolbarOrder.length === 0) {
|
|
451
|
+
return buttonIds;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const available = new Set(buttonIds);
|
|
455
|
+
const ordered: string[] = [];
|
|
456
|
+
const included = new Set<string>();
|
|
457
|
+
for (const id of toolbarOrder) {
|
|
458
|
+
if (available.has(id) && !included.has(id)) {
|
|
459
|
+
ordered.push(id);
|
|
460
|
+
included.add(id);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const id of buttonIds) {
|
|
465
|
+
if (!included.has(id)) {
|
|
466
|
+
ordered.push(id);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return ordered;
|
|
471
|
+
}
|