@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/index.d.mts +288 -0
  3. package/dist/index.d.ts +288 -0
  4. package/dist/index.js +5423 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +5385 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/e2e/monaco-editor.spec.ts +126 -0
  9. package/package.json +57 -0
  10. package/src/SsmlEditor.tsx +1302 -0
  11. package/src/buttonVisibility.ts +36 -0
  12. package/src/clearSsmlDocument.ts +57 -0
  13. package/src/components/popovers/InsertionPopover.tsx +136 -0
  14. package/src/components/popovers/InsertionPopovers.tsx +47 -0
  15. package/src/components/popovers/ProsodyPopovers.tsx +78 -0
  16. package/src/components/popovers/TextPopovers.tsx +8 -0
  17. package/src/components/popovers/TimingPopovers.tsx +8 -0
  18. package/src/constants/ssmlPresets.ts +660 -0
  19. package/src/constants/ui.ts +11 -0
  20. package/src/editableSsml.ts +251 -0
  21. package/src/formatXml.ts +600 -0
  22. package/src/hooks/useSsmlEditorState.ts +704 -0
  23. package/src/hooks/useSsmlMonaco.ts +393 -0
  24. package/src/index.tsx +50 -0
  25. package/src/locales.ts +435 -0
  26. package/src/ssmlCodeAction.ts +144 -0
  27. package/src/ssmlCodeLens.ts +226 -0
  28. package/src/ssmlCompletion.ts +129 -0
  29. package/src/ssmlContext.ts +196 -0
  30. package/src/ssmlDiagnostics.ts +191 -0
  31. package/src/ssmlHover.ts +703 -0
  32. package/src/ssmlInsertion.ts +75 -0
  33. package/src/ssmlInsertions.ts +471 -0
  34. package/src/styles/editorStyles.ts +282 -0
  35. package/test/format-xml-edge-cases.test.ts +107 -0
  36. package/test/index.test.ts +597 -0
  37. package/test/randomized-editor-invariants.test.ts +520 -0
  38. package/test/ui-components.test.tsx +854 -0
  39. package/tsconfig.json +10 -0
  40. package/tsup.config.ts +17 -0
  41. package/vitest.config.mjs +10 -0
