@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,191 @@
1
+ import type { Monaco, OnMount } from "@monaco-editor/react";
2
+ import { validateSsml } from "@ssml-builder-js/ssml-core";
3
+ import { SSML_ATTRIBUTE_PRESETS } from "./constants/ssmlPresets";
4
+
5
+ const EDITABLE_SSML_PREFIX = '<speak version="1.0" xml:lang="en-US">';
6
+ const EDITABLE_SSML_SUFFIX = "</speak>";
7
+
8
+ export const SSML_DIAGNOSTIC_OWNER = "ssml";
9
+
10
+ export const SSML_DIAGNOSTIC_CODES = {
11
+ INVALID_ATTR_VALUE: "INVALID_ATTR_VALUE",
12
+ MISSING_TIME_UNIT: "MISSING_TIME_UNIT",
13
+ SYNTAX_ERROR: "SSML_SYNTAX_ERROR",
14
+ UNCLOSED_TAG: "UNCLOSED_TAG",
15
+ } as const;
16
+
17
+ export type SsmlDiagnosticCode = (typeof SSML_DIAGNOSTIC_CODES)[keyof typeof SSML_DIAGNOSTIC_CODES];
18
+
19
+ export type SsmlDiagnosticMarkerCode =
20
+ | {
21
+ value: typeof SSML_DIAGNOSTIC_CODES.INVALID_ATTR_VALUE;
22
+ suggestedValue: string;
23
+ }
24
+ | {
25
+ value: typeof SSML_DIAGNOSTIC_CODES.UNCLOSED_TAG;
26
+ target: string;
27
+ };
28
+
29
+ export type MonacoEditor = Parameters<OnMount>[0];
30
+ export type MonacoModel = NonNullable<ReturnType<MonacoEditor["getModel"]>>;
31
+
32
+ export interface SsmlSyntaxError {
33
+ code: SsmlDiagnosticCode | SsmlDiagnosticMarkerCode;
34
+ length?: number;
35
+ message: string;
36
+ offset: number;
37
+ }
38
+
39
+ const MISSING_TIME_UNIT_PATTERN = /<break\b[^>]*?\s+time\s*=\s*(["'])(\s*)(\d+(?:\.\d+)?)(\s*)\1/i;
40
+ const OPEN_TAG_PATTERN = /<([A-Za-z_][A-Za-z0-9_.:-]*)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/g;
41
+ const ATTRIBUTE_PATTERN = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
42
+ const UNCLOSED_TAG_MESSAGE_PATTERN =
43
+ /^(?:Unclosed XML element: <([^>]+)>|Mismatched closing element: expected <\/([^>]+)> but found <\/speak>)$/;
44
+
45
+ function getSsmlAttributePresets(tagName: string, attributeName: string): readonly string[] | undefined {
46
+ const tagPresets = Object.entries(SSML_ATTRIBUTE_PRESETS).find(
47
+ ([presetTagName]) => presetTagName.toLowerCase() === tagName.toLowerCase(),
48
+ )?.[1];
49
+
50
+ return Object.entries(tagPresets ?? {}).find(
51
+ ([presetAttributeName]) => presetAttributeName.toLowerCase() === attributeName.toLowerCase(),
52
+ )?.[1];
53
+ }
54
+
55
+ function findInvalidAttribute(value: string): {
56
+ attributeName: string;
57
+ attributeValue: string;
58
+ offset: number;
59
+ suggestedValue: string;
60
+ tagName: string;
61
+ } | null {
62
+ for (const tagMatch of value.matchAll(OPEN_TAG_PATTERN)) {
63
+ const tagName = tagMatch[1];
64
+ const tagContent = tagMatch[2];
65
+ if (!tagName || tagContent === undefined) {
66
+ continue;
67
+ }
68
+
69
+ for (const attributeMatch of tagContent.matchAll(ATTRIBUTE_PATTERN)) {
70
+ const attributeName = attributeMatch[1];
71
+ const quote = attributeMatch[2];
72
+ const attributeValue = attributeMatch[3];
73
+ if (!attributeName || !quote || attributeValue === undefined) {
74
+ continue;
75
+ }
76
+
77
+ const presets = getSsmlAttributePresets(tagName, attributeName);
78
+ if (!presets || presets.includes(attributeValue)) {
79
+ continue;
80
+ }
81
+
82
+ const suggestedValue = presets.includes("x-fast") ? "x-fast" : presets[0];
83
+ if (!suggestedValue) {
84
+ continue;
85
+ }
86
+
87
+ const attributeOffset = attributeMatch.index ?? 0;
88
+ const quoteOffset = attributeMatch[0].indexOf(quote);
89
+ return {
90
+ attributeName,
91
+ attributeValue,
92
+ offset: (tagMatch.index ?? 0) + 1 + tagName.length + attributeOffset + quoteOffset + 1,
93
+ suggestedValue,
94
+ tagName,
95
+ };
96
+ }
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ export function validateSsmlText(value: string): SsmlSyntaxError | null {
103
+ if (!value.includes("<")) {
104
+ return null;
105
+ }
106
+
107
+ const source = `${EDITABLE_SSML_PREFIX}${value}${EDITABLE_SSML_SUFFIX}`;
108
+ const validationError = validateSsml(source);
109
+ if (!validationError) {
110
+ const missingTimeUnit = MISSING_TIME_UNIT_PATTERN.exec(value);
111
+ if (missingTimeUnit) {
112
+ const numericValue = missingTimeUnit[3];
113
+ const offset =
114
+ (missingTimeUnit.index ?? 0) + missingTimeUnit[0].length - 1 - missingTimeUnit[4].length - numericValue.length;
115
+
116
+ return {
117
+ code: SSML_DIAGNOSTIC_CODES.MISSING_TIME_UNIT,
118
+ length: numericValue.length,
119
+ message: 'SSML time values must include a unit ("ms" or "s")',
120
+ offset,
121
+ };
122
+ }
123
+
124
+ const invalidAttribute = findInvalidAttribute(value);
125
+ if (invalidAttribute) {
126
+ return {
127
+ code: {
128
+ value: SSML_DIAGNOSTIC_CODES.INVALID_ATTR_VALUE,
129
+ suggestedValue: invalidAttribute.suggestedValue,
130
+ },
131
+ length: invalidAttribute.attributeValue.length,
132
+ message: `Invalid value "${invalidAttribute.attributeValue}" for ${invalidAttribute.tagName} attribute "${invalidAttribute.attributeName}"`,
133
+ offset: invalidAttribute.offset,
134
+ };
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ const unclosedTagMatch = UNCLOSED_TAG_MESSAGE_PATTERN.exec(validationError.message);
141
+ const unclosedTag = unclosedTagMatch?.[1] ?? unclosedTagMatch?.[2];
142
+ if (unclosedTag) {
143
+ return {
144
+ code: {
145
+ value: SSML_DIAGNOSTIC_CODES.UNCLOSED_TAG,
146
+ target: unclosedTag,
147
+ },
148
+ message: `Unclosed XML element: <${unclosedTag}>`,
149
+ offset: value.length,
150
+ };
151
+ }
152
+
153
+ return {
154
+ code: SSML_DIAGNOSTIC_CODES.SYNTAX_ERROR,
155
+ message: validationError.message,
156
+ offset: Math.min(Math.max(validationError.position - EDITABLE_SSML_PREFIX.length, 0), value.length),
157
+ };
158
+ }
159
+
160
+ type MonacoMarkerData = Parameters<Monaco["editor"]["setModelMarkers"]>[2][number];
161
+
162
+ export function clearSsmlDiagnostics(monaco: Monaco, model: MonacoModel): void {
163
+ monaco.editor.setModelMarkers(model, SSML_DIAGNOSTIC_OWNER, []);
164
+ }
165
+
166
+ export function updateSsmlDiagnostics(monaco: Monaco, model: MonacoModel): SsmlSyntaxError | null {
167
+ const value = model.getValue();
168
+ const syntaxError = validateSsmlText(value);
169
+ if (!syntaxError) {
170
+ clearSsmlDiagnostics(monaco, model);
171
+ return null;
172
+ }
173
+
174
+ const startOffset = value.length === 0 ? 0 : Math.min(syntaxError.offset, value.length - 1);
175
+ const endOffset = Math.min(startOffset + (syntaxError.length ?? 1), value.length);
176
+ const start = model.getPositionAt(startOffset);
177
+ const end = model.getPositionAt(endOffset);
178
+
179
+ monaco.editor.setModelMarkers(model, SSML_DIAGNOSTIC_OWNER, [
180
+ {
181
+ message: syntaxError.message,
182
+ severity: monaco.MarkerSeverity.Error,
183
+ code: syntaxError.code as unknown as MonacoMarkerData["code"],
184
+ startLineNumber: start.lineNumber,
185
+ startColumn: start.column,
186
+ endLineNumber: end.lineNumber,
187
+ endColumn: end.column,
188
+ },
189
+ ]);
190
+ return syntaxError;
191
+ }