From 285684dfc4ed87c3fa48a8189b58592e489c38ae Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 10 Aug 2025 10:34:54 +0200 Subject: [PATCH 1/4] 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 --- .changeset/mean-paths-fold.md | 5 + .changeset/rare-sloths-train.md | 5 + .changeset/twenty-colts-roll.md | 5 + esbuild.config.mjs | 2 +- package.json | 2 + pnpm-lock.yaml | 510 +++++++++++++++++- src/cellParser/RenderContext.ts | 62 --- src/cellParser/factory.ts | 13 - src/codeblockHandler/CodeblockProcessor.ts | 144 ----- src/editorExtension/inlineCodeBlock.ts | 125 ----- src/main.ts | 265 +-------- src/modules/api/init.ts | 55 ++ src/modules/api/module.ts | 21 + src/modules/api/pluginApi/sqlSealApi.ts | 134 +++++ .../api/pluginApi}/table.ts | 8 +- src/modules/contextMenu/init.ts | 68 +++ src/modules/contextMenu/module.ts | 16 + src/{ => modules}/database/database.ts | 40 +- src/modules/database/factory.ts | 16 + src/modules/database/module.ts | 13 + src/{ => modules}/database/worker/database.ts | 8 +- src/modules/debug/DityGraphView.ts | 43 ++ src/modules/debug/dityGraph.ts | 39 ++ src/modules/debug/module.ts | 16 + .../codeblockHandler/CodeblockProcessor.ts | 210 ++++++++ .../SqlSealCodeblockHandler.ts | 21 +- .../inline/InlineCodeHandler.ts | 18 +- .../inline/InlineProcessor.ts | 19 +- src/modules/editor/init.ts | 85 +++ src/modules/editor/module.ts | 30 ++ .../editor}/parser.test.ts | 0 src/{grammar => modules/editor}/parser.ts | 2 +- .../editor}/renderer/GridRenderer.ts | 19 +- .../editor}/renderer/ListRenderer.ts | 6 +- .../editor}/renderer/MarkdownRenderer.ts | 8 +- .../editor}/renderer/TableRenderer.ts | 6 +- .../editor}/renderer/TemplateRenderer.ts | 6 +- .../editor}/renderer/rendererRegistry.ts | 4 +- .../editor}/sql/sqlTransformer.ts | 0 .../editor}/sql/transformer.test.ts | 0 src/modules/main/init.ts | 36 ++ src/modules/main/module.ts | 82 +++ src/modules/settings/SQLSealSettingsTab.ts | 124 +++++ src/modules/settings/Settings.ts | 25 + src/modules/settings/init.ts | 32 ++ .../modal/deleteConfirmationModal.ts | 0 .../settings}/modal/renameColumnModal.ts | 0 .../settings}/modal/showCodeSample.ts | 2 +- src/modules/settings/module.ts | 22 + src/modules/settings/settingsFactory.ts | 19 + .../settingsTabSection/SettingsCSVControls.ts | 89 +++ .../settingsTabSection/SettingsControls.ts | 12 + .../SettingsJsonControls.ts | 71 +++ src/modules/settings/utils/viewInspector.ts | 12 + src/modules/settings/view/CSVView.ts | 465 ++++++++++++++++ src/{ => modules/settings}/view/JsonView.ts | 0 .../sync/fileSyncController/FileSync.ts} | 2 +- .../fileSyncController/fileSyncFactory.ts | 29 + src/modules/sync/module.ts | 22 + .../sync}/repository/abstractRepository.ts | 0 .../sync}/repository/configuration.ts | 2 +- .../sync}/repository/tableAliases.ts | 14 +- .../sync}/repository/tableDefinitions.ts | 12 +- src/modules/sync/sync/init.ts | 17 + src/{datamodel => modules/sync/sync}/sync.ts | 16 +- src/modules/sync/sync/syncFactory.ts | 19 + .../sync}/tables/abstractFileSyncTable.ts | 2 +- .../sync/sync}/tables/filesTable.ts | 10 +- .../sync/sync}/tables/linksTable.ts | 0 .../sync/sync}/tables/tagsTable.ts | 0 .../sync/sync}/tables/tasksTable.ts | 0 .../sync}/syncStrategy/CsvFileSyncStrategy.ts | 6 +- .../syncStrategy/JSONFileSyncStrategy.ts | 2 +- .../syncStrategy/MarkdownTableSyncStrategy.ts | 4 +- .../sync}/syncStrategy/SyncStrategyFactory.ts | 2 +- .../syncStrategy/abstractSyncStrategy.ts | 2 +- .../sync}/syncStrategy/types.ts | 0 .../cellParser/CellFunction.ts | 0 .../cellParser/ModernCellParser.ts | 4 +- .../syntaxHighlight/cellParser/factory.ts | 32 ++ .../cellParser/parseResults.ts | 0 .../cellParser/parser/checkbox.ts | 0 .../cellParser/parser/image.ts | 2 +- .../cellParser/parser/link.ts | 4 +- .../editorExtension/inlineCodeBlock.ts | 143 +++++ .../editorExtension/syntaxHighlight.ts | 4 +- .../editorExtension/widgets/FilePathWidget.ts | 2 +- .../highlighter/handlebarsHighlighter.ts | 0 .../grammar/highlighter/jsHighlighter.ts | 0 .../grammar/highlighterOperation.ts | 0 src/modules/syntaxHighlight/init.ts | 23 + src/modules/syntaxHighlight/module.ts | 23 + src/pluginApi/sqlSealApi.ts | 93 ---- src/settings/SQLSealSettingsTab.ts | 162 ------ src/sqlSeal.ts | 50 -- src/styles/canvas.scss | 4 + src/styles/main.scss | 4 +- src/styles/settings.scss | 7 + src/view/CSVView.ts | 435 --------------- src/view/SidebarView.ts | 159 ------ styles.css | 2 +- tsconfig.json | 3 +- types-package/src/index.ts | 4 +- 103 files changed, 2722 insertions(+), 1639 deletions(-) create mode 100644 .changeset/mean-paths-fold.md create mode 100644 .changeset/rare-sloths-train.md create mode 100644 .changeset/twenty-colts-roll.md delete mode 100644 src/cellParser/RenderContext.ts delete mode 100644 src/cellParser/factory.ts delete mode 100644 src/codeblockHandler/CodeblockProcessor.ts delete mode 100644 src/editorExtension/inlineCodeBlock.ts create mode 100644 src/modules/api/init.ts create mode 100644 src/modules/api/module.ts create mode 100644 src/modules/api/pluginApi/sqlSealApi.ts rename src/{database => modules/api/pluginApi}/table.ts (88%) create mode 100644 src/modules/contextMenu/init.ts create mode 100644 src/modules/contextMenu/module.ts rename src/{ => modules}/database/database.ts (73%) create mode 100644 src/modules/database/factory.ts create mode 100644 src/modules/database/module.ts rename src/{ => modules}/database/worker/database.ts (96%) create mode 100644 src/modules/debug/DityGraphView.ts create mode 100644 src/modules/debug/dityGraph.ts create mode 100644 src/modules/debug/module.ts create mode 100644 src/modules/editor/codeblockHandler/CodeblockProcessor.ts rename src/{ => modules/editor}/codeblockHandler/SqlSealCodeblockHandler.ts (54%) rename src/{ => modules/editor}/codeblockHandler/inline/InlineCodeHandler.ts (61%) rename src/{ => modules/editor}/codeblockHandler/inline/InlineProcessor.ts (80%) create mode 100644 src/modules/editor/init.ts create mode 100644 src/modules/editor/module.ts rename src/{grammar => modules/editor}/parser.test.ts (100%) rename src/{grammar => modules/editor}/parser.ts (99%) rename src/{ => modules/editor}/renderer/GridRenderer.ts (91%) rename src/{ => modules/editor}/renderer/ListRenderer.ts (92%) rename src/{ => modules/editor}/renderer/MarkdownRenderer.ts (85%) rename src/{ => modules/editor}/renderer/TableRenderer.ts (94%) rename src/{ => modules/editor}/renderer/TemplateRenderer.ts (90%) rename src/{ => modules/editor}/renderer/rendererRegistry.ts (95%) rename src/{ => modules/editor}/sql/sqlTransformer.ts (100%) rename src/{ => modules/editor}/sql/transformer.test.ts (100%) create mode 100644 src/modules/main/init.ts create mode 100644 src/modules/main/module.ts create mode 100644 src/modules/settings/SQLSealSettingsTab.ts create mode 100644 src/modules/settings/Settings.ts create mode 100644 src/modules/settings/init.ts rename src/{ => modules/settings}/modal/deleteConfirmationModal.ts (100%) rename src/{ => modules/settings}/modal/renameColumnModal.ts (100%) rename src/{ => modules/settings}/modal/showCodeSample.ts (95%) create mode 100644 src/modules/settings/module.ts create mode 100644 src/modules/settings/settingsFactory.ts create mode 100644 src/modules/settings/settingsTabSection/SettingsCSVControls.ts create mode 100644 src/modules/settings/settingsTabSection/SettingsControls.ts create mode 100644 src/modules/settings/settingsTabSection/SettingsJsonControls.ts create mode 100644 src/modules/settings/utils/viewInspector.ts create mode 100644 src/modules/settings/view/CSVView.ts rename src/{ => modules/settings}/view/JsonView.ts (100%) rename src/{vaultSync/SealFileSync.ts => modules/sync/fileSyncController/FileSync.ts} (97%) create mode 100644 src/modules/sync/fileSyncController/fileSyncFactory.ts create mode 100644 src/modules/sync/module.ts rename src/{datamodel => modules/sync}/repository/abstractRepository.ts (100%) rename src/{datamodel => modules/sync}/repository/configuration.ts (87%) rename src/{datamodel => modules/sync}/repository/tableAliases.ts (84%) rename src/{datamodel => modules/sync}/repository/tableDefinitions.ts (91%) create mode 100644 src/modules/sync/sync/init.ts rename src/{datamodel => modules/sync/sync}/sync.ts (91%) create mode 100644 src/modules/sync/sync/syncFactory.ts rename src/{vaultSync => modules/sync/sync}/tables/abstractFileSyncTable.ts (90%) rename src/{vaultSync => modules/sync/sync}/tables/filesTable.ts (89%) rename src/{vaultSync => modules/sync/sync}/tables/linksTable.ts (100%) rename src/{vaultSync => modules/sync/sync}/tables/tagsTable.ts (100%) rename src/{vaultSync => modules/sync/sync}/tables/tasksTable.ts (100%) rename src/{datamodel => modules/sync}/syncStrategy/CsvFileSyncStrategy.ts (93%) rename src/{datamodel => modules/sync}/syncStrategy/JSONFileSyncStrategy.ts (97%) rename src/{datamodel => modules/sync}/syncStrategy/MarkdownTableSyncStrategy.ts (98%) rename src/{datamodel => modules/sync}/syncStrategy/SyncStrategyFactory.ts (95%) rename src/{datamodel => modules/sync}/syncStrategy/abstractSyncStrategy.ts (91%) rename src/{datamodel => modules/sync}/syncStrategy/types.ts (100%) rename src/{ => modules/syntaxHighlight}/cellParser/CellFunction.ts (100%) rename src/{ => modules/syntaxHighlight}/cellParser/ModernCellParser.ts (98%) create mode 100644 src/modules/syntaxHighlight/cellParser/factory.ts rename src/{ => modules/syntaxHighlight}/cellParser/parseResults.ts (100%) rename src/{ => modules/syntaxHighlight}/cellParser/parser/checkbox.ts (100%) rename src/{ => modules/syntaxHighlight}/cellParser/parser/image.ts (96%) rename src/{ => modules/syntaxHighlight}/cellParser/parser/link.ts (97%) create mode 100644 src/modules/syntaxHighlight/editorExtension/inlineCodeBlock.ts rename src/{ => modules/syntaxHighlight}/editorExtension/syntaxHighlight.ts (96%) rename src/{ => modules/syntaxHighlight}/editorExtension/widgets/FilePathWidget.ts (90%) rename src/{ => modules/syntaxHighlight}/grammar/highlighter/handlebarsHighlighter.ts (100%) rename src/{ => modules/syntaxHighlight}/grammar/highlighter/jsHighlighter.ts (100%) rename src/{ => modules/syntaxHighlight}/grammar/highlighterOperation.ts (100%) create mode 100644 src/modules/syntaxHighlight/init.ts create mode 100644 src/modules/syntaxHighlight/module.ts delete mode 100644 src/pluginApi/sqlSealApi.ts delete mode 100644 src/settings/SQLSealSettingsTab.ts delete mode 100644 src/sqlSeal.ts create mode 100644 src/styles/canvas.scss create mode 100644 src/styles/settings.scss delete mode 100644 src/view/CSVView.ts delete mode 100644 src/view/SidebarView.ts diff --git a/.changeset/mean-paths-fold.md b/.changeset/mean-paths-fold.md new file mode 100644 index 0000000..000454e --- /dev/null +++ b/.changeset/mean-paths-fold.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +tables and charts now render better on canvas and work when you use external files too diff --git a/.changeset/rare-sloths-train.md b/.changeset/rare-sloths-train.md new file mode 100644 index 0000000..5e5f9f2 --- /dev/null +++ b/.changeset/rare-sloths-train.md @@ -0,0 +1,5 @@ +--- +"sqlseal": patch +--- + +csv and json views are now only registered when not colliding with other existing plugins diff --git a/.changeset/twenty-colts-roll.md b/.changeset/twenty-colts-roll.md new file mode 100644 index 0000000..d354490 --- /dev/null +++ b/.changeset/twenty-colts-roll.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +reworking plugin internals to organise code into modules diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a73226b..9402e78 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -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', diff --git a/package.json b/package.json index 5c703a3..5e8e197 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19f8d0a..e0226d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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' diff --git a/src/cellParser/RenderContext.ts b/src/cellParser/RenderContext.ts deleted file mode 100644 index 43bf8e5..0000000 --- a/src/cellParser/RenderContext.ts +++ /dev/null @@ -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)[] = [] - 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 = [] - } -} \ No newline at end of file diff --git a/src/cellParser/factory.ts b/src/cellParser/factory.ts deleted file mode 100644 index 7a2e869..0000000 --- a/src/cellParser/factory.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/src/codeblockHandler/CodeblockProcessor.ts b/src/codeblockHandler/CodeblockProcessor.ts deleted file mode 100644 index 8179d10..0000000 --- a/src/codeblockHandler/CodeblockProcessor.ts +++ /dev/null @@ -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 - }))) - } -} \ No newline at end of file diff --git a/src/editorExtension/inlineCodeBlock.ts b/src/editorExtension/inlineCodeBlock.ts deleted file mode 100644 index a2fe96f..0000000 --- a/src/editorExtension/inlineCodeBlock.ts +++ /dev/null @@ -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(); - 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; - } -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 3718839..2b6009c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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(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() {} } - diff --git a/src/modules/api/init.ts b/src/modules/api/init.ts new file mode 100644 index 0000000..8de5362 --- /dev/null +++ b/src/modules/api/init.ts @@ -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()([ + "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] = []; + }; + } +} diff --git a/src/modules/api/module.ts b/src/modules/api/module.ts new file mode 100644 index 0000000..1136a3b --- /dev/null +++ b/src/modules/api/module.ts @@ -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 \ No newline at end of file diff --git a/src/modules/api/pluginApi/sqlSealApi.ts b/src/modules/api/pluginApi/sqlSealApi.ts new file mode 100644 index 0000000..0c0055a --- /dev/null +++ b/src/modules/api/pluginApi/sqlSealApi.ts @@ -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 = [] + 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 = [] + private functions: Array = [] + private flags: Array = [] + + 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(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) + } + } +} diff --git a/src/database/table.ts b/src/modules/api/pluginApi/table.ts similarity index 88% rename from src/database/table.ts rename to src/modules/api/pluginApi/table.ts index ac09ca5..83bc9aa 100644 --- a/src/database/table.ts +++ b/src/modules/api/pluginApi/table.ts @@ -1,4 +1,4 @@ -import { SqlSealDatabase } from "./database"; +import { SqlSealDatabase } from "../../database/database" type Item = Record @@ -14,11 +14,17 @@ export class DatabaseTable { async getAll() { const data = await this.db.select(`SELECT * FROM ${this.tableName}`, {}) + if (!data) { + return [] + } return data.data as Item[] } 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[] } diff --git a/src/modules/contextMenu/init.ts b/src/modules/contextMenu/init.ts new file mode 100644 index 0000000..0873e32 --- /dev/null +++ b/src/modules/contextMenu/init.ts @@ -0,0 +1,68 @@ +import { makeInjector } from "@hypersphere/dity" +import { ContextMenuModule } from "./module" +import { App, Menu, Plugin, TAbstractFile, TFile, TFolder } from "obsidian" + +@(makeInjector()([ + '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) + ); + } + } +} \ No newline at end of file diff --git a/src/modules/contextMenu/module.ts b/src/modules/contextMenu/module.ts new file mode 100644 index 0000000..f44ef27 --- /dev/null +++ b/src/modules/contextMenu/module.ts @@ -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 diff --git a/src/database/database.ts b/src/modules/database/database.ts similarity index 73% rename from src/database/database.ts rename to src/modules/database/database.ts index 987b695..0c6af1b 100644 --- a/src/database/database.ts +++ b/src/modules/database/database.ts @@ -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 + db?: Comlink.Remote private isConnected = false - private connectingPromise: Promise; - constructor(private readonly app: App, private readonly verbose = false) { + private connectingPromise?: Promise; + 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>, key: string = 'id') { - return this.db.updateData(name, data, key) + return this.db?.updateData(name, data, key) } async deleteData(name: string, data: Array>, key: string = 'id') { - return this.db.deleteData(name, data, key) + return this.db?.deleteData(name, data, key) } async insertData(name: string, inData: Array>) { - 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) { - const result = await this.db.select(statement, frontmatter) + const result = await this.db?.select(statement, frontmatter) return result } async explain(statement: string, frontmatter: Record) { - const explainResults = await this.db.explainQuery(statement, frontmatter) + const explainResults = await this.db?.explainQuery(statement, frontmatter) let strResult = ''; const map = new Map() 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()([ + 'app' +])) +export class DatabaseFactory { + async make(app: App) { + const db = new SqlSealDatabase(app) + await db.connect() + return db + } +} \ No newline at end of file diff --git a/src/modules/database/module.ts b/src/modules/database/module.ts new file mode 100644 index 0000000..8b483ec --- /dev/null +++ b/src/modules/database/module.ts @@ -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 diff --git a/src/database/worker/database.ts b/src/modules/database/worker/database.ts similarity index 96% rename from src/database/worker/database.ts rename to src/modules/database/worker/database.ts index e5915d4..2475588 100644 --- a/src/database/worker/database.ts +++ b/src/modules/database/worker/database.ts @@ -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) { diff --git a/src/modules/debug/DityGraphView.ts b/src/modules/debug/DityGraphView.ts new file mode 100644 index 0000000..27e522c --- /dev/null +++ b/src/modules/debug/DityGraphView.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/modules/debug/dityGraph.ts b/src/modules/debug/dityGraph.ts new file mode 100644 index 0000000..f83f77a --- /dev/null +++ b/src/modules/debug/dityGraph.ts @@ -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()( + ['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) + + } + } +} \ No newline at end of file diff --git a/src/modules/debug/module.ts b/src/modules/debug/module.ts new file mode 100644 index 0000000..ed01a98 --- /dev/null +++ b/src/modules/debug/module.ts @@ -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 diff --git a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts new file mode 100644 index 0000000..dcbb8f3 --- /dev/null +++ b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts @@ -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, + }), + ), + ); + } +} diff --git a/src/codeblockHandler/SqlSealCodeblockHandler.ts b/src/modules/editor/codeblockHandler/SqlSealCodeblockHandler.ts similarity index 54% rename from src/codeblockHandler/SqlSealCodeblockHandler.ts rename to src/modules/editor/codeblockHandler/SqlSealCodeblockHandler.ts index 34eab8a..5dbf1a0 100644 --- a/src/codeblockHandler/SqlSealCodeblockHandler.ts +++ b/src/modules/editor/codeblockHandler/SqlSealCodeblockHandler.ts @@ -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()( + ['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 ) diff --git a/src/codeblockHandler/inline/InlineCodeHandler.ts b/src/modules/editor/codeblockHandler/inline/InlineCodeHandler.ts similarity index 61% rename from src/codeblockHandler/inline/InlineCodeHandler.ts rename to src/modules/editor/codeblockHandler/inline/InlineCodeHandler.ts index d33d245..7331f73 100644 --- a/src/codeblockHandler/inline/InlineCodeHandler.ts +++ b/src/modules/editor/codeblockHandler/inline/InlineCodeHandler.ts @@ -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()( + ['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 ); diff --git a/src/codeblockHandler/inline/InlineProcessor.ts b/src/modules/editor/codeblockHandler/inline/InlineProcessor.ts similarity index 80% rename from src/codeblockHandler/inline/InlineProcessor.ts rename to src/modules/editor/codeblockHandler/inline/InlineProcessor.ts index 1cef51f..5fab118 100644 --- a/src/codeblockHandler/inline/InlineProcessor.ts +++ b/src/modules/editor/codeblockHandler/inline/InlineProcessor.ts @@ -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]] ?? '' diff --git a/src/modules/editor/init.ts b/src/modules/editor/init.ts new file mode 100644 index 0000000..24d4c1b --- /dev/null +++ b/src/modules/editor/init.ts @@ -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()([ + '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 + } + } +} \ No newline at end of file diff --git a/src/modules/editor/module.ts b/src/modules/editor/module.ts new file mode 100644 index 0000000..981e8ab --- /dev/null +++ b/src/modules/editor/module.ts @@ -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 diff --git a/src/grammar/parser.test.ts b/src/modules/editor/parser.test.ts similarity index 100% rename from src/grammar/parser.test.ts rename to src/modules/editor/parser.test.ts diff --git a/src/grammar/parser.ts b/src/modules/editor/parser.ts similarity index 99% rename from src/grammar/parser.ts rename to src/modules/editor/parser.ts index 76640ab..9d5f08a 100644 --- a/src/grammar/parser.ts +++ b/src/modules/editor/parser.ts @@ -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, diff --git a/src/renderer/GridRenderer.ts b/src/modules/editor/renderer/GridRenderer.ts similarity index 91% rename from src/renderer/GridRenderer.ts rename to src/modules/editor/renderer/GridRenderer.ts index 77e8aca..30aa333 100644 --- a/src/renderer/GridRenderer.ts +++ b/src/modules/editor/renderer/GridRenderer.ts @@ -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[], @@ -35,7 +35,8 @@ export class GridRendererCommunicator { constructor( private el: HTMLElement, private config: Partial, - 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, 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) diff --git a/src/renderer/ListRenderer.ts b/src/modules/editor/renderer/ListRenderer.ts similarity index 92% rename from src/renderer/ListRenderer.ts rename to src/modules/editor/renderer/ListRenderer.ts index 4c72949..c41dc88 100644 --- a/src/renderer/ListRenderer.ts +++ b/src/modules/editor/renderer/ListRenderer.ts @@ -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[] diff --git a/src/renderer/MarkdownRenderer.ts b/src/modules/editor/renderer/MarkdownRenderer.ts similarity index 85% rename from src/renderer/MarkdownRenderer.ts rename to src/modules/editor/renderer/MarkdownRenderer.ts index 725a9c1..c735436 100644 --- a/src/renderer/MarkdownRenderer.ts +++ b/src/modules/editor/renderer/MarkdownRenderer.ts @@ -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[]) => { return data.map(d => columns.map(c => String(d[c]))) diff --git a/src/renderer/TableRenderer.ts b/src/modules/editor/renderer/TableRenderer.ts similarity index 94% rename from src/renderer/TableRenderer.ts rename to src/modules/editor/renderer/TableRenderer.ts index 05eb9c4..ee053e5 100644 --- a/src/renderer/TableRenderer.ts +++ b/src/modules/editor/renderer/TableRenderer.ts @@ -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[] diff --git a/src/renderer/TemplateRenderer.ts b/src/modules/editor/renderer/TemplateRenderer.ts similarity index 90% rename from src/renderer/TemplateRenderer.ts rename to src/modules/editor/renderer/TemplateRenderer.ts index 157b1ef..c3927a4 100644 --- a/src/renderer/TemplateRenderer.ts +++ b/src/modules/editor/renderer/TemplateRenderer.ts @@ -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 diff --git a/src/renderer/rendererRegistry.ts b/src/modules/editor/renderer/rendererRegistry.ts similarity index 95% rename from src/renderer/rendererRegistry.ts rename to src/modules/editor/renderer/rendererRegistry.ts index f853c8b..5691054 100644 --- a/src/renderer/rendererRegistry.ts +++ b/src/modules/editor/renderer/rendererRegistry.ts @@ -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[], diff --git a/src/sql/sqlTransformer.ts b/src/modules/editor/sql/sqlTransformer.ts similarity index 100% rename from src/sql/sqlTransformer.ts rename to src/modules/editor/sql/sqlTransformer.ts diff --git a/src/sql/transformer.test.ts b/src/modules/editor/sql/transformer.test.ts similarity index 100% rename from src/sql/transformer.test.ts rename to src/modules/editor/sql/transformer.test.ts diff --git a/src/modules/main/init.ts b/src/modules/main/init.ts new file mode 100644 index 0000000..65115c0 --- /dev/null +++ b/src/modules/main/init.ts @@ -0,0 +1,36 @@ +import { makeInjector } from "@hypersphere/dity"; +import { MainModule } from "./module"; + +type InitFn = () => void + + +@(makeInjector()([ + '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() + } + } +} \ No newline at end of file diff --git a/src/modules/main/module.ts b/src/modules/main/module.ts new file mode 100644 index 0000000..ac21979 --- /dev/null +++ b/src/modules/main/module.ts @@ -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 diff --git a/src/modules/settings/SQLSealSettingsTab.ts b/src/modules/settings/SQLSealSettingsTab.ts new file mode 100644 index 0000000..6cfee73 --- /dev/null +++ b/src/modules/settings/SQLSealSettingsTab.ts @@ -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()(['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) + } +} \ No newline at end of file diff --git a/src/modules/settings/Settings.ts b/src/modules/settings/Settings.ts new file mode 100644 index 0000000..72061a5 --- /dev/null +++ b/src/modules/settings/Settings.ts @@ -0,0 +1,25 @@ +import { args, BusBuilder, CallbackType } from "@hypersphere/omnibus"; +import { SQLSealSettings } from "./SQLSealSettingsTab"; + +export class Settings { + bus = BusBuilder + .init() + .register('change', args()) + .build() + + constructor(private settings: SQLSealSettings) { + } + + get(key: K): SQLSealSettings[K] { + return this.settings[key] + } + + set(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) + } +} \ No newline at end of file diff --git a/src/modules/settings/init.ts b/src/modules/settings/init.ts new file mode 100644 index 0000000..d961177 --- /dev/null +++ b/src/modules/settings/init.ts @@ -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()(["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()); + }); + }; + } +} diff --git a/src/modal/deleteConfirmationModal.ts b/src/modules/settings/modal/deleteConfirmationModal.ts similarity index 100% rename from src/modal/deleteConfirmationModal.ts rename to src/modules/settings/modal/deleteConfirmationModal.ts diff --git a/src/modal/renameColumnModal.ts b/src/modules/settings/modal/renameColumnModal.ts similarity index 100% rename from src/modal/renameColumnModal.ts rename to src/modules/settings/modal/renameColumnModal.ts diff --git a/src/modal/showCodeSample.ts b/src/modules/settings/modal/showCodeSample.ts similarity index 95% rename from src/modal/showCodeSample.ts rename to src/modules/settings/modal/showCodeSample.ts index c92bc3d..d51e1c5 100644 --- a/src/modal/showCodeSample.ts +++ b/src/modules/settings/modal/showCodeSample.ts @@ -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) { diff --git a/src/modules/settings/module.ts b/src/modules/settings/module.ts new file mode 100644 index 0000000..3980c52 --- /dev/null +++ b/src/modules/settings/module.ts @@ -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 \ No newline at end of file diff --git a/src/modules/settings/settingsFactory.ts b/src/modules/settings/settingsFactory.ts new file mode 100644 index 0000000..2d2872b --- /dev/null +++ b/src/modules/settings/settingsFactory.ts @@ -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()(['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 + } +} \ No newline at end of file diff --git a/src/modules/settings/settingsTabSection/SettingsCSVControls.ts b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts new file mode 100644 index 0000000..96dbe83 --- /dev/null +++ b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts @@ -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() + }), + ); + } +} diff --git a/src/modules/settings/settingsTabSection/SettingsControls.ts b/src/modules/settings/settingsTabSection/SettingsControls.ts new file mode 100644 index 0000000..f0a387e --- /dev/null +++ b/src/modules/settings/settingsTabSection/SettingsControls.ts @@ -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 +} diff --git a/src/modules/settings/settingsTabSection/SettingsJsonControls.ts b/src/modules/settings/settingsTabSection/SettingsJsonControls.ts new file mode 100644 index 0000000..b3965aa --- /dev/null +++ b/src/modules/settings/settingsTabSection/SettingsJsonControls.ts @@ -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(); + } + }), + ); + } +} diff --git a/src/modules/settings/utils/viewInspector.ts b/src/modules/settings/utils/viewInspector.ts new file mode 100644 index 0000000..13c58d1 --- /dev/null +++ b/src/modules/settings/utils/viewInspector.ts @@ -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 + } +}; diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts new file mode 100644 index 0000000..fc657e5 --- /dev/null +++ b/src/modules/settings/view/CSVView.ts @@ -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 { + super.onload(); + this.contentEl.addClass("csv-viewer-container"); + } + + async onOpen() { + this.renderCSV(); + } + + async onClose() { + // Cleanup if needed + } + + async setViewData(data: string, clear: boolean): Promise { + this.content = data; + await this.renderCSV(); + } + + getViewData(): string { + return this.content; + } + + clear(): void { + this.content = ""; + this.table.empty(); + } + + private result: any; + updateRow(newRowData: Record) { + 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) => { + 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, 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; + 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) { + 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); + } +} diff --git a/src/view/JsonView.ts b/src/modules/settings/view/JsonView.ts similarity index 100% rename from src/view/JsonView.ts rename to src/modules/settings/view/JsonView.ts diff --git a/src/vaultSync/SealFileSync.ts b/src/modules/sync/fileSyncController/FileSync.ts similarity index 97% rename from src/vaultSync/SealFileSync.ts rename to src/modules/sync/fileSyncController/FileSync.ts index 18219ce..116aae6 100644 --- a/src/vaultSync/SealFileSync.ts +++ b/src/modules/sync/fileSyncController/FileSync.ts @@ -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 = [] diff --git a/src/modules/sync/fileSyncController/fileSyncFactory.ts b/src/modules/sync/fileSyncController/fileSyncFactory.ts new file mode 100644 index 0000000..0ba5edf --- /dev/null +++ b/src/modules/sync/fileSyncController/fileSyncFactory.ts @@ -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()(["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; + }; + } +} diff --git a/src/modules/sync/module.ts b/src/modules/sync/module.ts new file mode 100644 index 0000000..cf6dd95 --- /dev/null +++ b/src/modules/sync/module.ts @@ -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 \ No newline at end of file diff --git a/src/datamodel/repository/abstractRepository.ts b/src/modules/sync/repository/abstractRepository.ts similarity index 100% rename from src/datamodel/repository/abstractRepository.ts rename to src/modules/sync/repository/abstractRepository.ts diff --git a/src/datamodel/repository/configuration.ts b/src/modules/sync/repository/configuration.ts similarity index 87% rename from src/datamodel/repository/configuration.ts rename to src/modules/sync/repository/configuration.ts index f49a0c3..5c132b3 100644 --- a/src/datamodel/repository/configuration.ts +++ b/src/modules/sync/repository/configuration.ts @@ -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 } diff --git a/src/datamodel/repository/tableAliases.ts b/src/modules/sync/repository/tableAliases.ts similarity index 84% rename from src/datamodel/repository/tableAliases.ts rename to src/modules/sync/repository/tableAliases.ts index 6f22266..47ec107 100644 --- a/src/datamodel/repository/tableAliases.ts +++ b/src/modules/sync/repository/tableAliases.ts @@ -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 [] } diff --git a/src/datamodel/repository/tableDefinitions.ts b/src/modules/sync/repository/tableDefinitions.ts similarity index 91% rename from src/datamodel/repository/tableDefinitions.ts rename to src/modules/sync/repository/tableDefinitions.ts index 7ba23aa..195dd60 100644 --- a/src/datamodel/repository/tableDefinitions.ts +++ b/src/modules/sync/repository/tableDefinitions.ts @@ -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') diff --git a/src/modules/sync/sync/init.ts b/src/modules/sync/sync/init.ts new file mode 100644 index 0000000..6fa7894 --- /dev/null +++ b/src/modules/sync/sync/init.ts @@ -0,0 +1,17 @@ +import { makeInjector } from "@hypersphere/dity"; +import { SyncModule } from "../module"; +import { App } from "obsidian"; +import { SealFileSync } from "../fileSyncController/FileSync"; + +@(makeInjector()([ + 'app', 'fileSync' +])) +export class SyncInit { + make(app: App, fileSync: () => Promise) { + return () => { + app.workspace.onLayoutReady(async () => { + await fileSync() + }) + } + } +} \ No newline at end of file diff --git a/src/datamodel/sync.ts b/src/modules/sync/sync/sync.ts similarity index 91% rename from src/datamodel/sync.ts rename to src/modules/sync/sync/sync.ts index d7422ee..cf7a36d 100644 --- a/src/datamodel/sync.ts +++ b/src/modules/sync/sync/sync.ts @@ -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 diff --git a/src/modules/sync/sync/syncFactory.ts b/src/modules/sync/sync/syncFactory.ts new file mode 100644 index 0000000..aaedd97 --- /dev/null +++ b/src/modules/sync/sync/syncFactory.ts @@ -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()([ + '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 + } +} \ No newline at end of file diff --git a/src/vaultSync/tables/abstractFileSyncTable.ts b/src/modules/sync/sync/tables/abstractFileSyncTable.ts similarity index 90% rename from src/vaultSync/tables/abstractFileSyncTable.ts rename to src/modules/sync/sync/tables/abstractFileSyncTable.ts index 5b0a976..dc77bde 100644 --- a/src/vaultSync/tables/abstractFileSyncTable.ts +++ b/src/modules/sync/sync/tables/abstractFileSyncTable.ts @@ -1,5 +1,5 @@ import { App, TFile } from "obsidian"; -import { SqlSealDatabase } from "../../database/database"; +import { SqlSealDatabase } from "../../../database/database"; export abstract class AFileSyncTable { constructor( diff --git a/src/vaultSync/tables/filesTable.ts b/src/modules/sync/sync/tables/filesTable.ts similarity index 89% rename from src/vaultSync/tables/filesTable.ts rename to src/modules/sync/sync/tables/filesTable.ts index 9e99a4d..a28f549 100644 --- a/src/vaultSync/tables/filesTable.ts +++ b/src/modules/sync/sync/tables/filesTable.ts @@ -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) { - 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 { 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'] diff --git a/src/vaultSync/tables/linksTable.ts b/src/modules/sync/sync/tables/linksTable.ts similarity index 100% rename from src/vaultSync/tables/linksTable.ts rename to src/modules/sync/sync/tables/linksTable.ts diff --git a/src/vaultSync/tables/tagsTable.ts b/src/modules/sync/sync/tables/tagsTable.ts similarity index 100% rename from src/vaultSync/tables/tagsTable.ts rename to src/modules/sync/sync/tables/tagsTable.ts diff --git a/src/vaultSync/tables/tasksTable.ts b/src/modules/sync/sync/tables/tasksTable.ts similarity index 100% rename from src/vaultSync/tables/tasksTable.ts rename to src/modules/sync/sync/tables/tasksTable.ts diff --git a/src/datamodel/syncStrategy/CsvFileSyncStrategy.ts b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts similarity index 93% rename from src/datamodel/syncStrategy/CsvFileSyncStrategy.ts rename to src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts index 186e41b..b45b886 100644 --- a/src/datamodel/syncStrategy/CsvFileSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts @@ -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 = '' diff --git a/src/datamodel/syncStrategy/JSONFileSyncStrategy.ts b/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts similarity index 97% rename from src/datamodel/syncStrategy/JSONFileSyncStrategy.ts rename to src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts index b11fc1e..37cd436 100644 --- a/src/datamodel/syncStrategy/JSONFileSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts @@ -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 = '' diff --git a/src/datamodel/syncStrategy/MarkdownTableSyncStrategy.ts b/src/modules/sync/syncStrategy/MarkdownTableSyncStrategy.ts similarity index 98% rename from src/datamodel/syncStrategy/MarkdownTableSyncStrategy.ts rename to src/modules/sync/syncStrategy/MarkdownTableSyncStrategy.ts index 6316d5c..1692a5c 100644 --- a/src/datamodel/syncStrategy/MarkdownTableSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/MarkdownTableSyncStrategy.ts @@ -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 = '' diff --git a/src/datamodel/syncStrategy/SyncStrategyFactory.ts b/src/modules/sync/syncStrategy/SyncStrategyFactory.ts similarity index 95% rename from src/datamodel/syncStrategy/SyncStrategyFactory.ts rename to src/modules/sync/syncStrategy/SyncStrategyFactory.ts index c8ca925..f748ce0 100644 --- a/src/datamodel/syncStrategy/SyncStrategyFactory.ts +++ b/src/modules/sync/syncStrategy/SyncStrategyFactory.ts @@ -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) => { diff --git a/src/datamodel/syncStrategy/abstractSyncStrategy.ts b/src/modules/sync/syncStrategy/abstractSyncStrategy.ts similarity index 91% rename from src/datamodel/syncStrategy/abstractSyncStrategy.ts rename to src/modules/sync/syncStrategy/abstractSyncStrategy.ts index d54172e..444cb5f 100644 --- a/src/datamodel/syncStrategy/abstractSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/abstractSyncStrategy.ts @@ -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) { diff --git a/src/datamodel/syncStrategy/types.ts b/src/modules/sync/syncStrategy/types.ts similarity index 100% rename from src/datamodel/syncStrategy/types.ts rename to src/modules/sync/syncStrategy/types.ts diff --git a/src/cellParser/CellFunction.ts b/src/modules/syntaxHighlight/cellParser/CellFunction.ts similarity index 100% rename from src/cellParser/CellFunction.ts rename to src/modules/syntaxHighlight/cellParser/CellFunction.ts diff --git a/src/cellParser/ModernCellParser.ts b/src/modules/syntaxHighlight/cellParser/ModernCellParser.ts similarity index 98% rename from src/cellParser/ModernCellParser.ts rename to src/modules/syntaxHighlight/cellParser/ModernCellParser.ts index 5772a3f..2b8f6a7 100644 --- a/src/cellParser/ModernCellParser.ts +++ b/src/modules/syntaxHighlight/cellParser/ModernCellParser.ts @@ -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; diff --git a/src/modules/syntaxHighlight/cellParser/factory.ts b/src/modules/syntaxHighlight/cellParser/factory.ts new file mode 100644 index 0000000..004113c --- /dev/null +++ b/src/modules/syntaxHighlight/cellParser/factory.ts @@ -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()([ + '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 + } +} \ No newline at end of file diff --git a/src/cellParser/parseResults.ts b/src/modules/syntaxHighlight/cellParser/parseResults.ts similarity index 100% rename from src/cellParser/parseResults.ts rename to src/modules/syntaxHighlight/cellParser/parseResults.ts diff --git a/src/cellParser/parser/checkbox.ts b/src/modules/syntaxHighlight/cellParser/parser/checkbox.ts similarity index 100% rename from src/cellParser/parser/checkbox.ts rename to src/modules/syntaxHighlight/cellParser/parser/checkbox.ts diff --git a/src/cellParser/parser/image.ts b/src/modules/syntaxHighlight/cellParser/parser/image.ts similarity index 96% rename from src/cellParser/parser/image.ts rename to src/modules/syntaxHighlight/cellParser/parser/image.ts index 5bbf0b7..a96456d 100644 --- a/src/cellParser/parser/image.ts +++ b/src/modules/syntaxHighlight/cellParser/parser/image.ts @@ -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"; diff --git a/src/cellParser/parser/link.ts b/src/modules/syntaxHighlight/cellParser/parser/link.ts similarity index 97% rename from src/cellParser/parser/link.ts rename to src/modules/syntaxHighlight/cellParser/parser/link.ts index 2e5745e..a843a8a 100644 --- a/src/cellParser/parser/link.ts +++ b/src/modules/syntaxHighlight/cellParser/parser/link.ts @@ -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"; diff --git a/src/modules/syntaxHighlight/editorExtension/inlineCodeBlock.ts b/src/modules/syntaxHighlight/editorExtension/inlineCodeBlock.ts new file mode 100644 index 0000000..aa8c387 --- /dev/null +++ b/src/modules/syntaxHighlight/editorExtension/inlineCodeBlock.ts @@ -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(); + 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; + } +} diff --git a/src/editorExtension/syntaxHighlight.ts b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts similarity index 96% rename from src/editorExtension/syntaxHighlight.ts rename to src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts index 8b337dd..b6c23f4 100644 --- a/src/editorExtension/syntaxHighlight.ts +++ b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts @@ -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' }), diff --git a/src/editorExtension/widgets/FilePathWidget.ts b/src/modules/syntaxHighlight/editorExtension/widgets/FilePathWidget.ts similarity index 90% rename from src/editorExtension/widgets/FilePathWidget.ts rename to src/modules/syntaxHighlight/editorExtension/widgets/FilePathWidget.ts index 97f3add..6d4a61d 100644 --- a/src/editorExtension/widgets/FilePathWidget.ts +++ b/src/modules/syntaxHighlight/editorExtension/widgets/FilePathWidget.ts @@ -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) { diff --git a/src/grammar/highlighter/handlebarsHighlighter.ts b/src/modules/syntaxHighlight/grammar/highlighter/handlebarsHighlighter.ts similarity index 100% rename from src/grammar/highlighter/handlebarsHighlighter.ts rename to src/modules/syntaxHighlight/grammar/highlighter/handlebarsHighlighter.ts diff --git a/src/grammar/highlighter/jsHighlighter.ts b/src/modules/syntaxHighlight/grammar/highlighter/jsHighlighter.ts similarity index 100% rename from src/grammar/highlighter/jsHighlighter.ts rename to src/modules/syntaxHighlight/grammar/highlighter/jsHighlighter.ts diff --git a/src/grammar/highlighterOperation.ts b/src/modules/syntaxHighlight/grammar/highlighterOperation.ts similarity index 100% rename from src/grammar/highlighterOperation.ts rename to src/modules/syntaxHighlight/grammar/highlighterOperation.ts diff --git a/src/modules/syntaxHighlight/init.ts b/src/modules/syntaxHighlight/init.ts new file mode 100644 index 0000000..a85ed11 --- /dev/null +++ b/src/modules/syntaxHighlight/init.ts @@ -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()([ + '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 } + ) + ]); + } + } +} \ No newline at end of file diff --git a/src/modules/syntaxHighlight/module.ts b/src/modules/syntaxHighlight/module.ts new file mode 100644 index 0000000..6fad2f5 --- /dev/null +++ b/src/modules/syntaxHighlight/module.ts @@ -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; diff --git a/src/pluginApi/sqlSealApi.ts b/src/pluginApi/sqlSealApi.ts deleted file mode 100644 index 47347b0..0000000 --- a/src/pluginApi/sqlSealApi.ts +++ /dev/null @@ -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 = [] - 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 = [] - private functions: Array = [] - private flags: Array = [] - - 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(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) - } - } -} diff --git a/src/settings/SQLSealSettingsTab.ts b/src/settings/SQLSealSettingsTab.ts deleted file mode 100644 index ff52170..0000000 --- a/src/settings/SQLSealSettingsTab.ts +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/src/sqlSeal.ts b/src/sqlSeal.ts deleted file mode 100644 index 558885c..0000000 --- a/src/sqlSeal.ts +++ /dev/null @@ -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() - } -} diff --git a/src/styles/canvas.scss b/src/styles/canvas.scss new file mode 100644 index 0000000..0444395 --- /dev/null +++ b/src/styles/canvas.scss @@ -0,0 +1,4 @@ +// Canvas specific styles +.sqlseal-renderer-on-canvas { + +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index cde0305..3944e15 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -4,4 +4,6 @@ @use 'listView'; @use 'syntaxHighlight'; @use 'obsidianMinimal'; -@use 'agGrid'; \ No newline at end of file +@use 'agGrid'; +@use 'settings'; +@use 'canvas'; \ No newline at end of file diff --git a/src/styles/settings.scss b/src/styles/settings.scss new file mode 100644 index 0000000..4db602f --- /dev/null +++ b/src/styles/settings.scss @@ -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; +} \ No newline at end of file diff --git a/src/view/CSVView.ts b/src/view/CSVView.ts deleted file mode 100644 index e1576b3..0000000 --- a/src/view/CSVView.ts +++ /dev/null @@ -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 { - super.onload(); - this.contentEl.addClass('csv-viewer-container'); - } - - async onOpen() { - this.renderCSV(); - } - - async onClose() { - // Cleanup if needed - } - - async setViewData(data: string, clear: boolean): Promise { - this.content = data; - await this.renderCSV(); - } - - getViewData(): string { - return this.content; - } - - clear(): void { - this.content = ''; - this.table.empty(); - } - - private result: any; - updateRow(newRowData: Record) { - 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) => { - 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, 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 - 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) { - 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) - } -} \ No newline at end of file diff --git a/src/view/SidebarView.ts b/src/view/SidebarView.ts deleted file mode 100644 index e77f999..0000000 --- a/src/view/SidebarView.ts +++ /dev/null @@ -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 { - 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 { - 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' - }); - }); - }); - } -} \ No newline at end of file diff --git a/styles.css b/styles.css index e6a7e2b..db7a30b 100644 --- a/styles.css +++ b/styles.css @@ -1,2 +1,2 @@ -.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important} +.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important}.sqlseal-settings-warn{background:#ffa600;padding:16px;border:2px solid rgb(134,87,0);border-radius:5px;margin:.5em 2em} /*# sourceMappingURL=styles.css.map */ diff --git a/tsconfig.json b/tsconfig.json index 62f3483..a88d262 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,8 @@ "ES6", "ES7" ], - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + // "strict": true }, "include": [ "**/*.ts"] diff --git a/types-package/src/index.ts b/types-package/src/index.ts index ef9a365..34589ca 100644 --- a/types-package/src/index.ts +++ b/types-package/src/index.ts @@ -1,5 +1,5 @@ -import type { SQLSealRegisterApi, SQLSealApi } from '../../src/pluginApi/sqlSealApi' -import { RendererConfig } from '../../src/renderer/rendererRegistry' +import type { SQLSealRegisterApi, SQLSealApi } from '../../src/modules/api/pluginApi/sqlSealApi' +import { RendererConfig } from '../../src/modules/editor/renderer/rendererRegistry' export type { From 7095130ea5677761fe20c84a07d0e754e7b3ba90 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 10 Aug 2025 10:49:56 +0200 Subject: [PATCH 2/4] Feat/hide csv columns (#176) * 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 * feat: added ability to hide columns from csv files --- .changeset/cool-taxis-boil.md | 5 + .../settings/menu/csvColumnContextMenu.ts | 99 ++++++++++++++ src/modules/settings/view/CSVView.ts | 127 +++++++----------- .../sync/syncStrategy/CsvFileSyncStrategy.ts | 22 ++- src/styles/agGrid.scss | 4 + src/utils/csvConfig.ts | 16 ++- styles.css | 2 - 7 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 .changeset/cool-taxis-boil.md create mode 100644 src/modules/settings/menu/csvColumnContextMenu.ts delete mode 100644 styles.css diff --git a/.changeset/cool-taxis-boil.md b/.changeset/cool-taxis-boil.md new file mode 100644 index 0000000..ddfdf84 --- /dev/null +++ b/.changeset/cool-taxis-boil.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +added ability to hide columns from CSV files diff --git a/src/modules/settings/menu/csvColumnContextMenu.ts b/src/modules/settings/menu/csvColumnContextMenu.ts new file mode 100644 index 0000000..1fc0944 --- /dev/null +++ b/src/modules/settings/menu/csvColumnContextMenu.ts @@ -0,0 +1,99 @@ +import { Menu, MenuItem } from "obsidian"; +import { RenameColumnModal } from "../modal/renameColumnModal"; +import { CSVView } from "../view/CSVView"; +import { AgColumn } from "ag-grid-community"; +import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal"; +import { ColumnType } from "../../../utils/types"; + +export class CSVColumnContextMenu { + constructor( + private csvView: CSVView, + private column: AgColumn, + ) { } + + onRename() { + const modal = new RenameColumnModal(this.csvView.app, (res) => { + this.csvView.renameColumn(this.columnField, res); + }); + modal.open(); + } + + onDelete() { + const colName = this.columnField; + const modal = new DeleteConfirmationModal( + this.csvView.app, + `column ${colName}`, + () => { + this.csvView.deleteColumn(colName); + }, + ); + modal.open(); + } + + get columnField() { + return this.column.getUserProvidedColDef()?.field!; + } + + get config() { + return this.csvView.getColumnConfig(this.columnField) + } + + show(pos: { x: number; y: number }) { + const menu = new Menu(); + + menu.addItem((item) => { + item.setTitle("Rename Column"); + item.onClick(this.onRename.bind(this)); + }); + + menu.addItem((item) => { + item.setTitle("Delete Column"); + item.onClick(this.onDelete.bind(this)); + }); + + menu.addSeparator(); + menu.addItem(item => { + const isHidden = !!this.config.isHidden + item.setTitle(isHidden ? 'Show in queries' : 'Hide from queries') + item.onClick(async () => { + await this.csvView.setConfig(this.columnField, { isHidden: !isHidden }) + this.csvView.refreshTypes() + this.csvView.saveData() + }) + }) + + + // 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.columnField; + + const current = this.csvView.getColumnType(colName); + types.forEach((type) => { + ipfSubmenu.addItem((subItem: MenuItem) => { + const checkbox = type === current ? "✓ " : ""; + subItem.setTitle(checkbox + type); + subItem.onClick(() => { + this.csvView.changeColumnType(colName, type); + this.csvView.refreshTypes(); + this.csvView.loadDataIntoGrid(); + }); + }); + }); + }); + + menu.showAtPosition(pos); + } +} diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts index fc657e5..6b17f5f 100644 --- a/src/modules/settings/view/CSVView.ts +++ b/src/modules/settings/view/CSVView.ts @@ -5,12 +5,14 @@ import { GridRendererCommunicator, } from "../../editor/renderer/GridRenderer"; import { errorNotice } from "../../../utils/notice"; -import { ConfigObject, loadConfig, saveConfig } from "src/utils/csvConfig"; +import { ColumnDefinition, ConfigObject, DEFAULT_CONFIG, 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"; +import { CSVColumnContextMenu } from "../menu/csvColumnContextMenu"; +import { AgColumn } from "ag-grid-community"; const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n)); @@ -158,14 +160,25 @@ export class CSVView extends TextFileView { return "auto"; } + getColumnConfig(columnName: string) { + if (this.config.columnDefinitions[columnName]) { + return this.config.columnDefinitions[columnName] + } + return { ...DEFAULT_CONFIG } + } + async changeColumnType(columnName: string, type: ColumnType) { + return await this.setConfig(columnName, { type }) + } + + async setConfig(columnName: string, config: Partial) { const prev = this.config.columnDefinitions[columnName] ?? {}; - this.config.columnDefinitions[columnName] = { + this.config.columnDefinitions[columnName] = { ...prev, - type: type, + ...config, }; await this.saveConfig(); - } + } async loadConfig() { while (!this.file) { @@ -179,25 +192,34 @@ export class CSVView extends TextFileView { } private getColumnConfigurations(columns: string[]) { - return columns.map((f) => { + return columns + .filter(f => this.showHidden || !this.getColumnConfig(f).isHidden) + .map((f) => { if (!this.config) { return { field: f, }; } - const def = this.config.columnDefinitions[f]?.type; - if (!def || def === "auto") { - return { field: f }; + const config = this.getColumnConfig(f) + const type = config.type + const cellStyle = { opacity: config.isHidden ? 0.5 : 1 } + const result = { + field: f, + cellStyle, + headerClass: config.isHidden ? 'sqlseal-hidden-column' : '' + } + if (!type || type === "auto") { + return result; } - if (def === "date") { + if (type === "date") { return { - field: f, + ...result, cellDataType: "dateString", }; } return { - field: f, - cellDataType: def, + ...result, + cellDataType: type, }; }); } @@ -283,6 +305,8 @@ export class CSVView extends TextFileView { isLoading: boolean = false; + showHidden: boolean = false; + private async renderCSV() { if (this.isLoading) { if (this.api) { @@ -323,6 +347,19 @@ export class CSVView extends TextFileView { const generateSqlCode = buttonsRow.createEl("button", { text: "Generate SQLSeal code", }); + + + + const showHidden = buttonsRow.createEl('button', { + text: 'Show Hidden Columns' + }) + + showHidden.addEventListener('click', () => { + this.showHidden = !this.showHidden + showHidden.textContent = this.showHidden ? 'Hide Hidden Columns' : 'Show Hidden Columns' + this.refreshTypes() + }) + const gridEl = csvEditorDiv.createDiv({ cls: "sql-seal-csv-viewer" }); generateSqlCode.addEventListener("click", (e) => { @@ -346,69 +383,9 @@ export class CSVView extends TextFileView { 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 }); + const menu = new CSVColumnContextMenu(csvView, this.column as AgColumn) + const pos = e.getBoundingClientRect(); + menu.show({ x: pos.x, y: pos.y + 20 }) }, }, }, diff --git a/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts index b45b886..21d7842 100644 --- a/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts @@ -59,14 +59,26 @@ export class CsvFileSyncStrategy extends ISyncStrategy { header: true, dynamicTyping: false, skipEmptyLines: true, - transformHeader: sanitise + transformHeader: sanitise, }) const config = await loadConfig(file, this.app.vault) - return { data: parsed.data, columns: parsed.meta.fields?.map(f => ({ - name: f, - type: config.columnDefinitions[f]?.type ?? 'auto' as const - })) ?? [] } + const columns = parsed.meta.fields?. + filter(f => !config.columnDefinitions[f]?.isHidden) + .map(f => ({ + name: f, + type: config.columnDefinitions[f]?.type ?? 'auto' as const + })) ?? [] + + const resultedData = parsed.data.map(d => { + const res: Record = {} + for (const c of columns) { + res[c.name] = d[c.name] + } + return res + }) + + return { data: resultedData, columns } } } \ No newline at end of file diff --git a/src/styles/agGrid.scss b/src/styles/agGrid.scss index bb16c27..bb3a330 100644 --- a/src/styles/agGrid.scss +++ b/src/styles/agGrid.scss @@ -1,4 +1,8 @@ /* AG GRID CUSTOMS */ .ag-checkbox-input { opacity: 1 !important; +} + +.sqlseal-hidden-column { + opacity: 0.5; } \ No newline at end of file diff --git a/src/utils/csvConfig.ts b/src/utils/csvConfig.ts index dc5a014..50f9d7b 100644 --- a/src/utils/csvConfig.ts +++ b/src/utils/csvConfig.ts @@ -5,10 +5,16 @@ import { parse as jsonParse, stringify as jsonStringify } from 'json5' // export type ColumnType = 'auto' | 'number' | 'text' export type ColumnType = string -interface ColumnDefinition { +export interface ColumnDefinition { type: ColumnType + isHidden: boolean } +export const DEFAULT_CONFIG = { + type: 'auto', + isHidden: false +} satisfies ColumnDefinition + export interface ConfigObject { columnDefinitions: {[key: string]: ColumnDefinition } } @@ -27,7 +33,13 @@ export const loadConfig = async (file: TFile, vault: Vault): Promise ([key.toLowerCase(), value]))) + return { + ...decoded, + columnDefinitions + } } export const saveConfig = async (file: TFile, content: Object, vault: Vault) => { diff --git a/styles.css b/styles.css deleted file mode 100644 index db7a30b..0000000 --- a/styles.css +++ /dev/null @@ -1,2 +0,0 @@ -.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important}.sqlseal-settings-warn{background:#ffa600;padding:16px;border:2px solid rgb(134,87,0);border-radius:5px;margin:.5em 2em} -/*# sourceMappingURL=styles.css.map */ From 2bfd2060d7a781f9d67cd126b7ebb381b5551f00 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 10 Aug 2025 11:01:08 +0200 Subject: [PATCH 3/4] Feat/global tables new (#173) * 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 * feat: added ability to hide columns from csv files * feat: adding global tables support (wip) * feat: global tables full implementation * feat: added changeset * chore: fixing typechecks --- .changeset/cool-knives-drop.md | 5 + esbuild.config.mjs | 5 +- src/modules/database/database.ts | 5 + .../codeblockHandler/CodeblockProcessor.ts | 7 +- src/modules/globalTables/FileConfig.ts | 49 +++++ src/modules/globalTables/GlobalTablesView.ts | 206 ++++++++++++++++++ .../globalTables/GlobalTablesView/MenuBar.ts | 39 ++++ .../globalTables/GlobalTablesViewRegister.ts | 46 ++++ src/modules/globalTables/InitFactory.ts | 11 + .../autocompleteElement/AutocompleteInput.ts | 82 +++++++ src/modules/globalTables/cells/ActionCell.ts | 28 +++ src/modules/globalTables/cells/ConfigCell.ts | 47 ++++ .../globalTables/cells/StatsRenderer.ts | 59 +++++ .../globalTables/modal/NewGlobalTableModal.ts | 133 +++++++++++ src/modules/globalTables/module.ts | 20 ++ src/modules/globalTables/sqlseal-bw.svg | 1 + src/modules/main/init.ts | 9 +- src/modules/main/module.ts | 9 +- src/modules/settings/view/CSVView.ts | 88 ++++---- src/modules/settings/view/CSVViewMenuBar.ts | 64 ++++++ src/modules/settings/view/JsonView.ts | 6 +- src/modules/sync/repository/tableAliases.ts | 9 +- src/modules/sync/sync/sync.ts | 39 +++- src/styles/autocomplete.scss | 58 +++++ src/styles/global-tables.scss | 6 + src/styles/main.scss | 4 +- 26 files changed, 975 insertions(+), 60 deletions(-) create mode 100644 .changeset/cool-knives-drop.md create mode 100644 src/modules/globalTables/FileConfig.ts create mode 100644 src/modules/globalTables/GlobalTablesView.ts create mode 100644 src/modules/globalTables/GlobalTablesView/MenuBar.ts create mode 100644 src/modules/globalTables/GlobalTablesViewRegister.ts create mode 100644 src/modules/globalTables/InitFactory.ts create mode 100644 src/modules/globalTables/autocompleteElement/AutocompleteInput.ts create mode 100644 src/modules/globalTables/cells/ActionCell.ts create mode 100644 src/modules/globalTables/cells/ConfigCell.ts create mode 100644 src/modules/globalTables/cells/StatsRenderer.ts create mode 100644 src/modules/globalTables/modal/NewGlobalTableModal.ts create mode 100644 src/modules/globalTables/module.ts create mode 100644 src/modules/globalTables/sqlseal-bw.svg create mode 100644 src/modules/settings/view/CSVViewMenuBar.ts create mode 100644 src/styles/autocomplete.scss create mode 100644 src/styles/global-tables.scss diff --git a/.changeset/cool-knives-drop.md b/.changeset/cool-knives-drop.md new file mode 100644 index 0000000..69eeacf --- /dev/null +++ b/.changeset/cool-knives-drop.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +adding global tables support - you can now define table that will be available in all your files diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 9402e78..bfad87e 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -37,7 +37,7 @@ const wasmPlugin = { loader: 'js', }; }); - }, + } }; // Plugin to inject worker code @@ -108,6 +108,9 @@ const context = await esbuild.context({ sourcemap: prod ? false : "inline", treeShaking: true, outfile: "main.js", + loader: { + '.svg': 'text' + }, plugins: [ wasmPlugin, workerPlugin diff --git a/src/modules/database/database.ts b/src/modules/database/database.ts index 0c6af1b..088827b 100644 --- a/src/modules/database/database.ts +++ b/src/modules/database/database.ts @@ -96,6 +96,11 @@ export class SqlSealDatabase { return this.db?.getColumns(tableName) } + async count(tableName: string) { + const data = await this.db?.select(`SELECT COUNT(*) as count FROM ${tableName}`, {}) + return data?.data[0]['count'] + } + async addColumns(tableName: string, newColumns: string[]) { return this.db?.addColumns(tableName, newColumns) } diff --git a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts index dcbb8f3..c1368ac 100644 --- a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts +++ b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts @@ -182,10 +182,11 @@ export class CodeblockProcessor extends MarkdownRenderChild { 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)) + const canvasView = leaf.view as any; // Canvas view has data and file properties not exposed in base View type + const nodes = JSON.parse(canvasView.data).nodes + const node = nodes.filter((n: any) => n.text).find((n: any) => n.text.contains(this.source)) if (node) { - this.cachedName = leaf.view.file.path + this.cachedName = canvasView.file.path return this.cachedName as string } } diff --git a/src/modules/globalTables/FileConfig.ts b/src/modules/globalTables/FileConfig.ts new file mode 100644 index 0000000..4446659 --- /dev/null +++ b/src/modules/globalTables/FileConfig.ts @@ -0,0 +1,49 @@ +import { TFile, Vault } from "obsidian"; + +export class FileConfig { + constructor(private path: string, private vault: Vault) { + + } + + private data: T[] = [] + private isLoaded: boolean = false + private fileHandler: TFile | null = null + + async load() { + this.fileHandler = this.vault.getFileByPath(this.path) + if (!this.fileHandler) { + return + } + + // FIXME: add proper ZOD parsing here + this.data = JSON.parse(await this.vault.cachedRead(this.fileHandler)) + this.isLoaded = true + } + + async insert(v: T) { + if (!this.isLoaded) { + await this.load() + } + this.data.push(v) + await this.save() + } + + get items() { + return this.data + } + + async save() { + const data = JSON.stringify(this.data) + if (!this.fileHandler) { + // Creating new file + this.fileHandler = await this.vault.create(this.path, data) + return + } + await this.vault.modify(this.fileHandler, data) + } + + async remove(v: T) { + this.data = this.data.filter(d => d !== v) + await this.save() + } +} \ No newline at end of file diff --git a/src/modules/globalTables/GlobalTablesView.ts b/src/modules/globalTables/GlobalTablesView.ts new file mode 100644 index 0000000..2e8d1f6 --- /dev/null +++ b/src/modules/globalTables/GlobalTablesView.ts @@ -0,0 +1,206 @@ +import { IconName, ItemView, Vault, WorkspaceLeaf } from "obsidian"; +import { MenuBar } from "./GlobalTablesView/MenuBar"; +import { FileConfig } from "./FileConfig"; +import { TableConfiguration } from "./modal/NewGlobalTableModal"; +import { GridRenderer } from "../editor/renderer/GridRenderer"; +import { createGrid, GridApi, themeQuartz } from "ag-grid-community"; +import { ConfigCellRenderer } from "./cells/ConfigCell"; +import { StatsRenderer } from "./cells/StatsRenderer"; +import { ActionCellRenderer } from "./cells/ActionCell"; +import { Sync } from "../sync/sync/sync"; +import { ParserTableDefinition } from "../sync/syncStrategy/types"; +import { throttle } from "lodash"; + +export const GLOBAL_TABLES_VIEW_TYPE = "sqlseal-global-tables"; + +const GLOBAL_SOURCE_FILE = "/"; + +export class GlobalTablesView extends ItemView { + config: FileConfig; + constructor( + leaf: WorkspaceLeaf, + vault: Vault, + public sync: Sync, + ) { + super(leaf); + this.config = new FileConfig("__globalviews.sqlseal", vault); + } + + getViewType() { + return GLOBAL_TABLES_VIEW_TYPE; + } + + getDisplayText() { + return "SQLSeal Global Tables"; + } + + getIcon(): IconName { + return "logo-sqlseal"; + } + + api: GridApi | null = null; + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + const c = container.createDiv({ cls: "sqlseal-global-tables-container" }); + const el = c.createEl("div"); + + const menuBar = new MenuBar(el, this.app); + + await this.config.load(); + for (const conf of this.gridData) { + this.sync.registerTable(this.getTableDefinition(conf)); + } + + menuBar.events.on("new-table", (data) => { + this.addElement(data); + }); + + // el.createDiv({ text: 'HELLO FROM SQLSEAL GLOBAL TABLES VIEW' }) + + const gridEl = c.createDiv({ cls: "sql-seal-csv-viewer" }); + const myTheme = themeQuartz.withParams({ + borderRadius: 0, + browserColorScheme: "light", + headerFontSize: 14, + headerRowBorder: false, + headerVerticalPaddingScale: 1, + rowBorder: false, + spacing: 16, + wrapperBorder: false, + wrapperBorderRadius: 0, + }); + + this.api = createGrid(gridEl, { + theme: myTheme, + detailRowAutoHeight: true, + paginationAutoPageSize: true, + rowData: this.gridData, + suppressMoveWhenColumnDragging: true, + suppressRowDrag: true, + suppressCellFocus: true, + suppressMaintainUnsortedOrder: true, + suppressDragLeaveHidesColumns: true, + suppressMoveWhenRowDragging: true, + defaultColDef: { + resizable: false, + sortable: false, + rowDrag: false, + suppressMovable: true, + flex: 1, + }, + autoSizeStrategy: { + type: "fitGridWidth", + }, + columnDefs: [ + { field: "name" }, + { field: "config.type", minWidth: 50 }, + { field: "config", cellRenderer: ConfigCellRenderer }, + { + field: "name", + cellRenderer: StatsRenderer, + headerName: "Stats", + minWidth: 200, + }, + { cellRenderer: ActionCellRenderer, headerName: "Actions" }, + ], + context: this, + }); + + this.setupResizeObserver(gridEl); + } + + // Vibe Coded. + private smartResize() { + if (!this.api) return; + + const api = this.api + const columnApi = this.api + const containerWidth = this.containerEl.clientWidth; + + // First, auto-size all columns based on content + const allColumnIds = columnApi.getColumns()?.map(col => col.getColId()) || []; + api.autoSizeColumns(allColumnIds); + + // Calculate total width needed + const totalContentWidth = columnApi.getColumns() + ?.reduce((sum, col) => sum + col.getActualWidth(), 0) || 0; + + // If content fits in container, optionally expand to fill + if (totalContentWidth < containerWidth * 0.8) { + // Content fits comfortably, you can choose to: + // Option A: Leave as-is (content-based sizing) + // Option B: Expand to fill container + api.sizeColumnsToFit(); + } + // If content is wider than container, keep auto-sized widths + // This will enable horizontal scrolling automatically + } + + get gridData() { + return this.config.items; + } + + async addElement(data: TableConfiguration) { + await this.config.insert(data); + switch (data.config.type) { + case "csv": + case "json": + await this.sync.registerTable(this.getTableDefinition(data)); + break; + case "md-table": + console.log("NOT IMPLEMENTED YET", data); + } + this.refresh(); + } + + getTableDefinition(data: TableConfiguration): ParserTableDefinition { + switch (data.config.type) { + case "csv": + return { + tableAlias: data.name, + sourceFile: GLOBAL_SOURCE_FILE, + type: "file", + arguments: [data.config.filename], + }; + case 'json': + return { + tableAlias: data.name, + sourceFile: GLOBAL_SOURCE_FILE, + type: "file", + arguments: [data.config.filename, data.config.xpath ? data.config.xpath : '$'], + } + case "md-table": + throw new Error("Not implemented"); + } + } + + async deleteElement(e: TableConfiguration) { + await this.config.remove(e); + const def = this.getTableDefinition(e); + await this.sync.unregisterTable(def); + this.refresh(); + } + + refresh() { + this.api?.setGridOption("rowData", this.gridData); + } + + async onClose() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + if (this.api) { + this.api.destroy(); + } + } + private resizeObserver: ResizeObserver; + + private setupResizeObserver(gridContainer: HTMLElement) { + const fn = throttle(() => this.smartResize(), 100) + this.resizeObserver = new ResizeObserver(fn); + + this.resizeObserver.observe(gridContainer); + } +} diff --git a/src/modules/globalTables/GlobalTablesView/MenuBar.ts b/src/modules/globalTables/GlobalTablesView/MenuBar.ts new file mode 100644 index 0000000..3847091 --- /dev/null +++ b/src/modules/globalTables/GlobalTablesView/MenuBar.ts @@ -0,0 +1,39 @@ +import { args, BusBuilder } from "@hypersphere/omnibus" +import { App, ButtonComponent } from "obsidian" +import { NewGlobalTableModal, TableConfiguration } from "../modal/NewGlobalTableModal" + +export class MenuBar { + + private bus =(new BusBuilder()) + .register('new-table', args()) + .build() + + constructor(private el: HTMLDivElement, private app: App) { + this.show() + } + + get events() { + return this.bus.getRegistrator() + } + + openNewTableModal() { + const modal = new NewGlobalTableModal(this.app) + modal.open() + modal.bus.on('data', d => this.bus.trigger('new-table', d)) + } + + private show() { + this.el.empty() + this.addButton('Create New Table', () => this.openNewTableModal(), true) + } + + private addButton(text: string, onClick: () => void, cta: boolean = false) { + const btn = new ButtonComponent(this.el) + .setButtonText(text) + .onClick(onClick) + + if (cta) { + btn.setCta() + } + } +} \ No newline at end of file diff --git a/src/modules/globalTables/GlobalTablesViewRegister.ts b/src/modules/globalTables/GlobalTablesViewRegister.ts new file mode 100644 index 0000000..5176067 --- /dev/null +++ b/src/modules/globalTables/GlobalTablesViewRegister.ts @@ -0,0 +1,46 @@ +import { makeInjector } from "@hypersphere/dity"; +import { GlobalTablesModule } from "./module"; +import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian"; +import { GLOBAL_TABLES_VIEW_TYPE, GlobalTablesView } from "./GlobalTablesView"; +// @ts-ignore: Handled by esbuild +import SQLSealIcon from './sqlseal-bw.svg' +import { Sync } from "../sync/sync/sync"; + + +@(makeInjector()( + ['plugin', 'app', 'sync'] +)) +export class GlobalTablesViewRegister { + make(plugin: Plugin, app: App, sync: Sync) { + + const activateView = async () => { + const { workspace } = plugin.app; + + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(GLOBAL_TABLES_VIEW_TYPE); + + if (leaves.length > 0) { + // A leaf with our view already exists, use that + leaf = leaves[0]; + } else { + leaf = workspace.getLeaf('tab') + if (!leaf) { return } + await leaf.setViewState({ type: GLOBAL_TABLES_VIEW_TYPE, active: true }); + } + + // "Reveal" the leaf in case it is in a collapsed sidebar + workspace.revealLeaf(leaf); + } + + return () => { + plugin.registerView(GLOBAL_TABLES_VIEW_TYPE, leaf => new GlobalTablesView(leaf, app.vault, sync)) + + + // SQLSeal Icon + addIcon('logo-sqlseal', SQLSealIcon) + + plugin.addRibbonIcon('logo-sqlseal', 'SQLSeal Global Tables', activateView) + + } + } +} \ No newline at end of file diff --git a/src/modules/globalTables/InitFactory.ts b/src/modules/globalTables/InitFactory.ts new file mode 100644 index 0000000..12ee2c1 --- /dev/null +++ b/src/modules/globalTables/InitFactory.ts @@ -0,0 +1,11 @@ +import { makeInjector } from "@hypersphere/dity" +import { GlobalTablesModule } from "./module" + +@(makeInjector()(['globalTablesViewRegister'])) +export class InitFactory { + make(register: () => void) { + return () => { + register() + } + } +} \ No newline at end of file diff --git a/src/modules/globalTables/autocompleteElement/AutocompleteInput.ts b/src/modules/globalTables/autocompleteElement/AutocompleteInput.ts new file mode 100644 index 0000000..e87acff --- /dev/null +++ b/src/modules/globalTables/autocompleteElement/AutocompleteInput.ts @@ -0,0 +1,82 @@ +import { args, BusBuilder } from "@hypersphere/omnibus" +import { TFile, Vault } from "obsidian" + +interface DropdownValue { + name: string + path: string + originalFile: TFile +} + +export class AutocompleteInput { + constructor(parent: HTMLElement, private vault: Vault, private extensions: string[]) { + const c = parent.createDiv({ cls: 'sqlseal-autocomplete-container'}) + this.selectedEl = c.createEl('div', { cls: 'selected-el'}) + this.selectedElText = this.selectedEl.createSpan() + this.selectedEl.createEl('button', { text: 'X'}).addEventListener('click', () => { + this.input.show() + this.input.value = '' + this.selectedEl.hide() + this.bus.trigger('change', '') + }) + this.selectedEl.hide() + this.input = c.createEl('input', { type: 'text' }) + this.dropdownContainer = c.createDiv({ cls: 'sqlseal-autocomplete-dropdown'}) + this.render() + } + + bus = new BusBuilder() + .register('item-selected', args()) + .register('change', args()) + .build() + + dropdownContainer: HTMLDivElement + input: HTMLInputElement + dropdownValues: DropdownValue[] = [] + selectedEl: HTMLDivElement + selectedElText: HTMLSpanElement + + render() { + this.input.addEventListener('keydown', e => { + requestAnimationFrame(() => { + this.dropdownValues = this.getFilesForInput((e.target! as any).value) + this.setDropdownValues() + }) + }) + + this.bus.on('item-selected', e => { + this.input.value = e.originalFile.path + this.bus.trigger('change', e.originalFile.path) + this.dropdownValues = [] + this.setDropdownValues() + + this.selectedEl.show() + this.selectedElText.textContent = e.originalFile.path + this.input.hide() + + }) + } + + setDropdownValues() { + this.dropdownContainer.empty() + for (const val of this.dropdownValues) { + this.dropdownContainer.appendChild(this.createDropdownOption(val)) + } + } + + private createDropdownOption(opt: DropdownValue) { + const el = createEl('div', { cls: 'sqlseal-dropdown-el' }) + el.createDiv({ text: opt.name, cls: 'sqlseal-dropdown-el-name' }) + el.createDiv({ text: opt.path, cls: 'sqlseal-dropdown-el-path' }) + el.addEventListener('click', e => { + this.bus.trigger('item-selected', opt) + }) + return el + } + + getFilesForInput(inp: string): DropdownValue[] { + return this.vault.getFiles() + .filter(f => this.extensions.includes(f.extension)) + .filter(f => f.path.includes(inp)) + .map(f => ({ name: f.name, path: f.parent?.path ?? '', originalFile: f })) + } +} \ No newline at end of file diff --git a/src/modules/globalTables/cells/ActionCell.ts b/src/modules/globalTables/cells/ActionCell.ts new file mode 100644 index 0000000..93872a2 --- /dev/null +++ b/src/modules/globalTables/cells/ActionCell.ts @@ -0,0 +1,28 @@ +import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community"; +import { TableConfiguration } from "../modal/NewGlobalTableModal"; +import { ButtonComponent } from "obsidian"; +import { GlobalTablesView } from "../GlobalTablesView"; + +export class ActionCellRenderer implements ICellRendererComp { + private eGui!: HTMLDivElement; + + public init(params: ICellRendererParams): void { + const { value, data, context } = params; + + + + this.eGui = document.createElement("div"); + + new ButtonComponent(this.eGui) + .setIcon('trash-2') + .onClick(() => context.deleteElement(data)) + } + + public getGui(): HTMLElement { + return this.eGui; + } + + public refresh(params: ICellRendererParams): boolean { + return true; + } +} \ No newline at end of file diff --git a/src/modules/globalTables/cells/ConfigCell.ts b/src/modules/globalTables/cells/ConfigCell.ts new file mode 100644 index 0000000..f8698f1 --- /dev/null +++ b/src/modules/globalTables/cells/ConfigCell.ts @@ -0,0 +1,47 @@ +import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community"; +import { TableConfiguration } from "../modal/NewGlobalTableModal"; +import { Component, MarkdownRenderer, TFile } from "obsidian"; +import { GlobalTablesView } from "../GlobalTablesView"; + +export class ConfigCellRenderer implements ICellRendererComp { + private eGui!: HTMLDivElement; + + public init( + params: ICellRendererParams< + TableConfiguration, + TableConfiguration["config"], + GlobalTablesView + >, + ): void { + const { value, data, context } = params; + + this.eGui = document.createElement("div"); + this.eGui.className = "config"; + + const container = this.eGui.createDiv(); + + if (value?.type === "csv") { + const link = container.createEl("a"); + link.href = value.filename; + link.textContent = value.filename; + link.addEventListener("click", (e) => { + e.preventDefault(); + const file = context.app.vault.getAbstractFileByPath(value.filename); + if (file instanceof TFile) { + context.app.workspace.openLinkText(value.filename, "", true); + } + }); + } + if (value?.type === 'json') { + container.textContent = value.filename + '#' + (value.xpath ? value.xpath : '$') + } + } + + public getGui(): HTMLElement { + return this.eGui; + } + + public refresh(params: ICellRendererParams): boolean { + return true; + } +} diff --git a/src/modules/globalTables/cells/StatsRenderer.ts b/src/modules/globalTables/cells/StatsRenderer.ts new file mode 100644 index 0000000..9848baa --- /dev/null +++ b/src/modules/globalTables/cells/StatsRenderer.ts @@ -0,0 +1,59 @@ +import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community"; +import { TableConfiguration } from "../modal/NewGlobalTableModal"; +import { GlobalTablesView } from "../GlobalTablesView"; +import { OmnibusRegistrator } from "@hypersphere/omnibus"; + +export class StatsRenderer implements ICellRendererComp { + private eGui!: HTMLDivElement; + + context: GlobalTablesView + data: TableConfiguration + eventName: string = '' + + reg: OmnibusRegistrator + + syncFn = () => { + this.sync() + } + + public init(params: ICellRendererParams): void { + const { value, data, context } = params; + this.context = context + this.data = data! + + this.eGui = document.createElement("div"); + this.eGui.className = "stats"; + + const stats = this.eGui.createSpan() + stats.textContent = 'Loading' + + requestAnimationFrame(async () => { this.sync() }) + + // Watching for the changes + this.setupWatchersAsync(context, data!) + + } + + async setupWatchersAsync(context: GlobalTablesView, data: TableConfiguration) { + this.reg = context.sync.getRegistrator() + this.eventName = await context.sync.getEventNameForAlias('/', data!.name) + this.reg.on(this.eventName, this.syncFn) + } + + async sync() { + const result = await this.context.sync.getStats('/', this.data!.name) + this.eGui.textContent = `Rows: ${result.rows} / Columns: ${result.columns}` + } + + public getGui(): HTMLElement { + return this.eGui; + } + + public refresh(params: ICellRendererParams): boolean { + return false; + } + + destroy(): void { + this.reg.off(this.eventName, this.syncFn) + } +} \ No newline at end of file diff --git a/src/modules/globalTables/modal/NewGlobalTableModal.ts b/src/modules/globalTables/modal/NewGlobalTableModal.ts new file mode 100644 index 0000000..0ecd383 --- /dev/null +++ b/src/modules/globalTables/modal/NewGlobalTableModal.ts @@ -0,0 +1,133 @@ +import { MenuSeparator, Modal, Setting } from "obsidian"; +import { AutocompleteInput } from "../autocompleteElement/AutocompleteInput"; +import { args, BusBuilder } from "@hypersphere/omnibus"; + +export interface CSVConfig { + type: 'csv' + filename: string +} + +export interface JsonConfig { + type: 'json' + filename: string + xpath: string +} + +export interface MDTable { + type: 'md-table' + filename: string + selector: string +} + +export interface TableConfiguration { + name: string + config: CSVConfig | JsonConfig | MDTable +} + +export type Type = TableConfiguration['config']['type'] + +export class NewGlobalTableModal extends Modal { + + bus = new BusBuilder() + .register('data', args()) + .build() + + data: TableConfiguration = { + name: "", + config: { + type: 'csv', + filename: '' + } + }; + + typeSectionEl: HTMLElement | null = null + + onOpen(): void { + const { contentEl } = this; + contentEl.closest('.modal')?.classList.add('modal-overflow') + contentEl.createEl("h2", { text: "New Table Modal" }); + + const settingsEl = contentEl.createDiv(); + + const name = new Setting(settingsEl).setName("Table Name").addText((txt) => + txt.setPlaceholder("eg. Data").onChange((val) => { + this.data.name = val; + }), + ); + + const type = new Setting(settingsEl) + .setName("Source Type") + .addDropdown((drop) => + drop.addOptions({ + csv: "CSV", + json: "JSON", + // "md-table": "Markdown Table", + }) + .setValue(this.data.config.type) + .onChange(v => { + this.data.config.type = v as Type + this.renderTypeSection() + }) + ); + + this.typeSectionEl = contentEl.createDiv() + this.renderTypeSection() + + new Setting(contentEl) + .addButton(b => b + .setButtonText('Add') + .setCta() + .onClick(() => { + this.bus.trigger('data', this.data) + this.close() + }) + ) + } + + renderTypeSection() { + if (!this.typeSectionEl) { + return + } + const el = this.typeSectionEl + el.empty() + + switch (this.data.config.type) { + case 'csv': + this.renderCsvSection() + break + case 'json': + this.renderJsonSection() + break + case 'md-table': + break + default: + break + } + } + + renderCsvSection() { + const filename = new Setting(this.typeSectionEl!) + .setName('Filename') + const autocomplete = new AutocompleteInput(filename.controlEl, this.app.vault, ['csv']) + autocomplete.bus.on('change', (path) => { + this.data.config.filename = path + }) + } + + renderJsonSection() { + const filename = new Setting(this.typeSectionEl!) + .setName('Filename') + const autocomplete = new AutocompleteInput(filename.controlEl, this.app.vault, ['json', 'json5']) + autocomplete.bus.on('change', (path) => { + this.data.config.filename = path + }) + + const xpath = new Setting(this.typeSectionEl!) + .setName('Selector') + .addText(c => c.setPlaceholder('$.[0]') + .onChange(e => { + (this.data.config as JsonConfig).xpath = e + })) + + } +} diff --git a/src/modules/globalTables/module.ts b/src/modules/globalTables/module.ts new file mode 100644 index 0000000..5b5b73c --- /dev/null +++ b/src/modules/globalTables/module.ts @@ -0,0 +1,20 @@ +import { asFactory, buildContainer } from "@hypersphere/dity"; +import { InitFactory } from "./InitFactory"; +import { App, Plugin } from "obsidian"; +import { GlobalTablesViewRegister } from "./GlobalTablesViewRegister"; +import { Sync } from "../sync/sync/sync"; + +export const globalTables = buildContainer(c => + c.register({ + init: asFactory(InitFactory), + globalTablesViewRegister: asFactory(GlobalTablesViewRegister) + }) + .externals<{ + plugin: Plugin, + app: App, + sync: Sync + }>() + .exports('init') +) + +export type GlobalTablesModule = typeof globalTables diff --git a/src/modules/globalTables/sqlseal-bw.svg b/src/modules/globalTables/sqlseal-bw.svg new file mode 100644 index 0000000..a914bc5 --- /dev/null +++ b/src/modules/globalTables/sqlseal-bw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/modules/main/init.ts b/src/modules/main/init.ts index 65115c0..8cfa8af 100644 --- a/src/modules/main/init.ts +++ b/src/modules/main/init.ts @@ -11,7 +11,8 @@ type InitFn = () => void 'contextMenu.init', 'sync.init', 'debug.init', - 'api.init' + 'api.init', + 'globalTables.init' ])) export class Init { async make( @@ -21,7 +22,8 @@ export class Init { contextMenu: InitFn, syncInit: InitFn, debugInit: InitFn, - apiInit: InitFn + apiInit: InitFn, + globalTablesInit: InitFn ) { return () => { settingsInit() @@ -29,8 +31,9 @@ export class Init { highlighInit() contextMenu() syncInit() - debugInit() + // debugInit() apiInit() + globalTablesInit() } } } \ No newline at end of file diff --git a/src/modules/main/module.ts b/src/modules/main/module.ts index ac21979..7126739 100644 --- a/src/modules/main/module.ts +++ b/src/modules/main/module.ts @@ -10,6 +10,7 @@ import { syntaxHighlight } from '../syntaxHighlight/module' import { contextMenu } from '../contextMenu/module' import { debugModule } from '../debug/module' import { apiModule } from '../api/module' +import { globalTables } from '../globalTables/module' const obsidian = buildContainer(c => c .externals<{ @@ -30,7 +31,8 @@ export const mainModule = buildContainer(c => c syntaxHighlight, contextMenu, debug: debugModule, - api: apiModule + api: apiModule, + globalTables }) .register({ init: asFactory(Init) @@ -77,6 +79,11 @@ export const mainModule = buildContainer(c => c 'api.db': 'db.db', 'api.rendererRegistry': 'editor.rendererRegistry' }) + .resolve({ + 'globalTables.plugin': 'obsidian.plugin', + 'globalTables.app': 'obsidian.app', + 'globalTables.sync': 'sync.syncBus' + }) ) export type MainModule = typeof mainModule diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts index 6b17f5f..2cb02e2 100644 --- a/src/modules/settings/view/CSVView.ts +++ b/src/modules/settings/view/CSVView.ts @@ -1,4 +1,4 @@ -import { WorkspaceLeaf, TextFileView, Menu, MenuItem } from "obsidian"; +import { WorkspaceLeaf, TextFileView, Menu, MenuItem, IconName } from "obsidian"; import { parse, unparse } from "papaparse"; import { GridRenderer, @@ -12,7 +12,8 @@ import { RenameColumnModal } from "../modal/renameColumnModal"; import { CodeSampleModal } from "../modal/showCodeSample"; import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal"; import { CSVColumnContextMenu } from "../menu/csvColumnContextMenu"; -import { AgColumn } from "ag-grid-community"; +import { AgColumn, Column } from "ag-grid-community"; +import { CSVViewMenuBar } from "./CSVViewMenuBar"; const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n)); @@ -236,6 +237,10 @@ export class CSVView extends TextFileView { } } + getIcon(): IconName { + return 'table' + } + moveColumn(name: string, toIndex: number) { let fields = this.result.fields as Array; fields = fields.filter((f) => f !== name); @@ -307,6 +312,38 @@ export class CSVView extends TextFileView { showHidden: boolean = false; + private menu(container: HTMLElement) { + + const buttonsRow = container.createDiv({ + cls: "sql-seal-csv-viewer-buttons", + }); + + const menuBar = new CSVViewMenuBar(buttonsRow, this.settings, this.app) + + const events = menuBar.events + + events.on('add-column', (columnName: string) => { + this.addNewColumn(columnName) + }) + + events.on('add-row', () => { + this.createRow(); + }) + + events.on('generate-code', () => { + if (!this.file) { + return; + } + const modal = new CodeSampleModal(this.app, this.file); + modal.open(); + }) + + events.on('toggle-hidden', (v) => { + this.showHidden = v + this.refreshTypes(); + }) + } + private async renderCSV() { if (this.isLoading) { if (this.api) { @@ -320,57 +357,12 @@ export class CSVView extends TextFileView { 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 showHidden = buttonsRow.createEl('button', { - text: 'Show Hidden Columns' - }) - - showHidden.addEventListener('click', () => { - this.showHidden = !this.showHidden - showHidden.textContent = this.showHidden ? 'Hide Hidden Columns' : 'Show Hidden Columns' - this.refreshTypes() - }) + this.menu(csvEditorDiv) 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(); diff --git a/src/modules/settings/view/CSVViewMenuBar.ts b/src/modules/settings/view/CSVViewMenuBar.ts new file mode 100644 index 0000000..ad9c85d --- /dev/null +++ b/src/modules/settings/view/CSVViewMenuBar.ts @@ -0,0 +1,64 @@ +import { App, ButtonComponent } from "obsidian"; +import { RenameColumnModal } from "../modal/renameColumnModal"; +import { Settings } from "../Settings"; +import { args, BusBuilder } from "@hypersphere/omnibus"; + +const SHOW_HIDDEN_TEXT = "Show Hidden Columns"; +const HIDE_HIDDEN_TEXT = "Hide Hidden Columns"; + +export class CSVViewMenuBar { + private bus = new BusBuilder() + .register("add-row", args<[]>()) + .register("add-column", args()) + .register("generate-code", args<[]>()) + .register("toggle-hidden", args()) + .build(); + + constructor( + private el: HTMLElement, + private settings: Settings, + private app: App, + ) { + this.show(); + } + + private showHidden: boolean = false; + + get events() { + return this.bus.getRegistrator(); + } + + private show() { + const el = this.el; + + el.empty(); + + if (this.settings.get("enableEditing")) { + new ButtonComponent(el) + .setButtonText("Add Row") + .setCta() + .onClick(() => this.bus.trigger("add-row")); + + new ButtonComponent(el).setButtonText("Add Column").onClick(() => { + const modal = new RenameColumnModal(this.app, (res) => { + this.bus.trigger("add-column", res); + }); + modal.open(); + }); + + new ButtonComponent(el) + .setButtonText("Generate SQLSeal Code") + .onClick(() => this.bus.trigger("generate-code")); + + const showHiddenButton = new ButtonComponent(el) + .setButtonText(SHOW_HIDDEN_TEXT) + .onClick(() => { + this.showHidden = !this.showHidden; + showHiddenButton.setButtonText( + this.showHidden ? HIDE_HIDDEN_TEXT : SHOW_HIDDEN_TEXT, + ); + this.bus.trigger("toggle-hidden", this.showHidden); + }); + } + } +} diff --git a/src/modules/settings/view/JsonView.ts b/src/modules/settings/view/JsonView.ts index 0bcb98a..7b67944 100644 --- a/src/modules/settings/view/JsonView.ts +++ b/src/modules/settings/view/JsonView.ts @@ -1,4 +1,4 @@ -import { WorkspaceLeaf, TextFileView, Menu } from 'obsidian'; +import { WorkspaceLeaf, TextFileView, Menu, IconName } from 'obsidian'; import { parse, stringify } from 'json5' export const JSON_VIEW_TYPE = "sqlseal-json-viewer"; @@ -33,6 +33,10 @@ export class JsonView extends TextFileView { // Cleanup if needed } + getIcon(): IconName { + return 'file-json' + } + async setViewData(data: string, clear: boolean): Promise { this.content = data; await this.renderJson(); diff --git a/src/modules/sync/repository/tableAliases.ts b/src/modules/sync/repository/tableAliases.ts index 47ec107..901062b 100644 --- a/src/modules/sync/repository/tableAliases.ts +++ b/src/modules/sync/repository/tableAliases.ts @@ -19,7 +19,14 @@ export class TableAliasesRepository extends Repository { } async deleteMapping(id: string) { - this.db.deleteData(this.TABLE_NAME, [{ id: id }], 'id') + await this.db.deleteData(this.TABLE_NAME, [{ id: id }], 'id') + } + + async deleteMappingByNames(aliasName: string, sourceFileName: string) { + const data = await this.getByAlias(sourceFileName, aliasName) + if (data) { + await this.db.deleteData(this.TABLE_NAME, [{ id: data.id }], 'id') + } } private async createTable() { diff --git a/src/modules/sync/sync/sync.ts b/src/modules/sync/sync/sync.ts index cf7a36d..486089c 100644 --- a/src/modules/sync/sync/sync.ts +++ b/src/modules/sync/sync/sync.ts @@ -63,6 +63,8 @@ export class Sync { // START SYNCING this.startSync() + + await this.refreshGlobalMappings() } async syncFileByName(fileName: string) { @@ -113,6 +115,17 @@ export class Sync { await this.tableDefinitionsRepo.insert(log) } + private globalTables: Record = {} + + async refreshGlobalMappings() { + const globalMappings = await this.tableMapLog.getByContext('/') as { alias_name: string, table_name: string }[] + this.globalTables = Object.fromEntries(globalMappings.map(g => [g.alias_name, g.table_name])) + } + + get globalTablesMapping() { + return this.globalTables + } + async getTablesMappingForContext(sourceFileName: string) { const tables = await this.tableMapLog.getByContext(sourceFileName) as { alias_name: string, table_name: string }[] const map = tables.reduce((acc, t) => ({ @@ -120,11 +133,14 @@ export class Sync { [t.alias_name as string]: t.table_name }), {}) + // FIXME: adding globals here. + return { ...map, + ...this.globalTablesMapping, files: 'files', tasks: 'tasks', - tags: 'tags' + tags: 'tags', } } @@ -171,6 +187,27 @@ export class Sync { }) } } + + if (reg.sourceFile === '/') { + await this.refreshGlobalMappings() + } + } + + unregisterTable(reg: ParserTableDefinition) { + return this.tableMapLog.deleteMappingByNames(reg.tableAlias, reg.sourceFile) + } + + async getStats(sourceFileName: string, table: string) { + const tab = await this.tableMapLog.getByAlias(sourceFileName, table) + if (!tab) { + return { rows: 0, columns: 0 } + } + const columns = await this.db.getColumns(tab.table_name) + const rows = await this.db.count(tab.table_name) + return { + rows, + columns: columns ? columns.length : 0 + } } getRegistrator() { diff --git a/src/styles/autocomplete.scss b/src/styles/autocomplete.scss new file mode 100644 index 0000000..f839cb7 --- /dev/null +++ b/src/styles/autocomplete.scss @@ -0,0 +1,58 @@ +.sqlseal-autocomplete-container { + position: relative; + width: 100%; + + & input[type="text"] { + width: 100%; + } +} + +.sqlseal-autocomplete-dropdown { + position: absolute; + top: 100%; left: 0; + margin-top: 0.5em; + right: 0; + text-align: left; + background: white; + max-height: 200px; + overflow-y: scroll; + border-radius: 5px; + box-shadow: 0 3px 5px #CCC; +} + + +.modal-overflow { + overflow: visible; + // --dialog-width: 1000px; +} + +.sqlseal-dropdown-el { + padding: 4px 16px 4px 8px; + border-top: 1px solid #EEE; + overflow: hidden; + &:first-child { + border-top: 0; + } + + &:hover { + background: #a8c4ef; + } +} + +.sqlseal-dropdown-el-name { + font-weight: bold; + font-size: 0.9em; + text-overflow: ellipsis; + max-width: 100%; + overflow: hidden; + text-wrap: nowrap; +} + +.sqlseal-dropdown-el-path { + opacity: 0.9; + font-size: 0.7em; + text-overflow: ellipsis; + max-width: 100%; + overflow: hidden; + text-wrap: nowrap; +} \ No newline at end of file diff --git a/src/styles/global-tables.scss b/src/styles/global-tables.scss new file mode 100644 index 0000000..098f2b0 --- /dev/null +++ b/src/styles/global-tables.scss @@ -0,0 +1,6 @@ +.sqlseal-global-tables-container { + height: 100%; + display: flex; + flex-direction: column; + gap: 1em; +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 3944e15..3f47e7d 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -6,4 +6,6 @@ @use 'obsidianMinimal'; @use 'agGrid'; @use 'settings'; -@use 'canvas'; \ No newline at end of file +@use 'canvas'; +@use 'autocomplete'; +@use 'global-tables'; From 531d486d06765ec66eeba7e4693775e4d0485781 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 10 Aug 2025 11:08:13 +0200 Subject: [PATCH 4/4] Feat/sql explorer (#174) * 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 * feat: added ability to hide columns from csv files * feat: adding global tables support (wip) * feat: global tables full implementation * feat: sqlite databases can now be previewed in explorer * feat: improved explorer view * feat: highlighting code in the copy code modal * chore: fixing typechecks * chore: adding missing changeset --- .changeset/dirty-sites-study.md | 5 + .changeset/wicked-lights-burn.md | 5 + .changeset/witty-laws-end.md | 5 + src/modules/database/worker/database.ts | 2 +- .../codeblockHandler/CodeblockProcessor.ts | 6 +- src/modules/explorer/Editor.ts | 134 +++++++++++++++ src/modules/explorer/EditorMenuBar.ts | 57 +++++++ .../explorer/FileDatabaseExplorerView.ts | 95 +++++++++++ src/modules/explorer/InitFactory.ts | 80 +++++++++ src/modules/explorer/activateView.ts | 22 +++ .../explorer/database/databaseManager.ts | 32 ++++ .../explorer/database/memoryDatabase.ts | 52 ++++++ src/modules/explorer/explorer/ExplorerView.ts | 83 ++++++++++ src/modules/explorer/explorer/style.scss | 81 +++++++++ src/modules/explorer/module.ts | 31 ++++ .../schemaVisualiser/SchemaVisualiser.ts | 14 ++ .../schemaVisualiser/TableVisualiser.ts | 30 ++++ .../schemaVisualiser/schemaVisualiser.scss | 29 ++++ .../{globalTables => explorer}/sqlseal-bw.svg | 0 .../globalTables/GlobalTablesViewRegister.ts | 57 +++---- src/modules/main/init.ts | 7 +- src/modules/main/module.ts | 15 +- src/modules/settings/SQLSealSettingsTab.ts | 2 - src/modules/settings/init.ts | 6 +- src/modules/settings/modal/showCodeSample.ts | 48 ++++-- src/modules/settings/module.ts | 4 +- .../settingsTabSection/SettingsCSVControls.ts | 7 +- src/modules/settings/view/CSVView.ts | 4 +- .../editorExtension/syntaxHighlight.ts | 156 +++++++++++++----- src/modules/syntaxHighlight/init.ts | 11 +- src/modules/syntaxHighlight/module.ts | 4 +- .../syntaxHighlight/viewPluginGenerator.ts | 22 +++ src/styles/main.scss | 2 + src/styles/syntaxHighlight.scss | 38 ++++- styles.css | 2 + 35 files changed, 1034 insertions(+), 114 deletions(-) create mode 100644 .changeset/dirty-sites-study.md create mode 100644 .changeset/wicked-lights-burn.md create mode 100644 .changeset/witty-laws-end.md create mode 100644 src/modules/explorer/Editor.ts create mode 100644 src/modules/explorer/EditorMenuBar.ts create mode 100644 src/modules/explorer/FileDatabaseExplorerView.ts create mode 100644 src/modules/explorer/InitFactory.ts create mode 100644 src/modules/explorer/activateView.ts create mode 100644 src/modules/explorer/database/databaseManager.ts create mode 100644 src/modules/explorer/database/memoryDatabase.ts create mode 100644 src/modules/explorer/explorer/ExplorerView.ts create mode 100644 src/modules/explorer/explorer/style.scss create mode 100644 src/modules/explorer/module.ts create mode 100644 src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts create mode 100644 src/modules/explorer/schemaVisualiser/TableVisualiser.ts create mode 100644 src/modules/explorer/schemaVisualiser/schemaVisualiser.scss rename src/modules/{globalTables => explorer}/sqlseal-bw.svg (100%) create mode 100644 src/modules/syntaxHighlight/viewPluginGenerator.ts create mode 100644 styles.css diff --git a/.changeset/dirty-sites-study.md b/.changeset/dirty-sites-study.md new file mode 100644 index 0000000..df58571 --- /dev/null +++ b/.changeset/dirty-sites-study.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +Added SQLSeal Explorer that makes it easy to work on new queries diff --git a/.changeset/wicked-lights-burn.md b/.changeset/wicked-lights-burn.md new file mode 100644 index 0000000..951eb06 --- /dev/null +++ b/.changeset/wicked-lights-burn.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +sqlite databases can now be previewed using explorer view diff --git a/.changeset/witty-laws-end.md b/.changeset/witty-laws-end.md new file mode 100644 index 0000000..effa067 --- /dev/null +++ b/.changeset/witty-laws-end.md @@ -0,0 +1,5 @@ +--- +"sqlseal": patch +--- + +highlighting code in the copy modal diff --git a/src/modules/database/worker/database.ts b/src/modules/database/worker/database.ts index 2475588..dd7af47 100644 --- a/src/modules/database/worker/database.ts +++ b/src/modules/database/worker/database.ts @@ -13,7 +13,7 @@ import { ColumnDefinition } from "../../../utils/types"; import { sanitise } from "../../../utils/sanitiseColumn"; -function toObjectArray(stmt: Statement) { +export function toObjectArray(stmt: Statement) { const ret = [] while (stmt.step()) { ret.push(stmt.getAsObject()) diff --git a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts index c1368ac..8fe47d3 100644 --- a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts +++ b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts @@ -26,11 +26,12 @@ export class CodeblockProcessor extends MarkdownRenderChild { private source: string, private ctx: MarkdownPostProcessorContext, private rendererRegistry: RendererRegistry, - private db: SqlSealDatabase, + private db: Pick, private cellParser: ModernCellParser, private settings: Settings, private app: App, private sync: Sync, + private tq: typeof transformQuery = transformQuery ) { super(el); @@ -118,7 +119,8 @@ export class CodeblockProcessor extends MarkdownRenderChild { const registeredTablesForContext = await this.sync.getTablesMappingForContext(this.sourceKey); - const res = transformQuery(this.query, registeredTablesForContext); + // Transforming Query + const res = this.tq(this.query, registeredTablesForContext); const transformedQuery = res.sql; if (this.flags.refresh) { diff --git a/src/modules/explorer/Editor.ts b/src/modules/explorer/Editor.ts new file mode 100644 index 0000000..c8bfb40 --- /dev/null +++ b/src/modules/explorer/Editor.ts @@ -0,0 +1,134 @@ +import { EditorState } from "@codemirror/state"; +import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; +import { EditorView, keymap } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { EditorMenuBar } from "./EditorMenuBar"; +import { MemoryDatabase } from "./database/memoryDatabase"; +import { SchemaVisualiser } from "./schemaVisualiser/SchemaVisualiser"; +import { activateView } from "./activateView"; +import { App } from "obsidian"; +import { GLOBAL_TABLES_VIEW_TYPE } from "../globalTables/GlobalTablesView"; +import { GridApi } from "ag-grid-community"; + +type CodeblockProcessorWrapper = ( + el: HTMLElement, + source: string, +) => Promise; + +const DEFAULT_QUERY = "SELECT *\nFROM files\nLIMIT 10"; + +export class Editor { + constructor( + private codeblockProcessorGenerator: CodeblockProcessorWrapper, + private viewPluginGenerator: ViewPluginGeneratorType, + private app: App, + private query: string = DEFAULT_QUERY, + private db: MemoryDatabase | null = null + ) {} + + codeblockElement: HTMLElement | null = null; + render(el: HTMLElement) { + el.empty(); + const menuBar = new EditorMenuBar(!!this.db); + const c = el.createDiv({ cls: "sqlseal-explorer-container" }); + menuBar.render(c); + const grid = c.createDiv({ cls: "sqlseal-explorer-grid-container" }); + const codeSidebar = grid.createDiv({ cls: "sqlseal-explorer-code" }); + codeSidebar.classList.add("cm-sqlseal-explorer"); + // codeSidebar.textContent = "CODE" + const rightPane = grid.createDiv({ cls: 'sqlseal-explorer-right-pane' }) + const contentSidebar = rightPane.createDiv({ cls: "sqlseal-explorer-render" }); + const structure = rightPane.createDiv({ cls: 'sqlseal-explorer-structure' }) + structure.hide() + + this.codeblockElement = contentSidebar; + + this.createEditor(codeSidebar); + this.createCodeblockProcessor(this.codeblockElement, this.query); + + if (this.db) { + // rendering structure + const schema = this.db.getSchema() + const vis = new SchemaVisualiser(schema) + vis.show(structure) + } else { + } + + const events = menuBar.events; + events.on("play", () => { + this.play() + }); + + events.on('structure', (b) => { + if (structure.checkVisibility({ checkVisibilityCSS: true })) { + // structure visible + structure.hide() + contentSidebar.show(); + (b as any).setIcon('database') + + } else { + // structure invisible + structure.show() + contentSidebar.hide(); + (b as any).setIcon('table') + } + }) + + events.on('globals', () => { + activateView(this.app, GLOBAL_TABLES_VIEW_TYPE) + }) + } + + createCodeblockProcessor(el: HTMLElement, source: string) { + return this.codeblockProcessorGenerator(el, source); + } + + editor: EditorView; + createEditor(el: HTMLElement) { + const state = EditorState.create({ + doc: this.query, + extensions: [ + // this.createCustomLanguage(), + // this.createChangeListener(), + this.createKeyBindings(), + this.viewPluginGenerator(true), + EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { fontFamily: "monospace" }, + ".cm-content": { + caretColor: "var(--color-base-100)", + }, + }), + ], + }); + + this.editor = new EditorView({ + state, + parent: el, + }); + } + + createKeyBindings() { + return keymap.of([ + { + key: "Mod-r", // Save shortcut example + run: () => { + this.play() + return true + }, + }, + ]); + } + + async play() { + this.query = this.editor.state.doc.toString(); + if (this.codeblockElement) { + const processor = await this.createCodeblockProcessor(this.codeblockElement, this.query); + // const renderer = processor.renderer + // if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { + // const api: GridApi = (renderer.communicator as any).gridApi + // api.setGridOption('paginationAutoPageSize', true) + // } + } + } +} diff --git a/src/modules/explorer/EditorMenuBar.ts b/src/modules/explorer/EditorMenuBar.ts new file mode 100644 index 0000000..f9e0ef1 --- /dev/null +++ b/src/modules/explorer/EditorMenuBar.ts @@ -0,0 +1,57 @@ +import { args, BusBuilder } from "@hypersphere/omnibus" +import { ButtonComponent } from "obsidian" + +export class EditorMenuBar { + bus = new BusBuilder() + .register('play', args<[]>()) + .register('structure', args<[ButtonComponent]>()) + .register('globals', args<[]>()) + .build() + constructor(private fileDatabasePreview: boolean = false) { + + } + + strucureButton: ButtonComponent + render(el: HTMLElement) { + const container = el.createDiv({ cls: 'sqlseal-menubar-container' }) + new ButtonComponent(container) + .setIcon('play') + .setClass('sqlseal-menubar-button') + .setTooltip('Run (CMD+R)', { + placement: 'bottom', + delay: 1 + }) + .onClick(() => this.bus.trigger('play')) + + if (this.fileDatabasePreview) { + const b = new ButtonComponent(container) + .setIcon('database') + .setClass('sqlseal-menubar-button') + .setTooltip('Structure', { + placement: 'bottom', + delay: 1 + }) + .onClick(() => { + const but = b + this.bus.trigger('structure', but as any) + }) + } + + container.createDiv({ cls: 'separator' }) + + if (!this.fileDatabasePreview) { + // global button + const b = new ButtonComponent(container) + .setIcon('bolt') + .setClass('sqlseal-menubar-button') + .setTooltip('Global Tables', { delay: 1, placement: 'bottom' }) + .onClick(() => { + this.bus.trigger('globals') + }) + } + } + + get events() { + return this.bus.getRegistrator() + } +} \ No newline at end of file diff --git a/src/modules/explorer/FileDatabaseExplorerView.ts b/src/modules/explorer/FileDatabaseExplorerView.ts new file mode 100644 index 0000000..6510783 --- /dev/null +++ b/src/modules/explorer/FileDatabaseExplorerView.ts @@ -0,0 +1,95 @@ +import { FileView, IconName, MarkdownPostProcessorContext, Menu, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; +import { MemoryDatabase } from "./database/memoryDatabase"; +import { DatabaseManager } from "./database/databaseManager"; +import { TableInfo } from "./schemaVisualiser/TableVisualiser"; +import { SchemaVisualiser } from "./schemaVisualiser/SchemaVisualiser"; +import { ExplorerView } from "./explorer/ExplorerView"; +import { Editor } from "./Editor"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { Settings } from "../settings/Settings"; +import { Sync } from "../sync/sync/sync"; + +export const FILE_DATABASE_VIEW = 'sqlseal-sqlite-file-view' + +const INITIAL_QUERY = "SELECT name\nFROM sqlite_master\nWHERE type='table'" + +export class FileDatabaseExplorerView extends FileView { + constructor( + leaf: WorkspaceLeaf, + private manager: DatabaseManager, + private viewPluginGenerator: ViewPluginGeneratorType, + private rendererRegistry: RendererRegistry, + private cellParser: ModernCellParser, + private settings: Settings, + private sync: Sync, + + ) { + super(leaf) + } + getViewType(): string { + return FILE_DATABASE_VIEW + } + getDisplayText(): string { + return this.file?.basename || 'Database' + } + + async onOpen() { + } + + onPaneMenu(menu: Menu, source: "more-options" | "tab-header" | string): void { + menu.addItem(i => i.setTitle('test')) + } + + db: MemoryDatabase + async onLoadFile(file: TFile): Promise { + const db = await this.manager.getDatabaseConnection(file) + await db.connect() + this.db = db + + // GETTING ALL TABLES + this.schema = db.getSchema() + this.render() + } + + schema: TableInfo[] + render() { + const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { + const ctx: MarkdownPostProcessorContext = { + docId: "", + sourcePath: "", + frontmatter: {}, + } as any; + + const processor = new CodeblockProcessor( + el, + source, + ctx, + this.rendererRegistry, + this.db as any, // FIXME + this.cellParser, + this.settings, + this.app, + this.sync, + ); + await processor.onload(); + await processor.render(); + return processor + } + + const editor = new Editor(codeblockProcessorGenerator, this.viewPluginGenerator,this.app, INITIAL_QUERY, this.db) + + editor.render(this.contentEl) + + // const vis = new SchemaVisualiser(this.schema) + // vis.show(container) + + } + + getIcon(): IconName { + return 'database' + } + +} \ No newline at end of file diff --git a/src/modules/explorer/InitFactory.ts b/src/modules/explorer/InitFactory.ts new file mode 100644 index 0000000..d75dcb8 --- /dev/null +++ b/src/modules/explorer/InitFactory.ts @@ -0,0 +1,80 @@ +import { makeInjector } from "@hypersphere/dity"; +import { ExplorerModule } from "./module"; +import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian"; +import { SqlSealDatabase } from "../database/database"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { Sync } from "../sync/sync/sync"; +import { Settings } from "../settings/Settings"; +import { ExplorerView } from "./explorer/ExplorerView"; +import { ViewPlugin } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { FILE_DATABASE_VIEW, FileDatabaseExplorerView } from "./FileDatabaseExplorerView"; +import { DatabaseManager } from "./database/databaseManager"; +import { activateView } from "./activateView"; + +// @ts-ignore: Handled by esbuild +import SQLSealIcon from "./sqlseal-bw.svg"; + +@(makeInjector()([ + "plugin", + "app", + "db", + "cellParser", + "rendererRegistry", + "sync", + "settings", + "viewPluginGenerator", + "dbManager" +])) +export class InitFactory { + make( + plugin: Plugin, + app: App, + db: SqlSealDatabase, + cellParser: ModernCellParser, + rendererRegistry: RendererRegistry, + sync: Sync, + settings: Settings, + viewPluginGenerator: ViewPluginGeneratorType, + dbManager: DatabaseManager + ) { + + return () => { + plugin.registerView( + "sqlseal-explorer-view", + (leaf) => + new ExplorerView( + leaf, + rendererRegistry, + db, + cellParser, + settings, + sync, + viewPluginGenerator + ), + ); + addIcon("logo-sqlseal", SQLSealIcon); + plugin.addRibbonIcon("logo-sqlseal", "SQLSeal Explorer", () => + activateView(plugin.app, "sqlseal-explorer-view"), + ); + + + // Register for extenion + plugin.registerView(FILE_DATABASE_VIEW, (leaf) => { + return new FileDatabaseExplorerView(leaf, dbManager, viewPluginGenerator, rendererRegistry, cellParser, settings, sync) + }) + + plugin.registerExtensions(['sqlite'], FILE_DATABASE_VIEW) + + plugin.addCommand({ + id: 'sqlseal-command-explorer', + name: 'Open SQLSeal Explorer', + icon: 'logo-sqlseal', + callback: () => activateView(app, 'sqlseal-explorer-view') + + }) + + }; + } +} diff --git a/src/modules/explorer/activateView.ts b/src/modules/explorer/activateView.ts new file mode 100644 index 0000000..374ad00 --- /dev/null +++ b/src/modules/explorer/activateView.ts @@ -0,0 +1,22 @@ +import { App, WorkspaceLeaf } from "obsidian"; + +export const activateView = async (app: App, name: string) => { + const { workspace } = app; + + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(name); + + if (leaves.length > 0) { + // A leaf with our view already exists, use that + leaf = leaves[0]; + } else { + leaf = workspace.getLeaf("tab"); + if (!leaf) { + return; + } + await leaf.setViewState({ type: name, active: true }); + } + + // "Reveal" the leaf in case it is in a collapsed sidebar + workspace.revealLeaf(leaf); +}; diff --git a/src/modules/explorer/database/databaseManager.ts b/src/modules/explorer/database/databaseManager.ts new file mode 100644 index 0000000..7fe03d5 --- /dev/null +++ b/src/modules/explorer/database/databaseManager.ts @@ -0,0 +1,32 @@ +import { TFile } from "obsidian"; +import { MemoryDatabase } from "./memoryDatabase"; +import wasmBinary from '../../../../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm' +import initSqlJs from '@jlongster/sql.js'; + +export class DatabaseManager { + constructor() {} + + private sql: initSqlJs.SqlJsStatic | null = null + + private async getConnection() { + if (this.sql) { + return this.sql + } + const SQL = await initSqlJs({ + wasmBinary: wasmBinary, + }); + this.sql = SQL + return SQL + } + + async getDatabaseConnection(file: TFile) { + // FIXME: connecting to database + const connection = await this.getConnection() + + const db = new MemoryDatabase(connection, file); + await db.connect() + return db + } + + getGlobalDatabaseConnection() {} +} diff --git a/src/modules/explorer/database/memoryDatabase.ts b/src/modules/explorer/database/memoryDatabase.ts new file mode 100644 index 0000000..dcfc6ae --- /dev/null +++ b/src/modules/explorer/database/memoryDatabase.ts @@ -0,0 +1,52 @@ +import { TFile } from "obsidian"; +import { BindParams, Database, ParamsObject } from "sql.js"; +import { toObjectArray } from "../../database/worker/database"; +import { TableInfo } from "../schemaVisualiser/TableVisualiser"; + +export class MemoryDatabase { + private db: Database + constructor(private sql: initSqlJs.SqlJsStatic, private file: TFile) { + + } + + async connect() { + const binary = await this.file.vault.readBinary(this.file) + this.db = new this.sql.Database(Buffer.from(binary)) + } + + query(query: string, params: BindParams = null): { data: T[], columns: keyof T } { + const stmt = this.db.prepare(query, params) + const data = toObjectArray(stmt) + const columns = stmt.getColumnNames() + stmt.free() + + return { data: data, columns } as any + } + + select(query: string, params: BindParams = null) { + return this.query(query, params) + } + + explain() { + return {} + } + + allTables() { + return this.query<{name: string}>(`select name from sqlite_master where type='table'`) + } + + getColumns(tableName: string) { + return this.query<{ name: string, type: string }>('select name, type from pragma_table_info(@tableName)', { '@tableName': tableName }) + } + + getSchema(): TableInfo[] { + const tables = this.allTables().data + return tables.map(t => { + const columns = this.getColumns(t.name) + return { + name: t.name, + columns: columns.data + } + }) + } +} \ No newline at end of file diff --git a/src/modules/explorer/explorer/ExplorerView.ts b/src/modules/explorer/explorer/ExplorerView.ts new file mode 100644 index 0000000..fe0907d --- /dev/null +++ b/src/modules/explorer/explorer/ExplorerView.ts @@ -0,0 +1,83 @@ +import { EditorState } from "@codemirror/state"; +import { EditorView, keymap, ViewPlugin } from "@codemirror/view"; +import { + ItemView, + MarkdownPostProcessorContext, + WorkspaceLeaf, +} from "obsidian"; +import { CodeblockProcessor } from "../../editor/codeblockHandler/CodeblockProcessor"; +import { SqlSealDatabase } from "../../database/database"; +import { RendererRegistry } from "../../editor/renderer/rendererRegistry"; +import { CellParser } from "../../../../types-package/dist/src/cellParser"; +import { Settings } from "../../settings/Settings"; +import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser"; +import { Sync } from "../../sync/sync/sync"; +import { Language } from "@codemirror/language"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; +import { Editor } from "../Editor"; +import { GridApi } from "ag-grid-community"; + +export class ExplorerView extends ItemView { + constructor( + leaf: WorkspaceLeaf, + private rendererRegistry: RendererRegistry, + private db: Pick, + private cellParser: ModernCellParser, + private settings: Settings, + private sync: Sync, + private viewPluginGenerator: ViewPluginGeneratorType + ) { + super(leaf); + } + private editor: EditorView; + getViewType() { + return "sqlseal-explorer-view"; + } + getDisplayText() { + return "SQLSeal Explorer"; + } + + getIcon() { + return 'logo-sqlseal' + } + async onOpen() { + const content = this.contentEl; + + + const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { + const ctx: MarkdownPostProcessorContext = { + docId: "", + sourcePath: "", + frontmatter: {}, + } as any; + + const processor = new CodeblockProcessor( + el, + source, + ctx, + this.rendererRegistry, + this.db, + this.cellParser, + this.settings, + this.app, + this.sync, + ); + await processor.onload(); + + + // Resizing + const renderer = processor.renderer + if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { + const api: GridApi = (renderer.communicator as any).gridApi + api.setGridOption('paginationAutoPageSize', true) + } + + await processor.render(); + return processor + } + + const editor = new Editor(codeblockProcessorGenerator, this.viewPluginGenerator, this.app) + + editor.render(content) + } +} diff --git a/src/modules/explorer/explorer/style.scss b/src/modules/explorer/explorer/style.scss new file mode 100644 index 0000000..efd8c69 --- /dev/null +++ b/src/modules/explorer/explorer/style.scss @@ -0,0 +1,81 @@ +@use '../schemaVisualiser/schemaVisualiser.scss'; + +.sqlseal-explorer-container { + height: 100%; + width: 100%; + max-height: 100%; + display: grid; + grid-template-rows: auto 1fr; +} + +.sqlseal-explorer-grid-container { + display: grid; + grid-template-columns: 50% 50%; + gap: 1em; + height: 100%; + width: 100%; + padding: 1em; +} + +.sqlseal-explorer-code { + // background: orange; +} + +.sqlseal-explorer-render { + // background: red; + overflow: scroll; +} + +.sqlseal-menubar-container { + padding: 0.2em 0; + display: flex; +} + +.sqlseal-menubar-container .separator { + flex: 1; +} + +.sqlseal-menubar-button { + border: 0; + --input-shadow: none; +} + +.sqlseal-explorer-right-pane { + display: grid; + container-type: inline-size; + height: 100%; +// place-content: center; +// align-content: stretch; +// grid-template-rows: 100%; +// width: 100%; +// height: 100%; + & > * { +// width: 100%; +// height: 100%; + grid-area: 1 / 1; + height: 100%; + } +} + +.sqlseal-explorer-right-pane { + + & .ag-root-wrapper { + height: 100%; + } + + & .ag-root-wrapper-body { + flex: 1; + } + + & .ag-paging-page-size { + display: none; + } + + & .ag-paging-row-summary-panel { + display: none; + } + + & .ag-paging-panel { + justify-content: center; + } +} diff --git a/src/modules/explorer/module.ts b/src/modules/explorer/module.ts new file mode 100644 index 0000000..81cd238 --- /dev/null +++ b/src/modules/explorer/module.ts @@ -0,0 +1,31 @@ +import { asClass, asFactory, buildContainer } from "@hypersphere/dity"; +import { InitFactory } from "./InitFactory"; +import { App, Plugin } from "obsidian"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { SqlSealDatabase } from "../database/database"; +import { Settings } from "../settings/Settings"; +import { Sync } from "../sync/sync/sync"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { ViewPlugin } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { DatabaseManager } from "./database/databaseManager"; + +export const explorer = buildContainer((c) => + c + .register({ + init: asFactory(InitFactory), + dbManager: asClass(DatabaseManager) + }) + .externals<{ + app: App, + cellParser: ModernCellParser, + db: SqlSealDatabase, + settings: Settings, + sync: Sync + rendererRegistry: RendererRegistry, + plugin: Plugin, + viewPluginGenerator: ViewPluginGeneratorType + }>(), +); + +export type ExplorerModule = typeof explorer; diff --git a/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts b/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts new file mode 100644 index 0000000..94c1186 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts @@ -0,0 +1,14 @@ +import { TableInfo, TableVisualiser } from "./TableVisualiser" + +export class SchemaVisualiser { + constructor(private info: TableInfo[]) { } + + show(container: HTMLElement) { + container.empty() + + const c = container.createDiv({ cls: 'sqlseal-tv-container' }) + this.info + .map(i => new TableVisualiser(i)) + .forEach(v => v.show(c)) + } +} \ No newline at end of file diff --git a/src/modules/explorer/schemaVisualiser/TableVisualiser.ts b/src/modules/explorer/schemaVisualiser/TableVisualiser.ts new file mode 100644 index 0000000..902f0f3 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/TableVisualiser.ts @@ -0,0 +1,30 @@ +export interface ColumnInfo { + name: string + type: string +} +export interface TableInfo { + name: string + columns: ColumnInfo[] +} + +export class TableVisualiser { + constructor(private info: TableInfo) { + + } + + show(el: HTMLElement) { + const cont = el.createDiv({ cls: 'sqlseal-tv-table' }) + cont.createDiv({ cls: 'sqlseal-tv-table-name', text: this.info.name }) + const columns = cont.createEl('ul', { cls: 'sqlseal-tv-columns' }) + this.info.columns.forEach(info => { + columns.appendChild(this.createColumn(info)) + }) + } + + createColumn(info: ColumnInfo) { + const row = createEl('li') + row.createEl('span', { text: info.name, cls: 'sqlseal-tv-column-name' }) + row.createEl('span', { text: info.type, cls: 'sqlseal-tv-column-class' }) + return row + } +} \ No newline at end of file diff --git a/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss b/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss new file mode 100644 index 0000000..62ebbf3 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss @@ -0,0 +1,29 @@ +.sqlseal-tv-container { + gap: 1em; +} + +.sqlseal-tv-table { + display: inline-block; + border: 1px solid black; + margin-right: 1em; + margin-bottom: 1em; +} + +.sqlseal-tv-table-name { + padding: 0.5em 1em; + background: #DDD; + border-bottom: 2px solid black; +} + +.sqlseal-tv-columns { + list-style: none; + margin: 0; + padding: 0.5em 1em; +} + +.sqlseal-tv-columns li { + display: flex; + flex-direction: row; + gap: 1em; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/modules/globalTables/sqlseal-bw.svg b/src/modules/explorer/sqlseal-bw.svg similarity index 100% rename from src/modules/globalTables/sqlseal-bw.svg rename to src/modules/explorer/sqlseal-bw.svg diff --git a/src/modules/globalTables/GlobalTablesViewRegister.ts b/src/modules/globalTables/GlobalTablesViewRegister.ts index 5176067..ace5382 100644 --- a/src/modules/globalTables/GlobalTablesViewRegister.ts +++ b/src/modules/globalTables/GlobalTablesViewRegister.ts @@ -1,46 +1,27 @@ import { makeInjector } from "@hypersphere/dity"; import { GlobalTablesModule } from "./module"; -import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian"; +import { App, Plugin, WorkspaceLeaf } from "obsidian"; import { GLOBAL_TABLES_VIEW_TYPE, GlobalTablesView } from "./GlobalTablesView"; -// @ts-ignore: Handled by esbuild -import SQLSealIcon from './sqlseal-bw.svg' import { Sync } from "../sync/sync/sync"; +import { activateView } from "../explorer/activateView"; - -@(makeInjector()( - ['plugin', 'app', 'sync'] -)) +@(makeInjector()(["plugin", "app", "sync"])) export class GlobalTablesViewRegister { - make(plugin: Plugin, app: App, sync: Sync) { + make(plugin: Plugin, app: App, sync: Sync) { - const activateView = async () => { - const { workspace } = plugin.app; + return () => { + plugin.registerView( + GLOBAL_TABLES_VIEW_TYPE, + (leaf) => new GlobalTablesView(leaf, app.vault, sync), + ); - let leaf: WorkspaceLeaf | null = null; - const leaves = workspace.getLeavesOfType(GLOBAL_TABLES_VIEW_TYPE); - - if (leaves.length > 0) { - // A leaf with our view already exists, use that - leaf = leaves[0]; - } else { - leaf = workspace.getLeaf('tab') - if (!leaf) { return } - await leaf.setViewState({ type: GLOBAL_TABLES_VIEW_TYPE, active: true }); - } - - // "Reveal" the leaf in case it is in a collapsed sidebar - workspace.revealLeaf(leaf); - } - - return () => { - plugin.registerView(GLOBAL_TABLES_VIEW_TYPE, leaf => new GlobalTablesView(leaf, app.vault, sync)) - - - // SQLSeal Icon - addIcon('logo-sqlseal', SQLSealIcon) - - plugin.addRibbonIcon('logo-sqlseal', 'SQLSeal Global Tables', activateView) - - } - } -} \ No newline at end of file + plugin.addCommand({ + id: 'sqlseal-command-global-tables', + name: 'Open global tables configuration', + icon: 'logo-sqlseal', + callback: () => activateView(app, GLOBAL_TABLES_VIEW_TYPE) + + }) + }; + } +} diff --git a/src/modules/main/init.ts b/src/modules/main/init.ts index 8cfa8af..4aceb24 100644 --- a/src/modules/main/init.ts +++ b/src/modules/main/init.ts @@ -12,7 +12,8 @@ type InitFn = () => void 'sync.init', 'debug.init', 'api.init', - 'globalTables.init' + 'globalTables.init', + 'explorer.init' ])) export class Init { async make( @@ -23,7 +24,8 @@ export class Init { syncInit: InitFn, debugInit: InitFn, apiInit: InitFn, - globalTablesInit: InitFn + globalTablesInit: InitFn, + explorerInit: InitFn ) { return () => { settingsInit() @@ -34,6 +36,7 @@ export class Init { // debugInit() apiInit() globalTablesInit() + explorerInit() } } } \ No newline at end of file diff --git a/src/modules/main/module.ts b/src/modules/main/module.ts index 7126739..e4e0ab8 100644 --- a/src/modules/main/module.ts +++ b/src/modules/main/module.ts @@ -11,6 +11,7 @@ import { contextMenu } from '../contextMenu/module' import { debugModule } from '../debug/module' import { apiModule } from '../api/module' import { globalTables } from '../globalTables/module' +import { explorer } from '../explorer/module' const obsidian = buildContainer(c => c .externals<{ @@ -32,7 +33,8 @@ export const mainModule = buildContainer(c => c contextMenu, debug: debugModule, api: apiModule, - globalTables + globalTables, + explorer }) .register({ init: asFactory(Init) @@ -59,6 +61,7 @@ export const mainModule = buildContainer(c => c 'settings.app': 'obsidian.app', 'settings.plugin': 'obsidian.plugin', 'settings.cellParser': 'syntaxHighlight.cellParser', + 'settings.viewPluginGenerator': 'syntaxHighlight.viewPluginGenerator' }) .resolve({ 'syntaxHighlight.app': 'obsidian.app', @@ -84,6 +87,16 @@ export const mainModule = buildContainer(c => c 'globalTables.app': 'obsidian.app', 'globalTables.sync': 'sync.syncBus' }) + .resolve({ + 'explorer.app': 'obsidian.app', + 'explorer.cellParser': 'syntaxHighlight.cellParser', + 'explorer.db': 'db.db', + 'explorer.settings': 'settings.settings', + 'explorer.plugin': 'obsidian.plugin', + 'explorer.rendererRegistry': 'editor.rendererRegistry', + 'explorer.sync': 'sync.syncBus', + 'explorer.viewPluginGenerator': 'syntaxHighlight.viewPluginGenerator' + }) ) export type MainModule = typeof mainModule diff --git a/src/modules/settings/SQLSealSettingsTab.ts b/src/modules/settings/SQLSealSettingsTab.ts index 6cfee73..432f903 100644 --- a/src/modules/settings/SQLSealSettingsTab.ts +++ b/src/modules/settings/SQLSealSettingsTab.ts @@ -2,8 +2,6 @@ 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 { diff --git a/src/modules/settings/init.ts b/src/modules/settings/init.ts index d961177..7d98168 100644 --- a/src/modules/settings/init.ts +++ b/src/modules/settings/init.ts @@ -5,16 +5,18 @@ import { SQLSealSettingsTab } from "./SQLSealSettingsTab"; import { Settings } from "./Settings"; import { SettingsCSVControls } from "./settingsTabSection/SettingsCSVControls"; import { SettingsJsonControls } from "./settingsTabSection/SettingsJsonControls"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; -@(makeInjector()(["plugin", "settingsTab", "app", "settings"])) +@(makeInjector()(["plugin", "settingsTab", "app", "settings", "viewPluginGenerator"])) export class SettingsInit { async make( plugin: Plugin, settingsTab: SQLSealSettingsTab, app: App, settings: Settings, + viewPluginGenerator: ViewPluginGeneratorType ) { - const csvControl = new SettingsCSVControls(settings, app, plugin); + const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator); const jsonControl = new SettingsJsonControls(settings, app, plugin); const controls = [csvControl, jsonControl]; diff --git a/src/modules/settings/modal/showCodeSample.ts b/src/modules/settings/modal/showCodeSample.ts index d51e1c5..4daf87f 100644 --- a/src/modules/settings/modal/showCodeSample.ts +++ b/src/modules/settings/modal/showCodeSample.ts @@ -1,8 +1,18 @@ import { App, Modal, Notice, Setting, TFile } from "obsidian"; import { sanitise } from "../../../utils/sanitiseColumn"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; + +const query = (tableName: string, path: string) => `\`\`\`sqlseal +TABLE ${tableName} = file(${path}) + +SELECT * FROM ${tableName} +LIMIT 100 +\`\`\`` export class CodeSampleModal extends Modal { - constructor(app: App, private file: TFile) { + constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType) { super(app); } @@ -10,29 +20,37 @@ export class CodeSampleModal extends Modal { const {contentEl} = this; contentEl.createEl('h2', {text: 'SQLSeal Code'}); - // Add container for code - const textArea = contentEl.createEl('textarea', { - cls: 'sql-seal-modal-code', - attr: { - rows: '10' - } - }); + contentEl.classList.add('sqlseal-modal-copycode') + // Setup actual editor here const tableName = sanitise(this.file.basename) - - textArea.setText(`\`\`\`sqlseal -TABLE ${tableName} = file(${this.file.path}) + const q = query(tableName, this.file.path) -SELECT * FROM ${tableName} -LIMIT 100 -\`\`\``); + const state = EditorState.create({ + doc: q, + extensions: [ + this.viewPluginGenerator(false), + EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { fontFamily: "monospace" }, + ".cm-content": { + caretColor: "var(--color-base-100)", + }, + }), + ] + }) + + new EditorView({ + state, + parent: contentEl.createDiv({ cls: 'cm-sqlseal-overlay' }), + }); // Add copy button new Setting(contentEl) .addButton(button => button .setButtonText('Copy to Clipboard') .onClick(async () => { - await navigator.clipboard.writeText(textArea.getText()); + await navigator.clipboard.writeText(q); new Notice('Copied to clipboard!'); })); } diff --git a/src/modules/settings/module.ts b/src/modules/settings/module.ts index 3980c52..16a90c7 100644 --- a/src/modules/settings/module.ts +++ b/src/modules/settings/module.ts @@ -4,12 +4,14 @@ import { SettingsFactory } from "./settingsFactory"; import { SQLSealSettingsTab } from "./SQLSealSettingsTab"; import { SettingsInit } from "./init"; import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; export const settingsModule = buildContainer(c => c .externals<{ 'plugin': Plugin, 'app': App, - 'cellParser': ModernCellParser + 'cellParser': ModernCellParser, + 'viewPluginGenerator': ViewPluginGeneratorType }>() .register({ 'settings': asFactory(SettingsFactory), diff --git a/src/modules/settings/settingsTabSection/SettingsCSVControls.ts b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts index 96dbe83..9549e65 100644 --- a/src/modules/settings/settingsTabSection/SettingsCSVControls.ts +++ b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts @@ -5,10 +5,15 @@ import { } from "../utils/viewInspector"; import { SettingsControls } from "./SettingsControls"; import { CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE, CSVView } from "../view/CSVView"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; export class SettingsCSVControls extends SettingsControls { private registeredView: string | null = null; + constructor(settings: Settings, app: App, plugin: Plugin, private viewPluginGenerator: ViewPluginGeneratorType) { + super(settings, app, plugin) + } + register() { if (this.settings.get("enableViewer")) { const view = checkTypeViewAvaiability(this.app, CSV_VIEW_EXTENSIONS[0]); @@ -19,7 +24,7 @@ export class SettingsCSVControls extends SettingsControls { this.plugin.registerView( CSV_VIEW_TYPE, - (leaf) => new CSVView(leaf, this.settings), + (leaf) => new CSVView(leaf, this.settings, this.viewPluginGenerator), ); this.plugin.registerExtensions(CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE); } diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts index 2cb02e2..64e0f92 100644 --- a/src/modules/settings/view/CSVView.ts +++ b/src/modules/settings/view/CSVView.ts @@ -14,6 +14,7 @@ import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal"; import { CSVColumnContextMenu } from "../menu/csvColumnContextMenu"; import { AgColumn, Column } from "ag-grid-community"; import { CSVViewMenuBar } from "./CSVViewMenuBar"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n)); @@ -28,6 +29,7 @@ export class CSVView extends TextFileView { constructor( leaf: WorkspaceLeaf, private readonly settings: Settings, + private readonly viewPluginGenerator: ViewPluginGeneratorType ) { super(leaf); } @@ -334,7 +336,7 @@ export class CSVView extends TextFileView { if (!this.file) { return; } - const modal = new CodeSampleModal(this.app, this.file); + const modal = new CodeSampleModal(this.app, this.file, this.viewPluginGenerator); modal.open(); }) diff --git a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts index b6c23f4..a03b6b0 100644 --- a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts +++ b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts @@ -17,6 +17,11 @@ import { FilePathWidget } from './widgets/FilePathWidget'; import { RendererRegistry } from '../../editor/renderer/rendererRegistry'; import { SQLSealLangDefinition } from '../../editor/parser'; +interface CodeBlockMatch { + startIndex: number, + content: string +} + const markDecorations = { blockFlag: Decoration.mark({ class: 'cm-sqlseal-block-flag' }), blockQuery: Decoration.mark({ class: 'cm-sqlseal-block-query' }), @@ -39,7 +44,7 @@ export class SQLSealViewPlugin implements PluginValue { private readonly app: App; private readonly renderers: RendererRegistry; - constructor(view: EditorView, app: App, renderers: RendererRegistry) { + constructor(view: EditorView, app: App, renderers: RendererRegistry, private allIsCode: boolean) { this.app = app; this.renderers = renderers; this.decorations = this.buildDecorations(view); @@ -69,59 +74,134 @@ export class SQLSealViewPlugin implements PluginValue { return results } - private buildDecorations(view: EditorView): DecorationSet { - const builder: Array> = []; + private getCodeBlocks(view: EditorView): CodeBlockMatch[] { const text = view.state.doc.toString(); + if (this.allIsCode) { + return [{ + startIndex: 0, + content: text + }] + } + + // Parsing const codeBlockRegex = /```(sqlseal)\n([\s\S]*?)```/g; let match; - + let results: CodeBlockMatch[] = [] while ((match = codeBlockRegex.exec(text)) !== null) { const blockStart = match.index; const langTagEnd = blockStart + match[1].length + 3; const sqlContent = match[2]; const contentStart = langTagEnd + 1; + results.push({ + content: sqlContent, + startIndex: contentStart + }) + } + return results + } + decorateFilename(dec: Decorator, { content, startIndex }: CodeBlockMatch) { + let hasQuotes = false; + // Get the actual filename text from the document + let filePath = content.slice(dec.start, dec.end) - const decorations = this.parseWithGrammar(sqlContent); + // Remove leading & trailing quotes, if captured. + if (filePath.startsWith('"')) { + filePath = filePath.substring(1, filePath.length - 1) + hasQuotes = true; + } - if (decorations) { - decorations.forEach(dec => { - if (dec.type === 'filename') { - let hasQuotes = false; - // Get the actual filename text from the document - let filePath = view.state.doc.sliceString( - contentStart + dec.start, - contentStart + dec.end - ); + // Create widget decoration for the filename + const widget = new FilePathWidget(filePath, this.app); + return Decoration.replace({ + widget, + inclusive: true + }).range( + startIndex + dec.start + Number(hasQuotes), + startIndex + dec.end - Number(hasQuotes) + ) + } - // Remove leading & trailing quotes, if captured. - if (filePath.startsWith('"')) { - filePath = filePath.substring(1, filePath.length - 1) - hasQuotes = true; - } - - // Create widget decoration for the filename - const widget = new FilePathWidget(filePath, this.app); - builder.push(Decoration.replace({ - widget, - inclusive: true - }).range( - contentStart + dec.start + Number(hasQuotes), - contentStart + dec.end - Number(hasQuotes) - )); - } else { - const decoration = markDecorations[dec.type as keyof typeof markDecorations]; + privateDecorateCodeblock(codeblockMatch: CodeBlockMatch): Array> { + const { content, startIndex } = codeblockMatch + const decorations = this.parseWithGrammar(content); + return (decorations || []).flatMap(dec => { + switch (dec.type) { + case 'filename': + return this.decorateFilename(dec, codeblockMatch) + default: + const decoration = markDecorations[dec.type as keyof typeof markDecorations]; if (decoration) { - builder.push(decoration.range( - contentStart + dec.start, - contentStart + dec.end - )); + return decoration.range( + startIndex + dec.start, + startIndex + dec.end + ) + } else { + return [] } } }); - } - } + } - return Decoration.set(builder, true); + private buildDecorations(view: EditorView): DecorationSet { + const builder: Array> = []; + // const text = view.state.doc.toString(); + // const codeBlockRegex = /```(sqlseal)\n([\s\S]*?)```/g; + // let match; + + const results = this.getCodeBlocks(view) + const decorators = results.flatMap(r => this.privateDecorateCodeblock(r)) + + return Decoration.set(decorators, true); + + + // while ((match = codeBlockRegex.exec(text)) !== null) { + // const blockStart = match.index; + // const langTagEnd = blockStart + match[1].length + 3; + // const sqlContent = match[2]; + // const contentStart = langTagEnd + 1; + + + // const decorations = this.parseWithGrammar(sqlContent) + + // if (decorations) { + // decorations.forEach(dec => { + // if (dec.type === 'filename') { + // let hasQuotes = false; + // // Get the actual filename text from the document + // let filePath = view.state.doc.sliceString( + // contentStart + dec.start, + // contentStart + dec.end + // ); + + // // Remove leading & trailing quotes, if captured. + // if (filePath.startsWith('"')) { + // filePath = filePath.substring(1, filePath.length - 1) + // hasQuotes = true; + // } + + // // Create widget decoration for the filename + // const widget = new FilePathWidget(filePath, this.app); + // builder.push(Decoration.replace({ + // widget, + // inclusive: true + // }).range( + // contentStart + dec.start + Number(hasQuotes), + // contentStart + dec.end - Number(hasQuotes) + // )); + // } else { + // const decoration = markDecorations[dec.type as keyof typeof markDecorations]; + // if (decoration) { + // builder.push(decoration.range( + // contentStart + dec.start, + // contentStart + dec.end + // )); + // } + // } + // }); + // } + // } + + // return Decoration.set(builder, true); } } \ No newline at end of file diff --git a/src/modules/syntaxHighlight/init.ts b/src/modules/syntaxHighlight/init.ts index a85ed11..aa84e32 100644 --- a/src/modules/syntaxHighlight/init.ts +++ b/src/modules/syntaxHighlight/init.ts @@ -6,18 +6,13 @@ import { RendererRegistry } from "../editor/renderer/rendererRegistry"; import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight"; @(makeInjector()([ - 'app', 'rendererRegistry', 'plugin' + 'plugin', 'viewPluginGenerator' ])) export class SyntaxHighlightInit { - make(app: App, rendererRegistry: RendererRegistry, plugin: Plugin) { + make(plugin: Plugin, viewPluginGenerator: () => ViewPlugin) { return () => { // FIXME: settings here. - plugin.registerEditorExtension([ - ViewPlugin.define( - (view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry), - { decorations: v => v.decorations } - ) - ]); + plugin.registerEditorExtension([viewPluginGenerator()]); } } } \ No newline at end of file diff --git a/src/modules/syntaxHighlight/module.ts b/src/modules/syntaxHighlight/module.ts index 6fad2f5..2e3ef05 100644 --- a/src/modules/syntaxHighlight/module.ts +++ b/src/modules/syntaxHighlight/module.ts @@ -4,6 +4,7 @@ import { App, Plugin } from "obsidian"; import { RendererRegistry } from "../editor/renderer/rendererRegistry"; import { CellParserFactory } from "./cellParser/factory"; import { SqlSealDatabase } from "../database/database"; +import { ViewPluginGenerator } from "./viewPluginGenerator"; export const syntaxHighlight = buildContainer((c) => c @@ -16,8 +17,9 @@ export const syntaxHighlight = buildContainer((c) => .register({ init: asFactory(SyntaxHighlightInit), cellParser: asFactory(CellParserFactory), + viewPluginGenerator: asFactory(ViewPluginGenerator) }) - .exports("init", "cellParser"), + .exports("init", "cellParser", "viewPluginGenerator"), ); export type SyntaxHighlightModule = typeof syntaxHighlight; diff --git a/src/modules/syntaxHighlight/viewPluginGenerator.ts b/src/modules/syntaxHighlight/viewPluginGenerator.ts new file mode 100644 index 0000000..35b1193 --- /dev/null +++ b/src/modules/syntaxHighlight/viewPluginGenerator.ts @@ -0,0 +1,22 @@ +import { makeInjector } from "@hypersphere/dity"; +import { SyntaxHighlightModule } from "./module"; +import { EditorView, ViewPlugin } from "@codemirror/view"; +import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight"; +import { App } from "obsidian"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; + +export type ViewPluginGeneratorType = ReturnType + +@(makeInjector()([ + 'app', 'rendererRegistry' +])) +export class ViewPluginGenerator { + make(app: App, rendererRegistry: RendererRegistry) { + return (allIsCode: boolean = false) => { + return ViewPlugin.define( + (view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry, allIsCode), + { decorations: v => v.decorations } + ) + } + } +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 3f47e7d..357ab1e 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -9,3 +9,5 @@ @use 'canvas'; @use 'autocomplete'; @use 'global-tables'; + +@use '../modules/explorer/explorer/style.scss'; \ No newline at end of file diff --git a/src/styles/syntaxHighlight.scss b/src/styles/syntaxHighlight.scss index af42d9a..9951b9b 100644 --- a/src/styles/syntaxHighlight.scss +++ b/src/styles/syntaxHighlight.scss @@ -97,7 +97,7 @@ color: #6ff77a; } -:is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { +:is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table, .cm-sqlseal-explorer)::before { background-color: var(--color); width: 5px; top: -3px; @@ -111,3 +111,39 @@ .cm-indent + :is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { display: none; } + +.cm-sqlseal-explorer .cm-line { + position: relative; +} + +.cm-sqlseal-overlay { + border: 1px solid #AAA; + padding: 1em; + border-radius: 5px; +} + +.cm-sqlseal-overlay .cm-line { + position: relative; +} + +.cm-sqlseal-overlay .cm-editor { + outline: none; +} + +.sqlseal-modal-copycode .setting-item { + border: 0; +} + +.cm-sqlseal-explorer :is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { + left: -3px; +} + +.cm-sqlseal-explorer .cm-focused { + outline: none; +} + +.cm-sqlseal-explorer .cm-editor { + border-left-width: 0; + background: var(--color-base-20); + padding: 8px; +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..4134ea6 --- /dev/null +++ b/styles.css @@ -0,0 +1,2 @@ +.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table,.cm-sqlseal-explorer):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.cm-sqlseal-explorer .cm-line{position:relative}.cm-sqlseal-overlay{border:1px solid #AAA;padding:1em;border-radius:5px}.cm-sqlseal-overlay .cm-line{position:relative}.cm-sqlseal-overlay .cm-editor{outline:none}.sqlseal-modal-copycode .setting-item{border:0}.cm-sqlseal-explorer :is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{left:-3px}.cm-sqlseal-explorer .cm-focused{outline:none}.cm-sqlseal-explorer .cm-editor{border-left-width:0;background:var(--color-base-20);padding:8px}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important}.sqlseal-hidden-column{opacity:.5}.sqlseal-settings-warn{background:#ffa600;padding:16px;border:2px solid rgb(134,87,0);border-radius:5px;margin:.5em 2em}.sqlseal-autocomplete-container{position:relative;width:100%}.sqlseal-autocomplete-container input[type=text]{width:100%}.sqlseal-autocomplete-dropdown{position:absolute;top:100%;left:0;margin-top:.5em;right:0;text-align:left;background:#fff;max-height:200px;overflow-y:scroll;border-radius:5px;box-shadow:0 3px 5px #ccc}.modal-overflow{overflow:visible}.sqlseal-dropdown-el{padding:4px 16px 4px 8px;border-top:1px solid #EEE;overflow:hidden}.sqlseal-dropdown-el:first-child{border-top:0}.sqlseal-dropdown-el:hover{background:#a8c4ef}.sqlseal-dropdown-el-name{font-weight:700;font-size:.9em;text-overflow:ellipsis;max-width:100%;overflow:hidden;text-wrap:nowrap}.sqlseal-dropdown-el-path{opacity:.9;font-size:.7em;text-overflow:ellipsis;max-width:100%;overflow:hidden;text-wrap:nowrap}.sqlseal-global-tables-container{height:100%;display:flex;flex-direction:column;gap:1em}.sqlseal-tv-container{gap:1em}.sqlseal-tv-table{display:inline-block;border:1px solid black;margin-right:1em;margin-bottom:1em}.sqlseal-tv-table-name{padding:.5em 1em;background:#ddd;border-bottom:2px solid black}.sqlseal-tv-columns{list-style:none;margin:0;padding:.5em 1em}.sqlseal-tv-columns li{display:flex;flex-direction:row;gap:1em;justify-content:space-between}.sqlseal-explorer-container{height:100%;width:100%;max-height:100%;display:grid;grid-template-rows:auto 1fr}.sqlseal-explorer-grid-container{display:grid;grid-template-columns:50% 50%;gap:1em;height:100%;width:100%;padding:1em}.sqlseal-explorer-render{overflow:scroll}.sqlseal-menubar-container{padding:.2em 0;display:flex}.sqlseal-menubar-container .separator{flex:1}.sqlseal-menubar-button{border:0;--input-shadow: none}.sqlseal-explorer-right-pane{display:grid;container-type:inline-size;height:100%}.sqlseal-explorer-right-pane>*{grid-area:1/1;height:100%}.sqlseal-explorer-right-pane .ag-root-wrapper{height:100%}.sqlseal-explorer-right-pane .ag-root-wrapper-body{flex:1}.sqlseal-explorer-right-pane .ag-paging-page-size,.sqlseal-explorer-right-pane .ag-paging-row-summary-panel{display:none}.sqlseal-explorer-right-pane .ag-paging-panel{justify-content:center} +/*# sourceMappingURL=styles.css.map */