@@ -0,0 +1,251 @@
1
+ import { buildSsml, parseSsml } from "@ssml-builder-js/ssml-core";
2
+ import type { ProsodyElement, SsmlDocument, SsmlElement, SsmlNode, VoiceElement } from "@ssml-builder-js/ssml-core";
3
+ import { INTRINSICALLY_EMPTY_ELEMENTS } from "./formatXml";
4
+
5
+ interface EditableStartTag {
6
+ name: string;
7
+ selfClosing: boolean;
8
+ }
9
+
10
+ function isSsmlElement(node: SsmlNode): node is SsmlElement {
11
+ return typeof node !== "string" && node.type !== "text";
12
+ }
13
+
14
+ function isVoice(element: SsmlElement): element is VoiceElement {
15
+ return element.type === "voice";
16
+ }
17
+
18
+ function isProsody(element: SsmlElement): element is ProsodyElement {
19
+ return element.type === "prosody";
20
+ }
21
+
22
+ function getSsmlElementName(element: SsmlElement): string {
23
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
24
+ }
25
+
26
+ function findEditableTagEnd(source: string, start: number): number {
27
+ let quote: string | undefined;
28
+ for (let index = start + 1; index < source.length; index += 1) {
29
+ const character = source[index];
30
+ if (quote !== undefined) {
31
+ if (character === quote) {
32
+ quote = undefined;
33
+ }
34
+ continue;
35
+ }
36
+ if (character === '"' || character === "'") {
37
+ quote = character;
38
+ continue;
39
+ }
40
+ if (character === ">") {
41
+ return index;
42
+ }
43
+ }
44
+ return source.length;
45
+ }
46
+
47
+ function collectEditableStartTags(source: string): EditableStartTag[] {
48
+ const tags: EditableStartTag[] = [];
49
+ let index = 0;
50
+
51
+ while (index < source.length) {
52
+ if (source[index] !== "<") {
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (source.startsWith("<!--", index)) {
57
+ const end = source.indexOf("-->", index + 4);
58
+ index = end === -1 ? source.length : end + 3;
59
+ continue;
60
+ }
61
+ if (source.startsWith("<![CDATA[", index)) {
62
+ const end = source.indexOf("]]>", index + 9);
63
+ index = end === -1 ? source.length : end + 3;
64
+ continue;
65
+ }
66
+ if (source.startsWith("<?", index)) {
67
+ const end = source.indexOf("?>", index + 2);
68
+ index = end === -1 ? source.length : end + 2;
69
+ continue;
70
+ }
71
+ if (source.startsWith("</", index) || source.startsWith("<!", index)) {
72
+ const end = findEditableTagEnd(source, index);
73
+ index = end === source.length ? source.length : end + 1;
74
+ continue;
75
+ }
76
+
77
+ const end = findEditableTagEnd(source, index);
78
+ if (end === source.length) {
79
+ break;
80
+ }
81
+ const raw = source.slice(index, end + 1);
82
+ const match = raw.match(/^<([A-Za-z_][A-Za-z0-9_.:-]*)/);
83
+ if (match) {
84
+ tags.push({ name: match[1], selfClosing: /\/\s*>$/.test(raw) });
85
+ }
86
+ index = end + 1;
87
+ }
88
+
89
+ return tags;
90
+ }
91
+
92
+ function preserveEmptyPairElements(
93
+ nodes: SsmlNode[],
94
+ startTags: readonly EditableStartTag[],
95
+ startTagIndex: { value: number },
96
+ ): SsmlNode[] {
97
+ return nodes.map((node) => {
98
+ if (!isSsmlElement(node)) {
99
+ return node;
100
+ }
101
+
102
+ const elementName = getSsmlElementName(node);
103
+ const startTag = startTags[startTagIndex.value];
104
+ startTagIndex.value += 1;
105
+ if (node.children === undefined || node.children.length === 0) {
106
+ return startTag?.name === elementName && !startTag.selfClosing && !INTRINSICALLY_EMPTY_ELEMENTS.has(elementName)
107
+ ? { ...node, children: [""] }
108
+ : node;
109
+ }
110
+
111
+ const children = preserveEmptyPairElements(node.children, startTags, startTagIndex);
112
+ if (children.every((child, index) => child === node.children?.[index])) {
113
+ return node;
114
+ }
115
+ return { ...node, children };
116
+ });
117
+ }
118
+
119
+ function getDocumentChildren(document: SsmlDocument): SsmlNode[] {
120
+ return document.children ?? (document.content === undefined ? [] : [document.content]);
121
+ }
122
+
123
+ function findFirstElementPath(
124
+ nodes: SsmlNode[],
125
+ predicate: (element: SsmlElement) => boolean,
126
+ ancestors: readonly SsmlElement[] = [],
127
+ ): SsmlElement[] | undefined {
128
+ for (const node of nodes) {
129
+ if (!isSsmlElement(node)) {
130
+ continue;
131
+ }
132
+
133
+ const path = [...ancestors, node];
134
+ if (predicate(node)) {
135
+ return path;
136
+ }
137
+
138
+ const childPath = findFirstElementPath(node.children ?? [], predicate, path);
139
+ if (childPath) {
140
+ return childPath;
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+
146
+ function updateFirstElement<T extends SsmlElement>(
147
+ nodes: SsmlNode[],
148
+ predicate: (element: SsmlElement) => element is T,
149
+ update: (element: T) => SsmlElement,
150
+ ): { nodes: SsmlNode[]; updated: boolean } {
151
+ let updated = false;
152
+ const nextNodes = nodes.map((node) => {
153
+ if (updated || !isSsmlElement(node)) {
154
+ return node;
155
+ }
156
+
157
+ if (predicate(node)) {
158
+ updated = true;
159
+ return update(node);
160
+ }
161
+
162
+ if (node.children) {
163
+ const result = updateFirstElement(node.children, predicate, update);
164
+ if (result.updated) {
165
+ updated = true;
166
+ return { ...node, children: result.nodes };
167
+ }
168
+ }
169
+
170
+ return node;
171
+ });
172
+
173
+ return { nodes: nextNodes, updated };
174
+ }
175
+
176
+ function withChildren(document: SsmlDocument, children: SsmlNode[]): SsmlDocument {
177
+ const nextDocument: SsmlDocument = { ...document, children };
178
+ if (nextDocument.content !== undefined) {
179
+ delete nextDocument.content;
180
+ }
181
+ return nextDocument;
182
+ }
183
+
184
+ function parseEditableText(value: string, lang: string): SsmlNode[] {
185
+ try {
186
+ const wrapper = buildSsml({
187
+ version: "1.0",
188
+ lang,
189
+ children: [],
190
+ });
191
+ const openingTagEnd = wrapper.indexOf(">") + 1;
192
+ const children = parseSsml(`${wrapper.slice(0, openingTagEnd)}${value}</speak>`).children ?? [];
193
+ return children.some(isSsmlElement)
194
+ ? preserveEmptyPairElements(children, collectEditableStartTags(value), { value: 0 })
195
+ : [value];
196
+ } catch {
197
+ return [value];
198
+ }
199
+ }
200
+
201
+ function serializeEditableText(nodes: SsmlNode[], lang: string): string {
202
+ if (nodes.length === 1 && typeof nodes[0] === "string") {
203
+ return nodes[0];
204
+ }
205
+
206
+ const xml = buildSsml({
207
+ version: "1.0",
208
+ lang,
209
+ children: nodes,
210
+ });
211
+ const contentStart = xml.indexOf(">") + 1;
212
+ return xml.slice(contentStart, -"</speak>".length);
213
+ }
214
+
215
+ export function getEditableRegion(document: SsmlDocument): { children: SsmlNode[]; voiceName?: string } {
216
+ const children = getDocumentChildren(document);
217
+ const path = findFirstElementPath(children, isProsody) ?? findFirstElementPath(children, isVoice);
218
+ const element = path ? path[path.length - 1] : undefined;
219
+ const voice = path ? [...path].reverse().find(isVoice) : undefined;
220
+ return {
221
+ children: element?.children ?? children,
222
+ ...(voice ? { voiceName: voice.name } : {}),
223
+ };
224
+ }
225
+
226
+ export function getEditableText(document: SsmlDocument): string {
227
+ return serializeEditableText(getEditableRegion(document).children, document.lang);
228
+ }
229
+
230
+ export function updateEditableText(document: SsmlDocument, value: string): SsmlDocument {
231
+ const nextChildren = parseEditableText(value, document.lang);
232
+ const editableChildren = nextChildren.length > 0 ? nextChildren : [value];
233
+ const children = getDocumentChildren(document);
234
+ const prosodyResult = updateFirstElement(children, isProsody, (prosody) => ({
235
+ ...prosody,
236
+ children: editableChildren,
237
+ }));
238
+ if (prosodyResult.updated) {
239
+ return withChildren(document, prosodyResult.nodes);
240
+ }
241
+
242
+ const voiceResult = updateFirstElement(children, isVoice, (voice) => ({
243
+ ...voice,
244
+ children: editableChildren,
245
+ }));
246
+ if (voiceResult.updated) {
247
+ return withChildren(document, voiceResult.nodes);
248
+ }
249
+
250
+ return withChildren(document, editableChildren);
251
+ }