Refactoring Project to use proper Inversion of Control (#171)

* chore: reworking plugin internals into modules

* feat: settings module is now working, added debug module, enabled new api module

* chore: restoring external API

* feat: refactoring project to use proper inversion of control

* feat: plugins can now be loaded in any order

* chore: final file migration, all typescript is in modules now

* chore: fixing dependencies of cellParser

* feat: csv and json views are only registered when they are not colliding with other plugins

* feat: fixed library behaviour on canvas
This commit is contained in:
Kacper Kula 2025-08-10 10:34:54 +02:00 committed by GitHub
parent dfdad63476
commit 285684dfc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 2722 additions and 1639 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": minor
---
tables and charts now render better on canvas and work when you use external files too

View file

@ -0,0 +1,5 @@
---
"sqlseal": patch
---
csv and json views are now only registered when not colliding with other existing plugins

View file

@ -0,0 +1,5 @@
---
"sqlseal": minor
---
reworking plugin internals to organise code into modules

View file

@ -52,7 +52,7 @@ const workerPlugin = {
build.onLoad({ filter: /.*/, namespace: 'worker-code' }, async () => {
// Build worker code
const result = await esbuild.build({
entryPoints: ['src/database/worker/database.ts'],
entryPoints: ['src/modules/database/worker/database.ts'],
bundle: true,
write: false,
format: 'iife',

View file

@ -55,6 +55,8 @@
"@codemirror/language": "^6.11.1",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.37.1",
"@hypersphere/dity": "^0.0.9",
"@hypersphere/dity-graph": "^0.0.8",
"@hypersphere/omnibus": "^0.1.6",
"@jlongster/sql.js": "^1.6.7",
"@types/jsonpath": "^0.2.4",

View file

@ -20,6 +20,12 @@ importers:
'@codemirror/view':
specifier: ^6.37.1
version: 6.37.1
'@hypersphere/dity':
specifier: ^0.0.9
version: 0.0.9(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)
'@hypersphere/dity-graph':
specifier: ^0.0.8
version: 0.0.8(@types/node@22.15.29)(graphology-types@0.24.8)(sass-embedded@1.89.0)(sass@1.89.1)
'@hypersphere/omnibus':
specifier: ^0.1.6
version: 0.1.6
@ -149,7 +155,7 @@ importers:
version: 5.8.3
vitepress:
specifier: 2.0.0-alpha.5
version: 2.0.0-alpha.5(@algolia/client-search@5.25.0)(@types/node@22.15.29)(postcss@8.5.4)(sass-embedded@1.89.0)(sass@1.89.1)(search-insights@2.17.3)(typescript@5.8.3)
version: 2.0.0-alpha.5(@algolia/client-search@5.25.0)(@types/node@22.15.29)(postcss@8.5.6)(sass-embedded@1.89.0)(sass@1.89.1)(search-insights@2.17.3)(typescript@5.8.3)
vue:
specifier: ^3.5.16
version: 3.5.16(typescript@5.8.3)
@ -403,8 +409,8 @@ packages:
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@bufbuild/protobuf@2.5.1':
resolution: {integrity: sha512-lut4UTvKL8tqtend0UDu7R79/n9jA7Jtxf77RNPbxtmWqfWI4qQ9bTjf7KCS4vfqLmpQbuHr1ciqJumAgJODdw==}
'@bufbuild/protobuf@2.6.1':
resolution: {integrity: sha512-DaG6XlyKpz08bmHY5SGX2gfIllaqtDJ/KwVoxsmP22COOLYwDBe7yD3DZGwXem/Xq7QOc9cuR7R3MpAv5CFfDw==}
'@changesets/apply-release-plan@7.0.12':
resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==}
@ -704,6 +710,15 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@hypersphere/dity-graph@0.0.8':
resolution: {integrity: sha512-xm50liUz2iYLkNhwTy3sgmvcWJyJD+zweA3WCt90CiJ0eYRCJb5Bv4AnhilSBweTiZ2F/NGxrgzYT/jwRNnnHA==}
'@hypersphere/dity@0.0.8':
resolution: {integrity: sha512-GH1jYfjEf5fgCXZUL6SL20kemzQB1u+69luyn5x4o8+6QQy4LE87UA3EQMBzy75gYZPsPKfdpNfHRzfJElv/Wg==}
'@hypersphere/dity@0.0.9':
resolution: {integrity: sha512-oHwWDjMEyWsbAn2oYVXFuDu5xpg4G8FfmaACUkYeijPxeVLTfzaEhyedhOzxfQ8veIg3S9JxyKwN+mj/S09X0w==}
'@hypersphere/omnibus@0.1.6':
resolution: {integrity: sha512-agZuKyhdW0n1JoLYZUuA6Du1QoQn39/LapFgRtbJs7fyRM62C9O2PWISHUCwAKnC1Splshpd8glQgx5pA2zkCg==}
@ -941,101 +956,201 @@ packages:
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm-eabi@4.45.1':
resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.41.1':
resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==}
cpu: [arm64]
os: [android]
'@rollup/rollup-android-arm64@4.45.1':
resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.41.1':
resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-arm64@4.45.1':
resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.41.1':
resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.45.1':
resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.41.1':
resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-arm64@4.45.1':
resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.41.1':
resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.45.1':
resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-gnueabihf@4.45.1':
resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.45.1':
resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.41.1':
resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.45.1':
resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.41.1':
resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.45.1':
resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.45.1':
resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.45.1':
resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.45.1':
resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.41.1':
resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.45.1':
resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.41.1':
resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.45.1':
resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.41.1':
resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.45.1':
resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.41.1':
resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.45.1':
resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.41.1':
resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-arm64-msvc@4.45.1':
resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.41.1':
resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.45.1':
resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.41.1':
resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.45.1':
resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==}
cpu: [x64]
os: [win32]
'@shikijs/core@3.5.0':
resolution: {integrity: sha512-iycvvnVG7MWZHRNuoqpYkV3Qc8DNLU74Lxh/roDwUqJJoXRnCTbbVJGfSWAdBslUgJMsjSHwFL42i55voavDDg==}
@ -1060,6 +1175,11 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@sigma/node-border@3.0.0':
resolution: {integrity: sha512-mE3zUfjvJVuAMhSjiP/zdlkqe0OVTETxd04XHUwof01YqdzTk0OB4ACJIhWrwgsBXl7tTd9lPuKoroafLh8MtQ==}
peerDependencies:
sigma: '>=3.0.0-beta.17'
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@ -1111,6 +1231,9 @@ packages:
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/graceful-fs@4.1.9':
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
@ -1414,8 +1537,8 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
@ -1639,6 +1762,9 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
chroma-js@3.1.2:
resolution: {integrity: sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==}
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
@ -1895,8 +2021,8 @@ packages:
engines: {node: '>=4.0'}
hasBin: true
eslint-scope@8.3.0:
resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3:
@ -1907,6 +2033,10 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.15.0:
resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1917,8 +2047,8 @@ packages:
jiti:
optional: true
espree@10.3.0:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
esprima@1.2.2:
@ -1954,6 +2084,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@ -2003,6 +2137,14 @@ packages:
picomatch:
optional: true
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -2139,6 +2281,29 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
graphology-layout-forceatlas2@0.10.1:
resolution: {integrity: sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==}
peerDependencies:
graphology-types: '>=0.19.0'
graphology-layout@0.6.1:
resolution: {integrity: sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==}
peerDependencies:
graphology-types: '>=0.19.0'
graphology-types@0.24.8:
resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==}
graphology-utils@2.5.2:
resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==}
peerDependencies:
graphology-types: '>=0.23.0'
graphology@0.26.0:
resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==}
peerDependencies:
graphology-types: '>=0.24.0'
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@ -2229,6 +2394,9 @@ packages:
immutable@5.1.2:
resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==}
immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@ -2733,6 +2901,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
mnemonist@0.39.8:
resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
@ -2808,6 +2979,9 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
deprecated: This package is no longer supported.
obliterator@2.0.5:
resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
obsidian@1.8.7:
resolution: {integrity: sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==}
peerDependencies:
@ -2889,6 +3063,9 @@ packages:
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
pandemonium@2.4.1:
resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==}
papaparse@5.5.3:
resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==}
@ -2933,6 +3110,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
@ -2953,6 +3134,10 @@ packages:
resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
preact@10.26.8:
resolution: {integrity: sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==}
@ -3097,6 +3282,11 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rollup@4.45.1:
resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@ -3279,6 +3469,9 @@ packages:
shiki@3.5.0:
resolution: {integrity: sha512-1lyPuqIPPAlmR1BKtDkxiuoZTB2IKSyr+GeHXu4ReOyHoEMhCnUoGZDUv4SJRH0Bi4QmsEPsrkQCRSOgnVRC+g==}
sigma@3.0.2:
resolution: {integrity: sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@ -3631,6 +3824,46 @@ packages:
yaml:
optional: true
vite@7.0.5:
resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitepress@2.0.0-alpha.5:
resolution: {integrity: sha512-fhuGpJ4CETS/lrAHjKu3m88HwesZvAjZLFeIRr9Jejmewyogn1tm2L6lsVg7PWxPmOGoMfihzl3+L6jg6hrTnA==}
hasBin: true
@ -4026,7 +4259,7 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@bufbuild/protobuf@2.5.1': {}
'@bufbuild/protobuf@2.6.1': {}
'@changesets/apply-release-plan@7.0.12':
dependencies:
@ -4316,7 +4549,7 @@ snapshots:
dependencies:
ajv: 6.12.6
debug: 4.4.1
espree: 10.3.0
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
@ -4350,6 +4583,64 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@hypersphere/dity-graph@0.0.8(@types/node@22.15.29)(graphology-types@0.24.8)(sass-embedded@1.89.0)(sass@1.89.1)':
dependencies:
'@hypersphere/dity': 0.0.8(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)
'@sigma/node-border': 3.0.0(sigma@3.0.2(graphology-types@0.24.8))
chroma-js: 3.1.2
graphology: 0.26.0(graphology-types@0.24.8)
graphology-layout: 0.6.1(graphology-types@0.24.8)
graphology-layout-forceatlas2: 0.10.1(graphology-types@0.24.8)
lodash: 4.17.21
sigma: 3.0.2(graphology-types@0.24.8)
transitivePeerDependencies:
- '@types/node'
- graphology-types
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
'@hypersphere/dity@0.0.8(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)':
dependencies:
typescript: 5.8.3
vite: 7.0.5(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
'@hypersphere/dity@0.0.9(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)':
dependencies:
typescript: 5.8.3
vite: 7.0.5(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
'@hypersphere/omnibus@0.1.6': {}
'@iconify-json/simple-icons@1.2.37':
@ -4669,63 +4960,123 @@ snapshots:
'@rollup/rollup-android-arm-eabi@4.41.1':
optional: true
'@rollup/rollup-android-arm-eabi@4.45.1':
optional: true
'@rollup/rollup-android-arm64@4.41.1':
optional: true
'@rollup/rollup-android-arm64@4.45.1':
optional: true
'@rollup/rollup-darwin-arm64@4.41.1':
optional: true
'@rollup/rollup-darwin-arm64@4.45.1':
optional: true
'@rollup/rollup-darwin-x64@4.41.1':
optional: true
'@rollup/rollup-darwin-x64@4.45.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.41.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.45.1':
optional: true
'@rollup/rollup-freebsd-x64@4.41.1':
optional: true
'@rollup/rollup-freebsd-x64@4.45.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.45.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.45.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.41.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.45.1':
optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.41.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.45.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.45.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.41.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.45.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.41.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.45.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.41.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.45.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.41.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.45.1':
optional: true
'@shikijs/core@3.5.0':
dependencies:
'@shikijs/types': 3.5.0
@ -4764,6 +5115,10 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
'@sigma/node-border@3.0.0(sigma@3.0.2(graphology-types@0.24.8))':
dependencies:
sigma: 3.0.2(graphology-types@0.24.8)
'@sinclair/typebox@0.27.8': {}
'@sindresorhus/is@4.6.0': {}
@ -4826,6 +5181,8 @@ snapshots:
'@types/estree@1.0.7': {}
'@types/estree@1.0.8': {}
'@types/graceful-fs@4.1.9':
dependencies:
'@types/node': 22.15.29
@ -5191,11 +5548,11 @@ snapshots:
dependencies:
safari-14-idb-fix: 1.0.6
acorn-jsx@5.3.2(acorn@8.14.1):
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.14.1
acorn: 8.15.0
acorn@8.14.1: {}
acorn@8.15.0: {}
ag-charts-types@10.3.5: {}
@ -5474,6 +5831,8 @@ snapshots:
chownr@2.0.0: {}
chroma-js@3.1.2: {}
ci-info@3.9.0: {}
cjs-module-lexer@1.4.3: {}
@ -5729,7 +6088,7 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-scope@8.3.0:
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
@ -5738,6 +6097,8 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint-visitor-keys@4.2.1: {}
eslint@9.15.0:
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.15.0)
@ -5750,16 +6111,16 @@ snapshots:
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.7
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.1
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
espree: 10.3.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
@ -5777,11 +6138,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
espree@10.3.0:
espree@10.4.0:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 4.2.0
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
esprima@1.2.2: {}
@ -5803,6 +6164,8 @@ snapshots:
esutils@2.0.3: {}
events@3.3.0: {}
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
@ -5861,6 +6224,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.6(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -6026,6 +6393,28 @@ snapshots:
graphemer@1.4.0: {}
graphology-layout-forceatlas2@0.10.1(graphology-types@0.24.8):
dependencies:
graphology-types: 0.24.8
graphology-utils: 2.5.2(graphology-types@0.24.8)
graphology-layout@0.6.1(graphology-types@0.24.8):
dependencies:
graphology-types: 0.24.8
graphology-utils: 2.5.2(graphology-types@0.24.8)
pandemonium: 2.4.1
graphology-types@0.24.8: {}
graphology-utils@2.5.2(graphology-types@0.24.8):
dependencies:
graphology-types: 0.24.8
graphology@0.26.0(graphology-types@0.24.8):
dependencies:
events: 3.3.0
graphology-types: 0.24.8
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
@ -6124,6 +6513,8 @@ snapshots:
immutable@5.1.2: {}
immutable@5.1.3: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@ -6812,6 +7203,10 @@ snapshots:
mkdirp@1.0.4: {}
mnemonist@0.39.8:
dependencies:
obliterator: 2.0.5
moment@2.29.4: {}
mri@1.2.0: {}
@ -6881,6 +7276,8 @@ snapshots:
gauge: 4.0.4
set-blocking: 2.0.0
obliterator@2.0.5: {}
obsidian@1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.37.1):
dependencies:
'@codemirror/state': 6.5.2
@ -6974,6 +7371,10 @@ snapshots:
dependencies:
quansync: 0.2.10
pandemonium@2.4.1:
dependencies:
mnemonist: 0.39.8
papaparse@5.5.3: {}
parent-module@1.0.1:
@ -7005,6 +7406,8 @@ snapshots:
picomatch@4.0.2: {}
picomatch@4.0.3: {}
pify@4.0.1: {}
pirates@4.0.7: {}
@ -7021,6 +7424,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
preact@10.26.8: {}
prelude-ls@1.1.2: {}
@ -7158,6 +7567,32 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3
rollup@4.45.1:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.45.1
'@rollup/rollup-android-arm64': 4.45.1
'@rollup/rollup-darwin-arm64': 4.45.1
'@rollup/rollup-darwin-x64': 4.45.1
'@rollup/rollup-freebsd-arm64': 4.45.1
'@rollup/rollup-freebsd-x64': 4.45.1
'@rollup/rollup-linux-arm-gnueabihf': 4.45.1
'@rollup/rollup-linux-arm-musleabihf': 4.45.1
'@rollup/rollup-linux-arm64-gnu': 4.45.1
'@rollup/rollup-linux-arm64-musl': 4.45.1
'@rollup/rollup-linux-loongarch64-gnu': 4.45.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.45.1
'@rollup/rollup-linux-riscv64-gnu': 4.45.1
'@rollup/rollup-linux-riscv64-musl': 4.45.1
'@rollup/rollup-linux-s390x-gnu': 4.45.1
'@rollup/rollup-linux-x64-gnu': 4.45.1
'@rollup/rollup-linux-x64-musl': 4.45.1
'@rollup/rollup-win32-arm64-msvc': 4.45.1
'@rollup/rollup-win32-ia32-msvc': 4.45.1
'@rollup/rollup-win32-x64-msvc': 4.45.1
fsevents: 2.3.3
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@ -7242,10 +7677,10 @@ snapshots:
sass-embedded@1.89.0:
dependencies:
'@bufbuild/protobuf': 2.5.1
'@bufbuild/protobuf': 2.6.1
buffer-builder: 0.2.0
colorjs.io: 0.5.2
immutable: 5.1.2
immutable: 5.1.3
rxjs: 7.8.2
supports-color: 8.1.1
sync-child-process: 1.0.2
@ -7314,6 +7749,13 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
sigma@3.0.2(graphology-types@0.24.8):
dependencies:
events: 3.3.0
graphology-utils: 2.5.2(graphology-types@0.24.8)
transitivePeerDependencies:
- graphology-types
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
@ -7612,7 +8054,21 @@ snapshots:
sass: 1.89.1
sass-embedded: 1.89.0
vitepress@2.0.0-alpha.5(@algolia/client-search@5.25.0)(@types/node@22.15.29)(postcss@8.5.4)(sass-embedded@1.89.0)(sass@1.89.1)(search-insights@2.17.3)(typescript@5.8.3):
vite@7.0.5(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.45.1
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 22.15.29
fsevents: 2.3.3
sass: 1.89.1
sass-embedded: 1.89.0
vitepress@2.0.0-alpha.5(@algolia/client-search@5.25.0)(@types/node@22.15.29)(postcss@8.5.6)(sass-embedded@1.89.0)(sass@1.89.1)(search-insights@2.17.3)(typescript@5.8.3):
dependencies:
'@docsearch/css': 3.9.0
'@docsearch/js': 3.9.0(@algolia/client-search@5.25.0)(search-insights@2.17.3)
@ -7632,7 +8088,7 @@ snapshots:
vite: 6.3.5(@types/node@22.15.29)(sass-embedded@1.89.0)(sass@1.89.1)
vue: 3.5.16(typescript@5.8.3)
optionalDependencies:
postcss: 8.5.4
postcss: 8.5.6
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'

View file

@ -1,62 +0,0 @@
// This is render context which collects all the parsed elements and maintains overvability of them.
import { ModernCellParser, OnRunCallback, UnregisterCallback } from "./ModernCellParser";
class RenderContext {
private isContextActive: boolean = false;
private contextActivateCalbacks: (() => ReturnType<OnRunCallback>)[] = []
private contextDeactivateCallbacks: UnregisterCallback[] = []
constructor(private readonly cellParser: ModernCellParser) {
}
render(context: string) {
const res = this.cellParser.prepare(context)
if (typeof res === 'string' || typeof res === 'number') {
return res
}
if (res instanceof HTMLElement) {
return res
}
if (res.onRunCallback) {
// FIXME: not sure if this is the proper element, this might get recreated inside the handlebars. To check.
this.contextActivateCalbacks.push(res.onRunCallback.apply(res.element, [res.element]))
}
return res.element
}
activate() {
if (this.isContextActive) {
throw new Error('Context already active')
}
this.isContextActive = true
for (const cb of this.contextActivateCalbacks) {
const res = cb()
if (res) {
this.contextDeactivateCallbacks.push(res)
}
}
}
deactivate() {
if (!this.isContextActive) {
throw new Error('Context not active')
}
this.isContextActive = false
for (const cb of this.contextDeactivateCallbacks) {
cb()
}
this.contextDeactivateCallbacks = []
}
clear() {
try {
this.deactivate()
} catch (e) { }
this.contextActivateCalbacks = []
}
}

View file

@ -1,13 +0,0 @@
import { App } from "obsidian"
import { ModernCellParser } from "./ModernCellParser"
import { LinkParser } from "./parser/link"
import { ImageParser } from "./parser/image"
import { CheckboxParser } from "./parser/checkbox"
export const getCellParser = (app: App, create = createEl) => {
const cellParser = new ModernCellParser()
cellParser.register(new LinkParser(app, create))
cellParser.register(new ImageParser(app, create))
cellParser.register(new CheckboxParser(app, create))
return cellParser
}

View file

@ -1,144 +0,0 @@
import { OmnibusRegistrator } from "@hypersphere/omnibus";
import { App, MarkdownPostProcessorContext, MarkdownRenderChild } from "obsidian";
import { SqlSealDatabase } from "../database/database";
import { Sync } from "../datamodel/sync";
import { RendererRegistry, RenderReturn } from "../renderer/rendererRegistry";
import { transformQuery } from "../sql/sqlTransformer";
import { displayError, displayNotice } from "../utils/ui";
import SqlSealPlugin from "../main";
import { registerObservers } from "../utils/registerObservers";
import { ParserResult, parseWithDefaults, TableDefinition } from "../grammar/parser";
export class CodeblockProcessor extends MarkdownRenderChild {
registrator: OmnibusRegistrator
renderer: RenderReturn
private flags: ParserResult['flags']
private extrasEl: HTMLElement
private explainEl: HTMLElement
constructor(
private el: HTMLElement,
private source: string,
private ctx: MarkdownPostProcessorContext,
private rendererRegistry: RendererRegistry,
private db: SqlSealDatabase,
private plugin: SqlSealPlugin,
private app: App,
private sync: Sync) {
super(el)
this.registrator = this.sync.getRegistrator()
}
query: string;
async onload() {
try {
const defaults: ParserResult = {
flags: {
refresh: this.plugin.settings.enableDynamicUpdates,
explain: false
},
query: '',
renderer: { options: '', type: this.plugin.settings.defaultView.toUpperCase() },
tables: []
}
const results = parseWithDefaults(this.source, this.rendererRegistry.getViewDefinitions(), defaults, this.rendererRegistry.flags)
// const results = parseLanguage(this.source, this.ctx.sourcePath)
if (results.tables) {
await this.registerTables(results.tables)
if (!results.query) {
displayNotice(this.el, `Creating tables: ${results.tables.map(t => t.tableAlias).join(', ')}`)
return
}
}
this.flags = results.flags
let rendererEl = this.el
if (this.flags.explain) {
this.extrasEl = this.el.createDiv({ cls: 'sqlseal-extras-container' })
if (this.flags.explain) {
this.explainEl = this.extrasEl.createEl('pre', { cls: 'sqlseal-extras-explain-container' })
}
rendererEl = this.el.createDiv({ cls: 'sqlseal-renderer-container' })
}
this.renderer = this.rendererRegistry
.prepareRender(
results.renderer.type.toLowerCase(), results.renderer.options
)(rendererEl, {
cellParser: this.plugin.cellParser,
sourcePath: this.ctx.sourcePath
})
// FIXME: probably should save the one before transform and perform transform every time we execute it.
this.query = results.query
await this.render()
} catch (e) {
displayError(this.el, e.toString())
}
}
onunload() {
this.registrator.offAll()
if (this.renderer?.cleanup) {
this.renderer.cleanup()
}
}
async render() {
try {
const registeredTablesForContext = await this.sync.getTablesMappingForContext(this.ctx.sourcePath)
const res = transformQuery(this.query, registeredTablesForContext)
const transformedQuery = res.sql
if (this.flags.refresh) {
registerObservers({
bus: this.registrator,
callback: () => this.render(),
fileName: this.ctx.sourcePath,
tables: res.mappedTables
})
}
let variables = {}
const file = this.app.vault.getFileByPath(this.ctx.sourcePath)
if (file) {
const fileCache = this.app.metadataCache.getFileCache(file)
variables = {
...(fileCache?.frontmatter ?? {}),
path: file.path,
fileName: file.name,
basename: file.basename,
parent: file.parent?.path,
extension: file.extension,
}
}
if (this.flags.explain) {
// Rendering explain
const result = await this.db.explain(transformedQuery, variables)
this.explainEl.textContent = result
}
const { data, columns } = await this.db.select(transformedQuery, variables)
this.renderer.render({ data, columns, flags: this.flags, frontmatter: variables })
} catch (e) {
this.renderer.error(e.toString())
}
}
async registerTables(tables: TableDefinition[]) {
await Promise.all(tables.map((table) => this.sync.registerTable({
...table,
sourceFile: this.ctx.sourcePath
})))
}
}

View file

@ -1,125 +0,0 @@
import { EditorView, ViewPlugin, ViewUpdate, DecorationSet, Decoration, WidgetType } from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { App } from "obsidian";
import { SqlSealDatabase } from "../database/database";
import { Sync } from "../datamodel/sync";
import { SqlSealInlineHandler } from "../codeblockHandler/inline/InlineCodeHandler";
import { InlineProcessor } from "../codeblockHandler/inline/InlineProcessor";
import SqlSealPlugin from "../main";
export function createSqlSealEditorExtension(
app: App,
db: SqlSealDatabase,
plugin: SqlSealPlugin,
sync: Sync,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
inlineHandler: SqlSealInlineHandler;
constructor(view: EditorView) {
this.inlineHandler = new SqlSealInlineHandler(app, db, plugin, sync);
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView) {
const builder = new RangeSetBuilder<Decoration>();
const tree = syntaxTree(view.state);
tree.iterate({
enter: ({ type, from, to }) => {
if (type.name.includes("inline-code")) {
const text = view.state.doc.sliceString(from, to);
if (text.startsWith('S>')) {
// Check if we're currently editing this specific inline code
const isEditing = view.hasFocus &&
view.state.selection.ranges.some(range =>
range.from >= from && range.to <= to);
if (!isEditing) {
// Create a replacement decoration
builder.add(from, to, Decoration.replace({
widget: new SqlSealInlineWidget(
text,
this.inlineHandler,
view.state.doc.lineAt(from).number,
app
)
}));
}
}
}
}
});
return builder.finish();
}
},
{
decorations: (v) => v.decorations
}
);
}
class SqlSealInlineWidget extends WidgetType {
constructor(
private query: string,
private handler: SqlSealInlineHandler,
private line: number,
private app: App
) {
super();
}
eq(other: SqlSealInlineWidget): boolean {
return (
other.query === this.query &&
other.line === this.line
);
}
processor: InlineProcessor
toDOM(): HTMLElement {
const container = document.createElement('span');
container.classList.add('sqlseal-inline-result');
// Create a new context for the inline query
const ctx = {
sourcePath: this.app.workspace.getActiveFile()?.path ?? '',
addChild: () => {},
getSectionInfo: () => ({
lineStart: this.line,
lineEnd: this.line
})
};
// Show the original query on hover
container.setAttribute('aria-label', this.query);
container.classList.add('has-tooltip');
this.processor = this.handler.instantiateProcessor(this.query, container, ctx.sourcePath)
this.processor.load()
return container;
}
destroy(dom: HTMLElement): void {
if (this.processor) {
this.processor.unload()
}
}
ignoreEvent(event: Event): boolean {
// Return false to allow events to propagate (important for editing)
return false;
}
}

View file

@ -1,259 +1,34 @@
import { Menu, Plugin, TAbstractFile, TFile } from 'obsidian';
import { GridRenderer } from './renderer/GridRenderer';
import { MarkdownRenderer } from './renderer/MarkdownRenderer';
import { TableRenderer } from './renderer/TableRenderer';
import { Flag, RendererConfig, RendererRegistry } from './renderer/rendererRegistry';
import { DEFAULT_SETTINGS, SQLSealSettings, SQLSealSettingsTab } from './settings/SQLSealSettingsTab';
import { SqlSeal } from './sqlSeal';
import { SealFileSync } from './vaultSync/SealFileSync';
import { CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE, CSVView } from './view/CSVView';
import { createSqlSealEditorExtension } from './editorExtension/inlineCodeBlock';
import { ListRenderer } from './renderer/ListRenderer';
import { JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE, JsonView } from './view/JsonView';
import { registerAPI } from '@vanakat/plugin-api';
import { SQLSealRegisterApi } from './pluginApi/sqlSealApi';
import { DatabaseTable } from './database/table';
import { FilepathHasher } from './utils/hasher';
import { SQLSealViewPlugin } from './editorExtension/syntaxHighlight';
import { EditorView, ViewPlugin } from '@codemirror/view';
import { TemplateRenderer } from './renderer/TemplateRenderer';
import { ModernCellParser } from './cellParser/ModernCellParser';
import { getCellParser } from './cellParser/factory';
import { CellFunction } from './cellParser/CellFunction';
import { Plugin } from "obsidian";
import { AllCommunityModule, ModuleRegistry, provideGlobalGridOptions } from 'ag-grid-community';
import {
AllCommunityModule,
ModuleRegistry,
provideGlobalGridOptions,
} from "ag-grid-community";
import { mainModule } from "./modules/main/module";
// Register all community features
ModuleRegistry.registerModules([AllCommunityModule]);
// Mark all grids as using legacy themes
provideGlobalGridOptions({ theme: "legacy"});
provideGlobalGridOptions({ theme: "legacy" });
export default class SqlSealPlugin extends Plugin {
settings: SQLSealSettings;
fileSync: SealFileSync;
sqlSeal: SqlSeal;
rendererRegistry: RendererRegistry = new RendererRegistry();
cellParser: ModernCellParser;
container: ReturnType<(typeof mainModule)["build"]>;
async onload() {
// Preparing cell parser
this.cellParser = getCellParser(this.app, createEl)
await this.loadSettings();
const settingsTab = new SQLSealSettingsTab(this.app, this, this.settings)
this.addSettingTab(settingsTab);
settingsTab.onChange(async settings => {
this.settings = settings
// FIXME: check how to unregister the view
await this.unregisterSQLSealExtensions()
await this.registerSQLSealExtensions()
})
await this.registerViews();
await this.registerSQLSealExtensions()
await this.loadSyntaxHighlighting()
this.registerGlobalApi();
const sqlSeal = new SqlSeal(this.app, false, this.rendererRegistry, this) // FIXME: set verbose based on the env.
this.sqlSeal = sqlSeal
await this.sqlSeal.db.connect()
this.cellParser.registerDbFunctions(this.sqlSeal.db)
// REGISTERING VIEWS
this.rendererRegistry.register('sql-seal-internal-table', new TableRenderer(this.app))
this.rendererRegistry.register('sql-seal-internal-grid', new GridRenderer(this.app, this))
this.rendererRegistry.register('sql-seal-internal-markdown', new MarkdownRenderer(this.app))
this.rendererRegistry.register('sql-seal-internal-list', new ListRenderer(this.app))
this.rendererRegistry.register('sql-seal-internal-template', new TemplateRenderer(this.app))
// start syncing when files are loaded
this.app.workspace.onLayoutReady(async () => {
await this.sqlSeal.startFileSync(this)
this.registerMarkdownCodeBlockProcessor("sqlseal", sqlSeal.getHandler())
this.registerInlineCodeblocks()
})
this.registerEvent(
this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => {
this.addCSVCreatorMenuItem(menu, file);
// CONTAINER
this.container = mainModule
.resolve({
"obsidian.app": this.app,
"obsidian.plugin": this,
"obsidian.vault": this.app.vault,
})
);
.build();
const init = await this.container.get("init");
init();
}
private registerInlineCodeblocks() {
// Extension for Live Preview
const editorExtension = createSqlSealEditorExtension(
this.app,
this.sqlSeal.db,
this,
this.sqlSeal.sync,
);
this.registerEditorExtension(editorExtension);
// Extension for Read mode
this.registerMarkdownPostProcessor((el, ctx) => {
const inlineCodeBlocks = el.querySelectorAll('code');
inlineCodeBlocks.forEach((node: HTMLSpanElement) => {
const text = node.innerText;
if (text.startsWith('S>')) {
const container = createEl('span', { cls: 'sqlseal-inline-result' });
container.setAttribute('aria-label', text.slice(3));
container.classList.add('has-tooltip');
node.replaceWith(container);
this.sqlSeal.getInlineHandler()(text, container, ctx);
}
});
});
}
private addCSVCreatorMenuItem(menu: Menu, file: TAbstractFile) {
menu.addItem((item) => {
item
.setTitle('New CSV file')
.setIcon('table')
.onClick(async () => {
try {
await this.createNewCSVFile(file)
} catch (error) {
console.error('Failed to create CSV file:', error)
}
});
});
}
private loadSyntaxHighlighting() {
if (!this.settings.enableSyntaxHighlighting) {
return
}
this.registerEditorExtension([
ViewPlugin.define(
(view: EditorView) => new SQLSealViewPlugin(view, this.app, this.rendererRegistry),
{ decorations: v => v.decorations }
)
]);
}
private async createNewCSVFile(file: TAbstractFile) {
const targetDir = file instanceof TFile ? file.parent : file
const basePath = targetDir!.path
const csvTemplate = 'Id,Name\n1,Test Data'
const defaultName = 'Untitled CSV'
let fileName = defaultName
let filePath = `${basePath}/${fileName}.csv`;
let counter = 1;
while (await this.app.vault.adapter.exists(filePath)) {
fileName = `${defaultName} ${counter}`;
filePath = `${basePath}/${fileName}.csv`;
counter++;
}
try {
const newFile = await this.app.vault.create(filePath, csvTemplate);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(newFile);
const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]?.view;
if (fileExplorer) {
await (fileExplorer as any).revealInFolder(newFile);
}
} catch (error) {
console.error('Error creating CSV file:', error);
throw error;
}
}
async registerSQLSealExtensions() {
if (this.settings.enableViewer) {
this.registerExtensions(CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE);
}
if (this.settings.enableJSONViewer) {
this.registerExtensions(JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE)
}
}
async unregisterSQLSealExtensions() {
this.app.workspace.detachLeavesOfType(CSV_VIEW_TYPE);
this.app.workspace.detachLeavesOfType(JSON_VIEW_TYPE);
(this.app as any).viewRegistry.unregisterExtensions([
...CSV_VIEW_EXTENSIONS,
...JSON_VIEW_EXTENSIONS
])
}
async registerViews() {
this.registerView(
CSV_VIEW_TYPE,
(leaf) => new CSVView(leaf, this.settings.enableEditing, this.cellParser)
);
this.registerView(
JSON_VIEW_TYPE,
(leaf) => new JsonView(leaf)
)
}
registerSQLSealView(name: string, config: RendererConfig) {
this.rendererRegistry.register(name, config)
}
unregisterSQLSealView(name: string) {
this.rendererRegistry.unregister(name)
}
registerSQLSealFunction(fn: CellFunction) {
this.cellParser.register(fn)
}
unregisterSQLSealFunction(name: string) {
this.cellParser.unregister(name)
}
registerSQLSealFlag(flag: Flag) {
this.rendererRegistry.registerFlag(flag)
}
unregisterSQLSealFlag(name: string) {
this.rendererRegistry.unregisterFlag(name)
}
async registerTable<const columns extends string[]>(plugin: Plugin, name: string, columns: columns) {
const hash = await FilepathHasher.sha256(`${plugin.manifest.name}`)
const tableName = `external_table_${hash}_name`
const newTable = new DatabaseTable(this.sqlSeal.db, tableName, columns)
await newTable.connect()
// FIXME: probably register this table in some type of map for the future use?
return newTable
}
registerGlobalApi() {
registerAPI('sqlseal', new SQLSealRegisterApi(this), this)
}
onunload() {
this.sqlSeal.db.disconect();
this.unregisterSQLSealExtensions();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
onunload() {}
}

55
src/modules/api/init.ts Normal file
View file

@ -0,0 +1,55 @@
import { Plugin } from "obsidian";
import {
PluginRegister,
SQLSealApi,
SQLSealRegisterApi,
} from "./pluginApi/sqlSealApi";
import { makeInjector } from "@hypersphere/dity";
import { ApiModule } from "./module";
import { RendererRegistry } from "../editor/renderer/rendererRegistry";
import { SqlSealDatabase } from "../database/database";
import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser";
const SQLSEAL_API_KEY = "___sqlSeal";
const SQLSEAL_QUEUED_PLUGINS = "___sqlSeal_queue";
@(makeInjector<ApiModule, "factory">()([
"plugin",
"cellParser",
"rendererRegistry",
"db",
]))
export class ApiInit {
make(
plugin: Plugin,
cellParser: ModernCellParser,
rendererRegistry: RendererRegistry,
db: SqlSealDatabase,
) {
return () => {
const api = new SQLSealRegisterApi(
plugin,
cellParser,
rendererRegistry,
db,
);
(window as any)[SQLSEAL_API_KEY] = api;
plugin.register(() => {
delete (window as any)[SQLSEAL_API_KEY];
});
const queuedPlugins = (window as any)[SQLSEAL_QUEUED_PLUGINS] as
| PluginRegister[]
| undefined;
if (!queuedPlugins) {
return;
}
queuedPlugins.forEach((pl) => {
api.registerForPluginNew(pl);
});
(window as any)[SQLSEAL_QUEUED_PLUGINS] = [];
};
}
}

21
src/modules/api/module.ts Normal file
View file

@ -0,0 +1,21 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { ApiInit } from "./init";
import { Plugin } from "obsidian";
import { SqlSealDatabase } from "../database/database";
import { RendererRegistry } from "../editor/renderer/rendererRegistry";
import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser";
export const apiModule = buildContainer(c => c
.register({
init: asFactory(ApiInit)
})
.exports('init')
.externals<{
plugin: Plugin,
cellParser: ModernCellParser,
db: SqlSealDatabase,
rendererRegistry: RendererRegistry
}>()
)
export type ApiModule = typeof apiModule

View file

@ -0,0 +1,134 @@
import { Plugin } from "obsidian"
import { version } from '../../../../package.json'
import { RendererConfig, RendererRegistry } from "../../editor/renderer/rendererRegistry";
import { FilepathHasher } from "../../../utils/hasher";
import { DatabaseTable } from "./table";
import { SqlSealDatabase } from "../../database/database";
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";
import { CellFunction } from "../../syntaxHighlight/cellParser/CellFunction";
export type PluginRegister = {
plugin: Plugin,
run: (api: SQLSealApi) => void
}
const API_VERSION = 4;
export class SQLSealRegisterApi {
registeredApis: Array<SQLSealApi> = []
constructor(
sqlSealPlugin: Plugin,
private readonly cellParser: ModernCellParser,
private readonly rendererRegistry: RendererRegistry,
private readonly db: SqlSealDatabase
) {
sqlSealPlugin.register(() => {
this.registeredApis.forEach(p => {
p.unregister()
})
this.registeredApis = []
})
}
get sqlSealVersion() {
return version
}
get apiVersion() {
return API_VERSION
}
registerForPluginNew(reg: PluginRegister) {
const api = new SQLSealApi(reg.plugin, this.cellParser, this.rendererRegistry, this.db)
this.registeredApis.push(api)
reg.run(api)
// If plugin gets unregistered, we need to handle it.
reg.plugin.register(() => {
api.unregister()
this.registeredApis = this.registeredApis.filter(a => a !== api)
})
return api
}
}
interface RegisteredView {
name: string;
viewClass: any;
}
// TODO: use the type from registrator
export interface RegisteredFlag {
name: string,
key: string
}
export class SQLSealApi {
private views: Array<RegisteredView> = []
private functions: Array<CellFunction> = []
private flags: Array<RegisteredFlag> = []
constructor(
private readonly plugin: Plugin,
private readonly cellParser: ModernCellParser,
private readonly rendererRegistry: RendererRegistry,
private readonly db: SqlSealDatabase
) {
plugin.register(() => {
this.unregister()
})
}
get sqlSealVersion() {
return version
}
get apiVersion() {
return API_VERSION
}
registerView(name: string, viewClass: RendererConfig) {
this.views.push({
name,
viewClass
})
this.rendererRegistry.register(name, viewClass)
}
registerCustomFunction(fn: CellFunction) {
this.functions.push(fn)
this.cellParser.register(fn)
}
async registerTable<const columns extends string[]>(name: string, columns: columns) {
const hash = await FilepathHasher.sha256(`${this.plugin.manifest.name}`)
const tableName = `external_table_${hash}_name`
const newTable = new DatabaseTable(this.db, tableName, columns)
await newTable.connect()
// FIXME: probably register this table in some type of map for the future use?
return newTable
}
registerFlag(flag: RegisteredFlag) {
this.flags.push(flag)
this.rendererRegistry.registerFlag(flag)
}
unregister() {
for(const view of this.views) {
this.rendererRegistry.unregister(view.name)
}
this.views = []
for(const fn of this.functions) {
this.cellParser.unregister(fn.name)
}
for(const flag of this.flags) {
this.rendererRegistry.unregisterFlag(flag.name)
}
}
}

View file

@ -1,4 +1,4 @@
import { SqlSealDatabase } from "./database";
import { SqlSealDatabase } from "../../database/database"
type Item<Columns extends string[]> = Record<Columns[number], string | number | undefined>
@ -14,11 +14,17 @@ export class DatabaseTable<const Columns extends string[]> {
async getAll() {
const data = await this.db.select(`SELECT * FROM ${this.tableName}`, {})
if (!data) {
return []
}
return data.data as Item<Columns>[]
}
async getByKey(key: Columns[number], value: string | number) {
const data = await this.db.select(`SELECT * FROM ${this.tableName} WHERE ${key}=@value`, { '@value': value })
if (!data) {
return []
}
return data.data as Item<Columns>[]
}

View file

@ -0,0 +1,68 @@
import { makeInjector } from "@hypersphere/dity"
import { ContextMenuModule } from "./module"
import { App, Menu, Plugin, TAbstractFile, TFile, TFolder } from "obsidian"
@(makeInjector<ContextMenuModule, 'factory'>()([
'plugin', 'app'
]))
export class ContextMenuInit {
make(plugin: Plugin, app: App) {
const createNewCSVFile = async (file: TAbstractFile) => {
const targetDir = file instanceof TFile ? file.parent : file
const basePath = targetDir!.path
const csvTemplate = 'Id,Name\n1,Test Data'
const defaultName = 'Untitled CSV'
let fileName = defaultName
let filePath = `${basePath}/${fileName}.csv`;
let counter = 1;
while (await app.vault.adapter.exists(filePath)) {
fileName = `${defaultName} ${counter}`;
filePath = `${basePath}/${fileName}.csv`;
counter++;
}
try {
const newFile = await app.vault.create(filePath, csvTemplate);
const leaf = app.workspace.getLeaf(false);
await leaf.openFile(newFile);
const fileExplorer = app.workspace.getLeavesOfType('file-explorer')[0]?.view;
if (fileExplorer) {
await(fileExplorer as any).revealInFolder(newFile);
}
} catch (error) {
console.error('Error creating CSV file:', error);
throw error;
}
}
const addCSVCreatorMenuItem = (menu: Menu, file: TAbstractFile) => {
if (!(file instanceof TFolder)) {
return
}
menu.addItem((item) => {
item
.setTitle('New CSV file')
.setSection('action-primary')
.setIcon('table')
.onClick(async () => {
try {
await createNewCSVFile(file)
} catch (error) {
console.error('Failed to create CSV file:', error)
}
});
});
}
return () => {
plugin.registerEvent(
app.workspace.on('file-menu', addCSVCreatorMenuItem)
);
}
}
}

View file

@ -0,0 +1,16 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { ContextMenuInit } from "./init";
import { App, Plugin } from "obsidian";
export const contextMenu = buildContainer(c =>
c.register({
init: asFactory(ContextMenuInit)
})
.externals<{
app: App,
plugin: Plugin
}>()
.exports('init')
)
export type ContextMenuModule = typeof contextMenu

View file

@ -3,19 +3,19 @@ import * as Comlink from 'comlink'
// @ts-ignore
import workerCode from 'virtual:worker-code'
import { WorkerDatabase } from "./worker/database";
import { sanitise } from "../utils/sanitiseColumn";
import { ColumnDefinition } from "../utils/types";
import { ColumnDefinition } from "../../utils/types";
import { sanitise } from "../../utils/sanitiseColumn";
export class SqlSealDatabase {
db: Comlink.Remote<WorkerDatabase>
db?: Comlink.Remote<WorkerDatabase>
private isConnected = false
private connectingPromise: Promise<void>;
constructor(private readonly app: App, private readonly verbose = false) {
private connectingPromise?: Promise<void>;
constructor(private readonly app: App) {
}
registerCustomFunction(name: string, argsCount = 1) {
return this.db.registerCustomFunction(name, argsCount)
return this.db?.registerCustomFunction(name, argsCount)
}
async connect() {
@ -56,62 +56,62 @@ export class SqlSealDatabase {
if (!this.isConnected) {
return
}
this.db.disconnect()
this.db?.disconnect()
this.isConnected = false
}
async recreateDatabase() {
await this.db.recreateDatabase()
await this.db?.recreateDatabase()
}
async updateData(name: string, data: Array<Record<string, unknown>>, key: string = 'id') {
return this.db.updateData(name, data, key)
return this.db?.updateData(name, data, key)
}
async deleteData(name: string, data: Array<Record<string, unknown>>, key: string = 'id') {
return this.db.deleteData(name, data, key)
return this.db?.deleteData(name, data, key)
}
async insertData(name: string, inData: Array<Record<string, unknown>>) {
return this.db.insertData(name, inData)
return this.db?.insertData(name, inData)
}
async dropTable(name: string) {
return this.db.dropTable(name)
return this.db?.dropTable(name)
}
async createTableNoTypes(name: string, columns: string[], noDrop?: boolean) {
await this.db.createTableNoTypes(name, columns, noDrop)
await this.db?.createTableNoTypes(name, columns, noDrop)
}
async createTable(name: string, columns: ColumnDefinition[], noDrop?: boolean) {
this.db.createTable(name, columns, noDrop)
this.db?.createTable(name, columns, noDrop)
}
async createIndex(indexName: string, tableName: string, columns: string[]) {
await this.db.createIndex(indexName, tableName, columns)
await this.db?.createIndex(indexName, tableName, columns)
}
async getColumns(tableName: string) {
return this.db.getColumns(tableName)
return this.db?.getColumns(tableName)
}
async addColumns(tableName: string, newColumns: string[]) {
return this.db.addColumns(tableName, newColumns)
return this.db?.addColumns(tableName, newColumns)
}
async select(statement: string, frontmatter: Record<string, unknown>) {
const result = await this.db.select(statement, frontmatter)
const result = await this.db?.select(statement, frontmatter)
return result
}
async explain(statement: string, frontmatter: Record<string, unknown>) {
const explainResults = await this.db.explainQuery(statement, frontmatter)
const explainResults = await this.db?.explainQuery(statement, frontmatter)
let strResult = '';
const map = new Map<number, number>()
const INDENT_INCREASE = 4
map.set(0, -INDENT_INCREASE)
for (const result of explainResults) {
for (const result of explainResults || []) {
const parent = parseInt((result.parent as unknown as string) ?? '0', 10)
const indent = map.get(parent)! + INDENT_INCREASE
for (let i=0;i<indent;i++) {

View file

@ -0,0 +1,16 @@
import { App } from "obsidian";
import { SqlSealDatabase } from "./database";
import { makeInjector } from "@hypersphere/dity";
import { DbModel } from "./module";
@(makeInjector<DbModel, 'factory'>()([
'app'
]))
export class DatabaseFactory {
async make(app: App) {
const db = new SqlSealDatabase(app)
await db.connect()
return db
}
}

View file

@ -0,0 +1,13 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { App } from "obsidian";
import { DatabaseFactory } from "./factory";
export const db = buildContainer(c => c
.register({
db: asFactory(DatabaseFactory)
})
.externals<{ app: App }>()
.exports('db')
)
export type DbModel = typeof db

View file

@ -2,15 +2,15 @@ import * as Comlink from "comlink"
// @ts-ignore
import initSqlJs from '@jlongster/sql.js';
// @ts-ignore
import wasmBinary from '../../../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm'
import wasmBinary from '../../../../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm'
// @ts-ignore
import { SQLiteFS } from 'absurd-sql';
// @ts-ignore
import IndexedDBBackend from '../../../node_modules/absurd-sql/dist/indexeddb-backend.js';
import IndexedDBBackend from '../../../../node_modules/absurd-sql/dist/indexeddb-backend.js';
import type { BindParams, Database, Statement } from "sql.js";
import { sanitise } from "../../utils/sanitiseColumn";
import { uniq, uniqBy } from "lodash";
import { ColumnDefinition } from "../../utils/types";
import { ColumnDefinition } from "../../../utils/types";
import { sanitise } from "../../../utils/sanitiseColumn";
function toObjectArray(stmt: Statement) {

View file

@ -0,0 +1,43 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { DityGraph } from '@hypersphere/dity-graph'
import { mainModule } from "../main/module";
import { settingsModule } from "../settings/module";
export const VIEW_TYPE_EXAMPLE = "example-view";
export class DityGraphView extends ItemView {
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType() {
return VIEW_TYPE_EXAMPLE;
}
getDisplayText() {
return "Example sidebar";
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
// container.createEl("h4", { text: "Example Sidebar" });
// container.createEl("p", { text: "Your sidebar content goes here" });
const el = container.createEl('div')
el.style = 'width: 100%; height: 100%'
const module = settingsModule.resolve({ plugin: { } as any, app: {} as any, cellParser: { } as any})
// .resolve({
// 'obsidian.app': {} as any,
// 'obsidian.plugin': {} as any,
// 'obsidian.vault': {} as any,
// })
const dityGraph = new DityGraph(module as any, el)
dityGraph.render()
}
async onClose() {
// Clean up if needed
}
}

View file

@ -0,0 +1,39 @@
import { makeInjector } from "@hypersphere/dity";
import { DebugModule } from "./module";
import { Plugin, WorkspaceLeaf } from "obsidian";
import { DityGraphView, VIEW_TYPE_EXAMPLE } from "./DityGraphView";
@(makeInjector<DebugModule, 'factory'>()(
['plugin']
))
export class DityGraph {
make(plugin: Plugin) {
const activateView = async () => {
const { workspace } = plugin.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
if (!leaf) { return }
await leaf.setViewState({ type: VIEW_TYPE_EXAMPLE, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
workspace.revealLeaf(leaf);
}
return () => {
plugin.registerView(VIEW_TYPE_EXAMPLE, leaf => new DityGraphView(leaf))
plugin.addRibbonIcon('dice', 'Dity Graph', activateView)
}
}
}

View file

@ -0,0 +1,16 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { DityGraph } from "./dityGraph";
import { Plugin } from "obsidian";
export const debugModule = buildContainer(c =>
c
.register({
init: asFactory(DityGraph)
})
.exports('init')
.externals<{
plugin: Plugin
}>()
)
export type DebugModule = typeof debugModule

View file

@ -0,0 +1,210 @@
import { OmnibusRegistrator } from "@hypersphere/omnibus";
import {
App,
MarkdownPostProcessorContext,
MarkdownRenderChild,
} from "obsidian";
import { Sync } from "../../sync/sync/sync";
import { RendererRegistry, RenderReturn } from "../renderer/rendererRegistry";
import { ParserResult, parseWithDefaults, TableDefinition } from "../parser";
import { SqlSealDatabase } from "../../database/database";
import { displayError, displayNotice } from "../../../utils/ui";
import { transformQuery } from "../sql/sqlTransformer";
import { registerObservers } from "../../../utils/registerObservers";
import { Settings } from "../../settings/Settings";
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";
export class CodeblockProcessor extends MarkdownRenderChild {
registrator: OmnibusRegistrator;
renderer: RenderReturn;
private flags: ParserResult["flags"];
private extrasEl: HTMLElement;
private explainEl: HTMLElement;
constructor(
private el: HTMLElement,
private source: string,
private ctx: MarkdownPostProcessorContext,
private rendererRegistry: RendererRegistry,
private db: SqlSealDatabase,
private cellParser: ModernCellParser,
private settings: Settings,
private app: App,
private sync: Sync,
) {
super(el);
this.registrator = this.sync.getRegistrator();
}
query: string;
async onload() {
try {
const defaults: ParserResult = {
flags: {
refresh: this.settings.get("enableDynamicUpdates"),
explain: false,
},
query: "",
renderer: {
options: "",
type: this.settings.get("defaultView").toUpperCase(),
},
tables: [],
};
const results = parseWithDefaults(
this.source,
this.rendererRegistry.getViewDefinitions(),
defaults,
this.rendererRegistry.flags,
);
if (results.tables) {
await this.registerTables(results.tables);
if (!results.query) {
displayNotice(
this.el,
`Creating tables: ${results.tables.map((t) => t.tableAlias).join(", ")}`,
);
return;
}
}
this.flags = results.flags;
let rendererEl = this.el;
if (this.flags.explain) {
this.extrasEl = this.el.createDiv({ cls: "sqlseal-extras-container" });
if (this.flags.explain) {
this.explainEl = this.extrasEl.createEl("pre", {
cls: "sqlseal-extras-explain-container",
});
}
rendererEl = this.el.createDiv({ cls: "sqlseal-renderer-container" });
}
// IF WE'RE ON CANVAS, LETS ADD BACKGRUND
if (this.isOnCanvas) {
rendererEl.classList.add('sqlseal-renderer-on-canvas')
}
this.renderer = this.rendererRegistry.prepareRender(
results.renderer.type.toLowerCase(),
results.renderer.options,
)(rendererEl, {
cellParser: this.cellParser,
sourcePath: this.sourceKey,
});
// FIXME: probably should save the one before transform and perform transform every time we execute it.
this.query = results.query;
await this.render();
} catch (e) {
displayError(this.el, e.toString());
}
}
onunload() {
this.registrator.offAll();
if (this.renderer?.cleanup) {
this.renderer.cleanup();
}
}
async render() {
try {
const registeredTablesForContext =
await this.sync.getTablesMappingForContext(this.sourceKey);
const res = transformQuery(this.query, registeredTablesForContext);
const transformedQuery = res.sql;
if (this.flags.refresh) {
registerObservers({
bus: this.registrator,
callback: () => this.render(),
fileName: this.sourceKey,
tables: res.mappedTables,
});
}
let variables = {};
const file = this.app.vault.getFileByPath(this.sourceKey);
if (file) {
const fileCache = this.app.metadataCache.getFileCache(file);
variables = {
...(fileCache?.frontmatter ?? {}),
path: file.path,
fileName: file.name,
basename: file.basename,
parent: file.parent?.path,
extension: file.extension,
};
}
if (this.flags.explain) {
// Rendering explain
const result = await this.db.explain(transformedQuery, variables);
this.explainEl.textContent = result;
}
const { data, columns } = (await this.db.select(
transformedQuery,
variables,
))!; // FIXME: check this
this.renderer.render({
data,
columns,
flags: this.flags,
frontmatter: variables,
});
} catch (e) {
this.renderer.error(e.toString());
}
}
private cachedName: string | null = null
get isOnCanvas() {
return !this.ctx.sourcePath
}
get canvasName() {
// This is hack to detect what name has current canvas.
// It's not fool proof but should work for majority of use-cases for now.
// We need to find proper way of getting it or ask Obsidian devs to expose some info.
if (this.cachedName !== null) {
return this.cachedName
}
const canvasViews = this.app.workspace.getLeavesOfType("canvas");
for (const leaf of canvasViews) {
const nodes = JSON.parse(leaf.view.data).nodes
const node = nodes.filter(n => n.text).find(n => n.text.contains(this.source))
if (node) {
this.cachedName = leaf.view.file.path
return this.cachedName as string
}
}
this.cachedName = ''
return ''
}
get sourceKey() {
return this.ctx.sourcePath.trim() ? this.ctx.sourcePath.trim() : this.canvasName;
}
async registerTables(tables: TableDefinition[]) {
await Promise.all(
tables.map((table) =>
this.sync.registerTable({
...table,
sourceFile: this.sourceKey,
}),
),
);
}
}

View file

@ -1,18 +1,24 @@
import { App, MarkdownPostProcessorContext } from "obsidian"
import { SqlSealDatabase } from "../database/database"
import { RendererRegistry } from "../renderer/rendererRegistry"
import { Sync } from "../datamodel/sync"
import { CodeblockProcessor } from "./CodeblockProcessor"
import SqlSealPlugin from "../main"
import { makeInjector } from "@hypersphere/dity"
import { EditorModule } from "../module"
import { SqlSealDatabase } from "../../database/database"
import { Sync } from "../../sync/sync/sync"
import { Settings } from "../../settings/Settings"
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser"
@(makeInjector<EditorModule>()(
['app', 'db', 'cellParser', 'sync', 'rendererRegistry', 'settings']
))
export class SqlSealCodeblockHandler {
constructor(
private readonly app: App,
private readonly db: SqlSealDatabase,
private readonly plugin: SqlSealPlugin,
private readonly cellParser: ModernCellParser,
private sync: Sync,
private rendererRegistry: RendererRegistry
private rendererRegistry: RendererRegistry,
private settings: Settings
) {
}
@ -24,7 +30,8 @@ export class SqlSealCodeblockHandler {
ctx,
this.rendererRegistry,
this.db,
this.plugin,
this.cellParser,
this.settings,
this.app,
this.sync
)

View file

@ -1,14 +1,20 @@
import { App, MarkdownPostProcessorContext } from "obsidian";
import { App, MarkdownPostProcessorContext, Plugin } from "obsidian";
import { InlineProcessor } from "./InlineProcessor";
import { SqlSealDatabase } from "../../database/database";
import { Sync } from "../../datamodel/sync";
import SqlSealPlugin from "../../main";
import { makeInjector } from "@hypersphere/dity";
import { EditorModule } from "../../module";
import { SqlSealDatabase } from "../../../database/database";
import { Sync } from "../../../sync/sync/sync";
import { Settings } from "../../../settings/Settings";
@(makeInjector<EditorModule>()(
['app', 'db', 'settings', 'sync', 'rendererRegistry']
))
export class SqlSealInlineHandler {
constructor(
private readonly app: App,
private readonly db: SqlSealDatabase,
private readonly plugin: SqlSealPlugin,
private readonly settings: Settings,
private sync: Sync
) { }
@ -25,7 +31,7 @@ export class SqlSealInlineHandler {
query,
sourcePath,
this.db,
this.plugin,
this.settings,
this.app,
this.sync
);

View file

@ -1,11 +1,11 @@
import { OmnibusRegistrator } from "@hypersphere/omnibus";
import { App, MarkdownRenderChild } from "obsidian";
import { SqlSealDatabase } from "../../database/database";
import { Sync } from "../../datamodel/sync";
import { transformQuery } from "../../sql/sqlTransformer";
import { registerObservers } from "../../utils/registerObservers";
import { displayError } from "../../utils/ui";
import SqlSealPlugin from "../../main";
import { SqlSealDatabase } from "../../../database/database";
import { Sync } from "../../../sync/sync/sync";
import { registerObservers } from "../../../../utils/registerObservers";
import { displayError } from "../../../../utils/ui";
import { Settings } from "../../../settings/Settings";
export class InlineProcessor extends MarkdownRenderChild {
private registrator: OmnibusRegistrator;
@ -15,7 +15,7 @@ export class InlineProcessor extends MarkdownRenderChild {
private query: string,
private sourcePath: string,
private db: SqlSealDatabase,
private plugin: SqlSealPlugin,
private settings: Settings,
private app: App,
private sync: Sync
) {
@ -40,7 +40,8 @@ export class InlineProcessor extends MarkdownRenderChild {
const registeredTablesForContext = await this.sync.getTablesMappingForContext(this.sourcePath);
const transformedQuery = transformQuery(this.query, registeredTablesForContext);
if (this.plugin.settings.enableDynamicUpdates) {
// FIXME: settings here instead of plugin
if (this.settings.get('enableDynamicUpdates')) {
registerObservers({
bus: this.registrator,
tables: transformedQuery.mappedTables,
@ -66,10 +67,10 @@ export class InlineProcessor extends MarkdownRenderChild {
extension: file.extension,
}
const { data, columns } = await this.db.select(
const { data, columns } = (await this.db.select(
transformedQuery.sql,
variables
);
))!; // FIXME: better code here.
this.el.empty()
let value = data[0][columns[0]] ?? ''

View file

@ -0,0 +1,85 @@
import { makeInjector } from "@hypersphere/dity";
import { App, Plugin } from "obsidian";
import { SqlSealDatabase } from "../database/database";
import { Sync } from "../sync/sync/sync";
import { EditorModule } from "./module";
import { RendererRegistry } from "./renderer/rendererRegistry";
import { TableRenderer } from "./renderer/TableRenderer";
import { GridRenderer } from "./renderer/GridRenderer";
import { ListRenderer } from "./renderer/ListRenderer";
import { TemplateRenderer } from "./renderer/TemplateRenderer";
import { MarkdownRenderer } from "./renderer/MarkdownRenderer";
import { Settings } from "../settings/Settings";
import { SqlSealInlineHandler } from "./codeblockHandler/inline/InlineCodeHandler";
import { SqlSealCodeblockHandler } from "./codeblockHandler/SqlSealCodeblockHandler";
import { createSqlSealEditorExtension } from "../syntaxHighlight/editorExtension/inlineCodeBlock";
@(makeInjector<EditorModule, 'factory'>()([
'app', 'db', 'plugin', 'sync', 'inlineHandler', 'blockHandler', 'rendererRegistry', 'settings'
]))
export class EditorInit {
make(
app: App,
db: SqlSealDatabase,
plugin: Plugin,
sync: Sync,
inlineHandler: SqlSealInlineHandler,
blockHandler: SqlSealCodeblockHandler,
rendererRegistry: RendererRegistry,
settings: Settings
) {
const registerInlineCodeblocks = () => {
// Extension for Live Preview
const editorExtension = createSqlSealEditorExtension(
app,
db,
settings,
sync,
);
plugin.registerEditorExtension(editorExtension);
// Extension for Read mode
plugin.registerMarkdownPostProcessor((el, ctx) => {
const inlineCodeBlocks = el.querySelectorAll('code');
inlineCodeBlocks.forEach((node: HTMLSpanElement) => {
const text = node.innerText;
if (text.startsWith('S>')) {
const container = createEl('span', { cls: 'sqlseal-inline-result' });
container.setAttribute('aria-label', text.slice(3));
container.classList.add('has-tooltip');
node.replaceWith(container);
inlineHandler.getHandler()(text, container, ctx);
}
});
});
}
const registerBlockCodeblock = () => {
plugin.registerMarkdownCodeBlockProcessor('sqlseal', blockHandler.getHandler())
}
const registerViews = () => {
rendererRegistry.register('sql-seal-internal-table', new TableRenderer(app))
rendererRegistry.register('sql-seal-internal-grid', new GridRenderer(settings, plugin, app))
rendererRegistry.register('sql-seal-internal-markdown', new MarkdownRenderer(app))
rendererRegistry.register('sql-seal-internal-list', new ListRenderer(app))
rendererRegistry.register('sql-seal-internal-template', new TemplateRenderer(app))
}
return () => {
registerViews()
app.workspace.onLayoutReady(async () => {
registerInlineCodeblocks()
registerBlockCodeblock()
})
// FIXME: block
}
}
}

View file

@ -0,0 +1,30 @@
import { asClass, asFactory, buildContainer } from "@hypersphere/dity"
import { App, Plugin } from "obsidian"
import { SqlSealDatabase } from "../database/database"
import { Sync } from "../sync/sync/sync"
import { RendererRegistry } from "./renderer/rendererRegistry"
import { EditorInit } from "./init"
import { SqlSealCodeblockHandler } from "./codeblockHandler/SqlSealCodeblockHandler"
import { SqlSealInlineHandler } from "./codeblockHandler/inline/InlineCodeHandler"
import { Settings } from "../settings/Settings"
import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"
export const editor = buildContainer(c => c
.register({
blockHandler: asClass(SqlSealCodeblockHandler),
inlineHandler: asClass(SqlSealInlineHandler),
rendererRegistry: asClass(RendererRegistry),
init: asFactory(EditorInit)
})
.externals<{
app: App,
db: SqlSealDatabase,
plugin: Plugin,
sync: Sync,
cellParser: ModernCellParser,
settings: Settings
}>()
.exports('rendererRegistry', 'init')
)
export type EditorModule = typeof editor

View file

@ -1,5 +1,5 @@
import * as ohm from 'ohm-js';
import { Flag } from '../renderer/rendererRegistry';
import { Flag } from './renderer/rendererRegistry';
export interface ViewDefinition {
name: string,

View file

@ -1,12 +1,12 @@
import { createGrid, GridApi, GridOptions, themeQuartz } from "ag-grid-community";
import { merge } from "lodash";
import { App } from "obsidian";
import { App, Plugin } from "obsidian";
import { RendererConfig, RendererContext } from "./rendererRegistry";
import { parse } from 'json5'
import { ViewDefinition } from "../grammar/parser";
import SqlSealPlugin from "../main";
import { ModernCellParser } from "../cellParser/ModernCellParser";
import { parse } from 'json5';
import { EventRef } from "obsidian";
import { Settings } from "../../settings/Settings";
import { ViewDefinition } from "../parser";
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";
interface DataParam {
data: Record<string, unknown>[],
@ -35,7 +35,8 @@ export class GridRendererCommunicator {
constructor(
private el: HTMLElement,
private config: Partial<GridOptions>,
private plugin: SqlSealPlugin | null,
private plugin: Plugin | null,
private settings: Settings,
private app: App,
private cellParser?: ModernCellParser
) {
@ -131,7 +132,7 @@ export class GridRendererCommunicator {
columnDefs: [],
domLayout: 'autoHeight',
enableCellTextSelection: true,
paginationPageSize: this.plugin? this.plugin.settings.gridItemsPerPage : undefined,
paginationPageSize: this.settings.get('gridItemsPerPage') ?? 10
// ensureDomOrder: true
}, this.config)
this._gridApi = createGrid(
@ -165,7 +166,7 @@ export class GridRendererCommunicator {
}
export class GridRenderer implements RendererConfig {
constructor(private app: App, private readonly plugin: SqlSealPlugin | null) { }
constructor(private settings: Settings, private readonly plugin: Plugin | null, private readonly app: App) { }
get viewDefinition(): ViewDefinition {
return {
name: this.rendererKey,
@ -188,7 +189,7 @@ export class GridRenderer implements RendererConfig {
render(config: Partial<GridOptions>, el: HTMLElement, { cellParser }: RendererContext) {
const communicator = new GridRendererCommunicator(el, config, this.plugin, this.app, cellParser)
const communicator = new GridRendererCommunicator(el, config, this.plugin, this.settings, this.app, cellParser)
return {
render: (data: DataParam) => {
communicator.setData(data.columns ?? [], data.data)

View file

@ -1,8 +1,8 @@
// This is renderer for a very basic List view.
import { App } from "obsidian";
import { RendererConfig, RendererContext } from "../renderer/rendererRegistry";
import { displayError } from "../utils/ui";
import { ViewDefinition } from "../grammar/parser";
import { RendererConfig, RendererContext } from "./rendererRegistry";
import { displayError } from "../../../utils/ui";
import { ViewDefinition } from "../parser";
interface ListRendererConfig {
classNames: string[]

View file

@ -1,10 +1,10 @@
// This is renderer for a very basic Table view.
import { getMarkdownTable } from "markdown-table-ts";
import { App } from "obsidian";
import { RendererConfig, RendererContext } from "../renderer/rendererRegistry";
import { displayError } from "../utils/ui";
import { ViewDefinition } from "../grammar/parser";
import { ParseResults } from "../cellParser/parseResults";
import { RendererConfig, RendererContext } from "./rendererRegistry";
import { displayError } from "../../../utils/ui";
import { ViewDefinition } from "../parser";
import { ParseResults } from "../../syntaxHighlight/cellParser/parseResults";
const mapDataFromHeaders = (columns: string[], data: Record<string, any>[]) => {
return data.map(d => columns.map(c => String(d[c])))

View file

@ -1,8 +1,8 @@
// This is renderer for a very basic Table view.
import { App } from "obsidian";
import { RendererConfig, RendererContext } from "../renderer/rendererRegistry";
import { displayError } from "../utils/ui";
import { ViewDefinition } from "../grammar/parser";
import { RendererConfig, RendererContext } from "./rendererRegistry";
import { displayError } from "../../../utils/ui";
import { ViewDefinition } from "../parser";
interface HTMLRendererConfig {
classNames: string[]

View file

@ -1,10 +1,10 @@
// This is renderer for a very basic List view.
import { App } from "obsidian";
import { RendererConfig, RendererContext } from "./rendererRegistry";
import { displayError } from "../utils/ui";
import { ViewDefinition } from "../grammar/parser";
import { displayError } from "../../../utils/ui";
import Handlebars from "handlebars";
import { ParseResults } from "../cellParser/parseResults";
import { ViewDefinition } from "../parser";
import { ParseResults } from "../../syntaxHighlight/cellParser/parseResults";
interface TemplateRendererConfig {
template: HandlebarsTemplateDelegate

View file

@ -1,5 +1,5 @@
import { ModernCellParser } from "../cellParser/ModernCellParser";
import { ViewDefinition } from "../grammar/parser";
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";
import { ViewDefinition } from "../parser";
export interface DataFormat {
data: Record<string, any>[],

36
src/modules/main/init.ts Normal file
View file

@ -0,0 +1,36 @@
import { makeInjector } from "@hypersphere/dity";
import { MainModule } from "./module";
type InitFn = () => void
@(makeInjector<MainModule, 'factory'>()([
'settings.init',
'editor.init',
'syntaxHighlight.init',
'contextMenu.init',
'sync.init',
'debug.init',
'api.init'
]))
export class Init {
async make(
settingsInit: InitFn,
editorInit: InitFn,
highlighInit: InitFn,
contextMenu: InitFn,
syncInit: InitFn,
debugInit: InitFn,
apiInit: InitFn
) {
return () => {
settingsInit()
editorInit()
highlighInit()
contextMenu()
syncInit()
debugInit()
apiInit()
}
}
}

View file

@ -0,0 +1,82 @@
import { asFactory, buildContainer } from '@hypersphere/dity'
import { App, Plugin, Vault } from 'obsidian'
import { db } from '../database/module'
import { editor } from '../editor/module'
import { sync } from '../sync/module'
import { SQLSealSettings } from '../settings/SQLSealSettingsTab'
import { Init } from './init'
import { settingsModule } from '../settings/module'
import { syntaxHighlight } from '../syntaxHighlight/module'
import { contextMenu } from '../contextMenu/module'
import { debugModule } from '../debug/module'
import { apiModule } from '../api/module'
const obsidian = buildContainer(c => c
.externals<{
app: App,
plugin: Plugin,
vault: Vault
}>()
)
export const mainModule = buildContainer(c => c
.submodules({
obsidian,
db,
editor,
sync,
settings: settingsModule,
syntaxHighlight,
contextMenu,
debug: debugModule,
api: apiModule
})
.register({
init: asFactory(Init)
})
.externals<{ settings: SQLSealSettings }>()
.resolve({
'db.app': 'obsidian.app',
})
.resolve({
'editor.app': 'obsidian.app', // THESE SHOULD BE INVALID NOW
'editor.db': 'db.db',
'editor.plugin': 'obsidian.plugin',
'editor.sync': 'sync.syncBus',
'editor.cellParser': 'syntaxHighlight.cellParser',
'editor.settings': 'settings.settings'
})
.resolve({
'sync.app': 'obsidian.app',
'sync.db': 'db.db',
'sync.plugin': 'obsidian.plugin',
'sync.vault': 'obsidian.vault',
})
.resolve({
'settings.app': 'obsidian.app',
'settings.plugin': 'obsidian.plugin',
'settings.cellParser': 'syntaxHighlight.cellParser',
})
.resolve({
'syntaxHighlight.app': 'obsidian.app',
'syntaxHighlight.db': 'db.db',
'syntaxHighlight.plugin': 'obsidian.plugin',
'syntaxHighlight.rendererRegistry': 'editor.rendererRegistry'
})
.resolve({
'contextMenu.app': 'obsidian.app',
'contextMenu.plugin': 'obsidian.plugin'
})
.resolve({
'debug.plugin': 'obsidian.plugin'
})
.resolve({
'api.plugin': 'obsidian.plugin',
'api.cellParser': 'syntaxHighlight.cellParser',
'api.db': 'db.db',
'api.rendererRegistry': 'editor.rendererRegistry'
})
)
export type MainModule = typeof mainModule

View file

@ -0,0 +1,124 @@
import { makeInjector } from '@hypersphere/dity';
import { App, PluginSettingTab, Setting, Plugin } from 'obsidian';
import { SettingsModule } from './module';
import { Settings } from './Settings';
import { SettingsCSVControls } from './settingsTabSection/SettingsCSVControls';
import { SettingsJsonControls } from './settingsTabSection/SettingsJsonControls';
import { SettingsControls } from './settingsTabSection/SettingsControls';
export interface SQLSealSettings {
enableViewer: boolean;
enableEditing: boolean;
enableJSONViewer: boolean;
enableDynamicUpdates: boolean;
enableSyntaxHighlighting: boolean;
defaultView: 'grid' | 'markdown' | 'html';
gridItemsPerPage: number
}
export const DEFAULT_SETTINGS: SQLSealSettings = {
enableViewer: true,
enableEditing: true,
enableJSONViewer: true,
enableDynamicUpdates: true,
enableSyntaxHighlighting: true,
defaultView: 'grid',
gridItemsPerPage: 20
};
@(makeInjector<SettingsModule>()(['app', 'plugin', 'settings']))
export class SQLSealSettingsTab extends PluginSettingTab {
plugin: Plugin;
// settings: SQLSealSettings;
private onChangeFns: Array<(setting: SQLSealSettings) => void> = []
constructor(app: App, plugin: Plugin, private settings: Settings) {
super(app, plugin);
this.plugin = plugin;
this.settings = settings;
}
private controls: SettingsControls[] = []
registerControls(...controls: SettingsControls[]) {
this.controls = controls
}
display(): void {
const { containerEl } = this;
containerEl.empty();
this.controls.forEach(c => {
c.display(containerEl.createDiv())
})
containerEl.createEl('h3', { text: 'Behavior' });
new Setting(containerEl)
.setName('Enable Dynamic Updates')
.setDesc('SQLSeal will refresh your tables when underlying data changes.')
.addToggle(toggle => toggle
.setValue(this.settings.get('enableDynamicUpdates'))
.onChange(async (value) => {
this.settings.set('enableDynamicUpdates', !!value)
// await this.plugin.saveData(this.settings);
this.display();
// this.callChanges()
}));
new Setting(containerEl)
.setName('Enable Syntax Highlighting')
.setDesc('Syntax will get highlighted when editing SQLSeal code')
.addToggle(toggle => toggle
.setValue(this.settings.get('enableSyntaxHighlighting'))
.onChange(async (value) => {
this.settings.set('enableSyntaxHighlighting', !!value)
// await this.plugin.saveData(this.settings);
this.display();
// this.callChanges()
}));
containerEl.createEl('h3', { text: 'Views' });
new Setting(containerEl)
.setName('Default View')
.setDesc('This view will be used by default when you don\'t provide any view definition in your query')
.addDropdown(dropdown => dropdown
.addOption('grid', 'Grid')
.addOption('html', 'HTML Table')
.addOption('markdown', 'Markdown Table')
.setValue(this.settings.get('defaultView'))
.onChange(async (value: 'grid' | 'html' | 'markdown') => {
this.settings.set('defaultView', value ?? DEFAULT_SETTINGS.defaultView)
// await this.plugin.saveData(this.settings);
this.display();
// this.callChanges()
}));
containerEl.createEl('h4', { text: 'Grid View' });
new Setting(containerEl)
.setName('Items per page ')
.setDesc('How many items should display for each page of the Grid view')
.addDropdown(dropdown => dropdown
.addOption('20', '20')
.addOption('50', '50')
.addOption('100', '100')
.setValue(this.settings.get('gridItemsPerPage').toString())
.onChange(async (value) => {
this.settings.set('gridItemsPerPage', parseInt(value, 10) ?? DEFAULT_SETTINGS.gridItemsPerPage)
// await this.plugin.saveData(this.settings);
this.display();
// this.callChanges()
}));
}
// private callChanges() {
// // this.onChangeFns.forEach(f => f(this.settings))
// }
onChange(fn: (settings: SQLSealSettings) => void) {
this.settings.onChange(fn)
// this.onChangeFns.push(fn)
}
}

View file

@ -0,0 +1,25 @@
import { args, BusBuilder, CallbackType } from "@hypersphere/omnibus";
import { SQLSealSettings } from "./SQLSealSettingsTab";
export class Settings {
bus = BusBuilder
.init()
.register('change', args<SQLSealSettings>())
.build()
constructor(private settings: SQLSealSettings) {
}
get<K extends keyof SQLSealSettings>(key: K): SQLSealSettings[K] {
return this.settings[key]
}
set<K extends keyof SQLSealSettings>(key: K, val: SQLSealSettings[K]) {
this.settings[key] = val
this.bus.trigger('change', this.settings)
}
onChange(fn: CallbackType<[SQLSealSettings]>) {
return this.bus.on('change', fn)
}
}

View file

@ -0,0 +1,32 @@
import { makeInjector } from "@hypersphere/dity";
import { SettingsModule } from "./module";
import { App, Plugin } from "obsidian";
import { SQLSealSettingsTab } from "./SQLSealSettingsTab";
import { Settings } from "./Settings";
import { SettingsCSVControls } from "./settingsTabSection/SettingsCSVControls";
import { SettingsJsonControls } from "./settingsTabSection/SettingsJsonControls";
@(makeInjector<SettingsModule>()(["plugin", "settingsTab", "app", "settings"]))
export class SettingsInit {
async make(
plugin: Plugin,
settingsTab: SQLSealSettingsTab,
app: App,
settings: Settings,
) {
const csvControl = new SettingsCSVControls(settings, app, plugin);
const jsonControl = new SettingsJsonControls(settings, app, plugin);
const controls = [csvControl, jsonControl];
settingsTab.registerControls(...controls);
return () => {
controls.forEach((c) => c.register());
plugin.addSettingTab(settingsTab);
plugin.register(() => {
controls.forEach((c) => c.unregister());
});
};
}
}

View file

@ -1,5 +1,5 @@
import { App, Modal, Notice, Setting, TFile } from "obsidian";
import { sanitise } from "../utils/sanitiseColumn";
import { sanitise } from "../../../utils/sanitiseColumn";
export class CodeSampleModal extends Modal {
constructor(app: App, private file: TFile) {

View file

@ -0,0 +1,22 @@
import { asClass, asFactory, buildContainer } from "@hypersphere/dity";
import { App, Plugin } from "obsidian";
import { SettingsFactory } from "./settingsFactory";
import { SQLSealSettingsTab } from "./SQLSealSettingsTab";
import { SettingsInit } from "./init";
import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser";
export const settingsModule = buildContainer(c => c
.externals<{
'plugin': Plugin,
'app': App,
'cellParser': ModernCellParser
}>()
.register({
'settings': asFactory(SettingsFactory),
'settingsTab': asClass(SQLSealSettingsTab),
init: asFactory(SettingsInit)
})
.exports('settings', 'init')
)
export type SettingsModule = typeof settingsModule

View file

@ -0,0 +1,19 @@
import { makeInjector } from "@hypersphere/dity";
import { SettingsModule } from "./module";
import { Plugin } from "obsidian";
import { DEFAULT_SETTINGS } from "./SQLSealSettingsTab";
import { Settings } from "./Settings";
@(makeInjector<SettingsModule, 'factory'>()(['plugin']))
export class SettingsFactory {
async make(plugin: Plugin) {
const settings = Object.assign({}, DEFAULT_SETTINGS, await plugin.loadData());
const obj = new Settings(settings)
obj.onChange((settings) => {
plugin.saveData(settings)
})
return obj
}
}

View file

@ -0,0 +1,89 @@
import { App, Plugin, Setting } from "obsidian";
import { Settings } from "../Settings";
import {
checkTypeViewAvaiability,
} from "../utils/viewInspector";
import { SettingsControls } from "./SettingsControls";
import { CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE, CSVView } from "../view/CSVView";
export class SettingsCSVControls extends SettingsControls {
private registeredView: string | null = null;
register() {
if (this.settings.get("enableViewer")) {
const view = checkTypeViewAvaiability(this.app, CSV_VIEW_EXTENSIONS[0]);
if (view) {
this.registeredView = view;
return;
}
this.plugin.registerView(
CSV_VIEW_TYPE,
(leaf) => new CSVView(leaf, this.settings),
);
this.plugin.registerExtensions(CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE);
}
}
unregister() {
this.app.workspace.detachLeavesOfType(CSV_VIEW_TYPE);
(this.app as any).viewRegistry.unregisterExtensions([
...CSV_VIEW_EXTENSIONS,
]);
(this.app as any).viewRegistry.unregisterView(CSV_VIEW_TYPE);
}
display(el: HTMLDivElement) {
el.empty();
el.createEl("h2", { text: "CSV File Viewer" });
const view = checkTypeViewAvaiability(this.app, "csv");
if (view && view !== CSV_VIEW_TYPE) {
el.createDiv({
text: "CSV files are already handled by different plugin. To enable SQLSeal CSV editor, disable other plugin that handles it",
cls: "sqlseal-settings-warn",
});
return;
}
new Setting(el)
.setName("Enable CSV Viewer")
.setDesc(
"Enables CSV files in your vault and adds ability to display them in a grid.",
)
.addToggle((toggle) =>
toggle
.setValue(this.settings.get("enableViewer"))
.onChange(async (value) => {
this.settings.set("enableViewer", !!value);
if (!!value) {
// Enabled
this.register();
} else {
// Disabled
this.unregister();
}
// await this.plugin.saveData(this.settings);
// this.display();
// this.callChanges()
}),
);
new Setting(el)
.setName("Enable CSV Editing")
.setDesc(
"Enables Editing functions in the CSV Viewer. This will add buttons to add columns, remove individual rows and columns; and edit each entry.",
)
.setDisabled(!this.settings.get("enableViewer"))
.addToggle((toggle) =>
toggle
.setValue(this.settings.get("enableEditing"))
.onChange(async (value) => {
this.settings.set("enableEditing", value);
// await this.plugin.saveData(this.settings);
// this.callChanges()
}),
);
}
}

View file

@ -0,0 +1,12 @@
import { App, Plugin } from "obsidian";
import { Settings } from "../Settings";
export abstract class SettingsControls {
constructor(
protected settings: Settings,
protected app: App,
protected plugin: Plugin
) {}
abstract display(el: HTMLDivElement): void
}

View file

@ -0,0 +1,71 @@
import { App, Setting } from "obsidian";
import { Settings } from "../Settings";
import { SettingsControls } from "./SettingsControls";
import { checkTypeViewAvaiability } from "../utils/viewInspector";
import {
JSON_VIEW_EXTENSIONS,
JSON_VIEW_TYPE,
JsonView,
} from "../view/JsonView";
export class SettingsJsonControls extends SettingsControls {
private registeredView: string | null = null;
register() {
if (this.settings.get("enableJSONViewer")) {
const view = checkTypeViewAvaiability(this.app, JSON_VIEW_EXTENSIONS[0]);
// FIXME: also allow for json5 checks
if (view) {
this.registeredView = view;
return;
}
this.plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf));
this.plugin.registerExtensions(JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE);
}
}
unregister() {
this.app.workspace.detachLeavesOfType(JSON_VIEW_TYPE);
(this.app as any).viewRegistry.unregisterExtensions([
...JSON_VIEW_EXTENSIONS,
]);
(this.app as any).viewRegistry.unregisterView(JSON_VIEW_TYPE);
}
display(el: HTMLDivElement) {
el.empty();
el.createEl("h2", { text: "JSON File Viewer" });
const view = checkTypeViewAvaiability(this.app, "json");
if (view && view !== JSON_VIEW_TYPE) {
el.createDiv({
text: "JSON files are already handled by different plugin. To enable SQLSeal JSON preview, disable other plugin that handles it",
cls: 'sqlseal-settings-warn'
});
return;
}
new Setting(el)
.setName("Enable JSON Viewer")
.setDesc(
"Enables JSON and JSON5 files in your files explorer and allows to preview them.",
)
.addToggle((toggle) =>
toggle
.setValue(this.settings.get("enableJSONViewer"))
.onChange(async (value) => {
this.settings.set("enableJSONViewer", !!value);
if (!!value) {
// Enabled
this.register();
} else {
// Disabled
this.unregister();
}
}),
);
}
}

View file

@ -0,0 +1,12 @@
import { App } from "obsidian";
export const checkTypeViewAvaiability = (app: App, extension: string) => {
const viewRegistry = (app as any).viewRegistry;
const csvHandler = viewRegistry.typeByExtension[extension];
if (csvHandler) {
return csvHandler as string
} else {
return null
}
};

View file

@ -0,0 +1,465 @@
import { WorkspaceLeaf, TextFileView, Menu, MenuItem } from "obsidian";
import { parse, unparse } from "papaparse";
import {
GridRenderer,
GridRendererCommunicator,
} from "../../editor/renderer/GridRenderer";
import { errorNotice } from "../../../utils/notice";
import { ConfigObject, loadConfig, saveConfig } from "src/utils/csvConfig";
import { ColumnType } from "../../../utils/types";
import { Settings } from "../Settings";
import { RenameColumnModal } from "../modal/renameColumnModal";
import { CodeSampleModal } from "../modal/showCodeSample";
import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal";
const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n));
export const CSV_VIEW_TYPE = "csv-viewer" as const;
export const CSV_VIEW_EXTENSIONS = ["csv"];
export class CSVView extends TextFileView {
private content: string;
private table: HTMLTableElement;
private config: ConfigObject;
constructor(
leaf: WorkspaceLeaf,
private readonly settings: Settings,
) {
super(leaf);
}
getViewType(): string {
return CSV_VIEW_TYPE;
}
getDisplayText(): string {
return this.file?.basename || "CSV Viewer";
}
async onload(): Promise<void> {
super.onload();
this.contentEl.addClass("csv-viewer-container");
}
async onOpen() {
this.renderCSV();
}
async onClose() {
// Cleanup if needed
}
async setViewData(data: string, clear: boolean): Promise<void> {
this.content = data;
await this.renderCSV();
}
getViewData(): string {
return this.content;
}
clear(): void {
this.content = "";
this.table.empty();
}
private result: any;
updateRow(newRowData: Record<string, any>) {
this.result.data[parseInt(newRowData.__index, 10)] = newRowData;
this.saveData(true);
}
deleteRow(idx: number) {
this.result.data.splice(idx, 1);
this.saveData();
}
deleteColumn(column: string) {
this.result.fields = this.result.fields.filter((c: string) => c !== column);
this.saveData();
}
addNewColumn(columnName: string) {
if (this.result.fields.indexOf(columnName) > -1) {
throw new Error("Column already exists");
}
this.result.fields.push(columnName);
this.result.data = this.result.data.map((d: any) => ({
[columnName]: "",
...d,
}));
this.saveData();
}
renameColumn(oldName: string, newName: string) {
if (!newName) {
return;
}
if (this.result.fields.contains(newName)) {
errorNotice("Column already exists");
return;
}
const oldNameIdx = this.result.fields.indexOf(oldName);
if (oldNameIdx < 0) {
errorNotice("Old column does not exist");
return;
}
this.result.fields[oldNameIdx] = newName;
this.result.data = this.result.data.map((d: Record<string, unknown>) => {
d[newName] = d[oldName];
delete d[oldName];
return d;
});
this.saveData();
this.loadDataIntoGrid();
}
setIsEditable(newValue: boolean) {
this.settings.set("enableEditing", newValue);
// FIXME: if there already rendered view, use this to rerender it?
}
saveData(noRefresh: boolean = false) {
if (noRefresh) {
this.refreshSkip = Date.now() + 500;
}
// Map results
const res = [...this.result.data].map((r) => {
const r2 = { ...r };
Object.keys(r).forEach((k) => {
if (typeof r[k] === "boolean") {
r2[k] = r[k] ? 1 : 0;
}
});
return r2;
});
const output = unparse({ ...this.result, data: res });
this.app.vault.modify(this.file!, output);
this.refreshTypes();
}
createRow() {
this.result.data.push(
this.result.fields.reduce(
(acc: Record<string, any>, f: string) => ({ ...acc, [f]: "" }),
{},
),
);
this.saveData();
}
getColumnType(columnName: string) {
if (this.config.columnDefinitions[columnName]) {
return this.config.columnDefinitions[columnName].type;
}
return "auto";
}
async changeColumnType(columnName: string, type: ColumnType) {
const prev = this.config.columnDefinitions[columnName] ?? {};
this.config.columnDefinitions[columnName] = {
...prev,
type: type,
};
await this.saveConfig();
}
async loadConfig() {
while (!this.file) {
await delay(100);
}
this.config = await loadConfig(this.file, this.app.vault);
}
async saveConfig() {
await saveConfig(this.file!, this.config, this.app.vault);
}
private getColumnConfigurations(columns: string[]) {
return columns.map((f) => {
if (!this.config) {
return {
field: f,
};
}
const def = this.config.columnDefinitions[f]?.type;
if (!def || def === "auto") {
return { field: f };
}
if (def === "date") {
return {
field: f,
cellDataType: "dateString",
};
}
return {
field: f,
cellDataType: def,
};
});
}
refreshTypes() {
if (this.gridCommunicator) {
const columns = this.result.fields;
if (columns && columns.length) {
this.gridCommunicator.gridApi.setGridOption(
"columnDefs",
this.getColumnConfigurations(columns),
);
}
}
}
moveColumn(name: string, toIndex: number) {
let fields = this.result.fields as Array<string>;
fields = fields.filter((f) => f !== name);
fields = [...fields.slice(0, toIndex), name, ...fields.slice(toIndex)];
this.result.fields = fields;
this.saveData();
}
api: any = null;
gridCommunicator: GridRendererCommunicator | null = null;
refreshSkip: number = 0;
formatWithTypes(d: Record<string, string | boolean | number>) {
Object.entries(this.config.columnDefinitions).forEach(([key, value]) => {
if (!d[key]) {
return;
}
if (value.type === "boolean") {
if (d[key] === "false" || d[key] === "0") {
d[key] = false;
}
d[key] = !!d[key];
}
if (value.type === "number") {
if (d[key] === null || d[key] === "") {
d[key] = "";
} else {
d[key] = parseFloat(d[key] as string);
}
}
if (value.type === "date") {
// try parsing
const val = d[key] as string;
const date = new Date(val);
d[key] = date.toISOString().split("T")[0];
}
});
return d;
}
prepareData() {
const result = parse(this.content, {
header: true,
skipEmptyLines: true,
});
const data = result.data.map((d: any, i) => ({
...this.formatWithTypes(d),
__index: i.toString(),
}));
return {
data: data,
fields: result.meta.fields,
};
}
loadDataIntoGrid() {
if (this.refreshSkip > Date.now()) {
return;
}
requestAnimationFrame(() => {
const result = this.prepareData();
this.result = result;
this.refreshTypes();
this.api!.render(result);
});
}
isLoading: boolean = false;
private async renderCSV() {
if (this.isLoading) {
if (this.api) {
this.loadDataIntoGrid();
}
return;
}
this.isLoading = true;
this.contentEl.empty();
const csvEditorDiv = this.contentEl.createDiv({
cls: "sql-seal-csv-editor",
});
const buttonsRow = csvEditorDiv.createDiv({
cls: "sql-seal-csv-viewer-buttons",
});
await this.loadConfig();
if (this.settings.get("enableEditing")) {
const createColumn = buttonsRow.createEl("button", {
text: "Add Column",
});
const createRow = buttonsRow.createEl("button", { text: "Add Row" });
createColumn.addEventListener("click", (e) => {
e.preventDefault();
const modal = new RenameColumnModal(this.app, (res) => {
this.addNewColumn(res);
});
modal.open();
});
createRow.addEventListener("click", (e) => {
e.preventDefault();
this.createRow();
});
}
const generateSqlCode = buttonsRow.createEl("button", {
text: "Generate SQLSeal code",
});
const gridEl = csvEditorDiv.createDiv({ cls: "sql-seal-csv-viewer" });
generateSqlCode.addEventListener("click", (e) => {
e.preventDefault();
if (!this.file) {
return;
}
const modal = new CodeSampleModal(this.app, this.file);
modal.open();
});
const grid = new GridRenderer(this.settings, null, this.app);
const csvView = this;
const data = this.prepareData();
this.result = data;
const api = grid.render(
{
columnDefs: this.getColumnConfigurations(data.fields ?? []),
defaultColDef: {
editable: this.settings.get("enableEditing"),
headerComponentParams: {
enableMenu: this.settings.get("enableEditing"),
showColumnMenu: function (e: any) {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("Rename Column");
item.onClick(() => {
const modal = new RenameColumnModal(csvView.app, (res) => {
csvView.renameColumn(
this.column.userProvidedColDef.field,
res,
);
});
modal.open();
});
});
menu.addItem((item) => {
item.setTitle("Delete Column");
item.onClick(() => {
const colName = this.column.userProvidedColDef.field;
const modal = new DeleteConfirmationModal(
csvView.app,
`column ${colName}`,
() => {
csvView.deleteColumn(colName);
},
);
modal.open();
});
});
// FIXME: rework it to submenus.
menu.addSeparator();
menu.addItem((item) => {
// item.setDisabled(true)
item.setTitle("Data Type");
// item.setIsLabel(true)
const ipfSubmenu = (item as any).setSubmenu();
const types = [
"auto",
"text",
"number",
"boolean",
"date",
] as ColumnType[];
const colName = this.column.userProvidedColDef.field;
const current = csvView.getColumnType(colName);
types.forEach((type) => {
ipfSubmenu.addItem((subItem: MenuItem) => {
const checkbox = type === current ? "✓ " : "";
subItem.setTitle(checkbox + type);
subItem.onClick(() => {
csvView.changeColumnType(colName, type);
csvView.refreshTypes();
csvView.loadDataIntoGrid();
});
});
});
});
const pos = e.getBoundingClientRect();
menu.showAtPosition({ x: pos.x, y: pos.y + 20 });
},
},
},
enableCellTextSelection: false,
ensureDomOrder: false,
paginationAutoPageSize: true,
suppressMovableColumns: false,
onColumnMoved: (e) => {
if (!e.finished) {
return;
}
const columnName = e.column?.getUserProvidedColDef();
if (!columnName) {
return;
}
csvView.moveColumn(columnName?.field!, e.toIndex!);
},
domLayout: "normal",
getRowId: (p) => p.data.__index,
onCellValueChanged: (event) => {
if (event.rowIndex === null) {
return;
}
this.updateRow(event.data);
},
onCellContextMenu: (e) => {
if (!this.settings.get("enableEditing")) {
return;
}
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("Delete Row");
item.onClick(() => {
const modal = new DeleteConfirmationModal(
csvView.app,
`row`,
() => {
csvView.deleteRow(e.data.__index);
},
);
modal.open();
});
});
menu.showAtMouseEvent(e.event as any);
},
},
gridEl,
{ sourcePath: this.file?.path || "" },
);
this.api = api;
this.gridCommunicator = api.communicator;
api.render(data);
}
}

View file

@ -1,5 +1,5 @@
import { App, Plugin, TFile } from "obsidian";
import { AFileSyncTable } from "./tables/abstractFileSyncTable";
import { AFileSyncTable } from "../sync/tables/abstractFileSyncTable";
export class SealFileSync {
private tablePlugins: Array<AFileSyncTable> = []

View file

@ -0,0 +1,29 @@
import { App, Plugin } from "obsidian";
import { SealFileSync } from "./FileSync";
import { Sync } from "../sync/sync";
import { makeInjector } from "@hypersphere/dity";
import { SyncModule } from "../module";
import { SqlSealDatabase } from "../../database/database";
import { FilesFileSyncTable } from "../sync/tables/filesTable";
import { TagsFileSyncTable } from "../sync/tables/tagsTable";
import { TasksFileSyncTable } from "../sync/tables/tasksTable";
import { LinksFileSyncTable } from "../sync/tables/linksTable";
@(makeInjector<SyncModule>()(["app", "plugin", "db", "syncBus"]))
export class FileSyncFactory {
async make(app: App, plugin: Plugin, db: SqlSealDatabase, sync: Sync) {
return async () => {
const fileSync = new SealFileSync(app, plugin, (name) =>
sync.triggerGlobalTableChange(name),
);
fileSync.addTablePlugin(new FilesFileSyncTable(db, app, plugin));
fileSync.addTablePlugin(new TagsFileSyncTable(db, app));
fileSync.addTablePlugin(new TasksFileSyncTable(db, app));
fileSync.addTablePlugin(new LinksFileSyncTable(db, app));
await fileSync.init();
return fileSync;
};
}
}

View file

@ -0,0 +1,22 @@
import { asFactory, buildContainer } from "@hypersphere/dity"
import { FileSyncFactory } from "./fileSyncController/fileSyncFactory"
import { App, Plugin, Vault } from "obsidian"
import { SyncFactory } from "./sync/syncFactory"
import { SqlSealDatabase } from "../database/database"
import { SyncInit } from "./sync/init"
export const sync = buildContainer(c => c
.register({
fileSync: asFactory(FileSyncFactory),
syncBus: asFactory(SyncFactory),
init: asFactory(SyncInit)
})
.externals<{
app: App,
plugin: Plugin,
db: SqlSealDatabase,
vault: Vault
}>()
.exports('init', 'syncBus')
)
export type SyncModule = typeof sync

View file

@ -11,7 +11,7 @@ export class ConfigurationRepository extends Repository {
public async getConfig(key: string) {
try {
const config = await this.db.select('SELECT value FROM configuration WHERE id = @id', { id: key })
const config = (await this.db.select('SELECT value FROM configuration WHERE id = @id', { id: key }))!
if (config.data && config.data.length) {
return config.data[0].value
}

View file

@ -42,17 +42,17 @@ export class TableAliasesRepository extends Repository {
}
async getAll() {
const { data } = await this.db.select('SELECT * FROM TABLE_ALIASES', {})
const { data } = (await this.db.select('SELECT * FROM TABLE_ALIASES', {}))!
return data as unknown as TableAlias[]
}
async getByAlias(sourceFileName: string, aliasName: string) {
const { data } = await this.db.select(`SELECT * FROM TABLE_ALIASES
const { data } = (await this.db.select(`SELECT * FROM TABLE_ALIASES
WHERE source_file_name=@source_file_name
AND alias_name=@alias_name`, {
'source_file_name': sourceFileName,
'alias_name': aliasName
})
}))!
if (!data || data.length < 0) {
return null
}
@ -60,10 +60,10 @@ export class TableAliasesRepository extends Repository {
}
async getByTableName(tableName: string) {
const { data } = await this.db.select(`SELECT * FROM TABLE_ALIASES
const { data } = (await this.db.select(`SELECT * FROM TABLE_ALIASES
WHERE table_name = @table_name`, {
table_name: tableName
})
}))!
if (!data) {
return []
}
@ -72,9 +72,9 @@ export class TableAliasesRepository extends Repository {
async getByContext(sourceFileName: string) {
const { data } = await this.db.select(`SELECT * FROM TABLE_ALIASES
const { data } = (await this.db.select(`SELECT * FROM TABLE_ALIASES
WHERE source_file_name=@source_file_name
`, { source_file_name: sourceFileName })
`, { source_file_name: sourceFileName }))!
if (!data) {
return []
}

View file

@ -51,10 +51,10 @@ export class TableDefinitionsRepository extends Repository {
}
async getBySourceFile(sourceFile: string) {
const { data } = await this.db.select(
const { data } = (await this.db.select(
`SELECT * FROM ${this.TABLE_NAME} WHERE source_file = @sourceFile`,
{ sourceFile }
)
))!
if (!data.length) {
return null
@ -63,10 +63,10 @@ export class TableDefinitionsRepository extends Repository {
}
async getByRefreshId(refreshId: string) {
const { data } = await this.db.select(
const { data } = (await this.db.select(
`SELECT * FROM ${this.TABLE_NAME} WHERE refresh_id= @refreshId`,
{ refreshId }
)
))!
if (!data.length) {
return null
@ -79,7 +79,7 @@ export class TableDefinitionsRepository extends Repository {
}
async getAll() {
const { data } = await this.db.select(`SELECT * FROM ${this.TABLE_NAME}`, {})
const { data } = (await this.db.select(`SELECT * FROM ${this.TABLE_NAME}`, {}))!
return data.map(d => ({
...d,
type: d.type ?? 'file',
@ -98,7 +98,7 @@ export class TableDefinitionsRepository extends Repository {
updateData.arguments = JSON.stringify(fields.arguments);
}
await this.db.db.updateData(this.TABLE_NAME, [{
await this.db.db!.updateData(this.TABLE_NAME, [{
id,
...updateData
}], 'id')

View file

@ -0,0 +1,17 @@
import { makeInjector } from "@hypersphere/dity";
import { SyncModule } from "../module";
import { App } from "obsidian";
import { SealFileSync } from "../fileSyncController/FileSync";
@(makeInjector<SyncModule, 'factory'>()([
'app', 'fileSync'
]))
export class SyncInit {
make(app: App, fileSync: () => Promise<SealFileSync>) {
return () => {
app.workspace.onLayoutReady(async () => {
await fileSync()
})
}
}
}

View file

@ -1,13 +1,13 @@
import { SqlSealDatabase } from "../database/database";
import { TableAliasesRepository } from "./repository/tableAliases";
import { App, TAbstractFile, TFile, Vault } from "obsidian";
import { FilepathHasher } from "../utils/hasher";
import { FilepathHasher } from "../../../utils/hasher";
import { Omnibus } from "@hypersphere/omnibus";
import { SyncStrategyFactory } from "./syncStrategy/SyncStrategyFactory";
import { ConfigurationRepository } from "./repository/configuration";
import { TableDefinitionsRepository, TableDefinition } from "./repository/tableDefinitions";
import { ParserTableDefinition } from "./syncStrategy/types";
import { uniq } from "lodash";
import { SqlSealDatabase } from "../../database/database";
import { TableDefinition, TableDefinitionsRepository } from "../repository/tableDefinitions";
import { TableAliasesRepository } from "../repository/tableAliases";
import { ConfigurationRepository } from "../repository/configuration";
import { ParserTableDefinition } from "../syncStrategy/types";
import { SyncStrategyFactory } from "../syncStrategy/SyncStrategyFactory";
const SQLSEAL_DATABASE_VERSION = 2;
@ -114,7 +114,7 @@ export class Sync {
}
async getTablesMappingForContext(sourceFileName: string) {
const tables = await this.tableMapLog.getByContext(sourceFileName)
const tables = await this.tableMapLog.getByContext(sourceFileName) as { alias_name: string, table_name: string }[]
const map = tables.reduce((acc, t) => ({
...acc,
[t.alias_name as string]: t.table_name

View file

@ -0,0 +1,19 @@
import { App, Vault } from "obsidian";
import { makeInjector } from "@hypersphere/dity";
import { Sync } from "./sync";
import { SyncModule } from "../module";
import { SqlSealDatabase } from "../../database/database";
@(makeInjector<SyncModule>()([
'db', 'vault', 'app'
]))
export class SyncFactory {
async make(
db: SqlSealDatabase,
vault: Vault,
app: App) {
const sync = new Sync(db, vault, app)
await sync.init()
return sync
}
}

View file

@ -1,5 +1,5 @@
import { App, TFile } from "obsidian";
import { SqlSealDatabase } from "../../database/database";
import { SqlSealDatabase } from "../../../database/database";
export abstract class AFileSyncTable {
constructor(

View file

@ -1,8 +1,8 @@
import { App, Plugin, TFile } from "obsidian";
import { AFileSyncTable } from "./abstractFileSyncTable";
import { sanitise } from "../../utils/sanitiseColumn";
import { SqlSealDatabase } from "../../database/database";
import { difference } from "lodash";
import { sanitise } from "../../../../utils/sanitiseColumn";
import { SqlSealDatabase } from "../../../database/database";
export const FILES_TABLE_NAME = 'files'
@ -56,7 +56,7 @@ export class FilesFileSyncTable extends AFileSyncTable {
const newColumns = difference(newSetOfColumns, this.columns)
if (newColumns.length) {
await this.db.addColumns(FILES_TABLE_NAME, newColumns)
this.columns = await this.db.getColumns(FILES_TABLE_NAME)
this.columns = (await this.db.getColumns(FILES_TABLE_NAME)) ?? []
}
}
@ -70,7 +70,7 @@ export class FilesFileSyncTable extends AFileSyncTable {
}
async onFileCreateBulk(files: Array<TFile>) {
this.columns = await this.db.getColumns(FILES_TABLE_NAME)
this.columns = (await this.db.getColumns(FILES_TABLE_NAME)) ?? []
// One by one
for (const file of files) {
@ -81,7 +81,7 @@ export class FilesFileSyncTable extends AFileSyncTable {
async onInit(): Promise<void> {
this.db.createTableNoTypes(FILES_TABLE_NAME, ['id', 'name', 'path', 'created_at', 'modified_at', 'file_size'])
this.columns = await this.db.getColumns(FILES_TABLE_NAME)
this.columns = (await this.db.getColumns(FILES_TABLE_NAME)) ?? []
// Indexes
const toIndex = ['id', 'name', 'path']

View file

@ -1,11 +1,11 @@
import { ISyncStrategy } from "./abstractSyncStrategy";
import { sanitise } from "../../utils/sanitiseColumn";
import { parse } from "papaparse";
import { FilepathHasher } from "../../utils/hasher";
import { TableDefinitionExternal } from "../repository/tableDefinitions";
import { ParserTableDefinition } from "./types";
import { App } from "obsidian";
import { loadConfig } from "../../utils/csvConfig";
import { FilepathHasher } from "../../../utils/hasher";
import { sanitise } from "../../../utils/sanitiseColumn";
import { loadConfig } from "../../../utils/csvConfig";
const DEFAULT_FILE_HASH = ''

View file

@ -1,10 +1,10 @@
import { App } from "obsidian";
import { ISyncStrategy } from "./abstractSyncStrategy";
import { FilepathHasher } from "../../utils/hasher";
import { ParserTableDefinition } from "./types";
import { parse } from 'json5';
import { uniq } from "lodash";
import * as jsonpath from 'jsonpath'
import { FilepathHasher } from "../../../utils/hasher";
const DEFAULT_FILE_HASH = ''

View file

@ -1,10 +1,10 @@
import { FilepathHasher } from "../../utils/hasher";
import { ISyncStrategy } from "./abstractSyncStrategy";
import type { App } from "obsidian";
import { Component, MarkdownRenderer, normalizePath } from "obsidian";
import { ParserTableDefinition } from "./types";
import { dirname, join } from "path";
import { sanitise } from "../../utils/sanitiseColumn";
import { FilepathHasher } from "../../../utils/hasher";
import { sanitise } from "../../../utils/sanitiseColumn";
const DEFAULT_FILE_HASH = ''

View file

@ -5,7 +5,7 @@ import { MarkdownTableSyncStrategy } from "./MarkdownTableSyncStrategy";
import { JsonFileSyncStrategy } from "./JSONFileSyncStrategy";
import { ParserTableDefinition } from "./types";
import { TableDefinitionExternal } from "../repository/tableDefinitions";
import { getFileExtension } from "../../utils/extractExtension";
import { getFileExtension } from "../../../utils/extractExtension";
const resolveFileStrategy = (filename: string) => {

View file

@ -1,7 +1,7 @@
import { App } from "obsidian";
import { TableDefinitionExternal } from "../repository/tableDefinitions";
import { ParserTableDefinition } from "./types";
import { ColumnDefinition } from "../../utils/types";
import { ColumnDefinition } from "../../../utils/types";
export abstract class ISyncStrategy {
constructor(protected def: TableDefinitionExternal, protected app: App) {

View file

@ -1,7 +1,7 @@
import { isStringifiedArray } from "../../../utils/ui";
import { SqlSealDatabase } from "../../database/database";
import { CellFunction } from "./CellFunction";
import { parse } from 'json5'
import { isStringifiedArray, renderStringifiedArray } from '../utils/ui'
import { SqlSealDatabase } from "../database/database";
export type UnregisterCallback = () => void;

View file

@ -0,0 +1,32 @@
import { App } from "obsidian"
import { ModernCellParser } from "./ModernCellParser"
import { LinkParser } from "./parser/link"
import { ImageParser } from "./parser/image"
import { CheckboxParser } from "./parser/checkbox"
import { makeInjector } from "@hypersphere/dity"
import { MainModule } from "../../main/module"
import { SqlSealDatabase } from "../../database/database"
import { SyntaxHighlightModule } from "../module"
export const getCellParser = (app: App, create = createEl) => {
const cellParser = new ModernCellParser()
cellParser.register(new LinkParser(app, create))
cellParser.register(new ImageParser(app, create))
cellParser.register(new CheckboxParser(app, create))
return cellParser
}
@(makeInjector<SyntaxHighlightModule>()([
'app',
'db'
]))
export class CellParserFactory {
make(app: App, db: SqlSealDatabase, create: typeof createEl = createEl) {
const cellParser = new ModernCellParser()
cellParser.register(new LinkParser(app, create))
cellParser.register(new ImageParser(app, create))
cellParser.register(new CheckboxParser(app, create))
cellParser.registerDbFunctions(db)
return cellParser
}
}

View file

@ -1,4 +1,4 @@
import { isLinkLocal } from "../../utils/ui/helperFunctions";
import { isLinkLocal } from "../../../../utils/ui/helperFunctions";
import { CellFunction } from "../CellFunction";
import { CellParserResult } from "../ModernCellParser";
import { App } from "obsidian";

View file

@ -1,5 +1,5 @@
import { isStringifiedArray, renderStringifiedArray } from "../../utils/ui";
import { isLinkLocal, isLinktext, removeExtension } from "../../utils/ui/helperFunctions";
import { isStringifiedArray, renderStringifiedArray } from "../../../../utils/ui";
import { isLinkLocal, isLinktext, removeExtension } from "../../../../utils/ui/helperFunctions";
import { CellFunction } from "../CellFunction";
import { App } from "obsidian";

View file

@ -0,0 +1,143 @@
import {
EditorView,
ViewPlugin,
ViewUpdate,
DecorationSet,
Decoration,
WidgetType,
} from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { App } from "obsidian";
import { SqlSealDatabase } from "../../database/database";
import { Settings } from "../../settings/Settings";
import { Sync } from "../../sync/sync/sync";
import { SqlSealInlineHandler } from "../../editor/codeblockHandler/inline/InlineCodeHandler";
import { InlineProcessor } from "../../editor/codeblockHandler/inline/InlineProcessor";
export function createSqlSealEditorExtension(
app: App,
db: SqlSealDatabase,
settings: Settings,
sync: Sync,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
inlineHandler: SqlSealInlineHandler;
constructor(view: EditorView) {
this.inlineHandler = new SqlSealInlineHandler(app, db, settings, sync);
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (
update.docChanged ||
update.viewportChanged ||
update.selectionSet
) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView) {
const builder = new RangeSetBuilder<Decoration>();
const tree = syntaxTree(view.state);
tree.iterate({
enter: ({ type, from, to }) => {
if (type.name.includes("inline-code")) {
const text = view.state.doc.sliceString(from, to);
if (text.startsWith("S>")) {
// Check if we're currently editing this specific inline code
const isEditing =
view.hasFocus &&
view.state.selection.ranges.some(
(range) => range.from >= from && range.to <= to,
);
if (!isEditing) {
// Create a replacement decoration
builder.add(
from,
to,
Decoration.replace({
widget: new SqlSealInlineWidget(
text,
this.inlineHandler,
view.state.doc.lineAt(from).number,
app,
),
}),
);
}
}
}
},
});
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
},
);
}
class SqlSealInlineWidget extends WidgetType {
constructor(
private query: string,
private handler: SqlSealInlineHandler,
private line: number,
private app: App,
) {
super();
}
eq(other: SqlSealInlineWidget): boolean {
return other.query === this.query && other.line === this.line;
}
processor: InlineProcessor;
toDOM(): HTMLElement {
const container = document.createElement("span");
container.classList.add("sqlseal-inline-result");
// Create a new context for the inline query
const ctx = {
sourcePath: this.app.workspace.getActiveFile()?.path ?? "",
addChild: () => {},
getSectionInfo: () => ({
lineStart: this.line,
lineEnd: this.line,
}),
};
// Show the original query on hover
container.setAttribute("aria-label", this.query);
container.classList.add("has-tooltip");
this.processor = this.handler.instantiateProcessor(
this.query,
container,
ctx.sourcePath,
);
this.processor.load();
return container;
}
destroy(_dom: HTMLElement): void {
if (this.processor) {
this.processor.unload();
}
}
ignoreEvent(_event: Event): boolean {
// Return false to allow events to propagate (important for editing)
return false;
}
}

View file

@ -11,11 +11,11 @@ import {
DecorationSet
} from '@codemirror/view';
import { SQLSealLangDefinition } from '../grammar/parser';
import { RendererRegistry } from '../renderer/rendererRegistry';
import { Range } from '@codemirror/state';
import { Decorator, highlighterOperation } from '../grammar/highlighterOperation';
import { FilePathWidget } from './widgets/FilePathWidget';
import { RendererRegistry } from '../../editor/renderer/rendererRegistry';
import { SQLSealLangDefinition } from '../../editor/parser';
const markDecorations = {
blockFlag: Decoration.mark({ class: 'cm-sqlseal-block-flag' }),

View file

@ -1,5 +1,5 @@
import { WidgetType } from "@codemirror/view";
import { App, Notice } from "obsidian";
import { App } from "obsidian";
export class FilePathWidget extends WidgetType {
constructor(private filePath: string, private app: App) {

View file

@ -0,0 +1,23 @@
import { makeInjector } from "@hypersphere/dity"
import { SyntaxHighlightModule } from "./module"
import { EditorView, ViewPlugin } from "@codemirror/view";
import { App, Plugin } from "obsidian";
import { RendererRegistry } from "../editor/renderer/rendererRegistry";
import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight";
@(makeInjector<SyntaxHighlightModule, 'factory'>()([
'app', 'rendererRegistry', 'plugin'
]))
export class SyntaxHighlightInit {
make(app: App, rendererRegistry: RendererRegistry, plugin: Plugin) {
return () => {
// FIXME: settings here.
plugin.registerEditorExtension([
ViewPlugin.define(
(view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry),
{ decorations: v => v.decorations }
)
]);
}
}
}

View file

@ -0,0 +1,23 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { SyntaxHighlightInit } from "./init";
import { App, Plugin } from "obsidian";
import { RendererRegistry } from "../editor/renderer/rendererRegistry";
import { CellParserFactory } from "./cellParser/factory";
import { SqlSealDatabase } from "../database/database";
export const syntaxHighlight = buildContainer((c) =>
c
.externals<{
app: App;
db: SqlSealDatabase;
rendererRegistry: RendererRegistry;
plugin: Plugin;
}>()
.register({
init: asFactory(SyntaxHighlightInit),
cellParser: asFactory(CellParserFactory),
})
.exports("init", "cellParser"),
);
export type SyntaxHighlightModule = typeof syntaxHighlight;

View file

@ -1,93 +0,0 @@
import { Plugin } from "obsidian"
import SqlSealPlugin from "../main"
import { version } from '../../package.json'
import { RendererConfig } from "../renderer/rendererRegistry";
import { CellFunction } from "../cellParser/CellFunction";
const API_VERSION = 4;
export class SQLSealRegisterApi {
registeredApis: Array<SQLSealApi> = []
constructor(private sqlSealPlugin: SqlSealPlugin) {
sqlSealPlugin.register(() => {
this.registeredApis.forEach(p => {
p.unregister()
})
this.registeredApis = []
})
}
get sqlSealVersion() {
return version
}
get apiVersion() {
return API_VERSION
}
registerForPlugin(plugin: Plugin) {
const api = new SQLSealApi(plugin, this.sqlSealPlugin)
this.registeredApis.push(api)
return api
}
}
interface RegisteredView {
name: string;
viewClass: any;
}
// TODO: use the type from registrator
export interface RegisteredFlag {
name: string,
key: string
}
export class SQLSealApi {
private views: Array<RegisteredView> = []
private functions: Array<CellFunction> = []
private flags: Array<RegisteredFlag> = []
constructor(private readonly plugin: Plugin, private sqlSealPlugin: SqlSealPlugin) {
plugin.register(() => {
this.unregister()
})
}
registerView(name: string, viewClass: RendererConfig) {
this.views.push({
name,
viewClass
})
this.sqlSealPlugin.registerSQLSealView(name, viewClass)
}
registerCustomFunction(fn: CellFunction) {
this.functions.push(fn)
this.sqlSealPlugin.registerSQLSealFunction(fn)
}
registerTable<const columns extends string[]>(tableName: string, columns: columns) {
return this.sqlSealPlugin.registerTable(this.plugin, tableName, columns)
}
registerFlag(flag: RegisteredFlag) {
this.flags.push(flag)
this.sqlSealPlugin.registerSQLSealFlag(flag)
}
unregister() {
for(const view of this.views) {
this.sqlSealPlugin.unregisterSQLSealView(view.name)
}
this.views = []
for(const fn of this.functions) {
this.sqlSealPlugin.unregisterSQLSealFunction(fn.name)
}
for(const flag of this.flags) {
this.sqlSealPlugin.unregisterSQLSealFlag(flag.name)
}
}
}

View file

@ -1,162 +0,0 @@
import { App, PluginSettingTab, Setting, Plugin } from 'obsidian';
export interface SQLSealSettings {
enableViewer: boolean;
enableEditing: boolean;
enableJSONViewer: boolean;
enableDynamicUpdates: boolean;
enableSyntaxHighlighting: boolean;
defaultView: 'grid' | 'markdown' | 'html';
gridItemsPerPage: number
}
export const DEFAULT_SETTINGS: SQLSealSettings = {
enableViewer: true,
enableEditing: true,
enableJSONViewer: true,
enableDynamicUpdates: true,
enableSyntaxHighlighting: true,
defaultView: 'grid',
gridItemsPerPage: 20
};
export class SQLSealSettingsTab extends PluginSettingTab {
plugin: Plugin;
settings: SQLSealSettings;
private onChangeFns: Array<(setting: SQLSealSettings) => void> = []
constructor(app: App, plugin: Plugin, settings: SQLSealSettings) {
super(app, plugin);
this.plugin = plugin;
this.settings = settings;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'CSV File Viewer' });
new Setting(containerEl)
.setName('Enable CSV Viewer')
.setDesc('Enables CSV files in your vault and adds ability to display them in a grid.')
.addToggle(toggle => toggle
.setValue(this.settings.enableViewer)
.onChange(async (value) => {
this.settings.enableViewer = value;
if (!value) {
this.settings.enableEditing = false;
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
new Setting(containerEl)
.setName('Enable CSV Editing')
.setDesc('Enables Editing functions in the CSV Viewer. This will add buttons to add columns, remove individual rows and columns; and edit each entry.')
.setDisabled(!this.settings.enableViewer)
.addToggle(toggle => toggle
.setValue(this.settings.enableEditing)
.onChange(async (value) => {
this.settings.enableEditing = value;
await this.plugin.saveData(this.settings);
this.callChanges()
}));
containerEl.createEl('h3', { text: 'JSON File Viewer' });
new Setting(containerEl)
.setName('Enable JSON Viewer')
.setDesc('Enables JSON and JSON5 files in your files explorer and allows to preview them.')
.addToggle(toggle => toggle
.setValue(this.settings.enableJSONViewer)
.onChange(async (value) => {
this.settings.enableJSONViewer = value;
if (!value) {
this.settings.enableJSONViewer = false;
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
containerEl.createEl('h3', { text: 'Behavior' });
new Setting(containerEl)
.setName('Enable Dynamic Updates')
.setDesc('SQLSeal will refresh your tables when underlying data changes.')
.addToggle(toggle => toggle
.setValue(this.settings.enableDynamicUpdates)
.onChange(async (value) => {
this.settings.enableDynamicUpdates = value;
if (!value) {
this.settings.enableDynamicUpdates = false;
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
new Setting(containerEl)
.setName('Enable Syntax Highlighting')
.setDesc('Syntax will get highlighted when editing SQLSeal code')
.addToggle(toggle => toggle
.setValue(this.settings.enableSyntaxHighlighting)
.onChange(async (value) => {
this.settings.enableSyntaxHighlighting = value;
if (!value) {
this.settings.enableSyntaxHighlighting = false;
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
containerEl.createEl('h3', { text: 'Views' });
new Setting(containerEl)
.setName('Default View')
.setDesc('This view will be used by default when you don\'t provide any view definition in your query')
.addDropdown(dropdown => dropdown
.addOption('grid', 'Grid')
.addOption('html', 'HTML Table')
.addOption('markdown', 'Markdown Table')
.setValue(this.settings.defaultView)
.onChange(async (value) => {
this.settings.defaultView = value as 'grid' | 'html' | 'markdown';
if (!value) {
this.settings.defaultView = DEFAULT_SETTINGS.defaultView
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
containerEl.createEl('h4', { text: 'Grid View' });
new Setting(containerEl)
.setName('Items per page ')
.setDesc('How many items should display for each page of the Grid view')
.addDropdown(dropdown => dropdown
.addOption('20', '20')
.addOption('50', '50')
.addOption('100', '100')
.setValue(this.settings.gridItemsPerPage + '')
.onChange(async (value) => {
this.settings.gridItemsPerPage = parseInt(value, 10);
if (!value) {
this.settings.gridItemsPerPage = DEFAULT_SETTINGS.gridItemsPerPage
}
await this.plugin.saveData(this.settings);
this.display();
this.callChanges()
}));
}
private callChanges() {
this.onChangeFns.forEach(f => f(this.settings))
}
onChange(fn: (settings: SQLSealSettings) => void) {
this.onChangeFns.push(fn)
}
}

View file

@ -1,50 +0,0 @@
import { SqlSealDatabase } from "./database/database";
import { App, Plugin } from "obsidian";
import { SqlSealCodeblockHandler } from "./codeblockHandler/SqlSealCodeblockHandler";
import { Logger } from "./utils/logger";
import { RendererRegistry } from "./renderer/rendererRegistry";
import { Sync } from "./datamodel/sync";
import { SqlSealInlineHandler } from "./codeblockHandler/inline/InlineCodeHandler";
import { SealFileSync } from "./vaultSync/SealFileSync";
import { FilesFileSyncTable } from "./vaultSync/tables/filesTable";
import { TagsFileSyncTable } from "./vaultSync/tables/tagsTable";
import { TasksFileSyncTable } from "./vaultSync/tables/tasksTable";
import { LinksFileSyncTable } from "./vaultSync/tables/linksTable";
import SqlSealPlugin from "./main";
export class SqlSeal {
public db: SqlSealDatabase
private codeBlockHandler: SqlSealCodeblockHandler
private inlineCodeBlock: SqlSealInlineHandler
public sync: Sync
public fileSync: SealFileSync
constructor(private readonly app: App, verbose = false, rendererRegistry: RendererRegistry, plugin: SqlSealPlugin) {
this.db = new SqlSealDatabase(app, verbose)
const logger = new Logger(verbose)
this.sync = new Sync(this.db, this.app.vault, this.app)
this.sync.init()
this.codeBlockHandler = new SqlSealCodeblockHandler(app, this.db, plugin, this.sync, rendererRegistry)
this.inlineCodeBlock = new SqlSealInlineHandler(app, this.db, plugin, this.sync)
}
getHandler() {
return this.codeBlockHandler.getHandler()
}
getInlineHandler() {
return this.inlineCodeBlock.getHandler()
}
async startFileSync(plugin: Plugin) {
this.fileSync = new SealFileSync(this.app, plugin, (name) => this.sync.triggerGlobalTableChange(name))
this.fileSync.addTablePlugin(new FilesFileSyncTable(this.db, this.app, plugin))
this.fileSync.addTablePlugin(new TagsFileSyncTable(this.db, this.app))
this.fileSync.addTablePlugin(new TasksFileSyncTable(this.db, this.app))
this.fileSync.addTablePlugin(new LinksFileSyncTable(this.db, this.app))
await this.fileSync.init()
}
}

4
src/styles/canvas.scss Normal file
View file

@ -0,0 +1,4 @@
// Canvas specific styles
.sqlseal-renderer-on-canvas {
}

View file

@ -4,4 +4,6 @@
@use 'listView';
@use 'syntaxHighlight';
@use 'obsidianMinimal';
@use 'agGrid';
@use 'agGrid';
@use 'settings';
@use 'canvas';

7
src/styles/settings.scss Normal file
View file

@ -0,0 +1,7 @@
.sqlseal-settings-warn {
background: rgb(255, 166, 0);
padding: 16px;
border: 2px solid rgb(134, 87, 0);
border-radius: 5px;
margin: 0.5em 2em;
}

View file

@ -1,435 +0,0 @@
import { WorkspaceLeaf, TextFileView, Menu, Notice, MenuItem } from 'obsidian';
import { parse, unparse } from 'papaparse';
import { DeleteConfirmationModal } from '../modal/deleteConfirmationModal';
import { RenameColumnModal } from '../modal/renameColumnModal';
import { CodeSampleModal } from '../modal/showCodeSample';
import { GridRenderer, GridRendererCommunicator } from '../renderer/GridRenderer';
import { errorNotice } from '../utils/notice';
import { ModernCellParser } from '../cellParser/ModernCellParser';
import { ConfigObject, loadConfig, saveConfig } from 'src/utils/csvConfig';
import { ColumnType } from '../utils/types';
const delay = (n: number) => new Promise(resolve => setTimeout(resolve, n))
export const CSV_VIEW_TYPE = "csv-viewer" as const;
export const CSV_VIEW_EXTENSIONS = ['csv'];
export class CSVView extends TextFileView {
private content: string;
private table: HTMLTableElement;
private config: ConfigObject;
constructor(
leaf: WorkspaceLeaf,
private enableEditing: boolean,
private cellParser: ModernCellParser
) {
super(leaf);
}
getViewType(): string {
return CSV_VIEW_TYPE;
}
getDisplayText(): string {
return this.file?.basename || 'CSV Viewer';
}
async onload(): Promise<void> {
super.onload();
this.contentEl.addClass('csv-viewer-container');
}
async onOpen() {
this.renderCSV();
}
async onClose() {
// Cleanup if needed
}
async setViewData(data: string, clear: boolean): Promise<void> {
this.content = data;
await this.renderCSV();
}
getViewData(): string {
return this.content;
}
clear(): void {
this.content = '';
this.table.empty();
}
private result: any;
updateRow(newRowData: Record<string, any>) {
this.result.data[parseInt(newRowData.__index, 10)] = newRowData;
this.saveData(true)
}
deleteRow(idx: number) {
this.result.data.splice(idx, 1)
this.saveData()
}
deleteColumn(column: string) {
this.result.fields = this.result.fields.filter((c: string) => c !== column)
this.saveData()
}
addNewColumn(columnName: string) {
if (this.result.fields.indexOf(columnName) > -1) {
throw new Error('Column already exists')
}
this.result.fields.push(columnName)
this.result.data = this.result.data.map((d: any) => ({ [columnName]: '', ...d }))
this.saveData()
}
renameColumn(oldName: string, newName: string) {
if (!newName) {
return
}
if (this.result.fields.contains(newName)) {
errorNotice('Column already exists')
return
}
const oldNameIdx = this.result.fields.indexOf(oldName)
if (oldNameIdx < 0) {
errorNotice('Old column does not exist')
return
}
this.result.fields[oldNameIdx] = newName
this.result.data = this.result.data.map((d: Record<string, unknown>) => {
d[newName] = d[oldName]
delete d[oldName]
return d
})
this.saveData()
this.loadDataIntoGrid()
}
setIsEditable(newValue: boolean) {
this.enableEditing = newValue
// FIXME: if there already rendered view, use this to rerender it?
}
saveData(noRefresh: boolean = false) {
if (noRefresh) {
this.refreshSkip = Date.now() + 500
}
// Map results
const res = [...this.result.data].map(r => {
const r2 = {...r}
Object.keys(r).forEach(k => {
if (typeof r[k] === 'boolean') {
r2[k] = r[k] ? 1 : 0
}
})
return r2
})
const output = unparse({ ...this.result, data: res })
this.app.vault.modify(this.file!, output)
this.refreshTypes()
}
createRow() {
this.result.data.push(this.result.fields.reduce((acc: Record<string, any>, f: string) => ({ ...acc, [f]: '' }), {}))
this.saveData()
}
getColumnType(columnName: string) {
if (this.config.columnDefinitions[columnName]) {
return this.config.columnDefinitions[columnName].type
}
return 'auto'
}
async changeColumnType(columnName: string, type: ColumnType) {
const prev = this.config.columnDefinitions[columnName] ?? {}
this.config.columnDefinitions[columnName] = {
...prev,
type: type
}
await this.saveConfig()
}
async loadConfig() {
while (!this.file) {
await delay(100)
}
this.config = await loadConfig(this.file, this.app.vault)
}
async saveConfig() {
await saveConfig(this.file!, this.config, this.app.vault)
}
private getColumnConfigurations(columns: string[]) {
return columns.map(f => {
if (!this.config) {
return {
field: f
}
}
const def = this.config.columnDefinitions[f]?.type
if (!def || def === 'auto') {
return { field: f }
}
if (def === 'date') {
return {
field: f,
cellDataType: 'dateString'
}
}
return {
field: f,
cellDataType: def
}
})
}
refreshTypes() {
if (this.gridCommunicator) {
const columns = this.result.fields
if (columns && columns.length) {
this.gridCommunicator.gridApi.setGridOption('columnDefs', this.getColumnConfigurations(columns))
}
}
}
moveColumn(name: string, toIndex: number) {
let fields = this.result.fields as Array<string>
fields = fields.filter(f => f !== name)
fields = [
...fields.slice(0, toIndex),
name,
...fields.slice(toIndex)
]
this.result.fields = fields
this.saveData()
}
api: any = null;
gridCommunicator: GridRendererCommunicator | null = null
refreshSkip: number = 0
formatWithTypes(d: Record<string, string | boolean | number>) {
Object.entries(this.config.columnDefinitions).forEach(([key, value]) => {
if (!d[key]) {
return
}
if (value.type === 'boolean') {
if (d[key] === 'false' || d[key] === '0') {
d[key] = false
}
d[key] = !!d[key]
}
if (value.type === 'number') {
if (d[key] === null || d[key] === '') {
d[key] = ''
} else {
d[key] = parseFloat(d[key] as string)
}
}
if (value.type === 'date') {
// try parsing
const val = d[key] as string
const date = new Date(val)
d[key] = date.toISOString().split('T')[0]
}
})
return d
}
prepareData() {
const result = parse(this.content, {
header: true,
skipEmptyLines: true,
});
const data = result.data.map((d: any, i) => ({
...this.formatWithTypes(d),
__index: i.toString()
}))
return {
data: data,
fields: result.meta.fields
}
}
loadDataIntoGrid() {
if (this.refreshSkip > Date.now()) {
return
}
requestAnimationFrame(() => {
const result = this.prepareData()
this.result = result
this.refreshTypes()
this.api!.render(result)
})
}
isLoading: boolean = false
private async renderCSV() {
if (this.isLoading) {
if (this.api) {
this.loadDataIntoGrid()
}
return
}
this.isLoading = true
this.contentEl.empty()
const csvEditorDiv = this.contentEl.createDiv({ cls: 'sql-seal-csv-editor' })
const buttonsRow = csvEditorDiv.createDiv({ cls: 'sql-seal-csv-viewer-buttons' })
await this.loadConfig()
if (this.enableEditing) {
const createColumn = buttonsRow.createEl('button', { text: 'Add Column' })
const createRow = buttonsRow.createEl('button', { text: 'Add Row' })
createColumn.addEventListener('click', e => {
e.preventDefault()
const modal = new RenameColumnModal(this.app, (res) => {
this.addNewColumn(res)
})
modal.open()
})
createRow.addEventListener('click', e => {
e.preventDefault()
this.createRow()
})
}
const generateSqlCode = buttonsRow.createEl('button', { text: 'Generate SQLSeal code' })
const gridEl = csvEditorDiv.createDiv({ cls: 'sql-seal-csv-viewer' })
generateSqlCode.addEventListener('click', e => {
e.preventDefault()
if (!this.file) {
return
}
const modal = new CodeSampleModal(this.app, this.file)
modal.open()
})
const grid = new GridRenderer(this.app, null)
const csvView = this;
const data = this.prepareData()
this.result = data
const api = grid.render({
columnDefs: this.getColumnConfigurations(data.fields ?? []),
defaultColDef: {
editable: this.enableEditing,
headerComponentParams: {
enableMenu: this.enableEditing,
showColumnMenu: function (e: any) {
const menu = new Menu()
menu.addItem(item => {
item.setTitle('Rename Column')
item.onClick(() => {
const modal = new RenameColumnModal(csvView.app, (res) => {
csvView.renameColumn(
this.column.userProvidedColDef.field, res)
})
modal.open()
})
})
menu.addItem(item => {
item.setTitle('Delete Column')
item.onClick(() => {
const colName = this.column.userProvidedColDef.field
const modal = new DeleteConfirmationModal(csvView.app, `column ${colName}`, () => {
csvView.deleteColumn(colName)
})
modal.open()
})
})
// FIXME: rework it to submenus.
menu.addSeparator()
menu.addItem(item => {
// item.setDisabled(true)
item.setTitle('Data Type')
// item.setIsLabel(true)
const ipfSubmenu = (item as any).setSubmenu();
const types = ['auto', 'text', 'number', 'boolean', 'date'] as ColumnType[]
const colName = this.column.userProvidedColDef.field
const current = csvView.getColumnType(colName)
types.forEach(type => {
ipfSubmenu.addItem((subItem: MenuItem) => {
const checkbox = type === current ? '✓ ' : ''
subItem.setTitle(checkbox + type)
subItem.onClick(() => {
csvView.changeColumnType(colName, type)
csvView.refreshTypes()
csvView.loadDataIntoGrid()
})
})
})
})
const pos = e.getBoundingClientRect();
menu.showAtPosition({ x: pos.x, y: pos.y + 20 })
}
}
},
enableCellTextSelection: false,
ensureDomOrder: false,
paginationAutoPageSize: true,
suppressMovableColumns: false,
onColumnMoved: (e) => {
if (!e.finished) {
return
}
const columnName = e.column?.getUserProvidedColDef()
if (!columnName) {
return
}
csvView.moveColumn(columnName?.field!, e.toIndex!)
},
domLayout: 'normal',
getRowId: (p) => p.data.__index,
onCellValueChanged: (event) => {
if (event.rowIndex === null) {
return;
}
this.updateRow(event.data)
},
onCellContextMenu: (e) => {
if (!this.enableEditing) {
return
}
const menu = new Menu()
menu.addItem(item => {
item.setTitle('Delete Row')
item.onClick(() => {
const modal = new DeleteConfirmationModal(csvView.app, `row`, () => {
csvView.deleteRow(e.data.__index)
})
modal.open()
})
})
menu.showAtMouseEvent(e.event as any)
}
}, gridEl, { sourcePath: this.file?.path || '' })
this.api = api;
this.gridCommunicator = api.communicator
api.render(data)
}
}

View file

@ -1,159 +0,0 @@
import { ItemView, TFile, WorkspaceLeaf } from 'obsidian';
import SqlSealPlugin from '../main';
export const SIDEBAR_VIEW_TYPE = 'sql-seal-sidebar-view';
export class SidebarView extends ItemView {
private currentFile: TFile | null = null;
public contentEl: HTMLElement;
private plugin: SqlSealPlugin;
constructor(leaf: WorkspaceLeaf, plugin: SqlSealPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return SIDEBAR_VIEW_TYPE;
}
getDisplayText(): string {
return 'SQL Seal Sidebar';
}
async onload(): Promise<void> {
super.onload();
// Create container for the sidebar content
this.contentEl = this.containerEl.createDiv('sql-seal-sidebar-container');
// Register event handler for file changes
this.registerEvent(
this.app.workspace.on('file-open', (file: TFile | null) => {
this.currentFile = file;
this.updateView();
})
);
}
async updateView(): Promise<void> {
this.contentEl.empty();
if (!this.currentFile) {
this.contentEl.createEl('div', {
text: 'No file is currently open',
cls: 'sql-seal-sidebar-empty'
});
return;
}
try {
// Get file metadata or frontmatter
const fileCache = this.app.metadataCache.getFileCache(this.currentFile);
const frontmatter = fileCache?.frontmatter || {};
// Add file-specific properties to frontmatter
const enhancedFrontmatter = {
...frontmatter,
'@file': {
name: this.currentFile.name,
path: this.currentFile.path,
basename: this.currentFile.basename,
extension: this.currentFile.extension,
stat: {
ctime: this.currentFile.stat.ctime,
mtime: this.currentFile.stat.mtime,
size: this.currentFile.stat.size
}
},
'@tags': fileCache?.tags?.map(tag => tag.tag) || [],
'@links': fileCache?.links?.map(link => link.link) || [],
'@headings': fileCache?.headings?.map(h => ({
text: h.heading,
level: h.level
})) || []
};
// Check if there's a specific SQL query defined in frontmatter
const query = frontmatter?.['sql-seal-query'];
const view = frontmatter?.['sql-seal-view'] || 'table'; // Default to table view
if (query) {
const result = await this.plugin.sqlSeal.db.select(query, enhancedFrontmatter);
this.renderQueryResults(result.data, view);
} else {
this.contentEl.createEl('div', {
text: 'No SQL query defined for this file',
cls: 'sql-seal-sidebar-empty'
});
}
} catch (error) {
this.contentEl.createEl('div', {
text: `Error: ${error.message}`,
cls: 'sql-seal-sidebar-error'
});
}
}
private renderQueryResults(results: any[], view: string): void {
if (!results || results.length === 0) {
this.contentEl.createEl('div', {
text: 'No results found',
cls: 'sql-seal-sidebar-empty'
});
return;
}
if (view === 'list') {
this.renderListView(results);
} else {
this.renderTableView(results);
}
}
private renderTableView(results: any[]): void {
const table = this.contentEl.createEl('table', {
cls: 'sql-seal-sidebar-table'
});
// Create header row
const headerRow = table.createEl('tr');
Object.keys(results[0]).forEach(key => {
headerRow.createEl('th', { text: key });
});
// Create data rows
results.forEach(row => {
const tr = table.createEl('tr');
Object.values(row).forEach(value => {
tr.createEl('td', { text: String(value) });
});
});
}
private renderListView(results: any[]): void {
const list = this.contentEl.createEl('ul', {
cls: 'sql-seal-sidebar-list'
});
results.forEach(row => {
const li = list.createEl('li');
Object.entries(row).forEach(([key, value]) => {
const span = li.createEl('span', {
cls: 'sql-seal-sidebar-list-item'
});
span.createEl('span', {
text: key,
cls: 'sql-seal-sidebar-list-key'
});
span.createEl('span', {
text: ': '
});
span.createEl('span', {
text: String(value),
cls: 'sql-seal-sidebar-list-value'
});
});
});
}
}

Some files were not shown because too many files have changed in this diff Show more