@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,126 @@
1
+ import { expect, test, type Locator, type Page } from "@playwright/test";
2
+
3
+ const JAPANESE_TEXT = "こんにちは、テストです";
4
+ const BREAK_HOVER_DESCRIPTION = "単語やその他の音声コンテンツ";
5
+ const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:3000";
6
+
7
+ test.use({
8
+ baseURL: BASE_URL,
9
+ });
10
+
11
+ function monacoEditor(page: Page) {
12
+ return page.locator(".monaco-editor");
13
+ }
14
+
15
+ async function openPlayground(page: Page) {
16
+ await page.goto("/");
17
+ await expect(page.locator('[data-ssml-editor][aria-label="SSMLエディター"]')).toBeVisible();
18
+ await expect(monacoEditor(page)).toBeVisible();
19
+ }
20
+
21
+ async function replaceEditorText(page: Page, text: string) {
22
+ const editor = monacoEditor(page);
23
+ await editor.locator(".view-lines").click();
24
+ await page.keyboard.press("ControlOrMeta+A");
25
+ await page.keyboard.type(text);
26
+ await expect.poll(async () => (await editor.locator(".view-lines").innerText()).trim()).toBe(text);
27
+ }
28
+
29
+ async function getZIndex(locator: Locator) {
30
+ return locator.evaluate((element) => Number.parseInt(getComputedStyle(element).zIndex, 10) || 0);
31
+ }
32
+
33
+ test.describe("Monaco SSML editor", () => {
34
+ test("accepts Japanese keyboard input and wraps the selection with prosody", async ({ page, browserName }) => {
35
+ test.skip(browserName !== "chromium", "This E2E spec requires Chromium.");
36
+ await openPlayground(page);
37
+ await replaceEditorText(page, JAPANESE_TEXT);
38
+
39
+ const editor = monacoEditor(page);
40
+ await page.keyboard.press("Escape");
41
+ await editor.locator(".view-lines").click();
42
+ await page.keyboard.press("ControlOrMeta+A");
43
+
44
+ const rateButton = page.getByRole("button", { name: "速度", exact: true });
45
+ await rateButton.click();
46
+
47
+ const rateMenu = page.getByRole("menu", { name: "速度", exact: true });
48
+ await expect(rateMenu).toBeVisible();
49
+ await rateMenu.getByRole("menuitem", { name: "遅い速度", exact: true }).click();
50
+
51
+ await expect
52
+ .poll(async () => (await editor.locator(".view-lines").innerText()).trim())
53
+ .toBe(`<prosody rate="slow">${JAPANESE_TEXT}</prosody>`);
54
+ await expect(page.locator(".output code")).toContainText(`<prosody rate="slow">${JAPANESE_TEXT}</prosody>`);
55
+ });
56
+
57
+ test("keeps the break hover visible and puts toolbar menus above Monaco context actions", async ({
58
+ page,
59
+ browserName,
60
+ }) => {
61
+ test.skip(browserName !== "chromium", "This E2E spec requires Chromium.");
62
+ await openPlayground(page);
63
+
64
+ const editor = monacoEditor(page);
65
+ await editor.locator(".view-lines").click();
66
+ await page.keyboard.press("ControlOrMeta+Home");
67
+
68
+ const breakButton = page.getByRole("button", { name: "間", exact: true });
69
+ await breakButton.click();
70
+ const breakMenu = page.getByRole("menu", { name: "間", exact: true });
71
+ await expect(breakMenu).toBeVisible();
72
+ await breakMenu.getByRole("menuitem", { name: "500ミリ秒の無音", exact: true }).click();
73
+
74
+ const breakLine = editor.locator(".view-line").filter({ hasText: '<break time="500ms"/>' }).first();
75
+ await expect(breakLine).toBeVisible();
76
+ const breakLineBox = await breakLine.boundingBox();
77
+ if (!breakLineBox) {
78
+ throw new Error("The inserted break tag is not laid out in Monaco.");
79
+ }
80
+
81
+ await page.mouse.move(breakLineBox.x + breakLineBox.width / 2, breakLineBox.y + breakLineBox.height / 2);
82
+
83
+ const hover = page.locator(".monaco-hover:visible").filter({ hasText: BREAK_HOVER_DESCRIPTION }).first();
84
+ await expect(hover).toBeVisible();
85
+ const hoverState = await hover.evaluate((element) => {
86
+ const bounds = element.getBoundingClientRect();
87
+ const centerX = bounds.left + bounds.width / 2;
88
+ const centerY = bounds.top + bounds.height / 2;
89
+ const topElement = document.elementFromPoint(centerX, centerY);
90
+
91
+ return {
92
+ isInViewport:
93
+ bounds.left >= 0 &&
94
+ bounds.top >= 0 &&
95
+ bounds.right <= window.innerWidth &&
96
+ bounds.bottom <= window.innerHeight,
97
+ isTopmost: topElement instanceof Node && element.contains(topElement),
98
+ zIndex: Number.parseInt(getComputedStyle(element).zIndex, 10) || 0,
99
+ };
100
+ });
101
+ expect(hoverState.isInViewport).toBe(true);
102
+ expect(hoverState.isTopmost).toBe(true);
103
+ expect(hoverState.zIndex).toBeGreaterThan(0);
104
+
105
+ await page.mouse.move(10, 10);
106
+ await editor.locator(".view-lines").click({ button: "right" });
107
+ const contextMenu = page.locator(".context-view:visible").first();
108
+ await expect(contextMenu).toBeVisible();
109
+ const contextMenuZIndex = await getZIndex(contextMenu);
110
+
111
+ await page.keyboard.press("Escape");
112
+ await expect(contextMenu).toBeHidden();
113
+ await breakButton.click();
114
+ await expect(breakMenu).toBeVisible();
115
+ const toolbarMenuZIndex = await getZIndex(breakMenu);
116
+ expect(toolbarMenuZIndex).toBeGreaterThan(contextMenuZIndex);
117
+
118
+ await breakMenu.getByRole("menuitem", { name: "500ミリ秒の無音", exact: true }).click();
119
+ await expect
120
+ .poll(async () => {
121
+ const output = await page.locator(".output code").textContent();
122
+ return output?.match(/<break time="500ms"\/>/g)?.length ?? 0;
123
+ })
124
+ .toBe(2);
125
+ });
126
+ });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@ssml-builder-js/ssml-editor-react",
3
+ "version": "2.4.0",
4
+ "description": "React-based SSML editor component",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/nitta-a/SSML-Builder.git",
9
+ "directory": "packages/ssml-editor-react"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/nitta-a/SSML-Builder/issues"
13
+ },
14
+ "homepage": "https://github.com/nitta-a/SSML-Builder#ssml-editor-react",
15
+ "main": "dist/index.js",
16
+ "module": "dist/index.mjs",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.mjs",
22
+ "require": "./dist/index.js"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "vitest run --config vitest.config.mjs test/randomized-editor-invariants.test.ts test/ui-components.test.tsx && node --experimental-strip-types --test test/index.test.ts test/format-xml-edge-cases.test.ts",
32
+ "typecheck": "tsc --build"
33
+ },
34
+ "devDependencies": {
35
+ "@testing-library/react": "^16.3.2",
36
+ "@testing-library/user-event": "^14.6.3",
37
+ "@types/react": "^19.2.18",
38
+ "@types/react-dom": "^19.2.4",
39
+ "fast-check": "^4.9.0",
40
+ "jsdom": "^30.0.1",
41
+ "typescript": "^6.0.3",
42
+ "vitest": "^4.1.10"
43
+ },
44
+ "peerDependencies": {
45
+ "@monaco-editor/react": "^4.7.0",
46
+ "react": ">=18.2.0 <20",
47
+ "react-dom": ">=18.2.0 <20"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@monaco-editor/react": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@ssml-builder-js/ssml-core": "^2.4.0"
56
+ }
57
+ }