diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index f9b501bf52..a032c28d34 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -40,10 +40,10 @@ object]} :discouraged-var - {rum.core/use-effect! {:message "Use frontend.hooks/use-effect! instead" :level :info} - rum.core/use-memo {:message "Use frontend.hooks/use-memo instead" :level :info} - rum.core/use-layout-effect! {:message "Use frontend.hooks/use-layout-effect! instead" :level :info} - rum.core/use-callback {:message "Use frontend.hooks/use-callback instead" :level :info}} + {rum.core/use-effect! {:message "Use logseq.shui.hooks/use-effect! instead" :level :info} + rum.core/use-memo {:message "Use logseq.shui.hooks/use-memo instead" :level :info} + rum.core/use-layout-effect! {:message "Use logseq.shui.hooks/use-layout-effect! instead" :level :info} + rum.core/use-callback {:message "Use logseq.shui.hooks/use-callback instead" :level :info}} :unused-namespace {:level :warning :exclude [logseq.db.common.entity-plus]} @@ -70,16 +70,14 @@ electron.ipc ipc electron.utils utils frontend.commands commands - frontend.common.date common-date - frontend.common.missionary-util c.m - frontend.common.schema-register sr + frontend.common.idb idb + frontend.common.missionary c.m frontend.common.search-fuzzy fuzzy frontend.components.block.macros block-macros frontend.components.class class-component frontend.components.property property-component frontend.components.query query frontend.components.query.result query-result - frontend.components.title title frontend.config config frontend.date date frontend.db db @@ -88,9 +86,8 @@ frontend.db.query-dsl query-dsl frontend.db.query-react query-react frontend.db.react react - frontend.db.util db-utils + frontend.db.utils db-utils frontend.diff diff - frontend.encrypt encrypt frontend.extensions.sci sci frontend.format.block block frontend.format.mldoc mldoc @@ -106,15 +103,12 @@ frontend.handler.db-based.page db-page-handler frontend.handler.db-based.property db-property-handler frontend.handler.db-based.property.util db-pu - frontend.handler.editor.property editor-property frontend.handler.events events - frontend.handler.extract extract frontend.handler.global-config global-config-handler frontend.handler.notification notification frontend.handler.page page-handler frontend.handler.plugin plugin-handler frontend.handler.plugin-config plugin-config-handler - frontend.handler.property.file property-file frontend.handler.property.util pu frontend.handler.query.builder query-builder frontend.handler.repo repo-handler @@ -122,8 +116,6 @@ frontend.handler.route route-handler frontend.handler.search search-handler frontend.handler.ui ui-handler - frontend.hooks hooks - frontend.idb idb frontend.loader loader frontend.mixins mixins frontend.mobile.util mobile-util @@ -138,17 +130,15 @@ frontend.ui ui frontend.util util frontend.util.page page-util - frontend.util.persist-var persist-var - frontend.util.property property frontend.util.text text-util frontend.util.thingatpt thingatpt frontend.util.url url-util frontend.util.ref ref + frontend.worker-common.util worker-util frontend.worker.shared-service shared-service frontend.worker.handler.page worker-page frontend.worker.pipeline worker-pipeline frontend.worker.state worker-state - frontend.worker.util worker-util lambdaisland.glogi log logseq.api.db-based db-based-api logseq.cli.common.graph cli-common-graph @@ -159,11 +149,12 @@ logseq.cli.common.mcp.tools cli-common-mcp-tools logseq.cli.text-util cli-text-util logseq.common.config common-config - logseq.common.date-time-util date-time-util + logseq.common.date common-date logseq.common.graph common-graph logseq.common.path path logseq.common.util common-util logseq.common.util.block-ref block-ref + logseq.common.util.date-time date-time-util logseq.common.util.macro macro-util logseq.common.util.namespace ns-util logseq.common.util.page-ref page-ref @@ -195,7 +186,6 @@ logseq.graph-parser.mldoc gp-mldoc logseq.graph-parser.property gp-property logseq.graph-parser.text text - logseq.outliner.batch-tx batch-tx logseq.outliner.core outliner-core logseq.outliner.datascript-report ds-report logseq.outliner.op outliner-op @@ -203,6 +193,7 @@ logseq.outliner.pipeline outliner-pipeline logseq.outliner.tree otree logseq.outliner.validate outliner-validate + logseq.shui.hooks hooks logseq.shui.popup.core shui-popup logseq.shui.ui shui medley.core medley diff --git a/.github/workflows/deps-publish.yml b/.github/workflows/deps-publish.yml index 34de612fcd..55f9e86f0e 100644 --- a/.github/workflows/deps-publish.yml +++ b/.github/workflows/deps-publish.yml @@ -36,7 +36,45 @@ env: BABASHKA_VERSION: '1.12.215' jobs: - test-release: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.33.0 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + cache-dependency-path: deps/publish/pnpm-lock.yaml + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: ${{ env.JAVA_VERSION }} + + # Clojure needed for test build step + - name: Set up Clojure + uses: DeLaGuardo/setup-clojure@13.5 + with: + cli: ${{ env.CLOJURE_VERSION }} + bb: ${{ env.BABASHKA_VERSION }} + + - name: Fetch pnpm deps + run: pnpm install --frozen-lockfile + + - name: Run publish unit tests + run: pnpm test + + build-release: runs-on: ubuntu-latest steps: @@ -94,7 +132,7 @@ jobs: bb: ${{ env.BABASHKA_VERSION }} - name: Run clj-kondo lint - run: clojure -M:clj-kondo --lint src + run: clojure -M:clj-kondo --lint src test - name: Carve lint for unused vars run: bb lint:carve diff --git a/.i18n-lint.toml b/.i18n-lint.toml index a1bb609afb..d82123c3f2 100644 --- a/.i18n-lint.toml +++ b/.i18n-lint.toml @@ -106,6 +106,7 @@ exclude_patterns = [ "src/main/frontend/handler/shell.cljs", # Run shell command "src/main/frontend/undo_redo/debug_ui.cljs", # Developer undo/redo tool "src/main/frontend/worker/commands.cljs", # Internal command identifier strings + "src/main/frontend/**/plugin*", # Plugin namespaces — may contain hardcoded strings that are not ] # Maximum character length of the text preview in output. diff --git a/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs b/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs index 38cc57658c..eacf6ad040 100644 --- a/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs +++ b/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs @@ -534,6 +534,16 @@ [value] (contains? #{"true" "1"} value)) +(defn- sqlite-too-big-error? + [error] + (let [message (-> (or (ex-message error) + (some-> error .-message) + (str error)) + string/lower-case)] + (or (string/includes? message "sqlite_toobig") + (string/includes? message "string or blob too big") + (string/includes? message "statement too long")))) + (defn- handle-sync-snapshot-upload [^js self request url] (let [graph-id (graph-id-from-request request) @@ -556,27 +566,27 @@ (if (and (= encoding snapshot-content-encoding) (not (exists? js/DecompressionStream))) (http/error-response "gzip not supported" 500) - (p/let [_ (ensure-schema! self) - _ (when reset? - (storage/set-meta! (.-sql self) snapshot-uploading-meta-key true)) - _ (when reset? - ( (p/with-redefs [sync-handler/import-snapshot-stream! (fn [_self _stream _reset?] - (p/resolved 0)) + (p/rejected (js/Error. "string or blob too big: SQLITE_TOOBIG"))) sync-handler/clj (js/JSON.parse text) :keywordize-keys true)] + (is (= 413 (.-status resp))) + (is (= {:error "snapshot row too large"} body)))) (p/then (fn [] (done))) (p/catch (fn [error] diff --git a/deps/outliner/.carve/config.edn b/deps/outliner/.carve/config.edn index 84716ce275..08806ebf1b 100644 --- a/deps/outliner/.carve/config.edn +++ b/deps/outliner/.carve/config.edn @@ -2,7 +2,6 @@ :api-namespaces [logseq.outliner.datascript-report logseq.outliner.pipeline logseq.outliner.cli - logseq.outliner.batch-tx logseq.outliner.core logseq.outliner.db-pipeline logseq.outliner.property diff --git a/deps/publish/deps.edn b/deps/publish/deps.edn index a16b052345..39dd76a6eb 100644 --- a/deps/publish/deps.edn +++ b/deps/publish/deps.edn @@ -1,4 +1,4 @@ -{:paths ["src" "../../resources"] +{:paths ["src" "test" "../../resources"] :deps {org.clojure/clojure {:mvn/version "1.12.4"} rum/rum {:git/url "https://github.com/logseq/rum" ;; fork diff --git a/deps/publish/package.json b/deps/publish/package.json index b9bfc5e9a0..b51804749b 100644 --- a/deps/publish/package.json +++ b/deps/publish/package.json @@ -7,11 +7,13 @@ "dev": "cd ./worker && pnpm exec wrangler dev", "watch": "clojure -M:cljs watch publish-worker", "release": "clojure -M:cljs release publish-worker", + "test": "clojure -M:cljs compile publish-test && node worker/dist/worker-test.js", "clean": "rm -rf ./worker/dist/", "bump-publish-version": "node ./scripts/bump-publish-version.js", "deploy": "pnpm bump-publish-version && pnpm clean && pnpm release && cd ./worker && pnpm exec wrangler deploy --env prod" }, "dependencies": { + "mldoc": "^1.5.9", "shadow-cljs": "^3.4.4" } } diff --git a/deps/publish/pnpm-lock.yaml b/deps/publish/pnpm-lock.yaml index b57232d9ec..dfcd3cfa35 100644 --- a/deps/publish/pnpm-lock.yaml +++ b/deps/publish/pnpm-lock.yaml @@ -8,12 +8,23 @@ importers: .: dependencies: + mldoc: + specifier: ^1.5.9 + version: 1.5.9 shadow-cljs: specifier: ^3.4.4 version: 3.4.4 packages: + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -23,21 +34,168 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + cliui@4.1.0: + resolution: {integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==} + + code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + get-caller-file@1.0.3: + resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + invert-kv@2.0.0: + resolution: {integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==} + engines: {node: '>=4'} + + is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + lcid@2.0.0: + resolution: {integrity: sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==} + engines: {node: '>=6'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + + mem@4.3.0: + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mldoc@1.5.9: + resolution: {integrity: sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ==} + hasBin: true + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + os-locale@3.1.0: + resolution: {integrity: sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==} + engines: {node: '>=6'} + + p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + readline-sync@1.4.10: resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} engines: {node: '>= 0.8.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@1.0.1: + resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + shadow-cljs-jar@1.3.4: resolution: {integrity: sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA==} @@ -46,6 +204,17 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -53,11 +222,45 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@5.0.0: resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + wrap-ansi@2.1.0: + resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -70,8 +273,21 @@ packages: utf-8-validate: optional: true + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@11.1.1: + resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==} + + yargs@12.0.5: + resolution: {integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==} + snapshots: + ansi-regex@2.1.1: {} + + ansi-regex@3.0.1: {} + base64-js@1.5.1: {} buffer-from@1.1.2: {} @@ -81,14 +297,146 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + camelcase@5.3.1: {} + + cliui@4.1.0: + dependencies: + string-width: 2.1.1 + strip-ansi: 4.0.0 + wrap-ansi: 2.1.0 + + code-point-at@1.1.0: {} + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + decamelize@1.2.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + get-caller-file@1.0.3: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + ieee754@1.2.1: {} + invert-kv@2.0.0: {} + + is-fullwidth-code-point@1.0.0: + dependencies: + number-is-nan: 1.0.1 + + is-fullwidth-code-point@2.0.0: {} + + is-stream@1.1.0: {} + + isexe@2.0.0: {} + isexe@3.1.1: {} + lcid@2.0.0: + dependencies: + invert-kv: 2.0.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + map-age-cleaner@0.1.3: + dependencies: + p-defer: 1.0.0 + + mem@4.3.0: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + + mimic-fn@2.1.0: {} + + mldoc@1.5.9: + dependencies: + yargs: 12.0.5 + + nice-try@1.0.5: {} + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + number-is-nan@1.0.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + os-locale@3.1.0: + dependencies: + execa: 1.0.0 + lcid: 2.0.0 + mem: 4.3.0 + + p-defer@1.0.0: {} + + p-finally@1.0.0: {} + + p-is-promise@2.1.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@3.0.0: {} + + path-key@2.0.1: {} + process@0.11.10: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + readline-sync@1.4.10: {} + require-directory@2.1.1: {} + + require-main-filename@1.0.1: {} + + semver@5.7.2: {} + + set-blocking@2.0.0: {} + shadow-cljs-jar@1.3.4: {} shadow-cljs@3.4.4: @@ -104,6 +452,14 @@ snapshots: - bufferutil - utf-8-validate + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-regex@1.0.0: {} + + signal-exit@3.0.7: {} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -111,8 +467,64 @@ snapshots: source-map@0.6.1: {} + string-width@1.0.2: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-eof@1.0.0: {} + + which-module@2.0.1: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@5.0.0: dependencies: isexe: 3.1.1 + wrap-ansi@2.1.0: + dependencies: + string-width: 1.0.2 + strip-ansi: 3.0.1 + + wrappy@1.0.2: {} + ws@8.18.3: {} + + y18n@4.0.3: {} + + yargs-parser@11.1.1: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@12.0.5: + dependencies: + cliui: 4.1.0 + decamelize: 1.2.0 + find-up: 3.0.0 + get-caller-file: 1.0.3 + os-locale: 3.1.0 + require-directory: 2.1.1 + require-main-filename: 1.0.1 + set-blocking: 2.0.0 + string-width: 2.1.1 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 11.1.1 diff --git a/deps/publish/shadow-cljs.edn b/deps/publish/shadow-cljs.edn index 396ea96b6e..c418f99d60 100644 --- a/deps/publish/shadow-cljs.edn +++ b/deps/publish/shadow-cljs.edn @@ -9,4 +9,9 @@ PublishMetaDO logseq.publish.worker/PublishMetaDO}}} :js-options {:js-provider :import} :closure-defines {shadow.cljs.devtools.client.env/enabled false} - :devtools {:enabled false}}}} + :devtools {:enabled false}} + :publish-test {:target :node-test + :output-to "worker/dist/worker-test.js" + :devtools {:enabled false} + :compiler-options {:static-fns false} + :main logseq.publish.test-runner/main}}} diff --git a/deps/publish/src/logseq/publish/meta_store.cljs b/deps/publish/src/logseq/publish/meta_store.cljs index 0243567ee6..671f0ed607 100644 --- a/deps/publish/src/logseq/publish/meta_store.cljs +++ b/deps/publish/src/logseq/publish/meta_store.cljs @@ -281,10 +281,11 @@ "page_tags.source_block_content, page_tags.source_block_format, page_tags.updated_at, " "pages.short_id " "FROM page_tags " - "LEFT JOIN pages " + "INNER JOIN pages " "ON pages.graph_uuid = page_tags.graph_uuid " "AND pages.page_uuid = page_tags.source_page_uuid " "WHERE page_tags.tag_title = ? " + "AND pages.password_hash IS NULL " "ORDER BY page_tags.updated_at DESC;") tag-name)) page-rows (publish-common/get-sql-rows @@ -293,10 +294,11 @@ "pages.short_id, " "MAX(page_tags.updated_at) AS updated_at " "FROM page_tags " - "LEFT JOIN pages " + "INNER JOIN pages " "ON pages.graph_uuid = page_tags.graph_uuid " "AND pages.page_uuid = page_tags.source_page_uuid " "WHERE page_tags.tag_title = ? " + "AND pages.password_hash IS NULL " "GROUP BY page_tags.graph_uuid, page_tags.source_page_uuid, page_tags.source_page_title, pages.short_id " "ORDER BY updated_at DESC;") tag-name))] @@ -316,11 +318,12 @@ "pages.short_id, " "MAX(page_refs.updated_at) AS updated_at " "FROM page_refs " - "LEFT JOIN pages " + "INNER JOIN pages " "ON pages.graph_uuid = page_refs.graph_uuid " "AND pages.page_uuid = page_refs.source_page_uuid " - "WHERE (lower(page_refs.target_page_title) = lower(?)) " - "OR (page_refs.target_page_name = lower(?)) " + "WHERE ((lower(page_refs.target_page_title) = lower(?)) " + "OR (page_refs.target_page_name = lower(?))) " + "AND pages.password_hash IS NULL " "GROUP BY page_refs.graph_uuid, page_refs.source_page_uuid, page_refs.source_page_title, pages.short_id " "ORDER BY updated_at DESC;") ref-name @@ -345,7 +348,10 @@ rows (publish-common/get-sql-rows (publish-common/sql-exec sql (str "SELECT page_uuid, page_title, short_id, graph_uuid, updated_at, owner_username " - "FROM pages WHERE owner_username = ? ORDER BY updated_at DESC;") + "FROM pages " + "WHERE owner_username = ? " + "AND password_hash IS NULL " + "ORDER BY updated_at DESC;") username))] (publish-common/json-response {:user {:username username} :pages (map (fn [row] @@ -381,11 +387,16 @@ (= (nth parts 4 nil) "tagged_nodes") (let [rows (publish-common/get-sql-rows (publish-common/sql-exec sql - (str "SELECT graph_uuid, tag_page_uuid, tag_title, source_page_uuid, " - "source_page_title, source_block_uuid, source_block_content, " - "source_block_format, updated_at " - "FROM page_tags WHERE graph_uuid = ? AND tag_page_uuid = ? " - "ORDER BY updated_at DESC;") + (str "SELECT page_tags.graph_uuid, page_tags.tag_page_uuid, page_tags.tag_title, page_tags.source_page_uuid, " + "page_tags.source_page_title, page_tags.source_block_uuid, page_tags.source_block_content, " + "page_tags.source_block_format, page_tags.updated_at " + "FROM page_tags " + "INNER JOIN pages " + "ON pages.graph_uuid = page_tags.graph_uuid " + "AND pages.page_uuid = page_tags.source_page_uuid " + "WHERE page_tags.graph_uuid = ? AND page_tags.tag_page_uuid = ? " + "AND pages.password_hash IS NULL " + "ORDER BY page_tags.updated_at DESC;") graph-uuid page-uuid))] (publish-common/json-response {:tagged_nodes (map (fn [row] diff --git a/deps/publish/src/logseq/publish/publish.css b/deps/publish/src/logseq/publish/publish.css index b2b55ff1cd..b82b97b280 100644 --- a/deps/publish/src/logseq/publish/publish.css +++ b/deps/publish/src/logseq/publish/publish.css @@ -72,6 +72,10 @@ body { letter-spacing: 0.01em; } +.inline-flex { + display: inline-flex; +} + .publish-home { display: flex; align-items: center; @@ -474,6 +478,7 @@ a:hover { display: flex; gap: 4px; align-items: flex-start; + min-height: 26px; } .positioned-properties { @@ -486,7 +491,10 @@ a:hover { .positioned-properties.block-left, .positioned-properties.block-right { display: flex; align-items: center; - margin-top: 2px; +} + +.block-content > .positioned-properties.block-left { + align-self: center; } .positioned-properties.block-right { @@ -520,16 +528,40 @@ a:hover { display: inline-flex; align-items: center; justify-content: center; - width: 1rem; - height: 1rem; + width: 18px; + height: 18px; line-height: 1; - font-size: 1rem; + font-size: 18px; color: currentColor; } .property-icon svg { - width: 1rem; - height: 1rem; + width: 18px; + height: 18px; +} + +.positioned-properties .ls-icon-Backlog { + color: var(--rx-gray-05, #a8a29e); +} + +.positioned-properties .ls-icon-Todo { + color: var(--rx-gray-10, #78716c); +} + +.positioned-properties .ls-icon-InProgress50 { + color: var(--rx-yellow-08, #ca8a04); +} + +.positioned-properties .ls-icon-InReview { + color: var(--rx-blue-09, #1d4ed8); +} + +.positioned-properties .ls-icon-Done { + color: var(--rx-green-08, #16a34a); +} + +.positioned-properties .ls-icon-Cancelled { + color: var(--rx-red-08, #dc2626); } .property-value-with-icon { diff --git a/deps/publish/src/logseq/publish/publish.js b/deps/publish/src/logseq/publish/publish.js index 1366fb2f98..0a14b141bc 100644 --- a/deps/publish/src/logseq/publish/publish.js +++ b/deps/publish/src/logseq/publish/publish.js @@ -151,7 +151,7 @@ const getTablerExtIcon = (id) => { const renderTablerExtIcon = (el, id) => { const iconFn = getTablerExtIcon(id); if (!iconFn) return false; - const node = iconFn({ size: 14, stroke: 2 }); + const node = iconFn({ size: 18 }); if (!node) return false; el.textContent = ""; const domNode = reactNodeToDom(node); diff --git a/deps/publish/src/logseq/publish/render.cljs b/deps/publish/src/logseq/publish/render.cljs index 9a628b1c37..ecc9d2d344 100644 --- a/deps/publish/src/logseq/publish/render.cljs +++ b/deps/publish/src/logseq/publish/render.cljs @@ -11,7 +11,7 @@ [logseq.publish.model :as publish-model])) ;; Timestamp in milliseconds used for cache busting static assets. -(defonce version 1767194868810) +(defonce version 1777365821532) (def ref-regex (js/RegExp. "\\[\\[([0-9a-fA-F-]{36})\\]\\]|\\(\\(([0-9a-fA-F-]{36})\\)\\)" "g")) @@ -100,13 +100,19 @@ (defn- icon-span [icon] (when (and (map? icon) (string? (:id icon)) (not (string/blank? (:id icon)))) + (let [icon-type (:type icon) + icon-id (:id icon) + class-name (str "property-icon" + (when (and (= :tabler-icon icon-type) + (not (string/blank? icon-id))) + (str " ls-icon-" icon-id)))] [:span (cond-> - {:class "property-icon" + {:class class-name :data-icon-id (:id icon) :data-icon-type (name (:type icon))} (:color icon) - (assoc :style (str "color: " (:color icon) ";")))])) + (assoc :style (str "color: " (:color icon) ";")))]))) (defn- with-icon [icon nodes] @@ -255,6 +261,11 @@ (or (get property-type-by-ident prop-key) (get-in db-property/built-in-properties [prop-key :schema :type]))) +(defn property-hidden? + [prop-key property-hidden-by-ident] + (or (true? (get property-hidden-by-ident prop-key)) + (true? (get-in db-property/built-in-properties [prop-key :schema :hide?])))) + (defn page-ref->uuid [name name->uuid] (or (get name->uuid name) (get name->uuid (common-util/page-name-sanity-lc name)))) @@ -351,7 +362,7 @@ props) props (->> props (remove (fn [[k _]] - (true? (get (:property-hidden-by-ident ctx) k)))) + (property-hidden? k (:property-hidden-by-ident ctx)))) (map (fn [[k v]] (if (= k :block/tags) [k (filter-tags v entities)] @@ -445,12 +456,12 @@ [:div.positioned-property [:span.property-name (property-title k (:property-title-by-ident ctx))] [:span.property-value - (into [:span] (positioned-value-nodes v k ctx entities))]])] + (into [:span.inline-flex] (positioned-value-nodes v k ctx entities))]])] [:div {:class (str "positioned-properties " (name position))} (for [[k v] (sorted-properties props ctx)] [:span.positioned-property - (into [:span] (positioned-value-nodes v k ctx entities))])]))) + (into [:span.inline-flex] (positioned-value-nodes v k ctx entities))])]))) (def ^:private youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$") (def ^:private vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$") diff --git a/deps/publish/src/logseq/publish/routes.cljs b/deps/publish/src/logseq/publish/routes.cljs index 88f8138614..b24d09e01d 100644 --- a/deps/publish/src/logseq/publish/routes.cljs +++ b/deps/publish/src/logseq/publish/routes.cljs @@ -312,20 +312,6 @@ (p/let [tags (.json tags-resp)] (publish-common/json-response (js->clj tags :keywordize-keys true) 200)))))))))) -(defn handle-list-pages [env] - (let [^js do-ns (aget env "PUBLISH_META_DO") - do-id (.idFromName do-ns "index") - do-stub (.get do-ns do-id)] - (p/let [meta-resp (.fetch do-stub "https://publish/pages" #js {:method "GET"})] - (if-not (.-ok meta-resp) - (js/Response. - (publish-render/render-404-html) - #js {:headers (publish-common/merge-headers - #js {"content-type" "text/html; charset=utf-8"} - (publish-common/cors-headers))}) - (p/let [meta (.json meta-resp)] - (publish-common/json-response (js->clj meta :keywordize-keys true) 200)))))) - (defn handle-list-graph-pages-by-uuid [graph-uuid env] (if-not graph-uuid (publish-common/bad-request "missing graph uuid") @@ -620,6 +606,13 @@ (publish-render/render-page-html transit page-uuid refs-json tagged-nodes) #js {:headers headers})))))))))))))) +(defn- rewrite-request-path + [request new-path] + (let [request-url (js/URL. (.-url request)) + new-url (js/URL. (str (.-origin request-url) new-path))] + (set! (.-search new-url) (.-search request-url)) + (js/Request. (str new-url) request))) + (defn ^:large-vars/cleanup-todo handle-fetch [request env] (let [url (js/URL. (.-url request)) path (.-pathname url) @@ -669,9 +662,6 @@ (and (= path "/pages") (= method "POST")) (handle-post-pages request env) - (and (= path "/pages") (= method "GET")) - (handle-list-pages env) - (and (string/starts-with? path "/search/") (= method "GET")) (handle-graph-search request env) @@ -726,7 +716,9 @@ (js/Response. (.-body object) #js {:headers headers})))))))) - (and (string/starts-with? path "/p/") (= method "GET")) + (and (or (string/starts-with? path "/p/") + (string/starts-with? path "/s/")) + (= method "GET")) (let [parts (string/split path #"/") short-id (nth parts 2 nil)] (if (string/blank? short-id) @@ -744,11 +736,9 @@ (publish-common/not-found) (let [graph-uuid (aget row "graph_uuid") page-uuid (aget row "page_uuid") - location (str "/page/" graph-uuid "/" page-uuid)] - (js/Response. nil #js {:status 302 - :headers (publish-common/merge-headers - #js {"location" location} - (publish-common/cors-headers))}))))))))) + page-path (str "/page/" graph-uuid "/" page-uuid) + page-request (rewrite-request-path request page-path)] + (handle-page-html page-request env))))))))) (and (string/starts-with? path "/u/") (= method "GET")) (let [parts (string/split path #"/") diff --git a/deps/publish/test/logseq/publish/common_test.cljs b/deps/publish/test/logseq/publish/common_test.cljs new file mode 100644 index 0000000000..97669a1f47 --- /dev/null +++ b/deps/publish/test/logseq/publish/common_test.cljs @@ -0,0 +1,100 @@ +(ns logseq.publish.common-test + (:require [cljs.test :refer [async deftest is testing]] + [logseq.publish.common :as publish-common] + [promesa.core :as p])) + +(deftest merge-headers-overrides-and-preserves-values + (let [headers (publish-common/merge-headers #js {"a" "1" "keep" "ok"} + #js {"a" "2" "b" "3"})] + (is (= "2" (.get headers "a"))) + (is (= "3" (.get headers "b"))) + (is (= "ok" (.get headers "keep"))))) + +(deftest parse-meta-header-valid-and-invalid-json + (testing "valid json meta header is parsed into keywordized map" + (let [request (js/Request. "https://publish.example/pages" + #js {:headers #js {"x-publish-meta" "{\"content_hash\":\"h\",\"graph\":\"g\",\"page_uuid\":\"p\"}"}}) + meta (publish-common/parse-meta-header request)] + (is (= "h" (:content_hash meta))) + (is (= "g" (:graph meta))) + (is (= "p" (:page_uuid meta))))) + (testing "invalid json returns nil" + (let [request (js/Request. "https://publish.example/pages" + #js {:headers #js {"x-publish-meta" "{not-json"}})] + (is (nil? (publish-common/parse-meta-header request)))))) + +(deftest valid-meta-requires-core-fields + (is (some? (publish-common/valid-meta? {:content_hash "h" :graph "g" :page_uuid "p"}))) + (is (nil? (publish-common/valid-meta? {:content_hash "h" :graph "g"}))) + (is (nil? (publish-common/valid-meta? nil)))) + +(deftest get-sql-rows-handles-supported-shapes + (is (= [] (publish-common/get-sql-rows nil))) + (is (= [{"a" 1}] + (js->clj (publish-common/get-sql-rows #js {:rows #js [#js {"a" 1}]}) + :keywordize-keys false))) + (let [row (js-obj)] + (aset row "rows" #js [#js {"b" 2}]) + (is (= [{"rows" [{"b" 2}]}] + (js->clj (publish-common/get-sql-rows #js [row]) + :keywordize-keys false))))) + +(deftest json-error-response-helpers-set-status + (async done + (-> (p/let [unauthorized (publish-common/unauthorized) + forbidden (publish-common/forbidden) + bad-request (publish-common/bad-request "bad") + not-found (publish-common/not-found) + unauthorized-body (.json unauthorized) + forbidden-body (.json forbidden) + bad-request-body (.json bad-request) + not-found-body (.json not-found)] + (is (= 401 (.-status unauthorized))) + (is (= 403 (.-status forbidden))) + (is (= 400 (.-status bad-request))) + (is (= 404 (.-status not-found))) + (is (= "unauthorized" (aget unauthorized-body "error"))) + (is (= "forbidden" (aget forbidden-body "error"))) + (is (= "bad" (aget bad-request-body "error"))) + (is (= "not found" (aget not-found-body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest normalize-etag-removes-double-quotes + (is (= "abc" (publish-common/normalize-etag "\"abc\""))) + (is (= "abc" (publish-common/normalize-etag "abc"))) + (is (nil? (publish-common/normalize-etag nil)))) + +(deftest encode-path-encodes-path-segments + (is (= "with%20space/plus%2Bsign" (publish-common/encode-path "with space/plus+sign")))) + +(deftest short-id-for-page-is-deterministic-and-fixed-length + (async done + (-> (p/let [a (publish-common/short-id-for-page "graph-1" "page-1") + b (publish-common/short-id-for-page "graph-1" "page-1") + c (publish-common/short-id-for-page "graph-1" "page-2")] + (is (= 10 (count a))) + (is (= a b)) + (is (not= a c)) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest hash-and-verify-password-roundtrip + (async done + (-> (p/let [hashed (publish-common/hash-password "secret-value") + ok? (publish-common/verify-password "secret-value" hashed) + wrong? (publish-common/verify-password "wrong-value" hashed)] + (is (string? hashed)) + (is (true? ok?)) + (is (false? wrong?)) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest verify-password-rejects-invalid-hash-format + (is (false? (publish-common/verify-password "secret" "not-a-valid-hash")))) diff --git a/deps/publish/test/logseq/publish/render_test.cljs b/deps/publish/test/logseq/publish/render_test.cljs new file mode 100644 index 0000000000..723d1d841f --- /dev/null +++ b/deps/publish/test/logseq/publish/render_test.cljs @@ -0,0 +1,27 @@ +(ns logseq.publish.render-test + (:require [cljs.test :refer [deftest is testing]] + [logseq.publish.render :as render])) + +(deftest entity-properties-filters-explicitly-hidden-properties + (testing "property is filtered when ctx marks it hidden" + (let [entity {:user.property/secret "value"} + ctx {:property-hidden-by-ident {:user.property/secret true}} + result (render/entity-properties entity ctx {})] + (is (nil? (get result :user.property/secret)))))) + +(deftest entity-properties-filters-built-in-hidden-properties + (testing "built-in properties with :hide? true are filtered without property entity metadata" + (let [entity {:logseq.property/created-from-property "some-value"} + ctx {:property-hidden-by-ident {}} + result (render/entity-properties entity ctx {})] + (is (nil? (get result :logseq.property/created-from-property)))))) + +(deftest filter-tags-removes-built-in-tag-values + (testing "built-in class keyword tags are removed" + (let [result (render/filter-tags [:logseq.class/Tag :user.property/custom] {})] + (is (= [:user.property/custom] result)))) + (testing "built-in class entities are removed" + (let [entities {1 {:db/ident :logseq.class/Tag} + 2 {:db/ident :user.property/custom}} + result (render/filter-tags [1 2] entities)] + (is (= [2] result))))) diff --git a/deps/publish/test/logseq/publish/routes_test.cljs b/deps/publish/test/logseq/publish/routes_test.cljs new file mode 100644 index 0000000000..a202cbfb72 --- /dev/null +++ b/deps/publish/test/logseq/publish/routes_test.cljs @@ -0,0 +1,393 @@ +(ns logseq.publish.routes-test + (:require [cljs.test :refer [async deftest is testing]] + [logseq.common.authorization :as authorization] + [logseq.publish.common :as publish-common] + [logseq.publish.routes :as routes] + [promesa.core :as p])) + +(defn- json-response + [data] + (js/Response. + (js/JSON.stringify data) + #js {:status 200 + :headers #js {"content-type" "application/json"}})) + +(defn- short-url-env + [] + (let [do-stub #js {:fetch (fn [url _opts] + (cond + (= "https://publish/short/abc123" url) + (js/Promise.resolve + (json-response + #js {:page #js {"graph_uuid" "graph-1" + "page_uuid" "page-1"}})) + + (re-matches #"https://publish/pages/[^/]+/[^/]+/password" url) + (js/Promise.resolve (json-response #js {})) + + (re-matches #"https://publish/pages/[^/]+/[^/]+" url) + (js/Promise.resolve + (json-response + #js {"content_hash" "etag-1" + "r2_key" "publish/graph-1/page-1.transit"})) + + :else + (js/Promise.resolve + (js/Response. "not found" #js {:status 404}))))} + do-ns #js {:idFromName (fn [_name] "index") + :get (fn [_id] do-stub)} + r2 #js {:get (fn [_key] + (js/Promise.resolve + #js {:arrayBuffer (fn [] + (let [payload (.encode (js/TextEncoder.) "{}")] + (js/Promise.resolve (.-buffer payload))))}))}] + #js {"PUBLISH_META_DO" do-ns + "PUBLISH_R2" r2})) + +(defn- empty-env [] + #js {}) + +(defn- json-error-response + [status message] + (js/Response. + (js/JSON.stringify #js {"error" message}) + #js {:status status + :headers #js {"content-type" "application/json"}})) + +(defn- ok-json-response + [data] + (js/Response. + (js/JSON.stringify data) + #js {:status 200 + :headers #js {"content-type" "application/json"}})) + +(defn- method-from-opts + [opts] + (or (some-> opts (aget "method")) + "GET")) + +(defn- permission-env + [route-dispatch] + (let [do-ns #js {:idFromName (fn [name] name) + :get (fn [id] + #js {:fetch (fn [url opts] + (js/Promise.resolve + (route-dispatch id url (method-from-opts opts))))})}] + #js {"PUBLISH_META_DO" do-ns})) + +(deftest short-url-does-not-redirect-to-uuid-url + (testing "short URL should not redirect to /page/:graph/:page" + (async done + (let [request (js/Request. "https://publish.example/p/abc123?password=s3cr3t") + env (short-url-env)] + (-> (p/let [response (routes/handle-fetch request env) + body (.text response)] + (is (= 200 (.-status response))) + (is (nil? (.get (.-headers response) "location"))) + (is (re-find #"" body)) + (is (not (re-find #"Page not found" body))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))))) + +(deftest legacy-s-short-url-is-supported + (testing "legacy /s/:short-id should render page html" + (async done + (let [request (js/Request. "https://publish.example/s/abc123") + env (short-url-env)] + (-> (p/let [response (routes/handle-fetch request env) + body (.text response)] + (is (= 200 (.-status response))) + (is (nil? (.get (.-headers response) "location"))) + (is (re-find #"" body)) + (is (not (re-find #"Page not found" body))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))))) + +(deftest page-uuid-route-is-still-supported + (testing "legacy /page/:graph-uuid/:page-uuid should still render page html" + (async done + (let [request (js/Request. "https://publish.example/page/3bc00ad3-f421-41e7-8c65-40861c298be5/6954ee2a-506b-4dd9-bd6d-0dc24db9c055") + env (short-url-env)] + (-> (p/let [response (routes/handle-fetch request env) + body (.text response)] + (is (= 200 (.-status response))) + (is (nil? (.get (.-headers response) "location"))) + (is (re-find #"" body)) + (is (not (re-find #"Page not found" body))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))))) + +(deftest options-route-returns-cors-no-content + (async done + (let [request (js/Request. "https://publish.example/any" + #js {:method "OPTIONS"})] + (-> (p/let [response (routes/handle-fetch request (empty-env))] + (is (= 204 (.-status response))) + (is (= "*" (.get (.-headers response) "access-control-allow-origin"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest home-route-renders-html + (async done + (let [request (js/Request. "https://publish.example/")] + (-> (p/let [response (routes/handle-fetch request (empty-env)) + body (.text response)] + (is (= 200 (.-status response))) + (is (re-find #"" body)) + (is (re-find #"Logseq Publish" body)) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest static-assets-return-content-types + (async done + (-> (p/let [css-resp (routes/handle-fetch (js/Request. "https://publish.example/static/publish.css") (empty-env)) + js-resp (routes/handle-fetch (js/Request. "https://publish.example/static/publish.js") (empty-env)) + ext-resp (routes/handle-fetch (js/Request. "https://publish.example/static/tabler.ext.js") (empty-env))] + (is (= "text/css; charset=utf-8" (.get (.-headers css-resp) "content-type"))) + (is (= "text/javascript; charset=utf-8" (.get (.-headers js-resp) "content-type"))) + (is (= "text/javascript; charset=utf-8" (.get (.-headers ext-resp) "content-type"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest short-url-missing-id-returns-bad-request + (async done + (-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/p/") (empty-env)) + body (.json response)] + (is (= 400 (.-status response))) + (is (= "missing short id" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest short-url-not-found-returns-not-found + (async done + (let [do-stub #js {:fetch (fn [_url _opts] + (js/Promise.resolve (js/Response. "nope" #js {:status 404})))} + do-ns #js {:idFromName (fn [_name] "index") + :get (fn [_id] do-stub)} + env #js {"PUBLISH_META_DO" do-ns}] + (-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/p/abc") env) + body (.json response)] + (is (= 404 (.-status response))) + (is (= "not found" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest asset-route-validates-missing-or-invalid-id + (async done + (-> (p/let [missing-id-resp (routes/handle-fetch (js/Request. "https://publish.example/asset//") (empty-env)) + missing-id-body (.json missing-id-resp) + invalid-id-resp (routes/handle-fetch (js/Request. "https://publish.example/asset/g/noext") (empty-env)) + invalid-id-body (.json invalid-id-resp)] + (is (= 400 (.-status missing-id-resp))) + (is (= "missing asset id" (aget missing-id-body "error"))) + (is (= 400 (.-status invalid-id-resp))) + (is (= "invalid asset id" (aget invalid-id-body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest user-route-validates-missing-username + (async done + (-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/u/") (empty-env)) + body (.json response)] + (is (= 400 (.-status response))) + (is (= "missing username" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest post-pages-without-auth-is-unauthorized + (async done + (-> (p/let [request (js/Request. "https://publish.example/pages" + #js {:method "POST" + :headers #js {"content-type" "application/transit+json"} + :body "{}"}) + response (routes/handle-fetch request (empty-env)) + body (.json response)] + (is (= 401 (.-status response))) + (is (= "unauthorized" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest delete-page-without-auth-is-unauthorized + (async done + (-> (p/let [request (js/Request. "https://publish.example/pages/graph-1/page-1" + #js {:method "DELETE"}) + response (routes/handle-fetch request (empty-env)) + body (.json response)] + (is (= 401 (.-status response))) + (is (= "unauthorized" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done)))))) + +(deftest delete-page-owner-mismatch-is-forbidden + (async done + (let [env (permission-env + (fn [id url method] + (cond + (and (= id "index") + (= method "GET") + (= url "https://publish/pages/graph-1/page-1")) + (ok-json-response #js {"owner_sub" "owner-a"}) + + :else + (json-error-response 404 "not found")))) + request (js/Request. "https://publish.example/pages/graph-1/page-1" + #js {:method "DELETE" + :headers #js {"authorization" "Bearer token"}})] + (-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-b"})] + (routes/handle-fetch request env)) + body (.json response)] + (is (= 403 (.-status response))) + (is (= "forbidden" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest delete-page-owner-match-succeeds + (async done + (let [env (permission-env + (fn [id url method] + (cond + (and (= id "index") + (= method "GET") + (= url "https://publish/pages/graph-1/page-1")) + (ok-json-response #js {"owner_sub" "owner-a"}) + + (and (= id "index") + (= method "DELETE") + (= url "https://publish/pages/graph-1/page-1")) + (ok-json-response #js {"ok" true}) + + (and (= id "graph-1:page-1") + (= method "DELETE") + (= url "https://publish/pages/graph-1/page-1")) + (ok-json-response #js {"ok" true}) + + :else + (json-error-response 404 "not found")))) + request (js/Request. "https://publish.example/pages/graph-1/page-1" + #js {:method "DELETE" + :headers #js {"authorization" "Bearer token"}})] + (-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-a"})] + (routes/handle-fetch request env)) + body (.json response)] + (is (= 200 (.-status response))) + (is (true? (aget body "ok"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest delete-graph-owner-mismatch-is-forbidden + (async done + (let [env (permission-env + (fn [id url method] + (cond + (and (= id "index") + (= method "GET") + (= url "https://publish/pages/graph-1")) + (ok-json-response #js {"pages" #js [#js {"owner_sub" "owner-a" "page_uuid" "page-1"}]}) + + :else + (json-error-response 404 "not found")))) + request (js/Request. "https://publish.example/pages/graph-1" + #js {:method "DELETE" + :headers #js {"authorization" "Bearer token"}})] + (-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-b"})] + (routes/handle-fetch request env)) + body (.json response)] + (is (= 403 (.-status response))) + (is (= "forbidden" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest delete-graph-owner-match-succeeds + (async done + (let [env (permission-env + (fn [id url method] + (cond + (and (= id "index") + (= method "GET") + (= url "https://publish/pages/graph-1")) + (ok-json-response #js {"pages" #js [#js {"owner_sub" "owner-a" "page_uuid" "page-1"} + #js {"owner_sub" "owner-a" "page_uuid" "page-2"}]}) + + (and (= id "index") + (= method "DELETE") + (= url "https://publish/pages/graph-1")) + (ok-json-response #js {"ok" true}) + + (and (= method "DELETE") + (or (and (= id "graph-1:page-1") (= url "https://publish/pages/graph-1/page-1")) + (and (= id "graph-1:page-2") (= url "https://publish/pages/graph-1/page-2")))) + (ok-json-response #js {"ok" true}) + + :else + (json-error-response 404 "not found")))) + request (js/Request. "https://publish.example/pages/graph-1" + #js {:method "DELETE" + :headers #js {"authorization" "Bearer token"}})] + (-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-a"})] + (routes/handle-fetch request env)) + body (.json response)] + (is (= 200 (.-status response))) + (is (true? (aget body "ok"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) + +(deftest get-page-requires-correct-password-when-protected + (async done + (let [env (permission-env + (fn [id url method] + (cond + (and (= id "graph-1:page-1") + (= method "GET") + (= url "https://publish/pages/graph-1/page-1")) + (ok-json-response #js {"content_hash" "h-1" + "r2_key" "publish/graph-1/page-1.transit"}) + + (and (= id "index") + (= method "GET") + (= url "https://publish/pages/graph-1/page-1/password")) + (ok-json-response #js {"password_hash" "pbkdf2$sha256$90000$x$y"}) + + :else + (json-error-response 404 "not found")))) + request (js/Request. "https://publish.example/pages/graph-1/page-1")] + (-> (p/let [response (p/with-redefs [publish-common/verify-password (fn [_ _] (p/resolved false))] + (routes/handle-fetch request env)) + body (.json response)] + (is (= 401 (.-status response))) + (is (= "password required" (aget body "error"))) + (done)) + (p/catch (fn [error] + (is nil (str error)) + (done))))))) diff --git a/deps/publish/test/logseq/publish/test_runner.cljs b/deps/publish/test/logseq/publish/test_runner.cljs new file mode 100644 index 0000000000..d82bd39da6 --- /dev/null +++ b/deps/publish/test/logseq/publish/test_runner.cljs @@ -0,0 +1,23 @@ +(ns logseq.publish.test-runner + (:require [cljs.test :as ct] + [logseq.publish.common-test] + [logseq.publish.render-test] + [logseq.publish.routes-test] + [shadow.test :as st] + [shadow.test.env :as env])) + +(derive ::node ::ct/default) + +(defmethod ct/report [::node :end-run-tests] [m] + (if (ct/successful? m) + (js/process.exit 0) + (js/process.exit 1))) + +(defn ^:dev/after-load reset-test-data! [] + (-> (env/get-test-data) + (env/reset-test-data!))) + +(defn main [& _args] + (reset-test-data!) + (let [test-env (ct/empty-env ::node)] + (st/run-all-tests test-env nil))) diff --git a/deps/publish/worker/README.md b/deps/publish/worker/README.md index 094f7fd65d..416c9d2de3 100644 --- a/deps/publish/worker/README.md +++ b/deps/publish/worker/README.md @@ -32,8 +32,6 @@ metadata in a Durable Object backed by SQLite. - Deletes a published page - `DELETE /pages/:graph-uuid` - Deletes all pages for a graph -- `GET /pages` - - Lists metadata entries (from the index DO) - `GET /tag/:tag-name` - List all pages tagged with `:tag-name` - `GET /ref/:page-name` diff --git a/libs/SKILL.md b/libs/SKILL.md new file mode 100644 index 0000000000..cdbb076f0a --- /dev/null +++ b/libs/SKILL.md @@ -0,0 +1,133 @@ +--- +name: logseq-plugin-sdk +description: Build, debug, or review Logseq plugins with the `@logseq/libs` SDK (TypeScript/JavaScript, iframe/shadow sandboxed). Use when the task involves writing plugin entry code, registering slash/command/UI items, provideUI/provideStyle/provideModel, settings schema, macro renderers, DB-graph properties & tags, Datascript/DSL queries, experimental APIs, theme plugins, or the `logseq/*` CLJS facade generated under this package. +--- + +# Logseq Plugin SDK Skill + +This skill governs work inside `libs/` — the source of the npm package [`@logseq/libs`](./package.json) and its CLJS facade under [`cljs-sdk/`](./cljs-sdk). Use it whenever the user is authoring, upgrading, or debugging a Logseq plugin, or extending the SDK itself. + +## When to use + +Trigger this skill when the task mentions any of: + +- `@logseq/libs`, `logseq.App`, `logseq.Editor`, `logseq.DB`, `logseq.UI`, `logseq.Assets`, `logseq.Git`, `logseq.Experiments` +- `provideUI` / `provideStyle` / `provideModel` / `useSettingsSchema` / `onMacroRendererSlotted` +- `registerSlashCommand`, `registerBlockContextMenuItem`, `registerCommandPalette`, `registerUIItem` +- Plugin `package.json` `logseq` block, themes, `effect` plugins, iframe/shadow sandbox +- DB-graph properties, tags/classes, property idents (`:logseq.property/*`, `:plugin.property./*`) +- Datascript / DSL queries through `logseq.DB.q` / `logseq.DB.datascriptQuery` +- Regenerating the CLJS SDK (`yarn run generate:schema`, `bb libs:generate-cljs-sdk`) + +If the user is editing core Logseq app code (not a plugin), prefer the repo-root `AGENTS.md` instead. + +## Golden rules + +1. **Always `await logseq.ready(main)`** before touching any API. Most SDK calls are async RPC over postMessage. +2. **Detect graph mode** before using DB-only APIs: `await logseq.App.checkCurrentIsDbGraph()`. `IBatchBlock.properties` is **not** supported for DB graphs — use `Editor.upsertBlockProperty` / `upsertProperty` instead. +3. **Clean up listeners** in `logseq.beforeunload` (collect the `off` functions returned by every `onXxx` hook). +4. **Batch mutations** (`Editor.insertBatchBlock`) and **debounce** `DB.onChanged` / `onBlockChanged` handlers — they fire on every keystroke. +5. **Prefer CSS variables** (`--ls-primary-text-color`, `--ls-primary-background-color`, `--ls-border-color`, …) over hard-coded colors so plugins follow the active theme. +6. **Unique plugin id** in `package.json > logseq.id`; keep it lowercase-kebab. `main`/`entry` must point at a built HTML file. +7. **Experimental APIs (`logseq.Experiments.*`) are unstable** — only use when no stable API exists and document the reason. +8. **Idents are identity.** For built-in or cross-graph stable references, use idents (`:logseq.property/created-at`, `:plugin.property./`) instead of display names. + +## Canonical plugin skeleton + +```ts +import '@logseq/libs' + +const offHooks: Array<() => void> = [] + +async function main() { + logseq.useSettingsSchema([ + { key: 'enabled', type: 'boolean', default: true, title: 'Enabled', description: '' }, + ]) + + logseq.Editor.registerSlashCommand('My Command', async () => { + await logseq.Editor.insertAtEditingCursor('Hello from my plugin!') + }) + + offHooks.push( + logseq.DB.onChanged(({ blocks }) => { + // debounce in real code + }), + ) + + logseq.beforeunload(async () => { + offHooks.forEach((off) => off()) + }) +} + +logseq.ready(main).catch(console.error) +``` + +## Workflow + +1. **Scope the request.** Is it a new plugin, a change to an existing plugin, SDK-internal work, or the CLJS facade? +2. **Load the right reference file(s)** from [`./guides/`](./guides) (see table below) before proposing code. +3. **For SDK-internal changes**, open the matching TypeScript under [`./src/`](./src) (`LSPlugin.ts` for types, `LSPlugin.user.ts` for the proxy implementation, `modules/` for Experiments/Storage/Request). +4. **For CLJS facade changes**, regenerate with: + ```bash + yarn run generate:schema # dist/logseq-sdk-schema.json + bb libs:generate-cljs-sdk # target/generated-cljs/logseq/*.cljs + ``` + Non-proxy methods land in `logseq.core`; each `IXxxProxy` gets its own namespace (`logseq.app`, `logseq.editor`, …). +5. **Validate.** Build the plugin (`npm run build` / `parcel build`) and load it via Settings → Developer mode → `t p` → *Load unpacked plugin*. Use DevTools (`Cmd+Shift+I`) and `logseq.UI.showMsg` for quick feedback. +6. **Respect the package.json rules** (see [`guides/AGENTS.md`](./guides/AGENTS.md) §Configuration Fields). + +## Reference map (`./guides/`) + +Load these on demand — do not dump their full contents unless needed: + +| File | Load when… | +|------|------------| +| [`guides/AGENTS.md`](./guides/AGENTS.md) | Authoritative overview of SDK namespaces, `package.json > logseq` schema, theme plugins, UI injection, macro renderers, lifecycle. Start here for most plugin tasks. | +| [`guides/custom_theme_guide.md`](./guides/custom_theme_guide.md) | Building or reviewing Logseq theme plugins, custom theme CSS, `logseq.themes`, `provideTheme`, theme variables, light/dark mode styling, or UI selector/theme-token guidance. | +| [`guides/starter_guide.md`](./guides/starter_guide.md) | Bootstrapping a new plugin project (Node/TS toolchain, desktop dev-mode loading, hello-world). | +| [`guides/db_properties_guide.md`](./guides/db_properties_guide.md) | Conceptual model: file-graph vs DB-graph properties, schema vs values, tag/class modeling. | +| [`guides/db_properties_references.md`](./guides/db_properties_references.md) | API reference for `upsertProperty`, `upsertBlockProperty`, property schemas/types/cardinality. | +| [`guides/db_tag_property_idents_guide.md`](./guides/db_tag_property_idents_guide.md) | Ident naming rules (`:logseq.property/*`, `:logseq.class/*`, `:plugin.property./*`, `:plugin.class./*`) and when to use them. | +| [`guides/db_query_guide.md`](./guides/db_query_guide.md) | DSL (`logseq.DB.q`) vs Datascript (`logseq.DB.datascriptQuery`) queries, parameters, change watchers. | +| [`guides/experiments_api_guide.md`](./guides/experiments_api_guide.md) | `logseq.Experiments.*` — React/ReactDOM reuse, internal components, CLJS interop, custom fenced-code / route / sidebar / property / block-body renderers. | + +## Core API quick index + +Full code examples live in [`guides/AGENTS.md`](./guides/AGENTS.md) — use this table to jump to the right namespace: + +- `logseq.App` — info, graph, navigation, `registerUIItem`, `registerCommandPalette`, lifecycle hooks (`onCurrentGraphChanged`, `onThemeModeChanged`, `onRouteChanged`, `onMacroRendererSlotted`), `checkCurrentIsDbGraph`. +- `logseq.Editor` — slash & context-menu commands, block CRUD, `insertBatchBlock`, pages, cursor/selection, `upsertBlockProperty` / `getBlockProperties` (DB). +- `logseq.DB` — `q`, `datascriptQuery`, `onChanged`, `onBlockChanged`, `getFileContent` / `setFileContent`. +- `logseq.UI` — `showMsg`, `closeMsg`, `queryElementRect`, `queryElementById`. +- `logseq.Assets` — `listFilesOfCurrentGraph`, `makeSandboxStorage`, `makeUrl`, `builtInOpen`. +- `logseq.Git` — `execCommand`, `loadIgnoreFile`, `saveIgnoreFile` (**file graphs / desktop only**). +- `logseq.Experiments` — unstable; see the Experiments guide before using. +- Top-level — `provideUI`, `provideStyle`, `provideModel`, `useSettingsSchema`, `onSettingsChanged`, `updateSettings`, `showMainUI` / `hideMainUI` / `toggleMainUI` / `setMainUIInlineStyle`, `beforeunload`, `ready`. + +## Common pitfalls + +- Forgetting `await` — nearly every API is async. +- Using `IBatchBlock.properties` in a DB graph (silently ignored). +- Treating `block.content` as current — it is deprecated; use `block.title`. +- Registering the same `key` twice in `provideUI` / `provideStyle` without intending to replace. +- Hard-coding colors instead of `--ls-*` CSS variables. +- Leaking listeners (no cleanup in `beforeunload`). +- Shipping plugins without `logseq.id` or with a non-unique id. +- Assuming Git APIs exist on mobile / DB graphs. + +## When editing SDK source + +- Type definitions: [`src/LSPlugin.ts`](./src/LSPlugin.ts). Keep `IAppProxy`, `IEditorProxy`, `IDBProxy`, `IUIProxy`, `IAssetsProxy`, `IGitProxy`, `IExperimentsProxy` and the `ILSPluginUser` surface in sync. +- User proxy implementation: [`src/LSPlugin.user.ts`](./src/LSPlugin.user.ts). +- Modules: [`src/modules/`](./src/modules) (Experiments, Storage, Request). +- After changing the public surface, regenerate the CLJS facade (see Workflow step 4) and update [`CHANGELOG.md`](./CHANGELOG.md). +- Follow the repo commit style: short imperative subjects, optional scope (e.g. `enhance(libs): …`, `fix(libs): …`). + +## Resources + +- API docs: +- Samples: +- CLJS template: +- TS template: +- Discord: + diff --git a/libs/development-notes/experiments_api_guide.md b/libs/development-notes/experiments_api_guide.md deleted file mode 100644 index 72329db74f..0000000000 --- a/libs/development-notes/experiments_api_guide.md +++ /dev/null @@ -1,460 +0,0 @@ -# Logseq Experiments API Guide - -This guide covers the **experimental APIs** available in the Logseq Plugin SDK. These APIs provide advanced functionality for creating custom renderers, loading external scripts, and accessing internal utilities. - -> **⚠️ WARNING**: These are experimental features that may change at any time. Plugins using these APIs may not be supported on the Marketplace temporarily. - ---- - -## Overview - -The Experiments API is accessed via `logseq.Experiments` and provides: - -1. **React Integration** - Access to React and ReactDOM from the host -2. **Custom Renderers** - Register custom code block, route, and daemon renderers -3. **Component Access** - Access to internal Logseq components -4. **Utilities** - ClojureScript interop utilities (toClj, toJs, etc.) -5. **Script Loading** - Dynamic loading of external scripts -6. **Extension Enhancers** - Enhance libraries like KaTeX and CodeMirror - ---- - -## 1. React Integration - -Access React and ReactDOM from the Logseq host environment. - -### Properties - -#### `logseq.Experiments.React` - -Returns the React instance from the host scope. - -```typescript -const React = logseq.Experiments.React -``` - -#### `logseq.Experiments.ReactDOM` - -Returns the ReactDOM instance from the host scope. - -```typescript -const ReactDOM = logseq.Experiments.ReactDOM -``` - -### Example Usage - -```typescript -const React = logseq.Experiments.React -const ReactDOM = logseq.Experiments.ReactDOM - -// Use React to create components -const MyComponent = React.createElement('div', null, 'Hello from plugin!') -``` - ---- - -## 2. Components - -Access internal Logseq components for advanced UI integration. - -### `logseq.Experiments.Components.Editor` - -A page editor component that can render Logseq page content. - -**Type**: `(props: { page: string } & any) => any` - -**Parameters**: -- `page` (string): The page name to render - -```typescript -const Editor = logseq.Experiments.Components.Editor - -// Render a page editor -const editor = Editor({ page: 'My Page Name' }) -``` - ---- - -## 3. Utilities - -ClojureScript interop utilities for data conversion between JavaScript and ClojureScript. - -### `logseq.Experiments.Utils` - -Provides conversion utilities: - -#### `toClj(input: any)` - -Convert JavaScript data to ClojureScript data structures. - -```typescript -const cljData = logseq.Experiments.Utils.toClj({ key: 'value' }) -``` - -#### `jsxToClj(input: any)` - -Convert JSX/JavaScript objects to ClojureScript, preserving JSX structures. - -```typescript -const cljData = logseq.Experiments.Utils.jsxToClj(
Content
) -``` - -#### `toJs(input: any)` - -Convert ClojureScript data structures to JavaScript. - -```typescript -const jsData = logseq.Experiments.Utils.toJs(cljData) -``` - -#### `toKeyword(input: any)` - -Convert a string to a ClojureScript keyword. - -```typescript -const keyword = logseq.Experiments.Utils.toKeyword('my-key') -``` - -#### `toSymbol(input: any)` - -Convert a string to a ClojureScript symbol. - -```typescript -const symbol = logseq.Experiments.Utils.toSymbol('my-symbol') -``` - ---- - -## 4. Script Loading - -### `logseq.Experiments.loadScripts(...scripts: string[])` - -Dynamically load external scripts into the Logseq environment. - -**Parameters**: -- `scripts` (string[]): Array of script URLs or relative paths - -**Returns**: `Promise` - -**Behavior**: -- Relative paths are resolved using the plugin's resource path -- HTTP/HTTPS URLs are loaded directly -- Scripts are loaded in order - -```typescript -// Load external library -await logseq.Experiments.loadScripts( - 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js' -) - -// Load local script from plugin resources -await logseq.Experiments.loadScripts('./my-script.js') - -// Load multiple scripts -await logseq.Experiments.loadScripts( - 'https://cdn.example.com/lib1.js', - 'https://cdn.example.com/lib2.js', - './local-script.js' -) -``` - ---- - -## 5. Custom Renderers - -### 5.1 Fenced Code Renderer - -Register a custom renderer for code blocks with specific language tags. - -#### `logseq.Experiments.registerFencedCodeRenderer(lang: string, opts: object)` - -**Parameters**: -- `lang` (string): The language identifier for the code block (e.g., 'mermaid', 'chart') -- `opts` (object): - - `render` (function, required): Render function that receives props - - `edit` (boolean, optional): Whether the block is editable - - `before` (function, optional): Async function to run before rendering - - `subs` (string[], optional): Subscriptions to state changes - -**Render Props**: -- `content` (string): The content of the code block - -```typescript -// Register a custom code block renderer -logseq.Experiments.registerFencedCodeRenderer('my-chart', { - edit: false, - before: async () => { - // Load dependencies before rendering - await logseq.Experiments.loadScripts( - 'https://cdn.jsdelivr.net/npm/chart.js' - ) - }, - render: (props) => { - const React = logseq.Experiments.React - - return React.createElement('div', { - ref: (el) => { - if (el) { - // Parse content and render chart - const config = JSON.parse(props.content) - new Chart(el, config) - } - } - }) - } -}) -``` - -**Usage in Logseq**: -````markdown -```my-chart -{ - "type": "bar", - "data": { - "labels": ["A", "B", "C"], - "datasets": [{"data": [10, 20, 30]}] - } -} -``` -```` - -### 5.2 Daemon Renderer - -Register a renderer that runs continuously in the background (daemon). - -#### `logseq.Experiments.registerDaemonRenderer(key: string, opts: object)` - -**Parameters**: -- `key` (string): Unique identifier for the daemon renderer -- `opts` (object): - - `render` (function, required): Render function - - `sub` (string[], optional): Subscriptions to state changes - -```typescript -// Register a daemon renderer for persistent UI -logseq.Experiments.registerDaemonRenderer('my-status-bar', { - sub: ['ui/theme', 'ui/sidebar-open'], - render: (props) => { - const React = logseq.Experiments.React - - return React.createElement('div', { - style: { - position: 'fixed', - bottom: 0, - right: 0, - padding: '10px', - background: '#333', - color: '#fff' - } - }, 'Status: Active') - } -}) -``` - -### 5.3 Route Renderer - -Register a custom renderer for specific routes in Logseq. - -#### `logseq.Experiments.registerRouteRenderer(key: string, opts: object)` - -**Parameters**: -- `key` (string): Unique identifier for the route renderer -- `opts` (object): - - `path` (string, required): Route path (e.g., '/my-plugin-page') - - `render` (function, required): Render function - - `name` (string, optional): Display name for the route - - `subs` (string[], optional): Subscriptions to state changes - -```typescript -// Register a custom route -logseq.Experiments.registerRouteRenderer('my-custom-page', { - path: '/my-plugin-dashboard', - name: 'Dashboard', - subs: ['ui/theme'], - render: (props) => { - const React = logseq.Experiments.React - - return React.createElement('div', { - className: 'my-plugin-dashboard' - }, [ - React.createElement('h1', null, 'Plugin Dashboard'), - React.createElement('p', null, 'Custom content here') - ]) - } -}) - -// Navigate to the route -logseq.App.pushState('page', { name: 'my-plugin-dashboard' }) -``` - ---- - -## 6. Extension Enhancers - -Enhance external libraries that Logseq uses (like KaTeX for math rendering). - -### `logseq.Experiments.registerExtensionsEnhancer(type: string, enhancer: function)` - -**Parameters**: -- `type` ('katex' | 'codemirror'): The extension type to enhance -- `enhancer` (function): Async function that receives the library instance and can modify it - -**Returns**: `Promise` - -```typescript -// Enhance KaTeX with custom macros -logseq.Experiments.registerExtensionsEnhancer('katex', async (katex) => { - // Add custom KaTeX macros - katex.macros = { - ...katex.macros, - '\\RR': '\\mathbb{R}', - '\\NN': '\\mathbb{N}', - '\\ZZ': '\\mathbb{Z}' - } - - console.log('KaTeX enhanced with custom macros') -}) -``` - ---- - -## 7. Plugin Local Access - -### `logseq.Experiments.pluginLocal` - -Access the internal plugin instance (PluginLocal) for advanced operations. - -**Type**: `PluginLocal` - -```typescript -const pluginLocal = logseq.Experiments.pluginLocal - -// Access plugin-specific internal state -console.log('Plugin ID:', pluginLocal.id) -``` - ---- - -## 8. Advanced: Invoke Experimental Methods - -### `logseq.Experiments.invokeExperMethod(type: string, ...args: any[])` - -Directly invoke experimental methods from the host scope. - -**Parameters**: -- `type` (string): Method name (converted to snake_case) -- `...args`: Arguments to pass to the method - -**Returns**: `any` - -```typescript -// Invoke a custom experimental method -const result = logseq.Experiments.invokeExperMethod( - 'someExperimentalFeature', - arg1, - arg2 -) -``` - ---- - -## Complete Example: Custom Chart Renderer - -Here's a complete example combining multiple APIs: - -```typescript -import '@logseq/libs' - -async function main() { - console.log('Chart Plugin Loaded') - - // Register fenced code renderer for charts - logseq.Experiments.registerFencedCodeRenderer('chart', { - edit: false, - before: async () => { - // Load Chart.js before rendering - await logseq.Experiments.loadScripts( - 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js' - ) - }, - render: (props) => { - const React = logseq.Experiments.React - - return React.createElement('div', null, [ - React.createElement('canvas', { - ref: (canvas) => { - if (canvas && window.Chart) { - try { - const config = JSON.parse(props.content) - new window.Chart(canvas, config) - } catch (e) { - console.error('Chart rendering error:', e) - } - } - } - }) - ]) - } - }) -} - -logseq.ready(main).catch(console.error) -``` - -**Usage**: -````markdown -```chart -{ - "type": "line", - "data": { - "labels": ["Jan", "Feb", "Mar", "Apr"], - "datasets": [{ - "label": "Sales", - "data": [10, 20, 15, 30], - "borderColor": "rgb(75, 192, 192)" - }] - } -} -``` -```` - ---- - -## Best Practices - -1. **Check Host Scope**: Always ensure the host scope is accessible before using experimental APIs -2. **Error Handling**: Wrap experimental API calls in try-catch blocks -3. **Dependencies**: Load external scripts in `before` hooks to ensure they're ready -4. **Memory Management**: Clean up event listeners and subscriptions in daemon renderers -5. **Compatibility**: Test thoroughly as these APIs may change between Logseq versions -6. **Documentation**: Document which experimental APIs your plugin uses -7. **Marketplace**: Be aware that plugins using these APIs may not be accepted on the Marketplace - ---- - -## Limitations - -- **Experimental Status**: These APIs are not stable and may change without notice -- **Marketplace Support**: Plugins using experimental APIs may not be approved for the Marketplace -- **Security**: Be cautious when loading external scripts or accessing host scope -- **Performance**: Custom renderers can impact performance if not optimized -- **Compatibility**: Limited backwards compatibility guarantees - ---- - -## See Also - -- [Starter Guide](./starter_guide.md) - Getting started with plugin development -- [DB Properties Guide](./db_properties_guide.md) - Working with database properties -- [DB Query Guide](./db_query_guide.md) - Querying the Logseq database - ---- - -## Support - -For questions and issues: -- [Logseq Discord](https://discord.gg/logseq) - #plugin-dev channel -- [GitHub Discussions](https://github.com/logseq/logseq/discussions) -- [Plugin API Documentation](https://plugins-doc.logseq.com/) - -Remember: These are experimental features. Use at your own risk and always test thoroughly! diff --git a/libs/development-notes/AGENTS.md b/libs/guides/AGENTS.md similarity index 100% rename from libs/development-notes/AGENTS.md rename to libs/guides/AGENTS.md diff --git a/libs/guides/custom_theme_guide.md b/libs/guides/custom_theme_guide.md new file mode 100644 index 0000000000..d35fe54321 --- /dev/null +++ b/libs/guides/custom_theme_guide.md @@ -0,0 +1,595 @@ +# Custom Theme Plugin Guide + +This guide summarizes Logseq's current UI theme architecture and gives a practical workflow for building a Logseq **theme plugin** with `@logseq/libs`. + +It is based on the current repository structure and these theme-related entry points: + +- CSS load order: `tailwind.all.css` +- Design tokens and theme variables: `packages/ui/src/radix.css`, `packages/ui/src/radix-hsl.css`, `packages/ui/src/vars-classic.css`, `packages/ui/src/colors.css` +- Shared shui/Radix component CSS: `resources/css/shui.css` +- CodeMirror theme bridge: `resources/css/codemirror.lsradix.css` +- Frontend component CSS: `src/main/frontend/**/*.css` +- Theme runtime attributes: `src/main/frontend/components/theme.cljs`, `src/main/frontend/state.cljs`, `src/main/frontend/ui.cljs` +- Plugin theme registration/injection: `libs/src/LSPlugin.ts`, `libs/src/LSPlugin.core.ts`, `libs/src/LSPlugin.user.ts`, `libs/src/common.ts` + +## Mental model + +Logseq themes are mostly CSS-variable based. A theme plugin should first override the semantic variables that the app already consumes, and only then add targeted selector overrides for areas that are not fully tokenized. + +### CSS loading order + +The main stylesheet imports theme foundations before app component CSS: + +1. `packages/ui/src/radix.css` — Radix color scales as `--rx-*` variables, for example `--rx-gray-01` through `--rx-gray-12` and alpha variants. +2. `packages/ui/src/radix-hsl.css` — HSL forms such as `--rx-gray-01-hsl` for Tailwind/shui tokens. +3. `packages/ui/src/vars-classic.css` — Logseq semantic variables, layout variables, default light/dark values for `data-color=logseq`. +4. `packages/ui/src/colors.css` — accent palettes selected by `html[data-color=...]`, mapping `--lx-accent-*`, `--lx-gray-*`, shui tokens, and many `--ls-*` values. +5. `packages/ui/src/index.css` — Tailwind base. +6. `resources/css/shui.css` — shared UI components built on shui/Radix/Tailwind tokens. +7. Third-party CSS: Inter, PhotoSwipe, KaTeX, CodeMirror, PDF.js, Tabler, `codemirror.lsradix.css`. +8. `src/main/frontend/**/[!_]*.css` — component and extension CSS. +9. A selected plugin theme is injected later as a ``, so it can override earlier rules when specificity is equal or higher. + +### Runtime attributes and classes + +Theme CSS should scope by attributes on `html` and compatibility classes on `body`: + +| Hook | Set by | Meaning | +| --- | --- | --- | +| `html[data-theme="light"]` / `html[data-theme="dark"]` | `frontend.components.theme/container` | Current light/dark mode. Prefer this for theme CSS. | +| `html.dark` | same component | Tailwind dark-mode hook. | +| `body.light-theme`, `body.white-theme`, `body.dark-theme` | same component | Backward-compatible hooks for older custom CSS/themes. | +| `html[data-color="logseq"]` and other colors | same component | Accent color selection. A theme plugin can override or ignore this. | +| `html[data-font="serif"|"mono"]`, `html[data-font-global="true"]` | same component | Editor/global font preferences. Avoid fighting these unless your theme intentionally owns typography. | +| Platform classes such as `html.is-mobile`, `html.is-electron`, `html.is-mac`, `html.is-native-iphone` | `frontend.ui/inject-document-devices-envs!` | Useful for responsive/mobile-safe fixes. | + +### Plugin theme lifecycle + +A theme can be registered in either of two ways: + +1. Declaratively in `package.json > logseq.themes`. During package preparation, Logseq resolves relative `url` values to plugin resources, registers each theme, and shows them in the theme selector. +2. Programmatically with `logseq.provideTheme(theme)`. This is useful for generated themes or settings-driven variants. + +When a user selects a theme, Logseq: + +- removes the previously injected custom theme link; +- injects the selected theme CSS link into `document.head`; +- persists the selected theme separately for `light` and `dark` modes; +- updates `:plugin/selected-theme` and emits plugin app hooks such as `:theme-changed`. + +## Minimal theme plugin + +### Directory layout + +```text +logseq-my-theme/ +├── package.json +├── index.html +└── themes/ + ├── my-theme-light.css + └── my-theme-dark.css +``` + +### `package.json` + +Use a stable lowercase-kebab `logseq.id`. For a theme-only plugin, set `effect: true` and declare each CSS file in `themes`. + +```json +{ + "name": "logseq-my-theme", + "version": "0.1.0", + "description": "A custom Logseq theme pack", + "license": "MIT", + "main": "index.html", + "logseq": { + "id": "my-theme", + "main": "index.html", + "title": "My Theme", + "icon": "./icon.png", + "effect": true, + "themes": [ + { + "name": "My Theme Light", + "url": "./themes/my-theme-light.css", + "mode": "light", + "description": "Light variant of My Theme" + }, + { + "name": "My Theme Dark", + "url": "./themes/my-theme-dark.css", + "mode": "dark", + "description": "Dark variant of My Theme" + } + ] + }, + "devDependencies": { + "@logseq/libs": "^0.0.17" + } +} +``` + +### `index.html` + +Declarative themes do not need runtime code, but keeping a tiny entry file makes the plugin package explicit and easy to extend later. + +```html + + + + + My Logseq Theme + + + +``` + +### Programmatic variant with `provideTheme` + +If the theme URL or metadata is generated at runtime, use the SDK. Always wait for Logseq to be ready. + +```ts +import '@logseq/libs' + +async function main() { + logseq.provideTheme({ + name: 'My Generated Dark Theme', + url: './themes/generated-dark.css', + mode: 'dark', + description: 'Generated from plugin settings', + }) +} + +logseq.ready(main).catch(console.error) +``` + +## CSS strategy + +Start with variables. Add selector overrides only for UI that cannot be changed via variables. + +### Dark theme starter + +```css +/* themes/my-theme-dark.css */ + +html[data-theme="dark"] { + color-scheme: dark; + + /* Core backgrounds */ + --ls-primary-background-color: #111827; + --ls-secondary-background-color: #172033; + --ls-tertiary-background-color: #202b42; + --ls-quaternary-background-color: #2a3650; + --ls-table-tr-even-background-color: var(--ls-secondary-background-color); + --ls-slide-background-color: var(--ls-primary-background-color); + + /* Text */ + --ls-primary-text-color: #d6deeb; + --ls-secondary-text-color: #eef2ff; + --ls-title-text-color: #f8fafc; + --ls-left-sidebar-text-color: #cbd5e1; + + /* Links and refs */ + --ls-link-text-color: #7dd3fc; + --ls-link-text-hover-color: #bae6fd; + --ls-link-ref-text-color: var(--ls-link-text-color); + --ls-link-ref-text-hover-color: var(--ls-link-text-hover-color); + --ls-block-ref-link-text-color: #38bdf8; + --ls-tag-text-color: #93c5fd; + --ls-tag-text-hover-color: #bfdbfe; + + /* Borders, guidelines, focus */ + --ls-border-color: #334155; + --ls-secondary-border-color: #475569; + --ls-tertiary-border-color: rgb(148 163 184 / 0.18); + --ls-guideline-color: rgb(148 163 184 / 0.18); + --ls-focus-ring-color: rgb(56 189 248 / 0.45); + + /* Blocks and properties */ + --ls-block-properties-background-color: #1e293b; + --ls-page-properties-background-color: #1e293b; + --ls-block-bullet-color: #64748b; + --ls-block-bullet-border-color: #475569; + --ls-block-highlight-color: rgb(14 165 233 / 0.22); + --ls-a-chosen-bg: rgb(56 189 248 / 0.16); + --ls-menu-hover-color: var(--ls-a-chosen-bg); + + /* Selection, checkbox, quote, mark, inline code */ + --ls-selection-background-color: rgb(56 189 248 / 0.32); + --ls-selection-text-color: #f8fafc; + --ls-page-checkbox-color: #64748b; + --ls-page-checkbox-border-color: #475569; + --ls-page-blockquote-color: var(--ls-primary-text-color); + --ls-page-blockquote-bg-color: #172033; + --ls-page-blockquote-border-color: #38bdf8; + --ls-page-mark-color: #111827; + --ls-page-mark-bg-color: #fde68a; + --ls-page-inline-code-color: #e0f2fe; + --ls-page-inline-code-bg-color: #0f172a; + + /* Scrollbars and notifications */ + --ls-scrollbar-foreground-color: rgb(148 163 184 / 0.35); + --ls-scrollbar-background-color: rgb(15 23 42 / 0.35); + --ls-scrollbar-thumb-hover-color: rgb(148 163 184 / 0.55); + --ls-notification-background: #1e293b; + --ls-notification-text-color: #f8fafc; + + /* shui/Tailwind HSL tokens: use space-separated HSL channels, not hsl(). */ + --background: 222 47% 11%; + --foreground: 210 40% 96%; + --card: 222 47% 13%; + --card-foreground: 210 40% 96%; + --popover: 222 47% 10%; + --popover-foreground: 210 40% 96%; + --primary: 199 89% 48%; + --primary-foreground: 210 40% 98%; + --secondary: 217 33% 18%; + --secondary-foreground: 210 40% 96%; + --muted: 217 33% 16%; + --border: 217 33% 24%; + --input: 217 33% 24%; + --ring: 199 89% 48%; + --accent: 199 89% 48%; + --accent-foreground: 210 40% 98%; +} + +html[data-theme="dark"] body { + background: var(--ls-primary-background-color); + color: var(--ls-primary-text-color); +} + +html[data-theme="dark"] ::selection { + background: var(--ls-selection-background-color); + color: var(--ls-selection-text-color); +} +``` + +### Light theme starter + +```css +/* themes/my-theme-light.css */ + +html[data-theme="light"] { + color-scheme: light; + + --ls-primary-background-color: #ffffff; + --ls-secondary-background-color: #f8fafc; + --ls-tertiary-background-color: #eef2f7; + --ls-quaternary-background-color: #e2e8f0; + + --ls-primary-text-color: #1e293b; + --ls-secondary-text-color: #0f172a; + --ls-title-text-color: #0f172a; + --ls-left-sidebar-text-color: #334155; + + --ls-link-text-color: #0369a1; + --ls-link-text-hover-color: #075985; + --ls-link-ref-text-color: var(--ls-link-text-color); + --ls-link-ref-text-hover-color: var(--ls-link-text-hover-color); + --ls-block-ref-link-text-color: #0284c7; + --ls-tag-text-color: #0369a1; + --ls-tag-text-hover-color: #075985; + + --ls-border-color: #cbd5e1; + --ls-secondary-border-color: #e2e8f0; + --ls-tertiary-border-color: rgb(15 23 42 / 0.08); + --ls-guideline-color: rgb(15 23 42 / 0.08); + --ls-focus-ring-color: rgb(14 165 233 / 0.35); + + --ls-block-properties-background-color: #f1f5f9; + --ls-page-properties-background-color: #f1f5f9; + --ls-block-bullet-color: #94a3b8; + --ls-block-bullet-border-color: #cbd5e1; + --ls-block-highlight-color: #e0f2fe; + --ls-a-chosen-bg: #e0f2fe; + --ls-menu-hover-color: var(--ls-a-chosen-bg); + + --ls-selection-background-color: #dbeafe; + --ls-selection-text-color: #0f172a; + --ls-page-checkbox-color: #94a3b8; + --ls-page-checkbox-border-color: #94a3b8; + --ls-page-blockquote-color: var(--ls-primary-text-color); + --ls-page-blockquote-bg-color: #f8fafc; + --ls-page-blockquote-border-color: #38bdf8; + --ls-page-mark-color: #0f172a; + --ls-page-mark-bg-color: #fef3c7; + --ls-page-inline-code-color: #0f172a; + --ls-page-inline-code-bg-color: #f1f5f9; + + --ls-scrollbar-foreground-color: rgb(15 23 42 / 0.12); + --ls-scrollbar-background-color: rgb(15 23 42 / 0.05); + --ls-scrollbar-thumb-hover-color: rgb(15 23 42 / 0.22); + + --background: 0 0% 100%; + --foreground: 222 47% 11%; + --card: 0 0% 100%; + --card-foreground: 222 47% 11%; + --popover: 0 0% 100%; + --popover-foreground: 222 47% 11%; + --primary: 199 89% 40%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96%; + --secondary-foreground: 222 47% 11%; + --muted: 210 40% 96%; + --border: 214 32% 91%; + --input: 214 32% 91%; + --ring: 199 89% 40%; + --accent: 199 89% 40%; + --accent-foreground: 0 0% 100%; +} +``` + +## Important variable groups + +Prefer these variables before reaching for selectors. + +### Global layout and typography + +| Variable | Purpose | +| --- | --- | +| `--ls-page-text-size` | Base page text size under `#root`. | +| `--ls-page-title-size` | Page title size. Mobile may override title layout. | +| `--ls-main-content-max-width`, `--ls-main-content-max-width-wide` | Main content width. | +| `--ls-font-family` | Global app font when not overridden by user font settings. | +| `--ls-scrollbar-width` | Custom scrollbar width. | +| `--ls-border-radius-low`, `--ls-border-radius-medium` | Classic radius tokens. | +| `--ls-headbar-height`, `--ls-headbar-inner-top-padding` | Header sizing. | +| `--ls-left-sidebar-width`, `--ls-left-sidebar-sm-width`, `--ls-left-sidebar-nav-btn-size` | Left sidebar sizing. | + +### Logseq semantic colors + +| Variable | Purpose | +| --- | --- | +| `--ls-primary-background-color` | Main background. | +| `--ls-secondary-background-color` | Secondary surfaces, editors, menus. | +| `--ls-tertiary-background-color` | Nested/raised surfaces. | +| `--ls-quaternary-background-color` | Active/hover surfaces. | +| `--ls-primary-text-color`, `--ls-secondary-text-color`, `--ls-title-text-color` | Body, stronger text, titles. | +| `--ls-border-color`, `--ls-secondary-border-color`, `--ls-tertiary-border-color` | Borders. | +| `--ls-guideline-color` | Block indentation guide lines. | +| `--ls-active-primary-color`, `--ls-active-secondary-color` | Active states. | +| `--ls-a-chosen-bg`, `--ls-menu-hover-color` | Chosen menu/list item backgrounds. | +| `--ls-focus-ring-color` | Focus rings. | + +### Links, references, tags, and blocks + +| Variable | Purpose | +| --- | --- | +| `--ls-link-text-color`, `--ls-link-text-hover-color` | General links. | +| `--ls-link-ref-text-color`, `--ls-link-ref-text-hover-color` | Page references. | +| `--ls-block-ref-link-text-color` | Block references. | +| `--ls-tag-text-color`, `--ls-tag-text-hover-color`, `--ls-tag-text-opacity`, `--ls-tag-text-hover-opacity` | Tags. | +| `--ls-block-bullet-color`, `--ls-block-bullet-border-color`, `--ls-block-bullet-active-color` | Block bullets. | +| `--ls-block-highlight-color` | Block highlight. | +| `--ls-block-properties-background-color`, `--ls-page-properties-background-color` | Property panels. | + +### Content tokens + +| Variable | Purpose | +| --- | --- | +| `--ls-selection-background-color`, `--ls-selection-text-color` | Text selection. | +| `--ls-page-checkbox-color`, `--ls-page-checkbox-border-color` | Markdown task checkboxes/radios. | +| `--ls-page-blockquote-color`, `--ls-page-blockquote-bg-color`, `--ls-page-blockquote-border-color` | Blockquotes. | +| `--ls-page-mark-color`, `--ls-page-mark-bg-color` | Highlight/mark text. | +| `--ls-page-inline-code-color`, `--ls-page-inline-code-bg-color` | Inline code. | +| `--ls-table-tr-even-background-color` | Alternating table rows. | +| `--ls-cloze-text-color` | Cloze text. | +| `--ls-slide-background-color` | Slide mode background. | + +### shui/Tailwind HSL tokens + +Newer shared UI components use HSL channel tokens, often through Tailwind classes such as `bg-background`, `bg-popover`, `border`, `text-foreground`, and `bg-primary`. + +Set these as space-separated HSL channels: + +```css +html[data-theme="dark"] { + --background: 222 47% 11%; + --foreground: 210 40% 96%; + --card: 222 47% 13%; + --card-foreground: 210 40% 96%; + --popover: 222 47% 10%; + --popover-foreground: 210 40% 96%; + --primary: 199 89% 48%; + --primary-foreground: 210 40% 98%; + --secondary: 217 33% 18%; + --secondary-foreground: 210 40% 96%; + --muted: 217 33% 16%; + --border: 217 33% 24%; + --input: 217 33% 24%; + --ring: 199 89% 48%; + --accent: 199 89% 48%; + --accent-foreground: 210 40% 98%; +} +``` + +Do not write `--primary: hsl(199 89% 48%)`; consumers call `hsl(var(--primary))`. + +### `--lx-*` and `--rx-*` color scales + +- `--rx-*` variables are Radix-style raw palettes. They are broad and already available. +- `--lx-gray-01` through `--lx-gray-12` and `--lx-gray-*-alpha` represent the active neutral scale. +- `--lx-accent-01` through `--lx-accent-12` and `--lx-accent-*-alpha` represent the active accent scale. + +If your theme is a complete palette, map `--lx-gray-*` and `--lx-accent-*` as well as `--ls-*`. This makes Radix/shui-heavy UI more consistent: + +```css +html[data-theme="dark"] { + --lx-gray-01: #0f172a; + --lx-gray-02: #111827; + --lx-gray-03: #1e293b; + --lx-gray-04: #273449; + --lx-gray-05: #334155; + --lx-gray-06: #475569; + --lx-gray-07: #64748b; + --lx-gray-08: #94a3b8; + --lx-gray-09: #cbd5e1; + --lx-gray-10: #dbeafe; + --lx-gray-11: #e2e8f0; + --lx-gray-12: #f8fafc; + + --lx-accent-09: #0ea5e9; + --lx-accent-10: #0284c7; + --lx-accent-11: #38bdf8; + --lx-accent-12: #e0f2fe; +} +``` + +## UI selector map + +Use this as a last-mile map after variables. Keep overrides narrow and prefer `:where(...)` to avoid specificity wars. + +### App shell and layout + +| Area | Useful selectors | Notes | +| --- | --- | --- | +| Root/app | `#root`, `#app-container`, `#root-container.theme-container`, `main.theme-container-inner` | `#root` uses `--ls-page-text-size`; `main.theme-container-inner` defines `--left-sidebar-bg-color`. | +| Main content | `#main-container`, `#main-content`, `#main-content-container`, `.page-blocks-inner` | Avoid hard-coded viewport hacks; test desktop and mobile. | +| Header | `.cp__header`, `.head`, `.button`, `.ui__button` | Mostly tokenized through shui/Tailwind variables. | +| Left sidebar | `.left-sidebar-inner`, `.sidebar-header-container`, `.sidebar-contents-container`, `.sidebar-content-group`, `.sidebar-navigations` | Prefer `--left-sidebar-bg-color`, `--ls-left-sidebar-text-color`, and `--ls-left-sidebar-width`. | +| Right sidebar | `#right-sidebar`, `.cp__right-sidebar-inner`, `.sidebar-item`, `.references-blocks-item` | Nested reference cards may need extra contrast. | + +### Pages, blocks, and editor + +| Area | Useful selectors | Notes | +| --- | --- | --- | +| Page title | `.ls-page-title`, `.page-title`, `.journal-title`, `.page-title-sizer-wrapper` | Prefer `--ls-page-title-size` and title text variables. | +| Blocks | `.ls-block`, `.block-main-container`, `.block-content-wrapper`, `.block-content`, `.block-body` | Avoid altering core layout unless necessary. | +| Block tree guides | `.block-children-container`, `.block-children`, `.block-children-left-border` | Governed by `--ls-guideline-color` and related border variables. | +| Bullets/control | `.block-control-wrap`, `.block-control`, `.bullet-container`, `.bullet-link-wrap` | Governed by bullet variables; keep hit areas accessible. | +| Page refs | `.page-ref`, `.page-reference`, `.breadcrumb` | Use link/reference variables first. | +| Block refs | `.block-ref`, `.block-ref-no-title`, `.open-block-ref-link` | Use `--ls-block-ref-link-text-color`, property surface variables. | +| Properties | `.block-properties`, `.page-properties`, `.property-value-inner`, `.property-key` | Use property background variables. | +| Editor textarea | `.editor-inner textarea`, `.edit-input`, `.non-block-editor textarea` | Background usually comes from `--ls-secondary-background-color`. | +| Autocomplete/slash popup | `#ui__ac-inner`, `.menu-link`, `.absolute-modal[data-modal-name]`, `.cp__commands-slash` | Use menu hover and popover tokens. | + +### Shared UI, dialogs, command palette + +| Area | Useful selectors | Notes | +| --- | --- | --- | +| Buttons | `.ui__button`, `.button`, `.ui__toggle`, `.ui__toggle-background-on`, `.ui__toggle-background-off` | Set `--primary`, `--primary-foreground`, `--accent`, `--ring`. | +| Dialogs/modals | `.ui__modal`, `.ui__modal-panel`, `.ui__dialog-overlay`, `.ui__dialog-content`, `.ui__alert-dialog-content` | `--ls-modal-overlay-gradient-start/end` can control modal overlays. | +| Dropdowns/popovers | `.ui__dropdown-menu-content`, `.ui__popover-content`, `div[data-radix-menu-content]`, `div[data-radix-popper-content-wrapper]` | Usually driven by `--popover`, `--border`, `--accent`, `--lx-popover-bg`. | +| Select/calendar | `.ui__select-content`, `.ui__calendar`, `.rc-datepicker` | shui tokens and `--accent` matter. | +| Notifications | `.ui__notifications .notification-area` | Use `--ls-notification-background`, `--ls-notification-text-color`. | +| Command palette | `.cp__cmdk` | Border and hint colors use `--ls-border-color`, `--lx-gray-*`, `--accent`. | +| Context menu | `#custom-context-menu`, `.ls-context-menu-content`, `.menu-links-wrapper` | Popover/menu variables usually cover it. | + +### Extensions and special surfaces + +| Area | Useful selectors/variables | Notes | +| --- | --- | --- | +| CodeMirror | `.cm-s-lsradix`, `.cm-s-lsradix.cm-s-dark`, `.cm-s-lsradix.cm-s-light`, `.CodeMirror-*`, `.cm-*` | `resources/css/codemirror.lsradix.css` already bridges many `--lx-*`/`--ls-*` variables. Override only syntax colors if needed. | +| PDF | `.extensions__pdf-container`, `.extensions__pdf-toolbar`, `.extensions__pdf-outline`, `--ph-highlight-color-*`, `--ph-link-color`, `--ph-view-container-width`, `--lx-pdf-container-dark-bg` | PDF highlight colors are `--ph-*`, not `--ls-highlight-color-*`. | +| Graph | `.graph-layout`, `.graph-filters`, graph extension CSS under `src/main/frontend/extensions/graph.css` | Some graph styles are canvas/SVG-driven; inspect DOM before overriding. | +| Whiteboard/tldraw | `.tl-container`, `.tl-button`, `--ls-wb-stroke-color-default`, `--ls-wb-background-color-default`, `--ls-wb-text-color-default` | Accent colors in `colors.css` set default whiteboard tokens. | +| Tables | `.table-wrapper`, `.table-auto`, table rows | Start with `--ls-table-tr-even-background-color`, borders, text tokens. | +| Cards/flashcards | `.ls-card`, `.ui__dialog-content[label=flashcards__cp]` | Check dialog contrast and card minimum sizes. | +| Plugins UI | `.cp__plugins-page`, `.cp__themes-installed` | Useful when styling the installed theme picker itself. | + +## Scoped overrides: examples + +### Keep overrides mode-specific + +```text +html[data-theme="dark"] :where(.cp__right-sidebar-inner .references-blocks-item) { + background: color-mix(in srgb, var(--ls-secondary-background-color) 88%, white 12%); +} +``` + +### Prefer low specificity for optional polish + +```text +html[data-theme="light"] :where(.page-reference:hover) { + background: rgb(14 165 233 / 0.12); +} +``` + +### Avoid global resets + +Avoid rules like this in a theme plugin: + +```css +/* Avoid */ +* { + transition: all 200ms ease; +} +``` + +They can slow editing, affect CodeMirror/PDF.js, and break subtle interaction states. + +## Mobile and desktop considerations + +- Test `html.is-mobile`, `html.is-native-iphone`, `html.is-native-android`, and `html.is-electron` layouts if your theme changes spacing or header/sidebar dimensions. +- Respect safe-area insets in mobile-specific layout. Existing CSS uses `env(safe-area-inset-*)` in some places. +- Avoid changing `-webkit-app-region` on Electron headers/PDF toolbars unless you are fixing a drag-region issue. +- Do not shrink block control or bullet hit areas below the existing touch-friendly sizes. +- Avoid fixed pixel widths for the main content unless they are variables such as `--ls-main-content-max-width`. + +## Assets and fonts + +Prefer local assets packaged with the plugin. Relative URLs inside the selected theme CSS are resolved relative to the CSS file by the browser. + +```css +@font-face { + font-family: "MyThemeText"; + src: url("../fonts/MyThemeText.woff2") format("woff2"); + font-display: swap; +} + +html[data-theme="dark"] { + --ls-font-family: "MyThemeText", Inter, ui-sans-serif, system-ui, sans-serif; +} +``` + +Be careful with remote `@import` URLs: they can slow startup, fail offline, and leak network requests from a local-first app. + +## Debugging workflow + +1. Enable Logseq Developer mode. +2. Load the unpacked theme plugin. +3. Open DevTools and inspect `document.documentElement`: + - `data-theme` + - `data-color` + - `data-font` + - platform classes +4. In the theme selector, choose each declared light/dark theme. +5. Verify the selected CSS appears as a `` in `document.head`. +6. Inspect computed variables on `html`, `body`, `.theme-container`, and the specific UI area you are styling. +7. If a variable is not taking effect, check whether the component is using `--lx-*`, shui HSL tokens, or a targeted class instead of `--ls-*`. + +## Validation checklist + +Before publishing a theme plugin, test at least these screens and states: + +- [ ] Light and dark mode selection; switching between modes keeps each custom theme selection. +- [ ] Default accent color and at least one non-default `data-color` accent. +- [ ] Page title, journal title, normal blocks, nested blocks, block refs, page refs, tags, properties. +- [ ] Editing state: textarea, slash command menu, autocomplete, date picker, block context menu. +- [ ] Left sidebar, right sidebar, search, command palette, settings dialogs. +- [ ] Notifications, confirmation dialogs, dropdowns, popovers, tooltips. +- [ ] Code blocks / CodeMirror, inline code, marks, blockquotes, tables. +- [ ] PDF viewer highlights and toolbar if PDF is in scope. +- [ ] Graph and whiteboard/tldraw surfaces if your palette changes accents broadly. +- [ ] Desktop Electron and at least one mobile/narrow viewport if the theme changes layout/spacing. +- [ ] High contrast of text against every background; visible focus rings; visible selected/hover states. +- [ ] Reduced-motion friendliness if you add animations. + +## Common pitfalls + +- Overriding only `--ls-*` while newer shui components still read `--background`, `--popover`, `--border`, `--primary`, or `--lx-*` variables. +- Writing HSL tokens as full CSS colors instead of channel values. +- Using high-specificity selectors or `!important` everywhere, making user custom CSS and future Logseq changes harder to coexist with. +- Styling by generated Tailwind class names instead of stable Logseq/Radix selectors and variables. +- Forgetting to scope a dark theme to `html[data-theme="dark"]` or a light theme to `html[data-theme="light"]`. +- Hard-coding external font/image URLs without offline fallback. +- Changing editor layout, block bullets, or mobile sidebars without testing touch interactions. + +## Quick reference: priority order for theme authors + +1. Set `--ls-*` semantic variables for Logseq-specific UI. +2. Set shui/Tailwind HSL tokens (`--background`, `--foreground`, `--popover`, `--border`, `--primary`, etc.). +3. Map `--lx-gray-*` and `--lx-accent-*` for newer shared UI consistency. +4. Add extension variables such as `--ph-*` and `--ls-wb-*` only if your theme covers PDF/whiteboard. +5. Add low-specificity, mode-scoped selector overrides for the remaining gaps. + + diff --git a/libs/development-notes/db_properties_guide.md b/libs/guides/db_properties_guide.md similarity index 100% rename from libs/development-notes/db_properties_guide.md rename to libs/guides/db_properties_guide.md diff --git a/libs/development-notes/db_properties_skill.md b/libs/guides/db_properties_references.md similarity index 100% rename from libs/development-notes/db_properties_skill.md rename to libs/guides/db_properties_references.md diff --git a/libs/development-notes/db_query_guide.md b/libs/guides/db_query_guide.md similarity index 100% rename from libs/development-notes/db_query_guide.md rename to libs/guides/db_query_guide.md diff --git a/libs/development-notes/db_tag_property_idents_notes.md b/libs/guides/db_tag_property_idents_guide.md similarity index 100% rename from libs/development-notes/db_tag_property_idents_notes.md rename to libs/guides/db_tag_property_idents_guide.md diff --git a/libs/guides/experiments_api_guide.md b/libs/guides/experiments_api_guide.md new file mode 100644 index 0000000000..db1f79224e --- /dev/null +++ b/libs/guides/experiments_api_guide.md @@ -0,0 +1,784 @@ +# Logseq Experiments API Guide + +This guide covers the **experimental APIs** exposed as `logseq.Experiments` in the Logseq Plugin SDK. + +These APIs are intentionally lower-level than the stable SDK. They let plugins: + +- reuse host React/ReactDOM +- render internal Logseq components +- convert between JS and ClojureScript data structures +- load scripts dynamically +- register custom renderers for fenced code, routes, sidebars, properties, and block bodies +- hook internal extensions such as KaTeX +- access host/plugin internals when absolutely necessary + +> **⚠️ Warning** +> +> Everything in `logseq.Experiments` is unstable. Signatures, render props, and behaviors may change without a normal deprecation window. Plugins using these APIs may be temporarily unsupported on the Marketplace. + +--- + +## Overview + +The current experimental surface includes: + +1. **React integration**: `React`, `ReactDOM` +2. **Internal components**: `Components.Editor` +3. **Interop utilities**: `Utils.toClj`, `toJs`, `jsxToClj`, `toKeyword`, `toSymbol` +4. **Script loading**: `loadScripts(...)` +5. **Renderer registration**: + - `registerFencedCodeRenderer(...)` + - `registerDaemonRenderer(...)` + - `registerRouteRenderer(...)` + - `registerHostedRenderer(...)` + - `registerSidebarRenderer(...)` + - `registerBlockPropertiesRenderer(...)` + - `registerBlockRenderer(...)` +6. **Extension enhancers**: `registerExtensionsEnhancer(...)` +7. **Host/plugin internals**: + - `pluginLocal` + - `ensureHostScope()` + - `invokeExperMethod(...)` + +--- + +## 1. React Integration + +Use the host's React runtime instead of bundling your own copy. + +### `logseq.Experiments.React` + +Returns the React instance from the host scope. + +```typescript +const React = logseq.Experiments.React +``` + +### `logseq.Experiments.ReactDOM` + +Returns the ReactDOM instance from the host scope. + +```typescript +const ReactDOM = logseq.Experiments.ReactDOM +``` + +### Example + +```typescript +const React = logseq.Experiments.React + +const MyComponent = React.createElement( + 'div', + { className: 'my-plugin-card' }, + 'Hello from a host React tree' +) +``` + +--- + +## 2. Components + +### `logseq.Experiments.Components.Editor` + +Renders Logseq page content using an internal page editor component. + +**Type** + +```typescript +(props: { page: string } & Record) => any +``` + +**Parameters** + +- `page`: page name to render + +```typescript +const Editor = logseq.Experiments.Components.Editor + +const preview = Editor({ page: 'My Page Name' }) +``` + +--- + +## 3. Utilities + +`logseq.Experiments.Utils` exposes host interop helpers. + +### `toClj(input: any)` + +Convert JavaScript data into ClojureScript data structures. + +```typescript +const cljData = logseq.Experiments.Utils.toClj({ key: 'value' }) +``` + +### `jsxToClj(input: any)` + +Convert JS/JSX-style input to ClojureScript while preserving JSX-ish structures better than a plain conversion. + +```typescript +const view = { type: 'div', props: { children: 'Content' } } +const cljView = logseq.Experiments.Utils.jsxToClj(view) +``` + +### `toJs(input: any)` + +Convert ClojureScript values back into plain JavaScript. + +```typescript +const jsData = logseq.Experiments.Utils.toJs(cljData) +``` + +### `toKeyword(input: any)` + +Convert a string into a ClojureScript keyword. + +```typescript +const keyword = logseq.Experiments.Utils.toKeyword('my-key') +``` + +### `toSymbol(input: any)` + +Convert a string into a ClojureScript symbol. + +```typescript +const symbol = logseq.Experiments.Utils.toSymbol('my-symbol') +``` + +--- + +## 4. Script Loading + +### `logseq.Experiments.loadScripts(...scripts: string[])` + +Dynamically load scripts into the host environment. + +**Parameters** + +- `scripts`: HTTP(S) URLs or relative plugin resource paths + +**Returns** + +```typescript +Promise +``` + +**Behavior** + +- relative paths are resolved against the current plugin resource root +- HTTP/HTTPS URLs are used as-is +- scripts are loaded in the given order + +```typescript +await logseq.Experiments.loadScripts( + 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js' +) + +await logseq.Experiments.loadScripts('./vendor/local-helper.js') + +await logseq.Experiments.loadScripts( + 'https://cdn.example.com/lib1.js', + 'https://cdn.example.com/lib2.js', + './local-script.js' +) +``` + +--- + +## 5. Custom Renderers + +Experimental renderers are where most of the newer APIs live. + +## 5.1 Fenced Code Renderer + +Register a custom renderer for fenced code blocks such as: + +````markdown +```my-lang +... +``` +```` + +### `logseq.Experiments.registerFencedCodeRenderer(lang, opts)` + +```text +registerFencedCodeRenderer( + lang: string, + opts: { + edit?: boolean + before?: () => Promise + subs?: string[] + render: (props: { content: string }) => any + } +): any +``` + +**Options** + +- `edit`: whether the fenced block remains editable +- `before`: async preload hook, usually for loading scripts/assets +- `subs`: experimental subscription list +- `render`: React renderer receiving `{ content }` + +```typescript +logseq.Experiments.registerFencedCodeRenderer('my-chart', { + edit: false, + before: async () => { + await logseq.Experiments.loadScripts( + 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js' + ) + }, + render: ({ content }) => { + const React = logseq.Experiments.React + + return React.createElement('canvas', { + ref: (canvas: HTMLCanvasElement | null) => { + if (!canvas || !window.Chart) return + + try { + const config = JSON.parse(content) + new window.Chart(canvas, config) + } catch (error) { + console.error('Chart renderer error', error) + } + }, + }) + }, +}) +``` + +## 5.2 Daemon Renderer + +Register a renderer that stays mounted in a global daemon container. + +### `logseq.Experiments.registerDaemonRenderer(key, opts)` + +```text +registerDaemonRenderer( + key: string, + opts: { + before?: () => Promise + subs?: string[] + render: (props: {}) => any + } +): any +``` + +**Notes** + +- use `subs`, not `sub` +- `before` is supported by the host even if older typings may not show it yet +- daemon renderers are useful for lightweight always-on UI, not large app shells + +```typescript +logseq.Experiments.registerDaemonRenderer('my-status-bar', { + subs: ['ui/theme'], + render: () => { + const React = logseq.Experiments.React + + return React.createElement( + 'div', + { + style: { + position: 'fixed', + right: 12, + bottom: 12, + padding: '6px 10px', + borderRadius: 8, + background: 'var(--ls-secondary-background-color)', + }, + }, + 'Plugin active' + ) + }, +}) +``` + +## 5.3 Route Renderer + +Register a custom route view. + +### `logseq.Experiments.registerRouteRenderer(key, opts)` + +```text +registerRouteRenderer( + key: string, + opts: { + path: string + name?: string + subs?: string[] + render: (props: {}) => any + } +): any +``` + +**Options** + +- `path`: route path, e.g. `'/my-plugin-dashboard'` +- `name`: optional display name; if omitted, the internal key is reused +- `subs`: experimental subscription list +- `render`: route component + +```typescript +logseq.Experiments.registerRouteRenderer('my-custom-page', { + path: '/my-plugin-dashboard', + name: 'Dashboard', + render: () => { + const React = logseq.Experiments.React + + return React.createElement('div', { className: 'my-plugin-dashboard' }, [ + React.createElement('h1', { key: 'title' }, 'Plugin Dashboard'), + React.createElement('p', { key: 'body' }, 'Custom content here'), + ]) + }, +}) +``` + +> Route navigation is handled by Logseq's router. In docs and examples, prefer describing the registered `path` rather than relying on page navigation APIs, which are not the same thing. + +## 5.4 Hosted Renderer + +Low-level API for host-managed render targets. + +Today, the main built-in consumer is the right sidebar, so most plugins should prefer `registerSidebarRenderer(...)` unless they specifically need the lower-level primitive. + +### `logseq.Experiments.registerHostedRenderer(key, opts)` + +```text +registerHostedRenderer( + key: string, + opts: { + title?: string + mode?: string + type?: string + subs?: string[] + render: (props: {}) => any + } +): any +``` + +**Options** + +- `title`: display title when the host surfaces the renderer +- `type`: host-specific placement type +- `mode`: host-specific placement mode +- `subs`: experimental subscription list +- `render`: React renderer + +The host currently passes the registered renderer record back into the render function in some placements. Treat that as implementation detail, not a stable contract. + +## 5.5 Sidebar Renderer + +Convenience wrapper over `registerHostedRenderer(...)` for right-sidebar tools. + +### `logseq.Experiments.registerSidebarRenderer(key, opts)` + +```text +registerSidebarRenderer( + key: string, + opts: { + title?: string + subs?: string[] + render: (props: {}) => any + [key: string]: any + } +): any +``` + +**Behavior** + +- your key is automatically namespaced internally as `_sidebar.${key}` +- `type` is forced to `'sidebar'` +- the renderer appears in the right-sidebar plugin menu + +```typescript +logseq.Experiments.registerSidebarRenderer('inspector', { + title: 'Inspector', + render: () => { + const React = logseq.Experiments.React + + return React.createElement('div', null, 'Hello from the sidebar renderer') + }, +}) +``` + +## 5.6 Block Properties Renderer + +Render custom UI inside a block's properties area. + +### `logseq.Experiments.registerBlockPropertiesRenderer(key, opts)` + +```text +type BlockPropertiesCondition = + | { has: string } + | { equals: [string, any] } + | { in: [string, any[]] } + | { not: BlockPropertiesCondition } + | { any: BlockPropertiesCondition[] } + | { all: BlockPropertiesCondition[] } + +type BlockPropertiesRendererProps = { + blockId: string + properties: Record +} + +registerBlockPropertiesRenderer( + key: string, + opts: { + when?: BlockPropertiesCondition | ((props: BlockPropertiesRendererProps) => boolean) + mode?: 'prepend' | 'append' | 'replace' + priority?: number + subs?: string[] + render: (props: BlockPropertiesRendererProps) => any + } +): any +``` + +**Behavior** + +- `when` may be omitted, a declarative condition, or a synchronous predicate +- `mode` controls placement in the properties area: + - `prepend`: before native properties + - `append`: after native properties + - `replace`: replace native properties UI +- higher `priority` wins for conflicts +- for `replace`, the highest-priority matching replace renderer wins +- for `prepend`/`append`, all matching renderers are rendered in priority order + +**Render props** + +- `blockId`: block UUID string +- `properties`: plain JS object keyed by property names without the leading `:` + +**Property serialization details** + +Before data is passed into plugins, Logseq normalizes some values: + +- keywords become strings like `'logseq.property/status'` +- UUIDs become strings +- entity references become small objects such as `{ uuid, title }` +- sets / collections of entity references become arrays of those objects + +```typescript +logseq.Experiments.registerBlockPropertiesRenderer('priority-pill', { + when: { has: 'priority' }, + mode: 'prepend', + priority: 10, + render: ({ properties }) => { + const React = logseq.Experiments.React + const priority = properties.priority + + if (!priority) return null + + return React.createElement( + 'span', + { + style: { + display: 'inline-flex', + marginRight: 8, + padding: '2px 8px', + borderRadius: 9999, + fontSize: 12, + background: 'var(--ls-tertiary-background-color)', + }, + }, + `Priority: ${priority}` + ) + }, +}) +``` + +## 5.7 Block Renderer + +Replace a block's main outline body with plugin UI. + +### `logseq.Experiments.registerBlockRenderer(key, opts)` + +```text +type BlockRendererChild = Record & { + children?: BlockRendererChild[] +} + +type BlockRendererProps = { + blockId: string + properties: Record + uuid?: string + page?: string + content?: string + format?: string + children?: BlockRendererChild[] +} + +registerBlockRenderer( + key: string, + opts: { + when?: (props: BlockRendererProps) => boolean + includeChildren?: boolean + priority?: number + subs?: string[] + render: (props: BlockRendererProps) => any + } +): any +``` + +**Behavior** + +- `when` must be a **synchronous predicate function** if provided +- declarative conditions are **not** supported here +- highest `priority` match wins +- when the plugin renderer is active, users can switch back to the native outline view via built-in UI on that block +- when `includeChildren` is `true`, Logseq passes a recursive child tree and hides native outline children while the plugin renderer is active + +**Render props** + +- `blockId`: block UUID string +- `uuid`: same block UUID +- `page`: page title +- `content`: block content/title text +- `format`: `'markdown'`, `'org'`, etc. +- `properties`: normalized property object +- `children`: recursive normalized child tree when `includeChildren` is enabled + +```typescript +logseq.Experiments.registerBlockRenderer('kanban-card', { + when: ({ properties }) => properties.view === 'kanban-card', + includeChildren: true, + priority: 20, + render: ({ content, children = [] }) => { + const React = logseq.Experiments.React + + return React.createElement('section', { className: 'my-kanban-card' }, [ + React.createElement('h3', { key: 'title' }, content || 'Untitled'), + React.createElement( + 'ul', + { key: 'children' }, + children.map((child, index) => + React.createElement('li', { key: child.uuid || index }, child.title || child.content) + ) + ), + ]) + }, +}) +``` + +--- + +## 6. Extension Enhancers + +### `logseq.Experiments.registerExtensionsEnhancer(type, enhancer)` + +Enhance host libraries such as KaTeX. + +```text +registerExtensionsEnhancer( + type: 'katex' | 'codemirror', + enhancer: (value: any) => Promise +): any +``` + +For `katex`, the host immediately invokes the enhancer if KaTeX is already present. + +```typescript +logseq.Experiments.registerExtensionsEnhancer('katex', async (katex) => { + katex.macros = { + ...katex.macros, + '\\RR': '\\mathbb{R}', + '\\NN': '\\mathbb{N}', + '\\ZZ': '\\mathbb{Z}', + } +}) +``` + +--- + +## 7. Host / Plugin Internals + +## 7.1 `logseq.Experiments.pluginLocal` + +Returns the internal `PluginLocal` instance for the current plugin. + +```typescript +const pluginLocal = logseq.Experiments.pluginLocal +console.log(pluginLocal.id) +``` + +Use this sparingly. It is intentionally internal. + +## 7.2 `logseq.Experiments.ensureHostScope()` + +Returns the host scope, currently `window.top`, after attempting an access check. + +```typescript +const host = logseq.Experiments.ensureHostScope() +``` + +This is mostly useful when you need direct access to host globals and understand the risks. + +## 7.3 `logseq.Experiments.invokeExperMethod(type, ...args)` + +Direct escape hatch for calling experimental host methods. + +```typescript +const result = logseq.Experiments.invokeExperMethod( + 'someExperimentalFeature', + arg1, + arg2 +) +``` + +`type` is normalized to snake_case before resolution. + +--- + +## 8. Complete Example: Fenced Code Renderer + +```typescript +import '@logseq/libs' + +async function main() { + logseq.Experiments.registerFencedCodeRenderer('chart', { + edit: false, + before: async () => { + await logseq.Experiments.loadScripts( + 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js' + ) + }, + render: ({ content }) => { + const React = logseq.Experiments.React + + return React.createElement('canvas', { + ref: (canvas: HTMLCanvasElement | null) => { + if (!canvas || !window.Chart) return + + try { + const config = JSON.parse(content) + new window.Chart(canvas, config) + } catch (error) { + console.error('Chart rendering error:', error) + } + }, + }) + }, + }) +} + +logseq.ready(main).catch(console.error) +``` + +**Usage** + +````markdown +```chart +{ + "type": "line", + "data": { + "labels": ["Jan", "Feb", "Mar", "Apr"], + "datasets": [{ + "label": "Sales", + "data": [10, 20, 15, 30], + "borderColor": "rgb(75, 192, 192)" + }] + } +} +``` +```` + +--- + +## 9. Complete Example: Block Properties Badge + +```typescript +import '@logseq/libs' + +async function main() { + logseq.Experiments.registerBlockPropertiesRenderer('task-status-chip', { + when: { + any: [ + { equals: ['status', 'todo'] }, + { equals: ['status', 'doing'] }, + ], + }, + mode: 'append', + priority: 5, + render: ({ properties }) => { + const React = logseq.Experiments.React + const value = properties.status + + return React.createElement( + 'span', + { + style: { + marginLeft: 8, + padding: '2px 8px', + borderRadius: 9999, + fontSize: 12, + background: 'var(--ls-secondary-background-color)', + }, + }, + `Status: ${value}` + ) + }, + }) +} + +logseq.ready(main).catch(console.error) +``` + +--- + +## 10. Best Practices + +1. **Prefer stable APIs first**. Only use `Experiments` when the stable SDK cannot solve the problem. +2. **Use host React**. Avoid bundling a second React runtime into the same tree. +3. **Keep `when` predicates synchronous**. This is especially important for `registerBlockRenderer(...)`. +4. **Use `before` to preload dependencies** instead of doing ad hoc script injection inside render. +5. **Treat `subs` as experimental**. Reactive semantics may change. +6. **Keep renderers lightweight**. Block and daemon renderers can affect overall app responsiveness. +7. **Handle bad input defensively**. Render props often contain user-authored content and properties. +8. **Document your experimental usage** in the plugin README so users understand the risk. +9. **Prefer `registerSidebarRenderer(...)` over raw hosted renderers** when your goal is a right-sidebar tool. +10. **Test against real graphs**. Property values, references, and child trees can vary a lot. + +--- + +## 11. Limitations and Notes + +- **Experimental status**: no stability guarantee +- **Marketplace support**: may be restricted temporarily +- **Security**: be careful with external scripts and direct host access +- **Performance**: custom renderers run inside the app UI, so poor implementations are noticeable +- **Typings may lag behavior**: some newer runtime options can land before every generated wrapper/type is refreshed + +### ClojureScript SDK note + +The generated ClojureScript wrapper namespace `com.logseq.experiments` currently includes wrappers for: + +- `load-scripts` +- `register-fenced-code-renderer` +- `register-daemon-renderer` +- `register-hosted-renderer` +- `register-sidebar-renderer` +- `register-route-renderer` +- `register-extensions-enhancer` + +At the time of writing, `register-block-properties-renderer` and `register-block-renderer` are not yet present in that generated wrapper, so ClojureScript plugins may need to call them via `invoke-exper-method` until the wrapper is regenerated. + +--- + +## See Also + +- [Starter Guide](./starter_guide.md) - getting started with plugin development +- [DB Properties Guide](./db_properties_guide.md) - working with database properties +- [DB Query Guide](./db_query_guide.md) - querying the Logseq database + +--- + +## Support + +For questions and issues: + +- [Logseq Discord](https://discord.gg/logseq) - `#plugin-dev` +- [GitHub Discussions](https://github.com/logseq/logseq/discussions) +- [Plugin API Documentation](https://plugins-doc.logseq.com/) + +Remember: these are experimental features. Use them carefully and test thoroughly. diff --git a/libs/development-notes/starter_guide.md b/libs/guides/starter_guide.md similarity index 100% rename from libs/development-notes/starter_guide.md rename to libs/guides/starter_guide.md diff --git a/libs/package.json b/libs/package.json index 29381bbe6b..e764ac7284 100644 --- a/libs/package.json +++ b/libs/package.json @@ -1,7 +1,7 @@ { "name": "@logseq/libs", "packageManager": "pnpm@10.33.0", - "version": "0.3.2", + "version": "0.3.3", "description": "Logseq SDK libraries", "main": "dist/lsplugin.user.js", "typings": "index.d.ts", diff --git a/libs/src/LSPlugin.core.ts b/libs/src/LSPlugin.core.ts index f06d44ec4a..288fdee08b 100644 --- a/libs/src/LSPlugin.core.ts +++ b/libs/src/LSPlugin.core.ts @@ -51,6 +51,46 @@ import { const debug = Debug('LSPlugin:core') const DIR_PLUGINS = 'plugins' +/** + * Compact one-line diff of two plain-object settings snapshots. + * Returns e.g. `foo: 1 -> 2, bar: "x" -> "y", +baz: true, -qux`. + * Returns null when no changes are detected. + */ +function diffSettings( + prev: Record | undefined, + next: Record | undefined +): string | null { + prev = prev || {} + next = next || {} + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]) + const parts: string[] = [] + const fmt = (v: any) => { + if (v === undefined) return 'undefined' + try { + const s = JSON.stringify(v) + return s && s.length > 80 ? s.slice(0, 77) + '...' : s + } catch (_e) { return String(v) } + } + for (const k of keys) { + const a = prev[k], b = next[k] + if (a === b) continue + if (!(k in prev)) { + parts.push(`+${k}: ${fmt(b)}`) + continue + } + if (!(k in next)) { + parts.push(`-${k}`) + continue + } + // deep equality fallback via JSON to skip ref-only changes + try { + if (JSON.stringify(a) === JSON.stringify(b)) continue + } catch (_e) { /* fallthrough */ } + parts.push(`${k}: ${fmt(a)} -> ${fmt(b)}`) + } + return parts.length ? parts.join(', ') : null +} + declare global { interface Window { LSPluginCore: LSPluginCore @@ -487,7 +527,12 @@ class PluginLocal extends EventEmitter< async _setupUserSettings(reload?: boolean) { const { _options } = this - const logger = (this._logger = new PluginLogger(`Loader:${this.debugTag}`)) + // Reuse the existing logger to preserve history across reloads; + // only update the tag. + if (!this._logger) { + this._logger = new PluginLogger(`Loader`) + } + const logger = this._logger if (_options.settings && !reload && this._disposeSettingsObserver) { return @@ -512,8 +557,9 @@ class PluginLocal extends EventEmitter< settings.replace(userSettings) } - const handler = async (a) => { - debug('Settings changed', this.debugTag, a) + const handler = async (a, b) => { + const changed = diffSettings(b, a) + if (changed) logger.debug('settings changed', changed) if (a) { invokeHostExportedApi('save_plugin_user_settings', this.id, a) @@ -533,7 +579,7 @@ class PluginLocal extends EventEmitter< this._disposeSettingsObserver = disposeSettingsObserver } catch (e) { debug('[load plugin user settings Error]', e) - logger?.error(e) + logger?.error('load user settings failed', e) } } @@ -580,6 +626,7 @@ class PluginLocal extends EventEmitter< packageConfigError = 'Can not resolve package config location' } else { debug('prepare package root', url) + this._logger?.debug('prepare package root', url) try { pkg = await invokeHostExportedApi('load_plugin_config', url) @@ -667,6 +714,7 @@ class PluginLocal extends EventEmitter< }) } catch (e) { debug('[save plugin ID Error] ', e) + this._logger?.warn('save plugin id failed', e) } } } @@ -689,6 +737,7 @@ class PluginLocal extends EventEmitter< } } catch (e) { debug('[prepare package effect Error]', e) + this._logger?.error('prepare package effect failed', e) } } } @@ -736,10 +785,14 @@ class PluginLocal extends EventEmitter< dirPathInstalled ) - entry = convertToLSPResource( - withFileProtocol(path.normalize(entryPath)), - this.dotPluginsRoot - ) + entry = withFileProtocol(path.normalize(entryPath)) + + if (!this._options.effect) { + entry = convertToLSPResource( + entry, + this.dotPluginsRoot + ) + } this._options.entry = entry } @@ -901,9 +954,14 @@ class PluginLocal extends EventEmitter< }> ) { if (this.pending || this.loaded) { + this._logger?.debug('load skipped', + this.pending ? '(pending)' : '(already loaded)') return } + const t0 = performance.now() + this._logger?.info('load:start', opts?.reload ? '(reload)' : '') + this._transitionStatus(PluginLocalLoadStatus.LOADING, [ PluginLocalLoadStatus.UNLOADED, PluginLocalLoadStatus.ERROR, @@ -922,16 +980,23 @@ class PluginLocal extends EventEmitter< await installPackageThemes.call(null) } - if (this.disabled || !this.options.entry) { + if (this.disabled) { + this._logger?.info('load:skip (disabled)') + return + } + if (!this.options.entry) { + this._logger?.info('load:skip (no entry - theme-only package)') return } this._ctx.emit('beforeload', this) await this._tryToNormalizeEntry() + this._logger?.debug('entry normalized', this.options.entry) this._caller = new LSPluginCaller(this) await this._caller.connectToChild() + this._logger?.debug('sandbox connected') const readyFn = () => { this._caller?.callUserModel(LSPMSG_READY, { pid: this.id }) @@ -950,8 +1015,10 @@ class PluginLocal extends EventEmitter< this._dispose(cleanInjectedScripts.bind(this)) this._ctx.emit('loadeded', this) + this._logger?.info( + `load:done in ${(performance.now() - t0).toFixed(1)}ms`) } catch (e) { - this.logger.error('load', e, true) + this.logger.error('load:failed', e, true) this.disposeRuntime().catch(null) this._status = PluginLocalLoadStatus.ERROR @@ -970,9 +1037,11 @@ class PluginLocal extends EventEmitter< async reload() { if (this.pending) { + this._logger?.debug('reload skipped (pending)') return } + this._logger?.info('reload:start') this._ctx.emit('beforereload', this) if (this.loaded) { @@ -981,6 +1050,7 @@ class PluginLocal extends EventEmitter< await this.load({ reload: true }) this._ctx.emit('reloaded', this) + this._logger?.info('reload:done') } /** @@ -988,6 +1058,7 @@ class PluginLocal extends EventEmitter< */ async unload(unregister: boolean = false) { if (this.pending) { + this._logger?.debug('unload skipped (pending)') return } @@ -997,6 +1068,7 @@ class PluginLocal extends EventEmitter< } if (unregister) { + this._logger?.info('unregister:start') await this.unload() await this.disposeRegistration() @@ -1004,9 +1076,11 @@ class PluginLocal extends EventEmitter< this._ctx.emit('unlink-plugin', this.id) } + this._logger?.info('unregister:done') return } + this._logger?.info('unload:start') try { const eventBeforeUnload = { unregister } @@ -1022,15 +1096,16 @@ class PluginLocal extends EventEmitter< ) this.emit('beforeunload', eventBeforeUnload) } catch (e) { - this.logger.error('beforeunload', e) + this.logger.error('beforeunload hook failed', e) } await this.disposeRuntime() } this.emit('unloaded') + this._logger?.info('unload:done') } catch (e) { - this.logger.error('unload', e) + this.logger.error('unload failed', e) } finally { this._status = PluginLocalLoadStatus.UNLOADED } @@ -1041,7 +1116,7 @@ class PluginLocal extends EventEmitter< try { fn && (await fn()) } catch (e) { - console.error(this.debugTag, 'dispose Error', e) + this._logger?.error('dispose failed', e) } } } @@ -1437,6 +1512,7 @@ class LSPluginCore if (loadErr) { debug('[Failed LOAD Plugin] #', pluginOptions) + pluginLocal.logger?.error('register: load failed', loadErr, true) this.emit('error', loadErr) @@ -1497,6 +1573,10 @@ class LSPluginCore const p = this.ensurePlugin(identity) await p.reload() } catch (e) { + try { + this.getPluginLogger(identity) + ?.error('reload failed', e) + } catch (_) { /* unknown plugin */ } debug(e) } } @@ -1538,12 +1618,14 @@ class LSPluginCore if (p.pending) return if (!p.disabled && p.loaded) return + p.logger?.info('enable:start') this.emit('beforeenable') p.settings?.set('disabled', false) await p.load() this.emit('enabled', p.id) + p.logger?.info('enable:done') } async disable(plugin: PluginLocalIdentity) { @@ -1551,12 +1633,14 @@ class LSPluginCore if (p.pending) return if (p.disabled && !p.loaded) return + p.logger?.info('disable:start') this.emit('beforedisable') p.settings?.set('disabled', true) await p.unload() this.emit('disabled', p.id) + p.logger?.info('disable:done') } async _hook(ns: string, type: string, payload?: any, pid?: string) { @@ -1637,6 +1721,28 @@ class LSPluginCore return p } + /** + * Return the {@link PluginLogger} of the given plugin (if any). + * Returns undefined when the plugin is unknown. + */ + getPluginLogger(id: PluginLocalIdentity) { + try { + return this.ensurePlugin(id)?.logger + } catch (_e) { + return undefined + } + } + + /** Return structured log entries for the given plugin. */ + getPluginLogs(id: PluginLocalIdentity) { + return this.getPluginLogger(id)?.getEntries() || [] + } + + /** Clear log entries for the given plugin. */ + clearPluginLogs(id: PluginLocalIdentity) { + this.getPluginLogger(id)?.clear() + } + hostMounted() { this._hostMountedActor.resolve() } @@ -1695,6 +1801,8 @@ class LSPluginCore themes.push(opt) this.emit('themes-changed', this.themes, { id, ...opt }) + this.getPluginLogger(id) + ?.debug('theme registered', opt?.name || (opt as any)?.url || '') } async selectTheme( diff --git a/libs/src/common.ts b/libs/src/common.ts index 077dc643f2..ad72bbbbfe 100644 --- a/libs/src/common.ts +++ b/libs/src/common.ts @@ -57,65 +57,155 @@ export function deepMerge(a: Partial, b: Partial): T { return merge(a, b, { arrayMerge: overwriteArrayMerge }) } -export class PluginLogger extends EventEmitter<'change'> { - private _logs: Array<[type: string, payload: any]> = [] +export type PluginLogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' + +export interface PluginLogEntry { + ts: number + level: PluginLogLevel + tag: string + message: string +} + +export interface PluginLoggerOptions { + console?: boolean + maxSize?: number + level?: PluginLogLevel +} + +const LOG_LEVEL_WEIGHT: Record = { + DEBUG: 10, + INFO: 20, + WARN: 30, + ERROR: 40, +} + +const DEFAULT_LOG_MAX_SIZE = 500 + +function safeStringifyArg(it: any): string { + if (it == null) return String(it) + if (it instanceof Error) return `${it.message}${it.stack ? '\n' + it.stack : ''}` + if (typeof it === 'string') return it + if (typeof it === 'object') { + try { + const seen = new WeakSet() + return JSON.stringify(it, (_k, v) => { + if (typeof v === 'object' && v !== null) { + if (seen.has(v)) return '[Circular]' + seen.add(v) + } + return v + }) + } catch (_e) { + try { return String(it) } catch (_) { return '[Unserializable]' } + } + } + try { return String(it) } catch (_e) { return '[Unserializable]' } +} + +export class PluginLogger extends EventEmitter<'change' | 'append' | 'clear'> { + private _logs: PluginLogEntry[] = [] + private _maxSize: number + private _level: PluginLogLevel constructor( private _tag?: string, - private _opts?: { - console: boolean - } + private _opts?: PluginLoggerOptions ) { super() + this._maxSize = Math.max(50, _opts?.maxSize ?? DEFAULT_LOG_MAX_SIZE) + this._level = _opts?.level ?? 'DEBUG' } - write(type: string, payload: any[], inConsole?: boolean) { - if (payload?.length && true === payload[payload.length - 1]) { + /** + * Write a log entry. + * Backwards compatible: the legacy boolean tail flag in `payload` to force + * console output is still honored. + */ + write(level: PluginLogLevel | string, payload: any[], inConsole?: boolean) { + // back-compat: trailing boolean === true means "force console" + if (Array.isArray(payload) && payload.length && + payload[payload.length - 1] === true) { inConsole = true - payload.pop() + payload = payload.slice(0, -1) } - const msg = payload.reduce((ac, it) => { - if (it && it instanceof Error) { - ac += `${it.message} ${it.stack}` - } else { - ac += it.toString() - } - return ac - }, `[${this._tag}][${new Date().toLocaleTimeString()}] `) + const lvl = (typeof level === 'string' + ? (level.toUpperCase() as PluginLogLevel) + : level) as PluginLogLevel + const normalizedLevel: PluginLogLevel = + lvl in LOG_LEVEL_WEIGHT ? lvl : 'INFO' - this._logs.push([type, msg]) + // level filtering + if (LOG_LEVEL_WEIGHT[normalizedLevel] < LOG_LEVEL_WEIGHT[this._level]) { + return + } + + const message = (payload || []).map(safeStringifyArg).join(' ') + const entry: PluginLogEntry = { + ts: Date.now(), + level: normalizedLevel, + tag: this._tag || '', + message, + } + + this._logs.push(entry) + // ring buffer + if (this._logs.length > this._maxSize) { + this._logs.splice(0, this._logs.length - this._maxSize) + } if (inConsole || this._opts?.console) { - console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`) + const fn = normalizedLevel === 'ERROR' + ? 'error' + : normalizedLevel === 'WARN' + ? 'warn' + : normalizedLevel === 'DEBUG' ? 'debug' : 'info' + try { + // eslint-disable-next-line no-console + console[fn](`[${entry.tag}][${new Date(entry.ts).toLocaleTimeString()}] ${normalizedLevel}: ${message}`) + } catch (_e) { /* noop */ } } + this.emit('append', entry) this.emit('change') } clear() { this._logs = [] + this.emit('clear') this.emit('change') } - info(...args: any[]) { - this.write('INFO', args) + debug(...args: any[]) { this.write('DEBUG', args) } + info(...args: any[]) { this.write('INFO', args) } + warn(...args: any[]) { this.write('WARN', args) } + error(...args: any[]) { this.write('ERROR', args) } + + setTag(s: string) { this._tag = s } + getTag() { return this._tag } + + setLevel(l: PluginLogLevel) { + if (l in LOG_LEVEL_WEIGHT) this._level = l + } + getLevel(): PluginLogLevel { return this._level } + + setMaxSize(n: number) { + this._maxSize = Math.max(50, n | 0) + if (this._logs.length > this._maxSize) { + this._logs.splice(0, this._logs.length - this._maxSize) + this.emit('change') + } } - error(...args: any[]) { - this.write('ERROR', args) + /** Structured entries (preferred). */ + getEntries(): PluginLogEntry[] { + return this._logs.slice() } - warn(...args: any[]) { - this.write('WARN', args) - } - - setTag(s: string) { - this._tag = s - } - - toJSON() { - return this._logs + /** Legacy tuple format kept for backwards compatibility. */ + toJSON(): Array<[PluginLogLevel, string]> { + return this._logs.map((e) => [e.level, + `[${e.tag}][${new Date(e.ts).toLocaleTimeString()}] ${e.message}`]) } } diff --git a/libs/src/modules/LSPlugin.Experiments.ts b/libs/src/modules/LSPlugin.Experiments.ts index b032107643..5379ff423a 100644 --- a/libs/src/modules/LSPlugin.Experiments.ts +++ b/libs/src/modules/LSPlugin.Experiments.ts @@ -2,6 +2,43 @@ import { LSPluginUser } from '../LSPlugin.user' import { PluginLocal } from '../LSPlugin.core' import { safeSnakeCase } from '../common' +/** + * Declarative condition for matching a block's properties map. + * Operators: has, equals, in, not, any, all. + */ +export type BlockPropertiesCondition = + | { has: string } + | { equals: [string, any] } + | { in: [string, Array] } + | { not: BlockPropertiesCondition } + | { any: Array } + | { all: Array } + +export type BlockPropertiesRendererProps = { + blockId: string + properties: Record +} + +export type BlockRendererChild = Record & { + children?: Array +} + +export type BlockRendererProps = BlockPropertiesRendererProps & { + uuid?: string + page?: string + content?: string + format?: string + children?: Array +} + +export type BlockPropertiesPredicate = ( + props: BlockPropertiesRendererProps +) => boolean + +export type BlockRendererPredicate = ( + props: BlockRendererProps +) => boolean + /** * WARN: These are some experience features and might be adjusted at any time. * These unofficial plugins that use these APIs are temporarily @@ -21,19 +58,20 @@ export class LSPluginExperiments { get Components() { const exper = this.ensureHostScope().logseq.sdk.experiments return { - Editor: exper.cp_page_editor as (props: { page: string } & any) => any + Editor: exper.cp_page_editor as (props: { page: string } & any) => any, } } get Utils() { const utils = this.ensureHostScope().logseq.sdk.utils - const withCall = (name: string): (input: any) => any => utils[safeSnakeCase(name)] + const withCall = (name: string): ((input: any) => any) => + utils[safeSnakeCase(name)] return { toClj: withCall('toClj'), jsxToClj: withCall('jsxToClj'), toJs: withCall('toJs'), toKeyword: withCall('toKeyword'), - toSymbol: withCall('toSymbol') + toSymbol: withCall('toSymbol'), } } @@ -46,7 +84,8 @@ export class LSPluginExperiments { public invokeExperMethod(type: string, ...args: Array) { const host = this.ensureHostScope() type = safeSnakeCase(type)?.toLowerCase() - const fn = host.logseq.api['exper_' + type] || host.logseq.sdk.experiments[type] + const fn = + host.logseq.api['exper_' + type] || host.logseq.sdk.experiments[type] return fn?.apply(host, args) } @@ -83,7 +122,8 @@ export class LSPluginExperiments { registerDaemonRenderer( key: string, opts: { - sub?: Array, + before?: () => Promise + subs?: Array render: (props: {}) => any } ) { @@ -98,9 +138,9 @@ export class LSPluginExperiments { registerHostedRenderer( key: string, opts: { - title?: string, + title?: string subs?: Array - type?: string, + type?: string render: (props: {}) => any } ) { @@ -115,9 +155,9 @@ export class LSPluginExperiments { registerSidebarRenderer( key: string, opts: { - title?: string, + title?: string subs?: Array - render: (props: {}) => any, + render: (props: {}) => any [k: string]: any } ) { @@ -129,9 +169,9 @@ export class LSPluginExperiments { registerRouteRenderer( key: string, opts: { - name?: string, + name?: string subs?: Array - path: string, + path: string render: (props: {}) => any } ) { @@ -143,6 +183,71 @@ export class LSPluginExperiments { ) } + /** + * Register a custom renderer for the block properties area. + * The renderer is shown when the block's properties match the `when` condition. + * `when` may be either a declarative condition object or a synchronous predicate. + * + * @param key Unique key for this renderer (scoped to the plugin). + * @param opts Renderer options. + * @param opts.when Optional condition or synchronous predicate; if omitted, always matches. + * @param opts.mode "prepend" | "append" (default) | "replace". + * @param opts.priority Higher number wins when multiple replace renderers match. + * @param opts.subs Reserved subscription list for future reactive updates. + * @param opts.render React function component receiving `{ blockId, properties }`. + */ + registerBlockPropertiesRenderer( + key: string, + opts: { + when?: BlockPropertiesCondition | BlockPropertiesPredicate + mode?: 'prepend' | 'append' | 'replace' + priority?: number + subs?: Array + render: (props: BlockPropertiesRendererProps) => any + } + ) { + return this.invokeExperMethod( + 'registerBlockPropertiesRenderer', + this.ctx.baseInfo.id, + key, + opts + ) + } + + /** + * Register a custom renderer for the block body. + * When the synchronous predicate matches, the plugin renderer replaces the + * default outline view by default. Users can switch back to outline view via + * an explicit UI toggle on each matched block. + * + * @param key Unique key for this renderer (scoped to the plugin). + * @param opts Renderer options. + * @param opts.when Optional synchronous predicate; if omitted, always matches. + * @param opts.includeChildren When true, passes the block's recursive children + * tree to the renderer and hides native outline children while the plugin + * renderer is active. + * @param opts.priority Higher number wins when multiple block renderers match. + * @param opts.subs Reserved subscription list for future reactive updates. + * @param opts.render React function component receiving block renderer props. + */ + registerBlockRenderer( + key: string, + opts: { + when?: BlockRendererPredicate + includeChildren?: boolean + priority?: number + subs?: Array + render: (props: BlockRendererProps) => any + } + ) { + return this.invokeExperMethod( + 'registerBlockRenderer', + this.ctx.baseInfo.id, + key, + opts + ) + } + registerExtensionsEnhancer( type: 'katex' | 'codemirror', enhancer: (v: T) => Promise @@ -168,7 +273,7 @@ export class LSPluginExperiments { ensureHostScope(): any { try { - const _ = window.top?.document + window.top?.document } catch (_e) { console.error('Can not access host scope!') } diff --git a/resources/js/lsplugin.core.js b/resources/js/lsplugin.core.js index af232231e0..66842ed7a6 100644 --- a/resources/js/lsplugin.core.js +++ b/resources/js/lsplugin.core.js @@ -1,2 +1,2 @@ /*! For license information please see lsplugin.core.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LSPlugin=t():e.LSPlugin=t()}(self,()=>(()=>{var e={833(e,t,n){var r=n(606);t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let r=0,s=0;t[0].replace(/%[a-zA-Z%]/g,e=>{"%%"!==e&&(r++,"%c"===e&&(s=r))}),t.splice(s,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")||t.storage.getItem("DEBUG")}catch(e){}return!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG),e},t.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(736)(t);const{formatters:s}=e.exports;s.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},736(e,t,n){e.exports=function(e){function t(e){let n,s,i,o=null;function a(...e){if(!a.enabled)return;const r=a,s=Number(new Date),i=s-(n||s);r.diff=i,r.prev=n,r.curr=s,n=s,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let o=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(n,s)=>{if("%%"===n)return"%";o++;const i=t.formatters[s];if("function"==typeof i){const t=e[o];n=i.call(r,t),e.splice(o,1),o--}return n}),t.formatArgs.call(r,e),(r.log||t.log).apply(r,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=r,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==o?o:(s!==t.namespaces&&(s=t.namespaces,i=t.enabled(e)),i),set:e=>{o=e}}),"function"==typeof t.init&&t.init(a),a}function r(e,n){const r=t(this.namespace+(void 0===n?":":n)+e);return r.log=this.log,r}function s(e,t){let n=0,r=0,s=-1,i=0;for(;n"-"+e)].join(",");return t.enable(""),e},t.enable=function(e){t.save(e),t.namespaces=e,t.names=[],t.skips=[];const n=("string"==typeof e?e:"").trim().replace(/\s+/g,",").split(",").filter(Boolean);for(const e of n)"-"===e[0]?t.skips.push(e.slice(1)):t.names.push(e)},t.enabled=function(e){for(const n of t.skips)if(s(e,n))return!1;for(const n of t.names)if(s(e,n))return!0;return!1},t.humanize=n(585),t.destroy=function(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")},Object.keys(e).forEach(n=>{t[n]=e[n]}),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t=1.5*n;return Math.round(e/n)+" "+r+(s?"s":"")}e.exports=function(e,a){a=a||{};var l,c,u=typeof e;if("string"===u&&e.length>0)return function(e){if(!((e=String(e)).length>100)){var o=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(o){var a=parseFloat(o[1]);switch((o[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return 315576e5*a;case"weeks":case"week":case"w":return a*i;case"days":case"day":case"d":return a*s;case"hours":case"hour":case"hrs":case"hr":case"h":return a*r;case"minutes":case"minute":case"mins":case"min":case"m":return a*n;case"seconds":case"second":case"secs":case"sec":case"s":return a*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return a;default:return}}}}(e);if("number"===u&&isFinite(e))return a.long?(l=e,(c=Math.abs(l))>=s?o(l,c,s,"day"):c>=r?o(l,c,r,"hour"):c>=n?o(l,c,n,"minute"):c>=t?o(l,c,t,"second"):l+" ms"):function(e){var i=Math.abs(e);return i>=s?Math.round(e/s)+"d":i>=r?Math.round(e/r)+"h":i>=n?Math.round(e/n)+"m":i>=t?Math.round(e/t)+"s":e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},627(e,t,n){"use strict";var r=n(606),s="win32"===r.platform,i=n(537);function o(e,t){for(var n=[],r=0;r=0&&!e[r];r--);return 0===n&&r===t?e:n>r?[]:e.slice(n,r+1)}var l=/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/,c=/^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/,u={};function h(e){var t=l.exec(e),n=(t[1]||"")+(t[2]||""),r=t[3]||"",s=c.exec(r);return[n,s[1],s[2],s[3]]}function d(e){var t=l.exec(e),n=t[1]||"",r=!!n&&":"!==n[1];return{device:n,isUnc:r,isAbsolute:r||!!t[2],tail:t[3]}}function p(e){return"\\\\"+e.replace(/^[\\\/]+/,"").replace(/[\\\/]+/g,"\\")}u.resolve=function(){for(var e="",t="",n=!1,s=arguments.length-1;s>=-1;s--){var a;if(s>=0?a=arguments[s]:e?(a=r.env["="+e])&&a.substr(0,3).toLowerCase()===e.toLowerCase()+"\\"||(a=e+"\\"):a=r.cwd(),!i.isString(a))throw new TypeError("Arguments to path.resolve must be strings");if(a){var l=d(a),c=l.device,u=l.isUnc,h=l.isAbsolute,f=l.tail;if((!c||!e||c.toLowerCase()===e.toLowerCase())&&(e||(e=c),n||(t=f+"\\"+t,n=h),e&&n))break}}return u&&(e=p(e)),e+(n?"\\":"")+(t=o(t.split(/[\\\/]+/),!n).join("\\"))||"."},u.normalize=function(e){var t=d(e),n=t.device,r=t.isUnc,s=t.isAbsolute,i=t.tail,a=/[\\\/]$/.test(i);return(i=o(i.split(/[\\\/]+/),!s).join("\\"))||s||(i="."),i&&a&&(i+="\\"),r&&(n=p(n)),n+(s?"\\":"")+i},u.isAbsolute=function(e){return d(e).isAbsolute},u.join=function(){for(var e=[],t=0;t=-1&&!t;n--){var s=n>=0?arguments[n]:r.cwd();if(!i.isString(s))throw new TypeError("Arguments to path.resolve must be strings");s&&(e=s+"/"+e,t="/"===s[0])}return(t?"/":"")+(e=o(e.split("/"),!t).join("/"))||"."},g.normalize=function(e){var t=g.isAbsolute(e),n=e&&"/"===e[e.length-1];return(e=o(e.split("/"),!t).join("/"))||t||(e="."),e&&n&&(e+="/"),(t?"/":"")+e},g.isAbsolute=function(e){return"/"===e.charAt(0)},g.join=function(){for(var e="",t=0;t1)for(var n=1;n=i)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),l=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),f(n)?r.showHidden=n:n&&t._extend(r,n),_(r.showHidden)&&(r.showHidden=!1),_(r.depth)&&(r.depth=2),_(r.colors)&&(r.colors=!1),_(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=l),u(r,e,r.depth)}function l(e,t){var n=a.styles[t];return n?"["+a.colors[n][0]+"m"+e+"["+a.colors[n][1]+"m":e}function c(e,t){return e}function u(e,n,r){if(e.customInspect&&n&&S(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var s=n.inspect(r,e);return y(s)||(s=u(e,s,r)),s}var i=function(e,t){if(_(t))return e.stylize("undefined","undefined");if(y(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}return m(t)?e.stylize(""+t,"number"):f(t)?e.stylize(""+t,"boolean"):g(t)?e.stylize("null","null"):void 0}(e,n);if(i)return i;var o=Object.keys(n),a=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(o);if(e.showHidden&&(o=Object.getOwnPropertyNames(n)),C(n)&&(o.indexOf("message")>=0||o.indexOf("description")>=0))return h(n);if(0===o.length){if(S(n)){var l=n.name?": "+n.name:"";return e.stylize("[Function"+l+"]","special")}if(b(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(w(n))return e.stylize(Date.prototype.toString.call(n),"date");if(C(n))return h(n)}var c,v="",x=!1,E=["{","}"];return p(n)&&(x=!0,E=["[","]"]),S(n)&&(v=" [Function"+(n.name?": "+n.name:"")+"]"),b(n)&&(v=" "+RegExp.prototype.toString.call(n)),w(n)&&(v=" "+Date.prototype.toUTCString.call(n)),C(n)&&(v=" "+h(n)),0!==o.length||x&&0!=n.length?r<0?b(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),c=x?function(e,t,n,r,s){for(var i=[],o=0,a=t.length;o60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}(c,v,E)):E[0]+v+E[1]}function h(e){return"["+Error.prototype.toString.call(e)+"]"}function d(e,t,n,r,s,i){var o,a,l;if((l=Object.getOwnPropertyDescriptor(t,s)||{value:t[s]}).get?a=l.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):l.set&&(a=e.stylize("[Setter]","special")),A(r,s)||(o="["+s+"]"),a||(e.seen.indexOf(l.value)<0?(a=g(n)?u(e,l.value,null):u(e,l.value,n-1)).indexOf("\n")>-1&&(a=i?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),_(o)){if(i&&s.match(/^\d+$/))return a;(o=JSON.stringify(""+s)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(o=o.substr(1,o.length-2),o=e.stylize(o,"name")):(o=o.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),o=e.stylize(o,"string"))}return o+": "+a}function p(e){return Array.isArray(e)}function f(e){return"boolean"==typeof e}function g(e){return null===e}function m(e){return"number"==typeof e}function y(e){return"string"==typeof e}function _(e){return void 0===e}function b(e){return v(e)&&"[object RegExp]"===x(e)}function v(e){return"object"==typeof e&&null!==e}function w(e){return v(e)&&"[object Date]"===x(e)}function C(e){return v(e)&&("[object Error]"===x(e)||e instanceof Error)}function S(e){return"function"==typeof e}function x(e){return Object.prototype.toString.call(e)}function E(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(e){if(_(i)&&(i=r.env.NODE_DEBUG||""),e=e.toUpperCase(),!o[e])if(new RegExp("\\b"+e+"\\b","i").test(i)){var n=r.pid;o[e]=function(){var r=t.format.apply(t,arguments);console.error("%s %d: %s",e,n,r)}}else o[e]=function(){};return o[e]},t.inspect=a,a.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},a.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=f,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=m,t.isString=y,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=_,t.isRegExp=b,t.isObject=v,t.isDate=w,t.isError=C,t.isFunction=S,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(135);var T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function A(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(n=[E((e=new Date).getHours()),E(e.getMinutes()),E(e.getSeconds())].join(":"),[e.getDate(),T[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(698),t._extend=function(e,t){if(!t||!v(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e}}},t={};function n(r){var s=t[r];if(void 0!==s)return s.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};return(()=>{"use strict";n.r(r),n.d(r,{PluginLocal:()=>vs,pluginHelpers:()=>t,setupPluginCore:()=>Cs});var e={};n.r(e),n.d(e,{setSDKMetadata:()=>ye});var t={};n.r(t),n.d(t,{IS_DEV:()=>be,PROTOCOL_FILE:()=>ve,PROTOCOL_LSP:()=>we,PluginLogger:()=>ke,URL_LSP:()=>Ce,cleanInjectedScripts:()=>He,cleanInjectedUI:()=>ze,deepMerge:()=>Oe,deferred:()=>De,genID:()=>Le,getAppPathRoot:()=>Ee,getSDKPathRoot:()=>Te,injectTheme:()=>qe,invokeHostExportedApi:()=>Ne,isObject:()=>Ae,isValidUUID:()=>Pe,mergeSettingsWithSchema:()=>Be,normalizeKeyStr:()=>Ge,path:()=>_e,safeSnakeCase:()=>xe,safetyPathJoin:()=>Me,safetyPathNormalize:()=>Re,setupInjectedStyle:()=>Ue,setupInjectedUI:()=>$e,transformableEvent:()=>We,ucFirst:()=>Ie,withFileProtocol:()=>je});var s=n(228);const i=s;var o=n(627);const{entries:a,setPrototypeOf:l,isFrozen:c,getPrototypeOf:u,getOwnPropertyDescriptor:h}=Object;let{freeze:d,seal:p,create:f}=Object,{apply:g,construct:m}="undefined"!=typeof Reflect&&Reflect;d||(d=function(e){return e}),p||(p=function(e){return e}),g||(g=function(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),s=2;s1?t-1:0),r=1;r1?n-1:0),s=1;s2&&void 0!==arguments[2]?arguments[2]:C;l&&l(e,null);let r=t.length;for(;r--;){let s=t[r];if("string"==typeof s){const e=n(s);e!==s&&(c(t)||(t[r]=e),s=e)}e[s]=!0}return e}function M(e){for(let t=0;t/gm),V=p(/\$\{[\w\W]*/gm),X=p(/^data-[\-\w.\u00B7-\uFFFF]+$/),Z=p(/^aria-[\-\w]+$/),Q=p(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ee=p(/^(?:\w+script|data):/i),te=p(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ne=p(/^html$/i),re=p(/^[a-z][.\w]*(-[.\w]+)+$/i);var se=Object.freeze({__proto__:null,ARIA_ATTR:Z,ATTR_WHITESPACE:te,CUSTOM_ELEMENT:re,DATA_ATTR:X,DOCTYPE_NAME:ne,ERB_EXPR:K,IS_ALLOWED_URI:Q,IS_SCRIPT_OR_DATA:ee,MUSTACHE_EXPR:Y,TMPLIT_EXPR:V});const ie=function(){return"undefined"==typeof window?null:window};var oe=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ie();const n=t=>e(t);if(n.version="3.3.3",n.removed=[],!t||!t.document||9!==t.document.nodeType||!t.Element)return n.isSupported=!1,n;let{document:r}=t;const s=r,i=s.currentScript,{DocumentFragment:o,HTMLTemplateElement:l,Node:c,Element:u,NodeFilter:h,NamedNodeMap:p=t.NamedNodeMap||t.MozNamedAttrMap,HTMLFormElement:g,DOMParser:m,trustedTypes:L}=t,I=u.prototype,M=D(I,"cloneNode"),Y=D(I,"remove"),K=D(I,"nextSibling"),V=D(I,"childNodes"),X=D(I,"parentNode");if("function"==typeof l){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let Z,ee="";const{implementation:te,createNodeIterator:re,createDocumentFragment:oe,getElementsByTagName:ae}=r,{importNode:le}=s;let ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};n.isSupported="function"==typeof a&&"function"==typeof X&&te&&void 0!==te.createHTMLDocument;const{MUSTACHE_EXPR:ue,ERB_EXPR:he,TMPLIT_EXPR:de,DATA_ATTR:pe,ARIA_ATTR:fe,IS_SCRIPT_OR_DATA:ge,ATTR_WHITESPACE:me,CUSTOM_ELEMENT:ye}=se;let{IS_ALLOWED_URI:_e}=se,be=null;const ve=j({},[...N,...U,...F,...z,...W]);let we=null;const Ce=j({},[...q,...B,...G,...J]);let Se=Object.seal(f(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),xe=null,Ee=null;const Te=Object.seal(f(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Ae=!0,Oe=!0,ke=!1,Pe=!0,Le=!1,Ie=!0,je=!1,Me=!1,Re=!1,De=!1,Ne=!1,Ue=!1,Fe=!0,$e=!1,ze=!0,He=!1,We={},qe=null;const Be=j({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ge=null;const Je=j({},["audio","video","img","source","image","track"]);let Ye=null;const Ke=j({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",Xe="http://www.w3.org/2000/svg",Ze="http://www.w3.org/1999/xhtml";let Qe=Ze,et=!1,tt=null;const nt=j({},[Ve,Xe,Ze],S);let rt=j({},["mi","mo","mn","ms","mtext"]),st=j({},["annotation-xml"]);const it=j({},["title","style","font","a","script"]);let ot=null;const at=["application/xhtml+xml","text/html"];let lt=null,ct=null;const ut=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},dt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ct||ct!==e){if(e&&"object"==typeof e||(e={}),e=R(e),ot=-1===at.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,lt="application/xhtml+xml"===ot?S:C,be=O(e,"ALLOWED_TAGS")?j({},e.ALLOWED_TAGS,lt):ve,we=O(e,"ALLOWED_ATTR")?j({},e.ALLOWED_ATTR,lt):Ce,tt=O(e,"ALLOWED_NAMESPACES")?j({},e.ALLOWED_NAMESPACES,S):nt,Ye=O(e,"ADD_URI_SAFE_ATTR")?j(R(Ke),e.ADD_URI_SAFE_ATTR,lt):Ke,Ge=O(e,"ADD_DATA_URI_TAGS")?j(R(Je),e.ADD_DATA_URI_TAGS,lt):Je,qe=O(e,"FORBID_CONTENTS")?j({},e.FORBID_CONTENTS,lt):Be,xe=O(e,"FORBID_TAGS")?j({},e.FORBID_TAGS,lt):R({}),Ee=O(e,"FORBID_ATTR")?j({},e.FORBID_ATTR,lt):R({}),We=!!O(e,"USE_PROFILES")&&e.USE_PROFILES,Ae=!1!==e.ALLOW_ARIA_ATTR,Oe=!1!==e.ALLOW_DATA_ATTR,ke=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Pe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Le=e.SAFE_FOR_TEMPLATES||!1,Ie=!1!==e.SAFE_FOR_XML,je=e.WHOLE_DOCUMENT||!1,De=e.RETURN_DOM||!1,Ne=e.RETURN_DOM_FRAGMENT||!1,Ue=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Fe=!1!==e.SANITIZE_DOM,$e=e.SANITIZE_NAMED_PROPS||!1,ze=!1!==e.KEEP_CONTENT,He=e.IN_PLACE||!1,_e=e.ALLOWED_URI_REGEXP||Q,Qe=e.NAMESPACE||Ze,rt=e.MATHML_TEXT_INTEGRATION_POINTS||rt,st=e.HTML_INTEGRATION_POINTS||st,Se=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Se.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Se.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Se.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Le&&(Oe=!1),Ne&&(De=!0),We&&(be=j({},W),we=f(null),!0===We.html&&(j(be,N),j(we,q)),!0===We.svg&&(j(be,U),j(we,B),j(we,J)),!0===We.svgFilters&&(j(be,F),j(we,B),j(we,J)),!0===We.mathMl&&(j(be,z),j(we,G),j(we,J))),O(e,"ADD_TAGS")||(Te.tagCheck=null),O(e,"ADD_ATTR")||(Te.attributeCheck=null),e.ADD_TAGS&&("function"==typeof e.ADD_TAGS?Te.tagCheck=e.ADD_TAGS:(be===ve&&(be=R(be)),j(be,e.ADD_TAGS,lt))),e.ADD_ATTR&&("function"==typeof e.ADD_ATTR?Te.attributeCheck=e.ADD_ATTR:(we===Ce&&(we=R(we)),j(we,e.ADD_ATTR,lt))),e.ADD_URI_SAFE_ATTR&&j(Ye,e.ADD_URI_SAFE_ATTR,lt),e.FORBID_CONTENTS&&(qe===Be&&(qe=R(qe)),j(qe,e.FORBID_CONTENTS,lt)),e.ADD_FORBID_CONTENTS&&(qe===Be&&(qe=R(qe)),j(qe,e.ADD_FORBID_CONTENTS,lt)),ze&&(be["#text"]=!0),je&&j(be,["html","head","body"]),be.table&&(j(be,["tbody"]),delete xe.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw P('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw P('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');Z=e.TRUSTED_TYPES_POLICY,ee=Z.createHTML("")}else void 0===Z&&(Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const r="data-tt-policy-suffix";t&&t.hasAttribute(r)&&(n=t.getAttribute(r));const s="dompurify"+(n?"#"+n:"");try{return e.createPolicy(s,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+s+" could not be created."),null}}(L,i)),null!==Z&&"string"==typeof ee&&(ee=Z.createHTML(""));d&&d(e),ct=e}},pt=j({},[...U,...F,...$]),ft=j({},[...z,...H]),gt=function(e){v(n.removed,{element:e});try{X(e).removeChild(e)}catch(t){Y(e)}},mt=function(e,t){try{v(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){v(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(De||Ne)try{gt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},yt=function(e){let t=null,n=null;if(Re)e=""+e;else{const t=x(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ot&&Qe===Ze&&(e=''+e+"");const s=Z?Z.createHTML(e):e;if(Qe===Ze)try{t=(new m).parseFromString(s,ot)}catch(e){}if(!t||!t.documentElement){t=te.createDocument(Qe,"template",null);try{t.documentElement.innerHTML=et?ee:s}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Qe===Ze?ae.call(t,je?"html":"body")[0]:je?t.documentElement:i},_t=function(e){return re.call(e.ownerDocument||e,e,h.SHOW_ELEMENT|h.SHOW_COMMENT|h.SHOW_TEXT|h.SHOW_PROCESSING_INSTRUCTION|h.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof g&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof p)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},vt=function(e){return"function"==typeof c&&e instanceof c};function wt(e,t,r){y(e,e=>{e.call(n,t,r,ct)})}const Ct=function(e){let t=null;if(wt(ce.beforeSanitizeElements,e,null),bt(e))return gt(e),!0;const r=lt(e.nodeName);if(wt(ce.uponSanitizeElement,e,{tagName:r,allowedTags:be}),Ie&&e.hasChildNodes()&&!vt(e.firstElementChild)&&k(/<[/\w!]/g,e.innerHTML)&&k(/<[/\w!]/g,e.textContent))return gt(e),!0;if(7===e.nodeType)return gt(e),!0;if(Ie&&8===e.nodeType&&k(/<[/\w]/g,e.data))return gt(e),!0;if(!(Te.tagCheck instanceof Function&&Te.tagCheck(r))&&(!be[r]||xe[r])){if(!xe[r]&&xt(r)){if(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,r))return!1;if(Se.tagNameCheck instanceof Function&&Se.tagNameCheck(r))return!1}if(ze&&!qe[r]){const t=X(e)||e.parentNode,n=V(e)||e.childNodes;if(n&&t)for(let r=n.length-1;r>=0;--r){const s=M(n[r],!0);s.__removalCount=(e.__removalCount||0)+1,t.insertBefore(s,K(e))}}return gt(e),!0}return e instanceof u&&!function(e){let t=X(e);t&&t.tagName||(t={namespaceURI:Qe,tagName:"template"});const n=C(e.tagName),r=C(t.tagName);return!!tt[e.namespaceURI]&&(e.namespaceURI===Xe?t.namespaceURI===Ze?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||rt[r]):Boolean(pt[n]):e.namespaceURI===Ve?t.namespaceURI===Ze?"math"===n:t.namespaceURI===Xe?"math"===n&&st[r]:Boolean(ft[n]):e.namespaceURI===Ze?!(t.namespaceURI===Xe&&!st[r])&&!(t.namespaceURI===Ve&&!rt[r])&&!ft[n]&&(it[n]||!pt[n]):!("application/xhtml+xml"!==ot||!tt[e.namespaceURI]))}(e)?(gt(e),!0):"noscript"!==r&&"noembed"!==r&&"noframes"!==r||!k(/<\/no(script|embed|frames)/i,e.innerHTML)?(Le&&3===e.nodeType&&(t=e.textContent,y([ue,he,de],e=>{t=E(t,e," ")}),e.textContent!==t&&(v(n.removed,{element:e.cloneNode()}),e.textContent=t)),wt(ce.afterSanitizeElements,e,null),!1):(gt(e),!0)},St=function(e,t,n){if(Ee[t])return!1;if(Fe&&("id"===t||"name"===t)&&(n in r||n in ut))return!1;if(Oe&&!Ee[t]&&k(pe,t));else if(Ae&&k(fe,t));else if(Te.attributeCheck instanceof Function&&Te.attributeCheck(t,e));else if(!we[t]||Ee[t]){if(!(xt(e)&&(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,e)||Se.tagNameCheck instanceof Function&&Se.tagNameCheck(e))&&(Se.attributeNameCheck instanceof RegExp&&k(Se.attributeNameCheck,t)||Se.attributeNameCheck instanceof Function&&Se.attributeNameCheck(t,e))||"is"===t&&Se.allowCustomizedBuiltInElements&&(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,n)||Se.tagNameCheck instanceof Function&&Se.tagNameCheck(n))))return!1}else if(Ye[t]);else if(k(_e,E(n,me,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ge[e])if(ke&&!k(ge,E(n,me,"")));else if(n)return!1;return!0},xt=function(e){return"annotation-xml"!==e&&x(e,ye)},Et=function(e){wt(ce.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const r={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let s=t.length;for(;s--;){const i=t[s],{name:o,namespaceURI:a,value:l}=i,c=lt(o),u=l;let h="value"===o?u:A(u);if(r.attrName=c,r.attrValue=h,r.keepAttr=!0,r.forceKeepAttr=void 0,wt(ce.uponSanitizeAttribute,e,r),h=r.attrValue,!$e||"id"!==c&&"name"!==c||(mt(o,e),h="user-content-"+h),Ie&&k(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,h)){mt(o,e);continue}if("attributename"===c&&x(h,"href")){mt(o,e);continue}if(r.forceKeepAttr)continue;if(!r.keepAttr){mt(o,e);continue}if(!Pe&&k(/\/>/i,h)){mt(o,e);continue}Le&&y([ue,he,de],e=>{h=E(h,e," ")});const d=lt(e.nodeName);if(St(d,c,h)){if(Z&&"object"==typeof L&&"function"==typeof L.getAttributeType)if(a);else switch(L.getAttributeType(d,c)){case"TrustedHTML":h=Z.createHTML(h);break;case"TrustedScriptURL":h=Z.createScriptURL(h)}if(h!==u)try{a?e.setAttributeNS(a,o,h):e.setAttribute(o,h),bt(e)?gt(e):b(n.removed)}catch(t){mt(o,e)}}else mt(o,e)}wt(ce.afterSanitizeAttributes,e,null)},Tt=function e(t){let n=null;const r=_t(t);for(wt(ce.beforeSanitizeShadowDOM,t,null);n=r.nextNode();)wt(ce.uponSanitizeShadowNode,n,null),Ct(n),Et(n),n.content instanceof o&&e(n.content);wt(ce.afterSanitizeShadowDOM,t,null)};return n.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=null,i=null,a=null,l=null;if(et=!e,et&&(e="\x3c!--\x3e"),"string"!=typeof e&&!vt(e)){if("function"!=typeof e.toString)throw P("toString is not a function");if("string"!=typeof(e=e.toString()))throw P("dirty is not a string, aborting")}if(!n.isSupported)return e;if(Me||dt(t),n.removed=[],"string"==typeof e&&(He=!1),He){if(e.nodeName){const t=lt(e.nodeName);if(!be[t]||xe[t])throw P("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)r=yt("\x3c!----\x3e"),i=r.ownerDocument.importNode(e,!0),1===i.nodeType&&"BODY"===i.nodeName||"HTML"===i.nodeName?r=i:r.appendChild(i);else{if(!De&&!Le&&!je&&-1===e.indexOf("<"))return Z&&Ue?Z.createHTML(e):e;if(r=yt(e),!r)return De?null:Ue?ee:""}r&&Re&>(r.firstChild);const u=_t(He?e:r);for(;a=u.nextNode();)Ct(a),Et(a),a.content instanceof o&&Tt(a.content);if(He)return e;if(De){if(Ne)for(l=oe.call(r.ownerDocument);r.firstChild;)l.appendChild(r.firstChild);else l=r;return(we.shadowroot||we.shadowrootmode)&&(l=le.call(s,l,!0)),l}let h=je?r.outerHTML:r.innerHTML;return je&&be["!doctype"]&&r.ownerDocument&&r.ownerDocument.doctype&&r.ownerDocument.doctype.name&&k(ne,r.ownerDocument.doctype.name)&&(h="\n"+h),Le&&y([ue,he,de],e=>{h=E(h,e," ")}),Z&&Ue?Z.createHTML(h):h},n.setConfig=function(){dt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Me=!0},n.clearConfig=function(){ct=null,Me=!1},n.isValidAttribute=function(e,t,n){ct||dt({});const r=lt(e),s=lt(t);return St(r,s,n)},n.addHook=function(e,t){"function"==typeof t&&v(ce[e],t)},n.removeHook=function(e,t){if(void 0!==t){const n=_(ce[e],t);return-1===n?void 0:w(ce[e],n,1)[0]}return b(ce[e])},n.removeHooks=function(e){ce[e]=[]},n.removeAllHooks=function(){ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},n}(),ae=n(744),le=n.n(ae);const ce=/([\p{Ll}\d])(\p{Lu})/gu,ue=/(\p{Lu})([\p{Lu}][\p{Ll}])/gu,he=/(\d)\p{Ll}|(\p{L})\d/u,de=/[^\p{L}\d]+/giu,pe="$1\0$2";function fe(e){let t=e.trim();t=t.replace(ce,pe).replace(ue,pe),t=t.replace(de,"\0");let n=0,r=t.length;for(;"\0"===t.charAt(n);)n++;if(n===r)return[];for(;"\0"===t.charAt(r-1);)r--;return t.slice(n,r).split(/\0/g)}function ge(e){const t=fe(e);for(let e=0;ei;){const t=o-1,n=e.charAt(t);if(!s.includes(n))break;o=t}return[e.slice(0,i),n(e.slice(i,o)),e.slice(o)]}(e,t);return n+r.map((i=t?.locale,!1===i?e=>e.toLowerCase():e=>e.toLocaleLowerCase(i))).join(t?.delimiter??" ")+s;var i}function ye(e){this?.sdk&&e&&(this.sdk=Object.assign({},this.sdk,e))}const _e="win32"===navigator.platform.toLowerCase()?o.win32:o.posix,be=!1,ve="file://",we="lsp://",Ce=we+"logseq.io/";let Se;const xe=function(e,t){return me(e,{delimiter:"_",...t})};async function Ee(){return Se||(Se=await Ne("_callApplication","getAppPath"))}async function Te(){return be?localStorage.getItem("LSP_DEV_SDK_ROOT")||"http://localhost:8080":Me(await Ee(),"js")}function Ae(e){return e===Object(e)&&!Array.isArray(e)}function Oe(e,t){return le()(e,t,{arrayMerge:(e,t)=>t})}class ke extends i{_tag;_opts;_logs=[];constructor(e,t){super(),this._tag=e,this._opts=t}write(e,t,n){t?.length&&!0===t[t.length-1]&&(n=!0,t.pop());const r=t.reduce((e,t)=>(t&&t instanceof Error?e+=`${t.message} ${t.stack}`:e+=t.toString(),e),`[${this._tag}][${(new Date).toLocaleTimeString()}] `);this._logs.push([e,r]),(n||this._opts?.console)&&console?.["ERROR"===e?"error":"debug"](`${e}: ${r}`),this.emit("change")}clear(){this._logs=[],this.emit("change")}info(...e){this.write("INFO",e)}error(...e){this.write("ERROR",e)}warn(...e){this.write("WARN",e)}setTag(e){this._tag=e}toJSON(){return this._logs}}function Pe(e){return"string"==typeof e&&36===e.length&&/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(e)}function Le(){return"_"+Math.random().toString(36).substr(2,9)}function Ie(e){return e.charAt(0).toUpperCase()+e.slice(1)}function je(e){return e?(/^(http|file|lsp)/.test(e)||(e=ve+e),e):""}function Me(e,...t){try{const n=new URL(e);if(!n.origin)throw new Error(null);const r=_e.join(e.substr(n.origin.length),...t);return n.origin+r}catch(n){return _e.join(e,...t)}}function Re(e){return e?.match(/^(http?|lsp|assets):/)||(e=_e.normalize(e)),e}function De(e,t){let n,r,s=!1;const i=t=>n=>{e&&clearTimeout(e),t(n),s=!0},o=new Promise((s,o)=>{n=i(s),r=i(o),e&&(e=setTimeout(()=>r(new Error(`[deferred timeout] ${t}`)),e))});return{created:Date.now(),setTag:e=>t=e,resolve:n,reject:r,promise:o,get settled(){return s}}}function Ne(t,...n){t=t?.startsWith("_call")?t:t?.replace(/^[_$]+/,"");let r=xe(t);const s=window.logseq?.sdk,i=s&&Object.keys(s);let o={};const a=r?.split("_")?.[0];a&&i.includes(a)&&(r=r.replace(new RegExp(`^${a}_`),""),o=s?.[a]);const l=Object.assign({},window.logseq?.api,o,e),c=l[r]||window.apis[r]||l[t]||window.apis[t];if(!c)throw new Error(`Not existed method #${t}`);return"function"!=typeof c?c:c.apply(this,n)}function Ue(e,t){const n=t["data-injected-style"];let r=n&&document.querySelector(`[data-injected-style=${n}]`);if(!r)return r=document.createElement("style"),r.textContent=e,t&&Object.entries(t).forEach(([e,t])=>{r.setAttribute(e,t)}),document.head.append(r),()=>{document.head.removeChild(r)};r.textContent=e}const Fe=new Map;function $e(e,t,n){let r,s,i="";const o=this;"slot"in e?(i=e.slot,r=`#${i}`):"path"in e?r=e.path:s=!0;const a=`${o.id}--${e.key||Le()}`,l=a,c=s?document.body:r&&document.querySelector(r);if(!c)return console.error(`${this.debugTag} can not resolve selector target ${r}`),!1;if(!e.template)return void Fe.get(a)?.call(null);e.template=oe.sanitize(e.template,{ADD_TAGS:["iframe"],ALLOW_UNKNOWN_PROTOCOLS:!0,ADD_ATTR:["allow","src","allowfullscreen","frameborder","scrolling","target"]});let u,h,d=document.querySelector(`#${a}`),p=s?d?.querySelector(".ls-ui-float-content"):d;if(p){p.innerHTML=e.template,t&&Object.entries(t).forEach(([e,t])=>{d.setAttribute(e,t)});let n=null!=d.dataset.dx;return void(e.style&&Object.entries(e.style).forEach(([e,t])=>{n&&["left","top","bottom","right","width","height"].includes(e)||(d.style[e]=t)}))}if(d=document.createElement("div"),d.id=a,d.dataset.injectedUi=l||"",s?(p=document.createElement("div"),p.classList.add("ls-ui-float-content"),d.appendChild(p)):p=d,p.innerHTML=e.template,t&&Object.entries(t).forEach(([e,t])=>{d.setAttribute(e,t)}),e.style&&Object.entries(e.style).forEach(([e,t])=>{d.style[e]=t}),s&&(d.setAttribute("draggable","true"),d.setAttribute("resizable","true"),e.close&&(d.dataset.close=e.close),d.classList.add("lsp-ui-float-container","visible"),o._setupResizableContainer(d,l),h=o._setupDraggableContainer(d,{key:l,close:()=>u(),title:t?.title})),i&&e.reset){const e=Array.from(c.querySelectorAll("[data-injected-ui]")).map(e=>e.id);e?.forEach(e=>{Fe.get(e)?.call(null)})}return c.appendChild(d),["click","focus","focusin","focusout","blur","dblclick","keyup","keypress","keydown","change","input","contextmenu"].forEach(e=>{d.addEventListener(e,t=>{const n=t.target.closest(`[data-on-${e}]`);if(!n)return;const{preventDefault:r}=n.dataset,s=n.dataset[`on${Ie(e)}`];s&&o.caller?.callUserModel(s,We(n,t)),"true"===r?.toLowerCase()&&t.preventDefault()},!1)}),n?.({el:d,float:s}),u=()=>{h?.(),Fe.delete(a),c.removeChild(d)},Fe.set(a,u),u}function ze(e){if(!Fe.has(e))return;const t=Fe.get(e);try{t()}catch(t){console.warn("[CLEAN Injected UI] ",e,t)}}function He(){const e=document.head.querySelectorAll(`script[data-ref=${this.id}]`);e?.forEach(e=>e.remove())}function We(e,t){const n={};if(e){n.type=t.type;const r=e.dataset,s="rect";["value","id","className","dataset",s].forEach(t=>{let i;if(t===s){if(!r.hasOwnProperty(s))return;i=e.getBoundingClientRect().toJSON()}else i=e[t];"object"==typeof i&&(i={...i}),n[t]=i})}return n}function qe(e){const t=document.createElement("link");return t.rel="stylesheet",t.href=e,document.head.appendChild(t),()=>{try{document.head.removeChild(t)}catch(e){console.error(e)}}}function Be(e,t){const n=(t||[]).reduce((e,t)=>("default"in t&&(e[t.key]=t.default),e),{});return Object.assign(n,e)}function Ge(e){if("string"==typeof e)return e.trim().replace(/\s/g,"_").toLowerCase()}window.__injectedUIEffects=Fe;var Je=n(833),Ye=n.n(Je);const Ke="application/x-postmate-v1+json";let Ve=0;const Xe={handshake:1,"handshake-reply":1,call:1,emit:1,reply:1,request:1},Ze=(e,t)=>!("string"==typeof t&&"*"!==t&&e.origin!==t||!e.data||"object"==typeof e.data&&!("postmate"in e.data)||e.data.type!==Ke||!Xe[e.data.postmate]);class Qe{parent;frame;child;events={};childOrigin;listener;messagePort;addTransportListener(e){this.messagePort?(console.debug("[DEBUG] Using MessagePort for communication:",this.frame.src),this.messagePort.addEventListener("message",e),this.messagePort.start?.()):(console.debug("[DEBUG] Using postMessage for communication:",this.frame.src),this.parent.addEventListener("message",e,!1))}removeTransportListener(e){this.messagePort?this.messagePort.removeEventListener("message",e):this.parent.removeEventListener("message",e,!1)}postToChild(e){this.messagePort?this.messagePort.postMessage(e):this.child.postMessage(e,this.childOrigin)}constructor(e){this.parent=e.parent,this.frame=e.frame,this.child=e.child,this.childOrigin=e.childOrigin,this.messagePort=e.messagePort,this.listener=e=>{if(this.messagePort){if(!e?.data)return!1;if("object"==typeof e.data&&!("postmate"in e.data))return!1;if(e.data.type!==Ke)return!1;if(!Xe[e.data.postmate])return!1}else if(!Ze(e,this.childOrigin))return!1;const{data:t,name:n}=((e||{}).data||{}).value||{};"emit"===e.data.postmate&&n in this.events&&this.events[n].forEach(e=>{e.call(this,t)})},this.addTransportListener(this.listener)}get(e,...t){return new Promise((n,r)=>{const s=++Ve,i="number"==typeof tt.requestTimeout?tt.requestTimeout:1e4;let o;const a=e=>{e?.data?.uid===s&&"reply"===e.data.postmate&&(this.removeTransportListener(a),o&&clearTimeout(o),e.data.error?r(e.data.error):n(e.data.value))};this.addTransportListener(a),i>0&&(o=setTimeout(()=>{this.removeTransportListener(a),r(new Error(`Postmate: request timeout (${i}ms)`))},i)),this.postToChild({postmate:"request",type:Ke,property:e,args:t,uid:s})})}call(e,t){this.postToChild({postmate:"call",type:Ke,property:e,data:t})}on(e,t){this.events[e]||(this.events[e]=[]),this.events[e].push(t)}destroy(){this.removeTransportListener(this.listener);try{this.messagePort?.close()}catch(e){}this.frame.parentNode.removeChild(this.frame)}}class et{model;parent;parentOrigin;child;messagePort;listener;addTransportListener(e){this.messagePort?(this.messagePort.addEventListener("message",e),this.messagePort.start?.()):this.child.addEventListener("message",e,!1)}postToParent(e,t){if(this.messagePort)this.messagePort.postMessage(e);else if(t?.source){const n="null"===t.origin?"*":t.origin;t.source.postMessage(e,n)}else this.parent.postMessage(e,this.parentOrigin)}constructor(e){if(this.model=e.model,this.parent=e.parent,this.parentOrigin=e.parentOrigin,this.child=e.child,this.messagePort=e.messagePort,this.listener=e=>{if(this.messagePort){if(!e?.data)return;if("object"==typeof e.data&&!("postmate"in e.data))return;if(e.data.type!==Ke)return;if(!Xe[e.data.postmate])return}else if(!Ze(e,this.parentOrigin))return;const{property:t,uid:n,data:r,args:s}=e.data;"call"!==e.data.postmate?((e,t,n)=>{const r="function"==typeof e[t]?e[t].apply(null,n):e[t];return Promise.resolve(r)})(this.model,t,s).then(r=>{this.postToParent({property:t,postmate:"reply",type:Ke,uid:n,value:r},e)}).catch(r=>{this.postToParent({property:t,postmate:"reply",type:Ke,uid:n,error:r},e)}):t in this.model&&"function"==typeof this.model[t]&&this.model[t](r)},this.addTransportListener(this.listener),!this.messagePort){const e=t=>{const n=t.detail?.port;n&&(this.child.removeEventListener("message",this.listener,!1),this.messagePort=n,this.messagePort.addEventListener("message",this.listener),this.messagePort.start?.(),this.child.removeEventListener("postmate:channel-ready",e))};this.child.addEventListener("postmate:channel-ready",e)}}emit(e,t){this.postToParent({postmate:"emit",type:Ke,value:{name:e,data:t}})}}class tt{static debug=!1;static requestTimeout=1e4;container;parent;frame;child;childOrigin;url;model;static Model;messagePort;enableMessageChannel;constructor(e){this.container=e.container,this.url=e.url,this.parent=window,this.frame=document.createElement("iframe"),e.id&&(this.frame.id=e.id),e.name&&(this.frame.name=e.name),e.allow&&(this.frame.allow=e.allow),this.frame.classList.add.apply(this.frame.classList,e.classListArray||[]),this.container.appendChild(this.frame),this.child=this.frame.contentWindow,this.model=e.model||{},this.enableMessageChannel=!!e.enableMessageChannel}sendHandshake(e){const t=(e=>{const t=document.createElement("a");if(t.href=e,"file:"===t.protocol)return"*";const n=t.protocol.length>4?t.protocol:window.location.protocol,r=t.host.length?"80"===t.port||"443"===t.port?t.hostname:t.host:window.location.host;return t.origin||`${n}//${r}`})(e=e||this.url);let n,r=0;return new Promise((s,i)=>{const o="undefined"!=typeof MessageChannel&&"function"==typeof MessageChannel,a=this.enableMessageChannel&&o,l=e=>{if(!Ze(e,t))return!1;if("handshake-reply"===e.data.postmate){if(clearInterval(n),this.parent.removeEventListener("message",l,!1),this.childOrigin=e.origin,a)if(e?.ports?.length){const t=e.ports[0];t&&(this.messagePort=t,this.messagePort.start?.())}else if(e.data.acceptsMessageChannel){const e=new MessageChannel;this.messagePort=e.port1,this.messagePort.start?.(),this.child.postMessage({postmate:"setup-channel",type:Ke},t,[e.port2])}return s(new Qe(this))}return i("Failed handshake")};this.parent.addEventListener("message",l,!1);const c=()=>{r++;const e={postmate:"handshake",type:Ke,model:this.model,enableMessageChannel:a?1:0};this.child.postMessage(e,t),5===r&&clearInterval(n)};this.frame.addEventListener("load",()=>{c(),n=setInterval(c,500)}),this.frame.src=e})}destroy(){try{this.messagePort?.close()}catch(e){}this.frame.parentNode.removeChild(this.frame)}}class nt{child;model;parent;parentOrigin;messagePort;enableMessageChannel;constructor(e){this.child=window,this.model=e,this.parent=this.child.parent,this.enableMessageChannel=!1}sendHandshakeReply(){return new Promise((e,t)=>{const n=r=>{if(r.data.postmate){if("handshake"===r.data.postmate){this.child.removeEventListener("message",n,!1),this.enableMessageChannel=!!r.data?.enableMessageChannel,this.parentOrigin=r.origin;const t="undefined"!=typeof MessageChannel&&"function"==typeof MessageChannel;r.source.postMessage({postmate:"handshake-reply",type:Ke,acceptsMessageChannel:this.enableMessageChannel&&t?1:0},"null"===r.origin?"*":r.origin);const s=r.data.model;if(s&&Object.keys(s).forEach(e=>{this.model[e]=s[e]}),this.enableMessageChannel&&t){const e=t=>{if("setup-channel"===t.data?.postmate&&t.data?.type===Ke&&t.origin===this.parentOrigin){const n=t?.ports?.[0];n&&(this.messagePort=n,this.messagePort.start?.(),this.child.dispatchEvent(new CustomEvent("postmate:channel-ready",{detail:{port:this.messagePort}}))),this.child.removeEventListener("message",e,!1)}};this.child.addEventListener("message",e,!1)}return e(new et(this))}return t("Handshake Reply Failed")}};this.child.addEventListener("message",n,!1)})}}class rt{ctx;opts;constructor(e,t){this.ctx=e,this.opts=t}get ctxId(){return this.ctx.baseInfo.id}setItem(e,t){return this.ctx.caller.callAsync("api:call",{method:"write-plugin-storage-file",args:[this.ctxId,e,t,this.opts?.assets]})}getItem(e){return this.ctx.caller.callAsync("api:call",{method:"read-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}removeItem(e){return this.ctx.caller.call("api:call",{method:"unlink-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}allKeys(){return this.ctx.caller.callAsync("api:call",{method:"list-plugin-storage-files",args:[this.ctxId,this.opts?.assets]})}clear(){return this.ctx.caller.call("api:call",{method:"clear-plugin-storage-files",args:[this.ctxId,this.opts?.assets]})}hasItem(e){return this.ctx.caller.callAsync("api:call",{method:"exist-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}}class st{ctx;constructor(e){this.ctx=e}get React(){return this.ensureHostScope().React}get ReactDOM(){return this.ensureHostScope().ReactDOM}get Components(){return{Editor:this.ensureHostScope().logseq.sdk.experiments.cp_page_editor}}get Utils(){const e=this.ensureHostScope().logseq.sdk.utils,t=t=>e[xe(t)];return{toClj:t("toClj"),jsxToClj:t("jsxToClj"),toJs:t("toJs"),toKeyword:t("toKeyword"),toSymbol:t("toSymbol")}}get pluginLocal(){return this.ensureHostScope().LSPluginCore.ensurePlugin(this.ctx.baseInfo.id)}invokeExperMethod(e,...t){const n=this.ensureHostScope();e=xe(e)?.toLowerCase();const r=n.logseq.api["exper_"+e]||n.logseq.sdk.experiments[e];return r?.apply(n,t)}async loadScripts(...e){(e=e.map(e=>e?.startsWith("http")?e:this.ctx.resolveResourceFullUrl(e))).unshift(this.ctx.baseInfo.id),await this.invokeExperMethod("loadScripts",...e)}registerFencedCodeRenderer(e,t){return this.invokeExperMethod("registerFencedCodeRenderer",this.ctx.baseInfo.id,e,t)}registerDaemonRenderer(e,t){return this.invokeExperMethod("registerDaemonRenderer",this.ctx.baseInfo.id,e,t)}registerHostedRenderer(e,t){return this.invokeExperMethod("registerHostedRenderer",this.ctx.baseInfo.id,e,t)}registerSidebarRenderer(e,t){return e=`_sidebar.${e}`,t.type="sidebar",this.registerHostedRenderer(e,t)}registerRouteRenderer(e,t){return this.invokeExperMethod("registerRouteRenderer",this.ctx.baseInfo.id,e,t)}registerExtensionsEnhancer(e,t){const n=this.ensureHostScope();return"katex"===e&&n.katex&&t(n.katex).catch(console.error),this.invokeExperMethod("registerExtensionsEnhancer",this.ctx.baseInfo.id,e,t)}ensureHostScope(){try{window.top}catch(e){console.error("Can not access host scope!")}return window.top}}const it=e=>`task_callback_${e}`;class ot{_client;_requestId;_requestOptions;_promise;_aborted=!1;constructor(e,t,n={}){this._client=e,this._requestId=t,this._requestOptions=n,this._promise=new Promise((e,t)=>{if(!this._requestId)return t(null);this._client.once(it(this._requestId),n=>{n&&n instanceof Error?t(n):e(n)})});const{success:r,fail:s,final:i}=this._requestOptions;this._promise.then(e=>{r?.(e)}).catch(e=>{s?.(e)}).finally(()=>{i?.()})}abort(){this._requestOptions.abortable&&!this._aborted&&(this._client.ctx._execCallableAPI("http_request_abort",this._requestId),this._aborted=!0)}get promise(){return this._promise}get client(){return this._client}get requestId(){return this._requestId}}class at extends s{_ctx;constructor(e){super(),this._ctx=e,this.ctx.caller.on("#lsp#request#callback",e=>{const t=e?.requestId;t&&this.emit(it(t),e?.payload)})}static createRequestTask(e,t,n){return new ot(e,t,n)}async _request(e){const t=this.ctx.baseInfo.id,{success:n,fail:r,final:s,...i}=e,o=this.ctx.Experiments.invokeExperMethod("request",t,i),a=at.createRequestTask(this.ctx.Request,o,e);return i.abortable?a:a.promise}get ctx(){return this._ctx}}const lt=Array.isArray,ct="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g;var ut="object"==typeof self&&self&&self.Object===Object&&self;const ht=ct||ut||Function("return this")(),dt=ht.Symbol;var pt=Object.prototype,ft=pt.hasOwnProperty,gt=pt.toString,mt=dt?dt.toStringTag:void 0;var yt=Object.prototype.toString;var _t=dt?dt.toStringTag:void 0;const bt=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":_t&&_t in Object(e)?function(e){var t=ft.call(e,mt),n=e[mt];try{e[mt]=void 0;var r=!0}catch(e){}var s=gt.call(e);return r&&(t?e[mt]=n:delete e[mt]),s}(e):function(e){return yt.call(e)}(e)},vt=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)},wt=function(e){if(!vt(e))return!1;var t=bt(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t},Ct=ht["__core-js_shared__"];var St,xt=(St=/[^.]+$/.exec(Ct&&Ct.keys&&Ct.keys.IE_PROTO||""))?"Symbol(src)_1."+St:"";var Et=Function.prototype.toString;const Tt=function(e){if(null!=e){try{return Et.call(e)}catch(e){}try{return e+""}catch(e){}}return""};var At=/^\[object .+?Constructor\]$/,Ot=Function.prototype,kt=Object.prototype,Pt=Ot.toString,Lt=kt.hasOwnProperty,It=RegExp("^"+Pt.call(Lt).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");const jt=function(e){return!(!vt(e)||(t=e,xt&&xt in t))&&(wt(e)?It:At).test(Tt(e));var t},Mt=function(e,t){var n=function(e,t){return null==e?void 0:e[t]}(e,t);return jt(n)?n:void 0},Rt=function(){try{var e=Mt(Object,"defineProperty");return e({},"",{}),e}catch(e){}}(),Dt=function(e,t,n){for(var r=-1,s=Object(e),i=n(e),o=i.length;o--;){var a=i[++r];if(!1===t(s[a],a,s))break}return e},Nt=function(e){return null!=e&&"object"==typeof e},Ut=function(e){return Nt(e)&&"[object Arguments]"==bt(e)};var Ft=Object.prototype,$t=Ft.hasOwnProperty,zt=Ft.propertyIsEnumerable;const Ht=Ut(function(){return arguments}())?Ut:function(e){return Nt(e)&&$t.call(e,"callee")&&!zt.call(e,"callee")};var Wt="object"==typeof exports&&exports&&!exports.nodeType&&exports,qt=Wt&&"object"==typeof module&&module&&!module.nodeType&&module,Bt=qt&&qt.exports===Wt?ht.Buffer:void 0;const Gt=(Bt?Bt.isBuffer:void 0)||function(){return!1};var Jt=/^(?:0|[1-9]\d*)$/;const Yt=function(e,t){var n=typeof e;return!!(t=null==t?9007199254740991:t)&&("number"==n||"symbol"!=n&&Jt.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=9007199254740991};var Vt={};Vt["[object Float32Array]"]=Vt["[object Float64Array]"]=Vt["[object Int8Array]"]=Vt["[object Int16Array]"]=Vt["[object Int32Array]"]=Vt["[object Uint8Array]"]=Vt["[object Uint8ClampedArray]"]=Vt["[object Uint16Array]"]=Vt["[object Uint32Array]"]=!0,Vt["[object Arguments]"]=Vt["[object Array]"]=Vt["[object ArrayBuffer]"]=Vt["[object Boolean]"]=Vt["[object DataView]"]=Vt["[object Date]"]=Vt["[object Error]"]=Vt["[object Function]"]=Vt["[object Map]"]=Vt["[object Number]"]=Vt["[object Object]"]=Vt["[object RegExp]"]=Vt["[object Set]"]=Vt["[object String]"]=Vt["[object WeakMap]"]=!1;var Xt="object"==typeof exports&&exports&&!exports.nodeType&&exports,Zt=Xt&&"object"==typeof module&&module&&!module.nodeType&&module,Qt=Zt&&Zt.exports===Xt&&ct.process,en=function(){try{return Zt&&Zt.require&&Zt.require("util").types||Qt&&Qt.binding&&Qt.binding("util")}catch(e){}}(),tn=en&&en.isTypedArray;const nn=tn?(rn=tn,function(e){return rn(e)}):function(e){return Nt(e)&&Kt(e.length)&&!!Vt[bt(e)]};var rn,sn=Object.prototype.hasOwnProperty;const on=function(e,t){var n=lt(e),r=!n&&Ht(e),s=!n&&!r&&Gt(e),i=!n&&!r&&!s&&nn(e),o=n||r||s||i,a=o?function(e,t){for(var n=-1,r=Array(e);++n-1},gn.prototype.set=function(e,t){var n=this.__data__,r=pn(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this};const mn=gn,yn=Mt(ht,"Map"),_n=Mt(Object,"create");var bn=Object.prototype.hasOwnProperty;var vn=Object.prototype.hasOwnProperty;function wn(e){var t=-1,n=null==e?0:e.length;for(this.clear();++ta))return!1;var c=i.get(e),u=i.get(t);if(c&&u)return c==t&&u==e;var h=-1,d=!0,p=2&n?new kn:void 0;for(i.set(e,t),i.set(t,e);++h(lt(e?.blocks)&&(e.blocks=e.blocks.map(e=>e&&Mr(e,(e,t)=>`block/${t}`))),e)},rebuildBlocksIndice:{f:"onIndiceInit",args:["graph","blocks"]},transactBlocks:{f:"onBlocksChanged",args:["graph","data"]},truncateBlocks:{f:"onIndiceReset",args:["graph"]},removeDb:{f:"onGraph",args:["graph"]}}).forEach(([n,r])=>{const s=(e=>`service:search:${e}:${t.name}`)(n);e.caller.on(s,async n=>{if(wt(t?.[r.f])){let i=null;try{i=await t[r.f].apply(t,(r.args||[]).map(e=>{if(n){if(!0===e)return n;if(n.hasOwnProperty(e)){const t=n[e];return delete n[e],t}}})),r.transformOutput&&(i=r.transformOutput(i))}catch(e){console.error("[SearchService] ",e),i=e}finally{r.reply&&e.caller.call(`${s}:reply`,i)}}})})}}const Dr=Symbol.for("proxy-continue"),Nr=Ye()("LSPlugin:user"),Ur=new ke("",{console:!0});function Fr(e,t,n){const{key:r,label:s,desc:i,palette:o,keybinding:a,extras:l}=t;if("function"!=typeof n)return this.logger.error(`${r||s}: command action should be function.`),!1;const c=Ge(r);if(!c)return this.logger.error(`${s}: command key is required.`),!1;const u=`SimpleCommandHook${c}${++qr}`;this.Editor["on"+u](n),this.caller?.call("api:call",{method:"register-plugin-simple-command",args:[this.baseInfo.id,[{key:c,label:s,type:e,desc:i,keybinding:a,extras:l},["editor/hook",u]],o]})}function $r(e){return!!Pe(e)||(Ur.error(`#${e} is not a valid UUID string.`),!1)}let zr=null,Hr=new Map;const Wr={async getInfo(e){return zr||(zr=await this._execCallableAPIAsync("get-app-info")),"string"==typeof e?zr[e]:zr},registerCommand:Fr,registerSearchService(e){if(Hr.has(e.name))throw new Error(`SearchService: #${e.name} has registered!`);Hr.set(e.name,new Rr(this,e))},registerCommandPalette(e,t){const{key:n,label:r,keybinding:s}=e;return Fr.call(this,"$palette$",{key:n,label:r,palette:!0,keybinding:s},t)},registerCommandShortcut(e,t,n={}){"string"==typeof e&&(e={mode:"global",binding:e});const{binding:r}=e,s="$shortcut$",i=n.key||s+xe(r?.toString());return Fr.call(this,s,{...n,key:i,palette:!1,keybinding:e},t)},registerUIItem(e,t){const n=this.baseInfo.id;this.caller?.call("api:call",{method:"register-plugin-ui-item",args:[n,e,t]})},registerPageMenuItem(e,t){if("function"!=typeof t)return!1;const n=e+"_"+this.baseInfo.id,r=e;Fr.call(this,"page-menu-item",{key:n,label:r},t)},onBlockRendererSlotted(e,t){if(!$r(e))return;const n=this.baseInfo.id,r=`hook:editor:${xe(`slot:${e}`)}`;return this.caller.on(r,t),this.App._installPluginHook(n,r),()=>{this.caller.off(r,t),this.App._uninstallPluginHook(n,r)}},invokeExternalPlugin(e,...t){if(e=e?.trim(),!e)return;let[n,r]=e.split(".");if(!["models","commands"].includes(r?.toLowerCase()))throw new Error("Type only support '.models' or '.commands' currently.");const s=e.replace(`${n}.${r}.`,"");if(!n||!r||!s)throw new Error(`Illegal type of #${e} to invoke external plugin.`);return this._execCallableAPIAsync("invoke_external_plugin_cmd",n,r.toLowerCase(),s,t)},setFullScreen(e){const t=(...e)=>this._callWin("setFullScreen",...e);"toggle"===e?this._callWin("isFullScreen").then(e=>{e?t():t(!0)}):e?t(!0):t()}};let qr=0;const Br={newBlockUUID(){return this._execCallableAPIAsync("new_block_uuid")},isPageBlock:e=>e.uuid&&e.hasOwnProperty("name"),registerSlashCommand(e,t){Nr("Register slash command #",this.baseInfo.id,e,t),"function"==typeof t&&(t=[["editor/clear-current-slash",!1],["editor/restore-saved-cursor"],["editor/hook",t]]),t=t.map(e=>{const[t,...n]=e;if("editor/hook"===t){let r=n[0],s=()=>{this.caller?.callUserModel(r)};"function"==typeof r&&(s=r);const i=`SlashCommandHook${t}${++qr}`;e[1]=i,this.Editor["on"+i](s)}return e}),this.caller?.call("api:call",{method:"register-plugin-slash-command",args:[this.baseInfo.id,[e,t]]})},registerBlockContextMenuItem(e,t){if("function"!=typeof t)return!1;const n=e+"_"+this.baseInfo.id;Fr.call(this,"block-context-menu-item",{key:n,label:e},t)},registerHighlightContextMenuItem(e,t,n){if("function"!=typeof t)return!1;const r=e+"_"+this.baseInfo.id;Fr.call(this,"highlight-context-menu-item",{key:r,label:e,extras:n},t)},scrollToBlockInPage(e,t,n){const r="block-content-"+t;n?.replaceState?this.App.replaceState("page",{name:e},{anchor:r}):this.App.pushState("page",{name:e},{anchor:r})}},Gr={onBlockChanged(e,t){if(!$r(e))return;const n=this.baseInfo.id,r=`hook:db:${xe(`block:${e}`)}`,s=({block:n,txData:r,txMeta:s})=>{n.uuid===e&&t(n,r,s)};return this.caller.on(r,s),this.App._installPluginHook(n,r),()=>{this.caller.off(r,s),this.App._uninstallPluginHook(n,r)}},datascriptQuery(e,...t){return t.pop(),t?.some(e=>"function"==typeof e)?this.Experiments.ensureHostScope().logseq.api.datascript_query(e,...t):this._execCallableAPIAsync("datascript_query",e,...t)}},Jr={},Yr={},Kr={},Vr={makeSandboxStorage(){return new rt(this,{assets:!0})}};class Xr extends i{_baseInfo;_caller;_version=LIB_VERSION;_debugTag="";_settingsSchema;_connected=!1;_ui=new Map;_mFileStorage;_mRequest;_mExperiments;_beforeunloadCallback;constructor(e,t){super(),this._baseInfo=e,this._caller=t,t.on("sys:ui:visible",e=>{e?.toggle&&this.toggleMainUI()}),t.on("settings:changed",e=>{const t={...this.settings||{}},n={...e||{}};this._baseInfo={...this._baseInfo,settings:n},this.emit("settings:changed",n,t)}),t.on("beforeunload",async e=>{const{actor:t,...n}=e,r=this._beforeunloadCallback;try{r&&await r(n),t?.resolve(null)}catch(e){this.logger.error("[beforeunload] ",e),t?.reject(e)}})}async ready(e,t){if(!this._connected)try{"function"==typeof e&&(t=e,e={});let n=await this._caller.connectToParent(e);const r=n?.settings;this._connected=!0,n=Oe(this._baseInfo,n),void 0!==r&&(n.settings=r),this._baseInfo=n,n?.id&&(this._debugTag=this._caller.debugTag=`#${n.id} [${n.name}]`,this.logger.setTag(this._debugTag)),this._settingsSchema&&(n.settings=Be(n.settings,this._settingsSchema),await this.useSettingsSchema(this._settingsSchema));try{await this._execCallableAPIAsync("setSDKMetadata",{version:this._version,runtime:"js"})}catch(e){console.warn(e)}t&&t.call(this,n)}catch(e){console.error(`${this._debugTag} [Ready Error]`,e)}}ensureConnected(){if(!this._connected)throw new Error("not connected")}beforeunload(e){"function"==typeof e&&(this._beforeunloadCallback=e)}provideModel(e){return this.caller._extendUserModel(e),this}provideTheme(e){return this.caller.call("provider:theme",e),this}provideStyle(e){return this.caller.call("provider:style",e),this}provideUI(e){return this.caller.call("provider:ui",e),this}useSettingsSchema(e){return this.connected&&this.caller.call("settings:schema",{schema:e,isSync:!0}),this._settingsSchema=e,this}updateSettings(e){this.caller.call("settings:update",e)}onSettingsChanged(e){const t="settings:changed";return this.on(t,e),()=>this.off(t,e)}showSettingsUI(){this.caller.call("settings:visible:changed",{visible:!0})}hideSettingsUI(){this.caller.call("settings:visible:changed",{visible:!1})}setMainUIAttrs(e){this.caller.call("main-ui:attrs",e)}setMainUIInlineStyle(e){this.caller.call("main-ui:style",e)}hideMainUI(e){const t={key:0,visible:!1,cursor:e?.restoreEditingCursor};this.caller.call("main-ui:visible",t),this.emit("ui:visible:changed",t),this._ui.set(t.key,t)}showMainUI(e){const t={key:0,visible:!0,autoFocus:e?.autoFocus};this.caller.call("main-ui:visible",t),this.emit("ui:visible:changed",t),this._ui.set(t.key,t)}toggleMainUI(){const e=this._ui.get(0);e&&e.visible?this.hideMainUI():this.showMainUI()}get version(){return this._version}get isMainUIVisible(){const e=this._ui.get(0);return Boolean(e&&e.visible)}get connected(){return this._connected}get baseInfo(){return this._baseInfo}get effect(){return this&&(this.baseInfo?.effect||!this.baseInfo?.iir)}get logger(){return Ur}get settings(){return this.baseInfo?.settings}get caller(){return this._caller}resolveResourceFullUrl(e){if(this.ensureConnected(),e)return e=e.replace(/^[.\\/]+/,""),Me(this._baseInfo.lsr,e)}_makeUserProxy(e,t){const n=this,r=this.caller;return new Proxy(e,{get(e,s,i){const o=e[s];return function(...e){if(o){0!==e?.length&&e.push(t);const r=o.apply(n,e);if(r!==Dr)return r}if(t){const i=s.toString().match(/^(once|off|on)/i);if(null!=i){const s=i[0].toLowerCase(),o=i.input,a="off"===s,l=n.baseInfo.id;let c=o.slice(s.length),u=e[0],h=e[1];"string"==typeof u&&"function"==typeof h&&(u=u.replace(/^logseq./,":"),c=`${c}${u}`,u=h,h=e[2]),c=`hook:${t}:${xe(c)}`,r[s](c,u);const d=()=>{r.off(c,u),r.listenerCount(c)||n.App._uninstallPluginHook(l,c)};return a?void d():(n.App._installPluginHook(l,c,h),d)}}let i=s;return["git","ui","assets","utils"].includes(t)&&(i=t+"_"+i),r.callAsync("api:call",{tag:t,method:i,args:e})}}})}_execCallableAPIAsync(e,...t){return this._caller.callAsync("api:call",{method:e,args:t})}_execCallableAPI(e,...t){this._caller.call("api:call",{method:e,args:t})}_callWin(...e){return this._execCallableAPIAsync("_callMainWin",...e)}#e;#t;#n;#r;#s;get App(){return this.#e?this.#e:this.#e=this._makeUserProxy(Wr,"app")}get Editor(){return this.#t?this.#t:this.#t=this._makeUserProxy(Br,"editor")}get DB(){return this.#n?this.#n:this.#n=this._makeUserProxy(Gr,"db")}get UI(){return this.#r?this.#r:this.#r=this._makeUserProxy(Yr,"ui")}get Utils(){return this.#s?this.#s:this.#s=this._makeUserProxy(Kr,"utils")}get Git(){return this._makeUserProxy(Jr,"git")}get Assets(){return this._makeUserProxy(Vr,"assets")}get FileStorage(){let e=this._mFileStorage;return e||(e=this._mFileStorage=new rt(this)),e}get Request(){let e=this._mRequest;return e||(e=this._mRequest=new at(this)),e}get Experiments(){let e=this._mExperiments;return e||(e=this._mExperiments=new st(this)),e}}if(null==window.__LSP__HOST__){const e=new ds(null);window.logseq=new Xr({},e)}const{importHTML:Zr,createSandboxContainer:Qr}=window.QSandbox||{};function es(e,t){return e.startsWith("http")?fetch(e,t):(e=e.replace("file://",""),new Promise(async(t,n)=>{try{const n=await window.apis.doAction(["readFile",e]);t({text:()=>n})}catch(e){console.error(e),n(e)}}))}class ts extends i{_pluginLocal;_frame;_root;_loaded=!1;_unmountFns=[];constructor(e){super(),this._pluginLocal=e,e._dispose(()=>{this._unmount()})}async load(){const{name:e,entry:t}=this._pluginLocal.options;if(this.loaded||!t)return;const{template:n,execScripts:r}=await Zr(t,{fetch:es});this._mount(n,document.body);const s=Qr(e,{elementGetter:()=>this._root?.firstChild}).instance.proxy;s.__shadow_mode__=!0,s.LSPluginLocal=this._pluginLocal,s.LSPluginShadow=this,s.LSPluginUser=s.logseq=new Xr(this._pluginLocal.toJSON(),this._pluginLocal.caller);const i=await r(s,!0);this._unmountFns.push(i.unmount),this._loaded=!0}_mount(e,t){const n=this._frame=document.createElement("div");n.classList.add("lsp-shadow-sandbox"),n.id=this._pluginLocal.id,this._root=n.attachShadow({mode:"open"}),this._root.innerHTML=`
${e}
`,t.appendChild(n),this.emit("mounted")}_unmount(){for(const e of this._unmountFns)e&&e.call(null)}destroy(){this.frame?.parentNode?.removeChild(this.frame)}get loaded(){return this._loaded}get document(){return this._root?.firstChild}get frame(){return this._frame}}const ns=Ye()("LSPlugin:caller"),rs="#await#response#",ss="#lspmsg#",is="#lspmsg#error#",os="#lspmsg#settings#",as="#lspmsg#beforeunload#",ls="#lspmsg#reply#",cs="#lspmsg#ready#",us=e=>`${ss}${e}`,hs=e=>`${rs}${e}`;class ds extends i{_pluginLocal;_connected=!1;_parent;_child;_shadow;_status;_userModel={};_syncGCTimer=null;_call;_callUserModel;_debugTag="";constructor(e){super(),this._pluginLocal=e,e&&(this._debugTag=e.debugTag)}async connectToChild(){if(this._connected)return;const{shadow:e}=this._pluginLocal;e?await this._setupShadowSandbox():await this._setupIframeSandbox()}async connectToParent(e={}){if(this._connected)return;const t=this,n=null!=this._pluginLocal;let r=0;const s=new Map,i=De(6e4),o=this._extendUserModel({[cs]:async e=>{o[us(e?.pid)]=({type:e,payload:n})=>{ns(`[host (_call) -> *user] ${this._debugTag}`,e,n),t.emit(e,n)},await i.resolve()},[as]:async e=>{const n=De(1e4);t.emit("beforeunload",Object.assign({actor:n},e)),await n.promise},[os]:async({payload:e})=>{t.emit("settings:changed",e)},[ss]:async({ns:e,type:n,payload:r})=>{ns(`[host (async) -> *user] ${this._debugTag} ns=${e} type=${n}`,r),e&&e.startsWith("hook")?t.emit(`${e}:${n}`,r):t.emit(n,r)},[ls]:({_sync:e,result:t})=>{if(ns(`[sync host -> *user] #${e}`,t),s.has(e)){const n=s.get(e);n&&(t?.hasOwnProperty(is)?n.reject(t[is]):n.resolve(t),s.delete(e))}},...e});if(n)return await i.promise,JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()));const a=new nt(o).sendHandshakeReply();return this._status="pending",await a.then(e=>{this._child=e,this._connected=!0,this._call=async(t,n={},i)=>{if(i){const e=++r;s.set(e,i),n._sync=e,i.setTag(`async call #${e}`),ns(`async call #${e}`)}return e.emit(us(o.baseInfo.id),{type:t,payload:n}),i?.promise},this._callUserModel=async(e,t)=>{try{o[e](t)}catch(t){ns(`call user model(${e}) not exist. #${this._debugTag}`)}},this._syncGCTimer=setInterval(()=>{if(s.size>100)for(const[e,t]of s)t.settled&&s.delete(e)},18e5)}).finally(()=>{this._status=void 0}),await i.promise,o.baseInfo}async call(e,t={}){return this._call?.call(this,e,t)}async callAsync(e,t={}){const n=De(1e4);return this._call?.call(this,e,t,n)}async callUserModel(e,...t){return this._callUserModel?.apply(this,[e,...t])}async callUserModelAsync(e,...t){return e=hs(e),this._callUserModel?.apply(this,[e,...t])}_calcLayoutPosition(e,t,n=0){const r=Math.max(e,n);return"number"==typeof t?`${Math.min(100*r/t,99)}%`:`${r}px`}async _applyContainerLayout(e){const t=(await(this._pluginLocal?._loadLayoutsData()))?.$$0;if(!t)return;const{width:n,height:r,left:s,top:i,vw:o,vh:a}=t;e.dataset.inited_layout="true",Object.assign(e.style,{width:`${n}px`,height:`${r}px`,left:this._calcLayoutPosition(s,o),top:this._calcLayoutPosition(i,a,45)})}async _setupIframeSandbox(){const e=this._pluginLocal,t=e.id,n=`${t}_lsp_main`,r=new URL(e.options.entry);r.searchParams.set("__v__",be?Date.now().toString():e.options.version),document.querySelector(`#${n}`)?.remove();const s=document.createElement("div");s.classList.add("lsp-iframe-sandbox-container"),s.id=n,s.dataset.pid=t;try{await this._applyContainerLayout(s)}catch(e){console.error("[Restore Layout Error]",e)}document.body.appendChild(s);const i=new tt({id:t+"_iframe",container:s,url:r.href,classListArray:["lsp-iframe-sandbox"],model:{baseInfo:JSON.parse(JSON.stringify(e.toJSON()))},allow:e.options.allow,enableMessageChannel:!0});let o;this._status="pending";const a=new Promise((e,t)=>{o=setTimeout(()=>{i.destroy(),t(new Error("handshake Timeout"))},8e3)});try{const t=await Promise.race([i.sendHandshake(),a]);this._parent=t,this._connected=!0,this.emit("connected"),t.on(us(e.id),({type:e,payload:t})=>{ns("[user -> *host] ",e,t),this._pluginLocal?.emit(e,t||{}),this._pluginLocal?.caller.emit(e,t||{})}),this._call=async(...n)=>{t.call(us(e.id),{type:n[0],payload:Object.assign(n[1]||{},{$$pid:e.id})})},this._callUserModel=async(e,...n)=>{if(e.startsWith(rs))return await t.get(e.replace(rs,""),...n);t.call(e,n?.[0])}}catch(e){throw ns("[iframe sandbox] error",e),e}finally{clearTimeout(o),this._status=void 0}}async _setupShadowSandbox(){const e=this._pluginLocal,t=this._shadow=new ts(e);try{this._status="pending",await t.load(),this._connected=!0,this.emit("connected"),this._call=async(t,n={},r)=>(r&&(n.actor=r),this._pluginLocal?.emit(t,Object.assign(n,{$$pid:e.id})),r?.promise),this._callUserModel=async(...e)=>{let t=e[0];t?.startsWith(rs)&&(t=t.replace(rs,""));const n=e[1]||{},r=this._userModel[t];"function"==typeof r&&await r.call(null,n)}}catch(e){throw ns("[shadow sandbox] error",e),e}finally{this._status=void 0}}_extendUserModel(e){return Object.assign(this._userModel,e)}_getSandboxIframeContainer(){return this._parent?.frame.parentNode}_getSandboxShadowContainer(){return this._shadow?.frame}_getSandboxIframeRoot(){return this._parent?.frame}_getSandboxShadowRoot(){return this._shadow?.frame}set debugTag(e){this._debugTag=e}async destroy(){let e=null;this._parent&&(e=this._getSandboxIframeContainer(),this._parent.destroy()),this._shadow&&(e=this._getSandboxShadowContainer(),this._shadow.destroy()),e?.parentNode?.removeChild(e),this._syncGCTimer&&(clearInterval(this._syncGCTimer),this._syncGCTimer=null),this._connected=!1,this._parent=void 0,this._child=void 0,this._shadow=void 0,this._call=void 0,this._callUserModel=void 0,this._status=void 0}}const ps=Ye()("LSPlugin:core"),fs="plugins";class gs extends i{_userPluginSettings;_schema;_settings={disabled:!1};constructor(e,t){super(),this._userPluginSettings=e,this._schema=t,Object.assign(this._settings,e)}get(e){return this._settings[e]}set(e,t){const n=Oe({},this._settings);this._settings[e]!==t&&(this._settings={...this._settings,[e]:t},this.emit("change",{...this._settings},n))}patch(e){if(!Ae(e))return;const t=Oe({},this._settings);this._settings=Oe(this._settings,e),this.emit("change",{...this._settings},t)}replace(e){const t=Oe({},this._settings);this._settings={disabled:!1,...e||{}},this.emit("change",{...this._settings},t)}set settings(e){this.replace(e)}get settings(){return this._settings}setSchema(e,t){this._schema=e,t&&this.replace(Be(this._settings,e))}reset(){const e=this.settings,t={};this._schema,this.replace(t),this.emit("reset",t,e)}toJSON(){return this._settings}}var ms;function ys(e,t){return t&&e.startsWith(ve+t)&&(e=Me(Ce,e.substr(ve.length+t.length))),e}!function(e){e.LOADING="loading",e.UNLOADING="unloading",e.LOADED="loaded",e.UNLOADED="unload",e.ERROR="error"}(ms||(ms={}));class _s extends Error{url;packageJsonPath;constructor(e,t={}){super(e),this.name="IllegalPluginPackageError",Object.assign(this,t)}}class bs extends Error{constructor(e){super(e),this.name="ExistedImportedPluginPackageError"}}class vs extends i{_options;_themeMgr;_ctx;_sdk={};_runtimeDisposes=[];_registrationDisposes=[];_id;_status=ms.UNLOADED;_loadErr;_localRoot;_dotSettingsFile;_caller;_logger=new ke("PluginLocal");_disposeSettingsObserver;constructor(e,t,n){var r;super(),this._options=e,this._themeMgr=t,this._ctx=n,this._id=e.key||Le(),this._disposeRegistration(async()=>{this._disposeSettingsObserver?.(),this._disposeSettingsObserver=void 0}),function(e){const t=e=>`settings:${e}`;e.on(t("schema"),({schema:t,isSync:n})=>{e.settingsSchema=t,e.settings?.setSchema(t,n)}),e.on(t("update"),t=>{t&&e.settings?.patch(t)}),e.on(t("visible:changed"),t=>{const n=t?.visible;Ne("set_focused_settings",n?e.id:null)})}(this),function(e){const t=e=>`main-ui:${e}`;e.on(t("visible"),({visible:t,toggle:n,cursor:r,autoFocus:s})=>{const i=e.getMainUIContainer();i?.classList[n?"toggle":t?"add":"remove"]("visible"),t?!e.shadow&&i&&!1!==s&&i.querySelector("iframe")?.contentWindow?.focus():i.ownerDocument.activeElement.blur(),r&&Ne("restore_editing_cursor")}),e.on(t("attrs"),t=>{const n=e.getMainUIContainer();Object.entries(t).forEach(([t,r])=>{n?.setAttribute(t,String(r)),"draggable"===t&&r&&e._dispose(e._setupDraggableContainer(n,{title:e.options.name,close:()=>{e.caller.call("sys:ui:visible",{toggle:!0})}})),"resizable"===t&&r&&e._dispose(e._setupResizableContainer(n))})}),e.on(t("style"),t=>{const n=e.getMainUIContainer(),r=!!n.dataset.inited_layout;Object.entries(t).forEach(([e,t])=>{r&&["left","top","bottom","right","width","height"].includes(e)||(n.style[e]=t)})})}(this),function(e){const t=e=>`provider:${e}`;let n=!1;e.on(t("theme"),t=>{e.themeMgr.registerTheme(e.id,t),n||(e._dispose(()=>{e.themeMgr.unregisterTheme(e.id)}),n=!0)}),e.on(t("style"),t=>{let n;"string"!=typeof t&&(n=t.key,t=t.style),t&&t.trim()&&e._dispose(Ue(t,{"data-injected-style":n?`${n}-${e.id}`:"","data-ref":e.id}))}),e.on(t("ui"),t=>{e._onHostMounted(()=>{const n=$e.call(e,t,Object.assign({"data-ref":e.id},t.attrs||{}),({el:t,float:n})=>{if(!n)return;const r=t.dataset.identity;e.layoutCore.move_container_to_top(r)});"function"==typeof n&&e._dispose(n)})})}(this),(r=this).on("api:call",async e=>{let t;try{window.$$callerPluginID=r.id,t=await Ne.apply(r,[e.method,...e.args])}catch(e){t={[is]:e}}finally{window.$$callerPluginID=void 0}if(r.shadow)return void(e.actor&&(t?.hasOwnProperty(is)?e.actor.reject(t[is]):e.actor.resolve(t)));const{_sync:n}=e;if(null!=n){const e=e=>{r.caller?.callUserModel(ls,{result:e,_sync:n})};Promise.resolve(t).then(e,e)}})}async _setupUserSettings(e){const{_options:t}=this,n=this._logger=new ke(`Loader:${this.debugTag}`);if(!t.settings||e||!this._disposeSettingsObserver)try{const n=()=>Ne("load_plugin_user_settings",this.id),[r,s]=await n();this._dotSettingsFile=r;let i=t.settings;i||(i=t.settings=new gs(s)),this._disposeSettingsObserver?.(),this._disposeSettingsObserver=void 0,e&&i.replace(s);const o=async e=>{ps("Settings changed",this.debugTag,e),e&&Ne("save_plugin_user_settings",this.id,e)};i.on("change",o);const a=()=>{i.off("change",o),this._disposeSettingsObserver===a&&(this._disposeSettingsObserver=void 0)};this._disposeSettingsObserver=a}catch(e){ps("[load plugin user settings Error]",e),n?.error(e)}}getMainUIContainer(){return this.shadow?this.caller?._getSandboxShadowContainer():this.caller?._getSandboxIframeContainer()}_resolveResourceFullUrl(e,t){if(!e?.trim())return;if(t=t||this._localRoot,this.isWebPlugin)return this.installedFromUserWebUrl?`${this.installedFromUserWebUrl}/${e}`:`https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${_e.join(t,e)}`;const n=/^(http|file)/;if(!n.test(e)){const r=_e.join(t,e);e=n.test(r)?r:ve+r}return!this.options.effect&&this.isInstalledInLocalDotRoot?ys(e,this.dotPluginsRoot):e}async _preparePackageConfigs(){const{url:e,webPkg:t}=this._options;let n=t;if(!n){let t;if(e){ps("prepare package root",e);try{n=await Ne("load_plugin_config",e),n?(n=JSON.parse(n),n||(t=`Parse package config error #${e}/package.json`)):t=`Parse package config error #${e}/package.json`}catch(e){t=e?.message||String(e)}}else t="Can not resolve package config location";if(t)throw new _s(t,{url:e,packageJsonPath:e?_e.join(e,"package.json"):void 0})}["name","author","repository","version","description","repo","title","effect","sponsors"].concat(this.isInstalledInLocalDotRoot?[]:["devEntry"]).forEach(e=>{this._options[e]=n[e]});const{repo:r,version:s}=this._options,i=this._localRoot=this.isWebPlugin?`${r||e}/${s}`:Re(e),o=n.logseq||{},a=o.entry||o.main||n.main;o.devEntry?(this._options.devEntry=o.devEntry,this._options.entry=o.devEntry):this._options.entry=this._resolveResourceFullUrl(a,i),o.mode&&(this._options.mode=o.mode);const l=o.title||n.title,c=o.icon||n.icon;if(this._options.title=l,this._options.icon=c&&this._resolveResourceFullUrl(c),this._options.theme=Boolean(o.theme||!!o.themes),this.isInstalledInLocalDotRoot)this._id=_e.basename(i);else if(!this.isWebPlugin)if(o.id)this._id=o.id;else{o.id=this.id;try{await Ne("save_plugin_package_json",e,{...n,logseq:o})}catch(e){ps("[save plugin ID Error] ",e)}}const{registeredPlugins:u,isRegistering:h}=this._ctx;if(h&&u.has(this.id))throw new bs(this.id);return async()=>{try{const e=o.themes;e&&await this._loadConfigThemes(Array.isArray(e)?e:[e])}catch(e){ps("[prepare package effect Error]",e)}}}async _tryToNormalizeEntry(){let{entry:e,settings:t,devEntry:n}=this.options;if(n=n||t?.get("_devEntry"),n)return void(this._options.entry=n);if(!e.endsWith(".js"))return;let r=null,s="write_user_tmp_file";this.isInstalledInLocalDotRoot&&(s="write_dotdir_file",r=this._localRoot.replace(this.dotPluginsRoot,""),r=_e.join(fs,r));const i=(new Date).getDay(),o=await Te(),a=await Ne(s,`${this._id}_index.html`,`\n\n \n \n logseq plugin entry\n ${this.isWebPlugin?`