From 007685f62a1ecf635e31689f4a1b4b08b0cf65ce Mon Sep 17 00:00:00 2001 From: Nulo Date: Sun, 5 Mar 2023 17:10:29 +0000 Subject: [PATCH] init --- .gitignore | 24 + .vscode/extensions.json | 3 + README.md | 5 + index.html | 13 + package.json | 42 + pnpm-lock.yaml | 1235 +++++++++++++++++++ src/App.svelte | 43 + src/app.css | 15 + src/app.d.ts | 26 + src/editor/BubbleMenu.svelte | 163 +++ src/editor/Editor.svelte | 81 ++ src/editor/MenuBar.svelte | 23 + src/editor/bubblemenu/SimpleMarkItem.svelte | 24 + src/editor/bubblemenu/coords.ts | 104 ++ src/editor/demo.css | 22 + src/editor/editor.css | 194 +++ src/editor/global.d.ts | 2 + src/editor/keymap.ts | 69 ++ src/editor/main.ts | 11 + src/editor/menubar/AlignSelect.svelte | 30 + src/editor/menubar/BlockQuoteItem.svelte | 27 + src/editor/menubar/BlockSelect.svelte | 49 + src/editor/menubar/ListItem.svelte | 44 + src/editor/menubar/UploadItem.svelte | 86 ++ src/editor/ps-utils.ts | 237 ++++ src/editor/schema.ts | 326 +++++ src/editor/upload.ts | 48 + src/editor/utils.ts | 49 + src/lib/doc.ts | 37 + src/lib/routes.ts | 34 + src/lib/worldStorage.ts | 18 + src/main.ts | 8 + src/views/ChooseWorld.svelte | 25 + src/views/CreateWorld.svelte | 19 + src/views/NotFound.svelte | 3 + src/views/Page.svelte | 45 + src/vite-env.d.ts | 2 + svelte.config.js | 7 + tsconfig.json | 20 + tsconfig.node.json | 8 + vite.config.ts | 26 + 41 files changed, 3247 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 README.md create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/App.svelte create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/editor/BubbleMenu.svelte create mode 100644 src/editor/Editor.svelte create mode 100644 src/editor/MenuBar.svelte create mode 100644 src/editor/bubblemenu/SimpleMarkItem.svelte create mode 100644 src/editor/bubblemenu/coords.ts create mode 100644 src/editor/demo.css create mode 100644 src/editor/editor.css create mode 100644 src/editor/global.d.ts create mode 100644 src/editor/keymap.ts create mode 100644 src/editor/main.ts create mode 100644 src/editor/menubar/AlignSelect.svelte create mode 100644 src/editor/menubar/BlockQuoteItem.svelte create mode 100644 src/editor/menubar/BlockSelect.svelte create mode 100644 src/editor/menubar/ListItem.svelte create mode 100644 src/editor/menubar/UploadItem.svelte create mode 100644 src/editor/ps-utils.ts create mode 100644 src/editor/schema.ts create mode 100644 src/editor/upload.ts create mode 100644 src/editor/utils.ts create mode 100644 src/lib/doc.ts create mode 100644 src/lib/routes.ts create mode 100644 src/lib/worldStorage.ts create mode 100644 src/main.ts create mode 100644 src/views/ChooseWorld.svelte create mode 100644 src/views/CreateWorld.svelte create mode 100644 src/views/NotFound.svelte create mode 100644 src/views/Page.svelte create mode 100644 src/vite-env.d.ts create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..bdef820 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c35664c --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# schreiben + +[WIP] Una aplicación web para escribir un mundo de cosas [knowledge base] en varios dispositivos. Sale de la frustración de que Notion funcione tan mal en Android (y iOS) y que sea software privativo en la nube. + +El editor (src/editor) está basado en [sutty/editor](https://0xacab.org/sutty/editor). diff --git a/index.html b/index.html new file mode 100644 index 0000000..88c31de --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Schreiben + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..5152391 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "schreiben", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.0.2", + "@tsconfig/svelte": "^3.0.0", + "@vitejs/plugin-basic-ssl": "^1.0.1", + "prosemirror-commands": "~1.3.0", + "prosemirror-dropcursor": "~1.6.0", + "prosemirror-gapcursor": "~1.3.0", + "prosemirror-keymap": "~1.2.0", + "prosemirror-markdown": "~1.10.0", + "prosemirror-model": "~1.18.0", + "prosemirror-schema-basic": "~1.2.0", + "prosemirror-schema-list": "~1.2.0", + "prosemirror-state": "~1.4.0", + "prosemirror-transform": "~1.7.0", + "prosemirror-view": "~1.29.0", + "svelte": "^3.55.1", + "svelte-check": "^2.10.3", + "tslib": "^2.5.0", + "typescript": "^4.9.3", + "vite": "^4.1.0" + }, + "dependencies": { + "bootstrap-icons": "^1.10.3", + "nanoid": "^4.0.1", + "navaid": "^1.2.0", + "y-prosemirror": "^1.2.0", + "y-protocols": "^1.0.5", + "y-webrtc": "^10.2.4", + "yjs": "^13.5.48" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0561085 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1235 @@ +lockfileVersion: 5.4 + +specifiers: + '@sveltejs/vite-plugin-svelte': ^2.0.2 + '@tsconfig/svelte': ^3.0.0 + '@vitejs/plugin-basic-ssl': ^1.0.1 + bootstrap-icons: ^1.10.3 + nanoid: ^4.0.1 + navaid: ^1.2.0 + prosemirror-commands: ~1.3.0 + prosemirror-dropcursor: ~1.6.0 + prosemirror-gapcursor: ~1.3.0 + prosemirror-keymap: ~1.2.0 + prosemirror-markdown: ~1.10.0 + prosemirror-model: ~1.18.0 + prosemirror-schema-basic: ~1.2.0 + prosemirror-schema-list: ~1.2.0 + prosemirror-state: ~1.4.0 + prosemirror-transform: ~1.7.0 + prosemirror-view: ~1.29.0 + svelte: ^3.55.1 + svelte-check: ^2.10.3 + tslib: ^2.5.0 + typescript: ^4.9.3 + vite: ^4.1.0 + y-prosemirror: ^1.2.0 + y-protocols: ^1.0.5 + y-webrtc: ^10.2.4 + yjs: ^13.5.48 + +dependencies: + bootstrap-icons: 1.10.3 + nanoid: 4.0.1 + navaid: 1.2.0 + y-prosemirror: 1.2.0_vhy2hiocjqydif5dwvxgfnpihi + y-protocols: 1.0.5 + y-webrtc: 10.2.4 + yjs: 13.5.48 + +devDependencies: + '@sveltejs/vite-plugin-svelte': 2.0.3_svelte@3.55.1+vite@4.1.4 + '@tsconfig/svelte': 3.0.0 + '@vitejs/plugin-basic-ssl': 1.0.1_vite@4.1.4 + prosemirror-commands: 1.3.1 + prosemirror-dropcursor: 1.6.1 + prosemirror-gapcursor: 1.3.1 + prosemirror-keymap: 1.2.1 + prosemirror-markdown: 1.10.1 + prosemirror-model: 1.18.3 + prosemirror-schema-basic: 1.2.1 + prosemirror-schema-list: 1.2.2 + prosemirror-state: 1.4.2 + prosemirror-transform: 1.7.1 + prosemirror-view: 1.29.2 + svelte: 3.55.1 + svelte-check: 2.10.3_svelte@3.55.1 + tslib: 2.5.0 + typescript: 4.9.5 + vite: 4.1.4 + +packages: + + /@esbuild/android-arm/0.16.17: + resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64/0.16.17: + resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64/0.16.17: + resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64/0.16.17: + resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64/0.16.17: + resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64/0.16.17: + resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64/0.16.17: + resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm/0.16.17: + resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64/0.16.17: + resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32/0.16.17: + resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64/0.16.17: + resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el/0.16.17: + resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64/0.16.17: + resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64/0.16.17: + resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x/0.16.17: + resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64/0.16.17: + resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64/0.16.17: + resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64/0.16.17: + resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64/0.16.17: + resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64/0.16.17: + resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32/0.16.17: + resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64/0.16.17: + resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@sveltejs/vite-plugin-svelte/2.0.3_svelte@3.55.1+vite@4.1.4: + resolution: {integrity: sha512-o+cguBFdwIGtRbNkYOyqTM7KvRUffxh5bfK4oJsWKG2obu+v/cbpT03tJrGl58C7tRXo/aEC0/axN5FVHBj0nA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 + vite: ^4.0.0 + dependencies: + debug: 4.3.4 + deepmerge: 4.3.0 + kleur: 4.1.5 + magic-string: 0.29.0 + svelte: 3.55.1 + svelte-hmr: 0.15.1_svelte@3.55.1 + vite: 4.1.4 + vitefu: 0.2.4_vite@4.1.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@tsconfig/svelte/3.0.0: + resolution: {integrity: sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==} + dev: true + + /@types/pug/2.0.6: + resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} + dev: true + + /@types/sass/1.45.0: + resolution: {integrity: sha512-jn7qwGFmJHwUSphV8zZneO3GmtlgLsmhs/LQyVvQbIIa+fzGMUiHI4HXJZL3FT8MJmgXWbLGiVVY7ElvHq6vDA==} + deprecated: This is a stub types definition. sass provides its own type definitions, so you do not need this installed. + dependencies: + sass: 1.58.3 + dev: true + + /@vitejs/plugin-basic-ssl/1.0.1_vite@4.1.4: + resolution: {integrity: sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==} + engines: {node: '>=14.6.0'} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + dependencies: + vite: 4.1.4 + dev: true + + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bootstrap-icons/1.10.3: + resolution: {integrity: sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==} + dev: false + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-crc32/0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /deepmerge/4.3.0: + resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} + engines: {node: '>=0.10.0'} + dev: true + + /detect-indent/6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + + /entities/3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + dev: true + + /err-code/3.0.1: + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + dev: false + + /es6-promise/3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + + /esbuild/0.16.17: + resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.16.17 + '@esbuild/android-arm64': 0.16.17 + '@esbuild/android-x64': 0.16.17 + '@esbuild/darwin-arm64': 0.16.17 + '@esbuild/darwin-x64': 0.16.17 + '@esbuild/freebsd-arm64': 0.16.17 + '@esbuild/freebsd-x64': 0.16.17 + '@esbuild/linux-arm': 0.16.17 + '@esbuild/linux-arm64': 0.16.17 + '@esbuild/linux-ia32': 0.16.17 + '@esbuild/linux-loong64': 0.16.17 + '@esbuild/linux-mips64el': 0.16.17 + '@esbuild/linux-ppc64': 0.16.17 + '@esbuild/linux-riscv64': 0.16.17 + '@esbuild/linux-s390x': 0.16.17 + '@esbuild/linux-x64': 0.16.17 + '@esbuild/netbsd-x64': 0.16.17 + '@esbuild/openbsd-x64': 0.16.17 + '@esbuild/sunos-x64': 0.16.17 + '@esbuild/win32-arm64': 0.16.17 + '@esbuild/win32-ia32': 0.16.17 + '@esbuild/win32-x64': 0.16.17 + dev: true + + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq/1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /get-browser-rtc/1.1.0: + resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==} + dev: false + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /immutable/4.2.4: + resolution: {integrity: sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==} + dev: true + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /isomorphic.js/0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /lib0/0.2.63: + resolution: {integrity: sha512-JhUd/JXR4rnWsSP1zup904ACbVFcdRieWoIGwGAtHww4zms0gNyi8EM30Rfftnk+6A10qQMSufjLM0/ncq06xw==} + engines: {node: '>=14'} + dependencies: + isomorphic.js: 0.2.5 + dev: false + + /linkify-it/4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + dependencies: + uc.micro: 1.0.6 + dev: true + + /magic-string/0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string/0.29.0: + resolution: {integrity: sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /markdown-it/13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /mdurl/1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist/1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /mkdirp/0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /mri/1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nanoid/4.0.1: + resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + + /navaid/1.2.0: + resolution: {integrity: sha512-Yh5mix394WrT5go29GFeFD4Gp4W0Xj1Ejs0KHXXCA24KKW74pq3PY3fwP3o18KveYO/pjUI2zzcAAp8kY98aNA==} + engines: {node: '>= 6'} + dependencies: + regexparam: 1.3.0 + dev: false + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /orderedmap/2.1.0: + resolution: {integrity: sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==} + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prosemirror-commands/1.3.1: + resolution: {integrity: sha512-XTporPgoECkOQACVw0JTe3RZGi+fls3/byqt+tXwGTkD7qLuB4KdVrJamDMJf4kfKga3uB8hZ+kUUyZ5oWpnfg==} + dependencies: + prosemirror-model: 1.18.3 + prosemirror-state: 1.4.2 + prosemirror-transform: 1.7.1 + dev: true + + /prosemirror-dropcursor/1.6.1: + resolution: {integrity: sha512-LtyqQpkIknaT7NnZl3vDr3TpkNcG4ABvGRXx37XJ8tJNUGtcrZBh40A0344rDwlRTfUEmynQS/grUsoSWz+HgA==} + dependencies: + prosemirror-state: 1.4.2 + prosemirror-transform: 1.7.1 + prosemirror-view: 1.29.2 + dev: true + + /prosemirror-gapcursor/1.3.1: + resolution: {integrity: sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==} + dependencies: + prosemirror-keymap: 1.2.1 + prosemirror-model: 1.18.3 + prosemirror-state: 1.4.2 + prosemirror-view: 1.29.2 + dev: true + + /prosemirror-keymap/1.2.1: + resolution: {integrity: sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==} + dependencies: + prosemirror-state: 1.4.2 + w3c-keyname: 2.2.6 + dev: true + + /prosemirror-markdown/1.10.1: + resolution: {integrity: sha512-s7iaTLiX+qO5z8kF2NcMmy2T7mIlxzkS4Sp3vTKSYChPtbMpg6YxFkU0Y06rUg2WtKlvBu7v1bXzlGBkfjUWAA==} + dependencies: + markdown-it: 13.0.1 + prosemirror-model: 1.18.3 + dev: true + + /prosemirror-model/1.18.3: + resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==} + dependencies: + orderedmap: 2.1.0 + + /prosemirror-model/1.19.0: + resolution: {integrity: sha512-/CvFGJnwc41EJSfDkQLly1cAJJJmBpZwwUJtwZPTjY2RqZJfM8HVbCreOY/jti8wTRbVyjagcylyGoeJH/g/3w==} + dependencies: + orderedmap: 2.1.0 + dev: true + + /prosemirror-schema-basic/1.2.1: + resolution: {integrity: sha512-vYBdIHsYKSDIqYmPBC7lnwk9DsKn8PnVqK97pMYP5MLEDFqWIX75JiaJTzndBii4bRuNqhC2UfDOfM3FKhlBHg==} + dependencies: + prosemirror-model: 1.19.0 + dev: true + + /prosemirror-schema-list/1.2.2: + resolution: {integrity: sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==} + dependencies: + prosemirror-model: 1.18.3 + prosemirror-state: 1.4.2 + prosemirror-transform: 1.7.1 + dev: true + + /prosemirror-state/1.4.2: + resolution: {integrity: sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==} + dependencies: + prosemirror-model: 1.18.3 + prosemirror-transform: 1.7.1 + prosemirror-view: 1.29.2 + + /prosemirror-transform/1.7.1: + resolution: {integrity: sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==} + dependencies: + prosemirror-model: 1.18.3 + + /prosemirror-view/1.29.2: + resolution: {integrity: sha512-T4Wm+eTpTH0N9gBJfJR6iecjRX2hYTKewoJUwa92hQOoEz2bYVZy6sYeN+hfnRR506TRvRcuZYqftp4KA8dN+Q==} + dependencies: + prosemirror-model: 1.18.3 + prosemirror-state: 1.4.2 + prosemirror-transform: 1.7.1 + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /readable-stream/3.6.1: + resolution: {integrity: sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /regexparam/1.3.0: + resolution: {integrity: sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==} + engines: {node: '>=6'} + dev: false + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup/3.18.0: + resolution: {integrity: sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /sade/1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /sander/0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + + /sass/1.58.3: + resolution: {integrity: sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.2.4 + source-map-js: 1.0.2 + dev: true + + /simple-peer/9.11.1: + resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==} + dependencies: + buffer: 6.0.3 + debug: 4.3.4 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + randombytes: 2.1.0 + readable-stream: 3.6.1 + transitivePeerDependencies: + - supports-color + dev: false + + /sorcery/0.10.0: + resolution: {integrity: sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==} + hasBin: true + dependencies: + buffer-crc32: 0.2.13 + minimist: 1.2.8 + sander: 0.5.1 + sourcemap-codec: 1.4.8 + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: true + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svelte-check/2.10.3_svelte@3.55.1: + resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==} + hasBin: true + peerDependencies: + svelte: ^3.24.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + chokidar: 3.5.3 + fast-glob: 3.2.12 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 3.55.1 + svelte-preprocess: 4.10.7_4x7phaipmicbaooxtnresslofa + typescript: 4.9.5 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - node-sass + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-hmr/0.15.1_svelte@3.55.1: + resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: '>=3.19.0' + dependencies: + svelte: 3.55.1 + dev: true + + /svelte-preprocess/4.10.7_4x7phaipmicbaooxtnresslofa: + resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==} + engines: {node: '>= 9.11.2'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + node-sass: '*' + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 + svelte: ^3.23.0 + typescript: ^3.9.5 || ^4.0.0 + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + node-sass: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + dependencies: + '@types/pug': 2.0.6 + '@types/sass': 1.45.0 + detect-indent: 6.1.0 + magic-string: 0.25.9 + sorcery: 0.10.0 + strip-indent: 3.0.0 + svelte: 3.55.1 + typescript: 4.9.5 + dev: true + + /svelte/3.55.1: + resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==} + engines: {node: '>= 8'} + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tslib/2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + dev: true + + /typescript/4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + /uc.micro/1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /vite/4.1.4: + resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.16.17 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.18.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitefu/0.2.4_vite@4.1.4: + resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 4.1.4 + dev: true + + /w3c-keyname/2.2.6: + resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==} + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws/7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + requiresBuild: true + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + optional: true + + /y-prosemirror/1.2.0_vhy2hiocjqydif5dwvxgfnpihi: + resolution: {integrity: sha512-t3uxuX4HIkb1GNt8jV+dplRbNH2OmQD/BNeCCbjLD3Mq0o6JEXxHedv58ZIPFDE6ma24jljlL+u8pGvN6B37XQ==} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + dependencies: + lib0: 0.2.63 + prosemirror-model: 1.18.3 + prosemirror-state: 1.4.2 + prosemirror-view: 1.29.2 + typescript: 4.9.5 + y-protocols: 1.0.5 + yjs: 13.5.48 + dev: false + + /y-protocols/1.0.5: + resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} + dependencies: + lib0: 0.2.63 + dev: false + + /y-webrtc/10.2.4: + resolution: {integrity: sha512-yoTl2tXdJsLLYIp2X2GRqNywgGwk4CEs3NcXJmgLLq2Ga8VX86q3Y3QBLy5mi30InRjjKL/ymCasAsydXTta+w==} + engines: {node: '>=12'} + hasBin: true + dependencies: + lib0: 0.2.63 + simple-peer: 9.11.1 + y-protocols: 1.0.5 + optionalDependencies: + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /yjs/13.5.48: + resolution: {integrity: sha512-RFUqe1UQa1iKfQ9wZkZQP33iEBBSTz6sW3S5CWzHEe0JVksOi8AzspSTvQk5VskoJj4HzMUYgaNcPrE0MqJ8xQ==} + dependencies: + lib0: 0.2.63 + dev: false diff --git a/src/App.svelte b/src/App.svelte new file mode 100644 index 0000000..d8e9a17 --- /dev/null +++ b/src/App.svelte @@ -0,0 +1,43 @@ + + +
+ + +
+ + diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..b1ed166 --- /dev/null +++ b/src/app.css @@ -0,0 +1,15 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..dba3da5 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,26 @@ +// https://www.npmjs.com/package/@poppanator/sveltekit-svg +declare module "*.svg" { + import type { ComponentType, SvelteComponentTyped } from "svelte"; + import type { SVGAttributes } from "svelte/elements"; + + const content: ComponentType< + SvelteComponentTyped> + >; + + export default content; +} + +declare module "*.svg?src" { + const content: string; + export default content; +} + +declare module "*.svg?url" { + const content: string; + export default content; +} + +declare module "*.svg?dataurl" { + const content: string; + export default content; +} diff --git a/src/editor/BubbleMenu.svelte b/src/editor/BubbleMenu.svelte new file mode 100644 index 0000000..fe91d6e --- /dev/null +++ b/src/editor/BubbleMenu.svelte @@ -0,0 +1,163 @@ + + + diff --git a/src/editor/Editor.svelte b/src/editor/Editor.svelte new file mode 100644 index 0000000..0a30278 --- /dev/null +++ b/src/editor/Editor.svelte @@ -0,0 +1,81 @@ + + +
+ {#if view} + + + {/if} + +
+
diff --git a/src/editor/MenuBar.svelte b/src/editor/MenuBar.svelte new file mode 100644 index 0000000..723ad57 --- /dev/null +++ b/src/editor/MenuBar.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/editor/bubblemenu/SimpleMarkItem.svelte b/src/editor/bubblemenu/SimpleMarkItem.svelte new file mode 100644 index 0000000..efc5227 --- /dev/null +++ b/src/editor/bubblemenu/SimpleMarkItem.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/editor/bubblemenu/coords.ts b/src/editor/bubblemenu/coords.ts new file mode 100644 index 0000000..f7c6142 --- /dev/null +++ b/src/editor/bubblemenu/coords.ts @@ -0,0 +1,104 @@ +import type { EditorView } from "prosemirror-view"; + +function textRange( + node: Node, + from: number = 0, + to: number | null = null +): Range { + const range = document.createRange(); + range.setEnd(node, to == null ? node.nodeValue.length : to); + range.setStart(node, Math.max(from, 0)); + return range; +} + +function singleRect(object: Element | Range, bias: number) { + const rects = object.getClientRects(); + return !rects.length + ? object.getBoundingClientRect() + : rects[bias < 0 ? 0 : rects.length - 1]; +} + +interface Coords { + top: number; + bottom: number; + left: number; + right: number; +} + +function coordsAtPos( + view: EditorView, + pos: number, + end: boolean = false +): Coords { + const { node, offset } = (view as any).docView.domFromPos(pos); + let side: "left" | "right"; + let rect: DOMRect; + if (node.nodeType === 3) { + if (end && offset < node.nodeValue.length) { + rect = singleRect(textRange(node, offset - 1, offset), -1); + side = "right"; + } else if (offset < node.nodeValue.length) { + rect = singleRect(textRange(node, offset, offset + 1), -1); + side = "left"; + } + } else if (node.firstChild) { + if (offset < node.childNodes.length) { + const child = node.childNodes[offset]; + rect = singleRect(child.nodeType === 3 ? textRange(child) : child, -1); + side = "left"; + } + if ((!rect || rect.top === rect.bottom) && offset) { + const child = node.childNodes[offset - 1]; + rect = singleRect(child.nodeType === 3 ? textRange(child) : child, 1); + side = "right"; + } + } else { + rect = node.getBoundingClientRect(); + side = "left"; + } + const x = rect[side]; + + return { + top: rect.top, + bottom: rect.bottom, + left: x, + right: x, + }; +} + +export function refreshCoords(view: EditorView, bubbleEl: HTMLElement) { + // Brutally stolen from https://github.com/ueberdosis/tiptap/blob/d2cf88fd166092d6df079cb47fe2a55520fadf80/packages/tiptap/src/Plugins/MenuBubble.js + const { from, to } = view.state.selection; + + // These are in screen coordinates + // We can't use EditorView.coordsAtPos here because it can't handle linebreaks correctly + // See: https://github.com/ProseMirror/prosemirror-view/pull/47 + const start = coordsAtPos(view, from); + const end = coordsAtPos(view, to, true); + + // The box in which the tooltip is positioned, to use as base + const parent = bubbleEl.offsetParent; + + if (!parent) { + console.error( + "Me parece que te falto importar el CSS. `import '@suttyweb/editor/dist/style.css';`" + ); + // TODO: i18n + throw new Error( + "¡El editor tuvo un error! Contactar a lxs desarrolladorxs." + ); + } + + const box = parent.getBoundingClientRect(); + const el = bubbleEl.getBoundingClientRect(); + + const _left = Math.max((start.left + end.left) / 2 - box.left, el.width / 2); + + return { + left: Math.round( + _left + el.width / 2 > box.width ? box.width - el.width / 2 : _left + ), + bottom: Math.round(box.bottom - start.top), + top: Math.round(end.bottom - box.top), + }; +} diff --git a/src/editor/demo.css b/src/editor/demo.css new file mode 100644 index 0000000..82c5ab3 --- /dev/null +++ b/src/editor/demo.css @@ -0,0 +1,22 @@ +@import "fork-awesome/css/fork-awesome.min.css"; + +* { + box-sizing: border-box; + text-rendering: optimizeLegibility; +} +*::before, +*::after { + box-sizing: border-box; +} + +html { + margin: 1rem; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +.editor { + max-width: 60rem; + margin: 2rem auto; +} diff --git a/src/editor/editor.css b/src/editor/editor.css new file mode 100644 index 0000000..09b15fe --- /dev/null +++ b/src/editor/editor.css @@ -0,0 +1,194 @@ +@import "prosemirror-view/style/prosemirror.css"; + +.editor { + position: relative; +} + +.editor *::before, +.editor *::after { + box-sizing: border-box; +} + +/* para ver los cambios con el sobrerayado */ +::selection, +::-moz-selection { + background: rgba(0, 0, 0, 0.3); +} + +.editor .menubar { + position: sticky; + top: 0px; + z-index: 69; + background: white; + border-bottom: 1px solid #bbb; +} + +.editor .menubar .separator { + border-right: 2px solid #bbb; + margin: 0 0.5rem; +} + +.editor .menubar button { + appearance: none; + background: none; + border: none; + border-radius: 2px; + line-height: 1; + padding: 0.4em 0.6em; + margin: 0.2em; + transition: all 0.2s; +} + +.editor .menubar button.active { + background: #ddd; +} + +.editor .menubar button:hover { + background: #eee; +} + +.editor .menubar button svg { + width: 1.5rem; + height: 1.5rem; +} + +.editor .bubble { + display: flex !important; + position: absolute; + z-index: 420; + transform: translateX(-50%); + background: black; + color: white; + border-radius: 5px; + padding: 0rem; + margin-bottom: 0.5rem; + + visibility: visible; + opacity: 1; + + transition: opacity 0.2s, visibility 0.2s; +} +.editor .bubble[hidden] { + visibility: hidden; + opacity: 0; +} + +.editor .bubble input { + appearance: none; + background: none; + color: inherit; + border: none; + font-size: 1.25em; +} + +.editor .bubble .separator { + border-right: 1px solid #777; + margin: 0 0.5rem; +} + +.editor .bubble button { + appearance: none; + background: none; + color: inherit; + border: none; + border-radius: 2px; + padding: 0.3em; + margin: 0.2em; + width: 1.8rem; + height: 1.8rem; + font-size: 1em; + line-height: 1; + + transition: background 0.2s; +} +.editor .bubble button:hover { + background: #333; +} +.editor .bubble button.active { + background: #555; +} + +.editor .bubble .color-button { + padding: 0.4em; +} +.editor .bubble .color { + display: inline-block; + width: 1em; + height: 1em; + border-radius: 100%; +} + +.editor .bubble p { + margin: 0; +} + +.ProseMirror { + width: 100%; + padding: 0.5em; + + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} +.ProseMirror:focus { + outline: lightskyblue solid; +} + +.ProseMirror img { + max-width: 100%; +} + +.ProseMirror figure { + margin: 0; + padding: 0.5em 2em; +} +.ProseMirror figure figcaption::before { + content: "Descripción: "; + color: #666; +} +.ProseMirror .ProseMirror-multimedia-placeholder { + margin-bottom: 0.5em; +} +.ProseMirror .ProseMirror-multimedia-placeholder::before { + content: "Clickea aquí para subir una imágen, audio o documento."; + color: #666; +} + +.ProseMirror blockquote { + background-color: #f5f5f5; + border-left: 5px solid #dbdbdb; + padding: 1.25em 1.5em; + margin: 0.5em; +} + +.ProseMirror-menubar { + position: sticky; + top: 0px; + z-index: 69; + background: white; + border-bottom: 1px solid #bbb; +} + +.ProseMirror-menubar-separator { + border-right: 2px solid #bbb; + margin: 0 0.5rem; +} + +.ProseMirror-menubar-button { + appearance: none; + background: none; + border: none; + border-radius: 8px; + font-size: 1em; + padding: 0.4em 0.6em; + margin: 0.2em; + transition: all 0.2s; +} + +.ProseMirror-menubar-button-active { + background: #ddd; +} + +.ProseMirror-menubar-button:hover { + background: #eee; +} diff --git a/src/editor/global.d.ts b/src/editor/global.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/src/editor/global.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/src/editor/keymap.ts b/src/editor/keymap.ts new file mode 100644 index 0000000..b0a6ef3 --- /dev/null +++ b/src/editor/keymap.ts @@ -0,0 +1,69 @@ +// derivado de prosemirror-commands/src/commands.js + +import { + chainCommands, + createParagraphNear, + deleteSelection, + exitCode, + joinBackward, + joinForward, + liftEmptyBlock, + newlineInCode, + selectAll, + selectNodeBackward, + selectNodeForward, + splitBlock, +} from "prosemirror-commands"; +import { undo, redo } from "y-prosemirror"; +import { + splitListItem, + liftListItem, + sinkListItem, +} from "prosemirror-schema-list"; + +import { schema } from "./schema"; + +const backspace = chainCommands( + deleteSelection, + joinBackward, + selectNodeBackward +); +const del = chainCommands(deleteSelection, joinForward, selectNodeForward); + +const pcBaseKeymap = { + Enter: chainCommands( + newlineInCode, + createParagraphNear, + liftEmptyBlock, + // XXX: hack + splitListItem(schema.nodes.list_item as any), + splitBlock + ), + "Mod-Enter": chainCommands(exitCode, splitBlock), + Backspace: backspace, + "Mod-Backspace": backspace, + Delete: del, + "Mod-Delete": del, + "Mod-a": selectAll, + "Shift-Tab": liftListItem(schema.nodes.list_item), + Tab: sinkListItem(schema.nodes.list_item), +}; + +const macBaseKeymap = { + ...pcBaseKeymap, + "Ctrl-h": pcBaseKeymap["Backspace"], + "Alt-Backspace": pcBaseKeymap["Mod-Backspace"], + "Ctrl-d": pcBaseKeymap["Delete"], + "Ctrl-Alt-Backspace": pcBaseKeymap["Mod-Delete"], + "Alt-Delete": pcBaseKeymap["Mod-Delete"], + "Alt-d": pcBaseKeymap["Mod-Delete"], +}; + +const mac = + typeof navigator != "undefined" ? /Mac/.test(navigator.platform) : false; + +export const baseKeymap = { + ...(mac ? macBaseKeymap : pcBaseKeymap), + "Mod-z": undo, + "Mod-y": redo, +}; diff --git a/src/editor/main.ts b/src/editor/main.ts new file mode 100644 index 0000000..616fac3 --- /dev/null +++ b/src/editor/main.ts @@ -0,0 +1,11 @@ +import "./demo.css"; +import Editor from "./Editor.svelte"; + +const editor = new Editor({ + target: document.body, + props: { + textareaEl: document.body.querySelector("textarea"), + }, +}); + +export default editor; diff --git a/src/editor/menubar/AlignSelect.svelte b/src/editor/menubar/AlignSelect.svelte new file mode 100644 index 0000000..e4840c9 --- /dev/null +++ b/src/editor/menubar/AlignSelect.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/editor/menubar/BlockQuoteItem.svelte b/src/editor/menubar/BlockQuoteItem.svelte new file mode 100644 index 0000000..f80d221 --- /dev/null +++ b/src/editor/menubar/BlockQuoteItem.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/editor/menubar/BlockSelect.svelte b/src/editor/menubar/BlockSelect.svelte new file mode 100644 index 0000000..9b75423 --- /dev/null +++ b/src/editor/menubar/BlockSelect.svelte @@ -0,0 +1,49 @@ + + + diff --git a/src/editor/menubar/ListItem.svelte b/src/editor/menubar/ListItem.svelte new file mode 100644 index 0000000..f90d73a --- /dev/null +++ b/src/editor/menubar/ListItem.svelte @@ -0,0 +1,44 @@ + + + diff --git a/src/editor/menubar/UploadItem.svelte b/src/editor/menubar/UploadItem.svelte new file mode 100644 index 0000000..08abf0c --- /dev/null +++ b/src/editor/menubar/UploadItem.svelte @@ -0,0 +1,86 @@ + + + + diff --git a/src/editor/ps-utils.ts b/src/editor/ps-utils.ts new file mode 100644 index 0000000..6df23d0 --- /dev/null +++ b/src/editor/ps-utils.ts @@ -0,0 +1,237 @@ +import { chainCommands, setBlockType } from "prosemirror-commands"; +import type { + Mark, + MarkType, + NodeType, + ResolvedPos, + Node as ProsemirrorNode, +} from "prosemirror-model"; +import type { EditorState, Selection } from "prosemirror-state"; +import type { EditorView } from "prosemirror-view"; + +import type { Align } from "./schema"; + +export type Command = ( + state: EditorState, + dispatch?: EditorView["dispatch"] +) => boolean; + +// A lot of this is from https://github.com/ueberdosis/tiptap/blob/main/packages/tiptap-commands + +export function getMarkRange( + $pos: ResolvedPos | null = null, + type: MarkType | null = null +) { + if (!$pos || !type) { + return false; + } + + const start = $pos.parent.childAfter($pos.parentOffset); + + if (!start.node) { + return false; + } + + const link = start.node.marks.find((mark) => mark.type === type); + if (!link) { + return false; + } + + let startIndex = $pos.index(); + let startPos = $pos.start() + start.offset; + let endIndex = startIndex + 1; + let endPos = startPos + start.node.nodeSize; + + while ( + startIndex > 0 && + link.isInSet($pos.parent.child(startIndex - 1).marks) + ) { + startIndex -= 1; + startPos -= $pos.parent.child(startIndex).nodeSize; + } + + while ( + endIndex < $pos.parent.childCount && + link.isInSet($pos.parent.child(endIndex).marks) + ) { + endPos += $pos.parent.child(endIndex).nodeSize; + endIndex += 1; + } + + return { from: startPos, to: endPos }; +} + +export function updateMark(type: MarkType, attrs: any): Command { + return (state, dispatch) => { + const { tr, selection, doc } = state; + + const { ranges, empty } = selection; + + if (empty) { + const range = getMarkRange(selection.$from, type); + if (!range) throw new Error("What the fuck"); + const { from, to } = range; + + if (doc.rangeHasMark(from, to, type)) { + tr.removeMark(from, to, type); + } + + tr.addMark(from, to, type.create(attrs)); + } else { + ranges.forEach((ref$1) => { + const { $to, $from } = ref$1; + + if (doc.rangeHasMark($from.pos, $to.pos, type)) { + tr.removeMark($from.pos, $to.pos, type); + } + + tr.addMark($from.pos, $to.pos, type.create(attrs)); + }); + } + + if (dispatch) { + dispatch(tr); + } + return true; + }; +} + +export function removeMark(type: MarkType): Command { + return (state, dispatch) => { + const { tr, selection } = state; + let { from, to } = selection; + const { $from, empty } = selection; + + if (empty) { + const range = getMarkRange($from, type); + if (!range) throw new Error("No"); + + from = range.from; + to = range.to; + } + + tr.removeMark(from, to, type); + + if (dispatch) { + dispatch(tr); + } + return true; + }; +} + +export function toggleNode( + type: NodeType, + attrs: any, + /// es el tipo que se setea si ya es el type querido; probablemente querés + /// que sea el type de párrafo + alternateType: NodeType +): Command { + return chainCommands(setBlockType(type, attrs), setBlockType(alternateType)); +} + +export function commandListener( + view: EditorView, + command: Command +): (event: Event) => void { + return (event) => { + event.preventDefault(); + command(view.state, view.dispatch); + }; +} + +export default function findParentNodeClosestToPos( + $pos: ResolvedPos, + predicate: (node: ProsemirrorNode) => boolean +) { + for (let i = $pos.depth; i > 0; i -= 1) { + const node = $pos.node(i); + + if (predicate(node)) { + return { + pos: i > 0 ? $pos.before(i) : 0, + start: $pos.start(i), + depth: i, + node, + }; + } + } +} + +export function nodeIsActiveFn( + type: NodeType, + attrs?: any, + checkParents: boolean = false +): (state: EditorState) => boolean { + return (state) => { + let { $from, to } = state.selection; + return ( + to <= $from.end() && + (checkParents + ? !!findParentNodeClosestToPos($from, (n) => n.type == type) + : $from.parent.hasMarkup(type, attrs)) + ); + }; +} +export function getAttrFn(attrKey: string): (state: EditorState) => any { + return (state) => { + let { from, to } = state.selection; + let value: any = undefined; + state.doc.nodesBetween(from, to, (node, pos) => { + if (value !== undefined) return false; + if (!node.isTextblock) return; + if (attrKey in node.attrs) value = node.attrs[attrKey]; + }); + return value; + }; +} + +export function markIsActive(state: EditorState, type: MarkType): boolean { + let { from, to } = state.selection; + return state.doc.rangeHasMark(from, to, type); +} + +export interface MarkMatch { + node: ProsemirrorNode; + position: number; + mark: Mark; +} + +export function getFirstMarkInSelection( + state: EditorState, + type: MarkType +): MarkMatch { + const { to, from } = state.selection; + + let match: MarkMatch; + state.selection.$from.doc.nodesBetween(from, to, (node, position) => { + if (!match) { + const mark = type.isInSet(node.marks); + if (!mark) return; + match = { node, position, mark }; + } + }); + + return match; +} + +export function setAlign(align: Align): Command { + return (state, dispatch) => { + let { from, to } = state.selection; + let node: ProsemirrorNode | null = null; + state.doc.nodesBetween(from, to, (_node, pos) => { + if (node) return false; + if (!_node.isTextblock) return; + if ( + _node.type == state.schema.nodes.paragraph || + _node.type == state.schema.nodes.heading + ) { + node = _node; + } + }); + if (!node) return false; + if (dispatch) + return setBlockType(node.type, { ...node.attrs, align })(state, dispatch); + + return true; + }; +} diff --git a/src/editor/schema.ts b/src/editor/schema.ts new file mode 100644 index 0000000..c2fa63e --- /dev/null +++ b/src/editor/schema.ts @@ -0,0 +1,326 @@ +import { Schema } from "prosemirror-model"; + +const hex = (x: string) => ("0" + parseInt(x).toString(16)).slice(-2); +// https://stackoverflow.com/a/3627747 +// TODO: cambiar por una solución más copada +function rgbToHex(rgb: string): string { + const matches = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + if (!matches) throw new Error("no pude parsear el rgb()"); + return "#" + hex(matches[1]) + hex(matches[2]) + hex(matches[3]); +} + +export type Align = null | "center" | "right"; + +export type MultimediaKind = "img" | "video" | "audio" | "iframe"; + +function getAlign(node: HTMLElement): Align | null { + let align = node.style.textAlign || node.getAttribute("data-align"); + if (align !== "center" && align !== "right") return null; + return align; +} + +function getHeadingAttrs( + level: number +): (n: Node) => { [key: string]: string } { + return (n) => ({ level, align: getAlign(n as HTMLElement) }); +} + +export const schema = new Schema({ + nodes: { + doc: { + content: "block+", + }, + + paragraph: { + content: "inline*", + group: "block", + attrs: { align: { default: null } }, + parseDOM: [ + { + tag: "p", + getAttrs(n) { + return { align: getAlign(n as HTMLElement) }; + }, + }, + ], + toDOM(node) { + return ["p", { style: `text-align: ${node.attrs.align}` }, 0]; + }, + }, + + blockquote: { + content: "block+", + group: "block", + parseDOM: [{ tag: "blockquote" }], + toDOM() { + return ["blockquote", 0]; + }, + }, + + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + toDOM() { + return ["div", ["hr"]]; + }, + }, + + heading: { + attrs: { level: { default: 1 }, align: { default: null } }, + content: "text*", + group: "block", + defining: true, + parseDOM: [ + { tag: "h1", getAttrs: getHeadingAttrs(1) }, + { tag: "h2", getAttrs: getHeadingAttrs(2) }, + { tag: "h3", getAttrs: getHeadingAttrs(3) }, + { tag: "h4", getAttrs: getHeadingAttrs(4) }, + { tag: "h5", getAttrs: getHeadingAttrs(5) }, + { tag: "h6", getAttrs: getHeadingAttrs(6) }, + ], + toDOM(node) { + return [ + "h" + node.attrs.level, + { style: `text-align: ${node.attrs.align}` }, + 0, + ]; + }, + }, + + code_block: { + content: "text*", + group: "block", + code: true, + defining: true, + marks: "", + attrs: { params: { default: "" } }, + parseDOM: [ + { + tag: "pre", + preserveWhitespace: "full", + getAttrs: (node) => ({ + params: (node as Element).getAttribute("data-params") || "", + }), + }, + ], + toDOM(node) { + return [ + "pre", + node.attrs.params ? { "data-params": node.attrs.params } : {}, + ["code", 0], + ]; + }, + }, + + ordered_list: { + content: "list_item+", + group: "block", + attrs: { order: { default: 1 } }, + parseDOM: [ + { + tag: "ol", + getAttrs(dom: Element) { + return { + order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1, + }; + }, + }, + ], + toDOM(node) { + return node.attrs.order == 1 + ? ["ol", 0] + : ["ol", { start: node.attrs.order }, 0]; + }, + }, + bullet_list: { + content: "list_item+", + group: "block", + parseDOM: [{ tag: "ul" }], + toDOM: () => ["ul", 0], + }, + + list_item: { + content: "paragraph block*", + defining: true, + parseDOM: [{ tag: "li" }], + toDOM() { + return ["li", 0]; + }, + }, + + text: { + group: "inline", + }, + + multimedia: { + group: "block", + attrs: { src: {}, kind: {} }, + content: "text*", + parseDOM: [ + { + tag: "figure", + getAttrs(dom) { + const child: HTMLElement = + (dom as Element).querySelector("img") || + (dom as Element).querySelector("video") || + (dom as Element).querySelector("audio") || + (dom as Element).querySelector("iframe"); + + if (child instanceof HTMLImageElement) { + return { src: child.src, kind: "img" }; + } else if (child instanceof HTMLVideoElement) { + return { src: child.src, kind: "video" }; + } else if (child instanceof HTMLAudioElement) { + return { src: child.src, kind: "audio" }; + } else if (child instanceof HTMLIFrameElement) { + return { src: child.src, kind: "iframe" }; + } + }, + }, + { + tag: "img", + getAttrs(dom) { + return { src: (dom as HTMLImageElement).src, kind: "img" }; + }, + }, + { + tag: "video", + getAttrs(dom) { + return { src: (dom as HTMLVideoElement).src, kind: "video" }; + }, + }, + { + tag: "audio", + getAttrs(dom) { + return { src: (dom as HTMLAudioElement).src, kind: "audio" }; + }, + }, + { + tag: "iframe", + getAttrs(dom) { + return { src: (dom as HTMLIFrameElement).src, kind: "iframe" }; + }, + }, + ], + toDOM(node) { + return [ + "figure", + [node.attrs.kind, { src: node.attrs.src }], + ["figcaption", 0], + ]; + }, + draggable: true, + }, + + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM() { + return ["br"]; + }, + }, + }, + + marks: { + em: { + parseDOM: [ + { tag: "i" }, + { tag: "em" }, + { style: "font-style", getAttrs: (value) => value == "italic" && null }, + ], + toDOM() { + return ["em"]; + }, + }, + + strong: { + parseDOM: [ + { tag: "b" }, + { tag: "strong" }, + { + style: "font-weight", + getAttrs: (value) => + /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null, + }, + ], + toDOM() { + return ["strong"]; + }, + }, + + underline: { + parseDOM: [{ tag: "u" }], + toDOM() { + return ["u"]; + }, + }, + strikethrough: { + parseDOM: [{ tag: "del" }], + toDOM() { + return ["del"]; + }, + }, + small: { + parseDOM: [{ tag: "small" }], + toDOM() { + return ["small"]; + }, + }, + + mark: { + attrs: { + color: { default: "#f206f9" }, + }, + parseDOM: [ + { + tag: "mark", + getAttrs(dom) { + const prop = (dom as HTMLElement).style.backgroundColor; + const hex = rgbToHex(prop); + return { + color: hex, + }; + }, + }, + ], + toDOM(node) { + return ["mark", { style: `background-color:${node.attrs.color}` }]; + }, + }, + + link: { + attrs: { + href: {}, + }, + inclusive: false, + parseDOM: [ + { + tag: "a[href]", + getAttrs(dom) { + return { + href: (dom as Element).getAttribute("href"), + }; + }, + }, + ], + toDOM(node) { + const attrs = { + ...node.attrs, + rel: "noopener", + referrerpolicy: "strict-origin-when-cross-origin", + }; + + return ["a", attrs]; + }, + }, + + code: { + parseDOM: [{ tag: "code" }], + toDOM() { + return ["code"]; + }, + }, + }, +}); diff --git a/src/editor/upload.ts b/src/editor/upload.ts new file mode 100644 index 0000000..d394726 --- /dev/null +++ b/src/editor/upload.ts @@ -0,0 +1,48 @@ +import { EditorState, Plugin } from "prosemirror-state"; +import { Decoration, DecorationSet } from "prosemirror-view"; +import { h } from "./utils"; + +export function uploadFile(file: File): Promise { + return new Promise((resolve, reject) => { + reject("TODO: implementar subidas"); + }); +} + +export const placeholderPlugin = new Plugin({ + state: { + init(): DecorationSet { + return DecorationSet.empty; + }, + apply(tr, set: DecorationSet) { + // Adjust decoration positions to changes made by the transaction + set = set.map(tr.mapping, tr.doc); + // See if the transaction adds or removes any placeholders + let action = tr.getMeta(this); + if (action && action.add) { + let widgetEl = h("div", { class: "ProseMirror-placeholder" }, [ + "Subiendo archivo...", + ]); + let deco = Decoration.widget(action.add.pos, widgetEl, { + id: action.add.id, + }); + set = set.add(tr.doc, [deco]); + } else if (action && action.remove) { + set = set.remove( + set.find(undefined, undefined, (spec) => spec.id == action.remove.id) + ); + } + return set; + }, + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, +}); + +export function findPlaceholder(state: EditorState, id: any): number | null { + const decos: DecorationSet = placeholderPlugin.getState(state); + const found = decos.find(undefined, undefined, (spec) => spec.id == id); + return found.length ? found[0].from : null; +} diff --git a/src/editor/utils.ts b/src/editor/utils.ts new file mode 100644 index 0000000..8669c9e --- /dev/null +++ b/src/editor/utils.ts @@ -0,0 +1,49 @@ +interface Props { + dataset?: { [key: string]: string }; + attributes?: { [key: string]: string }; + contenteditable?: "true" | "false"; + class?: string; + on?: { [key: string]: EventListener }; +} + +export function h( + tagName: string, + props: Props, + children: (Node | string | undefined)[] +): HTMLElement { + const el = document.createElement(tagName); + if (props.class) { + el.setAttribute("class", props.class); + } + if (props.dataset) { + for (const [key, value] of Object.entries(props.dataset)) { + el.dataset[key] = value; + } + } + if (props.contenteditable) { + el.contentEditable = props.contenteditable; + } + if (props.attributes) { + for (const [key, value] of Object.entries(props.attributes)) { + el.setAttribute(key, value); + } + } + if (props.on) { + for (const [key, value] of Object.entries(props.on)) { + el.addEventListener(key, value); + } + } + for (const node of children) { + if (typeof node === "string") { + el.appendChild(document.createTextNode(node)); + } else if (node) { + el.appendChild(node); + } + } + return el; +} + +export enum ListKind { + Unordered, + Ordered, +} diff --git a/src/lib/doc.ts b/src/lib/doc.ts new file mode 100644 index 0000000..8c73fa1 --- /dev/null +++ b/src/lib/doc.ts @@ -0,0 +1,37 @@ +import * as Y from "yjs"; +import { WebrtcProvider } from "y-webrtc"; +import { nanoid } from "nanoid"; + +export type WorldIdentifier = { + room: string; + password: string; +}; + +export type WorldY = { + ydoc: Y.Doc; + webrtcProvider: WebrtcProvider; +}; + +export function generateNewWorld(): WorldIdentifier { + return { + room: nanoid(), + password: nanoid(), + }; +} + +export function getWorldY(world: WorldIdentifier): WorldY { + const ydoc = new Y.Doc(); + const provider = new WebrtcProvider(world.room, ydoc, { + password: world.password, + signaling: [ + "wss://signaling.yjs.dev", + "wss://y-webrtc-signaling-eu.herokuapp.com", + "wss://y-webrtc-signaling-us.herokuapp.com", + ], + }); + return { ydoc, webrtcProvider: provider }; +} + +export function getWorldPage(ydoc: Y.Doc, pageId: string): Y.XmlFragment { + return ydoc.getXmlFragment(`doc/${pageId}`); +} diff --git a/src/lib/routes.ts b/src/lib/routes.ts new file mode 100644 index 0000000..13361ce --- /dev/null +++ b/src/lib/routes.ts @@ -0,0 +1,34 @@ +import navaid from "navaid"; +import { writable } from "svelte/store"; + +import ChooseWorld from "../views/ChooseWorld.svelte"; +import CreateWorld from "../views/CreateWorld.svelte"; +import NotFound from "../views/NotFound.svelte"; +import Page from "../views/Page.svelte"; + +export let router = navaid("/", () => + currentRoute.set({ component: NotFound }) +); +export let currentRoute = writable<{ + // XXX: in lack of a better type for Svelte components + component: any; + params?: Record; +}>({ component: ChooseWorld }); + +export const routes = { + ChooseWorld: "/", + CreateWorld: "/create", + Page: "/w/:worldId/:pageId", +}; + +router.on(routes.ChooseWorld, () => + currentRoute.set({ component: ChooseWorld }) +); +router.on(routes.CreateWorld, () => + currentRoute.set({ component: CreateWorld }) +); +router.on(routes.Page, (params) => + currentRoute.set({ component: Page, params }) +); + +router.listen(); diff --git a/src/lib/worldStorage.ts b/src/lib/worldStorage.ts new file mode 100644 index 0000000..cc9770d --- /dev/null +++ b/src/lib/worldStorage.ts @@ -0,0 +1,18 @@ +import type { WorldIdentifier } from "./doc"; + +const localStorageKey = "schreiben-worlds"; + +export function loadWorlds(): Promise { + let json = localStorage.getItem(localStorageKey); + if (!json) json = "[]"; + return Promise.resolve(JSON.parse(json)); +} + +export async function writeWorlds( + callback: (worlds: WorldIdentifier[]) => WorldIdentifier[] +): Promise { + const oldWorlds = await loadWorlds(); + const newWorlds = callback(oldWorlds); + localStorage.setItem(localStorageKey, JSON.stringify(newWorlds)); + return newWorlds; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..8a909a1 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,8 @@ +import './app.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app'), +}) + +export default app diff --git a/src/views/ChooseWorld.svelte b/src/views/ChooseWorld.svelte new file mode 100644 index 0000000..75cd2e9 --- /dev/null +++ b/src/views/ChooseWorld.svelte @@ -0,0 +1,25 @@ + + +

Buen día.

+

Elegí un mundo.

+ +{#await worldsPromise then worlds} + +{/await} diff --git a/src/views/CreateWorld.svelte b/src/views/CreateWorld.svelte new file mode 100644 index 0000000..fb41d31 --- /dev/null +++ b/src/views/CreateWorld.svelte @@ -0,0 +1,19 @@ + + +
+ +
diff --git a/src/views/NotFound.svelte b/src/views/NotFound.svelte new file mode 100644 index 0000000..4e6a138 --- /dev/null +++ b/src/views/NotFound.svelte @@ -0,0 +1,3 @@ +

No encontré esa página

+ + diff --git a/src/views/Page.svelte b/src/views/Page.svelte new file mode 100644 index 0000000..6da54c2 --- /dev/null +++ b/src/views/Page.svelte @@ -0,0 +1,45 @@ + + +🠔 Elegir otro mundo +{#await docPromise then doc} + +{:catch error} + {error} + Volver al inicio +{/await} + + diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c4e1c5f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..65dbdb9 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node" + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5291ba7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import svg from "@poppanator/sveltekit-svg"; +import basicSsl from "@vitejs/plugin-basic-ssl"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + svelte(), + svg({ + svgoOptions: { + multipass: true, + plugins: [ + { + name: "preset-default", + // by default svgo removes the viewBox which prevents svg icons from scaling + // not a good idea! https://github.com/svg/svgo/pull/1461 + params: { overrides: { removeViewBox: false } }, + }, + ], + }, + }), + // lo necesitamos para crypto.subtle para nanoid + basicSsl(), + ], +});