diff --git a/package-lock.json b/package-lock.json
index beba6d027..a30dd0d70 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -189,18 +189,18 @@
"license": "MIT"
},
"node_modules/@astrojs/internal-helpers": {
- "version": "0.7.5",
- "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz",
- "integrity": "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==",
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.6.tgz",
+ "integrity": "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q==",
"license": "MIT"
},
"node_modules/@astrojs/markdown-remark": {
- "version": "6.3.10",
- "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.10.tgz",
- "integrity": "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A==",
+ "version": "6.3.11",
+ "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.11.tgz",
+ "integrity": "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==",
"license": "MIT",
"dependencies": {
- "@astrojs/internal-helpers": "0.7.5",
+ "@astrojs/internal-helpers": "0.7.6",
"@astrojs/prism": "3.3.0",
"github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3",
@@ -214,8 +214,8 @@
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2",
- "shiki": "^3.19.0",
- "smol-toml": "^1.5.2",
+ "shiki": "^3.21.0",
+ "smol-toml": "^1.6.0",
"unified": "^11.0.5",
"unist-util-remove-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
@@ -224,12 +224,12 @@
}
},
"node_modules/@astrojs/mdx": {
- "version": "4.3.13",
- "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-4.3.13.tgz",
- "integrity": "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q==",
+ "version": "4.3.14",
+ "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-4.3.14.tgz",
+ "integrity": "sha512-FBrqJQORVm+rkRa2TS5CjU9PBA6hkhrwLVBSS9A77gN2+iehvjq1w6yya/d0YKC7osiVorKkr3Qd9wNbl0ZkGA==",
"license": "MIT",
"dependencies": {
- "@astrojs/markdown-remark": "6.3.10",
+ "@astrojs/markdown-remark": "6.3.11",
"@mdx-js/mdx": "^3.1.1",
"acorn": "^8.15.0",
"es-module-lexer": "^1.7.0",
@@ -749,9 +749,9 @@
}
},
"node_modules/@astrojs/react/node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@@ -843,9 +843,9 @@
}
},
"node_modules/@astrojs/starlight": {
- "version": "0.37.6",
- "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.37.6.tgz",
- "integrity": "sha512-wQrKwH431q+8FsLBnNQeG+R36TMtEGxTQ2AuiVpcx9APcazvL3n7wVW8mMmYyxX0POjTnxlcWPkdMGR3Yj1L+w==",
+ "version": "0.37.7",
+ "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.37.7.tgz",
+ "integrity": "sha512-KyBnou8aKIlPJUSNx6a1SN7XyH22oj/VAvTGC+Edld4Bnei1A//pmCRTBvSrSeoGrdUjK0ErFUfaEhhO1bPfDg==",
"license": "MIT",
"dependencies": {
"@astrojs/markdown-remark": "^6.3.1",
@@ -953,12 +953,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
- "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -967,29 +967,29 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
- "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
- "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/generator": "^7.28.6",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -1021,13 +1021,13 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
- "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -1050,13 +1050,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
- "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@babel/helper-validator-option": "^7.27.1",
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -1161,9 +1161,9 @@
}
},
"node_modules/@babel/helper-globals": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
- "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1184,27 +1184,27 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
- "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
- "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1227,9 +1227,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
- "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1286,27 +1286,27 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1328,25 +1328,25 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
- "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
- "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.0"
+ "@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1955,16 +1955,16 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
- "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz",
+ "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^7.28.3",
- "@babel/helper-plugin-utils": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.5"
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -2685,31 +2685,31 @@
}
},
"node_modules/@babel/template": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
- "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
- "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/generator": "^7.28.6",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.6",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -2717,13 +2717,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -2834,9 +2834,9 @@
}
},
"node_modules/@chakra-ui/cli": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-3.35.0.tgz",
- "integrity": "sha512-QZzMuLPKZJMdI6wgtTT3muDtTYXcqyT+pmB8fkS6pHSzRZ5bxiq+vI6F9qvAW2VpjvbhcsRXGW2VLgaAI6BfDw==",
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-3.36.0.tgz",
+ "integrity": "sha512-I41JaWdjaJFJGO5XIdQbol30YFcaWfjz/TTEECYSvyew7KN0aqw9niTBvO1DdBvXn+3g1ycWJ9Sjg3NzDW8S9A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2844,7 +2844,7 @@
"@babel/parser": "^7.26.0",
"@babel/plugin-transform-typescript": "^7.26.0",
"@clack/prompts": "1.0.1",
- "@pandacss/is-valid-prop": "1.10.0",
+ "@pandacss/is-valid-prop": "1.11.1",
"@types/babel__core": "^7.20.5",
"@types/cli-table": "^0.3.4",
"@types/debug": "^4.1.12",
@@ -4096,25 +4096,21 @@
}
},
"node_modules/@electron-forge/cli": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.11.1.tgz",
- "integrity": "sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.11.2.tgz",
+ "integrity": "sha512-c+C4ndLfHbxwZuCn9G8iT9wD/woLdaVkoSVjAIbj+0nJhi8UmiVsz/+Gxlj4cvhMRTzBMBxudstLU7RocMikfg==",
"dev": true,
"funding": [
{
- "type": "individual",
- "url": "https://github.com/sponsors/malept"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-cli?utm_medium=referral&utm_source=npm_fund"
+ "type": "opencollective",
+ "url": "https://opencollective.com/electron"
}
],
"license": "MIT",
"dependencies": {
- "@electron-forge/core": "7.11.1",
- "@electron-forge/core-utils": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/core": "7.11.2",
+ "@electron-forge/core-utils": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
"@electron/get": "^3.0.0",
"@inquirer/prompts": "^6.0.1",
"@listr2/prompt-adapter-inquirer": "^2.0.22",
@@ -4205,33 +4201,29 @@
}
},
"node_modules/@electron-forge/core": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.11.1.tgz",
- "integrity": "sha512-YtuPLzggPKPabFAD2rOZFE0s7f4KaUTpGRduhSMbZUqpqD1TIPyfoDBpYiZvao3Ht8pyZeOJjbzcC0LpFs9gIQ==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.11.2.tgz",
+ "integrity": "sha512-RbOvlCahSlYBkY1XFgD5QuoifZltEY3ezYGqJYnV1z6RiUK1DfUXwdidmclBLI9d6u8NNr9xWPv79LHVc9ZA3Q==",
"dev": true,
"funding": [
{
- "type": "individual",
- "url": "https://github.com/sponsors/malept"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-core?utm_medium=referral&utm_source=npm_fund"
+ "type": "opencollective",
+ "url": "https://opencollective.com/electron"
}
],
"license": "MIT",
"dependencies": {
- "@electron-forge/core-utils": "7.11.1",
- "@electron-forge/maker-base": "7.11.1",
- "@electron-forge/plugin-base": "7.11.1",
- "@electron-forge/publisher-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
- "@electron-forge/template-base": "7.11.1",
- "@electron-forge/template-vite": "7.11.1",
- "@electron-forge/template-vite-typescript": "7.11.1",
- "@electron-forge/template-webpack": "7.11.1",
- "@electron-forge/template-webpack-typescript": "7.11.1",
- "@electron-forge/tracer": "7.11.1",
+ "@electron-forge/core-utils": "7.11.2",
+ "@electron-forge/maker-base": "7.11.2",
+ "@electron-forge/plugin-base": "7.11.2",
+ "@electron-forge/publisher-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
+ "@electron-forge/template-base": "7.11.2",
+ "@electron-forge/template-vite": "7.11.2",
+ "@electron-forge/template-vite-typescript": "7.11.2",
+ "@electron-forge/template-webpack": "7.11.2",
+ "@electron-forge/template-webpack-typescript": "7.11.2",
+ "@electron-forge/tracer": "7.11.2",
"@electron/get": "^3.0.0",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
@@ -4239,6 +4231,7 @@
"@vscode/sudo-prompt": "^9.3.1",
"chalk": "^4.0.0",
"debug": "^4.3.1",
+ "eta": "^3.5.0",
"fast-glob": "^3.2.7",
"filenamify": "^4.1.0",
"find-up": "^5.0.0",
@@ -4248,7 +4241,6 @@
"interpret": "^3.1.1",
"jiti": "^2.4.2",
"listr2": "^7.0.2",
- "lodash": "^4.17.20",
"log-symbols": "^4.0.0",
"node-fetch": "^2.6.7",
"rechoir": "^0.8.0",
@@ -4261,13 +4253,13 @@
}
},
"node_modules/@electron-forge/core-utils": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.11.1.tgz",
- "integrity": "sha512-9UxRWVsfcziBsbAA2MS0Oz4yYovQCO2BhnGIfsbKNTBtMc/RcVSxAS0NMyymce44i43p1ZC/FqWhnt1XqYw3bQ==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.11.2.tgz",
+ "integrity": "sha512-/Fpwo44an6ulUdq94co5OOcbRCohgYNci/E6eoZZuTO9f72X+PqJkMkghqkMX3iQ8Aq2QRLkGKFwrKWJNTjL7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
"@electron/rebuild": "^3.7.0",
"@malept/cross-spawn-promise": "^2.0.0",
"chalk": "^4.0.0",
@@ -4577,13 +4569,13 @@
}
},
"node_modules/@electron-forge/maker-base": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.11.1.tgz",
- "integrity": "sha512-yhZrCGoN6bDeiB5DHFaueZ1h84AReElEj+f0hl2Ph4UbZnO0cnLpbx+Bs+XfMLAiA+beC8muB5UDK5ysfuT9BQ==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.11.2.tgz",
+ "integrity": "sha512-9934zYu9WVdgCYQXvtS+eL1oyLagsY8JlWhZmoK8yWTYftSAydH7jb3seVpfy6n85SYmY/yjcAy2lvOTy5dUwA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
"fs-extra": "^10.0.0",
"which": "^2.0.2"
},
@@ -4592,14 +4584,14 @@
}
},
"node_modules/@electron-forge/maker-deb": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/maker-deb/-/maker-deb-7.11.1.tgz",
- "integrity": "sha512-QTYiryQLYPDkq6pIfBmx0GQ6D8QatUkowH7rTlW5MnCUa0uumX0Xu7yGIjesuwW37fxT3Lv4xi+FSXMCm2eC1w==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/maker-deb/-/maker-deb-7.11.2.tgz",
+ "integrity": "sha512-MYSdCTsqzKNmsmaq7CIFh2kJdBWUZ4njxnVGrIRClzueVITk5Kots3+eQo+e5QQLvXTVn2XTNDc2nYjvtBh+Mw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/maker-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1"
+ "@electron-forge/maker-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2"
},
"engines": {
"node": ">= 16.4.0"
@@ -4609,14 +4601,14 @@
}
},
"node_modules/@electron-forge/maker-dmg": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/maker-dmg/-/maker-dmg-7.11.1.tgz",
- "integrity": "sha512-7zs5/Ewz1PcOl4N1102stFgBiFGWxU18+UPFUSd/fgf9MErBl4HBWuVNMIHyeJ/56rdfkcmTxTqE+9TBEYrZcg==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/maker-dmg/-/maker-dmg-7.11.2.tgz",
+ "integrity": "sha512-vh8/mnu3xyMbRcz3S0TV1rB7ZyGhMK60Yz2f2R+Zmj1EbAivLEq5UAvzHdB+kLttr0Dpjp2FIX4TcZ9neD4Y9g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/maker-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/maker-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -4627,15 +4619,15 @@
}
},
"node_modules/@electron-forge/maker-wix": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/maker-wix/-/maker-wix-7.11.1.tgz",
- "integrity": "sha512-yKsnfPQVBrjyjWndmSB0rqQSXAiIukwz5fuVyoCTnYpfJQojrEEMUN/eXSHA08d+rOPbB/hkMAR+QJcjJ8vO2Q==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/maker-wix/-/maker-wix-7.11.2.tgz",
+ "integrity": "sha512-uf+wmWCqTA23m0KabkQT7m+lyEtdB/Hbtn9Rb8KVSK4FPLrcSC/glYLlHKJhg6DikQraZVfD8JcUPKyZ1YTX2A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/core-utils": "7.11.1",
- "@electron-forge/maker-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/core-utils": "7.11.2",
+ "@electron-forge/maker-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
"chalk": "^4.0.0",
"electron-wix-msi": "^5.1.3",
"log-symbols": "^4.0.0",
@@ -4716,14 +4708,14 @@
}
},
"node_modules/@electron-forge/maker-zip": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-7.11.1.tgz",
- "integrity": "sha512-30rcp0AbJLfkFBX2hmO14LKXx7z9V61LffTVbTCFMh5vUB2kZvcA5xAhsBk2oUJWfGVxe1DuSEU0rDR9bUMHUg==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-7.11.2.tgz",
+ "integrity": "sha512-FWnOm2MORX/nt8psnEtID3Vnt8Blby1NkzjU3KjXBPF9kave71C3lI8KbBbCeKKyTQ/S00i2FiglKdRWQ1WNTw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/maker-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/maker-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
"cross-zip": "^4.0.0",
"fs-extra": "^10.0.0",
"got": "^11.8.5"
@@ -4733,53 +4725,53 @@
}
},
"node_modules/@electron-forge/plugin-auto-unpack-natives": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.11.1.tgz",
- "integrity": "sha512-5uRM3WNv7jIeDt8pLP3V4U2puWHPGJ/3qRuSE47RKgTp5qxpZidWHSYcEJJxjoqOL/7KFwSqKSQ/a36GoZV4Fg==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.11.2.tgz",
+ "integrity": "sha512-ktMQxO80vGjqVXVSFha+kWJscBmJaWud4Kqu7rKkbrCphr1zqaG+8EDNbkpGzV/+mhKOLuA5qII1hJ69gFNpdA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/plugin-base": "7.11.1",
- "@electron-forge/shared-types": "7.11.1"
+ "@electron-forge/plugin-base": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/plugin-base": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.11.1.tgz",
- "integrity": "sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.11.2.tgz",
+ "integrity": "sha512-tIFzEE2+D9NnCAn/rLwSkh8H59IqN+G973JNl7xmCzquO6qa7/veitZOQFGO79Zmmgkc8R/fmiCbh7LIdLS9Tg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1"
+ "@electron-forge/shared-types": "7.11.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/publisher-base": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.11.1.tgz",
- "integrity": "sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.11.2.tgz",
+ "integrity": "sha512-YwK4ZF3+uW7PBEV/ho59NVTriP3fCahskORrztUaFIdG0QP3hqMsfmo01euv98FDsBEW9UXo7/EW8t5jpmYZ0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1"
+ "@electron-forge/shared-types": "7.11.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/shared-types": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
- "integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.2.tgz",
+ "integrity": "sha512-Tcles7y74xy3jN5dEC+Pt1duJYk4c7W2xu98tjWW8RewmfKD2uHkie6I1I3yifPFZXZ/QfTlaFOOoKIQ9ENZjg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/tracer": "7.11.1",
+ "@electron-forge/tracer": "7.11.2",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
@@ -4789,14 +4781,14 @@
}
},
"node_modules/@electron-forge/template-base": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.11.1.tgz",
- "integrity": "sha512-XpTaEf+EfQw+0BlSAtSpZKYIKYvKu4raNzSGHZZoSYHp+HDC7R+MlpFQmSJiGdYQzQ14C+uxO42tVjgM0DMbpw==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.11.2.tgz",
+ "integrity": "sha512-l10I+XZRbbxFGiDLMnuXmlOppmLYmimKj6FWjEGUvft4VJFXW2BIDrLIugIGdM1nbrl/0aYjen2xRg0nZlcWzg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/core-utils": "7.11.1",
- "@electron-forge/shared-types": "7.11.1",
+ "@electron-forge/core-utils": "7.11.2",
+ "@electron-forge/shared-types": "7.11.2",
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
@@ -4808,14 +4800,14 @@
}
},
"node_modules/@electron-forge/template-vite": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.11.1.tgz",
- "integrity": "sha512-Or8Lxf4awoeUZoMTKJEw5KQDIhqOFs24WhVka3yZXxc6VgVWN79KmYKYM6uM/YMQttmafhsBhY2t1Lxo1WR/ug==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.11.2.tgz",
+ "integrity": "sha512-yFSDSu3IdyNpgLXzrwODSUyaWniHRSZI82gwcXdnJLx7D7DIDLtbx6KzEoy7QBmWZRULO3F7rLsYG+Ur7orvyA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
- "@electron-forge/template-base": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
+ "@electron-forge/template-base": "7.11.2",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -4823,14 +4815,14 @@
}
},
"node_modules/@electron-forge/template-vite-typescript": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.1.tgz",
- "integrity": "sha512-Us4AHXFb+4z+gXgZImSqMBS63oKnsQWLOhqRg321xiDzu2UcQPlwgWNb4rAEKNVC1e7LXrUNDHuBiTrQkvWXbg==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.2.tgz",
+ "integrity": "sha512-QvvdmO9Gdv+3aISI9+bBLKPBTyKaucs6HhXxz+IDALcdykIL9wVN0/BrWuwwgbwuw4BiJTyXGSPNXuJ+EWnP6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
- "@electron-forge/template-base": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
+ "@electron-forge/template-base": "7.11.2",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -4838,14 +4830,14 @@
}
},
"node_modules/@electron-forge/template-webpack": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.11.1.tgz",
- "integrity": "sha512-15lbXxi+er461MPk6sbwAOyjofAHwmQjTvxNCiNpaU2naEwbj3t0SlLq/BMr5HxnVOaMmA7+lKV9afkIom+d4Q==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.11.2.tgz",
+ "integrity": "sha512-JjG8XIZctrSZvTlii7Hqvt/pHDKigRk4PoLTQCs1TiT05ZWsn40itBm8cbja3L7bfm0ccDd3JTWWOl2G7PhlmA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
- "@electron-forge/template-base": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
+ "@electron-forge/template-base": "7.11.2",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -4853,14 +4845,14 @@
}
},
"node_modules/@electron-forge/template-webpack-typescript": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.1.tgz",
- "integrity": "sha512-6ExfFnFkHBz8rvRFTFg5HVGTC12uJpbVk4q8DVg0R8rhhxhqiVNh8lF2UPtZ2yT2UtGWjXNVlyP3Y3T6q6E3GQ==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.2.tgz",
+ "integrity": "sha512-2lwK+OrCeZgYM8WqsUXJzk94rdF0z/kA7WnAf79U3COEmAAMcFIwJtwF8c/n+52UecP3yrEE70LIGmM1sjGZJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@electron-forge/shared-types": "7.11.1",
- "@electron-forge/template-base": "7.11.1",
+ "@electron-forge/shared-types": "7.11.2",
+ "@electron-forge/template-base": "7.11.2",
"fs-extra": "^10.0.0",
"typescript": "~5.4.5",
"webpack": "^5.69.1"
@@ -4884,9 +4876,9 @@
}
},
"node_modules/@electron-forge/tracer": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
- "integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.2.tgz",
+ "integrity": "sha512-U8j5Hyj2Zt7I5PciJvPJfmEv69Gb/Da9v+k655z3Jj1cuY0UnToEJ61IhXrzlTYqo+jUKC+fgAjDJ6vltJTS0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5017,9 +5009,9 @@
}
},
"node_modules/@electron/node-gyp/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5285,9 +5277,9 @@
}
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5524,9 +5516,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
- "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -5540,9 +5532,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
- "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -5556,9 +5548,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
- "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -5572,9 +5564,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
- "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -5588,9 +5580,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
- "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -5604,9 +5596,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
- "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
@@ -5620,9 +5612,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
- "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -5636,9 +5628,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
- "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
@@ -5652,9 +5644,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
- "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -5668,9 +5660,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
- "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -5684,9 +5676,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
- "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
@@ -5700,9 +5692,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
- "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -5716,9 +5708,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
- "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -5732,9 +5724,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
- "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
@@ -5748,9 +5740,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
- "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
@@ -5764,9 +5756,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
- "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -5780,9 +5772,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
- "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
@@ -5796,9 +5788,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -5812,9 +5804,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
- "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
@@ -5828,9 +5820,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -5844,9 +5836,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
- "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -5860,9 +5852,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
- "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -5876,9 +5868,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
- "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
@@ -5892,9 +5884,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
- "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -5908,9 +5900,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
- "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -5924,9 +5916,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
- "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -8305,9 +8297,9 @@
]
},
"node_modules/@pandacss/is-valid-prop": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.10.0.tgz",
- "integrity": "sha512-OmEHkDj3BRkHYLxHwNFYJkcwF84c7PuHsWHmmmNLJnmk8z0RVlDQei4GxWjfW1bnvCihCwakJ8Xq6GDRlmcDiQ==",
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.11.1.tgz",
+ "integrity": "sha512-tvfThJmSw88Lgk5qVYzVckZ2FY8T376mGvP1cWUC5SR58AYm82ZKEFciDbGOQKcyN70rF9qqJBB5JVWgJo3JmA==",
"license": "MIT"
},
"node_modules/@peculiar/asn1-cms": {
@@ -10100,25 +10092,15 @@
}
},
"node_modules/@tootallnate/once": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
- "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
+ "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
- "node_modules/@trysound/sax": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
- "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -10998,9 +10980,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11745,9 +11727,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.11",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
- "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13045,9 +13027,9 @@
}
},
"node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -13445,14 +13427,14 @@
}
},
"node_modules/astro": {
- "version": "5.17.3",
- "resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
- "integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/astro/-/astro-5.18.2.tgz",
+ "integrity": "sha512-TnFwLnAXty5MXKPDGuKXqK4AMBXG+FH6RUdK7Oyc3gyfNoFIthT+4eRbzOK43bdRlLaZuxgciDSjgtggZ3OtGQ==",
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.13.0",
- "@astrojs/internal-helpers": "0.7.5",
- "@astrojs/markdown-remark": "6.3.10",
+ "@astrojs/internal-helpers": "0.7.6",
+ "@astrojs/markdown-remark": "6.3.11",
"@astrojs/telemetry": "3.3.0",
"@capsizecss/unpack": "^4.0.0",
"@oslojs/encoding": "^1.1.0",
@@ -14051,9 +14033,9 @@
}
},
"node_modules/astro/node_modules/svgo": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
- "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",
+ "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==",
"license": "MIT",
"dependencies": {
"commander": "^11.1.0",
@@ -14062,7 +14044,7 @@
"css-what": "^6.1.0",
"csso": "^5.0.5",
"picocolors": "^1.1.1",
- "sax": "^1.4.1"
+ "sax": "^1.5.0"
},
"bin": {
"svgo": "bin/svgo.js"
@@ -14076,9 +14058,9 @@
}
},
"node_modules/astro/node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@@ -14330,9 +14312,9 @@
}
},
"node_modules/babel-plugin-macros/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
+ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"license": "ISC",
"engines": {
"node": ">= 6"
@@ -14793,9 +14775,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -14940,9 +14922,9 @@
}
},
"node_modules/cacache/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15878,9 +15860,9 @@
}
},
"node_modules/cookie-es": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
- "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
+ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
"license": "MIT"
},
"node_modules/cookie-signature": {
@@ -16772,9 +16754,9 @@
}
},
"node_modules/defu": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
- "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/depd": {
@@ -16848,9 +16830,9 @@
}
},
"node_modules/devalue": {
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
- "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
+ "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
"license": "MIT"
},
"node_modules/devlop": {
@@ -17226,9 +17208,9 @@
}
},
"node_modules/electron": {
- "version": "39.8.2",
- "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.2.tgz",
- "integrity": "sha512-uwNJHeqm8pzQEZf/KX4XM1fJctZpHcA0Za/MlP9mOg0FAWHbKo6yRC33QbdfLX7PeNjYZC3I3nnVhE5L2CLqxw==",
+ "version": "39.8.10",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.10.tgz",
+ "integrity": "sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -18080,9 +18062,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
- "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -18092,32 +18074,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/escalade": {
@@ -18924,6 +18906,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eta": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz",
+ "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/eta-dev/eta?sponsor=1"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -19288,9 +19283,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
"dev": true,
"funding": [
{
@@ -19584,9 +19579,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -20665,14 +20660,14 @@
"license": "ISC"
},
"node_modules/h3": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz",
- "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==",
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
+ "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==",
"license": "MIT",
"dependencies": {
- "cookie-es": "^1.2.2",
+ "cookie-es": "^1.2.3",
"crossws": "^0.3.5",
- "defu": "^6.1.4",
+ "defu": "^6.1.6",
"destr": "^2.0.5",
"iron-webcrypto": "^1.2.1",
"node-mock-http": "^1.0.4",
@@ -21598,9 +21593,9 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "2.0.9",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
- "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz",
+ "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -21756,15 +21751,15 @@
}
},
"node_modules/i18next-fs-backend": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.3.tgz",
- "integrity": "sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==",
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.6.tgz",
+ "integrity": "sha512-mYGu6Nt8RIp3X/U8Y+Gej1wo5xmYWmGKLqBGMCC2OCAou5rW5epeHgHmVcw20mJs9Z9+DAPHIxQPNCgFyPRMeg==",
"license": "MIT"
},
"node_modules/i18next-http-backend": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.4.tgz",
- "integrity": "sha512-udwrBIE6cNpqn1gRAqRULq3+7MzIIuaiKRWrz++dVz5SqWW2VwXmPJtAgkI0JtMLFaADC9qNmnZAxWAhsxXx2g==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.6.tgz",
+ "integrity": "sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.1.0"
@@ -22050,9 +22045,9 @@
}
},
"node_modules/ip-address": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
- "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
+ "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -22937,9 +22932,19 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -23261,14 +23266,14 @@
"license": "MIT"
},
"node_modules/launch-editor": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz",
- "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==",
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
+ "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.1.1",
- "shell-quote": "^1.8.3"
+ "shell-quote": "^1.8.4"
}
},
"node_modules/leviathan-crypto": {
@@ -23793,9 +23798,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -25650,9 +25655,9 @@
}
},
"node_modules/micromatch/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -26023,9 +26028,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"funding": [
{
"type": "github",
@@ -27493,9 +27498,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"funding": [
{
"type": "opencollective",
@@ -27512,7 +27517,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -29269,9 +29274,9 @@
}
},
"node_modules/remark-parse-frontmatter/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
+ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"license": "ISC",
"engines": {
"node": ">= 6"
@@ -30410,9 +30415,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
+ "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -30630,9 +30635,9 @@
}
},
"node_modules/smol-toml": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
- "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
+ "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
@@ -31568,19 +31573,19 @@
"dev": true
},
"node_modules/svgo": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
- "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz",
+ "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.3.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
- "picocolors": "^1.0.0"
+ "picocolors": "^1.0.0",
+ "sax": "^1.5.0"
},
"bin": {
"svgo": "bin/svgo"
@@ -31741,9 +31746,9 @@
}
},
"node_modules/systeminformation": {
- "version": "5.31.1",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.1.tgz",
- "integrity": "sha512-6pRwxoGeV/roJYpsfcP6tN9mep6pPeCtXbUOCdVa0nme05Brwcwdge/fVNhIZn2wuUitAKZm4IYa7QjnRIa9zA==",
+ "version": "5.31.11",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.11.tgz",
+ "integrity": "sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==",
"license": "MIT",
"os": [
"darwin",
@@ -32163,9 +32168,9 @@
}
},
"node_modules/tmp-promise/node_modules/tmp": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
- "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
+ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -32961,9 +32966,9 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
- "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -33707,9 +33712,9 @@
}
},
"node_modules/uuid": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
- "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
+ "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -33790,13 +33795,13 @@
}
},
"node_modules/vite": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
- "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
"license": "MIT",
"peer": true,
"dependencies": {
- "esbuild": "^0.27.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
@@ -34240,9 +34245,9 @@
}
},
"node_modules/webpack-dev-server": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz",
- "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==",
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz",
+ "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -34790,9 +34795,9 @@
}
},
"node_modules/ws": {
- "version": "8.19.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
- "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -34960,9 +34965,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/packages/app/src/features/NoteEditor/RichEditor/RichEditor.tsx b/packages/app/src/features/NoteEditor/RichEditor/RichEditor.tsx
index 6bc2d027c..13b1ecc94 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/RichEditor.tsx
+++ b/packages/app/src/features/NoteEditor/RichEditor/RichEditor.tsx
@@ -10,7 +10,6 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
import { ImageNode } from './plugins/Image/ImageNode';
-import { FormattingNode } from './plugins/Markdown/nodes/FormattingNode';
import { RawNode } from './plugins/Markdown/nodes/RawNode';
import { RichEditorContent, RichEditorContentProps } from './RichEditorContent';
import { theme } from './theme/RichEditor';
@@ -29,7 +28,6 @@ export const RichEditor = memo((props: RichEditorContentProps) => {
nodes: [
// App specific nodes
RawNode,
- FormattingNode,
ImageNode,
// Plugins
diff --git a/packages/app/src/features/NoteEditor/RichEditor/RichEditorContent.tsx b/packages/app/src/features/NoteEditor/RichEditor/RichEditorContent.tsx
index 26b296725..2a58248d4 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/RichEditorContent.tsx
+++ b/packages/app/src/features/NoteEditor/RichEditor/RichEditorContent.tsx
@@ -22,9 +22,9 @@ import { ContextMenuPlugin } from './plugins/ContextMenu/ContextMenuPlugin';
import { EditorPanelPlugin } from './plugins/EditorPanelPlugin/EditorPanelPlugin';
import { DropFilesPlugin } from './plugins/Files/DropFilesPlugin';
import { FilesPlugin } from './plugins/Files/FilesPlugin';
-import { FormattingPlugin } from './plugins/Formatting/FormattingPlugin';
import { HighlightingPlugin } from './plugins/HighlightingPlugin/HighlightingPlugin';
import ImagesPlugin from './plugins/Image/ImagesPlugin';
+import { KeyboardControlsPlugin } from './plugins/KeyboardControlsPlugin/KeyboardControlsPlugin';
import { LinkClickHandlerPlugin } from './plugins/LinkClickHandlerPlugin';
import {
MarkdownSerializePlugin,
@@ -113,7 +113,7 @@ export const RichEditorContent = ({
-
+
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/interactions/readonly.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/interactions/readonly.dom.test.ts
new file mode 100644
index 000000000..5401bdead
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/interactions/readonly.dom.test.ts
@@ -0,0 +1,47 @@
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+
+test('Editor is not editable in readonly mode', async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({
+ value: '# Hello',
+ isReadOnly: true,
+ });
+
+ expect(screen.getByRole('textbox')).toHaveAttribute('contenteditable', 'false');
+
+ // Cannot enter text
+ await user.click(screen.getByRole('heading'));
+ await user.keyboard('Some text');
+
+ expect(screen.getByRole('textbox')).toHaveTextContent('Hello');
+ expect(screen.getByRole('textbox')).not.toHaveTextContent('Some text');
+});
+
+test('ReadOnly editor is not editable while the other editor remains editable', async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({ value: 'Editable text' });
+ await renderRichEditor({ value: 'ReadOnly text', isReadOnly: true });
+
+ const textBoxes = screen.getAllByRole('textbox');
+ expect(textBoxes).toHaveLength(2);
+
+ expect(textBoxes[0]).toHaveAttribute('contenteditable', 'true');
+ expect(textBoxes[1]).toHaveAttribute('contenteditable', 'false');
+
+ // editorA is editable — editing works
+ await user.click(within(textBoxes[0]).getByText('Editable text'));
+ await user.keyboard('New text ');
+
+ expect(textBoxes[0]).toHaveTextContent('New text Editable text');
+
+ // editorB is readOnly — editing must be ignored
+ await user.click(within(textBoxes[1]).getByText('ReadOnly text'));
+ await user.keyboard('Some text');
+
+ expect(textBoxes[1]).toHaveTextContent('ReadOnly text');
+ expect(textBoxes[1]).not.toHaveTextContent('Some text');
+ expect(textBoxes[1]).toHaveAttribute('contenteditable', 'false');
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/isolation.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/isolation.dom.test.ts
new file mode 100644
index 000000000..e221472d7
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/isolation.dom.test.ts
@@ -0,0 +1,43 @@
+import { screen, within } from '@testing-library/react';
+
+import { renderRichEditor } from './utils/renderRichEditor';
+import { textFormatClasses } from './utils/richEditorFixtures';
+import { selectContent } from './utils/utils';
+
+test('Formatting text in one editor does not affect the other editor', async () => {
+ const editorA = await renderRichEditor({ value: 'Big text' });
+ const editorB = await renderRichEditor({ value: 'Small text' });
+
+ const editorContainers = screen.getAllByRole('textbox');
+ expect(editorContainers).toHaveLength(2);
+
+ // initial state
+ expect(editorContainers[0]).toHaveTextContent('Big text');
+ expect(editorContainers[1]).toHaveTextContent('Small text');
+
+ // apply italic in editorA
+ selectContent(editorContainers[0], 'Big text');
+ await editorA.format('italic');
+ expect(within(editorContainers[0]).getByRole('emphasis')).toHaveTextContent(
+ 'Big text',
+ );
+ expect(within(editorContainers[1]).queryByRole('emphasis')).not.toBeInTheDocument();
+
+ // apply strikethrough in editorB
+ selectContent(editorContainers[1], 'Small text');
+ await editorB.format('strikethrough');
+ expect(within(editorContainers[1]).getByText('Small text')).toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+ expect(within(editorContainers[1]).getByText('Small text')).not.toHaveClass(
+ textFormatClasses.italic,
+ );
+
+ // No changes in editorA
+ expect(within(editorContainers[0]).getByText('Big text')).toHaveClass(
+ textFormatClasses.italic,
+ );
+ expect(within(editorContainers[0]).getByText('Big text')).not.toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+});
diff --git a/packages/app/src/features/NoteEditor/__tests__/render.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/render.dom.test.ts
similarity index 59%
rename from packages/app/src/features/NoteEditor/__tests__/render.dom.test.ts
rename to packages/app/src/features/NoteEditor/RichEditor/__tests__/render.dom.test.ts
index d9b693250..98bb66980 100644
--- a/packages/app/src/features/NoteEditor/__tests__/render.dom.test.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/render.dom.test.ts
@@ -3,6 +3,23 @@ import path from 'path';
import { screen, within } from '@testing-library/react';
import { renderRichEditor } from './utils/renderRichEditor';
+import { textFormatClasses } from './utils/richEditorFixtures';
+
+test('Editor updates when value changes', async () => {
+ const editor = await renderRichEditor({ value: `# Big text` });
+
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Big text');
+
+ // Run component rerender with new value
+ await editor.rerender({ value: `### Not so big text` });
+
+ expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
+ 'Not so big text',
+ );
+
+ // The old header was removed
+ expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument();
+});
test('Renders markdown correctly', async () => {
const markdown = readFileSync(
@@ -18,10 +35,13 @@ test('Renders markdown correctly', async () => {
expect(paragraph).toHaveTextContent(
'This is a regular paragraph with bold text, italic text, strikethrough text.',
);
- expect(paragraph.querySelector('b')).toHaveTextContent('bold text');
- expect(within(paragraph).getByRole('emphasis')).toHaveTextContent('italic text');
- expect(within(paragraph).getByRole('deletion')).toHaveTextContent(
- 'strikethrough text',
+
+ expect(within(paragraph).getByText('bold text')).toHaveClass(textFormatClasses.bold);
+ expect(within(paragraph).getByText('italic text')).toHaveClass(
+ textFormatClasses.italic,
+ );
+ expect(within(paragraph).getByText('strikethrough text')).toHaveClass(
+ textFormatClasses.strikethrough,
);
// Horizontal rule
@@ -102,63 +122,3 @@ test('Renders markdown correctly', async () => {
expect(within(rows[1]).getByRole('cell', { name: 'Alice' })).toBeInTheDocument();
expect(within(rows[1]).getByRole('cell', { name: 'Admin' })).toBeInTheDocument();
});
-
-test('Renders a checklist with checked and unchecked items', async () => {
- await renderRichEditor({
- value: `- [x] First item
- - [ ] Nested item
- - [x] Deep nested item
-- [ ] Second item`,
- });
-
- const checkboxes = within(screen.getByRole('textbox')).getAllByRole('checkbox');
- const [firstItem, nestedItem, deepNestedItem, secondItem] = checkboxes;
- expect(checkboxes).toHaveLength(4);
-
- // First item contains nested list
- expect(firstItem).toHaveTextContent('First item');
- expect(firstItem).toBeChecked();
-
- const [firstItemNestedList] = within(firstItem).getAllByRole('list');
- expect(firstItemNestedList).toContainElement(nestedItem);
-
- // Nested item contains deep nested list
- expect(nestedItem).toHaveTextContent('Nested item');
- expect(nestedItem).not.toBeChecked();
-
- const nestedItemList = within(nestedItem).getByRole('list');
- expect(nestedItemList).toContainElement(deepNestedItem);
-
- // Deep nested item
- expect(deepNestedItem).toHaveTextContent('Deep nested item');
- expect(deepNestedItem).toBeChecked();
-
- // Second item
- expect(secondItem).toHaveTextContent('Second item');
- expect(secondItem).not.toBeChecked();
-});
-
-test('Renders a mixed list with regular and checkbox items correctly', async () => {
- await renderRichEditor({
- value: `- [x] First item
- - Nested simple item
-- [ ] Second item`,
- });
-
- const checkboxes = screen.getAllByRole('checkbox');
- expect(checkboxes).toHaveLength(2);
- const [first, second] = checkboxes;
-
- expect(first).toHaveTextContent('First item');
- expect(first).toBeChecked();
-
- expect(second).toHaveTextContent('Second item');
- expect(second).not.toBeChecked();
-
- // Nested item - regular, not checkbox
- const nestedList = within(first).getByRole('list');
- const nestedItem = within(nestedList).getByRole('listitem');
-
- expect(nestedItem).toHaveTextContent('Nested simple item');
- expect(within(nestedItem).queryByRole('checkbox')).toBeNull();
-});
diff --git a/packages/app/src/features/NoteEditor/__tests__/resources/example.md b/packages/app/src/features/NoteEditor/RichEditor/__tests__/resources/example.md
similarity index 100%
rename from packages/app/src/features/NoteEditor/__tests__/resources/example.md
rename to packages/app/src/features/NoteEditor/RichEditor/__tests__/resources/example.md
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/formatting.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/formatting.dom.test.ts
new file mode 100644
index 000000000..6e321b218
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/formatting.dom.test.ts
@@ -0,0 +1,124 @@
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+import { textFormatClasses } from '../utils/richEditorFixtures';
+import { selectContent, selectText } from '../utils/utils';
+
+test('Text formatting can be toggled', async () => {
+ const content = 'Hello, my dear friends!';
+ const richEditor = await renderRichEditor({ value: content });
+
+ const editor = screen.getByRole('textbox');
+
+ // Apply strikethrough
+ selectContent(editor, content);
+ await richEditor.format('strikethrough');
+ expect(within(editor).getByText(content)).toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+
+ // Remove strikethrough
+ selectContent(editor, content);
+ await richEditor.format('strikethrough');
+ expect(within(editor).getByText(content)).not.toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+
+ // Apply italic
+ selectContent(editor, content);
+ await richEditor.format('italic');
+ expect(within(editor).getByText(content)).toHaveClass(textFormatClasses.italic);
+
+ // Remove italic
+ selectContent(editor, content);
+ await richEditor.format('italic');
+ expect(within(editor).getByText(content)).not.toHaveClass(textFormatClasses.italic);
+});
+
+test('One text node can have different formatting at once', async () => {
+ const content = 'Hello, my dear friends!';
+ const richEditor = await renderRichEditor({ value: content });
+
+ const editor = screen.getByRole('textbox');
+
+ selectContent(editor, content);
+ await richEditor.format('italic');
+ await richEditor.format('bold');
+ await richEditor.format('strikethrough');
+
+ expect(within(editor).getByText(content)).toHaveClass(textFormatClasses.bold);
+ expect(within(editor).getByText(content)).toHaveClass(textFormatClasses.italic);
+ expect(within(editor).getByText(content)).toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+
+ // Removes bold without breaking others formatting
+ selectContent(editor, content);
+ await richEditor.format('bold');
+
+ expect(within(editor).getByText(content)).not.toHaveClass(textFormatClasses.bold);
+ expect(within(editor).getByText(content)).toHaveClass(textFormatClasses.italic);
+ expect(within(editor).getByText(content)).toHaveClass(
+ textFormatClasses.strikethrough,
+ );
+});
+
+test('Formatting can be applied to a part of text', async () => {
+ const user = userEvent.setup();
+ const richEditor = await renderRichEditor({ value: 'Hello, my dear friends!' });
+
+ const editor = screen.getByRole('textbox');
+ await user.click(editor);
+
+ // Apply formatting
+ selectText(editor, 'friends');
+ await richEditor.format('italic');
+
+ expect(within(editor).getByText('friends')).toHaveClass(textFormatClasses.italic);
+});
+
+describe('Formatting via keyboard shortcuts', () => {
+ const cases: {
+ title: string;
+ shortcut: string;
+ formatClass: RegExp;
+ }[] = [
+ {
+ title: 'Ctrl+I must toggle Italic format',
+ shortcut: '{Control>}i{/Control}',
+ formatClass: textFormatClasses.italic,
+ },
+ {
+ title: 'Ctrl+B must toggle Bold format',
+ shortcut: '{Control>}b{/Control}',
+ formatClass: textFormatClasses.bold,
+ },
+ ];
+
+ cases.forEach(({ title, shortcut, formatClass }) =>
+ test(title, async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({
+ value: 'The quick brown fox jumps over the lazy dog',
+ });
+
+ const editor = screen.getByRole('textbox');
+ await user.click(editor);
+
+ const selectionText = 'quick brown fox';
+
+ // Apply formatting
+ selectText(editor, selectionText);
+ await user.keyboard(shortcut);
+ expect(within(editor).getByText(selectionText)).toHaveClass(formatClass);
+
+ // Remove formatting
+ selectText(editor, selectionText);
+ await user.keyboard(shortcut);
+ expect(
+ within(editor).getByText(selectionText, { exact: false }),
+ ).not.toHaveClass(formatClass);
+ }),
+ );
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/heading.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/heading.dom.test.ts
new file mode 100644
index 000000000..55255c662
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/heading.dom.test.ts
@@ -0,0 +1,34 @@
+import { screen, within } from '@testing-library/react';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+import { selectContent } from '../utils/utils';
+
+test('Updates heading level correctly', async () => {
+ const content = 'Hello, my dear friends!';
+ const richEditor = await renderRichEditor({ value: content });
+
+ const editor = screen.getByRole('textbox');
+ selectContent(editor, content);
+
+ // Plain text becomes heading
+ await richEditor.insert({ type: 'heading', data: { level: 1 } });
+ expect(within(editor).getByRole('heading', { level: 1 })).toHaveTextContent(content);
+
+ // Heading level is updated when different level applied
+ await richEditor.insert({ type: 'heading', data: { level: 3 } });
+
+ expect(within(editor).getByRole('heading', { level: 3 })).toHaveTextContent(content);
+ expect(within(editor).queryByRole('heading', { level: 1 })).not.toBeInTheDocument();
+
+ // Heading reverts to paragraph when same level applied again
+ await richEditor.insert({ type: 'heading', data: { level: 3 } });
+
+ expect(within(editor).queryByRole('heading')).not.toBeInTheDocument();
+ expect(within(editor).getByText(content)).toBeInTheDocument();
+
+ // After editing the screen should contain one paragraph with a text
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(1);
+ expect(editorChildren[0]).toHaveRole('paragraph');
+ expect(editorChildren[0]).toHaveTextContent(content);
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/image.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/image.dom.test.ts
new file mode 100644
index 000000000..a2466bd22
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/image.dom.test.ts
@@ -0,0 +1,78 @@
+import { screen, within } from '@testing-library/react';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+import { setCursorPosition } from '../utils/utils';
+
+test(`Inserts image between text nodes`, async () => {
+ const richEditor = await renderRichEditor({
+ value: `My favorite image\n\n I love cat`,
+ });
+
+ const editor = screen.getByRole('textbox');
+ setCursorPosition(editor, 'My favorite image'.length);
+
+ // Simulate inserting an image via the editor panel action
+ await richEditor.insert({
+ type: 'image',
+ data: { url: 'http://example.com/cat.png', altText: 'My cat' },
+ });
+
+ // Image nodes inserting asynchronously, so use findByRole to wait for the img to appear
+ const img = await within(editor).findByRole('img');
+ expect(img).toBeInTheDocument();
+ expect(img).toHaveAttribute('src', 'http://example.com/cat.png');
+ expect(img).toHaveAttribute('alt', 'My cat');
+
+ // Image between two texts
+ const firstText = within(editor).getByText('My favorite image');
+ const secondText = within(editor).getByText('I love cat');
+ expect(img).toAppearAfter(firstText);
+ expect(img).toAppearBefore(secondText);
+
+ // Editor contains only expected nodes
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(3);
+ Array.from(editor.children).forEach((child) => {
+ expect(child).toHaveRole('paragraph');
+ });
+
+ // First paragraph contains text and image
+ expect(editorChildren[0]).toContainElement(img);
+ expect(editorChildren[0]).toHaveTextContent('My favorite image');
+ expect(editorChildren[1]).toHaveTextContent('');
+ expect(editorChildren[2]).toHaveTextContent('I love cat');
+});
+
+test('Inserts image after block node', async () => {
+ const richEditor = await renderRichEditor({
+ value: '```js\nconst a = 1;\n```',
+ });
+
+ // Place cursor position inside the code node
+ const editor = screen.getByRole('textbox');
+ setCursorPosition(within(editor).getByRole('code'), 10);
+
+ await richEditor.insert({
+ type: 'image',
+ data: { url: 'http://example.com/cat.png', altText: 'My cat' },
+ });
+
+ // Wait before image to appear
+ const img = await within(editor).findByRole('img');
+ expect(img).toBeInTheDocument();
+
+ const codeNode = within(editor).getByRole('code');
+ expect(codeNode).toBeInTheDocument();
+
+ // Image is inserted as next sibling of the code block
+ expect(img).toAppearAfter(codeNode);
+ expect(codeNode.nextElementSibling).toContainElement(img);
+
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(2);
+ expect(editorChildren[0]).toHaveRole('code');
+ expect(editorChildren[1]).toHaveRole('paragraph');
+
+ // Image is inside the paragraph
+ expect(editorChildren[1]).toContainElement(img);
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/list.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/list.dom.test.ts
new file mode 100644
index 000000000..5ef632627
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/list.dom.test.ts
@@ -0,0 +1,165 @@
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+import { selectContent, setCursorPosition } from '../utils/utils';
+
+test('Converts an unordered list to an ordered list', async () => {
+ const richEditor = await renderRichEditor({
+ value: `- First item
+ - Nested item
+- Second item`,
+ });
+ const editor = screen.getByRole('textbox');
+
+ expect(editor.children[0].tagName).toBe('UL');
+ expect(editor.children[0].children).toHaveLength(2);
+
+ // Select text and convert to ordered list
+ selectContent(editor, 'First item');
+ await richEditor.insert({ type: 'list', data: { type: 'ordered' } });
+
+ expect(editor.children[0].tagName).toBe('OL');
+
+ const listItems = editor.children[0].children;
+ expect(listItems).toHaveLength(2);
+
+ // First item has text and nested list
+ const firstItemChildren = listItems[0].children;
+ expect(firstItemChildren).toHaveLength(2);
+ expect(listItems[0]).toHaveTextContent('First item');
+
+ const nestedListChildren = firstItemChildren[1].children;
+ expect(nestedListChildren).toHaveLength(1);
+ expect(nestedListChildren[0]).toHaveTextContent('Nested item');
+
+ expect(listItems[1].children).toHaveLength(1);
+ expect(listItems[1]).toHaveTextContent('Second item');
+});
+
+test('Pressing Enter adds a new item to the list', async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({ value: '- First item' });
+ const editor = screen.getByRole('textbox');
+
+ const initialChildren = editor.children;
+ expect(initialChildren).toHaveLength(1);
+ expect(initialChildren[0]).toHaveRole('list');
+ expect(initialChildren[0].children).toHaveLength(1);
+
+ const [firstItem] = within(editor).getAllByRole('listitem');
+ expect(firstItem).toHaveTextContent('First item');
+
+ // Place cursor and press Enter
+ await user.click(firstItem);
+ setCursorPosition(firstItem, 'First item'.length);
+ await user.keyboard('{Enter}');
+
+ // Editor contains only one list with two items
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(1);
+ expect(editorChildren[0]).toHaveRole('list');
+
+ const items = editorChildren[0].children;
+ expect(items).toHaveLength(2);
+ expect(items[0]).toHaveTextContent('First item');
+ expect(items[1]).toHaveTextContent('');
+});
+
+test('Pressing Enter on an empty last list item exits the list', async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({ value: '- First item' });
+ const editor = screen.getByRole('textbox');
+
+ const initialChildren = editor.children;
+ expect(initialChildren).toHaveLength(1);
+ expect(initialChildren[0]).toHaveRole('list');
+ expect(initialChildren[0].children).toHaveLength(1);
+
+ expect(within(editor).getAllByRole('listitem')).toHaveLength(1);
+ expect(within(editor).queryByRole('paragraph')).not.toBeInTheDocument();
+
+ // Set cursor and press Enter
+ const firstItem = within(editor).getByRole('listitem');
+ await user.click(firstItem);
+ setCursorPosition(firstItem, 'First item'.length);
+ await user.keyboard('{Enter}');
+
+ expect(within(editor).getAllByRole('listitem')).toHaveLength(2);
+ expect(within(editor).queryByRole('paragraph')).not.toBeInTheDocument();
+
+ await user.keyboard('{Enter}');
+
+ // A new empty paragraph is created
+ expect(within(editor).getByRole('paragraph')).toHaveTextContent('');
+ expect(within(editor).getAllByRole('listitem')).toHaveLength(1);
+
+ // Editor contains only list with one items and one paragraph
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(2);
+ expect(editorChildren[0]).toHaveRole('list');
+ expect(editorChildren[0].children).toHaveLength(1);
+ expect(editorChildren[1]).toHaveRole('paragraph');
+});
+
+describe('Rendering', () => {
+ test('Renders a checklist with checked and unchecked items', async () => {
+ await renderRichEditor({
+ value: `- [x] First item
+ - [ ] Nested item
+ - [x] Deep nested item
+- [ ] Second item`,
+ });
+
+ const checkboxes = within(screen.getByRole('textbox')).getAllByRole('checkbox');
+ const [firstItem, nestedItem, deepNestedItem, secondItem] = checkboxes;
+ expect(checkboxes).toHaveLength(4);
+
+ // First item contains nested list
+ expect(firstItem).toHaveTextContent('First item');
+ expect(firstItem).toBeChecked();
+
+ const [firstItemNestedList] = within(firstItem).getAllByRole('list');
+ expect(firstItemNestedList).toContainElement(nestedItem);
+
+ // Nested item contains deep nested list
+ expect(nestedItem).toHaveTextContent('Nested item');
+ expect(nestedItem).not.toBeChecked();
+
+ const nestedItemList = within(nestedItem).getByRole('list');
+ expect(nestedItemList).toContainElement(deepNestedItem);
+
+ // Deep nested item
+ expect(deepNestedItem).toHaveTextContent('Deep nested item');
+ expect(deepNestedItem).toBeChecked();
+
+ // Second item
+ expect(secondItem).toHaveTextContent('Second item');
+ expect(secondItem).not.toBeChecked();
+ });
+
+ test('Renders a mixed list with regular and checkbox items correctly', async () => {
+ await renderRichEditor({
+ value: `- [x] First item
+ - Nested simple item
+- [ ] Second item`,
+ });
+
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes).toHaveLength(2);
+ const [first, second] = checkboxes;
+
+ expect(first).toHaveTextContent('First item');
+ expect(first).toBeChecked();
+
+ expect(second).toHaveTextContent('Second item');
+ expect(second).not.toBeChecked();
+
+ // Nested item - regular, not checkbox
+ const nestedList = within(first).getByRole('list');
+ const nestedItem = within(nestedList).getByRole('listitem');
+
+ expect(nestedItem).toHaveTextContent('Nested simple item');
+ expect(within(nestedItem).queryByRole('checkbox')).toBeNull();
+ });
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/paragraph.dom.test.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/paragraph.dom.test.ts
new file mode 100644
index 000000000..6891ce474
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/spec/paragraph.dom.test.ts
@@ -0,0 +1,67 @@
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderRichEditor } from '../utils/renderRichEditor';
+import { setCursorPosition } from '../utils/utils';
+
+test('Pressing Enter inside a paragraph splits it into two paragraphs', async () => {
+ const user = userEvent.setup();
+ await renderRichEditor({ value: 'My favorite dish is cake' });
+
+ // Initial state
+ const editor = screen.getByRole('textbox');
+ expect(editor.children).toHaveLength(1);
+ const paragraph = editor.children[0];
+ expect(paragraph).toHaveRole('paragraph');
+ expect(paragraph).toHaveTextContent('My favorite dish is cake');
+
+ // Place cursor and press Enter
+ await user.click(paragraph);
+ setCursorPosition(paragraph, 'My favorite dis'.length);
+ await user.keyboard('{Enter}');
+
+ // Editor should now have two paragraphs split at the cursor
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(2);
+
+ expect(editorChildren[0]).toHaveRole('paragraph');
+ expect(editorChildren[0]).toHaveTextContent('My favorite dis');
+
+ expect(editorChildren[1]).toHaveRole('paragraph');
+ expect(editorChildren[1]).toHaveTextContent('h is cake');
+});
+
+test('Ctrl+Enter exits a block node and creates a new empty paragraph', async () => {
+ const user = userEvent.setup();
+ const content = 'This is a blockquote';
+ const richEditor = await renderRichEditor({ value: `> ${content}` });
+
+ // One blockquote with one paragraph inside
+ const editor = screen.getByRole('textbox');
+ const initialChildren = editor.children;
+ expect(initialChildren).toHaveLength(1);
+ expect(initialChildren[0]).toHaveRole('blockquote');
+ expect(initialChildren[0]).toHaveTextContent(content);
+
+ expect(within(editor).getAllByRole('paragraph')).toHaveLength(1);
+
+ // Press Ctrl+Enter to exit the blockquote
+ await user.click(within(editor).getByRole('blockquote'));
+ await user.keyboard('{Control>}{Enter}{/Control}');
+
+ // New empty paragraph is added
+ const editorChildren = editor.children;
+ expect(editorChildren).toHaveLength(2);
+ expect(editorChildren[0]).toHaveRole('blockquote');
+ expect(editorChildren[1]).toHaveRole('paragraph');
+
+ const paragraphs = within(editor).getAllByRole('paragraph');
+ expect(paragraphs).toHaveLength(2);
+ expect(paragraphs[0]).toHaveTextContent(content);
+ expect(paragraphs[1]).toHaveTextContent('');
+
+ // Cursor lands in the new paragraph - inserted content inside new paragraph
+ await richEditor.insert({ type: 'date', data: { date: '01.01.2025' } });
+ expect(within(editor).getByText('01.01.2025')).toBeInTheDocument();
+ expect(within(editor).getByRole('blockquote')).not.toHaveTextContent('01.01.2025');
+});
diff --git a/packages/app/src/features/NoteEditor/__tests__/utils/renderRichEditor.tsx b/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/renderRichEditor.tsx
similarity index 100%
rename from packages/app/src/features/NoteEditor/__tests__/utils/renderRichEditor.tsx
rename to packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/renderRichEditor.tsx
diff --git a/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/richEditorFixtures.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/richEditorFixtures.ts
new file mode 100644
index 000000000..0d2384963
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/richEditorFixtures.ts
@@ -0,0 +1,5 @@
+export const textFormatClasses = {
+ bold: /__textBold/,
+ italic: /__textItalic/,
+ strikethrough: /__textStrikethrough/,
+};
diff --git a/packages/app/src/features/NoteEditor/__tests__/utils/utils.ts b/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/utils.ts
similarity index 67%
rename from packages/app/src/features/NoteEditor/__tests__/utils/utils.ts
rename to packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/utils.ts
index 941bb261f..303d7ba74 100644
--- a/packages/app/src/features/NoteEditor/__tests__/utils/utils.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/__tests__/utils/utils.ts
@@ -50,23 +50,56 @@ export const selectContent = (
/**
* Selects the given text within a provided element
*/
-export const selectText = (element: HTMLElement, text: string) => {
- const textNode = getFirstTextNode(element);
- if (!textNode) throw new Error('No text node found in the provided element');
+export function selectText(container: Node, text: string): Range {
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
- const startPosition = textNode.textContent.indexOf(text);
- if (startPosition === -1)
- throw new Error(`Text "${text}" not found in the provided element`);
+ const nodes: Text[] = [];
+ let full = '';
+
+ while (walker.nextNode()) {
+ const n = walker.currentNode as Text;
+ nodes.push(n);
+ full += n.data;
+ }
+
+ const start = full.indexOf(text);
+ if (start === -1) throw new Error('Not found');
+
+ const end = start + text.length;
+
+ let pos = 0;
+ let startNode: Text | undefined;
+ let endNode: Text | undefined;
+ let startOffset = 0;
+ let endOffset = 0;
+
+ for (const n of nodes) {
+ const next = pos + n.data.length;
+
+ if (!startNode && start < next) {
+ startNode = n;
+ startOffset = start - pos;
+ }
+
+ if (!endNode && end <= next) {
+ endNode = n;
+ endOffset = end - pos;
+ break;
+ }
+
+ pos = next;
+ }
const range = document.createRange();
- range.setStart(textNode, startPosition);
- range.setEnd(textNode, startPosition + text.length);
+ range.setStart(startNode!, startOffset);
+ range.setEnd(endNode!, endOffset);
- window.getSelection()?.removeAllRanges();
- window.getSelection()?.addRange(range);
+ const sel = window.getSelection();
+ sel?.removeAllRanges();
+ sel?.addRange(range);
- fireEvent(document, new Event('selectionchange'));
-};
+ return range;
+}
/**
* Simulates placing the cursor at a given position within a node
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/EditorPanelPlugin.tsx b/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/EditorPanelPlugin.tsx
index e16a77533..0dfd87249 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/EditorPanelPlugin.tsx
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/EditorPanelPlugin.tsx
@@ -3,14 +3,14 @@ import {
$createParagraphNode,
$createTextNode,
$getRoot,
- $getSelection,
$isBlockElementNode,
$isParagraphNode,
$isRootNode,
$isTextNode,
CONTROLLED_TEXT_INSERTION_COMMAND,
+ FORMAT_TEXT_COMMAND,
} from 'lexical';
-import { $createCodeNode, $isCodeNode } from '@lexical/code';
+import { $createCodeNode } from '@lexical/code';
import { TOGGLE_LINK_COMMAND } from '@lexical/link';
import {
INSERT_CHECK_LIST_COMMAND,
@@ -20,20 +20,13 @@ import {
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from '@lexical/rich-text';
-import { $dfs } from '@lexical/utils';
import { InsertingPayloadMap, useEditorPanelContext } from '../../../EditorPanel';
import { $getCursorNode } from '../../utils/selection';
import { INSERT_FILES_COMMAND } from '../Files/FilesPlugin';
import { $createImageNode } from '../Image/ImageNode';
-import { $toggleFormatNode } from './utils/format';
-import {
- $canInsertElementsToNode,
- $findCommonAncestor,
- $getNearestSibling,
- $wrapNodes,
-} from './utils/tree';
+import { $canInsertElementsToNode, $getNearestSibling, $wrapNodes } from './utils/tree';
/**
* Plugin to handle editor panel actions about formatting and nodes insertion
@@ -45,21 +38,7 @@ export const EditorPanelPlugin = () => {
useEffect(() => {
const cleanupFormatting = onFormatting.watch((format) => {
- // TODO: support formatting selected text slices
- editor.update(() => {
- const selection = $getSelection();
- if (!selection) return;
-
- const nodes = selection.getNodes();
- if (nodes.length === 0) return;
-
- const commonAncestor = $findCommonAncestor(nodes[0], nodes, (node) =>
- $dfs(node).some(({ node }) => $isCodeNode(node)),
- );
- if (!commonAncestor) return;
-
- $toggleFormatNode(commonAncestor, format);
- });
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
});
const cleanupInserting = onInserting.watch((evt) => {
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/utils/format.tsx b/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/utils/format.tsx
deleted file mode 100644
index ebe7f2a22..000000000
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/EditorPanelPlugin/utils/format.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { $isBlockElementNode, LexicalNode } from 'lexical';
-
-import { TextFormat } from '../../../../EditorPanel';
-import { $insertAfter } from '../../../utils/selection';
-import {
- $createFormattingNode,
- $isFormattingNode,
- FormattingNode,
-} from '../../Markdown/nodes/FormattingNode';
-
-export const $getFormatNodes = (node: LexicalNode): FormattingNode[] => {
- return node.getParents().filter((node) => $isFormattingNode(node));
-};
-
-const formatMap = {
- bold: 'b',
- italic: 'em',
- strikethrough: 'del',
-} satisfies Record;
-
-export const $getFormatNode = (node: LexicalNode, format: TextFormat) => {
- const isMatch = (formatNode: FormattingNode) =>
- formatNode.getTagName() === formatMap[format];
-
- if ($isFormattingNode(node) && isMatch(node)) {
- return node;
- }
-
- return $getFormatNodes(node).find(isMatch) ?? null;
-};
-
-export const $setFormatNode = (node: LexicalNode, format: TextFormat) => {
- const parentFormat = $getFormatNode(node, format);
- if (parentFormat) return;
-
- const formattingNode = $createFormattingNode({ tag: formatMap[format] });
- if ($isBlockElementNode(node)) {
- formattingNode.append(...node.getChildren());
- node.append(formattingNode);
- return formattingNode;
- }
-
- node.replace(formattingNode);
- formattingNode.append(node);
- return formattingNode;
-};
-
-export const $removeFormatNode = (node: LexicalNode, format: TextFormat) => {
- const parentFormat = $getFormatNode(node, format);
- if (!parentFormat) return;
-
- $insertAfter(parentFormat, parentFormat.getChildren());
-};
-
-export const $toggleFormatNode = (node: LexicalNode, format: TextFormat) => {
- const parentFormat = $getFormatNode(node, format);
-
- if (!parentFormat) {
- $setFormatNode(node, format);
- } else {
- $removeFormatNode(node, format);
- }
-};
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/utils.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/utils.ts
deleted file mode 100644
index 66ba45bb9..000000000
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/utils.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { IS_BOLD, IS_ITALIC, IS_STRIKETHROUGH, TextFormatType, TextNode } from 'lexical';
-import { $findMatchingParent } from '@lexical/utils';
-
-import { $createFormattingNode, FormattingNode } from '../Markdown/nodes/FormattingNode';
-
-const formatsMap = {
- bold: {
- tag: 'b',
- code: IS_BOLD,
- },
- italic: {
- tag: 'em',
- code: IS_ITALIC,
- },
- strikethrough: {
- tag: 'del',
- code: IS_STRIKETHROUGH,
- },
-} as const;
-
-export const $convertTextNodeFormatting = (node: TextNode) => {
- const format = node.getFormat();
- if (format === 0) return;
-
- for (const [format, meta] of Object.entries(formatsMap)) {
- if (!node.hasFormat(format as TextFormatType)) continue;
-
- // Looking for parent format node with current format
- const parentFormatNode = $findMatchingParent(node, (node) => {
- if (!(node instanceof FormattingNode)) return false;
-
- return node.getTagName().toLowerCase() === meta.tag.toLowerCase();
- });
- if (parentFormatNode) continue;
-
- // Remove format
- node.toggleFormat(format as TextFormatType);
-
- // Wrap to formatting node
- const newNode = $createFormattingNode({ tag: meta.tag });
- node.replace(newNode);
- newNode.append(node);
- node.select();
- }
-};
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/FormattingPlugin.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/KeyboardControlsPlugin/KeyboardControlsPlugin.ts
similarity index 58%
rename from packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/FormattingPlugin.ts
rename to packages/app/src/features/NoteEditor/RichEditor/plugins/KeyboardControlsPlugin/KeyboardControlsPlugin.ts
index 25343657d..b01406f95 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Formatting/FormattingPlugin.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/KeyboardControlsPlugin/KeyboardControlsPlugin.ts
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import {
$createParagraphNode,
- $createTextNode,
$getSelection,
$isParagraphNode,
$isTextNode,
@@ -10,17 +9,12 @@ import {
createCommand,
ElementNode,
KEY_ENTER_COMMAND,
- KEY_SPACE_COMMAND,
- TextNode,
} from 'lexical';
import { $isCodeNode } from '@lexical/code';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $isQuoteNode } from '@lexical/rich-text';
import { mergeRegister } from '@lexical/utils';
-import { $isFormattingNode, FormattingNode } from '../Markdown/nodes/FormattingNode';
-import { $convertTextNodeFormatting } from './utils';
-
const OUT_OF_BLOCK_NODE_COMMAND = createCommand();
/**
@@ -60,20 +54,15 @@ const $getParentOfTextOnEnd = (selection: BaseSelection | null) => {
return null;
};
-export const FormattingPlugin = () => {
+/**
+ * Plugin for nodes management via keyboard
+ */
+export const KeyboardControlsPlugin = () => {
const [editor] = useLexicalComposerContext();
useEffect(
() =>
mergeRegister(
- editor.registerNodeTransform(TextNode, $convertTextNodeFormatting),
- editor.registerNodeTransform(FormattingNode, (node: FormattingNode) => {
- // Remove empty formatting nodes
- const textContent = node.getTextContent();
- if (textContent.length > 0) return;
-
- node.remove();
- }),
editor.registerCommand(
KEY_ENTER_COMMAND,
(event) => {
@@ -105,51 +94,6 @@ export const FormattingPlugin = () => {
},
COMMAND_PRIORITY_LOW,
),
- editor.registerCommand(
- KEY_SPACE_COMMAND,
- (event) => {
- // Out of inline nodes by Shift+Space
- if (!event.shiftKey) return false;
-
- const selection = $getSelection();
- if (!selection) return false;
-
- const points = selection.getStartEndPoints();
- if (!points) return false;
-
- const [start, end] = points;
-
- if (
- start.getNode() !== end.getNode() ||
- start.offset !== end.offset
- )
- return false;
-
- const focusedNode = end.getNode();
- if (!$isTextNode(focusedNode)) return false;
-
- const parent = focusedNode.getParent();
- if (
- !parent ||
- !$isFormattingNode(parent) ||
- parent.getLastChild() !== focusedNode
- )
- return false;
-
- if (end.offset !== focusedNode.getTextContentSize()) return false;
-
- const textNode = $createTextNode(' ');
- parent.insertAfter(textNode);
- textNode.select();
-
- event.stopImmediatePropagation();
- event.stopPropagation();
- event.preventDefault();
-
- return true;
- },
- COMMAND_PRIORITY_LOW,
- ),
),
[editor],
);
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/__snapshots__/markdown-roundtrips-consistency.test.ts.snap b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/__snapshots__/markdown-roundtrips-consistency.test.ts.snap
index aff132457..9b9dd5f91 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/__snapshots__/markdown-roundtrips-consistency.test.ts.snap
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/__snapshots__/markdown-roundtrips-consistency.test.ts.snap
@@ -1,5 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Bold node inside bold node 1`] = `
+"**foo bar baz**
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Check list with all checked items 1`] = `
"- [x] foo
- [x] bar
@@ -38,6 +43,11 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Header with formatting 1`] = `
+"### *All header is italic, **something bold**, ~~strikethrough~~, \`inline code\`*
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Image 1`] = `
"
"
@@ -48,6 +58,16 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Inline code 1`] = `
+"Text with \`code\`
+"
+`;
+
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Inline code with formatting 1`] = `
+"Text with ***~~\`formatted code\`~~***
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Links 1`] = `
"Some [link](https://url "Title") and [another link](proto://url)
"
@@ -65,6 +85,15 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > List with formatting 1`] = `
+"- *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
+- Plain text
+- *Italic*
+- **Bold**
+- ~~Delete~~
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Mixed list 1`] = `
"- level 1-1
- [x] level 2-1
@@ -106,6 +135,20 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Quote with formatting 1`] = `
+"> *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
+> Plain text
+> *Italic*
+> **Bold**
+> ~~Delete~~
+"
+`;
+
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Rich formatting 1`] = `
+"*All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~***
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple code 1`] = `
"\`\`\`js
const x = 42;
@@ -114,6 +157,11 @@ console.log("Your number is", x ** x);
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple formatting 1`] = `
+"Text may be **bold**, *italic*, ~~strikethrough~~.
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple post with headers 1`] = `
"# The title
@@ -153,6 +201,14 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;
+exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Table with formatting 1`] = `
+"| foo | bar |
+| ---------------------------------------------------------------------------------------------------------------------- | -------------------------- |
+| *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~ |
+| **bold** | *italic* |
+"
+`;
+
exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Text with few empty lines 1`] = `
"foo
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-roundtrips-consistency.test.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-roundtrips-consistency.test.ts
index bba1a6b17..7105ea31f 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-roundtrips-consistency.test.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-roundtrips-consistency.test.ts
@@ -6,15 +6,21 @@ import {
$convertFromMarkdownString,
$convertToMarkdownString,
$serializeAsMarkdownAST,
+ markdownProcessor,
parseMarkdownToAST,
} from '../markdownParser';
import {
detailsWithSummary,
formattedLine,
+ formattingInList,
+ formattingInQuote,
+ formattingInTable,
mixedList,
nestedQuote,
postWithHeaders,
+ richFormatting,
simpleCode,
+ simpleFormatting,
simpleQuote,
simpleTable,
unsupportedFeatures,
@@ -25,12 +31,59 @@ import {
updateEditorState,
} from './utils';
+test('Markdown parser round-trips', () => {
+ const ast = parseMarkdownToAST(richFormatting);
+ const out = markdownProcessor.stringify(ast);
+
+ expect(normalizeMarkdownTree(parseMarkdownToAST(out))).toEqual(
+ normalizeMarkdownTree(ast),
+ );
+});
+
describe('Markdown-Lexical-Markdown round-trips must be consistent on AST level', () => {
const cases = [
+ {
+ title: 'Bold node inside bold node',
+ markdown: '**foo __bar__ baz**',
+ inconsistentAST: true,
+ },
+ {
+ title: 'Inline code',
+ markdown: 'Text with `code`',
+ },
+ {
+ title: 'Inline code with formatting',
+ markdown: 'Text with ***~~`formatted code`~~***',
+ },
+ {
+ title: 'Rich formatting',
+ markdown: richFormatting,
+ },
{
title: 'Plain list',
markdown: '- foo\n - bar\n - baz',
},
+ {
+ title: 'Simple formatting',
+ markdown: simpleFormatting,
+ },
+ {
+ title: 'Header with formatting',
+ markdown:
+ '### *All header is italic, **something bold**, ~~strikethrough~~, `inline code`*',
+ },
+ {
+ title: 'List with formatting',
+ markdown: formattingInList,
+ },
+ {
+ title: 'Quote with formatting',
+ markdown: formattingInQuote,
+ },
+ {
+ title: 'Table with formatting',
+ markdown: formattingInTable,
+ },
{
title: 'List item with inline elements',
markdown: '- foo [bar](...) baz',
@@ -111,7 +164,7 @@ describe('Markdown-Lexical-Markdown round-trips must be consistent on AST level'
},
];
- cases.forEach(({ title, markdown: sourceText }) =>
+ cases.forEach(({ title, markdown: sourceText, inconsistentAST }) =>
test(title, async () => {
const { editor, destroy } = createLexicalEditorInstance();
onTestFinished(destroy);
@@ -120,11 +173,23 @@ describe('Markdown-Lexical-Markdown round-trips must be consistent on AST level'
$convertFromMarkdownString(sourceText);
});
- expect(
- editor.read(() => normalizeMarkdownTree($serializeAsMarkdownAST())),
- ).toMatchObject(normalizeMarkdownTree(parseMarkdownToAST(sourceText)));
+ // Skip AST equality check for cases where AST may be optimized
+ // Even in that cases the source and output must be equal visually
+ if (!inconsistentAST) {
+ expect(
+ editor.read(() => normalizeMarkdownTree($serializeAsMarkdownAST())),
+ ).toMatchObject(normalizeMarkdownTree(parseMarkdownToAST(sourceText)));
+ }
+
+ const out = editor.read(() => $convertToMarkdownString());
+ expect(out).toMatchSnapshot();
+
+ // Parse markdown
+ await updateEditorState(editor, () => {
+ $convertFromMarkdownString(out);
+ });
- expect(editor.read(() => $convertToMarkdownString())).toMatchSnapshot();
+ expect(editor.read(() => $convertToMarkdownString())).toBe(out);
}),
);
});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-samples/index.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-samples/index.ts
index 039915c86..9084c67c5 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-samples/index.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/markdown-samples/index.ts
@@ -1,3 +1,34 @@
+export const simpleFormatting = `
+Text may be **bold**, *italic*, ~~strikethrough~~.
+`.trim();
+
+export const richFormatting = `
+*All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~***
+`.trim();
+
+export const formattingInList = `
+- *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
+- Plain text
+- *Italic*
+- **Bold**
+- ~~Delete~~
+`.trim();
+
+export const formattingInQuote = `
+> *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
+> Plain text
+> *Italic*
+> **Bold**
+> ~~Delete~~
+`.trim();
+
+export const formattingInTable = `
+| foo | bar |
+| ---- | ----- |
+| *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~ |
+| **bold** | *italic* |
+`.trim();
+
export const mixedList = `
- level 1-1
- [x] level 2-1
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/utils.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/utils.ts
index 5010a7824..01e2a1aa1 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/utils.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/__tests__/utils.ts
@@ -11,7 +11,6 @@ import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
import { ImageNode } from '../../Image/ImageNode';
-import { FormattingNode } from '../nodes/FormattingNode';
import { RawNode } from '../nodes/RawNode';
/**
@@ -47,7 +46,6 @@ export const createLexicalEditorInstance = () => {
nodes: [
// App specific nodes
RawNode,
- FormattingNode,
ImageNode,
// Plugins
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/convertLexicalNodeToMarkdownNode.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/convertLexicalNodeToMarkdownNode.ts
index 0f464f480..d6e5fcae5 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/convertLexicalNodeToMarkdownNode.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/convertLexicalNodeToMarkdownNode.ts
@@ -1,11 +1,15 @@
-import { $isLineBreakNode, $isParagraphNode, $isTextNode, LexicalNode } from 'lexical';
+import {
+ $isLineBreakNode,
+ $isParagraphNode,
+ $isTextNode,
+ LexicalNode,
+ TextFormatType,
+} from 'lexical';
import {
Blockquote,
Break,
Code,
Content,
- Delete,
- Emphasis,
Heading,
HTML,
Image,
@@ -16,7 +20,6 @@ import {
Paragraph,
PhrasingContent,
RootContent,
- Strong,
Table,
TableCell,
TableRow,
@@ -32,7 +35,6 @@ import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
import { $isTableCellNode, $isTableNode, $isTableRowNode } from '@lexical/table';
import { $isImageNode } from '../Image/ImageNode';
-import { $isFormattingNode } from './nodes/FormattingNode';
const inlineTypes = new Set([
'text',
@@ -98,13 +100,30 @@ export const convertLexicalNodeToMarkdownNode = (node: LexicalNode): Content =>
}
if ($isTextNode(node)) {
- if (node.hasFormat('code')) {
- return u('inlineCode', {
- value: node.getTextContent(),
- }) satisfies InlineCode;
- }
-
- return u('text', { value: node.getTextContent() }) satisfies Text;
+ let wrappedNode: PhrasingContent = node.hasFormat('code')
+ ? (u('inlineCode', {
+ value: node.getTextContent(),
+ }) satisfies InlineCode)
+ : (u('text', {
+ value: node.getTextContent(),
+ }) satisfies Text);
+
+ // TODO: support all formats like super/sub, etc
+ const formatsOrder = (
+ ['bold', 'italic', 'strikethrough'] satisfies TextFormatType[]
+ ).reverse();
+ const nodesMap = {
+ italic: 'emphasis',
+ bold: 'strong',
+ strikethrough: 'delete',
+ } satisfies Partial>;
+
+ formatsOrder.forEach((format) => {
+ if (!node.hasFormat(format)) return;
+ wrappedNode = u(nodesMap[format], { children: [wrappedNode] });
+ });
+
+ return wrappedNode;
}
if ($isCodeNode(node)) {
@@ -115,33 +134,6 @@ export const convertLexicalNodeToMarkdownNode = (node: LexicalNode): Content =>
}) satisfies Code;
}
- if ($isFormattingNode(node)) {
- const tagName = node.getTagName();
- switch (tagName) {
- case 'em': {
- return u('emphasis', {
- children: node
- .getChildren()
- .map(convertLexicalNodeToMarkdownNode) as Emphasis['children'],
- }) satisfies Emphasis;
- }
- case 'del': {
- return u('delete', {
- children: node
- .getChildren()
- .map(convertLexicalNodeToMarkdownNode) as Delete['children'],
- }) satisfies Delete;
- }
- case 'b': {
- return u('strong', {
- children: node
- .getChildren()
- .map(convertLexicalNodeToMarkdownNode) as Strong['children'],
- }) satisfies Strong;
- }
- }
- }
-
if ($isHorizontalRuleNode(node)) {
return u('thematicBreak') satisfies ThematicBreak;
}
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.test.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.test.ts
new file mode 100644
index 000000000..f1dd79715
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.test.ts
@@ -0,0 +1,64 @@
+import { wait } from '@utils/time';
+
+import { createSyncContext } from './createSyncContext';
+
+test('Default value can be used anywhere', () => {
+ const ctx = createSyncContext(1);
+ expect(ctx.get()).toBe(1);
+});
+
+test('Overridden value can be accessed inside context', () => {
+ const ctx = createSyncContext(1);
+
+ expect.assertions(1);
+ ctx.use(2, () => {
+ expect(ctx.get()).toBe(2);
+ });
+});
+
+test('The nearest value must be accessible inside context', () => {
+ expect.assertions(3);
+
+ const ctx = createSyncContext(1);
+ ctx.use(2, () => {
+ ctx.use(3, () => {
+ expect(ctx.get()).toBe(3);
+
+ ctx.use(4, () => {
+ ctx.use(5, () => {
+ expect(ctx.get()).toBe(5);
+ });
+ });
+
+ expect(ctx.get()).toBe(3);
+ });
+ });
+});
+
+test('Context use can return value', () => {
+ const ctx = createSyncContext(1);
+ expect(
+ ctx.use(2, () => 'test'),
+ 'Callback may return any value',
+ ).toBe('test');
+
+ expect(
+ ctx.use(2, () => ctx.use(3, () => ctx.use(4, () => ctx.get()))),
+ 'Callback may return its context value',
+ ).toBe(4);
+});
+
+test('Async code may access context via closure', async () => {
+ const ctx = createSyncContext(1);
+
+ const getValue = () => {
+ const value = ctx.get();
+
+ return (async () => {
+ await wait(10);
+ return value;
+ })();
+ };
+
+ await expect(ctx.use(2, () => getValue())).resolves.toBe(2);
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.ts
new file mode 100644
index 000000000..526350976
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/createSyncContext.ts
@@ -0,0 +1,18 @@
+export const createSyncContext = (defaultValue: T) => {
+ let context: { current: T } | null = null;
+
+ const get = () => (context ? context.current : defaultValue);
+ const use = (value: T, callback: () => R) => {
+ // Set context
+ const currentContext = context;
+ context = { current: value };
+ try {
+ return callback();
+ } finally {
+ // Restore previous context
+ context = currentContext;
+ }
+ };
+
+ return { get, use };
+};
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/markdownParser.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/markdownParser.ts
index b418bf011..16bd97908 100644
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/markdownParser.ts
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/markdownParser.ts
@@ -5,16 +5,17 @@ import {
$createTextNode,
$getRoot,
$isTextNode,
- IS_CODE,
LexicalNode,
+ TextFormatType,
+ TextNode,
} from 'lexical';
-import { Content, Root, RootContent } from 'mdast';
+import { Content, type Root } from 'mdast';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
-import { Plugin, unified } from 'unified';
+import { unified } from 'unified';
import { u } from 'unist-builder';
-import { CONTINUE, SKIP, visit } from 'unist-util-visit';
+import { TextFormat } from '@features/NoteEditor/EditorPanel';
import { $createCodeNode } from '@lexical/code';
import { $createLinkNode } from '@lexical/link';
import { $createListItemNode, $createListNode, ListType } from '@lexical/list';
@@ -29,67 +30,10 @@ import {
import { $createImageNode } from '../Image/ImageNode';
import { convertLexicalNodeToMarkdownNode } from './convertLexicalNodeToMarkdownNode';
-import { $createFormattingNode } from './nodes/FormattingNode';
+import { createSyncContext } from './createSyncContext';
import { $createRawNode } from './nodes/RawNode';
-
-const remarkPreserveBlankLines: Plugin<[], Root> = () => {
- const ignoredNodeTypes = new Set([
- 'table',
- 'tableCell',
- 'tableRow',
- ] satisfies RootContent['type'][]);
-
- return (tree: Root) => {
- const skipNodes = new Set();
-
- visit(tree, (node) => {
- // Skip nodes with no nested elements
- if (!('children' in node)) return SKIP;
-
- // Skip ignored node types
- if (ignoredNodeTypes.has(node.type)) return SKIP;
-
- // Skip already handled nodes
- if (skipNodes.has(node)) return SKIP;
-
- const newChildren: RootContent[] = [];
- for (let i = 0; i < node.children.length; i++) {
- const current = node.children[i];
- const next = node.children[i + 1];
-
- // Collect its own children
- newChildren.push(current);
-
- // Add empty lines to preserve
- if (next && current.position && next.position) {
- const lineGap = next.position.start.line - current.position.end.line;
- // lineGap === 2 means exactly one blank line, 3 means two, etc.
- const blankLineCount = lineGap - 1;
-
- for (let b = 0; b < blankLineCount; b++) {
- const line = current.position.end.line + 1 + b;
- const emptyLine = {
- type: 'paragraph',
- children: [],
- // TODO: add tests to verify position are correct in complex cases
- position: {
- start: { line, column: 1, offset: 0 },
- end: { line, column: 1, offset: 0 },
- },
- } as RootContent;
-
- newChildren.push(emptyLine);
- skipNodes.add(emptyLine);
- }
- }
- }
-
- node.children = newChildren;
-
- return CONTINUE;
- });
- };
-};
+import { liftFormattingNodes } from './remark/remarkLiftFormatting';
+import { remarkPreserveBlankLines } from './remark/remarkPreserveBlankLines';
export const markdownProcessor = unified()
.use(remarkParse)
@@ -131,31 +75,51 @@ export const $wrapWithParagraph = (children: LexicalNode[]) => {
return p;
};
+const $setTextNodeFormat = (node: TextNode, formats: Iterable) => {
+ for (const format of formats) {
+ node.toggleFormat(format);
+ }
+};
+
+/**
+ * MDAST node type to Lexical text format
+ */
+const textFormattingMap = {
+ emphasis: 'italic',
+ strong: 'bold',
+ delete: 'strikethrough',
+} satisfies Record;
+
export const $convertFromMarkdownString = (rawMarkdown: string) => {
const mdTree = parseMarkdownToAST(rawMarkdown);
- function convertToMarkdownNode(node: Content): LexicalNode {
+ const textFormatContext = createSyncContext>(new Set());
+ function convertToMarkdownNode(node: Content): LexicalNode[] {
switch (node.type) {
case 'text': {
- return $createTextNode(node.value);
+ const t = $createTextNode(node.value);
+ $setTextNodeFormat(t, textFormatContext.get());
+ return [t];
}
case 'paragraph': {
const paragraph = $createParagraphNode();
paragraph.append(...convertToMarkdownNodes(node.children));
- return paragraph;
+ return [paragraph];
}
case 'image': {
- return $createImageNode({
- src: node.url,
- altText: node.alt || '',
- });
+ return [
+ $createImageNode({
+ src: node.url,
+ altText: node.alt || '',
+ }),
+ ];
}
case 'heading': {
const heading = $createHeadingNode(`h${node.depth}`);
heading.append(...convertToMarkdownNodes(node.children));
- return heading;
+ return [heading];
}
case 'list': {
let listType: ListType = 'bullet';
@@ -172,7 +136,7 @@ export const $convertFromMarkdownString = (rawMarkdown: string) => {
const list = $createListNode(listType);
list.append(...convertToMarkdownNodes(node.children));
- return list;
+ return [list];
}
case 'listItem': {
const listItem = $createListItemNode(node.checked ?? undefined);
@@ -180,29 +144,29 @@ export const $convertFromMarkdownString = (rawMarkdown: string) => {
$wrapWithParagraph(convertToMarkdownNodes(node.children)),
);
- return listItem;
+ return [listItem];
}
case 'link': {
const link = $createLinkNode(node.url, { title: node.title });
link.append(...convertToMarkdownNodes(node.children));
- return link;
+ return [link];
}
case 'blockquote': {
const quote = $createQuoteNode();
quote.append(...convertToMarkdownNodes(node.children));
- return quote;
+ return [quote];
}
case 'table': {
const table = $createTableNode();
table.append(...convertToMarkdownNodes(node.children));
- return table;
+ return [table];
}
case 'tableRow': {
const tableRow = $createTableRowNode();
tableRow.append(...convertToMarkdownNodes(node.children));
- return tableRow;
+ return [tableRow];
}
case 'tableCell': {
const tableCell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
@@ -211,55 +175,47 @@ export const $convertFromMarkdownString = (rawMarkdown: string) => {
p.append(...convertToMarkdownNodes(node.children));
tableCell.append(p);
- return tableCell;
+ return [tableCell];
}
case 'code': {
const code = $createCodeNode(node.lang);
code.append($createTextNode(node.value));
- return code;
+ return [code];
}
case 'inlineCode': {
const text = $createTextNode(node.value);
- text.setFormat(IS_CODE);
- return text;
- }
- case 'emphasis': {
- const format = $createFormattingNode({ tag: 'em' });
- format.append(...convertToMarkdownNodes(node.children));
-
- return format;
- }
- case 'strong': {
- const format = $createFormattingNode({ tag: 'b' });
- format.append(...convertToMarkdownNodes(node.children));
-
- return format;
+ $setTextNodeFormat(text, [...textFormatContext.get(), 'code']);
+ return [text];
}
+ // TODO: handle sub/super/etc
+ case 'emphasis':
+ case 'strong':
case 'delete': {
- const format = $createFormattingNode({ tag: 'del' });
- format.append(...convertToMarkdownNodes(node.children));
-
- return format;
+ return textFormatContext.use(
+ textFormatContext
+ .get()
+ .union(new Set([textFormattingMap[node.type]])),
+ () => convertToMarkdownNodes(node.children),
+ );
}
case 'break': {
- return $createLineBreakNode();
+ return [$createLineBreakNode()];
}
case 'thematicBreak': {
- const format = $createHorizontalRuleNode();
- return format;
+ return [$createHorizontalRuleNode()];
}
}
const rawNode = $createRawNode();
rawNode.append($createTextNode(dumpMarkdownNode(node)));
- return rawNode;
+ return [rawNode];
}
function convertToMarkdownNodes(mdTree: Content[]): LexicalNode[] {
const lexicalTree: LexicalNode[] = [];
for (const mdNode of mdTree) {
- lexicalTree.push(convertToMarkdownNode(mdNode));
+ lexicalTree.push(...convertToMarkdownNode(mdNode));
}
return lexicalTree;
@@ -281,9 +237,14 @@ export const $convertFromMarkdownString = (rawMarkdown: string) => {
export const $serializeAsMarkdownAST = () => {
const rootNode = $getRoot();
const children = rootNode.getChildren();
- return u('root', {
+
+ const tree = u('root', {
children: children.map(convertLexicalNodeToMarkdownNode),
}) satisfies Root;
+
+ liftFormattingNodes(tree);
+
+ return tree;
};
export const $convertToMarkdownString = () => {
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/nodes/FormattingNode.tsx b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/nodes/FormattingNode.tsx
deleted file mode 100644
index 0a86fa223..000000000
--- a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/nodes/FormattingNode.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/* eslint-disable @typescript-eslint/no-use-before-define */
-
-import {
- $applyNodeReplacement,
- DOMExportOutput,
- ElementNode,
- LexicalNode,
- NodeKey,
- SerializedElementNode,
- Spread,
-} from 'lexical';
-
-export interface FormattingNodePayload {
- tag: string;
- key?: NodeKey;
-}
-
-export type SerializedFormattingNode = Spread<
- {
- tag: string;
- },
- SerializedElementNode
->;
-
-export class FormattingNode extends ElementNode {
- __tagName: string;
-
- static getType(): string {
- return 'formatting';
- }
-
- static clone(node: FormattingNode): FormattingNode {
- return new FormattingNode(node.__tagName, node.__key);
- }
-
- static importJSON(serializedNode: SerializedFormattingNode): FormattingNode {
- const { tag } = serializedNode;
- return $createFormattingNode({ tag });
- }
-
- exportDOM(): DOMExportOutput {
- const element = document.createElement(this.__tagName);
- return { element };
- }
-
- constructor(tagName: string, key?: NodeKey) {
- super(key);
- this.__tagName = tagName;
- }
-
- isInline(): boolean {
- return true;
- }
-
- exportJSON(): SerializedFormattingNode {
- return {
- ...super.exportJSON(),
- type: this.getType(),
- tag: this.getTagName(),
- };
- }
-
- getTagName() {
- return this.__tagName;
- }
-
- // View
-
- createDOM(): HTMLElement {
- const element = document.createElement(this.__tagName);
- return element;
- }
-
- updateDOM(): false {
- return false;
- }
-}
-
-export function $createFormattingNode({
- tag,
- key,
-}: FormattingNodePayload): FormattingNode {
- return $applyNodeReplacement(new FormattingNode(tag, key));
-}
-
-export function $isFormattingNode(
- node: LexicalNode | null | undefined,
-): node is FormattingNode {
- return node instanceof FormattingNode;
-}
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/liftFormattingNodes.test.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/liftFormattingNodes.test.ts
new file mode 100644
index 000000000..32549c60b
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/liftFormattingNodes.test.ts
@@ -0,0 +1,368 @@
+import { Root } from 'mdast';
+import { u } from 'unist-builder';
+
+import { liftFormattingNodes } from './remarkLiftFormatting';
+
+describe('Normalize Lexical tree', () => {
+ test('Complex formatting', () => {
+ expect(
+ liftFormattingNodes({
+ type: 'root',
+ children: [
+ {
+ type: 'paragraph',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'text',
+ value: 'All text can be italic, something additionally can be ',
+ },
+ ],
+ },
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'strong',
+ children: [{ type: 'text', value: 'bold' }],
+ },
+ ],
+ },
+ {
+ type: 'emphasis',
+ children: [{ type: 'text', value: ', ' }],
+ },
+ {
+ type: 'delete',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ { type: 'text', value: 'strikethrough' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'emphasis',
+ children: [{ type: 'text', value: ', or ' }],
+ },
+ {
+ type: 'delete',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'strong',
+ children: [
+ {
+ type: 'text',
+ value: 'bold AND strikethrough',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ } satisfies Root),
+ ).toEqual({
+ type: 'root',
+ children: [
+ {
+ type: 'paragraph',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'text',
+ value: 'All text can be italic, something additionally can be ',
+ },
+ {
+ type: 'strong',
+ children: [{ type: 'text', value: 'bold' }],
+ },
+ { type: 'text', value: ', ' },
+ {
+ type: 'delete',
+ children: [{ type: 'text', value: 'strikethrough' }],
+ },
+ { type: 'text', value: ', or ' },
+ {
+ type: 'strong',
+ children: [
+ {
+ type: 'delete',
+ children: [
+ {
+ type: 'text',
+ value: 'bold AND strikethrough',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ } satisfies Root);
+ });
+
+ test('Grouping siblings', () => {
+ expect(
+ liftFormattingNodes({
+ type: 'root',
+ children: [
+ {
+ type: 'paragraph',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'text',
+ value: 'All text can be italic, something additionally can be ',
+ },
+ ],
+ },
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'strong',
+ children: [{ type: 'text', value: 'bold' }],
+ },
+ ],
+ },
+ {
+ type: 'emphasis',
+ children: [{ type: 'text', value: ', ' }],
+ },
+ {
+ type: 'delete',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ { type: 'text', value: 'strikethrough' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'delete',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'strong',
+ children: [
+ {
+ type: 'text',
+ value: 'bold AND strikethrough',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ } satisfies Root),
+ ).toEqual({
+ type: 'root',
+ children: [
+ {
+ type: 'paragraph',
+ children: [
+ {
+ type: 'emphasis',
+ children: [
+ {
+ type: 'text',
+ value: 'All text can be italic, something additionally can be ',
+ },
+ {
+ type: 'strong',
+ children: [{ type: 'text', value: 'bold' }],
+ },
+ { type: 'text', value: ', ' },
+ {
+ type: 'delete',
+ children: [
+ { type: 'text', value: 'strikethrough' },
+ {
+ type: 'strong',
+ children: [
+ {
+ type: 'text',
+ value: 'bold AND strikethrough',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ } satisfies Root);
+ });
+});
+
+test('Sibling nodes with exact format must be grouped', () => {
+ expect(
+ liftFormattingNodes(
+ u('root', {
+ children: [
+ u('paragraph', {
+ children: [
+ u('emphasis', {
+ children: [
+ u('text', { value: 'Italic text' }),
+ u('strong', {
+ children: [
+ u('delete', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #1',
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('strong', {
+ children: [
+ u('delete', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #2',
+ }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('text', { value: 'Text with no formatting' }),
+ ],
+ }),
+ ],
+ }) satisfies Root,
+ ),
+ ).toEqual(
+ u('root', {
+ children: [
+ u('paragraph', {
+ children: [
+ u('emphasis', {
+ children: [
+ u('text', { value: 'Italic text' }),
+ u('strong', {
+ children: [
+ u('delete', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #1',
+ }),
+ u('text', {
+ value: 'Strong & delete text #2',
+ }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('text', { value: 'Text with no formatting' }),
+ ],
+ }),
+ ],
+ }) satisfies Root,
+ );
+});
+
+test('Sibling nodes with equal format must be grouped', () => {
+ expect(
+ liftFormattingNodes(
+ u('root', {
+ children: [
+ u('paragraph', {
+ children: [
+ u('emphasis', {
+ children: [
+ u('text', { value: 'Italic text' }),
+ u('delete', {
+ children: [
+ u('strong', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #1',
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('strong', {
+ children: [
+ u('delete', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #2',
+ }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('text', { value: 'Text with no formatting' }),
+ ],
+ }),
+ ],
+ }) satisfies Root,
+ ),
+ ).toEqual(
+ u('root', {
+ children: [
+ u('paragraph', {
+ children: [
+ u('emphasis', {
+ children: [
+ u('text', { value: 'Italic text' }),
+ u('strong', {
+ children: [
+ u('delete', {
+ children: [
+ u('text', {
+ value: 'Strong & delete text #1',
+ }),
+ u('text', {
+ value: 'Strong & delete text #2',
+ }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+ u('text', { value: 'Text with no formatting' }),
+ ],
+ }),
+ ],
+ }) satisfies Root,
+ );
+});
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkLiftFormatting.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkLiftFormatting.ts
new file mode 100644
index 000000000..dafdc7764
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkLiftFormatting.ts
@@ -0,0 +1,137 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import type { Delete, Emphasis, PhrasingContent, Root, RootContent, Strong } from 'mdast';
+import { SKIP, visit } from 'unist-util-visit';
+
+/**
+ * Formatting node types treated as pure "marks": they wrap exactly one
+ * thing and carry no data of their own (unlike e.g. `link`/`image`), so
+ * they can be freely merged, split and re-nested.
+ *
+ * The ORDER here is significant: it's the canonical outer -> inner nesting
+ * order that lifted formatting gets rebuilt in, regardless of whatever
+ * (possibly inconsistent) order the input used. Add more types here to
+ * support lifting them too - no other code needs to change.
+ */
+const MARK_ORDER = ['emphasis', 'strong', 'delete'] satisfies (
+ | Emphasis['type']
+ | Strong['type']
+ | Delete['type']
+)[];
+
+type Mark = (typeof MARK_ORDER)[number];
+type MarkNode = Emphasis | Strong | Delete;
+
+const MARK_TYPES = new Set(MARK_ORDER);
+
+function isMarkNode(node: PhrasingContent): node is MarkNode {
+ return MARK_TYPES.has(node.type);
+}
+
+function hasChildren(
+ node: PhrasingContent,
+): node is PhrasingContent & { children: PhrasingContent[] } {
+ return (
+ 'children' in node &&
+ Array.isArray((node as { children?: unknown }).children) &&
+ (node as { children: unknown[] }).children.length > 0
+ );
+}
+
+/** Flattened leaf: some content plus the set of marks wrapping it. */
+interface Leaf {
+ content: PhrasingContent;
+ marks: Set;
+}
+
+/**
+ * Descends through a chain of single-child mark wrappers, collecting every
+ * mark found along the way, stopping at the first node that either isn't a
+ * mark node or doesn't have exactly one child. That stopping node's own
+ * children (if any) are recursively re-lifted, so formatting nested inside
+ * e.g. links, or inside already-multi-child mark nodes, is still
+ * normalized without being lifted past a boundary it shouldn't cross.
+ */
+function flattenToLeaf(node: PhrasingContent, marks: Set): Leaf {
+ if (isMarkNode(node) && node.children.length === 1) {
+ marks.add(node.type);
+ return flattenToLeaf(node.children[0], marks);
+ }
+
+ if (hasChildren(node))
+ return {
+ content: {
+ ...node,
+ children: liftChildren(node.children),
+ } as PhrasingContent,
+ marks,
+ };
+
+ return { content: node, marks };
+}
+
+/**
+ * Rebuilds a flat list of leaves into a tree. Marks already consumed are
+ * removed from each leaf's `Set`, so scanning `MARK_ORDER` from the start
+ * every time is both safe (can't re-select a consumed mark) and necessary:
+ * different leaves inside the same run may carry different subsets of
+ * marks, so the "next" priority mark to lift can legitimately be one that
+ * comes *before* a mark that was just grouped one level up.
+ */
+function rebuildFromLeaves(leaves: Leaf[]): PhrasingContent[] {
+ const result: PhrasingContent[] = [];
+ let i = 0;
+
+ while (i < leaves.length) {
+ let markIndex = 0;
+ while (
+ markIndex < MARK_ORDER.length &&
+ !leaves[i].marks.has(MARK_ORDER[markIndex])
+ )
+ markIndex++;
+
+ if (markIndex === MARK_ORDER.length) {
+ result.push(leaves[i].content);
+ i++;
+ continue;
+ }
+
+ const mark = MARK_ORDER[markIndex];
+ const group: Leaf[] = [];
+
+ while (i < leaves.length && leaves[i].marks.has(mark)) {
+ leaves[i].marks.delete(mark);
+ group.push(leaves[i]);
+ i++;
+ }
+
+ result.push({
+ type: mark,
+ children: rebuildFromLeaves(group),
+ } as PhrasingContent);
+ }
+
+ return result;
+}
+
+function liftChildren(children: PhrasingContent[]): PhrasingContent[] {
+ return rebuildFromLeaves(children.map((child) => flattenToLeaf(child, new Set())));
+}
+
+export const liftFormattingNodes = (tree: Root) => {
+ visit(
+ tree,
+ ['heading', 'paragraph', 'tableCell'] satisfies RootContent['type'][],
+ (node) => {
+ if ('children' in node) {
+ node.children = liftChildren(node.children);
+ }
+ return SKIP;
+ },
+ );
+
+ return tree;
+};
+
+export default function remarkLiftFormatting() {
+ return liftFormattingNodes;
+}
diff --git a/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkPreserveBlankLines.ts b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkPreserveBlankLines.ts
new file mode 100644
index 000000000..450800060
--- /dev/null
+++ b/packages/app/src/features/NoteEditor/RichEditor/plugins/Markdown/remark/remarkPreserveBlankLines.ts
@@ -0,0 +1,62 @@
+import { type Root, RootContent } from 'mdast';
+import { Plugin } from 'unified';
+import { CONTINUE, SKIP, visit } from 'unist-util-visit';
+
+export const remarkPreserveBlankLines: Plugin<[], Root> = () => {
+ const ignoredNodeTypes = new Set([
+ 'table',
+ 'tableCell',
+ 'tableRow',
+ ] satisfies RootContent['type'][]);
+
+ return (tree: Root) => {
+ const skipNodes = new Set();
+
+ visit(tree, (node) => {
+ // Skip nodes with no nested elements
+ if (!('children' in node)) return SKIP;
+
+ // Skip ignored node types
+ if (ignoredNodeTypes.has(node.type)) return SKIP;
+
+ // Skip already handled nodes
+ if (skipNodes.has(node)) return SKIP;
+
+ const newChildren: RootContent[] = [];
+ for (let i = 0; i < node.children.length; i++) {
+ const current = node.children[i];
+ const next = node.children[i + 1];
+
+ // Collect its own children
+ newChildren.push(current);
+
+ // Add empty lines to preserve
+ if (next && current.position && next.position) {
+ const lineGap = next.position.start.line - current.position.end.line;
+ // lineGap === 2 means exactly one blank line, 3 means two, etc.
+ const blankLineCount = lineGap - 1;
+
+ for (let b = 0; b < blankLineCount; b++) {
+ const line = current.position.end.line + 1 + b;
+ const emptyLine = {
+ type: 'paragraph',
+ children: [],
+ // TODO: add tests to verify position are correct in complex cases
+ position: {
+ start: { line, column: 1, offset: 0 },
+ end: { line, column: 1, offset: 0 },
+ },
+ } as RootContent;
+
+ newChildren.push(emptyLine);
+ skipNodes.add(emptyLine);
+ }
+ }
+ }
+
+ node.children = newChildren;
+
+ return CONTINUE;
+ });
+ };
+};
diff --git a/packages/app/src/features/NoteEditor/__tests__/behavior.dom.test.ts b/packages/app/src/features/NoteEditor/__tests__/behavior.dom.test.ts
deleted file mode 100644
index d252a6ca5..000000000
--- a/packages/app/src/features/NoteEditor/__tests__/behavior.dom.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { screen, within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import { renderRichEditor } from './utils/renderRichEditor';
-import { selectContent } from './utils/utils';
-
-test('Editor updates when value changes', async () => {
- const editor = await renderRichEditor({ value: `# Big text` });
-
- expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Big text');
-
- // Run component rerender with new value
- await editor.rerender({ value: `### Not so big text` });
-
- expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
- 'Not so big text',
- );
-
- // The old header was removed
- expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument();
-});
-
-test('Editor is not editable in readonly mode', async () => {
- const user = userEvent.setup();
- await renderRichEditor({
- value: '# Hello',
- isReadOnly: true,
- });
-
- expect(screen.getByRole('textbox')).toHaveAttribute('contenteditable', 'false');
-
- // Cannot enter text
- await user.click(screen.getByRole('heading'));
- await user.keyboard('Some text');
-
- expect(screen.getByRole('textbox')).toHaveTextContent('Hello');
- expect(screen.getByRole('textbox')).not.toHaveTextContent('Some text');
-});
-
-test('Formatting text in one editor does not affect the other editor', async () => {
- const editorA = await renderRichEditor({ value: 'Big text' });
- const editorB = await renderRichEditor({ value: 'Small text' });
-
- const textBoxes = screen.getAllByRole('textbox');
- expect(textBoxes).toHaveLength(2);
-
- // initial state
- expect(within(textBoxes[0]).queryByRole('paragraph')).toHaveTextContent('Big text');
- expect(within(textBoxes[1]).queryByRole('paragraph')).toHaveTextContent('Small text');
-
- // apply italic in editorA
- selectContent(textBoxes[0], 'Big text');
- await editorA.format('italic');
-
- expect(within(textBoxes[0]).getByRole('emphasis')).toHaveTextContent('Big text');
- expect(within(textBoxes[1]).queryByRole('emphasis')).not.toBeInTheDocument();
-
- // apply strikethrough in editorB
- selectContent(textBoxes[1], 'Small text');
- await editorB.format('strikethrough');
-
- expect(within(textBoxes[1]).getByRole('deletion')).toHaveTextContent('Small text');
- expect(within(textBoxes[1]).queryByRole('emphasis')).not.toBeInTheDocument();
-
- expect(within(textBoxes[0]).getByRole('emphasis')).toHaveTextContent('Big text');
- expect(within(textBoxes[0]).queryByRole('deletion')).not.toBeInTheDocument();
-});
-
-test('ReadOnly editor is not editable while the other editor remains editable', async () => {
- const user = userEvent.setup();
- await renderRichEditor({ value: 'Editable text' });
- await renderRichEditor({ value: 'ReadOnly text', isReadOnly: true });
-
- const textBoxes = screen.getAllByRole('textbox');
- expect(textBoxes).toHaveLength(2);
-
- expect(textBoxes[0]).toHaveAttribute('contenteditable', 'true');
- expect(textBoxes[1]).toHaveAttribute('contenteditable', 'false');
-
- // editorA is editable — editing works
- await user.click(within(textBoxes[0]).getByText('Editable text'));
- await user.keyboard('New text ');
-
- expect(textBoxes[0]).toHaveTextContent('New text Editable text');
-
- // editorB is readOnly — editing must be ignored
- await user.click(within(textBoxes[1]).getByText('ReadOnly text'));
- await user.keyboard('Some text');
-
- expect(textBoxes[1]).toHaveTextContent('ReadOnly text');
- expect(textBoxes[1]).not.toHaveTextContent('Some text');
- expect(textBoxes[1]).toHaveAttribute('contenteditable', 'false');
-});
diff --git a/packages/app/src/features/NoteEditor/__tests__/interactive.dom.test.ts b/packages/app/src/features/NoteEditor/__tests__/interactive.dom.test.ts
deleted file mode 100644
index a1c0d7a18..000000000
--- a/packages/app/src/features/NoteEditor/__tests__/interactive.dom.test.ts
+++ /dev/null
@@ -1,367 +0,0 @@
-import { screen, within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import { renderRichEditor } from './utils/renderRichEditor';
-import { selectContent, selectText, setCursorPosition } from './utils/utils';
-
-test('Pressing Enter inside a paragraph splits it into two paragraphs', async () => {
- const user = userEvent.setup();
- await renderRichEditor({ value: 'My favorite dish is cake' });
-
- // Initial state
- const editor = screen.getByRole('textbox');
- expect(editor.children).toHaveLength(1);
- const paragraph = editor.children[0];
- expect(paragraph).toHaveRole('paragraph');
- expect(paragraph).toHaveTextContent('My favorite dish is cake');
-
- // Place cursor and press Enter
- await user.click(paragraph);
- setCursorPosition(paragraph, 'My favorite dis'.length);
- await user.keyboard('{Enter}');
-
- // Editor should now have two paragraphs split at the cursor
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(2);
-
- expect(editorChildren[0]).toHaveRole('paragraph');
- expect(editorChildren[0]).toHaveTextContent('My favorite dis');
-
- expect(editorChildren[1]).toHaveRole('paragraph');
- expect(editorChildren[1]).toHaveTextContent('h is cake');
-});
-
-test('Ctrl+Enter exits a block node and creates a new empty paragraph', async () => {
- const user = userEvent.setup();
- const content = 'This is a blockquote';
- const richEditor = await renderRichEditor({ value: `> ${content}` });
-
- // One blockquote with one paragraph inside
- const editor = screen.getByRole('textbox');
- const initialChildren = editor.children;
- expect(initialChildren).toHaveLength(1);
- expect(initialChildren[0]).toHaveRole('blockquote');
- expect(initialChildren[0]).toHaveTextContent(content);
-
- expect(within(editor).getAllByRole('paragraph')).toHaveLength(1);
-
- // Press Ctrl+Enter to exit the blockquote
- await user.click(within(editor).getByRole('blockquote'));
- await user.keyboard('{Control>}{Enter}{/Control}');
-
- // New empty paragraph is added
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(2);
- expect(editorChildren[0]).toHaveRole('blockquote');
- expect(editorChildren[1]).toHaveRole('paragraph');
-
- const paragraphs = within(editor).getAllByRole('paragraph');
- expect(paragraphs).toHaveLength(2);
- expect(paragraphs[0]).toHaveTextContent(content);
- expect(paragraphs[1]).toHaveTextContent('');
-
- // Cursor lands in the new paragraph - inserted content inside new paragraph
- await richEditor.insert({ type: 'date', data: { date: '01.01.2025' } });
- expect(within(editor).getByText('01.01.2025')).toBeInTheDocument();
- expect(within(editor).getByRole('blockquote')).not.toHaveTextContent('01.01.2025');
-});
-
-test(`Inserts image between text nodes`, async () => {
- const richEditor = await renderRichEditor({
- value: `My favorite image\n\n I love cat`,
- });
-
- const editor = screen.getByRole('textbox');
- setCursorPosition(editor, 'My favorite image'.length);
-
- // Simulate inserting an image via the editor panel action
- await richEditor.insert({
- type: 'image',
- data: { url: 'http://example.com/cat.png', altText: 'My cat' },
- });
-
- // Image nodes inserting asynchronously, so use findByRole to wait for the img to appear
- const img = await within(editor).findByRole('img');
- expect(img).toBeInTheDocument();
- expect(img).toHaveAttribute('src', 'http://example.com/cat.png');
- expect(img).toHaveAttribute('alt', 'My cat');
-
- // Image between two texts
- const firstText = within(editor).getByText('My favorite image');
- const secondText = within(editor).getByText('I love cat');
- expect(img).toAppearAfter(firstText);
- expect(img).toAppearBefore(secondText);
-
- // Editor contains only expected nodes
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(3);
- Array.from(editor.children).forEach((child) => {
- expect(child).toHaveRole('paragraph');
- });
-
- // First paragraph contains text and image
- expect(editorChildren[0]).toContainElement(img);
- expect(editorChildren[0]).toHaveTextContent('My favorite image');
- expect(editorChildren[1]).toHaveTextContent('');
- expect(editorChildren[2]).toHaveTextContent('I love cat');
-});
-
-test('Inserts image after block node', async () => {
- const richEditor = await renderRichEditor({
- value: '```js\nconst a = 1;\n```',
- });
-
- // Place cursor position inside the code node
- const editor = screen.getByRole('textbox');
- setCursorPosition(within(editor).getByRole('code'), 10);
-
- await richEditor.insert({
- type: 'image',
- data: { url: 'http://example.com/cat.png', altText: 'My cat' },
- });
-
- // Wait before image to appear
- const img = await within(editor).findByRole('img');
- expect(img).toBeInTheDocument();
-
- const codeNode = within(editor).getByRole('code');
- expect(codeNode).toBeInTheDocument();
-
- // Image is inserted as next sibling of the code block
- expect(img).toAppearAfter(codeNode);
- expect(codeNode.nextElementSibling).toContainElement(img);
-
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(2);
- expect(editorChildren[0]).toHaveRole('code');
- expect(editorChildren[1]).toHaveRole('paragraph');
-
- // Image is inside the paragraph
- expect(editorChildren[1]).toContainElement(img);
-});
-
-test('Updates heading level correctly', async () => {
- const content = 'Hello, my dear friends!';
- const richEditor = await renderRichEditor({ value: content });
-
- const editor = screen.getByRole('textbox');
- selectContent(editor, content);
-
- // Plain text becomes heading
- await richEditor.insert({ type: 'heading', data: { level: 1 } });
- expect(within(editor).getByRole('heading', { level: 1 })).toHaveTextContent(content);
-
- // Heading level is updated when different level applied
- await richEditor.insert({ type: 'heading', data: { level: 3 } });
-
- expect(within(editor).getByRole('heading', { level: 3 })).toHaveTextContent(content);
- expect(within(editor).queryByRole('heading', { level: 1 })).not.toBeInTheDocument();
-
- // Heading reverts to paragraph when same level applied again
- await richEditor.insert({ type: 'heading', data: { level: 3 } });
-
- expect(within(editor).queryByRole('heading')).not.toBeInTheDocument();
- expect(within(editor).getByText(content)).toBeInTheDocument();
-
- // After editing the screen should contain one paragraph with a text
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(1);
- expect(editorChildren[0]).toHaveRole('paragraph');
- expect(editorChildren[0]).toHaveTextContent(content);
-});
-
-test('Converts an unordered list to an ordered list', async () => {
- const richEditor = await renderRichEditor({
- value: `- First item
- - Nested item
-- Second item`,
- });
- const editor = screen.getByRole('textbox');
-
- expect(editor.children[0].tagName).toBe('UL');
- expect(editor.children[0].children).toHaveLength(2);
-
- // Select text and convert to ordered list
- selectContent(editor, 'First item');
- await richEditor.insert({ type: 'list', data: { type: 'ordered' } });
-
- expect(editor.children[0].tagName).toBe('OL');
-
- const listItems = editor.children[0].children;
- expect(listItems).toHaveLength(2);
-
- // First item has text and nested list
- const firstItemChildren = listItems[0].children;
- expect(firstItemChildren).toHaveLength(2);
- expect(listItems[0]).toHaveTextContent('First item');
-
- const nestedListChildren = firstItemChildren[1].children;
- expect(nestedListChildren).toHaveLength(1);
- expect(nestedListChildren[0]).toHaveTextContent('Nested item');
-
- expect(listItems[1].children).toHaveLength(1);
- expect(listItems[1]).toHaveTextContent('Second item');
-});
-
-test('Pressing Enter adds a new item to the list', async () => {
- const user = userEvent.setup();
- await renderRichEditor({ value: '- First item' });
- const editor = screen.getByRole('textbox');
-
- const initialChildren = editor.children;
- expect(initialChildren).toHaveLength(1);
- expect(initialChildren[0]).toHaveRole('list');
- expect(initialChildren[0].children).toHaveLength(1);
-
- const [firstItem] = within(editor).getAllByRole('listitem');
- expect(firstItem).toHaveTextContent('First item');
-
- // Place cursor and press Enter
- await user.click(firstItem);
- setCursorPosition(firstItem, 'First item'.length);
- await user.keyboard('{Enter}');
-
- // Editor contains only one list with two items
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(1);
- expect(editorChildren[0]).toHaveRole('list');
-
- const items = editorChildren[0].children;
- expect(items).toHaveLength(2);
- expect(items[0]).toHaveTextContent('First item');
- expect(items[1]).toHaveTextContent('');
-});
-
-test('Pressing Enter on an empty last list item exits the list', async () => {
- const user = userEvent.setup();
- await renderRichEditor({ value: '- First item' });
- const editor = screen.getByRole('textbox');
-
- const initialChildren = editor.children;
- expect(initialChildren).toHaveLength(1);
- expect(initialChildren[0]).toHaveRole('list');
- expect(initialChildren[0].children).toHaveLength(1);
-
- expect(within(editor).getAllByRole('listitem')).toHaveLength(1);
- expect(within(editor).queryByRole('paragraph')).not.toBeInTheDocument();
-
- // Set cursor and press Enter
- const firstItem = within(editor).getByRole('listitem');
- await user.click(firstItem);
- setCursorPosition(firstItem, 'First item'.length);
- await user.keyboard('{Enter}');
-
- expect(within(editor).getAllByRole('listitem')).toHaveLength(2);
- expect(within(editor).queryByRole('paragraph')).not.toBeInTheDocument();
-
- await user.keyboard('{Enter}');
-
- // A new empty paragraph is created
- expect(within(editor).getByRole('paragraph')).toHaveTextContent('');
- expect(within(editor).getAllByRole('listitem')).toHaveLength(1);
-
- // Editor contains only list with one items and one paragraph
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(2);
- expect(editorChildren[0]).toHaveRole('list');
- expect(editorChildren[0].children).toHaveLength(1);
- expect(editorChildren[1]).toHaveRole('paragraph');
-});
-
-test('Toggles text formatting', async () => {
- const content = 'Hello, my dear friends!';
- const richEditor = await renderRichEditor({ value: content });
-
- const editor = screen.getByRole('textbox');
- selectContent(editor, content);
-
- // Apply strikethrough
- await richEditor.format('strikethrough');
- expect(within(editor).getByRole('deletion')).toHaveTextContent(content);
-
- // Remove strikethrough
- await richEditor.format('strikethrough');
- expect(editor.children).toHaveLength(1);
- expect(editor.children[0]).toHaveTextContent(content);
- expect(within(editor).queryByRole('deletion')).not.toBeInTheDocument();
-
- // Apply italic
- await richEditor.format('italic');
- expect(within(editor).getByRole('emphasis')).toHaveTextContent(content);
-
- // Remove italic
- await richEditor.format('italic');
- expect(editor.children).toHaveLength(1);
- expect(editor.children[0]).toHaveTextContent(content);
- expect(within(editor).queryByRole('emphasis')).not.toBeInTheDocument();
-});
-
-test('Combines multiple text formatting', async () => {
- const content = 'Hello, my dear friends!';
- const richEditor = await renderRichEditor({ value: content });
-
- const editor = screen.getByRole('textbox');
- selectContent(editor, content);
-
- await richEditor.format('italic');
- await richEditor.format('bold');
- await richEditor.format('strikethrough');
-
- // Bold formatting is implemented using the tag which has no ARIA role
- expect(within(editor).getByText(content).closest('b')).toBeInTheDocument();
- expect(within(editor).getByRole('emphasis')).toHaveTextContent(content);
- expect(within(editor).getByRole('deletion')).toHaveTextContent(content);
-
- // Removes bold without breaking others formatting
- await richEditor.format('bold');
-
- expect(within(editor).getByText(content).closest('b')).not.toBeInTheDocument();
- expect(within(editor).getByRole('emphasis')).toHaveTextContent(content);
- expect(within(editor).getByRole('deletion')).toHaveTextContent(content);
-});
-
-// TODO: Remove `.fails` after fixing the formatting implementation
-// The assertions below describe the expected behavior and should remain unchanged
-test.fails('Applies formatting to a selected part of a text node', async () => {
- const richEditor = await renderRichEditor({ value: 'Hello, my dear friends!' });
-
- const editor = screen.getByRole('textbox');
- selectText(editor, 'friends');
-
- // Apply formatting
- await richEditor.format('italic');
-
- // Editor contains one paragraph
- const editorChildren = editor.children;
- expect(editorChildren).toHaveLength(1);
- expect(editorChildren[0]).toHaveRole('paragraph');
-
- const paragraph = editorChildren[0];
- expect(paragraph).toHaveTextContent('Hello, my dear friends!');
- expect(paragraph.children).toHaveLength(3);
-
- // Only "friends" is wrapped in emphasis
- expect(paragraph.children[0]).toHaveTextContent('Hello, my dear ');
- expect(paragraph.children[1]).toHaveRole('emphasis');
- expect(paragraph.children[1]).toHaveTextContent('friends');
- expect(paragraph.children[2]).toHaveTextContent('!');
-});
-
-test.fails('Applies formatting across multiple text blocks', async () => {
- const richEditor = await renderRichEditor({
- value: 'Hello, my dear friends! \n\n Nice to see you. \n\n How are you ?',
- });
-
- const editor = screen.getByRole('textbox');
- selectContent(editor, 'Hello, my dear friends!', 'How are you ?');
-
- // Apply formatting
- await richEditor.format('italic');
-
- // Each line should be wrapped in emphasis
- const formattingNodes = within(editor).getAllByRole('emphasis');
- expect(formattingNodes).toHaveLength(3);
- expect(formattingNodes[0]).toHaveTextContent('Hello, my dear friends!');
- expect(formattingNodes[1]).toHaveTextContent('Nice to see you');
- expect(formattingNodes[2]).toHaveTextContent('How are you ?');
-});