@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,282 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { OVERLAY_Z_INDEX } from "../constants/ui";
|
|
3
|
+
|
|
4
|
+
export const editorStyles: Record<string, CSSProperties> = {
|
|
5
|
+
container: {
|
|
6
|
+
display: "grid",
|
|
7
|
+
gap: "0.75rem",
|
|
8
|
+
padding: "1rem",
|
|
9
|
+
border: "1px solid var(--ssml-editor-border)",
|
|
10
|
+
borderRadius: "0.5rem",
|
|
11
|
+
color: "var(--ssml-editor-color)",
|
|
12
|
+
backgroundColor: "var(--ssml-editor-bg)",
|
|
13
|
+
},
|
|
14
|
+
toolbarContainer: {
|
|
15
|
+
display: "flex",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
flexWrap: "wrap",
|
|
18
|
+
gap: "0.5rem",
|
|
19
|
+
},
|
|
20
|
+
toolbarActions: {
|
|
21
|
+
display: "flex",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
flexWrap: "wrap",
|
|
24
|
+
gap: "0.5rem",
|
|
25
|
+
},
|
|
26
|
+
toolbarSeparator: {
|
|
27
|
+
width: "1px",
|
|
28
|
+
height: "2.25rem",
|
|
29
|
+
margin: "0 0.25rem",
|
|
30
|
+
backgroundColor: "var(--ssml-editor-border)",
|
|
31
|
+
},
|
|
32
|
+
toolbarDropdown: {
|
|
33
|
+
position: "relative",
|
|
34
|
+
display: "inline-block",
|
|
35
|
+
},
|
|
36
|
+
toolbarSwitch: {
|
|
37
|
+
display: "inline-flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
gap: "0.375rem",
|
|
40
|
+
minHeight: "2.25rem",
|
|
41
|
+
color: "var(--ssml-editor-color)",
|
|
42
|
+
font: "inherit",
|
|
43
|
+
},
|
|
44
|
+
toolbarSwitchTrack: {
|
|
45
|
+
display: "inline-flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
width: "2.75rem",
|
|
48
|
+
height: "1.5rem",
|
|
49
|
+
padding: "0.1875rem",
|
|
50
|
+
border: 0,
|
|
51
|
+
borderRadius: "999px",
|
|
52
|
+
backgroundColor: "var(--ssml-editor-control-border)",
|
|
53
|
+
cursor: "pointer",
|
|
54
|
+
font: "inherit",
|
|
55
|
+
transition: "background-color 0.2s ease",
|
|
56
|
+
},
|
|
57
|
+
toolbarSwitchThumb: {
|
|
58
|
+
width: "1.125rem",
|
|
59
|
+
height: "1.125rem",
|
|
60
|
+
borderRadius: "50%",
|
|
61
|
+
backgroundColor: "var(--ssml-editor-bg)",
|
|
62
|
+
transition: "transform 0.2s ease",
|
|
63
|
+
},
|
|
64
|
+
toolbarButton: {
|
|
65
|
+
display: "inline-flex",
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
gap: "0.375rem",
|
|
68
|
+
minHeight: "2.25rem",
|
|
69
|
+
padding: "0.375rem 0.625rem",
|
|
70
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
71
|
+
borderRadius: "0.25rem",
|
|
72
|
+
color: "var(--ssml-editor-color)",
|
|
73
|
+
backgroundColor: "var(--ssml-editor-control-bg)",
|
|
74
|
+
font: "inherit",
|
|
75
|
+
cursor: "pointer",
|
|
76
|
+
},
|
|
77
|
+
toolbarButtonActive: {
|
|
78
|
+
border: "1px solid var(--ssml-editor-active-border)",
|
|
79
|
+
backgroundColor: "var(--ssml-editor-active-bg)",
|
|
80
|
+
},
|
|
81
|
+
display: {
|
|
82
|
+
display: "grid",
|
|
83
|
+
gap: "0.5rem",
|
|
84
|
+
},
|
|
85
|
+
toolbarIconOnly: {
|
|
86
|
+
justifyContent: "center",
|
|
87
|
+
minWidth: "2.25rem",
|
|
88
|
+
padding: "0.375rem",
|
|
89
|
+
},
|
|
90
|
+
toolbarIcon: {
|
|
91
|
+
display: "inline-flex",
|
|
92
|
+
width: "1.25rem",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
fontSize: "1.1rem",
|
|
95
|
+
lineHeight: 1,
|
|
96
|
+
},
|
|
97
|
+
toolbarChevron: {
|
|
98
|
+
fontSize: "0.7rem",
|
|
99
|
+
lineHeight: 1,
|
|
100
|
+
},
|
|
101
|
+
toolbarMenu: {
|
|
102
|
+
position: "fixed",
|
|
103
|
+
zIndex: OVERLAY_Z_INDEX,
|
|
104
|
+
display: "grid",
|
|
105
|
+
minWidth: "max-content",
|
|
106
|
+
maxHeight: "min(24rem, calc(100vh - 1rem))",
|
|
107
|
+
gap: "0.125rem",
|
|
108
|
+
padding: "0.25rem",
|
|
109
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
110
|
+
borderRadius: "0.25rem",
|
|
111
|
+
backgroundColor: "var(--ssml-editor-control-bg)",
|
|
112
|
+
boxShadow: "0 0.25rem 0.75rem rgb(0 0 0 / 20%)",
|
|
113
|
+
overflowY: "auto",
|
|
114
|
+
},
|
|
115
|
+
toolbarOption: {
|
|
116
|
+
padding: "0.375rem 0.5rem",
|
|
117
|
+
border: 0,
|
|
118
|
+
borderRadius: "0.125rem",
|
|
119
|
+
color: "var(--ssml-editor-color)",
|
|
120
|
+
backgroundColor: "transparent",
|
|
121
|
+
font: "inherit",
|
|
122
|
+
textAlign: "left",
|
|
123
|
+
whiteSpace: "nowrap",
|
|
124
|
+
cursor: "pointer",
|
|
125
|
+
},
|
|
126
|
+
toolbarOptionGroup: {
|
|
127
|
+
display: "grid",
|
|
128
|
+
gap: "0.125rem",
|
|
129
|
+
margin: 0,
|
|
130
|
+
padding: 0,
|
|
131
|
+
border: 0,
|
|
132
|
+
},
|
|
133
|
+
toolbarOptionGroupLabel: {
|
|
134
|
+
padding: "0.375rem 0.5rem 0.125rem",
|
|
135
|
+
color: "var(--ssml-editor-color)",
|
|
136
|
+
fontSize: "0.875rem",
|
|
137
|
+
fontWeight: 600,
|
|
138
|
+
whiteSpace: "nowrap",
|
|
139
|
+
},
|
|
140
|
+
toolbarEmpty: {
|
|
141
|
+
margin: 0,
|
|
142
|
+
padding: "0.375rem 0.5rem",
|
|
143
|
+
color: "var(--ssml-editor-color)",
|
|
144
|
+
whiteSpace: "nowrap",
|
|
145
|
+
},
|
|
146
|
+
toolbarEmptySelect: {
|
|
147
|
+
padding: "0.375rem 0.5rem",
|
|
148
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
149
|
+
borderRadius: "0.125rem",
|
|
150
|
+
color: "var(--ssml-editor-color)",
|
|
151
|
+
backgroundColor: "var(--ssml-editor-control-bg)",
|
|
152
|
+
font: "inherit",
|
|
153
|
+
textAlign: "left",
|
|
154
|
+
},
|
|
155
|
+
helpPanel: {
|
|
156
|
+
display: "grid",
|
|
157
|
+
gap: "0.5rem",
|
|
158
|
+
width: "100%",
|
|
159
|
+
padding: "0.75rem",
|
|
160
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
161
|
+
borderRadius: "0.25rem",
|
|
162
|
+
backgroundColor: "var(--ssml-editor-preview-bg)",
|
|
163
|
+
},
|
|
164
|
+
helpHeading: {
|
|
165
|
+
margin: 0,
|
|
166
|
+
fontSize: "1rem",
|
|
167
|
+
},
|
|
168
|
+
helpDescription: {
|
|
169
|
+
margin: 0,
|
|
170
|
+
lineHeight: 1.5,
|
|
171
|
+
},
|
|
172
|
+
helpList: {
|
|
173
|
+
display: "grid",
|
|
174
|
+
gap: "0.375rem",
|
|
175
|
+
margin: 0,
|
|
176
|
+
paddingLeft: "1.25rem",
|
|
177
|
+
},
|
|
178
|
+
helpItem: {
|
|
179
|
+
listStyleType: "none",
|
|
180
|
+
lineHeight: 1.45,
|
|
181
|
+
},
|
|
182
|
+
helpIcon: {
|
|
183
|
+
display: "inline-flex",
|
|
184
|
+
width: "1.25rem",
|
|
185
|
+
justifyContent: "center",
|
|
186
|
+
fontSize: "1.1rem",
|
|
187
|
+
lineHeight: 1.45,
|
|
188
|
+
},
|
|
189
|
+
helpSettingsAccordion: {
|
|
190
|
+
marginTop: "0.375rem",
|
|
191
|
+
overflow: "hidden",
|
|
192
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
193
|
+
borderRadius: "0.25rem",
|
|
194
|
+
backgroundColor: "var(--ssml-editor-control-bg)",
|
|
195
|
+
},
|
|
196
|
+
helpSettingsSummary: {
|
|
197
|
+
display: "grid",
|
|
198
|
+
gridTemplateColumns: "1rem 1.25rem minmax(0, 1fr)",
|
|
199
|
+
columnGap: "0.5rem",
|
|
200
|
+
alignItems: "start",
|
|
201
|
+
padding: "0.5rem 0.625rem",
|
|
202
|
+
cursor: "pointer",
|
|
203
|
+
},
|
|
204
|
+
helpSettingsSummaryContent: {
|
|
205
|
+
minWidth: 0,
|
|
206
|
+
},
|
|
207
|
+
helpSettingsDescription: {
|
|
208
|
+
margin: "0.5rem 0.75rem 0",
|
|
209
|
+
fontSize: "0.875rem",
|
|
210
|
+
},
|
|
211
|
+
helpSettingsList: {
|
|
212
|
+
display: "grid",
|
|
213
|
+
gap: "0.125rem",
|
|
214
|
+
margin: "0.25rem 0.75rem 0.75rem",
|
|
215
|
+
paddingLeft: "1.25rem",
|
|
216
|
+
fontSize: "0.875rem",
|
|
217
|
+
},
|
|
218
|
+
editor: {
|
|
219
|
+
boxSizing: "border-box",
|
|
220
|
+
position: "relative",
|
|
221
|
+
width: "100%",
|
|
222
|
+
minHeight: "8rem",
|
|
223
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
224
|
+
borderRadius: "0.25rem",
|
|
225
|
+
overflow: "visible",
|
|
226
|
+
},
|
|
227
|
+
selectionActions: {
|
|
228
|
+
position: "absolute",
|
|
229
|
+
zIndex: OVERLAY_Z_INDEX,
|
|
230
|
+
display: "flex",
|
|
231
|
+
alignItems: "center",
|
|
232
|
+
flexWrap: "wrap",
|
|
233
|
+
gap: "0.25rem",
|
|
234
|
+
maxWidth: "calc(100% - 1rem)",
|
|
235
|
+
padding: "0.25rem",
|
|
236
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
237
|
+
borderRadius: "0.375rem",
|
|
238
|
+
color: "var(--ssml-editor-color)",
|
|
239
|
+
backgroundColor: "var(--ssml-editor-control-bg)",
|
|
240
|
+
boxShadow: "0 0.25rem 0.75rem rgb(0 0 0 / 20%)",
|
|
241
|
+
},
|
|
242
|
+
selectionCount: {
|
|
243
|
+
padding: "0.25rem 0.375rem",
|
|
244
|
+
fontSize: "0.875rem",
|
|
245
|
+
fontWeight: 600,
|
|
246
|
+
whiteSpace: "nowrap",
|
|
247
|
+
},
|
|
248
|
+
selectionActionsDivider: {
|
|
249
|
+
width: "1px",
|
|
250
|
+
height: "1.5rem",
|
|
251
|
+
backgroundColor: "var(--ssml-editor-border)",
|
|
252
|
+
},
|
|
253
|
+
selectionActionButton: {
|
|
254
|
+
display: "inline-flex",
|
|
255
|
+
alignItems: "center",
|
|
256
|
+
gap: "0.25rem",
|
|
257
|
+
minHeight: "1.75rem",
|
|
258
|
+
padding: "0.25rem 0.375rem",
|
|
259
|
+
border: "1px solid var(--ssml-editor-control-border)",
|
|
260
|
+
borderRadius: "0.25rem",
|
|
261
|
+
color: "var(--ssml-editor-color)",
|
|
262
|
+
backgroundColor: "var(--ssml-editor-bg)",
|
|
263
|
+
font: "inherit",
|
|
264
|
+
fontSize: "0.875rem",
|
|
265
|
+
cursor: "pointer",
|
|
266
|
+
},
|
|
267
|
+
selectionActionIcon: {
|
|
268
|
+
display: "inline-flex",
|
|
269
|
+
width: "1rem",
|
|
270
|
+
justifyContent: "center",
|
|
271
|
+
lineHeight: 1,
|
|
272
|
+
},
|
|
273
|
+
error: {
|
|
274
|
+
margin: 0,
|
|
275
|
+
padding: "0.5rem 0.75rem",
|
|
276
|
+
border: "1px solid var(--ssml-editor-error)",
|
|
277
|
+
borderRadius: "0.25rem",
|
|
278
|
+
color: "var(--ssml-editor-error)",
|
|
279
|
+
backgroundColor: "var(--ssml-editor-error-bg)",
|
|
280
|
+
lineHeight: 1.5,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { formatXml, formatXmlFragment } from "../src/formatXml.ts";
|
|
4
|
+
|
|
5
|
+
test("formatXml keeps declarations, comments, and CDATA at the current depth", () => {
|
|
6
|
+
assert.equal(
|
|
7
|
+
formatXml('<?xml version="1.0"?><speak><!-- comment --><voice>text</voice><![CDATA[raw <xml>]]></speak>'),
|
|
8
|
+
'<?xml version="1.0"?>\n<speak>\n <!-- comment -->\n <voice>text</voice>\n <![CDATA[raw <xml>]]>\n</speak>',
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("formatXml handles inline text and self-closing siblings", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
formatXml(" \n<root>Hello<item>world</item><empty />!</root>\n "),
|
|
15
|
+
"<root>Hello<item>world</item><empty />!</root>",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("formatXml preserves attribute contents containing angle brackets", () => {
|
|
20
|
+
assert.equal(
|
|
21
|
+
formatXml('<root><item name="first > second" value="second"/></root>'),
|
|
22
|
+
'<root>\n <item name="first > second" value="second"/>\n</root>',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("formatXml preserves namespaces, entities, and mixed-content whitespace", () => {
|
|
27
|
+
const source =
|
|
28
|
+
'<speak xml:lang="en-US" xmlns:mstts="https://example.test/mstts"><voice name="first > second" data="& "">left <break time="500ms"/> right & text</voice></speak>';
|
|
29
|
+
|
|
30
|
+
assert.equal(
|
|
31
|
+
formatXml(source),
|
|
32
|
+
'<speak xml:lang="en-US" xmlns:mstts="https://example.test/mstts">\n <voice name="first > second" data="& "">left <break time="500ms"/> right & text</voice>\n</speak>',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("formatXml returns malformed input unchanged", () => {
|
|
37
|
+
const malformed = " \n<root><child></root> \n";
|
|
38
|
+
const incomplete = '<root attribute="unterminated>';
|
|
39
|
+
|
|
40
|
+
assert.equal(formatXml(malformed), malformed);
|
|
41
|
+
assert.equal(formatXml(incomplete), incomplete);
|
|
42
|
+
assert.equal(formatXml("<root>&unknown;</root>"), "<root>&unknown;</root>");
|
|
43
|
+
assert.equal(formatXml("<root/><second/>"), "<root/><second/>");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("formatXml removes formatting whitespace and remains idempotent", () => {
|
|
47
|
+
const source =
|
|
48
|
+
' \n<?xml version="1.0"?>\n<speak>\n <!-- comment --> \n <voice><![CDATA[raw <xml>]]></voice>\n <break time="500ms"/>\n</speak>\n ';
|
|
49
|
+
const formatted = formatXml(source);
|
|
50
|
+
|
|
51
|
+
assert.equal(
|
|
52
|
+
formatted,
|
|
53
|
+
'<?xml version="1.0"?>\n<speak>\n <!-- comment -->\n <voice>\n <![CDATA[raw <xml>]]>\n </voice>\n <break time="500ms"/>\n</speak>',
|
|
54
|
+
);
|
|
55
|
+
assert.equal(formatXml(formatted), formatted);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("formatXml formats empty elements as a stable block", () => {
|
|
59
|
+
const formatted = formatXml("<root><empty></empty></root>");
|
|
60
|
+
|
|
61
|
+
assert.equal(formatted, "<root>\n <empty>\n </empty>\n</root>");
|
|
62
|
+
assert.equal(formatXml(formatted), formatted);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("formatXml keeps empty pair elements that may contain text", () => {
|
|
66
|
+
assert.equal(
|
|
67
|
+
formatXmlFragment("<emphasis></emphasis><prosody></prosody><voice></voice>"),
|
|
68
|
+
"<emphasis>\n</emphasis>\n<prosody>\n</prosody>\n<voice>\n</voice>",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("formatXml self-closes intrinsically empty elements only", () => {
|
|
73
|
+
assert.equal(
|
|
74
|
+
formatXmlFragment("<break></break><bookmark></bookmark><mark></mark><mstts:silence></mstts:silence>"),
|
|
75
|
+
"<break/>\n<bookmark/>\n<mark/>\n<mstts:silence/>",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("formatXmlFragment formats sibling elements from the editor body", () => {
|
|
80
|
+
assert.equal(
|
|
81
|
+
formatXmlFragment('<break time="500ms"/><emphasis level="strong">Important</emphasis>'),
|
|
82
|
+
'<break time="500ms"/>\n<emphasis level="strong">Important</emphasis>',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("formatXml preserves a trailing line ending", () => {
|
|
87
|
+
const formattedDocument = formatXml("<root><child/></root>\n");
|
|
88
|
+
const formattedFragment = formatXmlFragment('<break time="500ms"/>\r\n');
|
|
89
|
+
|
|
90
|
+
assert.equal(formattedDocument, "<root>\n <child/>\n</root>\n");
|
|
91
|
+
assert.equal(formattedFragment, '<break time="500ms"/>\r\n');
|
|
92
|
+
assert.equal(formatXml(formattedDocument), formattedDocument);
|
|
93
|
+
assert.equal(formatXmlFragment(formattedFragment), formattedFragment);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("formatXml preserves repeated trailing line endings", () => {
|
|
97
|
+
assert.equal(formatXml("<root/>\n\n"), "<root/>\n\n");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("formatXmlFragment preserves mixed content and malformed input", () => {
|
|
101
|
+
const mixed = 'Hello<break time="500ms"/>world';
|
|
102
|
+
const malformed = " \n<break><emphasis></break> \n";
|
|
103
|
+
|
|
104
|
+
assert.equal(formatXmlFragment(mixed), mixed);
|
|
105
|
+
assert.equal(formatXmlFragment(` \n${mixed}\n `), mixed);
|
|
106
|
+
assert.equal(formatXmlFragment(malformed), malformed);
|
|
107
|
+
});
|