From 653820a66a5ac1b6a4ffff21e2fef2dfd0e62fcb Mon Sep 17 00:00:00 2001 From: Mantano <91400447+mntno@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:56:05 +0700 Subject: [PATCH] 0.6.1 --- .gitignore | 9 +- README.md | 2 + esbuild.config.mjs | 7 +- eslint.config.js | 21 +- manifest.json | 4 +- package.json | 21 +- pnpm-lock.yaml | 1004 ++++++++--------- src/ContentParser.ts | 48 +- src/Settings.ts | 12 +- src/TypeAssistant.ts | 20 +- src/commands/editor.ts | 70 ++ src/commands/openView.ts | 17 +- src/constants.ts | 14 + src/data/DataStore.ts | 203 +++- src/data/FullID.ts | 4 +- src/data/SyncManager.ts | 97 +- src/declarations/CardDeclaration.ts | 219 +--- src/declarations/Collectionable.ts | 82 ++ src/declarations/CommandDeclaration.ts | 106 -- src/declarations/CommandDeclarationParser.ts | 44 +- src/declarations/CommandNames.ts | 35 + src/declarations/Commandable.ts | 40 + src/declarations/Declarable.ts | 102 ++ src/declarations/Declaration.ts | 143 --- src/declarations/DeclarationCodec.ts | 54 + src/declarations/DeclarationManager.ts | 68 +- src/declarations/DeclarationParser.ts | 139 ++- src/declarations/ExplicitDeclaration.ts | 190 ++++ src/declarations/ParserRegistry.ts | 19 + .../commands/AlternateHeadings.ts | 61 +- .../commands/HeadingAndDelimiter.ts | 63 +- src/declarations/commands/HeadingIsFront.ts | 55 +- .../commands/HeadingsCommandable.ts | 90 +- src/declarations/constants.ts | 17 + src/env.ts | 33 +- src/main.ts | 98 +- src/modals/BaseModal.ts | 17 +- src/modals/DeckModal.ts | 208 ++-- src/modals/ReviewItemInfoModal.ts | 4 +- src/modals/SelectDeckModal.ts | 36 +- src/renderings/RendererRegistry.ts | 32 + src/renderings/content/AudioProcessor.ts | 10 +- src/renderings/content/ContentRenderer.ts | 12 +- .../content/ContentRendererProcessor.ts | 4 +- src/renderings/content/VideoProcessor.ts | 2 +- .../declarations/AlternateHeadingsRenderer.ts | 10 +- .../declarations/CardDeclarationRenderer.ts | 29 - .../declarations/DeclarationErrorRenderer.ts | 15 +- .../declarations/DeclarationRenderChild.ts | 94 +- .../declarations/DeclarationRenderable.ts | 68 +- .../HeadingAndDelimiterRenderer.ts | 27 +- .../declarations/HeadingIsFrontRenderer.ts | 10 +- .../declarations/PageDeclarationRenderer.ts | 83 ++ src/scheduling/Fsrs.ts | 2 - src/ui/SettingTab.ts | 4 +- src/ui/UIAssistant.ts | 36 +- src/utils/datetime.ts | 5 + src/utils/dom/constants.ts | 18 +- src/utils/dom/dom.ts | 44 + src/utils/dom/table.ts | 16 +- src/utils/obs/DomState.ts | 102 ++ src/utils/obs/FileParser.ts | 16 +- src/utils/obs/InteractionAssistant.ts | 221 ++++ src/utils/obs/RecycleComponent.ts | 19 +- src/utils/obs/ViewAssistant.ts | 49 +- src/utils/obs/constants.ts | 41 + src/utils/obs/dom.ts | 14 - src/utils/obs/internal.ts | 29 +- src/utils/ts.ts | 152 ++- src/views/BaseView.ts | 80 +- src/views/DecksView.ts | 10 +- src/views/DefinedContentView.ts | 33 +- src/views/constants.ts | 3 - src/views/review/ReviewView.ts | 131 ++- src/views/review/elements.ts | 4 +- styles/reviewView.css | 5 + styles/view.css | 14 + tsconfig.json | 38 +- version-bump.mjs | 1 + versions.json | 1 + 80 files changed, 3177 insertions(+), 1783 deletions(-) create mode 100644 src/commands/editor.ts create mode 100644 src/constants.ts create mode 100644 src/declarations/Collectionable.ts delete mode 100644 src/declarations/CommandDeclaration.ts create mode 100644 src/declarations/CommandNames.ts create mode 100644 src/declarations/Commandable.ts create mode 100644 src/declarations/Declarable.ts delete mode 100644 src/declarations/Declaration.ts create mode 100644 src/declarations/DeclarationCodec.ts create mode 100644 src/declarations/ExplicitDeclaration.ts create mode 100644 src/declarations/ParserRegistry.ts create mode 100644 src/declarations/constants.ts create mode 100644 src/renderings/RendererRegistry.ts delete mode 100644 src/renderings/declarations/CardDeclarationRenderer.ts create mode 100644 src/renderings/declarations/PageDeclarationRenderer.ts create mode 100644 src/utils/dom/dom.ts create mode 100644 src/utils/obs/DomState.ts create mode 100644 src/utils/obs/InteractionAssistant.ts delete mode 100644 src/views/constants.ts diff --git a/.gitignore b/.gitignore index f478f21..cd93ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,12 @@ data.json # This plugin +## Files are copied here on `pnpm run build`. +/dist/ + +## Sym-linked to the plugin's folder in the dev/test vault. Files are copied here `pnpm run dev`. +## Git stores symlinks as a special file containing the target path string, not as a directory. So the gitignore rule needs to match a file, not a directory pattern. +dev-vault + main.js styles.css - -/dist/ diff --git a/README.md b/README.md index 8bf95f2..cc51c30 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ id: card2 ``` ```` +There are commands that let you insert these declarations without typing in the [Command palette](https://help.obsidian.md/plugins/command-palette). + > [!NOTE] > > The card declaration format is plain YAML (same as Obsidian [properties](https://help.obsidian.md/properties#Property+format)) so the YAML specification has to be adhered to. For example, there must be a space between the colon and the value. diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 1f2727c..aad4052 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,7 @@ import builtins from "builtin-modules"; import esbuild from "esbuild"; import fs from "fs"; +import path from "path"; import process from "process"; const banner = @@ -11,7 +12,9 @@ if you want to view the source, please visit the github repository of this plugi `; const prod = (process.argv[2] === "production"); -const outdir = prod ? "dist" : "."; +const outdir = prod ? "dist" : "dev-vault"; + +fs.copyFileSync("manifest.json", path.join(outdir, "manifest.json")); const context = await esbuild.context({ banner: { @@ -39,6 +42,7 @@ const context = await esbuild.context({ "@lezer/lr", ...builtins], format: "cjs", + // Runtime of min supported Obsidian version 1.8.2, see manifest.json target: "es2022", logLevel: "info", sourcemap: prod ? false : "inline", @@ -52,7 +56,6 @@ const context = await esbuild.context({ if (prod) { await context.rebuild(); - fs.copyFileSync("manifest.json", "dist/manifest.json"); process.exit(0); } else { await context.watch(); diff --git a/eslint.config.js b/eslint.config.js index 9d5deab..a6d5784 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -32,10 +32,9 @@ export default defineConfig( languageOptions: { parser: tseslint.parser, parserOptions: { - project: "./tsconfig.json", - projectService: "true", + projectService: true, sourceType: "module", - ecmaVersion: "latest", + ecmaVersion: 2022, }, globals: { ...globals.browser, @@ -46,22 +45,34 @@ export default defineConfig( } }, rules: { + // You should always have "no-unused-vars": "off" alongside @typescript-eslint/no-unused-vars, + // https://typescript-eslint.io/rules/no-unused-vars/ "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["warn", { - args: "none" + "@typescript-eslint/no-unused-vars": ["error", { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true, }], + + // "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": false, "ts-ignore": true, "ts-nocheck": true, "ts-check": true, }], + "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unnecessary-condition": ["warn", { // https://typescript-eslint.io/rules/no-unnecessary-condition/#only-allowed-literals "allowConstantLoopConditions": "only-allowed-literals" }], + "@typescript-eslint/switch-exhaustiveness-check": "error", }, }, ); diff --git a/manifest.json b/manifest.json index 4e77656..18e9c4d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "come-through", "name": "Come Through", - "version": "0.6.0", - "minAppVersion": "1.8.0", + "version": "0.6.1", + "minAppVersion": "1.8.2", "description": "Drill flashcards using spaced repetition.", "author": "mntno", "authorUrl": "https://github.com/mntno", diff --git a/package.json b/package.json index 51a79fa..bc3ecac 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,29 @@ "description": "Drill flashcards using spaced repetition.", "main": "main.js", "scripts": { + "lint": "pnpm eslint .", "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "build": "(pnpm eslint . || true) && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], "author": "mntno", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.37.0", - "@types/node": "24.6.2", - "builtin-modules": "5.0.0", - "esbuild": "^0.25.10", - "eslint": "^9.37.0", - "eslint-plugin-obsidianmd": "^0.1.4", + "@eslint/js": "^9.39.4", + "@types/node": "25.6.0", + "builtin-modules": "5.1.0", + "esbuild": "^0.28.0", + "eslint": "^9.39.4", + "eslint-plugin-obsidianmd": "^0.1.9", "fast-equals": "5.2.2", - "globals": "16.4.0", + "globals": "17.5.0", "luxon": "3.7.2", "obsidian": "latest", "ts-fsrs": "5.2.1", "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.1" + "typescript": "^6.0.2", + "typescript-eslint": "^8.58.1" }, "type": "module", "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3af1959..8aaf4fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,35 +9,35 @@ importers: .: devDependencies: '@eslint/js': - specifier: ^9.37.0 - version: 9.37.0 + specifier: ^9.39.4 + version: 9.39.4 '@types/node': - specifier: 24.6.2 - version: 24.6.2 + specifier: 25.6.0 + version: 25.6.0 builtin-modules: - specifier: 5.0.0 - version: 5.0.0 + specifier: 5.1.0 + version: 5.1.0 esbuild: - specifier: ^0.25.10 - version: 0.25.10 + specifier: ^0.28.0 + version: 0.28.0 eslint: - specifier: ^9.37.0 - version: 9.37.0 + specifier: ^9.39.4 + version: 9.39.4 eslint-plugin-obsidianmd: - specifier: ^0.1.4 - version: 0.1.4(@eslint/js@9.37.0)(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript-eslint@8.46.1(eslint@9.37.0)(typescript@5.9.3)) + specifier: ^0.1.9 + version: 0.1.9(@eslint/js@9.39.4)(@eslint/json@0.14.0)(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4)(obsidian@1.12.3(@codemirror/state@6.5.0)(@codemirror/view@6.38.6))(typescript-eslint@8.58.1(eslint@9.39.4)(typescript@6.0.2)) fast-equals: specifier: 5.2.2 version: 5.2.2 globals: - specifier: 16.4.0 - version: 16.4.0 + specifier: 17.5.0 + version: 17.5.0 luxon: - specifier: ^3.7.2 + specifier: 3.7.2 version: 3.7.2 obsidian: specifier: latest - version: 1.10.0(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) + version: 1.12.3(@codemirror/state@6.5.0)(@codemirror/view@6.38.6) ts-fsrs: specifier: 5.2.1 version: 5.2.1 @@ -45,172 +45,172 @@ importers: specifier: ^2.8.1 version: 2.8.1 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.2 + version: 6.0.2 typescript-eslint: - specifier: ^8.46.1 - version: 8.46.1(eslint@9.37.0)(typescript@5.9.3) + specifier: ^8.58.1 + version: 8.58.1(eslint@9.39.4)(typescript@6.0.2) packages: - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + '@codemirror/state@6.5.0': + resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==} - '@codemirror/view@6.38.1': - resolution: {integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==} + '@codemirror/view@6.38.6': + resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==} - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -221,36 +221,50 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.0': - resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.16.0': - resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.37.0': - resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/json@0.14.0': + resolution: {integrity: sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.0': - resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -265,6 +279,10 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} + '@humanwhocodes/momoa@3.3.10': + resolution: {integrity: sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==} + engines: {node: '>=18'} + '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -278,18 +296,6 @@ packages: peerDependencies: eslint: ^9 - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@pkgr/core@0.1.2': resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -315,69 +321,69 @@ packages: '@types/node@20.12.12': resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} - '@types/node@24.6.2': - resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/tern@0.23.9': resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} - '@typescript-eslint/eslint-plugin@8.46.1': - resolution: {integrity: sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==} + '@typescript-eslint/eslint-plugin@8.58.1': + resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.1 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser': ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.46.1': - resolution: {integrity: sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==} + '@typescript-eslint/parser@8.58.1': + resolution: {integrity: sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.46.1': - resolution: {integrity: sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==} + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.46.1': - resolution: {integrity: sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==} + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.1': - resolution: {integrity: sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==} + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.46.1': - resolution: {integrity: sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==} + '@typescript-eslint/type-utils@8.58.1': + resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.46.1': - resolution: {integrity: sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==} + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.1': - resolution: {integrity: sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==} + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.46.1': - resolution: {integrity: sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==} + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.46.1': - resolution: {integrity: sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==} + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -390,11 +396,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -446,18 +457,22 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - builtin-modules@5.0.0: - resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + builtin-modules@5.1.0: + resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} engines: {node: '>=18.20'} call-bind-apply-helpers@1.0.2: @@ -545,6 +560,10 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -581,8 +600,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true @@ -620,6 +639,9 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-depend@1.3.1: + resolution: {integrity: sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw==} + eslint-plugin-es-x@7.8.0: resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -648,13 +670,15 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-obsidianmd@0.1.4: - resolution: {integrity: sha512-RybpN86mv79ypvkczqnFWbGsMKSwYAb60oYd9jMEXHtSEm0KFHytRKZ7DGCu2JU6nYyD5/8RINBOXAJ3dZy7PQ==} + eslint-plugin-obsidianmd@0.1.9: + resolution: {integrity: sha512-/gyo5vky3Y7re4BtT/8MQbHU5Wes4o6VRqas3YmXE7aTCnMsdV0kfzV1GDXJN9Hrsc9UQPoeKUMiapKL0aGE4g==} engines: {node: '>= 18'} hasBin: true peerDependencies: '@eslint/js': ^9.30.1 + '@eslint/json': 0.14.0 eslint: '>=9.0.0 <10.0.0' + obsidian: 1.8.7 typescript-eslint: ^8.35.1 eslint-plugin-react@7.37.3: @@ -681,8 +705,12 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.37.0: - resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -699,8 +727,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -722,10 +750,6 @@ packages: resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} engines: {node: '>=6.0.0'} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -735,17 +759,19 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + 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'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -754,8 +780,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} @@ -790,10 +816,6 @@ packages: get-tsconfig@4.12.0: resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -806,8 +828,8 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} globalthis@1.0.4: @@ -821,9 +843,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -931,10 +950,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -984,8 +999,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true json-buffer@3.0.1: @@ -1041,28 +1056,27 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + module-replacements@2.11.0: + resolution: {integrity: sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==} + moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} @@ -1104,11 +1118,11 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obsidian@1.10.0: - resolution: {integrity: sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw==} + obsidian@1.12.3: + resolution: {integrity: sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==} peerDependencies: '@codemirror/state': 6.5.0 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.6 optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -1141,9 +1155,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} @@ -1160,9 +1174,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1202,13 +1213,6 @@ packages: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -1306,8 +1310,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -1325,16 +1329,16 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} toml-eslint-parser@0.9.3: resolution: {integrity: sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -1372,20 +1376,20 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.46.1: - resolution: {integrity: sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==} + typescript-eslint@8.58.1: + resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -1396,8 +1400,8 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@7.13.0: - resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1434,8 +1438,8 @@ packages: resolution: {integrity: sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==} engines: {node: ^14.17.0 || >=16.0.0} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -1445,139 +1449,153 @@ packages: snapshots: - '@codemirror/state@6.5.2': + '@codemirror/state@6.5.0': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.38.1': + '@codemirror/view@6.38.6': dependencies: - '@codemirror/state': 6.5.2 + '@codemirror/state': 6.5.0 crelt: 1.0.6 - style-mod: 4.1.2 + style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@esbuild/aix-ppc64@0.25.10': + '@esbuild/aix-ppc64@0.28.0': optional: true - '@esbuild/android-arm64@0.25.10': + '@esbuild/android-arm64@0.28.0': optional: true - '@esbuild/android-arm@0.25.10': + '@esbuild/android-arm@0.28.0': optional: true - '@esbuild/android-x64@0.25.10': + '@esbuild/android-x64@0.28.0': optional: true - '@esbuild/darwin-arm64@0.25.10': + '@esbuild/darwin-arm64@0.28.0': optional: true - '@esbuild/darwin-x64@0.25.10': + '@esbuild/darwin-x64@0.28.0': optional: true - '@esbuild/freebsd-arm64@0.25.10': + '@esbuild/freebsd-arm64@0.28.0': optional: true - '@esbuild/freebsd-x64@0.25.10': + '@esbuild/freebsd-x64@0.28.0': optional: true - '@esbuild/linux-arm64@0.25.10': + '@esbuild/linux-arm64@0.28.0': optional: true - '@esbuild/linux-arm@0.25.10': + '@esbuild/linux-arm@0.28.0': optional: true - '@esbuild/linux-ia32@0.25.10': + '@esbuild/linux-ia32@0.28.0': optional: true - '@esbuild/linux-loong64@0.25.10': + '@esbuild/linux-loong64@0.28.0': optional: true - '@esbuild/linux-mips64el@0.25.10': + '@esbuild/linux-mips64el@0.28.0': optional: true - '@esbuild/linux-ppc64@0.25.10': + '@esbuild/linux-ppc64@0.28.0': optional: true - '@esbuild/linux-riscv64@0.25.10': + '@esbuild/linux-riscv64@0.28.0': optional: true - '@esbuild/linux-s390x@0.25.10': + '@esbuild/linux-s390x@0.28.0': optional: true - '@esbuild/linux-x64@0.25.10': + '@esbuild/linux-x64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.25.10': + '@esbuild/netbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.25.10': + '@esbuild/netbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-arm64@0.25.10': + '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.25.10': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openharmony-arm64@0.25.10': + '@esbuild/openharmony-arm64@0.28.0': optional: true - '@esbuild/sunos-x64@0.25.10': + '@esbuild/sunos-x64@0.28.0': optional: true - '@esbuild/win32-arm64@0.25.10': + '@esbuild/win32-arm64@0.28.0': optional: true - '@esbuild/win32-ia32@0.25.10': + '@esbuild/win32-ia32@0.28.0': optional: true - '@esbuild/win32-x64@0.25.10': + '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.4)': dependencies: - eslint: 9.37.0 + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.21.0': + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': dependencies: - '@eslint/object-schema': 2.1.6 + '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.0': + '@eslint/config-helpers@0.4.2': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 - '@eslint/core@0.16.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 + js-yaml: 4.1.1 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.37.0': {} + '@eslint/js@9.39.4': {} - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.4.0': + '@eslint/json@0.14.0': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 + '@eslint/plugin-kit': 0.4.1 + '@humanwhocodes/momoa': 3.3.10 + natural-compare: 1.4.0 + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -1589,29 +1607,19 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} + '@humanwhocodes/momoa@3.3.10': {} + '@humanwhocodes/retry@0.4.3': {} '@marijn/find-cluster-break@1.0.2': {} - '@microsoft/eslint-plugin-sdl@1.1.0(eslint@9.37.0)': + '@microsoft/eslint-plugin-sdl@1.1.0(eslint@9.39.4)': dependencies: - eslint: 9.37.0 - eslint-plugin-n: 17.10.3(eslint@9.37.0) - eslint-plugin-react: 7.37.3(eslint@9.37.0) + eslint: 9.39.4 + eslint-plugin-n: 17.10.3(eslint@9.39.4) + eslint-plugin-react: 7.37.3(eslint@9.39.4) eslint-plugin-security: 1.4.0 - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@pkgr/core@0.1.2': {} '@rtsao/scc@1.1.0': {} @@ -1635,121 +1643,125 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@24.6.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.13.0 + undici-types: 7.19.2 '@types/tern@0.23.9': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/eslint-plugin@8.46.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4)(typescript@6.0.2)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.1 - '@typescript-eslint/type-utils': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.1 - eslint: 9.37.0 - graphemer: 1.4.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.46.1 - '@typescript-eslint/types': 8.46.1 - '@typescript-eslint/typescript-estree': 8.46.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.1 + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 debug: 4.4.3 - eslint: 9.37.0 - typescript: 5.9.3 + eslint: 9.39.4 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.1(typescript@5.9.3) - '@typescript-eslint/types': 8.46.1 + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.1': + '@typescript-eslint/scope-manager@8.58.1': dependencies: - '@typescript-eslint/types': 8.46.1 - '@typescript-eslint/visitor-keys': 8.46.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 - '@typescript-eslint/tsconfig-utils@8.46.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.2)': dependencies: - typescript: 5.9.3 + typescript: 6.0.2 - '@typescript-eslint/type-utils@8.46.1(eslint@9.37.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4)(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.46.1 - '@typescript-eslint/typescript-estree': 8.46.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.1(eslint@9.37.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4)(typescript@6.0.2) debug: 4.4.3 - eslint: 9.37.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.1': {} + '@typescript-eslint/types@8.58.1': {} - '@typescript-eslint/typescript-estree@8.46.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.46.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.1(typescript@5.9.3) - '@typescript-eslint/types': 8.46.1 - '@typescript-eslint/visitor-keys': 8.46.1 + '@typescript-eslint/project-service': 8.58.1(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 + minimatch: 10.2.5 semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.1(eslint@9.37.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.1(eslint@9.39.4)(typescript@6.0.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) - '@typescript-eslint/scope-manager': 8.46.1 - '@typescript-eslint/types': 8.46.1 - '@typescript-eslint/typescript-estree': 8.46.1(typescript@5.9.3) - eslint: 9.37.0 - typescript: 5.9.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + eslint: 9.39.4 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.1': + '@typescript-eslint/visitor-keys@8.58.1': dependencies: - '@typescript-eslint/types': 8.46.1 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.15.0: {} - ajv@6.12.6: + acorn@8.16.0: {} + + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -1837,20 +1849,22 @@ snapshots: balanced-match@1.0.2: {} - brace-expansion@1.1.12: + balanced-match@4.0.4: {} + + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 - braces@3.0.3: + brace-expansion@5.0.5: dependencies: - fill-range: 7.1.1 + balanced-match: 4.0.4 - builtin-modules@5.0.0: {} + builtin-modules@5.1.0: {} call-bind-apply-helpers@1.0.2: dependencies: @@ -1942,6 +1956,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + empathic@2.0.0: {} + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -2048,40 +2064,40 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.10: + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.37.0): + eslint-compat-utils@0.5.1(eslint@9.39.4): dependencies: - eslint: 9.37.0 + eslint: 9.39.4 semver: 7.7.3 eslint-import-resolver-node@0.3.9: @@ -2092,24 +2108,30 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - eslint: 9.37.0 + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@9.37.0): + eslint-plugin-depend@1.3.1: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) - '@eslint-community/regexpp': 4.12.1 - eslint: 9.37.0 - eslint-compat-utils: 0.5.1(eslint@9.37.0) + empathic: 2.0.0 + module-replacements: 2.11.0 + semver: 7.7.3 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0): + eslint-plugin-es-x@7.8.0(eslint@9.39.4): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.1 + eslint: 9.39.4 + eslint-compat-utils: 0.5.1(eslint@9.39.4) + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -2118,13 +2140,13 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.37.0 + eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -2132,22 +2154,22 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.1(eslint@9.37.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4)(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-json-schema-validator@5.1.0(eslint@9.37.0): + eslint-plugin-json-schema-validator@5.1.0(eslint@9.39.4): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) - ajv: 8.17.1 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4) + ajv: 8.18.0 debug: 4.4.3 - eslint: 9.37.0 - eslint-compat-utils: 0.5.1(eslint@9.37.0) + eslint: 9.39.4 + eslint-compat-utils: 0.5.1(eslint@9.39.4) json-schema-migrate: 2.0.0 jsonc-eslint-parser: 2.4.1 - minimatch: 8.0.4 + minimatch: 8.0.7 synckit: 0.9.3 toml-eslint-parser: 0.9.3 tunnel-agent: 0.6.0 @@ -2155,38 +2177,41 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-n@17.10.3(eslint@9.37.0): + eslint-plugin-n@17.10.3(eslint@9.39.4): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4) enhanced-resolve: 5.18.3 - eslint: 9.37.0 - eslint-plugin-es-x: 7.8.0(eslint@9.37.0) + eslint: 9.39.4 + eslint-plugin-es-x: 7.8.0(eslint@9.39.4) get-tsconfig: 4.12.0 globals: 15.15.0 ignore: 5.3.2 - minimatch: 9.0.5 + minimatch: 9.0.9 semver: 7.7.3 - eslint-plugin-obsidianmd@0.1.4(@eslint/js@9.37.0)(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript-eslint@8.46.1(eslint@9.37.0)(typescript@5.9.3)): + eslint-plugin-obsidianmd@0.1.9(@eslint/js@9.39.4)(@eslint/json@0.14.0)(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4)(obsidian@1.12.3(@codemirror/state@6.5.0)(@codemirror/view@6.38.6))(typescript-eslint@8.58.1(eslint@9.39.4)(typescript@6.0.2)): dependencies: - '@eslint/js': 9.37.0 - '@microsoft/eslint-plugin-sdl': 1.1.0(eslint@9.37.0) + '@eslint/js': 9.39.4 + '@eslint/json': 0.14.0 + '@microsoft/eslint-plugin-sdl': 1.1.0(eslint@9.39.4) '@types/eslint': 8.56.2 '@types/node': 20.12.12 - eslint: 9.37.0 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0) - eslint-plugin-json-schema-validator: 5.1.0(eslint@9.37.0) + eslint: 9.39.4 + eslint-plugin-depend: 1.3.1 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4) + eslint-plugin-json-schema-validator: 5.1.0(eslint@9.39.4) eslint-plugin-security: 2.1.1 globals: 14.0.0 + obsidian: 1.12.3(@codemirror/state@6.5.0)(@codemirror/view@6.38.6) typescript: 5.4.5 - typescript-eslint: 8.46.1(eslint@9.37.0)(typescript@5.9.3) + typescript-eslint: 8.58.1(eslint@9.39.4)(typescript@6.0.2) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-react@7.37.3(eslint@9.37.0): + eslint-plugin-react@7.37.3(eslint@9.39.4): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -2194,11 +2219,11 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.37.0 + eslint: 9.39.4 estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 + minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 @@ -2225,22 +2250,23 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.37.0: + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.4.0 - '@eslint/core': 0.16.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.37.0 - '@eslint/plugin-kit': 0.4.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -2248,7 +2274,7 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -2259,7 +2285,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -2267,8 +2293,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 espree@9.6.1: @@ -2277,7 +2303,7 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -2293,32 +2319,20 @@ snapshots: fast-equals@5.2.2: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} fast-uri@3.1.0: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2326,10 +2340,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.3: {} + flatted@3.4.2: {} for-each@0.3.5: dependencies: @@ -2378,10 +2392,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2390,7 +2400,7 @@ snapshots: globals@15.15.0: {} - globals@16.4.0: {} + globals@17.5.0: {} globalthis@1.0.4: dependencies: @@ -2401,8 +2411,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -2509,8 +2517,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -2565,7 +2571,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2573,7 +2579,7 @@ snapshots: json-schema-migrate@2.0.0: dependencies: - ajv: 8.17.1 + ajv: 8.18.0 json-schema-traverse@0.4.1: {} @@ -2622,27 +2628,26 @@ snapshots: math-intrinsics@1.1.0: {} - merge2@1.4.1: {} - - micromatch@4.0.8: + minimatch@10.2.5: dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + brace-expansion: 5.0.5 - minimatch@3.1.2: + minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.14 - minimatch@8.0.4: + minimatch@8.0.7: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.0 - minimatch@9.0.5: + minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.0 minimist@1.2.8: {} + module-replacements@2.11.0: {} + moment@2.29.4: {} ms@2.1.3: {} @@ -2691,10 +2696,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - obsidian@1.10.0(@codemirror/state@6.5.2)(@codemirror/view@6.38.1): + obsidian@1.12.3(@codemirror/state@6.5.0)(@codemirror/view@6.38.6): dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.38.6 '@types/codemirror': 5.60.8 moment: 2.29.4 @@ -2731,7 +2736,7 @@ snapshots: path-parse@1.0.7: {} - picomatch@2.3.1: {} + picomatch@4.0.4: {} possible-typed-array-names@1.1.0: {} @@ -2745,8 +2750,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - react-is@16.13.1: {} reflect.getprototypeof@1.0.10: @@ -2791,12 +2794,6 @@ snapshots: ret@0.1.15: {} - reusify@1.1.0: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -2939,7 +2936,7 @@ snapshots: strip-json-comments@3.1.1: {} - style-mod@4.1.2: {} + style-mod@4.1.3: {} supports-color@7.2.0: dependencies: @@ -2954,17 +2951,18 @@ snapshots: tapable@2.3.0: {} - to-regex-range@5.0.1: + tinyglobby@0.2.16: dependencies: - is-number: 7.0.0 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 toml-eslint-parser@0.9.3: dependencies: eslint-visitor-keys: 3.4.3 - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 ts-fsrs@5.2.1: {} @@ -3018,20 +3016,20 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.46.1(eslint@9.37.0)(typescript@5.9.3): + typescript-eslint@8.58.1(eslint@9.39.4)(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.1(eslint@9.37.0)(typescript@5.9.3) - eslint: 9.37.0 - typescript: 5.9.3 + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@6.0.2))(eslint@9.39.4)(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4)(typescript@6.0.2) + eslint: 9.39.4 + typescript: 6.0.2 transitivePeerDependencies: - supports-color typescript@5.4.5: {} - typescript@5.9.3: {} + typescript@6.0.2: {} unbox-primitive@1.1.0: dependencies: @@ -3042,7 +3040,7 @@ snapshots: undici-types@5.26.5: {} - undici-types@7.13.0: {} + undici-types@7.19.2: {} uri-js@4.4.1: dependencies: @@ -3100,8 +3098,8 @@ snapshots: yaml-eslint-parser@1.3.0: dependencies: eslint-visitor-keys: 3.4.3 - yaml: 2.8.1 + yaml: 2.8.3 - yaml@2.8.1: {} + yaml@2.8.3: {} yocto-queue@0.1.0: {} diff --git a/src/ContentParser.ts b/src/ContentParser.ts index 2907209..dab761d 100644 --- a/src/ContentParser.ts +++ b/src/ContentParser.ts @@ -1,11 +1,11 @@ -import { CardDeclarationAssistant, IDScope } from "declarations/CardDeclaration"; -import { Declaration } from "declarations/Declaration"; -import { DeclarationParser } from "declarations/DeclarationParser"; -import { FullSectionRange, SectionRange } from "utils/obs/FileParser"; -import { CardID, FullID, NoteID } from "data/FullID"; +import { CardID, FullID, NoteID } from "#/data/FullID"; +import { DeclarationConstants } from "#/declarations/constants"; +import { DeclarationParser } from "#/declarations/DeclarationParser"; +import { IDScope } from "#/declarations/ExplicitDeclaration"; +import { asNoteID, fullIDFromDeclaration } from "#/TypeAssistant"; +import { UnexpectedUndefinedError } from "#/utils/errors"; +import { FullSectionRange, SectionRange } from "#/utils/obs/FileParser"; import { App, CachedMetadata, HeadingCache, SectionCache, TFile } from "obsidian"; -import { asNoteID, fullIDFromDeclaration } from "TypeAssistant"; -import { UnexpectedUndefinedError } from "utils/errors"; /** * The result of attempting to retrieve the content of a particular {@link FullID}. @@ -127,14 +127,16 @@ export class ContentParser extends DeclarationParser { const predicate: PopulationPredicate = { iterationFilter: (idContentInfo) => { - // For unique ids. If also filtering on noteID, then only this file will be looked at, - // while the other side is in another file and thus won't be found. - if (idContentInfo.scope == IDScope.UNIQUE) - return idContentInfo.id.isCardEqual(id); + switch (idContentInfo.scope) { + case IDScope.Unique: + // For unique ids. If also filtering on noteID, then only this file will be looked at, + // while the other side is in another file and thus won't be found. + return idContentInfo.id.isCardEqual(id); - // Should work for file-scoped IDs. - if (idContentInfo.scope == IDScope.NOTE) - return idContentInfo.id.isEqual(id, true); + case IDScope.Note: + // Should work for file-scoped IDs. + return idContentInfo.id.isEqual(id, true); + } throw new Error(`Unrecognized ID scope for ID: ${idContentInfo.id}`); }, @@ -142,11 +144,13 @@ export class ContentParser extends DeclarationParser { if (!this.isComplete(maybeParsedCard)) return false; - if (idContentInfo.scope == IDScope.UNIQUE) - return maybeParsedCard.frontID.isCardEqual(id); + switch (idContentInfo.scope) { + case IDScope.Unique: + return maybeParsedCard.frontID.isCardEqual(id); - if (idContentInfo.scope == IDScope.NOTE) - return maybeParsedCard.frontID.isEqual(id, true); + case IDScope.Note: + return maybeParsedCard.frontID.isEqual(id, true); + } throw new Error(`Unrecognized ID scope for ID: ${idContentInfo.id}`); } @@ -269,7 +273,7 @@ export class ContentParser extends DeclarationParser { cardInfos.push({ id: id, - scope: IDScope.NOTE, + scope: IDScope.Note, contentInfo: { range: { start: currentHeading, @@ -287,8 +291,8 @@ export class ContentParser extends DeclarationParser { // Look for a declaration in the frontmatter if (cache.frontmatter) { - for (const key of Declaration.supportedFrontmatterKeys) { - const declaration = CardDeclarationAssistant.fromFrontmatter(cache.frontmatter[key]); + for (const key of DeclarationConstants.Frontmatter.KEYS) { + const declaration = ContentParser.declarationFromFrontmatter(cache.frontmatter[key]); if (declaration) { cardInfos.push({ id: fullIDFromDeclaration(declaration, noteID), @@ -306,7 +310,7 @@ export class ContentParser extends DeclarationParser { for (const section of cache.sections ?? []) { - const declaration = this.getDeclarationFromSection(section, noteID, fileContent); + const declaration = ContentParser.getDeclarationFromSection(section, noteID, fileContent); if (!declaration) continue; diff --git a/src/Settings.ts b/src/Settings.ts index ebcb630..062c9a8 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -4,6 +4,7 @@ import { isString } from "TypeAssistant"; import { PLUGIN_NAME } from "ui/constants"; export interface PluginSettings { + /** Trimmed. */ uiPrefix: string; hideCardSectionMarker: boolean; hideDeclarationInReadingView: boolean; @@ -47,7 +48,7 @@ const DEFAULT_SCHEDULER: FsrsScheduler = { export type SettingsChanged = (settings: PluginSettings, isExternal: boolean) => void; -export type SettingsChangedInfo = "schedulerConfig"; // | "x" +export type SettingsChangedInfo = "schedulerConfig";// | typeof UNARY_UNION_DEFAULT; export class SettingsManager { @@ -55,7 +56,7 @@ export class SettingsManager { public settings: PluginSettings; /** Saves the {@link settings} to disk. */ - public save: (changedInfo?: SettingsChangedInfo) => Promise; + public readonly save: (changedInfo?: SettingsChangedInfo) => Promise; public static readonly DEFAULT_DATA: PluginSettings = { uiPrefix: PLUGIN_NAME, @@ -68,11 +69,14 @@ export class SettingsManager { } }; - public constructor(settings: PluginSettings, save: (settings: PluginSettings) => Promise, onSaved: (changedInfo?: SettingsChangedInfo) => void) { + public constructor( + settings: PluginSettings, + save: (settings: PluginSettings) => Promise, + onSaved: (changedInfo?: SettingsChangedInfo) => void) { this.settings = settings; this.save = async (changedInfo?: SettingsChangedInfo) => { await save(this.settings); - onSaved?.(changedInfo); + onSaved(changedInfo); this.notifyOnChangedListeners(false); }; } diff --git a/src/TypeAssistant.ts b/src/TypeAssistant.ts index f797238..bf6a873 100644 --- a/src/TypeAssistant.ts +++ b/src/TypeAssistant.ts @@ -1,7 +1,7 @@ -import { DeckableFullID, FullID, NoteID } from "data/FullID"; -import { CardDeclarable, CardDeclarationAssistant } from "declarations/CardDeclaration"; +import { DeckableFullID, FullID, NoteID } from "#/data/FullID"; +import { CardDeclarable, ExplicitDeclarationAssistant } from "#/declarations/ExplicitDeclaration"; +import { Num, Str } from "#/utils/ts"; import { TFile } from "obsidian"; -import { Num, Obj, Str } from "utils/ts"; export function asNoteID(value: TFile | string): NoteID { if (value instanceof TFile) @@ -13,18 +13,8 @@ export function asNoteID(value: TFile | string): NoteID { export function fullIDFromDeclaration(declaration: CardDeclarable, noteID: NoteID): DeckableFullID | FullID { return declaration.deckID - ? new DeckableFullID(noteID, declaration.id, CardDeclarationAssistant.isFrontSide(declaration, true), [declaration.deckID]) - : FullID.create(noteID, declaration.id, CardDeclarationAssistant.isFrontSide(declaration, true)); -} - -/** - * If {@link value} is `null`, `false` is returned even thoigh `null` is an object. - * - * @param value - * @returns `true` if `typeof` for {@link value} returns `"object"` and {@link value} is not `null`. - */ -export function isObject(value: unknown): value is object { - return Obj.is(value); + ? new DeckableFullID(noteID, declaration.id, ExplicitDeclarationAssistant.isFrontSide(declaration, true), [declaration.deckID]) + : FullID.create(noteID, declaration.id, ExplicitDeclarationAssistant.isFrontSide(declaration, true)); } export function isString(value: unknown): value is string { diff --git a/src/commands/editor.ts b/src/commands/editor.ts new file mode 100644 index 0000000..45188ab --- /dev/null +++ b/src/commands/editor.ts @@ -0,0 +1,70 @@ +import { UniqueID } from "#/data/UniqueID"; +import t from "#/Localization"; +import { Command, Editor, MarkdownFileInfo, MarkdownView } from "obsidian"; +import { Str } from "utils/ts"; + +export const EditorCommand = { + + generateId: (): Command => ({ + id: "generate-id-cursor", + name: t.commands.generateId.name, + editorCallback: (editor: Editor, _ctx) => { + editor.replaceRange(UniqueID.generateID(), editor.getCursor()) + } + }), + + insertReviewUnit: (copyLastPage: boolean): Command => ({ + id: "insert-review-unit" + (copyLastPage ? "-copy-last-page" : ""), + name: "Insert empty declaration under cursor " + (copyLastPage ? "(put answer side in clipboard)" : "(both sides)"), + editorCallback: (editor: Editor, ctx: MarkdownView | MarkdownFileInfo) => { + + const file = ctx.file; + if (file === null) + return; + + const cursor = editor.getCursor(); + const fileCache = ctx.app.metadataCache.getFileCache(file); + const headings = fileCache?.headings || []; + + let headingLevel = 0; + for (let i = headings.length - 1; i >= 0; i--) { + if (headings[i]!.position.start.line <= cursor.line) { + headingLevel = headings[i]!.level; + break; + } + } + headingLevel = Math.max(headingLevel + 1, 2); + const hashes = '#'.repeat(headingLevel || 2); + + const id = UniqueID.generateID(); + + const createCardPage = (isBack: boolean) => { + const title = isBack ? "Answer heading" : "Question heading"; + const side = isBack ? "b" : "f"; + + return [ + `${hashes} ${title}`, + Str.LF, + "```ct", + "id: " + id, + "side: " + side, + "```", + Str.LF, + "*Add content here*", + Str.LF, + ].join(Str.LF); + }; + + const firstPage = createCardPage(false); + const lastPage = createCardPage(true); + const codeBlock = copyLastPage ? firstPage : firstPage.concat(lastPage); + + editor.replaceRange(codeBlock, cursor); + editor.setCursor(editor.offsetToPos(editor.posToOffset(cursor) + firstPage.length - 2)); + + if (copyLastPage) + navigator.clipboard.writeText(lastPage); + } + }), + +}; diff --git a/src/commands/openView.ts b/src/commands/openView.ts index 95edc86..58d69d9 100644 --- a/src/commands/openView.ts +++ b/src/commands/openView.ts @@ -3,7 +3,6 @@ import { DeclarationParser } from "declarations/DeclarationParser"; import t from "Localization"; import { SelectDeckModal } from "modals/SelectDeckModal"; import { App, Command, Keymap, MarkdownView, PaneType, TFile } from "obsidian"; -import { UIAssistant } from "ui/UIAssistant"; import { DecksView } from "views/DecksView"; import { DefinedContentView } from "views/DefinedContentView"; import { ReviewView } from "views/review/ReviewView"; @@ -15,6 +14,7 @@ export const OpenView = { const leaf = app.workspace.getLeaf(paneType); await leaf.setViewState({ type: DecksView.TYPE, + state: DecksView.createViewState(), active: true, }); }, @@ -45,21 +45,24 @@ export const OpenView = { }; const allDecks = dataStore.getAllDecks(); - if (allDecks.length) { + if (allDecks.length > 0) { const modal = new SelectDeckModal( app, dataStore, - [...[UIAssistant.allDecksOptionItem()], ...allDecks], - async (deck, evt) => { - await openView(ReviewView.createViewState(deck.id === UIAssistant.DECK_ID_NONE ? null : deck.id), Keymap.isModEvent(evt)); + allDecks, + (deck, evt) => { + openView( + ReviewView.createViewState(deck === null ? null : deck.id), + Keymap.isModEvent(evt) + ).catch(console.error); }); modal.setPlaceholder(t.modals.selectDeck.placeholder); modal.open(); } else { - await openView(undefined, paneType); + await openView(ReviewView.createViewState(null), paneType); } -} + } }; export const OpenViewCommand = { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..c4caa50 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,14 @@ +import { CssClass as ObsCssClass } from "utils/obs/constants"; + +const CSS_PREFIX = "come-through-"; + +export const CssClass = { + PREFIX: CSS_PREFIX, + View: { + WORKSPACE_LEAF_CONTENT_MODIFIER: CSS_PREFIX + ObsCssClass.Workspace.LEAF_CONTENT_MODIFIER, + } as const, + + Modal: { + CONTENT: CSS_PREFIX + "modal-content", + } as const, +} as const; diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts index c0c35d8..3869cd7 100644 --- a/src/data/DataStore.ts +++ b/src/data/DataStore.ts @@ -5,7 +5,7 @@ import { deepEqual, strictDeepEqual } from 'fast-equals'; import { asNoteID, isDate, isString } from "TypeAssistant"; import { DateTime } from "utils/datetime"; import { UnexpectedUndefinedError } from "utils/errors"; -import { Obj, Str } from "utils/ts"; +import { Arr, Obj, Str } from "utils/ts"; export interface DataStoreRoot { decks: DecksData; @@ -15,7 +15,7 @@ export interface DataStoreRoot { type DecksData = Record; -interface DeckData { +export interface DeckData { /** Name of the deck */ n: string; /** Parent decks */ @@ -202,6 +202,10 @@ export class DeckEditor { /** Use with {@link DataStore.registerOnChangedCallback} */ export type DataChanged = (data: DataStoreRoot) => void; +type DataSection = Exclude | "all"; +/** See {@link DataStore.Internal.dispatchSection}. */ +type SectionActions = { [K in DataSection]: () => R; }; + export class DataStore { public static readonly DEFAULT_DATA: DataStoreRoot = { @@ -222,6 +226,8 @@ export class DataStore { } public cardInfo(id: FullID) { + Env.log.data("DataStore:cardInfo: id", id); + let info = `${id.isFrontSide ? "front" : "back"} of ${id.cardID}` const card = this.getCard(id); @@ -234,7 +240,7 @@ export class DataStore { const decks: DeckData[] = []; for (const deckID of card.d) { const deckData = this.getDeck(deckID); - console.assert(deckData); + console.assert(deckData !== null); if (deckData !== null) decks.push(deckData); } @@ -248,7 +254,7 @@ export class DataStore { * @param cb Return value is ignored. */ public createDeck(cb: (editor: DeckEditor) => unknown): DeckIDDataTuple { - + Env.log.data("DataStore:createDeck"); const editor = new DeckEditor(UniqueID.generateID(), { n: "", p: [], @@ -261,18 +267,25 @@ export class DataStore { } public editCard(id: FullID, cb: (editor: CardEditor) => boolean) { + Env.log.data("DataStore:editCard: id", id); const data = this.getCard(id, true); if (data && cb(new CardEditor(id, data))) this.setDataDirty(); } - public editDeck(id: DeckID, cb: (editor: DeckEditor) => boolean) { - const data = this.getDeck(id, true); - if (data && cb(new DeckEditor(id, data))) + /** + * @returns `null` if {@link id} was not found and {@link cb} was not called. If the {@link id} was found, the {@link DeckData} is returned regardless of whether {@link cb} was called. + */ + public editDeck(id: DeckID, cb: (editor: DeckEditor) => boolean, throwIfNotFound = false): DeckData | null { + Env.log.data("DataStore:editDeck: id", id); + const data = this.getDeck(id, throwIfNotFound); + if (data !== null && cb(new DeckEditor(id, data))) this.setDataDirty(); + return data; } public getDeck(id: DeckID, throwIfNotFound = false): DeckData | null { + Env.log.data("DataStore:getDeck: id", id); const data = this.data.decks[id] ?? null; if (data === null && throwIfNotFound) throw new Error(`Deck with ID "${id}" was not found.`); @@ -280,6 +293,7 @@ export class DataStore { } public deleteDeck(idToDelete: DeckID, moveChildrenToID?: DeckID, throwIfNotFound = false) { + Env.log.data("DataStore:deleteDeck: idToDelete", idToDelete, "moveChildrenToID", moveChildrenToID); const data = this.getDeck(idToDelete, throwIfNotFound); if (!data) return null; @@ -312,7 +326,7 @@ export class DataStore { const { predicate, } = options || {}; - + Env.log.data("DataStore:getAllDecks: predicate", predicate); const decks: DeckIDDataTuple[] = []; for (const [id, data] of Object.entries(this.data.decks)) { @@ -336,12 +350,13 @@ export class DataStore { * @returns The removed {@link CardData}, or `null` if {@link id} was not found. */ private deleteRemovedCard(id: FullID, throwIfNotFound = false) { + Env.log.data("DataStore:deleteRemovedCard: id", id); const removedCard = this.getRemovedCard(id, throwIfNotFound); if (!removedCard) return null; const removedNoteData = this.getRemovedNote(id.noteID, throwIfNotFound); - Env.assert(removedNoteData); + Env.assert(removedNoteData !== null); if (removedNoteData === null) return null; @@ -349,7 +364,7 @@ export class DataStore { this.setDataDirty(); if (StatisticsHelper.isRemovedNoteEmpty(removedNoteData)) - this.deleteRemovedNote(id.noteID, throwIfNotFound); + this.deleteNote("removed", id.noteID, throwIfNotFound); return removedCard; } @@ -358,11 +373,12 @@ export class DataStore { * @param removedBeforeDate Delete only items that were removed before this date. Set to `undefined` to delete all items. */ private deleteRemovedCards(removedBeforeDate?: Date) { - if (removedBeforeDate) { + Env.log.data("DataStore:deleteRemovedCards: removedBeforeDate", removedBeforeDate); + if (removedBeforeDate !== undefined) { const time = removedBeforeDate.getTime(); this.getAllRemovedCards(undefined, (_cardID: CardID, data: RemovedCardData) => { const date = StatisticsHelper.ensureDate(data.date); - Env.dev?.assert(date, "Expected date parsable string."); + Env.dev?.assert(date !== null, "Expected date parsable string."); return date && date.getTime() < time ? true : false; }).forEach(tuple => this.deleteRemovedCard(tuple.id)); } @@ -380,18 +396,31 @@ export class DataStore { * @param throwIfExists If card already exist as active. */ private ensureActiveCard(id: FullID, statisticsFactory: () => StatisticsData, throwIfExists = false) { + Env.log.data("DataStore:ensureActiveCard: id", id); - const removedCard = this.getAllRemovedCards( - undefined, //(noteID, _) => id.hasNoteID(noteID), + // First check if already active in any note. + // - Prevents dublicates, e.g., it the removal event occurs after the add event. + const existingActive = Arr.firstOrNull(this.getAllCards((cardID, _) => id.hasCardID(cardID))); + if (existingActive !== null) { + if (existingActive.id.hasNoteID(id.noteID)) { + if (throwIfExists) + throw new CardAlreadyExistsError(id, [existingActive.id]); + return existingActive.data; + } + return this.moveActiveCard(existingActive.id, id.noteID); + } + + // Check removed + const removed = Arr.firstOrNull(this.getAllRemovedCards(undefined, (cardID, _) => id.hasCardID(cardID) // For unique IDs. They can be in different notes. Just match on the hash. - ).first(); + )); this.createActiveNote(id, false); let cardToAdd: CardIDDataTuple; - if (removedCard) { - this.deleteRemovedCard(removedCard.id, true); - cardToAdd = StatisticsHelper.toCardIDDataTuple(id, StatisticsHelper.removedCardToCard(removedCard.data)); + if (removed !== null) { + this.deleteRemovedCard(removed.id, true); + cardToAdd = StatisticsHelper.toCardIDDataTuple(id, StatisticsHelper.removedCardToCard(removed.data)); } else { const deckIDs = id instanceof DeckableFullID ? id.deckIDs : []; @@ -410,7 +439,7 @@ export class DataStore { * @returns The created card or `null` if already existed. */ private addAsActiveCard(card: CardIDDataTuple, throwIfExists = false) { - + Env.log.data("DataStore:addAsActiveCard: card", card); if (this.getCard(card.id, false) !== null) { if (throwIfExists) throw new CardAlreadyExistsError(card.id, []); @@ -432,6 +461,7 @@ export class DataStore { * @returns Returns the note, whether it was created or not. */ private ensureActiveNote(id: FullID) { + Env.log.data("DataStore:ensureActiveNote: id", id); return this.createActiveNote(id, false) ?? this.getNote(id.noteID, true)!; } @@ -439,6 +469,7 @@ export class DataStore { * Returns existing {@link RemovedNoteData} from {@link DataStoreRoot.removed} or creates and returns a new one if not found. */ private ensureRemovedNote(noteID: NoteID) { + Env.log.data("DataStore:ensureRemovedNote: noteID", noteID); let note = this.getRemovedNote(noteID); if (!note) { note = StatisticsHelper.createRemovedNoteData(); @@ -454,6 +485,7 @@ export class DataStore { * @returns The created note or `null` if already existed. */ private createActiveNote(id: FullID, throwIfExists = false): NoteData | null { + Env.log.data("DataStore:createActiveNote: id", id); id.throwIfNoNoteID() if (this.getNote(id.noteID, false)) { @@ -471,30 +503,55 @@ export class DataStore { } public removeNote(noteID: NoteID) { + Env.log.data("DataStore:removeNote: noteID", noteID); return this.moveActiveNoteToRemoved(noteID); } public async removeAllCards() { + Env.log.data("DataStore:removeAllCards"); for (const [noteID, note] of Object.entries(this.data.active)) { for (const cardID of Object.keys(note.cs)) this.moveActiveCardToRemoved(StatisticsHelper.createFullID(noteID, cardID)); } } - private moveActiveCardToRemoved(id: FullID, throwIfNotFound = false) { + /** + * Move an active {@link CardData} to another note. + * @param id Item to move. + * @param toNoteID Target to move to. + * @param throwIfNotFound If set to `true`, throws an error if the card is not found in {@link id} or already exists in {@link toNoteID}. + * @returns `null` if item cannot be found or already exists in {@link toNoteID}. + */ + private moveActiveCard(id: FullID, toNoteID: NoteID, throwIfNotFound = false) { + Env.log.data("DataStore:moveActiveCard: id", id, "toNoteID", toNoteID); + const card = this.deleteActiveCard(id, throwIfNotFound); - if (!card) - return false; + if (card === null) + return null; + + const newID = FullID.create(toNoteID, id.cardIDOrThrow(), id.isFrontSide); + return this.addAsActiveCard({ id: newID, data: card }, throwIfNotFound); + } + + private moveActiveCardToRemoved(id: FullID, throwIfNotFound = false) { + Env.log.data("DataStore:moveActiveCardToRemoved: id", id); + const card = this.deleteActiveCard(id, throwIfNotFound); + if (card === null) + return null; const removedNote = this.ensureRemovedNote(id.noteID); - removedNote.cs[id.cardIDOrThrow()] = StatisticsHelper.cardToRemovedCard(card); + const removedItem = StatisticsHelper.cardToRemovedCard(card); + + removedNote.cs[id.cardIDOrThrow()] = removedItem; this.setDataDirty(); - return true; + + return removedItem; } private moveActiveNoteToRemoved(noteID: NoteID, throwIfNotFound = false) { - const note = this.deleteActiveNote(noteID, throwIfNotFound); - if (!note) + Env.log.data("DataStore:moveActiveNoteToRemoved: noteID", noteID); + const note = this.deleteNote("active", noteID, throwIfNotFound); + if (note === null) return false; this.data.removed[noteID] = StatisticsHelper.noteToRemovedNote(note); @@ -509,6 +566,7 @@ export class DataStore { * @returns The removed {@link CardData}, or `null` if {@link id} was not found. */ private deleteActiveCard(id: FullID, throwIfNotFound = false) { + Env.log.data("DataStore:deleteActiveCard: id", id); id.throwIfNoCardID(); const note = this.getNote(id.noteID, throwIfNotFound); @@ -521,41 +579,50 @@ export class DataStore { delete note.cs[id.cardID]; if (StatisticsHelper.isNoteEmpty(note)) - this.deleteActiveNote(id.noteID); + this.deleteNote("active", id.noteID); this.setDataDirty(); return card; } /** - * Deletes {@link NoteData} with {@link noteID} from {@link DataStoreRoot.active} and returns it. - * @param noteID - * @returns The removed {@link NoteData}, or `null` if {@link noteID} was not found. - */ - private deleteActiveNote(noteID: NoteID, throwIfNotFound = false) { - const note = this.getNote(noteID, throwIfNotFound); - if (note !== null) { - delete this.data.active[noteID]; - this.setDataDirty(); - } - return note; - } + * Deletes {@link NoteData} with {@link noteID} from {@link section} and returns it. + * @param section + * @param noteID + * @returns The removed {@link NoteData}, or `null` if {@link noteID} was not found. + */ + private deleteNote(section: DataSection, noteID: NoteID, throwIfNotFound = false): NoteData | null { + Env.log.data(`DataStore:deleteNote: section: ${section}, noteID: ${noteID}`); - /** - * Deletes {@link NoteData} with {@link noteID} from {@link DataStoreRoot.removed} and returns it. - * @param noteID - * @returns The removed {@link NoteData}, or `null` if {@link noteID} was not found. - */ - private deleteRemovedNote(noteID: NoteID, throwIfNotFound = false) { - const note = this.getRemovedNote(noteID, throwIfNotFound); - if (note !== null) { - delete this.data.removed[noteID]; + const actions: SectionActions = { + active: () => { + const n = this.getNote(noteID, section === "active" && throwIfNotFound); // Because `all` calls `active` first. + if (n !== null) + delete this.data.active[noteID]; + return n; + }, + removed: () => { + const n = this.getRemovedNote(noteID, throwIfNotFound); + if (n !== null) + delete this.data.removed[noteID]; + return n; + }, + all: () => { + const note = actions.active(); + return note !== null ? note : actions.removed(); + } + }; + + const note = DataStore.Internal.dispatchSection(section, actions); + + if (note !== null) this.setDataDirty(); - } + return note; } public getCard(id: FullID, throwIfNotFound = false): CardData | null { + Env.log.data("DataStore:getCard: id", id); id.throwIfNoNoteID(); id.throwIfNoCardID(); @@ -572,6 +639,7 @@ export class DataStore { } public getNote(noteID: NoteID, throwIfNotFound = false): NoteData | null { + Env.log.data("DataStore:getNote: noteID", noteID); const note = this.data.active[noteID] ?? null; if (note === null && throwIfNotFound) throw new Error(`Note with ID "${noteID}" was not found.`); @@ -588,6 +656,7 @@ export class DataStore { * @returns */ private getRemovedNote(noteID: NoteID, throwIfNotFound = false): RemovedNoteData | null { + Env.log.data("DataStore:getRemovedNote: noteID", noteID); const note = this.data.removed[noteID] ?? null; if (note === null && throwIfNotFound) throw new Error(`Removed note with ID "${noteID}" was not found.`); @@ -595,6 +664,7 @@ export class DataStore { } private getRemovedCard(id: FullID, throwIfNotFound = false): RemovedCardData | null { + Env.log.data("DataStore:getRemovedCard: id", id); const note = this.getRemovedNote(id.noteID, throwIfNotFound); if (!note) return null; @@ -611,8 +681,8 @@ export class DataStore { * @returns */ public getAllCardsForDeck(deckID?: DeckID): CardIDDataTuple[] { - Env.log.d("DataStore:getAllCardsForDeck:deckID", deckID); - Env.dev?.assert(deckID === undefined || isString(deckID) && deckID !== Env.str.EMPTY, deckID); + Env.log.data("DataStore:getAllCardsForDeck: deckID", deckID); + Env.dev?.assert(deckID === undefined || isString(deckID) && deckID !== Str.EMPTY, deckID); if (deckID === undefined) return this.getAllCards(); @@ -629,6 +699,7 @@ export class DataStore { } private descendantDecks(parentID?: DeckID): DeckIDDataTuple[] { + Env.log.data("DataStore:descendantDecks: parentID", parentID); if (parentID === undefined) return []; @@ -645,13 +716,14 @@ export class DataStore { } public getAllCards(cardFilter?: (cardID: CardID, data: CardData) => boolean): CardIDDataTuple[] { + Env.log.data("DataStore:getAllCards"); return this.getAllCardsWithFilters(undefined, cardFilter); } private getAllCardsWithFilters( noteFilter?: (noteID: NoteID, data: NoteData) => boolean, cardFilter?: (cardID: CardID, data: CardData) => boolean): CardIDDataTuple[] { - + Env.log.data("DataStore:getAllCardsWithFilters"); const cards: CardIDDataTuple[] = []; for (const [noteID, note] of Object.entries(this.data.active)) { @@ -668,6 +740,7 @@ export class DataStore { } public getAllNotes(noteFilter?: (noteID: NoteID, data: NoteData) => boolean): NoteID[] { + Env.log.data("DataStore:getAllNotes"); if (!noteFilter) return Object.keys(this.data.active).map(k => asNoteID(k)); throw new Error("Not Implemented"); @@ -676,7 +749,7 @@ export class DataStore { private getAllRemovedCards( noteFilter?: (noteID: NoteID, data: RemovedNoteData) => boolean, cardFilter?: (cardID: CardID, data: RemovedCardData) => boolean) { - + Env.log.data("DataStore:getAllRemovedCards"); const cards: RemovedCardIDDataTuple[] = []; for (const [noteID, removedData] of Object.entries(this.data.removed)) { @@ -720,11 +793,11 @@ export class DataStore { * @returns `true` if the ID was changed successfully. */ public changeNoteID(oldID: NoteID, newID: NoteID, throwIfNotFound = false) { - + Env.log.data("DataStore:changeNoteID: oldID", oldID, "newID", newID); if (this.getNote(newID)) throw new Error(`Cannot overwrite ${newID}.`) - const deletedNote = this.deleteActiveNote(oldID, throwIfNotFound); + const deletedNote = this.deleteNote("active", oldID, throwIfNotFound); if (deletedNote) { this.data.active[newID] = deletedNote; this.setDataDirty(); @@ -817,7 +890,7 @@ export class DataStore { } public async save() { - Env.log.d(`DataStore:save: dirty: ${this._isDataDirty}`); + Env.log.d("DataStore:save: dirty: ", this._isDataDirty); if (this._isDataDirty) { const purgeRemovedBeforeDate = new Date((new Date()).getTime() - (this.purgeThreshold * 1000)); this.deleteRemovedCards(purgeRemovedBeforeDate); @@ -890,6 +963,7 @@ export class DataStore { } private triggerDataChanged() { + Env.log.data("DataStore:triggerDataChanged", this.registeredChangedCallbacks.length); this.registeredChangedCallbacks.forEach(callback => { try { callback(this.data); @@ -902,6 +976,23 @@ export class DataStore { private registeredChangedCallbacks: DataChanged[] = []; + private static readonly Internal = { + dispatchSection: function (section: DataSection, actions: SectionActions): R { + switch (section) { + case "active": + return actions.active(); + case "removed": + return actions.removed(); + case "all": + return actions.all(); + default: { + const _exhaustiveCheck: never = section; + throw new Error(`DataStore: Unhandled section: ${_exhaustiveCheck}`); + } + } + } + }; + public readonly filter = { cardsWithoutDeck: (card: CardIDDataTuple) => DataStore.Predicate.cardsInDeck(undefined)(card.id, card.data), cardsInDeck: (deckId: DeckID, card: CardIDDataTuple) => DataStore.Predicate.cardsInDeck(deckId)(card.id, card.data), diff --git a/src/data/FullID.ts b/src/data/FullID.ts index 1b669f4..df59be1 100644 --- a/src/data/FullID.ts +++ b/src/data/FullID.ts @@ -1,3 +1,5 @@ +import { Env } from "env"; + export type CardID = string; export type NoteID = string; export type IDFilter = (id: FullID) => boolean; @@ -136,7 +138,7 @@ export class FullID implements FullID { * @throws `Error` if {@link cardSide} is not set. */ public get isFrontSide() { - console.assert(this.cardSide); + Env.assert(this.cardSide !== undefined); if (!this.cardSide) throw new Error(`Side not specified on id: ${this.toString()}`); return this.cardSide === "f" || this.cardSide === "front"; diff --git a/src/data/SyncManager.ts b/src/data/SyncManager.ts index 74bfd63..abfcf5a 100644 --- a/src/data/SyncManager.ts +++ b/src/data/SyncManager.ts @@ -1,10 +1,12 @@ -import { DataStore, StatisticsData } from "data/DataStore"; -import { FullID, NoteID } from "data/FullID"; -import { CardDeclarable, CardDeclarationAssistant } from "declarations/CardDeclaration"; -import { DeclarationInfo, DeclarationParser, PostParseInfo } from "declarations/DeclarationParser"; -import { Env } from "env"; +import { DataStore, StatisticsData } from "#/data/DataStore"; +import { FullID, NoteID } from "#/data/FullID"; +import { DeclarationCodec } from "#/declarations/DeclarationCodec"; +import { DeclarationInfo, DeclarationParser, PostParseInfo } from "#/declarations/DeclarationParser"; +import { CardDeclarable, ExplicitDeclarationAssistant } from "#/declarations/ExplicitDeclaration"; +import { Env } from "#/env"; +import { asNoteID, isString } from "#/TypeAssistant"; +import { UNARY_UNION_SUPPRESS } from "#/utils/ts"; import { App, CachedMetadata, Editor, FileManager, TAbstractFile, TFile } from "obsidian"; -import { asNoteID, isString } from "TypeAssistant"; /** Keeps the plugin's internal data synchronized with card declarations in the vault by listening for and processing file system events. */ export class SyncManager { @@ -44,7 +46,7 @@ export class SyncManager { } } - public async open(file: TFile | null) { + public open = async (file: TFile | null) => { Env.log.d(`SyncManager:open isSuspended: ${this.isSuspended}`); if (this.isSuspended) return; @@ -53,34 +55,34 @@ export class SyncManager { const ids = await SyncManager.processFile(file, this.app); await this.syncIDs(ids, file); } - } + }; - public async changed(file: TFile, data: string, cache: CachedMetadata) { + public changed = async (file: TFile, data: string, cache: CachedMetadata) => { Env.log.d(`SyncManager:changed isSuspended: ${this.isSuspended}`); if (this.isSuspended) return; const ids = await SyncManager.processFileChanged(file, data, cache, this.app); await this.syncIDs(ids, file); - } + }; - public async delete(file: TAbstractFile) { + public delete = async (file: TAbstractFile) => { Env.log.d(`SyncManager:delete isSuspended: ${this.isSuspended}`); if (this.isSuspended) return; if (file instanceof TFile && this.dataStore.removeNote(asNoteID(file.path))) await this.dataStore.save(); - } + }; - public async rename(file: TAbstractFile, oldPath: string) { + public rename = async (file: TAbstractFile, oldPath: string) => { Env.log.d(`SyncManager:rename isSuspended: ${this.isSuspended}`); if (this.isSuspended) return; if (file instanceof TFile && this.dataStore.changeNoteID(asNoteID(oldPath), asNoteID(file), false)) await this.dataStore.save(); - } + }; private static async processFile(file: TFile, app: App) { const { ids, output } = await DeclarationParser.getAllIDsInFile(file, app, (id) => id.isFrontSide) @@ -104,7 +106,7 @@ export class SyncManager { * The returned values are the {@link FullID}s of those declarations that were completed * and therefore also need to be saved to persistant storage. * - * This method is preferred to {@link postProcessWithFile} when the file can be modified via an + * This method is preferred over {@link postProcessWithFile} when the file can be modified via an * {@link Editor} instance. * * @param editor @@ -138,8 +140,8 @@ export class SyncManager { cursorPosition.line <= endPosBeforeModification.line) continue; - const completeDeclaration = CardDeclarationAssistant.makeValidOrThrow(info.declaration, existingIDs); - const replacement = CardDeclarationAssistant.toString(completeDeclaration); + const completeDeclaration = ExplicitDeclarationAssistant.completeOrThrow(info.declaration, existingIDs); + const replacement = DeclarationCodec.toYaml(completeDeclaration); // This will trigger file changed events. editor.replaceRange( @@ -153,7 +155,7 @@ export class SyncManager { autoGeneratedIDs.push(FullID.create( info.noteID, completeDeclaration.id, - CardDeclarationAssistant.isFrontSide(completeDeclaration) + ExplicitDeclarationAssistant.isFrontSide(completeDeclaration) )); diff += replacement.length - (info.location.end - info.location.start); @@ -196,9 +198,9 @@ export class SyncManager { for (const info of replacementDeclarationInfos) { const startOffset = info.section.position.start.offset + info.location.start; - const replace = CardDeclarationAssistant.toString(info.declaration); - const completeDeclaration = CardDeclarationAssistant.makeValidOrThrow(info.declaration, existingIDs); - const replacement = CardDeclarationAssistant.toString(completeDeclaration); + const replace = DeclarationCodec.toYaml(info.declaration); + const completeDeclaration = ExplicitDeclarationAssistant.completeOrThrow(info.declaration, existingIDs); + const replacement = DeclarationCodec.toYaml(completeDeclaration); parts.push(data.slice(sliceStartIndex, startOffset)); parts.push(replacement); @@ -216,7 +218,7 @@ export class SyncManager { /** * {@link PostParseInfo.incompleteDeclarationInfos} should contain {@link DeclarationInfo}s - * that can be represented as statistics items and persisted in disk if some missing values are filled in. + * that can be represented as statistics items and persisted to disk if missing values are generated. * * This method filters out declarations that should not be persisted. * @param postInfo As populated by {@link DeclarationParser.getAllIDsInFile} or {@link DeclarationParser.getAllIDsFromMetadata}. @@ -225,10 +227,10 @@ export class SyncManager { private static getFrontSideInvalidDeclarations(postInfo: PostParseInfo) { const declarations = postInfo.incompleteDeclarationInfos .filter(info => ( - CardDeclarationAssistant.canMakeValidCardDeclarable(info.declaration) && - CardDeclarationAssistant.isFrontSide(info.declaration)) + ExplicitDeclarationAssistant.canComplete(info.declaration) && + ExplicitDeclarationAssistant.isFrontSide(info.declaration)) ); - console.assert(postInfo.incompleteDeclarationInfos.length == declarations.length, "Hmm") + return declarations; } @@ -237,7 +239,7 @@ export class SyncManager { ids.push(FullID.create( noteID, declarable.id, - CardDeclarationAssistant.isFrontSide(declarable) + ExplicitDeclarationAssistant.isFrontSide(declarable) )); } @@ -253,21 +255,29 @@ export class SyncManager { private static async processFrontmatter(file: TFile, fileManager: FileManager, info: DeclarationInfo, preventIDs: Set) { let completeDeclaration: CardDeclarable | null = null; - if (DeclarationParser.isExternalSectionCache(info.section) && info.section.externalType === "frontmatter") { - if (CardDeclarationAssistant.isValidCardDeclarable(info.declaration)) { - console.assert(false, "Expected an invalid declaration."); - completeDeclaration = null; // info.declaration; - } - else { - // Set default values and modify the file. - completeDeclaration = CardDeclarationAssistant.makeValidOrThrow(info.declaration, preventIDs); - const key = info.section.id; - if (isString(key)) { - // This seems to work fine while cursor is in the frontmatter (so no need to skip this declaration now because of that). - await fileManager.processFrontMatter(file, (fm) => fm[key] = completeDeclaration).catch(console.error); - } - else { - console.assert(key, "Expected section id to be set to the frontmatter YAML key assinged to the declaration."); + if (DeclarationParser.isExternalSectionCache(info.section)) { + switch (info.section.externalType) { + case UNARY_UNION_SUPPRESS: + break; + + case "frontmatter": { + if (ExplicitDeclarationAssistant.is(info.declaration) && ExplicitDeclarationAssistant.isValid(info.declaration)) { + Env.assert(false, "Expected an invalid declaration."); + completeDeclaration = null; // info.declaration; + } + else { + // Set default values and modify the file. + completeDeclaration = ExplicitDeclarationAssistant.completeOrThrow(info.declaration, preventIDs); + const key = info.section.id; + if (isString(key)) { + // This seems to work fine while cursor is in the frontmatter (so no need to skip this declaration now because of that). + await fileManager.processFrontMatter(file, (fm) => fm[key] = completeDeclaration).catch(console.error); + } + else { + Env.assert(key, "Expected section id to be set to the frontmatter YAML key assinged to the declaration."); + } + } + break; } } } @@ -279,8 +289,9 @@ export class SyncManager { Env.log.d(`SyncManager:syncIDs isSuspended: ${this.isSuspended}, num IDs: ${ids.length}`); if (this.isSuspended) return; - if (ids.length == 0) - return; + + // Continue even if ids.length == 0 + // The syncing mechanism looks at the diffs to be able to determine if IDs were removed. try { this.dataStore.syncData(ids, file.path, this.statisticsFactory); diff --git a/src/declarations/CardDeclaration.ts b/src/declarations/CardDeclaration.ts index c72c325..e3f31c0 100644 --- a/src/declarations/CardDeclaration.ts +++ b/src/declarations/CardDeclaration.ts @@ -1,228 +1,21 @@ -import { DeckableDeclarable, Declarable, Declaration, DeclarationRange, YamlParseErrorCallback } from "declarations/Declaration"; -import { isString } from "TypeAssistant"; -import { UniqueID } from "data/UniqueID"; - -export const enum IDScope { - UNIQUE, - NOTE, -}; - -export type DeclarationSide = "front" | "back"; - -/** -* A {@link DefaultableCardDeclarable} where all default values are set. -*/ -export interface CardDeclarable extends DefaultableCardDeclarable { - id: string; -} - -/** - * The minimum required propertes that need to be specified before - * default values and/or generated values can be applied - * to turn it into a {@link CardDeclarable}. - * - * See also: {@link CardDeclarationAssistant.conformsToDefaultable}. - */ -export interface DefaultableCardDeclarable extends DeckableDeclarable { - side: DeclarationSide; -} - -const DeclarationInterfacePropertyName = { - ID: "id", - SIDE: "side", -}; +import { CardDeclarable, DeclarationSide, IDScope } from "#/declarations/ExplicitDeclaration"; +import { DeclarableProperty, NullableStringDeclarableProperty } from "#/declarations/Declarable"; export class CardDeclaration implements CardDeclarable { + [key: string]: DeclarableProperty; + public deckID: NullableStringDeclarableProperty; public readonly id: string; public readonly side: DeclarationSide; - public deckID: string | null; - [key: string]: unknown; public readonly isAutoGenerated: boolean = false; public readonly idScope: IDScope; - public constructor(id: string, side: DeclarationSide, idScope: IDScope, deckID: string | null = null, isAutoGenerated = false) { + public constructor(id: string, side: DeclarationSide, idScope: IDScope, deckID: string | null = null, isAutoGenerated: boolean) { this.id = id; - this.side = side.trim().toLowerCase() as DeclarationSide; + this.side = side; this.idScope = idScope; this.deckID = deckID; this.isAutoGenerated = isAutoGenerated; } - - public get isFrontSide(): boolean { - return CardDeclarationAssistant.isFrontSide(this); - } -} - -export class CardDeclarationAssistant extends Declaration { - - public static fromFrontmatter(maybeDeclaration: Record, incompleteCallback?: (incomplete: DefaultableCardDeclarable, position: DeclarationRange) => void) { - if (this.conformsToDeclarable(maybeDeclaration)) - return new CardDeclaration(maybeDeclaration.id, maybeDeclaration.side, IDScope.UNIQUE, maybeDeclaration.deckID); - if (this.conformsToDefaultable(maybeDeclaration) && incompleteCallback) - // This position should really be the position in the front matter YAML where the declaration is. - // But, since this is the frontmatter, there's no need slice strings as editing is done with `obsidian` `FileManager.processFrontMatter`. - incompleteCallback(maybeDeclaration, { start: 0, end: 0 }); - return null; - } - - /** - * @param source The code block including the three ticks at the beginning and end. - * @param onParseError - * @param incompleteCallback Invoked if content of {@link source} is recognized but is missing required properties. - * @returns `null` if this block is unknown or it contains invalid YAML. - */ - public static parseCodeBlock( - source: string, - onParseError?: YamlParseErrorCallback, - incompleteCallback?: (incomplete: DefaultableCardDeclarable, position: DeclarationRange) => void) { - - const info = super.parseAndCheckCodeBlock(source); - if (!info) - return null; - - const maybeDeclaration = this.tryParseYaml(info.content, onParseError); - if (!maybeDeclaration) - return null; - - if (this.conformsToDeclarable(maybeDeclaration)) // Doesn't need to be valid here. - return new CardDeclaration(maybeDeclaration.id, maybeDeclaration.side, IDScope.UNIQUE, maybeDeclaration.deckID); - - if (CardDeclarationAssistant.conformsToDefaultable(maybeDeclaration) && incompleteCallback) - incompleteCallback(maybeDeclaration, info.location satisfies DeclarationRange); - - return null; - } - - //#region - - public static canMakeValidCardDeclarable(decl: unknown): decl is DefaultableCardDeclarable { - // Cannot complete if its not the correct object. - if (!CardDeclarationAssistant.conformsToDefaultable(decl)) - return false; - - // Already valid - if (CardDeclarationAssistant.isValidCardDeclarable(decl)) - return false; - - return true; - } - - /** - * Checks if all values of the given {@link CardDeclarable} are valid. - * @param decl - * @returns - */ - public static isValidCardDeclarable(decl: Declarable): decl is CardDeclarable { - return ( - CardDeclarationAssistant.conformsToDeclarable(decl) && - - // Check all values - CardDeclarationAssistant.isIDValid(decl.id) && - CardDeclarationAssistant.isSideValid(decl.side) && - CardDeclarationAssistant.isDeckIDValid(decl.deckID) - ); - } - - /** - * Completes a {@link DefaultableCardDeclarable} by assigning default and generated values - * to non-required properties. - * - * @param decl - * @param preventIDs See {@link UniqueID.generateID} - * @returns `null` if {@link canMakeValidCardDeclarable} returns `false`. - */ - private static tryToMakeValid(decl: DefaultableCardDeclarable, preventIDs?: Set) { - if (this.canMakeValidCardDeclarable(decl)) { - const declWithDefaultValues = { - ...decl, - ...{ - // Add all default values - id: UniqueID.generateID(preventIDs) - } - } satisfies CardDeclarable; - return declWithDefaultValues as CardDeclarable; - } - return null; - } - - /** - * May use if already checked with {@link canMakeValidCardDeclarable}. - * @param decl - * @param preventIDs See {@link UniqueID.generateID} - * @returns Result of calling {@link tryToMakeValid}. - */ - public static makeValidOrThrow(decl: DefaultableCardDeclarable, preventIDs?: Set) { - const maybeCompleted = this.tryToMakeValid(decl, preventIDs); - if (maybeCompleted === null) - throw new Error("Could not complete given declaration block."); - return maybeCompleted; - } - - //#endregion - - //#region - - /** - * Check if {@link value} conforms to {@link CardDeclarable}; - * i.e., if the former can be cast to the latter. - * - * @param value - * @returns `false` if {@link value} is `null`. - */ - public static conformsToDeclarable(value: unknown): value is CardDeclarable { - if (!CardDeclarationAssistant.conformsToDefaultable(value)) - return false; - - const id = value[DeclarationInterfacePropertyName.ID]; - if (id === undefined) - value[DeclarationInterfacePropertyName.ID] = ""; - if (isString(id)) - return true; - - return false; // e.g. id is a number - } - - /** - * Check if {@link value} conforms to {@link DefaultableCardDeclarable}; - * i.e., if the former can be cast to the latter. - * - * @param value - * @returns `false` if {@link value} is `null`. - */ - public static conformsToDefaultable(value: unknown): value is DefaultableCardDeclarable { - if (!CardDeclarationAssistant.conformsToDeckable(value)) - return false; - - if (!Object.hasOwn(value, DeclarationInterfacePropertyName.SIDE)) - return false; - - return true; - } - - private static isIDValid(id: string) { - return UniqueID.isValid(id); - } - - /** - * @returns `true` if {@link side} is one of the allowed values for {@link DefaultableCardDeclarable.side} - */ - private static isSideValid(side: string) { - return [ - ...this.frontSideValues, - ...this.backSideValues - ].includes(side.trim().toLowerCase()); - } - - public static isFrontSide(decl: DefaultableCardDeclarable, throwIfNotValid = false) { - if (throwIfNotValid && !this.isSideValid(decl.side)) - throw new Error(`Side is not valid: ${decl.side}`); - return CardDeclarationAssistant.frontSideValues.includes(decl.side); - } - - private static readonly frontSideValues = ["f", "front"]; - private static readonly backSideValues = ["b", "back"]; - - //#endregion } diff --git a/src/declarations/Collectionable.ts b/src/declarations/Collectionable.ts new file mode 100644 index 0000000..fa5a793 --- /dev/null +++ b/src/declarations/Collectionable.ts @@ -0,0 +1,82 @@ +import { UniqueID } from "#/data/UniqueID"; +import { Declarable, DeclarableAssistant, NullableStringDeclarableProperty } from "#/declarations/Declarable"; +import { LocalStrictKeys, Obj } from "#/utils/ts"; + +export interface DeckableDeclarable extends Declarable { + /** Optional. */ + deckID: NullableStringDeclarableProperty; +} + +const DeckableUserKeyName = { + DECK: "deck" +}; + +const DeckablePropertyName = { + DECK_ID: "deckID", +}; + +type PropertyNames = LocalStrictKeys; + +/** + * Helpers related to {@link DeckableDeclarable}. + */ +export class CollectionableAssistant extends DeclarableAssistant { + + public static override is(value: unknown): value is DeckableDeclarable { + if (!DeclarableAssistant.is(value)) + return false; + + // Optional. Add if not exists + if (!Obj.hasKey(value, "deckID")) + Obj.setKey(value, "deckID", CollectionableAssistant.PropertyValue.FALLBACK_TO_SYSTEM_DEFAULT); + + if (!DeclarableAssistant.PropertyType.isNullableString(Obj.getKey(value, "deckID"))) + return false; + + return true; + } + + public static override isValid(declarable: DeckableDeclarable) { + if (!DeclarableAssistant.isValid(declarable)) + return false; + return declarable.deckID === null || UniqueID.isValid(declarable.deckID); + } + + /** + * Creates a copy of the declaration with the specified deck ID. + */ + public static copyWithDeck(declaration: T, deckID: string | null): T { + return { + ...declaration, + deckID: deckID, + }; + } + + /** + * Transform user friendly YAML keys to interface/class properties. + * Opposite of {@link toUserFriendlyKeys}. + * + * @param obj + */ + public static override fromUserFriendlyKeys(obj: Record) { + DeclarableAssistant.fromUserFriendlyKeys(obj); + + if (Object.hasOwn(obj, DeckableUserKeyName.DECK)) { + const deckID = obj[DeckableUserKeyName.DECK]; + delete obj[DeckableUserKeyName.DECK]; + obj[DeckablePropertyName.DECK_ID] = deckID; + } + } + + public static override toUserFriendlyKeys(obj: Declarable) { + DeclarableAssistant.toUserFriendlyKeys(obj); + + if (Object.hasOwn(obj, DeckablePropertyName.DECK_ID)) { + const deckID = obj[DeckablePropertyName.DECK_ID]; + delete obj[DeckablePropertyName.DECK_ID]; + // Only include if specific deck set. + if (deckID) + obj[DeckableUserKeyName.DECK] = deckID; + } + } +} diff --git a/src/declarations/CommandDeclaration.ts b/src/declarations/CommandDeclaration.ts deleted file mode 100644 index 1b1cbb2..0000000 --- a/src/declarations/CommandDeclaration.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { isObject, isString } from "TypeAssistant"; -import { CommandDeclarationParsable } from "declarations/CommandDeclarationParser"; -import { DeckableDeclarable, Declaration, DeclarationRange, YamlParseErrorCallback } from "declarations/Declaration"; -import { AlternateHeadingsAssistant, AlternateHeadingsDeclarable } from "declarations/commands/AlternateHeadings"; -import { HeadingAndDelimiterAssistant, HeadingAndDelimiterDeclarable } from "declarations/commands/HeadingAndDelimiter"; -import { HeadingIsFrontAssistant, HeadingIsFrontDeclarable } from "declarations/commands/HeadingIsFront"; - -const AlternateHeadingsCommandNames = [ - "alternate headings", "alt headings", "ah", -] as const; - -const HeadingAndDelimiterCommandNames = [ - "heading and delimiter", "hd", -] as const; - -const HeadingIsFrontCommandNames = [ - "heading is front", "hf", -] as const; - -const TableCommandNames = [ - "table", -] as const; - -const CommandNames = [ - ...AlternateHeadingsCommandNames, - ...HeadingAndDelimiterCommandNames, - ...HeadingIsFrontCommandNames, - ...TableCommandNames, -] as const; - -type CommandName = typeof CommandNames[number]; - -export interface CommandableDeclarable extends DeckableDeclarable { - name: CommandName; -} - -export class CommandDeclarationAssistant extends Declaration { - - /** - * @param source - * @param onParseError The formatting of {@link source} invalid YAML. - * @param onInvalidType - * @returns `null` if {@link source} is not recognized. - */ - public static createParser( - source: string, - onParseError?: YamlParseErrorCallback, - onInvalidType?: (command: CommandableDeclarable, range: DeclarationRange) => void) { - - const info = super.parseAndCheckCodeBlock(source); - if (!info) - return null; - - const obj = super.tryParseYaml(info.content, onParseError); - const commandable = obj && CommandDeclarationAssistant.conforms(obj) ? obj : null; - if (!commandable) - return null; - - let parser: CommandDeclarationParsable | null = null; - - if (CommandDeclarationAssistant.isAlternateHeadings(commandable)) - parser = AlternateHeadingsAssistant.tryCreateParser(commandable); - else if (CommandDeclarationAssistant.isHeadingAndDelimiter(commandable)) - parser = HeadingAndDelimiterAssistant.tryCreateParser(commandable); - else if (CommandDeclarationAssistant.isHeadingIsFront(commandable)) - parser = HeadingIsFrontAssistant.tryCreateParser(commandable); - - if (!parser && onInvalidType) - onInvalidType(commandable, info.location satisfies DeclarationRange); - - return parser; - } - - public static isAlternateHeadings(declaration: CommandableDeclarable): declaration is AlternateHeadingsDeclarable { - return AlternateHeadingsCommandNames.includes(declaration.name as any); - } - - public static isHeadingAndDelimiter(declaration: CommandableDeclarable): declaration is HeadingAndDelimiterDeclarable { - return HeadingAndDelimiterCommandNames.includes(declaration.name as any); - } - - public static isHeadingIsFront(declaration: CommandableDeclarable): declaration is HeadingIsFrontDeclarable { - return HeadingIsFrontCommandNames.includes(declaration.name as any); - } - - /** - * Checks if the provided object minimally conforms to the structure of a {@link CommandableDeclarable}. - * @param obj The object to check. - * @returns `true` if the object has at least the minimum properties expected of a {@link CommandableDeclarable}, `false` otherwise. - */ - public static conforms(obj: Record): obj is CommandableDeclarable { - return ( - isObject(obj) && - Object.hasOwn(obj, "name") && isString(obj.name) - ); - } - - /** - * Whether the type value of the provided {@link CommandableDeclarable} is an valid/existing type. - * @param command - * @returns - */ - public static isNameValid(command: CommandableDeclarable) { - return CommandNames.includes(command.name); - } -} diff --git a/src/declarations/CommandDeclarationParser.ts b/src/declarations/CommandDeclarationParser.ts index 529c538..e872ee2 100644 --- a/src/declarations/CommandDeclarationParser.ts +++ b/src/declarations/CommandDeclarationParser.ts @@ -1,9 +1,17 @@ -import { CardDeclaration, IDScope } from "declarations/CardDeclaration"; -import { CommandableDeclarable } from "declarations/CommandDeclaration"; -import { FileParser, SectionRange } from "utils/obs/FileParser"; +import { CardDeclaration } from "declarations/CardDeclaration"; +import { CommandableDeclarable } from "declarations/Commandable"; +import { IDScope } from "declarations/ExplicitDeclaration"; import { CacheItem } from "obsidian"; +import { FileParser, SectionRange } from "utils/obs/FileParser"; export interface CommandDeclarationParsable { + /** + * Creates a {@link CardDeclaration} and adds it to {@link generatedDeclarations}. + * @param sectionLevel + * @param inBetweenDelimiter + * @param index + * @param delimiters + */ parse(sectionLevel: number, inBetweenDelimiter: CacheItem, index: number, delimiters: CacheItem[]): void; generatedDeclarations: GeneratedContentDeclaration[]; } @@ -16,10 +24,26 @@ export type GeneratedContentDeclaration = { range: SectionRange, }; +/** + * Provides common functionality for {@link CommandableDeclarable} parsers. + * @abstract + */ export abstract class CommandDeclarationParser extends FileParser implements CommandDeclarationParsable { + public static readonly Factory = { + createEntry: ( + names: readonly string[], + ParserClass: { + new(commandable: T): CommandDeclarationParsable; + tryCreate(value: CommandableDeclarable): CommandDeclarationParsable | null; + }) => ({ + names, + create: (d: CommandableDeclarable) => ParserClass.tryCreate(d) + }) + }; + abstract parse(sectionLevel: number, inBetweenDelimiter: CacheItem, index: number, delimiters: CacheItem[]): void; public generatedDeclarations: GeneratedContentDeclaration[] = []; @@ -30,7 +54,12 @@ export abstract class CommandDeclarationParser this.commandable = commandable; } - protected generateDeclaration(id: string, isFront: boolean, startDelimiter: CacheItem | null, endDelimiter: CacheItem | null, scope: IDScope = IDScope.NOTE) { + protected generateDeclaration( + id: string, + isFront: boolean, + startDelimiter: CacheItem | null, + endDelimiter: CacheItem | null, + scope: IDScope = IDScope.Note) { this.generatedDeclarations.push({ declaration: new CardDeclaration( id, @@ -59,4 +88,11 @@ export abstract class CommandDeclarationParser protected get lastID() { return this.lastDeclaration().declaration.id; } + + protected tryParseUniqueID(text: string) { + const match = this.FULL_ID_REGEX.exec(text); + const result = match?.[1]; + return result !== undefined ? result.toLowerCase() : null; + } + protected readonly FULL_ID_REGEX = /@([^\s]+)/i; } diff --git a/src/declarations/CommandNames.ts b/src/declarations/CommandNames.ts new file mode 100644 index 0000000..3610160 --- /dev/null +++ b/src/declarations/CommandNames.ts @@ -0,0 +1,35 @@ +const AlternateHeadingsCommandNames = [ + "alternate headings", "alt headings", "ah", +] as const; + +const HeadingAndDelimiterCommandNames = [ + "heading and delimiter", "hd", +] as const; + +const HeadingIsFrontCommandNames = [ + "heading is front", "hf", +] as const; + +const TableCommandNames = [ + "table", +] as const; + +const AllCommandNames = [ + ...AlternateHeadingsCommandNames, + ...HeadingAndDelimiterCommandNames, + ...HeadingIsFrontCommandNames, + ...TableCommandNames, +] as const + +export type CommandName = typeof AllCommandNames[number]; + +export const Commands = { + Name: { + All: AllCommandNames, + AlternateHeadings: AlternateHeadingsCommandNames, + HeadingAndDelimiter: HeadingAndDelimiterCommandNames, + HeadingIsFront: HeadingIsFrontCommandNames, + Table: TableCommandNames, + exists: (name: string): name is CommandName => (AllCommandNames as readonly string[]).includes(name), + } +}; diff --git a/src/declarations/Commandable.ts b/src/declarations/Commandable.ts new file mode 100644 index 0000000..a18c3d8 --- /dev/null +++ b/src/declarations/Commandable.ts @@ -0,0 +1,40 @@ +import { CollectionableAssistant, DeckableDeclarable } from "#/declarations/Collectionable"; +import { CommandName, Commands } from "#/declarations/CommandNames"; +import { LocalStrictKeys, Obj, Str } from "#/utils/ts"; + +/** + * @abstract All commands extend this interface. + */ +export interface CommandableDeclarable extends DeckableDeclarable { + name: CommandName; +} + +type PropertyNames = LocalStrictKeys; + +/** + * Helpers related to {@link CommandableDeclarable}. + */ +export class CommandableAssistant extends CollectionableAssistant { + + /** + * Checks if the provided object minimally conforms to the structure of a {@link CommandableDeclarable}. + * @param value The object to check. + * @returns `true` if the object has at least the minimum properties expected of a {@link CommandableDeclarable}, `false` otherwise. + */ + public static override is(value: unknown): value is CommandableDeclarable { + if (!CollectionableAssistant.is(value)) + return false; + + if (!Obj.hasKey(value, "name", (value) => Str.is(value))) + return false; + + return true; + } + + public static override isValid(declaration: CommandableDeclarable): boolean { + if (!CollectionableAssistant.isValid(declaration)) + return false; + + return Commands.Name.exists(declaration.name); + } +} diff --git a/src/declarations/Declarable.ts b/src/declarations/Declarable.ts new file mode 100644 index 0000000..c3c7bf6 --- /dev/null +++ b/src/declarations/Declarable.ts @@ -0,0 +1,102 @@ +import { Bln, Null, Num, Obj, Str } from "#/utils/ts"; + +/** + * Allowable non-nullable types. + * + * - {@link DeclarableAssistant.PropertyType.isNullable} needs to be updated if types are added or removed. + */ +type NonNullableDeclarableProperty = string | number | boolean; + +/** Allowable types. */ +export type DeclarableProperty = NonNullableDeclarableProperty | null | undefined; + +export type StringDeclarableProperty = Extract; +export type NullableStringDeclarableProperty = Extract; +export type OptionalNullableStringDeclarableProperty = Extract; + +export type NumberDeclarableProperty = Extract; +export type NullableNumberDeclarableProperty = Extract; +export type OptionalNullableNumberDeclarableProperty = Extract; + +/** + * @abstract + */ +export interface Declarable { + [key: string]: DeclarableProperty; +} + +/** + * Helpers related to {@link Declarable}. + */ +export class DeclarableAssistant { + + public static is(value: unknown): value is Declarable { + return Obj.is(value); + } + + public static isValid(_value: unknown): boolean { + return true; + } + + protected static readonly PropertyValue = { + /** Indicates that this optional property was not provided. */ + OPTIONAL_NOT_ADDED: undefined, + /** "I am not providing a value, so use the system default." */ + FALLBACK_TO_SYSTEM_DEFAULT: null, + /** "I am explicitly providing a value, and that value is 'nothing'." */ + EXPLICIT_EMPTY_STRING: Str.EMPTY, + /** This property is of type {@link StringDeclarableProperty} and will be populated with a value programatically. Set it to this temporary value to satisfy the non-nullable type. */ + AWAITING_VALUE: Str.EMPTY, + } as const; + + /** + * Some of these do exactly the same as the {@link PropertyType} equivalents, but use them anyway for "semantic clarity". + */ + public static readonly PropertyEq = { + /** Property was not provided. */ + optional: (value: unknown): value is typeof DeclarableAssistant.PropertyValue.OPTIONAL_NOT_ADDED => + value === DeclarableAssistant.PropertyValue.OPTIONAL_NOT_ADDED, + + optionalOrNull: (value: unknown): value is typeof DeclarableAssistant.PropertyValue.OPTIONAL_NOT_ADDED | typeof DeclarableAssistant.PropertyValue.FALLBACK_TO_SYSTEM_DEFAULT => + value === DeclarableAssistant.PropertyValue.OPTIONAL_NOT_ADDED || value === null, + + optionalNullOrString: (value: unknown, predicate?: (str: string) => boolean): value is OptionalNullableStringDeclarableProperty => + Str.is(value) && (predicate === undefined || predicate(value)) || DeclarableAssistant.PropertyEq.optionalOrNull(value), + + optionalNullOrNumber: (value: unknown, predicate?: (num: number) => boolean): value is OptionalNullableNumberDeclarableProperty => + Num.is(value) && (predicate === undefined || predicate(value)) || DeclarableAssistant.PropertyEq.optionalOrNull(value), + + any: (value: unknown): value is DeclarableProperty => + Str.is(value) || Num.is(value) || Bln.is(value) || DeclarableAssistant.PropertyEq.optionalOrNull(value), + }; + + protected static readonly PropertyType = { + + isNullable: (value: unknown) => Null.is(value), + + isNullableString: (value: unknown): value is NullableStringDeclarableProperty => Str.is(value) || Null.is(value), + + isNullableNumber: (value: unknown): value is NullableNumberDeclarableProperty => Num.is(value) || Null.is(value), + + isOptionalNullableString: (value: unknown): value is OptionalNullableStringDeclarableProperty => + value === undefined || DeclarableAssistant.PropertyType.isNullableString(value), + + isOptionalNullableNumber: (value: unknown): value is OptionalNullableNumberDeclarableProperty => + value === undefined || DeclarableAssistant.PropertyType.isNullableNumber(value), + + isStr: (value: unknown): value is StringDeclarableProperty => Str.is(value), + isNum: (value: unknown): value is NumberDeclarableProperty => Num.is(value), + }; + + /** + * Transform user friendly YAML keys to interface/class properties. + * Opposite of {@link toUserFriendlyKeys}. + * + * @param obj + */ + protected static fromUserFriendlyKeys(_obj: Record) { + } + + protected static toUserFriendlyKeys(_obj: Declarable) { + } +} diff --git a/src/declarations/Declaration.ts b/src/declarations/Declaration.ts deleted file mode 100644 index 9f3c328..0000000 --- a/src/declarations/Declaration.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { FileParser } from "utils/obs/FileParser"; -import { parseYaml, stringifyYaml } from "obsidian"; -import { isObject, isString } from "TypeAssistant"; -import { UniqueID } from "data/UniqueID"; - -export interface Declarable { - [key: string]: unknown; -} - -export interface DeckableDeclarable extends Declarable { - deckID: string | null; -} - -const DeckableUserKeyName = { - DECK: "deck" -}; - -const DeckablePropertyName = { - DECK_ID: "deckID", -}; - -/** - * Locates the portion of a string that contains the raw declaration string. - * @todo Remove. See types in {@link FileParser} - */ -export interface DeclarationRange { - start: number, - end: number, -} - -export type YamlParseErrorCallback = (error: Error) => void; - -export abstract class Declaration { - - private static readonly LANGUAGE = "comethrough"; - private static readonly LANGUAGE_SHORT = "ct"; - - public static get supportedCodeBlockLanguages() { - return [Declaration.LANGUAGE, Declaration.LANGUAGE_SHORT]; - } - - public static get supportedFrontmatterKeys() { - return [Declaration.LANGUAGE, Declaration.LANGUAGE_SHORT, "come through"]; - } - - protected static slice(source: string, location: DeclarationRange) { - return source.slice(location.start, location.end); - } - - /** - * Checks if this a code block with one of the expected languages; if so, parses it. - * @param source The code block including the three ticks at the beginning and end. - * @returns `null` is {@link source} is not a code block or if the block's language is unexpected. - */ - protected static parseAndCheckCodeBlock(source: string) { - const info = FileParser.parseCodeBlock(source); - - if (info && !this.supportedCodeBlockLanguages.includes(info.language)) - return null; - - return info; - } - private static readonly CODE_BLOCK_MARKER_LENGTH = 3; - - /** - * Case insensitive. - * - * @param yaml Will be converted to lower case before parsing. - * @param onParseError - * @returns `null` if YAML parsing failed, in which case {@link onParseError} will be invoked. - */ - public static tryParseYaml(yaml: string, onParseError?: YamlParseErrorCallback) { - let parsedObject: Record | null = null; - - try { - parsedObject = parseYaml(yaml.toLowerCase()); - if (parsedObject) - this.fromUserFriendlyKeys(parsedObject); - } - catch (error) { - if (error instanceof Error && error.name === "YAMLParseError") - onParseError?.(error); - else - throw error; - } - - return parsedObject; - } - - public static toString(declaration: Declarable) { - this.toUserFriendlyKeys(declaration); - return stringifyYaml(declaration); - } - - /** - * Transform user friendly YAML keys to interface/class properties. - * Opposite of {@link toUserFriendlyKeys}. - * - * @param obj - */ - private static fromUserFriendlyKeys(obj: Record) { - if (Object.hasOwn(obj, DeckableUserKeyName.DECK)) { - const deckID = obj[DeckableUserKeyName.DECK]; - delete obj[DeckableUserKeyName.DECK]; - obj[DeckablePropertyName.DECK_ID] = deckID; - } - } - - private static toUserFriendlyKeys(obj: Declarable) { - if (Object.hasOwn(obj, DeckablePropertyName.DECK_ID)) { - const deckID = obj[DeckablePropertyName.DECK_ID]; - delete obj[DeckablePropertyName.DECK_ID]; - // Only include if specific deck set. - if (deckID) - obj[DeckableUserKeyName.DECK] = deckID; - } - } - - protected static conformsToDeclarableBase(value: unknown): value is Declarable { - return isObject(value); - } - - protected static conformsToDeckable(value: unknown): value is DeckableDeclarable { - if (!Declaration.conformsToDeclarableBase(value)) - return false; - if (!Object.hasOwn(value, DeckablePropertyName.DECK_ID)) - value[DeckablePropertyName.DECK_ID] = null; - return true; - } - - public static copyWithDeck(declaration: T, deckID: string | null): T { - return { - ...declaration, - ...{ - deckID: deckID, - } satisfies DeckableDeclarable - } - } - - protected static isDeckIDValid(id: string | null) { - return id === null || (isString(id) && UniqueID.isValid(id)); - } -} diff --git a/src/declarations/DeclarationCodec.ts b/src/declarations/DeclarationCodec.ts new file mode 100644 index 0000000..8eb6a6e --- /dev/null +++ b/src/declarations/DeclarationCodec.ts @@ -0,0 +1,54 @@ +import { CollectionableAssistant } from "#/declarations/Collectionable"; +import { Declarable } from "#/declarations/Declarable"; +import { Obj } from "#/utils/ts"; +import { parseYaml, stringifyYaml } from "obsidian"; + +/** Key types are constrained to string keys only. */ +export type YamlObject = Record; +export type YamlParseErrorCallback = (error: Error) => void; + +export class DeclarationCodec { + + /** + * - Case insensitive. + * - Values of empty properties (e.g. `prop: `) are set to `null` by the YAML parser as `null` is a valid YAML value. `undefined` does not exist in YAML. + * + * @param yaml Will be converted to lower case before parsing. + * @param onParseError + * @returns `null` if YAML parsing failed, in which case {@link onParseError} will be invoked. + */ + public static tryFromYaml(yaml: string, onParseError?: YamlParseErrorCallback) { + let parsedObject: YamlObject | null = null; + + try { + const raw = parseYaml(yaml); + + if (Obj.is(raw)) { + parsedObject = {}; + + for (const [key, value] of Object.entries(raw)) { + // - YAML allows keys to be e.g. numbers. Make sure they are strings. + // - Also lowercase them to match property names in code (or camel cased) + parsedObject[String(key).toLowerCase()] = value; + } + + // If the value has white space, e.g. `prop: `, `null` is still returned as YAML trims whitespace after the colon. + // However, content inside quotes are preserverd. `prop: " "` will have the value of a string with a space. + Obj.trimValues(parsedObject); + CollectionableAssistant.fromUserFriendlyKeys(parsedObject); + } + } catch (error) { + if (error instanceof Error && error.name === "YAMLParseError") + onParseError?.(error); + else + throw error; + } + + return parsedObject; + } + + public static toYaml(declaration: Declarable): string { + CollectionableAssistant.toUserFriendlyKeys(declaration); + return stringifyYaml(declaration); + } +} diff --git a/src/declarations/DeclarationManager.ts b/src/declarations/DeclarationManager.ts index 14188da..869cc36 100644 --- a/src/declarations/DeclarationManager.ts +++ b/src/declarations/DeclarationManager.ts @@ -1,8 +1,12 @@ -import { DataStore } from "data/DataStore"; -import { CardDeclarationAssistant } from "declarations/CardDeclaration"; -import { DeckableDeclarable, Declaration } from "declarations/Declaration"; -import { DeclarationRenderChild } from "renderings/declarations/DeclarationRenderChild"; -import { DeckModal } from "modals/DeckModal"; +import { DataStore } from "#/data/DataStore"; +import { DeckID } from "#/data/FullID"; +import { CollectionableAssistant, DeckableDeclarable } from "#/declarations/Collectionable"; +import { DeclarationConstants } from "#/declarations/constants"; +import { DeclarationCodec } from "#/declarations/DeclarationCodec"; +import { Env } from "#/env"; +import { DeckModal } from "#/modals/DeckModal"; +import { DeclarationRenderChild } from "#/renderings/declarations/DeclarationRenderChild"; +import { HtmlTag } from "#/utils/dom/constants"; import { App, MarkdownPostProcessorContext, MarkdownSectionInformation, TFile, Vault } from "obsidian"; /** @@ -13,7 +17,7 @@ import { App, MarkdownPostProcessorContext, MarkdownSectionInformation, TFile, V export class DeclarationManager { public static get supportedCodeBlockLanguages() { - return Declaration.supportedCodeBlockLanguages; + return DeclarationConstants.CodeBlock.LANGUAGES; } public static async processCodeBlock( @@ -24,7 +28,8 @@ export class DeclarationManager { data: DataStore) { const renderer = new DeclarationRenderChild(el, source, { - getAllDecks: () => data.getAllDecks() + getAllDecks: () => data.getAllDecks(), + getDeck: (id: DeckID) => data.getDeck(id), }); ctx.addChild(renderer); // The MarkdownPostProcessorContext manage unload, e.g., when file is closed. @@ -36,44 +41,49 @@ export class DeclarationManager { app.vault, file, () => ctx.getSectionInfo(el), - Declaration.toString(changedDeclaration) + DeclarationCodec.toYaml(changedDeclaration) ); } renderer.render((declaration, type, deckSelectEl) => { const file = app.vault.getFileByPath(ctx.sourcePath); - console.assert(file); + Env.assert(file !== null); if (!file) return; - if (type === "deckAdded") { - DeckModal.add(app, data, async (addedDeck) => { + switch (type) { + case "deckAdded": { + DeckModal.add(app, data, async (addedDeck) => { - // Add a new option for the created deck - deckSelectEl.createEl("option", { - text: addedDeck.data.n, - value: addedDeck.id, - }, (el) => { - el.selected = true; + // Add a new option for the created deck + deckSelectEl.createEl(HtmlTag.SELECT.OPTION.NAME, { + text: addedDeck.data.n, + value: addedDeck.id, + }, (el) => { + HtmlTag.SELECT.OPTION.select(el); + }); + + handleChangedDeclaration( + CollectionableAssistant.copyWithDeck(declaration, addedDeck.id), + file + ).catch(console.error); }); + break; + } + case "deckChanged": { + const selectedDeckID = HtmlTag.SELECT.OPTION.isNone(deckSelectEl.value) ? null : deckSelectEl.value; + if (selectedDeckID === declaration.deckID) + return; + handleChangedDeclaration( - CardDeclarationAssistant.copyWithDeck(declaration, addedDeck.id), + CollectionableAssistant.copyWithDeck(declaration, selectedDeckID), file ).catch(console.error); - }); - } - else if (type === "deckChanged") { - const selectedDeckID = deckSelectEl.value ? deckSelectEl.value : null; - if (selectedDeckID === declaration.deckID) - return; - - handleChangedDeclaration( - CardDeclarationAssistant.copyWithDeck(declaration, selectedDeckID), - file - ).catch(console.error); + break; + } } }); } diff --git a/src/declarations/DeclarationParser.ts b/src/declarations/DeclarationParser.ts index 9ebaa97..6cd5fa7 100644 --- a/src/declarations/DeclarationParser.ts +++ b/src/declarations/DeclarationParser.ts @@ -1,11 +1,14 @@ -import { FullID, IDFilter, NoteID } from "data/FullID"; -import { CardDeclarable, CardDeclarationAssistant, DefaultableCardDeclarable } from "declarations/CardDeclaration"; -import { CommandableDeclarable, CommandDeclarationAssistant } from "declarations/CommandDeclaration"; -import { Declaration, DeclarationRange } from "declarations/Declaration"; +import { FullID, IDFilter, NoteID } from "#/data/FullID"; +import { CommandableAssistant, CommandableDeclarable } from "#/declarations/Commandable"; +import { CommandDeclarationParsable } from "#/declarations/CommandDeclarationParser"; +import { DeclarationConstants } from "#/declarations/constants"; +import { DeclarationCodec, YamlParseErrorCallback } from "#/declarations/DeclarationCodec"; +import { CardDeclarable, DefaultableCardDeclarable, ExplicitDeclarationAssistant } from "#/declarations/ExplicitDeclaration"; +import { ParserRegistry } from "#/declarations/ParserRegistry"; +import { asNoteID, fullIDFromDeclaration } from "#/TypeAssistant"; +import { UnexpectedUndefinedError } from "#/utils/errors"; +import { FileParser, OffsetRange, SectionRange } from "#/utils/obs/FileParser"; import { App, CachedMetadata, CacheItem, FrontMatterCache, HeadingCache, SectionCache, TFile } from "obsidian"; -import { asNoteID, fullIDFromDeclaration } from "TypeAssistant"; -import { UnexpectedUndefinedError } from "utils/errors"; -import { FileParser, SectionRange } from "utils/obs/FileParser"; /** * Contains auxiliary information collected during the parsing process. @@ -27,7 +30,7 @@ export interface PostParseInfo { } /** - * All info needed to extract a {@link CardDeclarationAssistant|declaration block} from a note. + * All info needed to extract a {@link CardDeclarable | declaration block} from a note. */ export interface DeclarationInfo extends DeclarationInfoBase { /** The declaration candidate. */ @@ -45,7 +48,7 @@ interface DeclarationInfoBase { /** The {@link SectionCache} in {@link noteID} where declaration was found. */ section: SectionCache; /** The location of the declaration within the {@link section}. */ - location: DeclarationRange; + location: OffsetRange; } export class DeclarationParser extends FileParser { @@ -67,7 +70,7 @@ export class DeclarationParser extends FileParser { let cache; try { - cache = this.fileCacheOrThrow(app, file); + cache = this.fileCacheOrThrow(app, file); } catch (error) { console.error(error); @@ -78,7 +81,7 @@ export class DeclarationParser extends FileParser { // Check frontmatter for explicit declaration if (cache.frontmatter) { - for (const key of Declaration.supportedFrontmatterKeys) + for (const key of DeclarationConstants.Frontmatter.KEYS) if (Object.hasOwn(cache.frontmatter, key)) return true; } @@ -94,8 +97,8 @@ export class DeclarationParser extends FileParser { for (const section of cache.sections) { if (this.isCodeSection(section)) { if (fileContent) { - const info = this.parseCodeBlock(this.extractContentFromSection(section, fileContent)); - if (info !== null && Declaration.supportedCodeBlockLanguages.includes(info.language)) + const info = DeclarationParser.parseCodeBlock(this.extractContentFromSection(section, fileContent)); + if (info !== null && DeclarationConstants.CodeBlock.isSupportedLanguage(info.language)) return true; } else { @@ -164,7 +167,7 @@ export class DeclarationParser extends FileParser { const declaration = this.getDeclarationFromFrontmatter(cache.frontmatter, noteID, parseInfo); if (declaration) { const id = fullIDFromDeclaration(declaration, noteID); - if (!checkExistance(id) && (filter === undefined || (filter && filter(id)))) + if (!checkExistance(id) && (filter === undefined || filter(id))) ids.push(id); } } @@ -173,7 +176,7 @@ export class DeclarationParser extends FileParser { for (const currentHeading of cache.headings ?? []) { const id = this.findFullIDInText(currentHeading.heading, noteID); if (id !== null && !checkExistance(id)) { - if (filter === undefined || (filter && filter(id))) + if (filter === undefined || filter(id)) ids.push(id); } } @@ -183,7 +186,7 @@ export class DeclarationParser extends FileParser { const createAndAddIDFromDeclaration = (declaration: CardDeclarable) => { const id = fullIDFromDeclaration(declaration, noteID); - if (!checkExistance(id) && (filter === undefined || (filter && filter(id)))) + if (!checkExistance(id) && (filter === undefined || filter(id))) ids.push(id); } @@ -236,8 +239,8 @@ export class DeclarationParser extends FileParser { * @returns The first declaration found in {@link frontmatter}. */ protected static getDeclarationFromFrontmatter(frontmatter: FrontMatterCache, noteID: NoteID, parseInfo?: PostParseInfo) { - for (const key of Declaration.supportedFrontmatterKeys) { - const declaration = CardDeclarationAssistant.fromFrontmatter(frontmatter[key], (incomplete, location) => { + for (const key of DeclarationConstants.Frontmatter.KEYS) { + const declaration = DeclarationParser.declarationFromFrontmatter(frontmatter[key], (incomplete, location) => { parseInfo?.incompleteDeclarationInfos.push({ noteID: noteID, declaration: incomplete, @@ -264,7 +267,7 @@ export class DeclarationParser extends FileParser { const source = FileParser.extractContentFromSection(section, fileContent); - return CardDeclarationAssistant.parseCodeBlock( + return DeclarationParser.createExplicitDeclaration( source, (parseError) => { parseInfo?.invalidYaml.push({ @@ -273,12 +276,12 @@ export class DeclarationParser extends FileParser { error: parseError }); }, - (incomplete, location) => { + (incomplete, range) => { parseInfo?.incompleteDeclarationInfos.push({ noteID: noteID, declaration: incomplete, section: section, - location: location, + location: range, }); } ); @@ -298,7 +301,7 @@ export class DeclarationParser extends FileParser { const source = fileContent.slice(section.position.start.offset, section.position.end.offset); - const parser = CommandDeclarationAssistant.createParser( + const parser = DeclarationParser.createCommandDeclarationParser( source, (parseError) => { parseInfo?.invalidYaml.push({ @@ -361,6 +364,98 @@ export class DeclarationParser extends FileParser { }); } + /** + * Attempts to create a {@link CardDeclarable} from {@link source}. + * + * @param source The code block including the three ticks at the beginning and end. + * @param onParseError + * @param incompleteCallback Invoked if content of {@link source} is recognized but is missing required properties. + * @returns `null` if {@link source} is not recognized or it contains invalid YAML. + */ + private static createExplicitDeclaration( + source: string, + onParseError?: YamlParseErrorCallback, + incompleteCallback?: (incomplete: DefaultableCardDeclarable, range: OffsetRange) => void) { + + const info = DeclarationParser.parseAndCheckCodeBlock(source); + if (info === null) + return null; + + const obj = DeclarationCodec.tryFromYaml(info.content, onParseError); + if (obj === null) + return null; + + const declaration = DeclarationParser.tryCreateDeclaration(obj); + + if (declaration === null && ExplicitDeclarationAssistant.Defaultable.is(obj) && incompleteCallback) + incompleteCallback(obj, info.location); + + return declaration; + } + + protected static declarationFromFrontmatter(obj: Record, incompleteCallback?: (incomplete: DefaultableCardDeclarable, range: OffsetRange) => void) { + const declaration = DeclarationParser.tryCreateDeclaration(obj); + + if (declaration === null && ExplicitDeclarationAssistant.Defaultable.is(obj) && incompleteCallback) { + // This position should really be the position in the front matter YAML where the declaration is. + // But, since this is the frontmatter, there's no need slice strings as editing is done with `obsidian` `FileManager.processFrontMatter`. + incompleteCallback(obj, { start: 0, end: 0 }); + } + + return declaration; + } + + /** + * Attempts to create a {@link CommandDeclarationParsable} from {@link source}. + * + * @param source The raw code block text string of the command declaration. + * @param onParseError The formatting of {@link source} invalid YAML. + * @param onInvalidType + * @returns `null` if {@link source} is not recognized. + */ + private static createCommandDeclarationParser( + source: string, + onParseError?: YamlParseErrorCallback, + onInvalidType?: (command: CommandableDeclarable, range: OffsetRange) => void): CommandDeclarationParsable | null { + + const info = DeclarationParser.parseAndCheckCodeBlock(source); + if (!info) + return null; + + const obj = DeclarationCodec.tryFromYaml(info.content, onParseError); + const commandable = obj !== null && CommandableAssistant.is(obj) ? obj : null; + + let parser: CommandDeclarationParsable | null = null; + if (commandable !== null) { + + parser = ParserRegistry.tryCreate(commandable); + + if (parser === null && onInvalidType) + onInvalidType(commandable, info.location); + } + + return parser; + } + + /** + * Checks if this a code block with one of the expected languages; if so, parses it. + * @param source The code block including the three ticks at the beginning and end. + * @returns `null` is {@link source} is not a code block or if the block's language is unexpected. + */ + private static parseAndCheckCodeBlock(source: string) { + const info = FileParser.parseCodeBlock(source); + + if (info !== null && !DeclarationConstants.CodeBlock.isSupportedLanguage(info.language)) + return null; + + return info; + } + + /** @returns `null` if the {@link obj} is not a valid {@link CardDeclarable} */ + private static tryCreateDeclaration(obj: Record) { + return ExplicitDeclarationAssistant.is(obj) ? ExplicitDeclarationAssistant.createWithUniqueScope(obj) : null; + } + /** * Finds the {@link SectionRange|range} that is associated and defined by {@link section} as its boundries. * diff --git a/src/declarations/ExplicitDeclaration.ts b/src/declarations/ExplicitDeclaration.ts new file mode 100644 index 0000000..f41bb3e --- /dev/null +++ b/src/declarations/ExplicitDeclaration.ts @@ -0,0 +1,190 @@ +import { UniqueID } from "#/data/UniqueID"; +import { CardDeclaration } from "#/declarations/CardDeclaration"; +import { CollectionableAssistant, DeckableDeclarable } from "#/declarations/Collectionable"; +import { Declarable, DeclarableAssistant, DeclarableProperty, OptionalNullableStringDeclarableProperty, StringDeclarableProperty } from "#/declarations/Declarable"; +import { LocalStrictKeys, Obj } from "#/utils/ts"; + +export const IDScope = { + Unique: "unique", + Note: "note", +} as const; + +export type IDScope = typeof IDScope[keyof typeof IDScope]; + +export type DeclarationSide = "front" | "back"; + +/** +* A {@link DefaultableCardDeclarable} where all default values are set. +*/ +export interface CardDeclarable extends DefaultableCardDeclarable { + id: StringDeclarableProperty; +} + +/** + * The minimum required propertes that need to be specified before + * default values and/or generated values can be applied + * to turn it into a {@link CardDeclarable}. + * + * See also: {@link CardDeclarationAssistant.conformsToDefaultable}. + */ +export interface DefaultableCardDeclarable extends PageDeclarable, DeckableDeclarable { + id: OptionalNullableStringDeclarableProperty; +} + +/** @abstract */ +export interface PageDeclarable extends MaybePageDeclarable { + side: DeclarationSide; +} + +/** @sealed */ +export interface MaybePageDeclarable extends Declarable { + side: DeclarableProperty; +} + +type DefaultablePropertyNames = LocalStrictKeys; +type PagePropertyNames = LocalStrictKeys; + +export class ExplicitDeclarationAssistant extends CollectionableAssistant { + + /** @returns `null` if the {@link declaration} is invalid */ + public static createWithUniqueScope(declaration: CardDeclarable) { + if (!ExplicitDeclarationAssistant.isValid(declaration)) + return null; + return new CardDeclaration(declaration.id, declaration.side, IDScope.Unique, declaration.deckID, false); + } + + /** @returns `true` if {@link value} is a valid {@link DefaultableCardDeclarable} and conforms to {@link CardDeclarable}. */ + public static canComplete(value: DefaultableCardDeclarable): value is CardDeclarable { + if (!ExplicitDeclarationAssistant.Defaultable.isValid(value)) + return false; + + // Completing the values of the top-level interface is the same as making it valid. + // So if it conforms to the top-level interface it is ready. + // Any additional non-completable properties should be added to sub interfaces. + if (!ExplicitDeclarationAssistant.is(value)) + return false; + + // value is top-level interface + // - Type of `id` is narrowed. + return value.id === ExplicitDeclarationAssistant.PropertyValue.AWAITING_VALUE; + } + + /** + * Makes a {@link DefaultableCardDeclarable} valid by assigning default and generated values to non-required properties. + * May use if already checked with {@link canComplete}. + * @param decl + * @param preventIDs See {@link UniqueID.generateID} + * @returns `null` if {@link canComplete} returns `false`. If {@link declaration} is already a valid {@link CardDeclarable}, returns the same unmodified {@link declaration}. Otherwise, returns a valid {@link CardDeclarable}. + * @throws If {@link canComplete} returns `false` or {@link declaration} is already complete/valid. + */ + public static completeOrThrow(declaration: DefaultableCardDeclarable, preventIDs?: Set) { + + if (!ExplicitDeclarationAssistant.canComplete(declaration)) + throw new Error("Could not complete declaration."); + + if (ExplicitDeclarationAssistant.isValid(declaration)) + throw new Error("Declaration already complete."); + + return { + ...declaration, + id: UniqueID.generateID(preventIDs) + } satisfies CardDeclarable; + } + + public static override isValid(declarable: CardDeclarable) { + if (!ExplicitDeclarationAssistant.Defaultable.isValid(declarable)) + return false; + + if (!UniqueID.isValid(declarable.id)) + return false; + + return true; + } + + public static override is(value: unknown): value is CardDeclarable { + if (!ExplicitDeclarationAssistant.Defaultable.is(value)) + return false; + + if (ExplicitDeclarationAssistant.PropertyEq.optionalOrNull(Obj.getKey(value, "id"))) + Obj.setKey(value, "id", ExplicitDeclarationAssistant.PropertyValue.AWAITING_VALUE); + + return true + } + + public static readonly Defaultable = { + + is(value: unknown): value is DefaultableCardDeclarable { + if (!CollectionableAssistant.is(value)) + return false; + + if (!Obj.hasKey(value, "side")) + return false; + if (!ExplicitDeclarationAssistant.PropertyType.isStr(Obj.getKey(value, "side"))) + return false; + + // Make sure optional properties are added to the object. + if (!Obj.hasKey(value, "id")) + Obj.setKey(value, "id", ExplicitDeclarationAssistant.PropertyValue.OPTIONAL_NOT_ADDED); + + if (!ExplicitDeclarationAssistant.PropertyType.isOptionalNullableString(Obj.getKey(value, "id"))) + return false; + + return true; + }, + + isValid(declarable: DefaultableCardDeclarable) { + if (!CollectionableAssistant.isValid(declarable)) + return false; + + if (!ExplicitDeclarationAssistant.isSideValid(declarable.side)) + return false; + + if (!ExplicitDeclarationAssistant.PropertyEq.optionalNullOrString(declarable.id)) + return false; + + return true; + } + }; + + public static readonly MaybePage = { + + is(value: unknown): value is MaybePageDeclarable { + if (!CollectionableAssistant.is(value)) + return false; + + if (!Obj.hasKey(value, "side")) + return false; + + return true; + }, + + isValid(declarable: MaybePageDeclarable): boolean { + if (!DeclarableAssistant.isValid(declarable)) + return false; + + if (!ExplicitDeclarationAssistant.PropertyEq.any(declarable.side)) + return false; + + return true; + } + }; + + public static isFrontSide(decl: DefaultableCardDeclarable, throwIfNotValid = false) { + if (throwIfNotValid && !this.isSideValid(decl.side)) + throw new Error(`Side is not valid: ${decl.side}`); + return ExplicitDeclarationAssistant.frontSideValues.includes(decl.side); + } + + /** + * @returns `true` if {@link side} is one of the allowed values for {@link DefaultableCardDeclarable.side} + */ + private static isSideValid(side: string) { + return [ + ...this.frontSideValues, + ...this.backSideValues + ].includes(side.trim().toLowerCase()); + } + + private static readonly frontSideValues = ["f", "front"]; + private static readonly backSideValues = ["b", "back"]; +} diff --git a/src/declarations/ParserRegistry.ts b/src/declarations/ParserRegistry.ts new file mode 100644 index 0000000..703bede --- /dev/null +++ b/src/declarations/ParserRegistry.ts @@ -0,0 +1,19 @@ +import { CommandableDeclarable } from "#/declarations/Commandable"; +import { CommandDeclarationParsable, CommandDeclarationParser } from "#/declarations/CommandDeclarationParser"; +import { Commands } from "#/declarations/CommandNames"; +import { AlternateHeadingsParser } from "#/declarations/commands/AlternateHeadings"; +import { HeadingAndDelimiterParser } from "#/declarations/commands/HeadingAndDelimiter"; +import { HeadingIsFrontParser } from "#/declarations/commands/HeadingIsFront"; + +const REGISTRY = [ + CommandDeclarationParser.Factory.createEntry(Commands.Name.AlternateHeadings, AlternateHeadingsParser), + CommandDeclarationParser.Factory.createEntry(Commands.Name.HeadingAndDelimiter, HeadingAndDelimiterParser), + CommandDeclarationParser.Factory.createEntry(Commands.Name.HeadingIsFront, HeadingIsFrontParser), +]; + +export const ParserRegistry = { + tryCreate(commandable: CommandableDeclarable): CommandDeclarationParsable | null { + const parsable = REGISTRY.find(e => e.names.includes(commandable.name)); + return parsable !== undefined ? parsable.create(commandable) : null; + } +}; diff --git a/src/declarations/commands/AlternateHeadings.ts b/src/declarations/commands/AlternateHeadings.ts index 54f0ed3..01ee797 100644 --- a/src/declarations/commands/AlternateHeadings.ts +++ b/src/declarations/commands/AlternateHeadings.ts @@ -1,24 +1,38 @@ -import { CommandableDeclarable } from "declarations/CommandDeclaration"; -import { CommandDeclarationParsable, CommandDeclarationParser } from "declarations/CommandDeclarationParser"; -import { HeadingsCommandableAssistant, HeadingsCommandableDeclarable, HeadingsDeclarationParser } from "declarations/commands/HeadingsCommandable"; -import { CacheItem, HeadingCache } from "obsidian"; +import { CommandableDeclarable } from "#/declarations/Commandable"; +import { CommandDeclarationParsable } from "#/declarations/CommandDeclarationParser"; +import { Commands, CommandName } from "#/declarations/CommandNames"; +import { IDScope } from "#/declarations/ExplicitDeclaration"; +import { HeadingsCommandableAssistant, HeadingsCommandableDeclarable, HeadingsDeclarationParser } from "#/declarations/commands/HeadingsCommandable"; -export interface AlternateHeadingsDeclarable extends HeadingsCommandableDeclarable { } +import { FileParser } from "#/utils/obs/FileParser"; +import { CacheItem } from "obsidian"; +export interface AlternateHeadingsDeclarable extends HeadingsCommandableDeclarable { // eslint-disable-line @typescript-eslint/no-empty-object-type +} + +/** Helpers related to {@link AlternateHeadingsDeclarable}. */ export class AlternateHeadingsAssistant extends HeadingsCommandableAssistant { - public static tryCreateParser(commandable: CommandableDeclarable): CommandDeclarationParsable | null { - return ( - AlternateHeadingsAssistant.conforms(commandable) && - AlternateHeadingsAssistant.isValid(commandable) - ) ? new AlternateHeadingsParser(commandable) : null; + + public static override is(value: unknown): value is AlternateHeadingsDeclarable { + if (!HeadingsCommandableAssistant.is(value)) + return false; + + return (Commands.Name.AlternateHeadings as readonly CommandName[]).includes(value.name); } } +const ThisAssistant = AlternateHeadingsAssistant; export class AlternateHeadingsParser extends HeadingsDeclarationParser { + public static tryCreate(declarable: CommandableDeclarable): CommandDeclarationParsable | null { + if (ThisAssistant.is(declarable) && ThisAssistant.isValid(declarable)) + return new this(declarable); + return null; + } + public parse(parentHeadingLevel: number, inBetweenDelimiter: CacheItem, index: number, delimiters: CacheItem[]) { - if (!AlternateHeadingsParser.isHeadingCache(inBetweenDelimiter)) + if (!FileParser.isHeadingCache(inBetweenDelimiter)) return; // Only interested in headings on the specified level @@ -26,10 +40,31 @@ export class AlternateHeadingsParser extends HeadingsDeclarationParser(commandable) && - HeadingAndDelimiterAssistant.isValid(commandable) - ) ? new HeadingAndDelimiterParser(commandable) : null; + public static override is(value: unknown): value is HeadingAndDelimiterDeclarable { + if (!HeadingsCommandableAssistant.is(value)) + return false; + + return (Commands.Name.HeadingAndDelimiter as readonly CommandName[]).includes(value.name); } - public static isValid(command: HeadingAndDelimiterDeclarable) { - return ( - super.isValid(command) && - this.isDelimiterValid(command) - ); + public static override isValid(declarable: HeadingAndDelimiterDeclarable) { + if (!HeadingsCommandableAssistant.isValid(declarable)) + return false; + + return ThisAssistant.isDelimiterValid(declarable); } private static isDelimiterValid(command: HeadingAndDelimiterDeclarable) { @@ -32,6 +35,7 @@ export class HeadingAndDelimiterAssistant extends HeadingsCommandableAssistant { switch (command.delimiter as string) { case "hr": setDefault(); + return true; case "horizontal rule": return true; } @@ -44,12 +48,17 @@ export class HeadingAndDelimiterAssistant extends HeadingsCommandableAssistant { return false; } } +const ThisAssistant = HeadingAndDelimiterAssistant; export class HeadingAndDelimiterParser extends CommandDeclarationParser { - /** - * If set, it means that the current iteration is the back side and that this is the expected level of the heading that marks the end of the back side. - */ + public static tryCreate(declarable: CommandableDeclarable): CommandDeclarationParsable | null { + if (ThisAssistant.is(declarable) && ThisAssistant.isValid(declarable)) + return new this(declarable) + return null; + } + + /** If set, it means that the current iteration is the back side and that this is the expected level of the heading that marks the end of the back side. */ private lastFrontHeadingLevel: number | undefined; public parse(parentHeadingLevel: number, inBetweenDelimiter: CacheItem, index: number, delimiters: CacheItem[]) { @@ -63,17 +72,17 @@ export class HeadingAndDelimiterParser extends CommandDeclarationParser(commandable) && - HeadingIsFrontAssistant.isValid(commandable) - ) ? new HeadingIsFrontParser(commandable) : null; - } +export interface HeadingIsFrontDeclarable extends HeadingsCommandableDeclarable { // eslint-disable-line @typescript-eslint/no-empty-object-type } +/** Helpers related to {@link HeadingIsFrontDeclarable}. */ +export class HeadingIsFrontAssistant extends HeadingsCommandableAssistant { + + public static override is(value: unknown): value is HeadingIsFrontDeclarable { + if (!HeadingsCommandableAssistant.is(value)) + return false; + + return (Commands.Name.HeadingIsFront as readonly CommandName[]).includes(value.name); + } +} +const ThisAssistant = HeadingIsFrontAssistant; + export class HeadingIsFrontParser extends HeadingsDeclarationParser { + public static tryCreate(declarable: CommandableDeclarable): CommandDeclarationParsable | null { + if (ThisAssistant.is(declarable) && ThisAssistant.isValid(declarable)) + return new this(declarable); + return null; + } + public parse(parentHeadingLevel: number, inBetweenDelimiter: CacheItem, index: number, delimiters: CacheItem[]) { if (!HeadingIsFrontParser.isHeadingCache(inBetweenDelimiter)) return; @@ -24,7 +36,15 @@ export class HeadingIsFrontParser extends HeadingsDeclarationParser(command: CommandableDeclarable): command is T { - return Object.hasOwn(command, "level"); +type PropertyNames = LocalStrictKeys; + + +export abstract class HeadingsCommandableAssistant extends CommandableAssistant { + + public static override is(value: unknown): value is HeadingsCommandableDeclarable { + if (!This.Defaultable.is(value)) + return false; + + if (!This.PropertyType.isNum(Obj.getKey(value, "level"))) + return false; + + return true; } - /** - * @param command - * @returns `true` if the values of properties were valid or undefined, in which case default values were set. - */ - public static isValid(command: HeadingsCommandableDeclarable) { - return ( - this.isLevelValid(command) - ); + public static override isValid(declarable: HeadingsCommandableDeclarable) { + if (!This.Defaultable.isValid(declarable)) + return false; + + if (declarable.level < 1) + return false; + + return true; } - private static isLevelValid(command: HeadingsCommandableDeclarable) { - if (isNumber(command.level)) { - return command.level >= 1; - } + public static readonly Defaultable = { + + is(value: unknown): value is DefaultableHeadingsCommandableDeclarable { + if (!CommandableAssistant.is(value)) + return false; + + // Add optional properties with default values. + if (!Obj.hasKey(value, "level")) + Obj.setKey(value, "level", This.PropertyValue.OPTIONAL_NOT_ADDED); + + if (!This.PropertyType.isOptionalNullableNumber(Obj.getKey(value, "level"))) + return false; + + return true; + }, + + isValid(declarable: HeadingsCommandableDeclarable) { + if (!CommandableAssistant.isValid(declarable)) + return false; + + if (!This.PropertyEq.optionalNullOrNumber(declarable.level)) + return false; - // If `level` is not set, default to 1; if less than one it's invalid. - if (command.level === undefined || command.level === null) { - command.level = 1; return true; } - - return false; } } +const This = HeadingsCommandableAssistant; +/** + * @abstract + */ export abstract class HeadingsDeclarationParser extends CommandDeclarationParser { @@ -66,7 +98,7 @@ export abstract class HeadingsDeclarationParser= nextDelimiter.level) return nextDelimiter; } diff --git a/src/declarations/constants.ts b/src/declarations/constants.ts new file mode 100644 index 0000000..0247f73 --- /dev/null +++ b/src/declarations/constants.ts @@ -0,0 +1,17 @@ +const LANGUAGE = "comethrough"; +const LANGUAGE_SHORT = "ct"; + +export const SUPPORTED_CODEBLOCK_LANGUAGES = [LANGUAGE, LANGUAGE_SHORT]; + +export const DeclarationConstants = { + + CodeBlock: { + LANGUAGES: SUPPORTED_CODEBLOCK_LANGUAGES, + isSupportedLanguage: (language: string) => SUPPORTED_CODEBLOCK_LANGUAGES.includes(language.toLowerCase()), + } as const, + + Frontmatter: { + KEYS: [LANGUAGE, LANGUAGE_SHORT, "come through"] + } as const, + +} as const; diff --git a/src/env.ts b/src/env.ts index cf46552..2c4eab5 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,14 +1,14 @@ import { Platform } from "obsidian"; -const isProduction = process.env.NODE_ENV === "production"; +const isProduction = process.env["NODE_ENV"] === "production"; const isDev = !isProduction; -const noop = () => {}; -const noopLogger = { - debug: noop, - log: noop, - info: noop, - warn: noop, +const noopLogger: Pick = { + debug: () => {}, + log: () => { }, + info: () => {}, + warn: () => {}, + error: () => {}, }; const devLogger = isDev ? console : noopLogger; @@ -22,7 +22,6 @@ export const Env = { /** Debug/Dev context */ dev: isDev ? DevContext : undefined, isDev: isDev, - noop: noop, /** Always logs */ error: console.error, @@ -53,11 +52,12 @@ export const Env = { e: console.error, /** Debug log for content processors. See {@link ContentRendererProcessor}. */ - proc: devLogger.info, // devLogger.info : noopLogger.debug, + proc: noopLogger.info, /** Debug log for views. */ - view: devLogger.info, // devLogger.info : noopLogger.debug, + view: noopLogger.info, + data: noopLogger.info, }, perf: { @@ -78,12 +78,13 @@ export const Env = { }, /** If running in mobile app that has very limited screen space. */ - isPhone: Platform.isPhone, + get isPhone(): boolean { + return Platform.isPhone; + }, + /** If running in mobile app that has sufficiently large screen space. */ - isTablet: Platform.isTablet, + get isTablet(): boolean { + return Platform.isTablet; + }, - - str: { - EMPTY: "", - } as const } as const; diff --git a/src/main.ts b/src/main.ts index 1813ae0..c329b2d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,23 +1,24 @@ -import { OpenView, OpenViewCommand } from "commands/openView"; -import { ReviewViewCommand } from "commands/reviewView"; -import { DataStore, DataStoreRoot } from "data/DataStore"; -import { SyncManager } from "data/SyncManager"; -import { UniqueID } from "data/UniqueID"; -import { DeclarationManager } from "declarations/DeclarationManager"; -import { DeclarationParser } from "declarations/DeclarationParser"; -import { Env } from "env"; -import t from "Localization"; -import { ConfirmationModal } from "modals/ConfirmationModal"; -import { Editor, Keymap, MarkdownPostProcessorContext, MarkdownView, Plugin, TFile } from "obsidian"; -import { Scheduler } from "scheduling/Scheduler"; -import { FsrsSchedulerConfig } from "scheduling/types"; -import { PluginSettings, SettingsChangedInfo, SettingsManager } from "Settings"; -import { Icon } from "ui/constants"; -import { SettingTab } from "ui/SettingTab"; -import { UIAssistant } from "ui/UIAssistant"; -import { DecksView } from "views/DecksView"; -import { DefinedContentView } from "views/DefinedContentView"; -import { ReviewView } from "views/review/ReviewView"; +import { OpenView, OpenViewCommand } from "#/commands/openView"; +import { ReviewViewCommand } from "#/commands/reviewView"; +import { DataStore, DataStoreRoot } from "#/data/DataStore"; +import { SyncManager } from "#/data/SyncManager"; +import { DeclarationManager } from "#/declarations/DeclarationManager"; +import { DeclarationParser } from "#/declarations/DeclarationParser"; +import { Env } from "#/env"; +import t from "#/Localization"; +import { ConfirmationModal } from "#/modals/ConfirmationModal"; +import { Scheduler } from "#/scheduling/Scheduler"; +import { FsrsSchedulerConfig } from "#/scheduling/types"; +import { PluginSettings, SettingsChangedInfo, SettingsManager } from "#/Settings"; +import { Icon } from "#/ui/constants"; +import { SettingTab } from "#/ui/SettingTab"; +import { UIAssistant } from "#/ui/UIAssistant"; +import { DomState } from "#/utils/obs/DomState"; +import { DecksView } from "#/views/DecksView"; +import { DefinedContentView } from "#/views/DefinedContentView"; +import { ReviewView } from "#/views/review/ReviewView"; +import { EditorCommand } from "commands/editor"; +import { Keymap, MarkdownPostProcessorContext, Plugin, TFile } from "obsidian"; interface PluginData { settings: PluginSettings; @@ -25,16 +26,16 @@ interface PluginData { } export default class ComeThroughPlugin extends Plugin { - private dataStore: DataStore; - private scheduler: Scheduler; - private settingsManager: SettingsManager; - private syncManager: SyncManager; - private ui: UIAssistant; + private dataStore!: DataStore; + private scheduler!: Scheduler; + private settingsManager!: SettingsManager; + private syncManager!: SyncManager; + private ui!: UIAssistant; - /** Must referene the latest data before {@link savePluginData} is called. Only needed to hold references in order to pass to {@link Plugin.saveData}, see {@link savePluginData}. */ - private latestPluginDataRef: PluginData; + /** Must reference the latest data before {@link savePluginData} is called. Only needed to hold references in order to pass to {@link Plugin.saveData}, see {@link savePluginData}. */ + private latestPluginDataRef!: PluginData; - public async onload() { + public override async onload() { this.latestPluginDataRef = await ComeThroughPlugin.loadPluginData(this); @@ -44,8 +45,9 @@ export default class ComeThroughPlugin extends Plugin { this.latestPluginDataRef.settings = settings; await this.savePluginData(); }, - this.onSettingsSaved.bind(this) + this.onSettingsSaved ); + DomState.init(this, document); this.dataStore = new DataStore(this.latestPluginDataRef.data, this.latestPluginDataRef.settings.removedItemsPurgeThreshold, async (data) => { this.latestPluginDataRef.data = data; @@ -101,22 +103,19 @@ export default class ComeThroughPlugin extends Plugin { this.addCommand(ReviewViewCommand.toggleInlineInfo(this.app)); this.addCommand(ReviewViewCommand.navigateToSourceFile(this.app)); - this.addCommand({ - id: 'generate-id-cursor', - name: t.commands.generateId.name, - editorCallback: (editor: Editor, _view: MarkdownView) => { - editor.replaceRange(UniqueID.generateID(), editor.getCursor()) - } - }); + this.addCommand(EditorCommand.generateId()); + this.addCommand(EditorCommand.insertReviewUnit(false)); + this.addCommand(EditorCommand.insertReviewUnit(true)); } - public onunload() { - if (this.confirmationModal) + public override onunload() { + DomState.deinit(); + if (this.confirmationModal !== null) this.confirmationModal.forceClose(); } /** This is triggered when the data file is modified by an external source, such as a sync service. */ - public async onExternalSettingsChange() { + public override async onExternalSettingsChange() { Env.log.d("Plugin:onExternalSettingsChange"); const overwrittenDataOnDisk = await ComeThroughPlugin.loadPluginData(this); @@ -152,9 +151,9 @@ export default class ComeThroughPlugin extends Plugin { if (info.activeChanged || info.collectionsChanged) { // Prevent multiple modals opening (as external changes might occur while the modal is showing). - if (!this.confirmationModal) { + if (this.confirmationModal === null) { this.confirmationModal = new ConfirmationModal(this.app); - this.confirmationModal.onClosed = () => this.confirmationModal = undefined; + this.confirmationModal.onClosed = () => this.confirmationModal = null; this.confirmationModal.open(); } @@ -172,7 +171,7 @@ export default class ComeThroughPlugin extends Plugin { ); }); } - private confirmationModal?: ConfirmationModal; + private confirmationModal: ConfirmationModal | null = null; // public onUserEnable(): void {} @@ -182,10 +181,10 @@ export default class ComeThroughPlugin extends Plugin { */ private registerEvents() { - this.registerEvent(this.app.workspace.on("file-open", this.syncManager.open.bind(this.syncManager))); - this.registerEvent(this.app.metadataCache.on("changed", this.syncManager.changed.bind(this.syncManager))); - this.registerEvent(this.app.vault.on("delete", this.syncManager.delete.bind(this.syncManager))); - this.registerEvent(this.app.vault.on("rename", this.syncManager.rename.bind(this.syncManager))); + this.registerEvent(this.app.workspace.on("file-open", this.syncManager.open)); + this.registerEvent(this.app.metadataCache.on("changed", this.syncManager.changed)); + this.registerEvent(this.app.vault.on("delete", this.syncManager.delete)); + this.registerEvent(this.app.vault.on("rename", this.syncManager.rename)); this.registerEvent(this.app.workspace.on("file-menu", (menu, file, source, _leaf) => { if (!(file instanceof TFile)) @@ -236,17 +235,18 @@ export default class ComeThroughPlugin extends Plugin { await this.saveData(this.latestPluginDataRef); } - private onSettingsSaved(changedInfo?: SettingsChangedInfo) { + private onSettingsSaved = (changedInfo?: SettingsChangedInfo) => { switch (changedInfo) { case "schedulerConfig": this.scheduler.reconfigure(this.createSchedulerConfig()); break; + case undefined: + break; } - } + }; private createSchedulerConfig() { const config = this.settingsManager.defaultScheduler.config; - Env.assert(config); return { enableFuzz: config.enableFuzz, } satisfies FsrsSchedulerConfig; diff --git a/src/modals/BaseModal.ts b/src/modals/BaseModal.ts index cdabab4..a19a294 100644 --- a/src/modals/BaseModal.ts +++ b/src/modals/BaseModal.ts @@ -1,6 +1,7 @@ -import { Env } from "env"; +import { CssClass } from "#/constants"; +import { Env } from "#/env"; +import { CssClass as ObsCssClass } from "#/utils/obs/constants"; import { App, Modal, Setting } from "obsidian"; -import { CssClass } from "utils/obs/constants"; /** * - Adds a plugin specific class to all modals for styling purposes. @@ -29,15 +30,15 @@ export class BaseModal extends Modal { // "Show debug info" modal has this class which makes the height 100% on mobile, i.e. fullscreen. // If modal contains a lot of content and therefore expands in height, it will never fill the entire screen. So in those cases, it's better to force fullscreen. if (options?.fullscreenOnLimitedScreenSpace && Env.isPhone) - this.modalEl.addClass(CssClass.Modal.LG); + this.modalEl.addClass(ObsCssClass.Modal.LG); - this.contentEl.addClass("come-through-modal-content"); + this.contentEl.addClass(CssClass.Modal.CONTENT); } protected createSetting(name: string, o?: { styleControlElAsDesc?: boolean, isHeading?: boolean }) { const s = new Setting(this.contentEl).setName(name) if (o?.styleControlElAsDesc) - s.controlEl.addClass(CssClass.Setting.Item.DESC); + s.controlEl.addClass(ObsCssClass.Setting.Item.DESC); if (o?.isHeading) s.setHeading(); return s; @@ -46,9 +47,9 @@ export class BaseModal extends Modal { /** Using this you get better bottom margins on mobile than adding buttons on a `Setting`. */ protected getButtonContainer() { // Try to find existing button container, or create one if it doesn't exist - let buttonContainer = this.modalEl.querySelector("." + CssClass.Modal.BUTTON_CONTAINER); + let buttonContainer = this.modalEl.querySelector("." + ObsCssClass.Modal.BUTTON_CONTAINER); if (buttonContainer === null) - buttonContainer = this.contentEl.createDiv(CssClass.Modal.BUTTON_CONTAINER); + buttonContainer = this.contentEl.createDiv(ObsCssClass.Modal.BUTTON_CONTAINER); return buttonContainer; } @@ -58,7 +59,7 @@ export class BaseModal extends Modal { this.getButtonContainer().createEl("button", { text: "Done", - cls: CssClass.Modal.CANCEL, + cls: ObsCssClass.Modal.CANCEL, }, (button => { button.addEventListener("click", () => this.close()); })) diff --git a/src/modals/DeckModal.ts b/src/modals/DeckModal.ts index aeefddd..7c65752 100644 --- a/src/modals/DeckModal.ts +++ b/src/modals/DeckModal.ts @@ -1,116 +1,132 @@ -import { DataStore, DeckEditor, DeckIDDataTuple } from "data/DataStore"; -import { DeckID } from "data/FullID"; -import { BaseModal } from "modals/BaseModal"; +import { DataStore, DeckEditor, DeckIDDataTuple } from "#/data/DataStore"; +import { DeckID } from "#/data/FullID"; +import { BaseModal } from "#/modals/BaseModal"; +import { HtmlTag } from "#/utils/dom/constants"; +import { Str } from "#/utils/ts"; import { App, ButtonComponent, Setting } from "obsidian"; -import { UIAssistant } from "ui/UIAssistant"; +export type OnSubmitCallback = (deck: DeckIDDataTuple) => void; export class DeckModal extends BaseModal { - private nameOfDeck: string = ""; - private parentDeckID: DeckID | null; - private createButton: ButtonComponent; - private result?: DeckIDDataTuple; + private nameOfDeck: string = Str.EMPTY; + private parentDeckID: DeckID | null = null; + private createButton!: ButtonComponent; + private result: DeckIDDataTuple | undefined; - public static add(app: App, - data: DataStore, - onAdded: (deck: DeckIDDataTuple) => void) { - new DeckModal(app, data, onAdded).open(); - } + private readonly data: DataStore; + private readonly onSubmit: OnSubmitCallback | undefined; + private readonly idToEdit: DeckID | undefined; - constructor( - readonly app: App, - private readonly data: DataStore, - private readonly onSubmit: (deck: DeckIDDataTuple) => void, - private readonly idToEdit?: DeckID) { - super(app, { fullscreenOnLimitedScreenSpace: true }); + public static add( + app: App, + data: DataStore, + onAdded?: OnSubmitCallback) { + new DeckModal(app, data, onAdded).open(); + } - if (idToEdit) { - const deckToEdit = data.getDeck(idToEdit, true)!; - this.nameOfDeck = deckToEdit.n; - this.parentDeckID = DeckEditor.parent(deckToEdit); - this.setTitle("Edit deck"); - } - else { - this.setTitle("Create a new deck"); - } + public static edit( + app: App, + data: DataStore, + id: DeckID, + onSubmit?: OnSubmitCallback) { + new DeckModal(app, data, onSubmit, id).open(); + } - new Setting(this.contentEl) - .setName("Name") - .addText((component) => { - component.setValue(this.nameOfDeck); - component.onChange((text) => { - this.nameOfDeck = text; - this.createButton.setDisabled(this.nameOfDeck.trim().length == 0); - }); - }); + private constructor( + app: App, + data: DataStore, + onSubmit?: OnSubmitCallback, + idToEdit?: DeckID + ) { + super(app, { fullscreenOnLimitedScreenSpace: true }); + this.data = data; + this.onSubmit = onSubmit; + this.idToEdit = idToEdit; - new Setting(this.contentEl) - .setName("Parent deck") - .setDesc(idToEdit ? "" : "To make this deck a subdeck, choose a parent deck.") - .addDropdown((component) => { + if (idToEdit) { + const deckToEdit = data.getDeck(idToEdit, true)!; + this.nameOfDeck = deckToEdit.n; + this.parentDeckID = DeckEditor.parent(deckToEdit); + this.setTitle("Edit deck"); + } + else { + this.setTitle("Create a new deck"); + } - component.addOption(UIAssistant.DECK_ID_NONE, "None"); - for (const deck of this.data.getAllDecks().filter(d => d.id !== this.idToEdit)) - component.addOption(deck.id, deck.data.n); - component.setValue(this.parentDeckID ?? UIAssistant.DECK_ID_NONE); + new Setting(this.contentEl) + .setName("Name") + .addText((component) => { + component.setValue(this.nameOfDeck); + component.onChange((text) => { + this.nameOfDeck = text; + this.createButton.setDisabled(this.nameOfDeck.trim().length == 0); + }); + }); - component.onChange((value) => { - this.parentDeckID = value === UIAssistant.DECK_ID_NONE ? null : value; - }); - }); + new Setting(this.contentEl) + .setName("Parent deck") + .setDesc(idToEdit ? "" : "To make this deck a subdeck, choose a parent deck.") + .addDropdown((component) => { - new Setting(this.contentEl) - .addButton((button) => { - this.createButton = button; - button.setDisabled(this.nameOfDeck.trim().length == 0); - button.setCta() - button.setButtonText(this.idToEdit ? "Save" : "Create new deck"); - button.onClick(async () => { - button.setDisabled(true); - await this.submit(); - this.close(); - }); - }) - .addButton((button) => { - button.setButtonText("Cancel"); - button.onClick(() => { - this.close(); - }); - }); - } + component.addOption(HtmlTag.SELECT.OPTION.Values.NONE, "None"); + for (const deck of this.data.getAllDecks().filter(d => d.id !== this.idToEdit)) + component.addOption(deck.id, deck.data.n); + component.setValue(this.parentDeckID ?? HtmlTag.SELECT.OPTION.Values.NONE); - // onOpen(): void { - // super.onOpen(); - // } + component.onChange((value) => { + this.parentDeckID = HtmlTag.SELECT.OPTION.isNone(value) ? null : value; + }); + }); - onClose(): void { - super.onClose(); + new Setting(this.contentEl) + .addButton((button) => { + this.createButton = button; + button.setDisabled(this.nameOfDeck.trim().length == 0); + button.setCta() + button.setButtonText(Str.isNonEmpty(this.idToEdit) ? "Save" : "Create new deck"); + button.onClick(async () => { + button.setDisabled(true); + await this.submit(); + this.close(); + }); + }) + .addButton((button) => { + button.setButtonText("Cancel"); + button.onClick(() => { + this.close(); + }); + }); + } - if (this.result) { - const result = this.result; - setTimeout(() => this.onSubmit(result), 1); - } - } + public override onClose(): void { + super.onClose(); - private async submit() { - const cb = (editor: DeckEditor) => { - editor.setName(this.nameOfDeck); - editor.setParent(this.parentDeckID); - return true; - }; + if (this.onSubmit !== undefined && this.result !== undefined) { + const result = this.result; + const onSubmit = this.onSubmit; + setTimeout(() => onSubmit(result), 1); + } + } - let deckID: DeckID; - if (this.idToEdit) { - deckID = this.idToEdit; - this.data.editDeck(this.idToEdit, cb); - } - else { - deckID = this.data.createDeck(cb).id; - } - await this.data.save(); + private async submit() { + const cb = (editor: DeckEditor) => { + editor.setName(this.nameOfDeck); + editor.setParent(this.parentDeckID); + return true; + }; - const deck = this.data.getDeck(deckID, true); - this.result = { id: deckID, data: deck! }; - } + let deckID: DeckID; + if (this.idToEdit !== undefined) { + deckID = this.idToEdit; + const edited = this.data.editDeck(this.idToEdit, cb); + if (edited !== null) + this.result = { id: deckID, data: edited }; + } + else { + const created = this.data.createDeck(cb); + this.result = { id: created.id, data: created.data }; + } + await this.data.save(); + } } diff --git a/src/modals/ReviewItemInfoModal.ts b/src/modals/ReviewItemInfoModal.ts index 3d0a181..fa33e00 100644 --- a/src/modals/ReviewItemInfoModal.ts +++ b/src/modals/ReviewItemInfoModal.ts @@ -107,7 +107,7 @@ export class ReviewItemInfoModal extends BaseModal { .setDesc("The number of review units left in the current review queue."); new TableSectionCreator(el.controlEl.createEl("table")).addBody(rowCreator => { - entries.forEach(([date, value], index) => { + entries.forEach(([date, value], _index) => { let timeString: string; const hours = date.getHours(); const minutes = date.getMinutes(); @@ -169,7 +169,7 @@ export class ReviewItemInfoModal extends BaseModal { .setDesc("The number of review units left in the current review queue below the given retrievability."); new TableSectionCreator(el.controlEl.createEl("table")).addBody(rowCreator => { - entries.forEach(([threshold, value], index) => { + entries.forEach(([threshold, value], _index) => { rowCreator.add((col) => { col.add({ text: `Below ${threshold * 100}%:` }); col.add({ text: `${Str.NON_BREAKING_SPACE}${value}` }); diff --git a/src/modals/SelectDeckModal.ts b/src/modals/SelectDeckModal.ts index 4aa432e..6417962 100644 --- a/src/modals/SelectDeckModal.ts +++ b/src/modals/SelectDeckModal.ts @@ -1,21 +1,43 @@ -import { DataStore, DeckIDDataTuple } from "data/DataStore"; +import { DataStore, DeckIDDataTuple } from "#/data/DataStore"; +import { HtmlTag } from "#/utils/dom/constants"; import { App, SuggestModal } from "obsidian"; -import { UIAssistant } from "ui/UIAssistant"; +const ALL_DECKS = { + id: HtmlTag.SELECT.OPTION.Values.NONE, + data: { + n: "All Decks", + p: [] + } +}; + +function isAllDecks(deck: DeckIDDataTuple): boolean { + return HtmlTag.SELECT.OPTION.isNone(deck.id); +} + +/** + * A `null` value indicates that the user selected "All Decks". + */ +export type OnChooseCallback = (deck: DeckIDDataTuple | null, evt: MouseEvent | KeyboardEvent) => void; export class SelectDeckModal extends SuggestModal { - private decks: DeckIDDataTuple[]; + private decks: DeckIDDataTuple[]; + private readonly onChoose: OnChooseCallback | undefined; + + /** + * @param decks If you already have all decks, or if you want to display a subset. + */ constructor( app: App, private readonly data: DataStore, decks?: DeckIDDataTuple[], - private readonly onChoose?: (deck: DeckIDDataTuple, evt: MouseEvent | KeyboardEvent) => void) { + onChoose?: OnChooseCallback) { super(app); this.setPlaceholder("Select deck"); - this.decks = decks ?? this.data.getAllDecks(); + this.decks = [...[ALL_DECKS], ...decks ?? this.data.getAllDecks()]; + this.onChoose = onChoose; } getSuggestions(query: string): DeckIDDataTuple[] | Promise { @@ -23,13 +45,13 @@ export class SelectDeckModal extends SuggestModal { } renderSuggestion(value: DeckIDDataTuple, el: HTMLElement): void { - const numberOfCards = this.data.getAllCardsForDeck(value.id === UIAssistant.DECK_ID_NONE ? undefined : value.id).length; + const numberOfCards = this.data.getAllCardsForDeck(isAllDecks(value) ? undefined : value.id).length; el.createEl('div', { text: `${value.data.n}` }).createEl('small', { text: ` (${numberOfCards})` }); el.createEl('small', { text: value.data.p.length > 0 ? this.descendants(value) : "" }); } onChooseSuggestion(item: DeckIDDataTuple, evt: MouseEvent | KeyboardEvent): void { - this.onChoose?.(item, evt); + this.onChoose?.(isAllDecks(item) ? null : item, evt); } private descendants(deck: DeckIDDataTuple): string { diff --git a/src/renderings/RendererRegistry.ts b/src/renderings/RendererRegistry.ts new file mode 100644 index 0000000..685acf9 --- /dev/null +++ b/src/renderings/RendererRegistry.ts @@ -0,0 +1,32 @@ +import { CommandableAssistant } from "#/declarations/Commandable"; +import { Commands } from "#/declarations/CommandNames"; +import { ExplicitDeclarationAssistant } from "#/declarations/ExplicitDeclaration"; +import { AlternateHeadingsRenderer } from "#/renderings/declarations/AlternateHeadingsRenderer"; +import { DeclarationRenderable, DeclarationRenderer, FactoryRegistryEntry } from "#/renderings/declarations/DeclarationRenderable"; +import { HeadingAndDelimiterRenderer } from "#/renderings/declarations/HeadingAndDelimiterRenderer"; +import { HeadingIsFrontRenderer } from "#/renderings/declarations/HeadingIsFrontRenderer"; +import { PageDeclarationRenderer } from "#/renderings/declarations/PageDeclarationRenderer"; + + +const COMMAND_RENDERERS: FactoryRegistryEntry[] = [ + DeclarationRenderer.Factory.createEntry(Commands.Name.AlternateHeadings, AlternateHeadingsRenderer), + DeclarationRenderer.Factory.createEntry(Commands.Name.HeadingAndDelimiter, HeadingAndDelimiterRenderer), + DeclarationRenderer.Factory.createEntry(Commands.Name.HeadingIsFront, HeadingIsFrontRenderer), +]; + +export const RendererRegistry = { + tryCreate(declaration: unknown): DeclarationRenderable | null { + + let r: DeclarationRenderable | null = null; + + if (CommandableAssistant.is(declaration)) { + const entry = COMMAND_RENDERERS.find(r => r.names.includes(declaration.name)); + r = entry ? entry.create(declaration) : null + } + else if (ExplicitDeclarationAssistant.MaybePage.is(declaration)) { + r = new PageDeclarationRenderer(declaration); + } + + return r; + } +}; diff --git a/src/renderings/content/AudioProcessor.ts b/src/renderings/content/AudioProcessor.ts index 0d26a10..b9f1706 100644 --- a/src/renderings/content/AudioProcessor.ts +++ b/src/renderings/content/AudioProcessor.ts @@ -73,7 +73,7 @@ export class AudioProcessor extends ContentRendererProcessor implements ContentR } const match = this.registeredAudioItems.find(p => p.el === audioElThatStartedPlaying) ?? null; - Env.assert(match, "Expected to find the element.") + Env.assert(match !== null, "Expected to find the element.") this.currentlyPlaying = match; }); @@ -89,12 +89,12 @@ export class AudioProcessor extends ContentRendererProcessor implements ContentR /** * - Note: If paused because of {@link HtmlAttribute.MediaElement.Plugin.Data.LoopDelay} it is still considered playing. */ - private currentlyPlaying: { el: HTMLAudioElement, controller: TimeLoopController } | null; + private currentlyPlaying: { el: HTMLAudioElement, controller: TimeLoopController } | null = null; /** * @returns `null` if no source was set. */ - private static getPlayerInfo(param: PostProcessorParameter, el: HTMLAudioElement): AudioPlayerInfo | null { + private static getPlayerInfo(_param: PostProcessorParameter, el: HTMLAudioElement): AudioPlayerInfo | null { Env.log.proc(`getPlayerInfo`); const getSource = () => { @@ -113,7 +113,7 @@ export class AudioProcessor extends ContentRendererProcessor implements ContentR return "current"; case HtmlAttribute.MediaElement.Plugin.Data.SeekOnPause.Values.INITIAL: - default: + case null: return "init"; } }; @@ -465,7 +465,7 @@ class TimeLoopController extends Component { Env.log.proc(`\t\tSetting playback position to ${this.info.startTime}.`); this.seekTo(player, this.info.startTime); break; - default: + case "current": Env.log.proc(`\t\tRetaining playback position`); } } diff --git a/src/renderings/content/ContentRenderer.ts b/src/renderings/content/ContentRenderer.ts index f6894ca..81b0812 100644 --- a/src/renderings/content/ContentRenderer.ts +++ b/src/renderings/content/ContentRenderer.ts @@ -9,7 +9,7 @@ import { RecycleComponent } from "utils/obs/RecycleComponent"; import { PluginSettings } from "Settings"; import { Async } from "utils/ts"; -export function createRenderConfig(settings: PluginSettings): ContentProcessorConfig { +export function createRenderConfig(_settings: PluginSettings): ContentProcessorConfig { return { media: { preventMultiplePlayback: true, @@ -45,7 +45,7 @@ export class ContentRenderer extends RecycleComponent { this.config = config; } - protected onRecycled(component: Component): void { + protected override onRecycled(component: Component): void { Env.log.d("ContentRenderer:onRecycled"); /** These will always run, and before any custom ones. */ @@ -59,7 +59,7 @@ export class ContentRenderer extends RecycleComponent { this.defaultProcessors.forEach(p => component.addChild(p)); } - protected onRecycling(): void { + protected override onRecycling(): void { Env.log.d("ContentRenderer:onRecycling"); this.defaultProcessors = []; this.customProcessors = []; @@ -112,8 +112,7 @@ export class ContentRenderer extends RecycleComponent { return; } - if (processors) - processors.forEach(p => this.recycleComponent.addChild(p)); + processors.forEach(p => this.recycleComponent.addChild(p)); const param = { app: this.app, @@ -151,7 +150,6 @@ export class ContentRenderer extends RecycleComponent { processor.handleHtml(postParameter); } - if (processors) - processors.forEach(p => this.recycleComponent.removeChild(p)); + processors.forEach(p => this.recycleComponent.removeChild(p)); } } diff --git a/src/renderings/content/ContentRendererProcessor.ts b/src/renderings/content/ContentRendererProcessor.ts index 34f7a10..8acae31 100644 --- a/src/renderings/content/ContentRendererProcessor.ts +++ b/src/renderings/content/ContentRendererProcessor.ts @@ -1,6 +1,6 @@ import { Env } from "env"; import { App, Component } from "obsidian"; -import { getDoc } from "utils/obs/dom"; +import { Doc } from "utils/dom/dom"; export type MediaContentProcessorConfig = { preventMultiplePlayback: boolean; @@ -74,7 +74,7 @@ export class ContentRendererPostProcessorAssistant { } public get doc() { - return getDoc(this.param.el); + return Doc.get(this.param.el); } public get el() { diff --git a/src/renderings/content/VideoProcessor.ts b/src/renderings/content/VideoProcessor.ts index 8a2628d..59ff3bb 100644 --- a/src/renderings/content/VideoProcessor.ts +++ b/src/renderings/content/VideoProcessor.ts @@ -6,7 +6,7 @@ export class VideoProcessor extends ContentRendererProcessor implements ContentR param.el.querySelectorAll("video").forEach(el => this.handleVideo(param, el)); } - private handleVideo(param: PostProcessorParameter, el: HTMLVideoElement) { + private handleVideo(_param: PostProcessorParameter, el: HTMLVideoElement) { this.setDefaults(el); } diff --git a/src/renderings/declarations/AlternateHeadingsRenderer.ts b/src/renderings/declarations/AlternateHeadingsRenderer.ts index 47e5f04..a577178 100644 --- a/src/renderings/declarations/AlternateHeadingsRenderer.ts +++ b/src/renderings/declarations/AlternateHeadingsRenderer.ts @@ -1,13 +1,17 @@ -import { DeclarationRenderer, DeclarationRenderable, DeclarationRenderAssistant } from "renderings/declarations/DeclarationRenderable"; -import { AlternateHeadingsDeclarable, AlternateHeadingsAssistant } from "declarations/commands/AlternateHeadings"; +import { AlternateHeadingsAssistant, AlternateHeadingsDeclarable } from "#/declarations/commands/AlternateHeadings"; +import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "#/renderings/declarations/DeclarationRenderable"; + +const Assistant = AlternateHeadingsAssistant; export class AlternateHeadingsRenderer extends DeclarationRenderer implements DeclarationRenderable { + public static override canRender = (value: unknown) => Assistant.is(value); + public render(r: DeclarationRenderAssistant) { - if (!AlternateHeadingsAssistant.conforms(this.declarable) || !AlternateHeadingsAssistant.isValid(this.declarable)) { + if (!Assistant.isValid(this.declarable)) { r.setError(); r.setTitle("Invalid alternate headings command"); r.addParagraph("Please check the entered values."); diff --git a/src/renderings/declarations/CardDeclarationRenderer.ts b/src/renderings/declarations/CardDeclarationRenderer.ts deleted file mode 100644 index e8cbebf..0000000 --- a/src/renderings/declarations/CardDeclarationRenderer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DeclarationRenderer, DeclarationRenderable, DeclarationRenderAssistant } from "renderings/declarations/DeclarationRenderable"; -import { CardDeclarationAssistant, DefaultableCardDeclarable } from "declarations/CardDeclaration"; - -export class CardDeclarationRenderer - extends DeclarationRenderer - implements DeclarationRenderable { - - public render(f: DeclarationRenderAssistant) { - - f.setTitle("Flashcard declaration"); - - const table = f.createEl("table"); - const body = table.createEl("tbody"); - - const rowID = body.createEl("tr"); - rowID.createEl("td", { text: "ID" }); - rowID.createEl("td", { - text: CardDeclarationAssistant.conformsToDeclarable(this.declarable) ? this.declarable.id : "" - }); - - const rowSide = body.createEl("tr"); - rowSide.createEl("td", { text: "Side" }); - rowSide.createEl("td", { text: `${CardDeclarationAssistant.isFrontSide(this.declarable) ? "Front" : "Back"}` }); - - if (CardDeclarationAssistant.isFrontSide(this.declarable)) { - f.createDeckRow(body, this.declarable); - } - } -} diff --git a/src/renderings/declarations/DeclarationErrorRenderer.ts b/src/renderings/declarations/DeclarationErrorRenderer.ts index 857e3a6..d6569d5 100644 --- a/src/renderings/declarations/DeclarationErrorRenderer.ts +++ b/src/renderings/declarations/DeclarationErrorRenderer.ts @@ -1,14 +1,19 @@ -import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "renderings/declarations/DeclarationRenderable"; -import { Declarable } from "declarations/Declaration"; +import { DeclarationRenderable, DeclarationRenderAssistant } from "#/renderings/declarations/DeclarationRenderable"; +import { YamlObject } from "#/declarations/DeclarationCodec"; export class DeclarationErrorRenderer - extends DeclarationRenderer implements DeclarationRenderable { public method?: (r: DeclarationRenderAssistant, errorMessage?: string) => void; public errorMessage?: string; + private declaration: YamlObject; + + constructor(declaration: YamlObject) { + this.declaration = declaration; + } + public render(r: DeclarationRenderAssistant) { r.setError(); if (this.method) { @@ -20,12 +25,12 @@ export class DeclarationErrorRenderer } } - public static invalidCardDeclaration(r: DeclarationRenderAssistant, errorMessage?: string) { + public static invalidCardDeclaration(r: DeclarationRenderAssistant, _errorMessage?: string) { r.setTitle("Invalid card declaration"); r.addParagraph("Please check entered keys and values."); } - public static invalidCommandDeclaration(r: DeclarationRenderAssistant, errorMessage?: string) { + public static invalidCommandDeclaration(r: DeclarationRenderAssistant, _errorMessage?: string) { r.setTitle("Invalid declaration command"); r.addParagraph("Please check the entered values."); } diff --git a/src/renderings/declarations/DeclarationRenderChild.ts b/src/renderings/declarations/DeclarationRenderChild.ts index 92c00aa..c0496fc 100644 --- a/src/renderings/declarations/DeclarationRenderChild.ts +++ b/src/renderings/declarations/DeclarationRenderChild.ts @@ -1,18 +1,22 @@ -import { CardDeclarationAssistant } from "declarations/CardDeclaration"; -import { CommandDeclarationAssistant } from "declarations/CommandDeclaration"; -import { Declaration } from "declarations/Declaration"; -import { Env } from "env"; +import { CommandableAssistant } from "#/declarations/Commandable"; +import { DeclarableAssistant } from "#/declarations/Declarable"; +import { DeclarationCodec } from "#/declarations/DeclarationCodec"; +import { Env } from "#/env"; +import { DeclarationErrorRenderer } from "#/renderings/declarations/DeclarationErrorRenderer"; +import { DataProvider, DeclarationChangedEvent, DeclarationRenderAssistant } from "#/renderings/declarations/DeclarationRenderable"; +import { RendererRegistry } from "#/renderings/RendererRegistry"; +import { Icon } from "#/ui/constants"; import { MarkdownRenderChild, setIcon } from "obsidian"; -import { AlternateHeadingsRenderer } from "renderings/declarations/AlternateHeadingsRenderer"; -import { CardDeclarationRenderer } from "renderings/declarations/CardDeclarationRenderer"; -import { DeclarationErrorRenderer } from "renderings/declarations/DeclarationErrorRenderer"; -import { DataProvider, DeclarationChangedEvent, DeclarationRenderable, DeclarationRenderAssistant } from "renderings/declarations/DeclarationRenderable"; -import { HeadingAndDelimiterRenderer } from "renderings/declarations/HeadingAndDelimiterRenderer"; -import { HeadingIsFrontRenderer } from "renderings/declarations/HeadingIsFrontRenderer"; -import { Icon } from "ui/constants"; export class DeclarationRenderChild extends MarkdownRenderChild { + private source: string; + private dataProvider: DataProvider; + + private titleContainer!: HTMLDivElement; + private titleEl!: HTMLDivElement; + private contentContainerEl!: HTMLDivElement; + public constructor(containerEl: HTMLElement, source: string, dataProvider: DataProvider) { super(containerEl); @@ -20,25 +24,31 @@ export class DeclarationRenderChild extends MarkdownRenderChild { this.dataProvider = dataProvider; } + public override onload(): void { + Env.log.d("DeclarationRenderChild:onload"); + super.onload(); + + // Manual cleanup for these elements in `onunload` is not necessary because they are added directly + // to `containerEl`, which is managed and eventually discarded by the base class. + this.containerEl.addClass("callout"); + + this.titleContainer = this.containerEl.createDiv({ cls: "callout-title" }); + this.titleContainer.createDiv({ cls: "callout-icon" }, (icon) => setIcon(icon, Icon.PLUGIN)); + this.titleEl = this.titleContainer.createDiv({ cls: "callout-title-inner" }); + + this.contentContainerEl = this.containerEl.createDiv({ cls: "callout-content" }); + } + public override onunload(): void { Env.log.d("DeclarationRenderChild:onunload"); super.onunload(); } - private contentContainerEl: HTMLDivElement; - private source: string; - private dataProvider: DataProvider; - - private titleContainer: HTMLDivElement; - private titleEl: HTMLDivElement; - /** * @param onDomEvent DOM event registered with rendered elements such as buttons or select. */ public render(onDomEvent: DeclarationChangedEvent) { - this.initRender(); - const r = new DeclarationRenderAssistant( this.containerEl, this.contentContainerEl, @@ -49,50 +59,28 @@ export class DeclarationRenderChild extends MarkdownRenderChild { onDomEvent ); - const declaration = Declaration.tryParseYaml(this.source, error => this.renderYamlError(r, error.message)); - if (declaration === null) + const declaration = DeclarationCodec.tryFromYaml(this.source, error => this.renderYamlError(r, error.message)); + if (declaration === null || !DeclarableAssistant.is(declaration)) return; - let declarationRenderer: DeclarationRenderable = new DeclarationErrorRenderer(declaration); + let declarationRenderer = RendererRegistry.tryCreate(declaration); - if (CommandDeclarationAssistant.conforms(declaration)) { + if (declarationRenderer === null) { + const errorRenderer = new DeclarationErrorRenderer(declaration); - if (CommandDeclarationAssistant.isNameValid(declaration)) { - if (CommandDeclarationAssistant.isHeadingAndDelimiter(declaration)) - declarationRenderer = new HeadingAndDelimiterRenderer(declaration); - else if (CommandDeclarationAssistant.isAlternateHeadings(declaration)) - declarationRenderer = new AlternateHeadingsRenderer(declaration); - else if (CommandDeclarationAssistant.isHeadingIsFront(declaration)) - declarationRenderer = new HeadingIsFrontRenderer(declaration); + if (CommandableAssistant.is(declaration)) { + errorRenderer.method = DeclarationErrorRenderer.unknownCommandName; + errorRenderer.errorMessage = `Name: ${declaration.name}`; + } else { + errorRenderer.method = DeclarationErrorRenderer.invalidCardDeclaration; } - if (declarationRenderer instanceof DeclarationErrorRenderer) { - declarationRenderer.method = DeclarationErrorRenderer.unknownCommandName; - declarationRenderer.errorMessage = `Name: ${declaration.name}`; - } - } - else { - if (CardDeclarationAssistant.conformsToDefaultable(declaration)) - declarationRenderer = new CardDeclarationRenderer(declaration); - - if (declarationRenderer instanceof DeclarationErrorRenderer) - declarationRenderer.method = DeclarationErrorRenderer.invalidCardDeclaration; + declarationRenderer = errorRenderer; } declarationRenderer.render(r); } - private initRender() { - this.containerEl.addClass("callout"); - - this.titleContainer = this.containerEl.createDiv({ cls: "callout-title" }); - this.titleContainer.createDiv({ cls: "callout-icon" }, (icon) => setIcon(icon, Icon.PLUGIN)); - this.titleEl = this.titleContainer.createDiv({ cls: "callout-title-inner" }); - - this.contentContainerEl = this.containerEl.createDiv({ cls: "callout-content" }); - return this.contentContainerEl; - } - private renderYamlError(r: DeclarationRenderAssistant, errorMessage?: string) { r.setError(); r.setTitle("Invalid format entered"); diff --git a/src/renderings/declarations/DeclarationRenderable.ts b/src/renderings/declarations/DeclarationRenderable.ts index d799764..fcbcf82 100644 --- a/src/renderings/declarations/DeclarationRenderable.ts +++ b/src/renderings/declarations/DeclarationRenderable.ts @@ -1,35 +1,63 @@ -import { DeckIDDataTuple } from "data/DataStore"; -import { DeckableDeclarable, Declarable } from "declarations/Declaration"; +import { DeckData, DeckIDDataTuple } from "#/data/DataStore"; +import { DeckID } from "#/data/FullID"; +import { DeckableDeclarable } from "#/declarations/Collectionable"; +import { Declarable } from "#/declarations/Declarable"; +import { HtmlTag } from "#/utils/dom/constants"; +import { TableCreator } from "#/utils/dom/table"; import { Component, setIcon } from "obsidian"; -import { UIAssistant } from "ui/UIAssistant"; export type DeclarationChangedType = "deckAdded" | "deckChanged"; export type DeclarationChangedEvent = (declaration: DeckableDeclarable, type: DeclarationChangedType, selectEl: HTMLSelectElement) => void; export interface DataProvider { getAllDecks: () => DeckIDDataTuple[]; + getDeck: (id: DeckID) => DeckData | null; }; export interface DeclarationRenderable { render(r: DeclarationRenderAssistant): void; } +export interface FactoryRegistryEntry { + names: readonly string[]; + create: (d: unknown) => DeclarationRenderable | null; +} + export abstract class DeclarationRenderer { protected declarable: T; - public constructor(declarable: T) { - this.declarable = declarable; - } + + public constructor(declarable: T) { this.declarable = declarable; } + + /** + * @abstract + * @returns Whether the renderer can render the given value. + */ + public static canRender = (_value: unknown): boolean => { throw new Error("Not implemented"); } + + public static readonly Factory = { + createEntry: ( + names: readonly string[], + RendererClass: { + new(declarable: T): DeclarationRenderable; + canRender(value: unknown): value is T; + }): FactoryRegistryEntry => { + return { + names, + create: (declarable) => RendererClass.canRender(declarable) ? new RendererClass(declarable) : null + }; + } + }; } export class DeclarationRenderAssistant { - private containerEl: HTMLElement; - protected contentContainerEl: HTMLDivElement; - private titleContainer: HTMLDivElement; - private titleEl: HTMLDivElement; - private component: Component; - private dataProvider: DataProvider; - protected onDomEvent: DeclarationChangedEvent + private readonly containerEl: HTMLElement; + private readonly contentContainerEl: HTMLDivElement; + private readonly titleContainer: HTMLDivElement; + private readonly titleEl: HTMLDivElement; + private readonly component: Component; + private readonly dataProvider: DataProvider; + private readonly onDomEvent: DeclarationChangedEvent; public constructor( containerEl: HTMLElement, @@ -81,15 +109,19 @@ export class DeclarationRenderAssistant { }); } + public createTable() { + return TableCreator.create(this.contentContainerEl); + } + public createDeckRow(body: HTMLTableSectionElement, declaration: DeckableDeclarable) { const rowDeck = body.createEl("tr"); rowDeck.createEl("td", { text: "Deck" }); const tdDropdown = rowDeck.createEl("td", { cls: "select-deck-cell" }); - const deckSelectEl = tdDropdown.createEl("select", { cls: "dropdown" }, (el) => { - el.createEl("option", { + const deckSelectEl = tdDropdown.createEl(HtmlTag.SELECT.NAME, { cls: "dropdown" }, (el) => { + el.createEl(HtmlTag.SELECT.OPTION.NAME, { text: "None", - value: UIAssistant.DECK_ID_NONE, + value: HtmlTag.SELECT.OPTION.Values.NONE, }); }); @@ -103,11 +135,11 @@ export class DeclarationRenderAssistant { const decks = this.dataProvider.getAllDecks(); if (decks.length > 0) { decks.forEach(deck => { - deckSelectEl.createEl("option", { + deckSelectEl.createEl(HtmlTag.SELECT.OPTION.NAME, { text: deck.data.n, value: deck.id, }, (el) => { - el.selected = declaration?.deckID === deck.id; + el.selected = declaration.deckID === deck.id; }); }); this.component.registerDomEvent(deckSelectEl, "change", () => this.onDomEvent(declaration, "deckChanged", deckSelectEl)); diff --git a/src/renderings/declarations/HeadingAndDelimiterRenderer.ts b/src/renderings/declarations/HeadingAndDelimiterRenderer.ts index 85f3393..2030460 100644 --- a/src/renderings/declarations/HeadingAndDelimiterRenderer.ts +++ b/src/renderings/declarations/HeadingAndDelimiterRenderer.ts @@ -1,13 +1,18 @@ -import { DeclarationRenderer, DeclarationRenderable, DeclarationRenderAssistant } from "renderings/declarations/DeclarationRenderable"; -import { HeadingAndDelimiterAssistant, HeadingAndDelimiterDeclarable } from "declarations/commands/HeadingAndDelimiter"; +import { HeadingAndDelimiterAssistant, HeadingAndDelimiterDeclarable } from "#/declarations/commands/HeadingAndDelimiter"; +import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "#/renderings/declarations/DeclarationRenderable"; +import { UNARY_UNION_SUPPRESS } from "#/utils/ts"; + +const Assistant = HeadingAndDelimiterAssistant; export class HeadingAndDelimiterRenderer extends DeclarationRenderer implements DeclarationRenderable { + public static override canRender = (value: unknown) => Assistant.is(value); + public render(r: DeclarationRenderAssistant) { - if (!HeadingAndDelimiterAssistant.conforms(this.declarable) || !HeadingAndDelimiterAssistant.isValid(this.declarable)) { + if (!HeadingAndDelimiterAssistant.is(this.declarable) || !HeadingAndDelimiterAssistant.isValid(this.declarable)) { r.setError(); r.setTitle("Invalid alternate headings command"); r.addParagraph("Please check the entered values."); @@ -22,10 +27,16 @@ export class HeadingAndDelimiterRenderer text: `Automatically generate a card for every heading ${this.declarable.level} levels below this one.` }), el.createEl("li", undefined, (el) => { - if (this.declarable.delimiter === "horizontal rule") { - el.appendText("The back side will begin after the first "); - el.createEl("a", { text: "horizontal rule", href: "https://daringfireball.net/projects/markdown/syntax#hr" }); - el.appendText(" within the heading’s section."); + switch (this.declarable.delimiter) { + case UNARY_UNION_SUPPRESS: + break; + + case "horizontal rule": { + el.appendText("The back side will begin after the first "); + el.createEl("a", { text: "horizontal rule", href: "https://daringfireball.net/projects/markdown/syntax#hr" }); + el.appendText(" within the heading’s section."); + break; + } } }) ]; @@ -40,7 +51,7 @@ export class HeadingAndDelimiterRenderer }); body.createEl("tr", undefined, (el) => { - el.createEl("td", { text: "Side delimiter" }); + el.createEl("td", { text: "Page delimiter" }); el.createEl("td", { text: "The first horizontal rule" }); }); diff --git a/src/renderings/declarations/HeadingIsFrontRenderer.ts b/src/renderings/declarations/HeadingIsFrontRenderer.ts index 01408a0..1e87cd4 100644 --- a/src/renderings/declarations/HeadingIsFrontRenderer.ts +++ b/src/renderings/declarations/HeadingIsFrontRenderer.ts @@ -1,13 +1,17 @@ -import { HeadingIsFrontAssistant, HeadingIsFrontDeclarable } from "declarations/commands/HeadingIsFront"; -import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "renderings/declarations/DeclarationRenderable"; +import { HeadingIsFrontAssistant, HeadingIsFrontDeclarable } from "#/declarations/commands/HeadingIsFront"; +import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "#/renderings/declarations/DeclarationRenderable"; + +const Assistant = HeadingIsFrontAssistant; export class HeadingIsFrontRenderer extends DeclarationRenderer implements DeclarationRenderable { + public static override canRender = (value: unknown) => Assistant.is(value); + public render(r: DeclarationRenderAssistant) { - if (!HeadingIsFrontAssistant.conforms(this.declarable) || !HeadingIsFrontAssistant.isValid(this.declarable)) { + if (!HeadingIsFrontAssistant.is(this.declarable) || !HeadingIsFrontAssistant.isValid(this.declarable)) { r.setError(); r.setTitle("Invalid heading is front command"); r.addParagraph("Please check the entered values."); diff --git a/src/renderings/declarations/PageDeclarationRenderer.ts b/src/renderings/declarations/PageDeclarationRenderer.ts new file mode 100644 index 0000000..3620267 --- /dev/null +++ b/src/renderings/declarations/PageDeclarationRenderer.ts @@ -0,0 +1,83 @@ +import { DefaultableCardDeclarable, ExplicitDeclarationAssistant, MaybePageDeclarable } from "#/declarations/ExplicitDeclaration"; +import { DeclarationRenderable, DeclarationRenderAssistant, DeclarationRenderer } from "#/renderings/declarations/DeclarationRenderable"; +import { Str } from "utils/ts"; + +const Assistant = ExplicitDeclarationAssistant; + +export class PageDeclarationRenderer + extends DeclarationRenderer + implements DeclarationRenderable { + + public render(r: DeclarationRenderAssistant) { + + if (!Assistant.MaybePage.isValid(this.declarable)) { + PageDeclarationRenderer.renderError(r, "`side` must be a string, number, or boolean."); + } + else { + if (Assistant.Defaultable.is(this.declarable)) { + PageDeclarationRenderer.renderDefaultDeclarable(r, this.declarable); + } + else { + PageDeclarationRenderer.renderError(r, "`id` and `side` must be text."); + } + } + } + + private static renderError(r: DeclarationRenderAssistant, msg?: string) { + r.setError(); + r.setTitle("Invalid page declaration"); + if (msg !== undefined) + r.addParagraph(msg); + } + + private static renderDefaultDeclarable(r: DeclarationRenderAssistant, declarable: DefaultableCardDeclarable) { + + if (!Assistant.Defaultable.isValid(declarable)) { + PageDeclarationRenderer.renderError(r); + r.addBulletList(["`side` must be f, front, b or back", "`id` must be a string"]); + return; + } + + const completeDeclarable = Assistant.is(declarable) ? declarable : null; + const validCompleteDeclarable = completeDeclarable !== null && Assistant.isValid(completeDeclarable) ? completeDeclarable : null; + + if (Assistant.canComplete(declarable) || validCompleteDeclarable !== null) { + const isFront = Assistant.isFrontSide(declarable); + + r.setTitle("Page declaration"); + + let idValue = validCompleteDeclarable !== null ? validCompleteDeclarable.id : declarable.id; + + if (Assistant.PropertyEq.optionalNullOrString(idValue, (str) => !Str.isNonEmpty(str))) { + idValue = isFront ? "Generating…" : Str.EMPTY; + if (!isFront) + r.addParagraph("`id` is missing."); + } + + r.createTable().addBody(row => { + + row.add((col) => { + col.add({ text: "ID" }); + col.add({ text: idValue }); + }); + + row.add((col) => { + col.add({ text: "Side" }); + col.add({ text: `${isFront ? "Front" : "Back"}` }); + }); + + if (isFront) { + r.createDeckRow(row.sectionEl, declarable); + } + }); + } + else { // validCompleteDeclarable === null + PageDeclarationRenderer.renderError(r); + + if (Assistant.isFrontSide(declarable)) + r.addParagraph("`id` should be a 10 character string."); + else + r.addParagraph("Add `id` that matches the front side."); + } + } +} diff --git a/src/scheduling/Fsrs.ts b/src/scheduling/Fsrs.ts index 522a3ec..3519619 100644 --- a/src/scheduling/Fsrs.ts +++ b/src/scheduling/Fsrs.ts @@ -1,4 +1,3 @@ -import { Env } from "env"; import { FsrsSchedulerConfig } from "scheduling/types"; import { Card, createEmptyCard, default_enable_fuzz, default_enable_short_term, default_learning_steps, default_maximum_interval, default_relearning_steps, default_request_retention, fsrs, FSRS, generatorParameters, Grade, RecordLogItem } from "ts-fsrs"; @@ -15,7 +14,6 @@ export class Fsrs { } private static createFromConfig(config: FsrsSchedulerConfig) { - Env.assert(config); const params = generatorParameters({ request_retention: default_request_retention, diff --git a/src/ui/SettingTab.ts b/src/ui/SettingTab.ts index e8fb5cd..190fb44 100644 --- a/src/ui/SettingTab.ts +++ b/src/ui/SettingTab.ts @@ -29,7 +29,7 @@ export class SettingTab extends PluginSettingTab { .addText((component) => { component.setValue(settings.uiPrefix); component.onChange(async (value) => { - settings.uiPrefix = value; + settings.uiPrefix = value.trim(); await this.settingsManager.save(); }); }); @@ -46,7 +46,7 @@ export class SettingTab extends PluginSettingTab { }); } - public hide(): void { + public override hide(): void { this.settingsManager.unregisterOnChangedCallback(this.onChangedCallback); } } diff --git a/src/ui/UIAssistant.ts b/src/ui/UIAssistant.ts index a32c414..aac871d 100644 --- a/src/ui/UIAssistant.ts +++ b/src/ui/UIAssistant.ts @@ -1,18 +1,11 @@ -import { DeckIDDataTuple } from "data/DataStore"; -import { DeckID } from "data/FullID"; import { App, MarkdownView, Menu, MenuItem, Notice } from "obsidian"; import { SettingsManager } from "Settings"; -import { Icon, PLUGIN_NAME } from "ui/constants"; +import { Icon } from "ui/constants"; +import { Str } from "utils/ts"; export class UIAssistant { - /** - * Deck ID to use to represent unassigned deck when a string is required to identify UI elements. - * For example string values in DOM elements. - */ - public static readonly DECK_ID_NONE: DeckID = "0000"; - private settingsManager: SettingsManager; constructor(settingsManager: SettingsManager) { @@ -21,10 +14,7 @@ export class UIAssistant { public contextulize(title: string) { const contextPrefix = this.settingsManager.settings.uiPrefix; - if (contextPrefix !== undefined) - return contextPrefix ? `${contextPrefix}: ${title}` : title; - else - return `${PLUGIN_NAME}: ${title}`; + return Str.isNonEmpty(contextPrefix) ? `${contextPrefix}: ${title}` : title; } public addMenuItem(menu: Menu, title: string, options?: { @@ -33,8 +23,8 @@ export class UIAssistant { icon?: string, prefix?: boolean, isLabel?: boolean, - onClick?: (evt: MouseEvent | KeyboardEvent) => any - callback?: (item: MenuItem) => any, + onClick?: (evt: MouseEvent | KeyboardEvent) => void, + callback?: (item: MenuItem) => void, }): Menu { const { callback, @@ -42,7 +32,7 @@ export class UIAssistant { menu.addItem(item => { this.configureMenuItem(item, title, options); - callback?.(item); + callback?.(item); }); return menu; @@ -54,7 +44,7 @@ export class UIAssistant { icon?: string, prefix?: boolean, isLabel?: boolean, - onClick?: (evt: MouseEvent | KeyboardEvent) => any + onClick?: (evt: MouseEvent | KeyboardEvent) => void, }): MenuItem { const { section, @@ -103,21 +93,11 @@ export class UIAssistant { return notice; } - public static allDecksOptionItem(): DeckIDDataTuple { - return { - id: UIAssistant.DECK_ID_NONE, - data: { - n: "All Decks", - p: [] - } - }; - } - public static isInInLivePreview(app: App) { const markdownView = app.workspace.getActiveViewOfType(MarkdownView) if (!markdownView) return false; const state = markdownView.getState(); - return state ? state.mode == "source" && state.source == false : false; + return state["mode"] == "source" && state["source"] == false; } } diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts index 36463fc..0fe2fe3 100644 --- a/src/utils/datetime.ts +++ b/src/utils/datetime.ts @@ -1,5 +1,6 @@ import { Env } from "env"; // Make sure you're doing import { moment} from 'obsidian' so that you don't import another copy — https://docs.obsidian.md/oo24/plugin#Performance +// Regarding use of `@ts-expect-error`: Obsidian's recommended import style (import { moment } from 'obsidian') is typed as a namespace and is flagged as "not callable" by TypeScript 6.0's stricter checks. Instead of setting `esModuleInterop` to `true` in `tsconfig` just use `@ts-expect-error` here. import { moment } from "obsidian"; // TODO: Use Luxon import { Str } from "utils/ts"; // @ts-expect-error @@ -9,16 +10,20 @@ const TIME_FORMAT = "LT"; const DATE_FORMAT = "MMM D, LT"; export const DateTime = { + // @ts-expect-error toTimeString: (date: Date) => moment(date).format(TIME_FORMAT), + // @ts-expect-error toString: (date: Date) => moment(date).format(DATE_FORMAT), dateStringFromIso: (iso8601: string): string => { Env.assert(!Str.is(iso8601 as unknown), "Expected an ISO string."); + // @ts-expect-error return moment(iso8601).format(DATE_FORMAT); }, dateFromIso: (iso8601: string): Date => { Env.assert(!Str.is(iso8601 as unknown), "Expected an ISO string."); + // @ts-expect-error return moment(iso8601).toDate(); }, diff --git a/src/utils/dom/constants.ts b/src/utils/dom/constants.ts index eea9bdf..5203641 100644 --- a/src/utils/dom/constants.ts +++ b/src/utils/dom/constants.ts @@ -1,5 +1,19 @@ +import { Str } from "#/utils/ts"; + export const HtmlTag = { SPAN: "span", + SELECT: { + NAME: "select", + OPTION: { + NAME: "option", + Values: { + /** Represents an empty option, used as a placeholder or for no selection. */ + NONE: Str.EMPTY, + }, + isNone: (value: string): boolean => value === Str.EMPTY, + select: (el: HTMLOptionElement) => el.selected = true, + } + } } as const; export const HtmlAttribute = { @@ -23,7 +37,7 @@ export const HtmlAttribute = { Controls: { NAME: "controls", Values: { - DISPLAY: "" + DISPLAY: Str.EMPTY } }, /** The controlsList property of an HTMLMediaElement (like