Repository: czottmann/obsidian-actions-uri Branch: main Commit: 3228d8a8e694 Files: 205 Total size: 947.9 KB Directory structure: gitextract_9irawtlb/ ├── .beans/ │ ├── auri-14wv--milestone-160.md │ ├── auri-2nvq--milestone-153.md │ ├── auri-48pt--milestone-170.md │ ├── auri-7bdl--milestone-173.md │ ├── auri-b61p--milestone-190.md │ ├── auri-civ4--milestone-163.md │ ├── auri-fzy4--milestone-171.md │ ├── auri-p05h--milestone-164.md │ ├── auri-plwy--milestone-172.md │ ├── auri-q7z3--milestone-150.md │ ├── auri-qz7a--milestone-183.md │ ├── auri-slfp--milestone-162.md │ ├── auri-ujej--milestone-181.md │ ├── auri-vp9j--milestone-161.md │ ├── zco-1156--change-appending-below-headline-to-redefine-headline-section.md │ ├── zco-1182--using-if-exists-skip-and-silent-false-in-note-create-does-no.md │ ├── zco-120--research-obsidian-sync-behaviour-re-plugins-and-startup-prio.md │ ├── zco-1201--checking-for-the-existence-of-a-missing-note-from-afo-should.md │ ├── zco-1239--fix-note-create-if-exists-skip-not-being-honored-for-periodi.md │ ├── zco-1348--check-reports-of-note-properties-set-overwriting-existing-ke.md │ ├── zco-1496--research-user-report-about-note-get-diverging-wrongly-when-f.md │ ├── zco-1541--research-obsidian-1-10-ios-not-returning-xcu-calls.md │ ├── zco-166--set-up-a-testing-framework.md │ ├── zco-276--allow-relative-path-input-for-templates-templater-related-pa.md │ ├── zco-341--add-note-touch-route.md │ ├── zco-397--make-x-parameters-optional-in-command-execute-route.md │ ├── zco-398--create-route-for-getting-currently-focussed-note.md │ ├── zco-409--create-route-for-getting-currently-focussed-file.md │ ├── zco-410--create-route-for-getting-note-by-name.md │ ├── zco-423--add-feature-to-auto-install-new-version.md │ ├── zco-424--release-v1-5-0.md │ ├── zco-429--revamp-the-returning-of-errors-to-pick-from-a-fixed-list-of-.md │ ├── zco-455--add-obsidian-note-uri-to-note-result-objects.md │ ├── zco-480--add-2nd-level-check-to-template-file-parameter.md │ ├── zco-563--remove-use-of-global-app-from-plugin.md │ ├── zco-588--fix-api-calls-not-reaching-actions-uri-when-vault-needs-to-o.md │ ├── zco-606--search-and-replace-string-with-special-character.md │ ├── zco-613--add-periodic-note-support-to-note-properties.md │ ├── zco-616--research-uid-support-in-actions-id.md │ ├── zco-617--implement-zod-magic-to-streamline-targeting-a-note-using-fil.md │ ├── zco-623--removing-the-last-property-key-from-fm-results-in-fm-of.md │ ├── zco-624--replace-use-of-self-now-that-all-handlers-are-bound-to-this.md │ ├── zco-625--add-pn-support-to-note-list.md │ ├── zco-626--add-support-for-getting-most-recent-pn-to-note-open-note-get.md │ ├── zco-627--when-creating-a-note-during-note-append-templater-s-watch-fo.md │ ├── zco-628--lean-into-zod-s-transform-for-resolving-incoming-url-paramet.md │ ├── zco-629--add-result-selection-to-note-get-active.md │ ├── zco-630--add-create-headline-if-not-found-option-for-note-append-note.md │ ├── zco-633--fix-note-create-in-1-6-0.md │ ├── zco-634--fix-headline-block-append-prepend.md │ ├── zco-636--fix-error-code-on-note-get-from-500-back-to-404-to-prevent-a.md │ ├── zco-642--templater-path-parameter-doesn-t-work-as-assumed-intended.md │ ├── zco-645--replace-vault-modify-w-vault-process-to-prevent-race-conditi.md │ ├── zco-651--add-debugging-info-output-to-settings-page.md │ ├── zco-692--insert-below-headline-fails-if-the-headline-is-the-last-line.md │ ├── zco-694--add-optional-logging-to-file.md │ ├── zco-695--fix-empty-note-when-trying-to-append-prepend-content-to-non-.md │ ├── zco-707--check-plugin-against-new-obsidian-guidelines.md │ ├── zco-918--make-dataview-list-result-consistent.md │ ├── zco-937--opening-a-note-in-another-vault-only-opens-the-requested-vau.md │ ├── zco-976--fix-wonky-frontmatter-parser.md │ ├── zco-977--remove-dedicated-periodic-note-related-routes.md │ ├── zco-985--dataview-list-results-return-unexpected-nested-array.md │ └── zco-987--fix-periodic-notes-coming-out-raw-when-using-templater.md ├── .beans.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── release.yml ├── .gitignore ├── .mise.toml ├── .npmrc ├── .prettierrc ├── AGENTS.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin/ │ ├── prepare-route-docs.sh │ └── tag-release.fish ├── docs/ │ ├── 404.md │ ├── _config.yml │ ├── _includes/ │ │ └── head_custom.html │ ├── anatomy.md │ ├── callbacks.md │ ├── changes.md │ ├── faq.md │ ├── index.md │ ├── installation.md │ ├── license.md │ ├── parameters.md │ ├── routes/ │ │ ├── command.md │ │ ├── dataview.md │ │ ├── file.md │ │ ├── folder.md │ │ ├── info.md │ │ ├── note-properties.md │ │ ├── note.md │ │ ├── omnisearch.md │ │ ├── root.md │ │ ├── search.md │ │ ├── tags.md │ │ └── vault.md │ └── routes.md ├── esbuild.config.mjs ├── jest.config.js ├── manifest.json ├── package.json ├── src/ │ ├── constants.ts │ ├── main.ts │ ├── plugin-info.json │ ├── plugin-info.ts │ ├── routes/ │ │ ├── command.ts │ │ ├── dataview.ts │ │ ├── file.ts │ │ ├── folder.ts │ │ ├── info.ts │ │ ├── note/ │ │ │ └── create.ts │ │ ├── note-properties.ts │ │ ├── note.ts │ │ ├── omnisearch.ts │ │ ├── root.ts │ │ ├── search.ts │ │ ├── settings.ts │ │ ├── tags.ts │ │ └── vault.ts │ ├── routes.ts │ ├── schemata.ts │ ├── settings.ts │ ├── types/ │ │ ├── handlers.d.ts │ │ ├── obsidian-objects.d.ts │ │ ├── plugins.d.ts │ │ └── results.d.ts │ ├── types.d.ts │ └── utils/ │ ├── callbacks.ts │ ├── file-handling.ts │ ├── parameters.ts │ ├── periodic-notes-handling.ts │ ├── plugins.ts │ ├── results-handling.ts │ ├── routing.ts │ ├── search.ts │ ├── self.ts │ ├── string-handling.ts │ ├── time.ts │ ├── ui.ts │ └── zod.ts ├── tests/ │ ├── README.md │ ├── callback-server.ts │ ├── global-setup.ts │ ├── global-teardown.ts │ ├── helpers.ts │ ├── periodic-notes.ts │ ├── plugin-test-vault.original/ │ │ ├── .obsidian/ │ │ │ ├── app.json │ │ │ ├── appearance.json │ │ │ ├── community-plugins.json │ │ │ ├── core-plugins.json │ │ │ ├── graph.json │ │ │ ├── plugins/ │ │ │ │ ├── actions-uri/ │ │ │ │ │ └── manifest.json │ │ │ │ ├── auto-periodic-notes/ │ │ │ │ │ ├── data.json │ │ │ │ │ ├── main.js │ │ │ │ │ └── manifest.json │ │ │ │ ├── logstravaganza/ │ │ │ │ │ ├── data.json │ │ │ │ │ ├── main.js │ │ │ │ │ └── manifest.json │ │ │ │ ├── periodic-notes/ │ │ │ │ │ ├── data.json │ │ │ │ │ ├── main.js │ │ │ │ │ ├── manifest.json │ │ │ │ │ └── styles.css │ │ │ │ └── templater-obsidian/ │ │ │ │ ├── main.js │ │ │ │ ├── manifest.json │ │ │ │ └── styles.css │ │ │ └── workspace.json │ │ ├── 2024.md │ │ ├── 2025-04.md │ │ ├── 2025-05-18.md │ │ ├── 2025-Q1.md │ │ ├── 2025-W20.md │ │ ├── Welcome.md │ │ ├── _templates/ │ │ │ ├── Daily Note.md │ │ │ ├── Monthly Note.md │ │ │ ├── Quarterly Note.md │ │ │ ├── Weekly Note.md │ │ │ └── Yearly Note.md │ │ ├── any/ │ │ │ └── standard-parameters.test.ts │ │ └── note/ │ │ ├── append/ │ │ │ └── noteAppend.test.ts │ │ ├── create/ │ │ │ └── noteCreate.test.ts │ │ ├── delete/ │ │ │ └── noteDelete.test.ts │ │ ├── get/ │ │ │ ├── note-1.md │ │ │ └── noteGet.test.ts │ │ ├── get-active/ │ │ │ └── noteGetActive.test.ts │ │ ├── get-first-named/ │ │ │ └── noteGetFirstNamed.test.ts │ │ ├── list/ │ │ │ └── noteList.test.ts │ │ ├── open/ │ │ │ ├── note-1.md │ │ │ ├── note-2.md │ │ │ └── noteOpen.test.ts │ │ ├── prepend/ │ │ │ └── notePrepend.test.ts │ │ ├── rename/ │ │ │ └── noteRename.test.ts │ │ ├── search-regex-and-replace/ │ │ │ └── noteSearchRegexAndReplace.test.ts │ │ ├── search-string-and-replace/ │ │ │ └── noteSearchStringAndReplace.test.ts │ │ ├── touch/ │ │ │ └── noteTouch.test.ts │ │ └── trash/ │ │ └── noteTrash.test.ts │ └── types.d.ts ├── tsconfig.json └── versions.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .beans/auri-14wv--milestone-160.md ================================================ --- # auri-14wv title: "Milestone 1.6.0" status: completed type: milestone tags: - from-linear created_at: 2024-02-19T15:21:54.408Z updated_at: 2024-07-29T08:48:01.589Z --- [Release 1.6.0 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.6.0) --- ## Linear Metadata - **Linear Milestone ID**: 17bac8e0-179e-429d-b308-9960e4106fb3 - **Project**: Actions URI - **Target Date**: 2024-07-23 ================================================ FILE: .beans/auri-2nvq--milestone-153.md ================================================ --- # auri-2nvq title: "Milestone 1.5.3" status: completed type: milestone tags: - from-linear created_at: 2024-06-18T14:43:02.062Z updated_at: 2024-07-23T16:10:44.256Z --- [Release 1.5.3 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.5.3) --- ## Linear Metadata - **Linear Milestone ID**: 1bcf266a-3c0e-47de-b71d-13997fa08191 - **Project**: Actions URI - **Target Date**: 2024-06-18 ================================================ FILE: .beans/auri-48pt--milestone-170.md ================================================ --- # auri-48pt title: "Milestone 1.7.0" status: completed type: milestone tags: - from-linear created_at: 2024-07-23T16:09:54.114Z updated_at: 2025-02-10T09:40:19.870Z --- * [Release: Actions URI 1.7.0 - Carlo's Obsidian Plugins - ActionsDotWork Forum](https://forum.actions.work/t/release-actions-uri-1-7-0/579) * [Release 1.7.0 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.7.0) --- ## Linear Metadata - **Linear Milestone ID**: a9bd6114-1b01-434d-b2da-3e8915372e52 - **Project**: Actions URI - **Target Date**: 2025-01-30 ================================================ FILE: .beans/auri-7bdl--milestone-173.md ================================================ --- # auri-7bdl title: "Milestone 1.7.3" status: completed type: milestone tags: - from-linear created_at: 2025-04-30T10:57:45.630Z updated_at: 2025-05-21T13:45:17.739Z --- [Release 1.7.3 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.7.3) --- ## Linear Metadata - **Linear Milestone ID**: 5b1ab6bf-80ac-4eb0-bd5d-a18bac09f5e2 - **Project**: Actions URI - **Target Date**: 2025-04-30 ================================================ FILE: .beans/auri-b61p--milestone-190.md ================================================ --- # auri-b61p title: "Milestone 1.9.0" status: completed type: milestone tags: - from-linear created_at: 2025-05-19T16:45:38.505Z updated_at: 2025-05-19T16:45:38.505Z --- ## Linear Metadata - **Linear Milestone ID**: 24a7f3df-5e88-43ed-b3cf-23f2c55018a9 - **Project**: Actions URI ================================================ FILE: .beans/auri-civ4--milestone-163.md ================================================ --- # auri-civ4 title: "Milestone 1.6.3" status: completed type: milestone tags: - from-linear created_at: 2024-08-02T09:13:12.494Z updated_at: 2024-09-20T07:53:52.870Z --- * [Release: Actions URI 1.6.3 - Carlo's Obsidian Plugins - ActionsDotWork Forum](https://forum.actions.work/t/release-actions-uri-1-6-3/440) * [Release 1.6.3 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.6.3) --- ## Linear Metadata - **Linear Milestone ID**: cf976b09-6b3b-484b-89ea-bd4fa9a1e08b - **Project**: Actions URI - **Target Date**: 2024-08-02 ================================================ FILE: .beans/auri-fzy4--milestone-171.md ================================================ --- # auri-fzy4 title: "Milestone 1.7.1" status: completed type: milestone tags: - from-linear created_at: 2025-02-04T11:49:30.090Z updated_at: 2025-02-10T09:40:07.055Z --- * [Release: Actions URI 1.7.1 - Carlo's Obsidian Plugins - ActionsDotWork Forum](https://forum.actions.work/t/release-actions-uri-1-7-1/582) * [Release 1.7.1 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.7.1) --- ## Linear Metadata - **Linear Milestone ID**: 2e88d5a2-22e3-4ebe-9573-12d057825551 - **Project**: Actions URI - **Target Date**: 2025-02-04 ================================================ FILE: .beans/auri-p05h--milestone-164.md ================================================ --- # auri-p05h title: "Milestone 1.6.4" status: completed type: milestone tags: - from-linear created_at: 2024-09-20T07:53:27.424Z updated_at: 2025-02-10T09:39:34.502Z --- [Release 1.6.4 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.7.2) --- ## Linear Metadata - **Linear Milestone ID**: bcdff61e-6250-4bc0-a53e-058a7205202c - **Project**: Actions URI - **Target Date**: 2024-09-20 ================================================ FILE: .beans/auri-plwy--milestone-172.md ================================================ --- # auri-plwy title: "Milestone 1.7.2" status: completed type: milestone tags: - from-linear created_at: 2025-02-10T09:14:54.942Z updated_at: 2025-02-10T09:38:46.562Z --- [Release 1.7.2 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.7.2) --- ## Linear Metadata - **Linear Milestone ID**: af1e8918-a9cb-419f-a756-cc921fc64299 - **Project**: Actions URI - **Target Date**: 2025-02-10 ================================================ FILE: .beans/auri-q7z3--milestone-150.md ================================================ --- # auri-q7z3 title: "Milestone 1.5.0" status: completed type: milestone tags: - from-linear created_at: 2024-02-27T12:24:05.162Z updated_at: 2024-02-27T12:24:56.953Z --- ## Linear Metadata - **Linear Milestone ID**: c19a867c-64a6-4600-98b2-552ef45b6676 - **Project**: Actions URI - **Target Date**: 2024-02-19 ================================================ FILE: .beans/auri-qz7a--milestone-183.md ================================================ --- # auri-qz7a title: "Milestone 1.8.3" status: completed type: milestone tags: - from-linear created_at: 2025-06-06T17:02:21.713Z updated_at: 2025-08-05T13:30:44.463Z --- [Release 1.8.3 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.8.3) 1.8.2 had build issues, thus: instant update to 1.8.3. --- ## Linear Metadata - **Linear Milestone ID**: 467d1891-8978-4877-8abb-a5c897d353e3 - **Project**: Actions URI - **Target Date**: 2025-08-05 ================================================ FILE: .beans/auri-slfp--milestone-162.md ================================================ --- # auri-slfp title: "Milestone 1.6.2" status: completed type: milestone tags: - from-linear created_at: 2024-07-29T08:47:17.519Z updated_at: 2024-07-29T15:51:39.784Z --- [Release 1.6.2 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.6.2) --- ## Linear Metadata - **Linear Milestone ID**: 721530d3-1b2b-4e6f-9dde-fa4a3b361f43 - **Project**: Actions URI - **Target Date**: 2024-07-29 ================================================ FILE: .beans/auri-ujej--milestone-181.md ================================================ --- # auri-ujej title: "Milestone 1.8.1" status: completed type: milestone tags: - from-linear created_at: 2025-01-30T11:24:03.869Z updated_at: 2025-06-06T17:04:32.669Z --- [Release 1.8.1 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.8.1) 1.8.0 had smaller issues, thus: instant update to 1.8.1. --- ## Linear Metadata - **Linear Milestone ID**: 7f5972b5-aa64-4f4c-872b-738868437f7b - **Project**: Actions URI - **Target Date**: 2025-05-22 ================================================ FILE: .beans/auri-vp9j--milestone-161.md ================================================ --- # auri-vp9j title: "Milestone 1.6.1" status: completed type: milestone tags: - from-linear created_at: 2024-07-25T07:26:24.807Z updated_at: 2024-07-29T08:48:06.228Z --- [Release 1.6.1 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/releases/tag/1.6.1) --- ## Linear Metadata - **Linear Milestone ID**: 53d545b1-6076-44d7-8556-6eeac55a8b86 - **Project**: Actions URI - **Target Date**: 2024-07-25 ================================================ FILE: .beans/zco-1156--change-appending-below-headline-to-redefine-headline-section.md ================================================ --- # zco-1156 title: "Change appending below headline to redefine \"headline section\" as, say, H2 to next H2" status: in-progress type: task parent: auri-b61p tags: - from-linear created_at: 2025-04-17T14:34:01.592Z updated_at: 2025-05-19T16:45:38.570Z --- ## Linear Metadata - **Linear**: [ZCO-1156](https://linear.app/actionsdotwork/issue/ZCO-1156) - **Project**: Actions URI - **Milestone**: 1.9.0 - **Branch**: \`feature/zco-1156-change-appending-below-headline-to-redefine-headline-section\` --- Currently, appending below a headline assumes a "headline section" goes from the start of that headline right up to the beginning of the next headline – **any** headline. So if you have a H3 which is immediately followed by a H4, the H3 "section" goes from the beginning of the H3 right up to the following H4. There is no semantic analysis like *"H4 follows H3 therefore it's part of the H3 section"*. I've opted for the simple rule *"from headline to next headline"* which is easily explained. Some customers are confused because they expect a "H2 section" to go from that H2 to the next H2 (or EOF, whatever comes first), and right now it doesn't. If you implement that, mark it as "breaking change". ================================================ FILE: .beans/zco-1182--using-if-exists-skip-and-silent-false-in-note-create-does-no.md ================================================ --- # zco-1182 title: "Using `if-exists=skip` and `silent=false` in `/note/create` does not open the note in Obsidian" status: completed type: bug parent: auri-7bdl tags: - from-linear created_at: 2025-04-30T10:58:31.209Z updated_at: 2025-04-30T11:15:03.656Z --- ## Linear Metadata - **Linear**: [ZCO-1182](https://linear.app/actionsdotwork/issue/ZCO-1182) - **Project**: Actions URI - **Milestone**: 1.7.3 - **Branch**: \`feature/zco-1182-using-if-existsskip-and-silentfalse-in-notecreate-does-not\` --- Basically the issue is that if that note already exists, the plugin fails to resolve the parameter `file=abc` into the file path `abc.md`. --- ## Linked Commits - [`2db7ad0`](https://github.com/czottmann/obsidian-actions-uri/commit/2db7ad0e1dc51a81e4d7cb21d7b853a5adf54b63) — [NEW] Adds 1.7.3 - [`0f05d58`](https://github.com/czottmann/obsidian-actions-uri/commit/0f05d584daa7e05d577b28b3cd6af30ef7d04821) — [FIX] Adds missing resolving of input file path ================================================ FILE: .beans/zco-120--research-obsidian-sync-behaviour-re-plugins-and-startup-prio.md ================================================ --- # zco-120 title: "Research Obsidian Sync behaviour re plugins and startup priority" status: todo type: task tags: - from-linear - research created_at: 2023-09-17T13:45:40.298Z updated_at: 2025-05-19T10:41:29.198Z --- ## Linear Metadata - **Linear**: [ZCO-120](https://linear.app/actionsdotwork/issue/ZCO-120) - **Project**: Actions URI - **Branch**: \`feature/zco-120-research-obsidian-sync-behaviour-re-plugins-and-startup\` --- [https://discord.com/channels/686053708261228577/840286264964022302/1073620746888298577](https://discord.com/channels/686053708261228577/840286264964022302/1073620746888298577) ff. Licat: * Obsidian Sync doesn't run before other plugins * `app.internalPlugins.getPluginById('sync').instance.getStatus()` gets you the following: `'synced' | 'syncing' | 'error' | 'paused'` (make sure that `getPluginById()` doesn't return you `null`) * "Probably consider it working only when that gives you `'syncing' | 'error'`" ================================================ FILE: .beans/zco-1201--checking-for-the-existence-of-a-missing-note-from-afo-should.md ================================================ --- # zco-1201 title: "Checking for the existence of a missing note (from AFO) should not pop up an error toast" status: completed type: task parent: auri-ujej tags: - from-linear created_at: 2025-05-07T14:11:55.620Z updated_at: 2025-06-03T11:36:58.049Z --- ## Linear Metadata - **Linear**: [ZCO-1201](https://linear.app/actionsdotwork/issue/ZCO-1201) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-1201-checking-for-the-existence-of-a-missing-note-from-afo-should\` --- From [https://secure.helpscout.net/conversation/2926508265/742?viewId=7423769](https://secure.helpscout.net/conversation/2926508265/742?viewId=7423769): > I run a \`Check for existence of note\` action. It works as expected on the shortcut side, but I get an alert on the Obsidian vault (screenshot below). I know the note won't exist on first run, but just checking for its existence, an alert/popup is displayed, which is seen as an error. **Idea:** Introduce some sort of `showToast` parameter which defaults to `true`, but if it's set to `false`, do not pop up any toasts. ![](assets/zco-1201-file.png) ![](assets/zco-1201-file.png) --- ## Linked Commits - [`c325ec6`](https://github.com/czottmann/Actions-For-Obsidian/commit/c325ec6dbf4fffd84a21a1e91288c4827f53ef47) — [NEW] Implements new ActionsURI "hide-ui-notice-on-error" parameter - [`4a9edb9`](https://github.com/czottmann/obsidian-actions-uri/commit/4a9edb974aa1c5f8d8f4e0a6a822d2ed203999b0) — [NEW] Adds optional standard parameter "hide-ui-notice-on-error" ================================================ FILE: .beans/zco-1239--fix-note-create-if-exists-skip-not-being-honored-for-periodi.md ================================================ --- # zco-1239 title: "Fix `/note/create?…&if-exists=skip` not being honored for periodic notes" status: scrapped type: bug tags: - from-linear created_at: 2025-06-04T16:14:33.360Z updated_at: 2025-06-05T14:35:12.700Z --- ## Linear Metadata - **Linear**: [ZCO-1239](https://linear.app/actionsdotwork/issue/ZCO-1239) - **Project**: Actions URI - **Branch**: \`feature/zco-1239-fix-notecreateif-existsskip-not-being-honored-for-periodic\` --- [Weekly Note File Name mismatch · Issue #101 · czottmann/obsidian-actions-uri](https://github.com/czottmann/obsidian-actions-uri/issues/101) --- ## Progress ### 2025-06-05 (Carlo Zottmann) > Not a bug but the user misunderstanding the date format in the Periodic Notes plugin. ================================================ FILE: .beans/zco-1348--check-reports-of-note-properties-set-overwriting-existing-ke.md ================================================ --- # zco-1348 title: "Check reports of `/note-properties/set` overwriting existing keys" status: completed type: bug parent: auri-qz7a tags: - from-linear created_at: 2025-08-04T08:10:05.209Z updated_at: 2025-08-05T10:00:12.450Z --- ## Linear Metadata - **Linear**: [ZCO-1348](https://linear.app/actionsdotwork/issue/ZCO-1348) - **Project**: Actions URI - **Milestone**: 1.8.3 - **Branch**: \`feature/zco-1348-check-reports-of-note-propertiesset-overwriting-existing\` --- [https://phanpy.social/#/norden.social/s/114964623532850832?view=full](https://phanpy.social/#/norden.social/s/114964623532850832?view=full) > Moin, ich glaube, ich bin ggf. über einen Bug in AFO gestolpert. Obwohl bei der Aktion "Set properties" "add new keys, update existing" ausgewählt ist, löscht er bestehende, andere properties. > > ![image.png](assets/zco-1348-image.png) > > Vorher: > > ![image.png](assets/zco-1348-image.png) > > Nachher: > > ![image.png](assets/zco-1348-image.png) > > Frontmatter: > > ``` > --- > tags: > - journal/daily > datum: 2025-08-04 > arbeitstag: true > weekly: "[[2025-W32]]" > monthly: "[[2025-08-M]]" > yearly: "[[2025-Y]]" > titel: "%%titel%%" > mood: "%%laune%%" > ort: > - Gießen > --- > ``` --- ## Linked Commits - [`dfe5c80`](https://github.com/czottmann/obsidian-actions-uri/commit/dfe5c805b0d6d7fd56a58c72e312718c3eda636b) — [FIX] Update FM using Obs' own `processFrontMatter()` now --- ## Progress ### 2025-08-05 (Carlo Zottmann) > I've replaced my own naïve implementation with Obsidian's own `processFrontMatter()` ([processFrontMatter - Developer Documentation](https://docs.obsidian.md/Reference/TypeScript+API/FileManager/processFrontMatter)). ================================================ FILE: .beans/zco-1496--research-user-report-about-note-get-diverging-wrongly-when-f.md ================================================ --- # zco-1496 title: "Research user report about `/note/get`diverging wrongly when fetching current and most recent PN" status: todo type: bug parent: auri-b61p tags: - from-linear - forum created_at: 2025-09-24T09:15:03.636Z updated_at: 2025-10-07T15:34:11.035Z --- ## Linear Metadata - **Linear**: [ZCO-1496](https://linear.app/actionsdotwork/issue/ZCO-1496) - **Project**: Actions URI - **Milestone**: 1.9.0 - **Branch**: \`feature/zco-1496-research-user-report-about-notegetdiverging-wrongly-when\` --- Customer has issues with correctly (?) configured weekly PN – sounds like \[`/note/get`\]([https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget)) is returning different results: The "most recent" is fetched by calling `getAllWeeklyNotes()` in Liam's plugin, then [returning the most recent one from that list](https://github.com/czottmann/obsidian-actions-uri/blob/release/1.8.x/src/utils/periodic-notes-handling.ts#L94-L102). The "current", too, is calling `getAllWeeklyNotes()` but additionally specifying a timestamp for which a note should be returned ([Actions URI is passing the current time, there](https://github.com/czottmann/obsidian-actions-uri/blob/release/1.8.x/src/utils/periodic-notes-handling.ts#L112-L113)). So both are querying the same data source (good) but one is returning faulty results (bad). I don't see anything in the **obsidian-daily-notes-interface** plugin that'd cause such a discrepancy, tho: [obsidian-daily-notes-interface/src/weekly.ts at main · liamcain/obsidian-daily-notes-interface](https://github.com/liamcain/obsidian-daily-notes-interface/blob/main/src/weekly.ts#L87-L92) --- ## Progress ### 2025-10-07 (Carlo Zottmann) > The syntax `MM-DD` works differently in the Periodic Notes plugin and the aforementioned package that Actions URI is using to resolve the periodic notes. In the Periodic Note plugin, it means *“first day of the week (Sunday or Monday, depending on your settings)”*. > > In the the [**liamcain/obsidian-daily-notes-interface** npm package](https://github.com/liamcain/obsidian-daily-notes-interface) (again: same author as Periodic Notes itself!) the format is parsed [in the “correct” way, i.e. the same way it works anywhere in Obsidian and everywhere else](https://momentjs.com/docs/#/displaying/format/): `DD` is the current day of the month. > > That means the “current weekly note” request in AFO worked well for me yesterday (a Monday, i.e. start of the week), and both Periodic Notes and the developer-focussed obsidian-daily-notes-interface resolved `YYYY/MM/gggg-[W]ww (MM-DD)` to `2025/10/2025-W41 (10-06).md`. But today it’s broken again, since Periodic Notes still returns the same computed path as yesterday while obsidian-daily-notes-interface resolves it to `2025/10/2025-W41 (10-07).md`. > > Might need to fork and fix [liamcain/obsidian-daily-notes-interface: Package to create, open, and find daily notes from your Obsidian plugin](https://github.com/liamcain/obsidian-daily-notes-interface). 🫤 ================================================ FILE: .beans/zco-1541--research-obsidian-1-10-ios-not-returning-xcu-calls.md ================================================ --- # zco-1541 title: "Research Obsidian 1.10 (iOS) not returning XCU calls" status: in-progress type: task tags: - from-linear - ios-only created_at: 2025-10-27T16:12:43.459Z updated_at: 2025-12-12T11:45:36.676Z --- ## Linear Metadata - **Linear**: [ZCO-1541](https://linear.app/actionsdotwork/issue/ZCO-1541) - **Project**: Actions URI - **Branch**: \`feature/zco-1541-research-obsidian-110-ios-not-returning-xcu-calls\` --- * [Obsidian Mobile Insider Build 1.10 (iOS) is having issues w/ AFO - Actions For Obsidian - ActionsDotWork Forum](https://forum.actions.work/t/obsidian-mobile-insider-build/765/1) * [#703 Abstürze mit Obsidian 1.10 unter iOS 26 - HySpirit](https://support.actions.work/conversation/703?folder_id=7) I've [put up a question](https://discord.com/channels/686053708261228577/817515900349448202/1432408945980342342) in the official Obsidian Discord. --- ## Progress ### 2025-12-12 (Carlo Zottmann) > Fixed in [Obsidian 1.11.0 Mobile (Early access) - Obsidian](https://obsidian.md/changelog/2025-12-10-mobile-v1.11.0/)? ================================================ FILE: .beans/zco-166--set-up-a-testing-framework.md ================================================ --- # zco-166 title: "Set up a testing framework" status: completed type: task priority: normal parent: auri-ujej tags: - from-linear created_at: 2023-09-18T17:24:26.604Z updated_at: 2025-05-22T11:40:10.366Z --- ## Linear Metadata - **Linear**: [ZCO-166](https://linear.app/actionsdotwork/issue/ZCO-166) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-166-set-up-a-testing-framework\` --- ## Linked Commits - [`41cfdec`](https://github.com/czottmann/obsidian-actions-uri/commit/41cfdecfebf03bc9919a1f8d56c29e486fcab1b6) — Merge pull request #100 from czottmann/feature/zco-166-jest --- ## Progress ### 2025-05-15 (Carlo Zottmann) > Working with Cline & [Gemini 2.5 Flash Preview](https://openrouter.ai/google/gemini-2.5-flash-preview), I was able to cobble together an E2E testing setup which contains a pre-configured "blueprint" vault folder, which is copied to the right folder prior to Jest starting, then opens it in Obsidian, and sends out XCU calls. So far, so good, but the callbacks are opened by `window.open()` (in Obsidian) which hands them over to the OS which passes them on … to the browser. > > Need to figure out a way to set up a URL scheme for the test script. ### 2025-05-15 (Carlo Zottmann) > Started, thought about it for a few hours, stopped again. I don't know where to start. I'd love to test the routes which are the most important thing but they use so much Obsidian code which would need to be mocked … oof. > > What about E2E calls? That'd require an actual test vault, and a HTTP server as a receiver, so Jest could make XCU calls to that vault and listen for the return values…? ================================================ FILE: .beans/zco-276--allow-relative-path-input-for-templates-templater-related-pa.md ================================================ --- # zco-276 title: "Allow relative path input for Templates/Templater-related parameters" status: completed type: task parent: auri-14wv tags: - from-linear created_at: 2023-11-24T12:45:07.270Z updated_at: 2024-07-29T14:43:13.005Z --- ## Linear Metadata - **Linear**: [ZCO-276](https://linear.app/actionsdotwork/issue/ZCO-276) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-276-allow-relative-path-input-for-templatestemplater-related\` --- Both Templates and Templater allow the user to configure the templates path. So it makes sense to allow users to omit the folder path, methinks. --- Feature request by [Marco (@esamecar@social.lol)](https://social.lol/@esamecar): [https://social.lol/@esamecar/111464716336517053](https://social.lol/@esamecar/111464716336517053) ================================================ FILE: .beans/zco-341--add-note-touch-route.md ================================================ --- # zco-341 title: "Add `/note/touch` route" status: completed type: task parent: auri-q7z3 tags: - from-linear created_at: 2024-01-06T11:48:46.342Z updated_at: 2024-02-27T12:25:01.985Z --- ## Linear Metadata - **Linear**: [ZCO-341](https://linear.app/actionsdotwork/issue/ZCO-341) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-341-add-notetouch-route\` --- * no modifications * callbacks optional (!) Just touch the note so Obsidian will reload it in views or embeddings. --- ## Linked Commits - [`de6923c`](https://github.com/czottmann/obsidian-actions-uri/commit/de6923ce1268af75f46aa69adc4b47b4bfb05ddd) — [NEW] Adds docs for `/note/touch` - [`95bbb02`](https://github.com/czottmann/obsidian-actions-uri/commit/95bbb02c7ef59251f8a0aa54eaacda7203eef07e) — [NEW] Adds route `/note/touch` ================================================ FILE: .beans/zco-397--make-x-parameters-optional-in-command-execute-route.md ================================================ --- # zco-397 title: "Make `x-*` parameters optional in `/command/execute` route" status: completed type: task parent: auri-q7z3 tags: - from-linear created_at: 2024-02-01T14:30:22.218Z updated_at: 2024-02-27T12:25:02.040Z --- ## Linear Metadata - **Linear**: [ZCO-397](https://linear.app/actionsdotwork/issue/ZCO-397) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-397-make-x-parameters-optional-in-commandexecute-route\` --- Feature request: [obsidian-actions-uri · #84 · FR: Can command/execute have optional callback parameters instead of required?](https://github.com/czottmann/obsidian-actions-uri/issues/84) --- ## Linked Commits - [`d7c5bbe`](https://github.com/czottmann/obsidian-actions-uri/commit/d7c5bbe3bfa5ff540ea374a2b79703e770b39ee4) — [CHG] Makes `x-*` parameters optional in `/command/execute` route ================================================ FILE: .beans/zco-398--create-route-for-getting-currently-focussed-note.md ================================================ --- # zco-398 title: "Create route for getting currently focussed note" status: completed type: feature parent: auri-q7z3 tags: - from-linear created_at: 2024-02-01T14:50:44.649Z updated_at: 2024-02-27T12:25:02.101Z --- ## Linear Metadata - **Linear**: [ZCO-398](https://linear.app/actionsdotwork/issue/ZCO-398) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-398-create-route-for-getting-currently-focussed-note\` --- ## Linked Commits - [`54ec6c0`](https://github.com/czottmann/obsidian-actions-uri/commit/54ec6c0495c8a961732cd6d5efe224bb9f373964) — [NEW] Adds `/vault/get-active-file` - [`7e05e11`](https://github.com/czottmann/obsidian-actions-uri/commit/7e05e11718c0c19e226f4b3ffdbdd4c553c6928f) — [NEW] Adds `/note/get-active` route ================================================ FILE: .beans/zco-409--create-route-for-getting-currently-focussed-file.md ================================================ --- # zco-409 title: "Create route for getting currently focussed file" status: completed type: feature parent: auri-q7z3 tags: - from-linear created_at: 2024-02-06T11:56:36.672Z updated_at: 2024-02-27T12:25:01.921Z --- ## Linear Metadata - **Linear**: [ZCO-409](https://linear.app/actionsdotwork/issue/ZCO-409) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-409-create-route-for-getting-currently-focussed-file\` --- `/vault/get-active-file` --- ## Linked Commits - [`54ec6c0`](https://github.com/czottmann/obsidian-actions-uri/commit/54ec6c0495c8a961732cd6d5efe224bb9f373964) — [NEW] Adds `/vault/get-active-file` · czottmann/obsidian-actions-uri@54ec6c0 ================================================ FILE: .beans/zco-410--create-route-for-getting-note-by-name.md ================================================ --- # zco-410 title: "Create route for getting note by name" status: completed type: feature parent: auri-q7z3 tags: - from-linear created_at: 2024-02-06T18:30:36.921Z updated_at: 2024-02-27T12:25:01.866Z --- ## Linear Metadata - **Linear**: [ZCO-410](https://linear.app/actionsdotwork/issue/ZCO-410) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-410-create-route-for-getting-note-by-name\` --- `/note/get-first-named` --- ## Linked Commits - [`d8e898d`](https://github.com/czottmann/obsidian-actions-uri/commit/d8e898db4c760356f6f1c125bc6cc919cb56052b) — [NEW] Adds `sort-by=best-guess` as new default - [`64cfbd1`](https://github.com/czottmann/obsidian-actions-uri/commit/64cfbd1d70e4d8b9387bc5f190714097484d5ffe) — [NEW] Adds `/note/get-first-named` ================================================ FILE: .beans/zco-423--add-feature-to-auto-install-new-version.md ================================================ --- # zco-423 title: "Add feature to auto-install new version (?)" status: todo type: task tags: - from-linear created_at: 2024-02-17T18:25:03.473Z updated_at: 2024-02-19T15:11:30.495Z --- ## Linear Metadata - **Linear**: [ZCO-423](https://linear.app/actionsdotwork/issue/ZCO-423) - **Project**: Actions URI - **Branch**: \`feature/zco-423-add-feature-to-auto-install-new-version\` --- ``` const i = {repo: 'czottmann/obsidian-actions-uri', version: '1.4.2', manifest: app.plugins.manifests["actions-uri"]} app.plugins.installPlugin(i.repo, i.version, i.manifest) ``` Not sure if clever or feasible, needs proper planning. ================================================ FILE: .beans/zco-424--release-v1-5-0.md ================================================ --- # zco-424 title: "Release v1.5.0" status: completed type: task parent: auri-q7z3 tags: - from-linear - release created_at: 2024-02-19T12:35:32.236Z updated_at: 2024-02-27T12:25:01.800Z --- ## Linear Metadata - **Linear**: [ZCO-424](https://linear.app/actionsdotwork/issue/ZCO-424) - **Project**: Actions URI - **Milestone**: 1.5.0 - **Branch**: \`feature/zco-424-release-v150\` --- - [X] Update `CHANGELOG.md` - [X] Run `bin/tag-release.fish` - [X] Update GitHub release notes - [X] Post to Mastodon ================================================ FILE: .beans/zco-429--revamp-the-returning-of-errors-to-pick-from-a-fixed-list-of-.md ================================================ --- # zco-429 title: "Revamp the returning of errors to pick from a fixed list of possible errors" status: completed type: task parent: auri-ujej tags: - from-linear created_at: 2024-02-20T14:20:38.107Z updated_at: 2025-05-22T11:40:10.356Z --- ## Linear Metadata - **Linear**: [ZCO-429](https://linear.app/actionsdotwork/issue/ZCO-429) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-429-revamp-the-returning-of-errors-to-pick-from-a-fixed-list-of\` --- Introduce a fixed list of error states so it's easier for AFO to interact with returned Actions URI errors. Changing all numeric error codes on failure will be a **breaking** **change.** ```ts enum Failure { fileNotFound, noteNotFound, } type FailureDetails = { errorCode: number; errorMessage: string }; const failureDetails: { [key in Failure]: FailureDetails } = { [Failure.fileNotFound]: { errorCode: 1, errorMessage: "File not found" }, [Failure.noteNotFound]: { errorCode: 3, errorMessage: "Note not found" }, }; function failure(f: Failure): ErrorObject { return { isSuccess: false, ...failureDetails[f] }; } // Later return failure(Failure.fileNotFound) ``` Prep work for [ZCO-419](https://linear.app/actionsdotwork/issue/ZCO-419/return-nicer-error-messages-than-the-raw-actions-uri-errors) – having a --- ## Linked Commits - [`38fcafa`](https://github.com/czottmann/obsidian-actions-uri/commit/38fcafa4f6f0b4a3b039a9fd31b750ebd386ffc8) — [CHG] Renames `ErrorCode` enum cases to lowercase ================================================ FILE: .beans/zco-455--add-obsidian-note-uri-to-note-result-objects.md ================================================ --- # zco-455 title: "Add Obsidian note URI to note result objects" status: completed type: task parent: auri-ujej tags: - from-linear created_at: 2024-02-29T13:53:18.594Z updated_at: 2025-05-22T11:40:10.841Z --- ## Linear Metadata - **Linear**: [ZCO-455](https://linear.app/actionsdotwork/issue/ZCO-455) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-455-add-obsidian-note-uri-to-note-result-objects\` --- Add `result-link` to `HandlerFileSuccess`. Also, `export async function getNoteDetails(filepath:)` in `src/utils/file-handling.ts`. - [X] implementation - [X] adjust `/note/create` so it returns UID and both URIs - [X] add infos about new `result-` keys to all applicable docs - [X] add info to changelog --- ## Linked Commits - [`1872c36`](https://github.com/czottmann/obsidian-actions-uri/commit/1872c367c362184b75b3c2d67324e3c883791d7b) — [FIX] Adds missing `await` to `propertiesForFile()` call - [`730ee6e`](https://github.com/czottmann/obsidian-actions-uri/commit/730ee6ed3a56a18c0fa818e3b699b3452c3548c5) — [NEW] Adds info re `result-uri-*` return values in `/note/*` - [`4ef19ea`](https://github.com/czottmann/obsidian-actions-uri/commit/4ef19eaf845d703649e7c8cd6b0b662379b816d2) — [NEW] Adds `result-uri-*` to docs - [`3868dac`](https://github.com/czottmann/obsidian-actions-uri/commit/3868dacb8b4bf8a1a6681bd970777984fb183ca0) — [NEW] Adds note URIs to note return values - [`f040fba`](https://github.com/czottmann/obsidian-actions-uri/commit/f040fba88ac7f2e9f9423f3d95fd1e2b3cbc33d9) — [NEW] Adds note URIs to note return values ================================================ FILE: .beans/zco-480--add-2nd-level-check-to-template-file-parameter.md ================================================ --- # zco-480 title: "Add 2nd-level check to `template-file` parameter" status: completed type: task parent: auri-14wv tags: - from-linear created_at: 2024-03-26T11:40:28.746Z updated_at: 2024-07-23T15:13:07.972Z --- ## Linear Metadata - **Linear**: [ZCO-480](https://linear.app/actionsdotwork/issue/ZCO-480) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-480-add-2nd-level-check-to-template-file-parameter\` --- Change the Actions URI-internal handling of template parameters to prepend the template folder set in the Obsidian configuration as a fallback, as in: first check the original parameter, if not valid, prepend the template path, if still not valid, complain. See: * [/note/create | Actions URI](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate) * [Create Note apply Templater template - Template Path - Actions For Obsidian - ActionsDotWork Forum](https://forum.actions.work/t/create-note-apply-templater-template-template-path/328/2) --- ## Linked Commits - [`4e94c35`](https://github.com/czottmann/obsidian-actions-uri/commit/4e94c35d59df587cc3190fc584d589581bf78365) — [NEW] Adds 1.6 stuff - [`fb758ef`](https://github.com/czottmann/obsidian-actions-uri/commit/fb758eff78d503fdcd5956452cdbe7566235174b) — [NEW] Adds fallback for when `template-file` parameter contains template folder ================================================ FILE: .beans/zco-563--remove-use-of-global-app-from-plugin.md ================================================ --- # zco-563 title: "Remove use of global `app` from plugin" status: completed type: task priority: high tags: - from-linear created_at: 2024-05-10T18:00:40.171Z updated_at: 2024-05-11T10:46:26.711Z --- ## Linear Metadata - **Linear**: [ZCO-563](https://linear.app/actionsdotwork/issue/ZCO-563) - **Project**: Actions URI - **Branch**: \`feature/zco-563-remove-use-of-global-app-from-plugin\` --- See [changelog for Obsidian 1.6.0](https://obsidian.md/changelog/2024-05-09-desktop-v1.6.0/#:\~:text=Global%20app%20has%20been%20completely%20removed%20from%20the%20API%20spec%20(previously%20it%20was%20marked%20as%20deprecated).). --- ## Linked Commits - [`52c0ed7`](https://github.com/czottmann/obsidian-actions-uri/commit/52c0ed7d7419767bac8734f208d7a2088919aaeb) — [CHG] Replaces deprecated `global.app` references ================================================ FILE: .beans/zco-588--fix-api-calls-not-reaching-actions-uri-when-vault-needs-to-o.md ================================================ --- # zco-588 title: "Fix API calls not reaching Actions URI when vault needs to open first" status: completed type: bug parent: auri-2nvq tags: - from-linear created_at: 2024-06-14T08:32:34.590Z updated_at: 2024-06-18T14:43:12.669Z --- ## Linear Metadata - **Linear**: [ZCO-588](https://linear.app/actionsdotwork/issue/ZCO-588) - **Project**: Actions URI - **Milestone**: 1.5.3 - **Branch**: \`feature/zco-588-fix-api-calls-not-reaching-actions-uri-when-vault-needs-to\` --- When launching Obsidian, or switching to a closed, the API call will open the vault but the call itself won't be handled. Weirdly, this works fine for Omnisearch (e.g. [obsidian://omnisearch?query=foo&vault=Workbench](obsidian://omnisearch?query=foo&vault=Workbench)) and other plugins, but not for Actions URI. Find the problem and fix it. --- ## Linked Commits - [`161caf2`](https://github.com/czottmann/obsidian-actions-uri/commit/161caf241d5c062a041332e2b37b4219701d29fc) — [REL] Release 1.5.3 · czottmann/obsidian-actions-uri@161caf2 ================================================ FILE: .beans/zco-606--search-and-replace-string-with-special-character.md ================================================ --- # zco-606 title: "Search and Replace string with special character" status: completed type: bug parent: zco-531 tags: - from-linear - help-scout created_at: 2024-06-27T16:42:41.084Z updated_at: 2024-07-23T15:13:06.430Z --- ## Linear Metadata - **Linear**: [ZCO-606](https://linear.app/actionsdotwork/issue/ZCO-606) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-606-search-and-replace-string-with-special-character\` --- S&R in text mode doesn't find the search terms when they contain unescaped special regex chars (`^`, `$`, etc.). If they are escaped, text replacement works, though. Example, note contains the text *"Today is $Tuesday"*: * Works: search for `\$Tuesday` * Doesn't work: search for `$Tuesday` --- ## Linked Commits - [`227c483`](https://github.com/czottmann/obsidian-actions-uri/commit/227c483cb5a11a5cc92ce3bc3c7418056bf1e79a) — [FIX] Makes search/replace strings containing regex chars work ================================================ FILE: .beans/zco-613--add-periodic-note-support-to-note-properties.md ================================================ --- # zco-613 title: "Add Periodic Note support to `/note-properties`" status: completed type: task parent: zco-610 tags: - from-linear created_at: 2024-07-05T16:55:29.319Z updated_at: 2024-07-23T15:13:07.944Z --- ## Linear Metadata - **Linear**: [ZCO-613](https://linear.app/actionsdotwork/issue/ZCO-613) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-613-add-periodic-note-support-to-note-properties\` --- - [X] implementation - [X] documentation - [X] change log --- ## Linked Commits - [`d539f4c`](https://github.com/czottmann/obsidian-actions-uri/commit/d539f4c3bce7f81c9a85950d3d2138e2eac317b5) — [NEW] Adds docs for Periodic Note support - [`33f7b34`](https://github.com/czottmann/obsidian-actions-uri/commit/33f7b3465a1a4ffb1ef426c0fe2700430d568b9d) — [NEW] Adds Periodic Note support to `/note-properties` routes ================================================ FILE: .beans/zco-616--research-uid-support-in-actions-id.md ================================================ --- # zco-616 title: "Research UID support in Actions ID" status: completed type: task parent: zco-30 tags: - from-linear - research created_at: 2024-07-09T15:41:42.567Z updated_at: 2024-09-12T17:44:26.872Z --- ## Linear Metadata - **Linear**: [ZCO-616](https://linear.app/actionsdotwork/issue/ZCO-616) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-616-research-uid-support-in-actions-id\` --- By querying the `metadataCache` I can find all cached objects which contain a particular key (e.g., "uuid"), and then use their metadata cache key to find the related file: ```typescript const uid = Object.entries(app.metadataCache.metadataCache) .find(([key, cached]) => cached.frontmatter?.uuid && [cached.frontmatter.uuid].flat().includes(123)) ?.first() // → '002f3dfa59410f7cdd90519680cac1097d00473c7dcc4b15b1d7948e1b786e38' const filePath = Object.entries(app.metadataCache.fileCache) .find(([filePath, cache]) => cache.hash === uid) ?.first() ``` (It's possible to have more than one UID per file, for example when files have been merged, hence the array-based check.) --- *Idea: Lookup of* `"file" | "uid" | "periodic-note"` *via *[*zod preprocessor*](https://zod.dev/?id=preprocess)*, so that only the* `file` *parameter remains? This way, I could remove even more complexity from the routes, i.e. only have* `/note` *which would take care of both general notes and periodic notes, and support UIDs to boot?* --- ## Progress ### 2024-07-09 (Carlo Zottmann) > Follow-up: [ZCO-617](https://linear.app/actionsdotwork/issue/ZCO-617/implement-zod-preprocessor-to-streamline-targeting-a-note-using-file) ================================================ FILE: .beans/zco-617--implement-zod-magic-to-streamline-targeting-a-note-using-fil.md ================================================ --- # zco-617 title: "Implement zod magic to streamline targeting a note using `\"file\" | \"uid\" | \"periodic-note\"` parameters" status: completed type: task parent: zco-30 tags: - from-linear created_at: 2024-07-09T16:52:35.164Z updated_at: 2024-07-23T15:13:06.553Z --- ## Linear Metadata - **Linear**: [ZCO-617](https://linear.app/actionsdotwork/issue/ZCO-617) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-617-implement-zod-magic-to-streamline-targeting-a-note-using\` --- - [X] fix lookup by UID, metadataCache is unreliable after the first hit (because the file changes and its cached info is invalidated) - [X] create on append: if targeted by UID, use UID as title - [X] create on prepend: if targeted by UID, use UID as title - [X] make frontmatter UID key configurable in Actions URI settings - [X] implement new targeting in `/note-properties` routes - [X] add explicit PN plugin check before checking file path, and throw on missing plugin or deactivated feature - [X] ~~create: UID support?~~ Nope - [X] create: explicitly create PN when requested instead of general note at right path - [X] add UID to standard result parameters (if available) - [X] update change log - [X] update docs - [X] check mobile compatibility --- ## Linked Commits - [`52fd2b2`](https://github.com/czottmann/obsidian-actions-uri/commit/52fd2b2cd1783d20755ccca036d0fb756578b93a) — [NEW] Adds settings ================================================ FILE: .beans/zco-623--removing-the-last-property-key-from-fm-results-in-fm-of.md ================================================ --- # zco-623 title: "Removing the last property key from FM results in FM of `{}`" status: completed type: bug parent: auri-14wv tags: - from-linear created_at: 2024-07-16T14:49:58.051Z updated_at: 2024-07-23T15:13:06.505Z --- ## Linear Metadata - **Linear**: [ZCO-623](https://linear.app/actionsdotwork/issue/ZCO-623) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-623-removing-the-last-property-key-from-fm-results-in-fm-of\` --- Instead of a blank FM block (or no FM block), there's an empty JSON object now. --- ## Linked Commits - [`9fedd5f`](https://github.com/czottmann/obsidian-actions-uri/commit/9fedd5f43e954f44e63dd864ecf339f5964fd4a4) — [FIX] Sanitizes YAML FM generation to avoid faulty FM ================================================ FILE: .beans/zco-624--replace-use-of-self-now-that-all-handlers-are-bound-to-this.md ================================================ --- # zco-624 title: "Replace use of `self()` now that all handlers are bound to `this`" status: completed type: task tags: - from-linear created_at: 2024-07-16T15:58:50.452Z updated_at: 2024-07-23T15:13:06.566Z --- ## Linear Metadata - **Linear**: [ZCO-624](https://linear.app/actionsdotwork/issue/ZCO-624) - **Project**: Actions URI - **Branch**: \`feature/zco-624-replace-use-of-self-now-that-all-handlers-are-bound-to-this\` --- ## Linked Commits - [`49568b5`](https://github.com/czottmann/obsidian-actions-uri/commit/49568b5e4def7946a5109b08ee90f9af89628510) — [CHG] Replace use of `self()` in handlers w/ `this` ================================================ FILE: .beans/zco-625--add-pn-support-to-note-list.md ================================================ --- # zco-625 title: "Add PN support to `/note/list`" status: completed type: task parent: zco-621 tags: - from-linear created_at: 2024-07-17T14:07:36.026Z updated_at: 2024-07-23T15:13:07.952Z --- ## Linear Metadata - **Linear**: [ZCO-625](https://linear.app/actionsdotwork/issue/ZCO-625) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-625-add-pn-support-to-notelist\` --- ## Linked Commits - [`b278d95`](https://github.com/czottmann/obsidian-actions-uri/commit/b278d95c023509447f7dde063b915f816346fcbb) — [NEW] Adds docs on PN support in `/note/list` - [`dd4e535`](https://github.com/czottmann/obsidian-actions-uri/commit/dd4e5357aab0dbd2a1322598cfb3ee1d87d6d282) — [NEW] Adds support to `/note/list` ================================================ FILE: .beans/zco-626--add-support-for-getting-most-recent-pn-to-note-open-note-get.md ================================================ --- # zco-626 title: "Add support for getting most recent PN to `/note/open`, `/note/get`" status: completed type: task parent: zco-620 tags: - from-linear created_at: 2024-07-17T16:18:25.529Z updated_at: 2024-07-23T15:13:08.401Z --- ## Linear Metadata - **Linear**: [ZCO-626](https://linear.app/actionsdotwork/issue/ZCO-626) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-626-add-support-for-getting-most-recent-pn-to-noteopen-noteget\` --- ## Linked Commits - [`e9df64d`](https://github.com/czottmann/obsidian-actions-uri/commit/e9df64dfdd0ddabc766ac5daf44d59643e9ad65a) — [FIX] Fixes validations exploding (caused by imported native `enum` w/ zod) - [`8627a43`](https://github.com/czottmann/obsidian-actions-uri/commit/8627a432aec7dfc6a90c82d71d61e6f911c8668e) — [CHG] Refactors, renames, cleans up several PN-related functions - [`ae83ab6`](https://github.com/czottmann/obsidian-actions-uri/commit/ae83ab6971dd0b53cfc19c4bab2a6349d46d59e8) — [NEW] Adds support for getting most recent PN to `/note/open`, `/note/get` ================================================ FILE: .beans/zco-627--when-creating-a-note-during-note-append-templater-s-watch-fo.md ================================================ --- # zco-627 title: "When creating a note during `/note/append`, Templater's watch-folder-and-apply-template may cause race condition" status: completed type: bug parent: auri-14wv tags: - from-linear created_at: 2024-07-18T08:33:43.987Z updated_at: 2024-07-23T16:09:06.634Z --- ## Linear Metadata - **Linear**: [ZCO-627](https://linear.app/actionsdotwork/issue/ZCO-627) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-627-when-creating-a-note-during-noteappend-templaters-watch\` --- Via [Help Scout #553](https://secure.helpscout.net/conversation/2655031968/553): > I'm using the "Append Text to Periodic Note" action with "Create note if necessary". I'm now using the Templater plugin for my daily notes--is it possible to configure this creation to use that plugin? At present, when I create on my phone, it creates the note with the raw templater code rather than executing it. See ticket for more details, I've done some sleuthing already. --- ## Linked Commits - [`c059bda`](https://github.com/czottmann/obsidian-actions-uri/commit/c059bda2254c890e0aaf8a51bb82e33fcc70f000) — [CHG] Increases post-create pause for Templater/Templates --- ## Progress ### 2024-07-23 (Carlo Zottmann) > Added more time post-creation to give Templater more time. Closing. ================================================ FILE: .beans/zco-628--lean-into-zod-s-transform-for-resolving-incoming-url-paramet.md ================================================ --- # zco-628 title: "Lean into Zod's `.transform()` for resolving incoming URL parameters" status: todo type: task tags: - from-linear created_at: 2024-07-18T15:32:44.910Z updated_at: 2025-05-21T13:44:38.256Z --- ## Linear Metadata - **Linear**: [ZCO-628](https://linear.app/actionsdotwork/issue/ZCO-628) - **Project**: Actions URI - **Branch**: \`feature/zco-628-lean-into-zods-transform-for-resolving-incoming-url\` --- Contemplate how s/th like `validateNoteTargetingAndResolvePath()` could work for anything incoming. I have a feeling this could make a lot of code redundant. When the handler gets the original zod-treated parameters plus a `_resolved` object (containing the actual requested file path etc.), that's a win. (It's partially what I did with `zodExistingFilePath` et al, but that's overwriting the incoming data, so… hmm.) But having single transformers which could be tacked on as needed…? ```typescript incomingBaseParams.extend({ file: zodExistingFilePath, "new-filename": zodSanitizedFilePath, silent: zodOptionalBoolean, }) .transform(hardResolveFile) // resolve file or abort with error .transform(resolveWhatever) .transform(something) ``` --- ## Progress ### 2025-05-21 (Carlo Zottmann) > New zod v4 is out, look at that first. ================================================ FILE: .beans/zco-629--add-result-selection-to-note-get-active.md ================================================ --- # zco-629 title: "Add `result-selection` to `/note/get-active`" status: completed type: task parent: auri-14wv tags: - from-linear created_at: 2024-07-22T10:42:46.013Z updated_at: 2024-07-23T15:13:07.964Z --- ## Linear Metadata - **Linear**: [ZCO-629](https://linear.app/actionsdotwork/issue/ZCO-629) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-629-add-result-selection-to-noteget-active\` --- [obsidian-actions-uri · #90 · \[Feature Request\] ](https://github.com/czottmann/obsidian-actions-uri/issues/90)`[/note/get-active](https://github.com/czottmann/obsidian-actions-uri/issues/90)`[ | New Return Value: Selection](https://github.com/czottmann/obsidian-actions-uri/issues/90) --- ## Linked Commits - [`4e94c35`](https://github.com/czottmann/obsidian-actions-uri/commit/4e94c35d59df587cc3190fc584d589581bf78365) — [NEW] Adds 1.6 stuff - [`b9c16d3`](https://github.com/czottmann/obsidian-actions-uri/commit/b9c16d3d2247eb03439a64a07a2f51cf24c5795a) — [NEW] Adds `result-selection` to `/note/get-active` ================================================ FILE: .beans/zco-630--add-create-headline-if-not-found-option-for-note-append-note.md ================================================ --- # zco-630 title: "Add \"create headline if not found\" option for `/note/append` & `/note/prepend`'s below-headline feature" status: completed type: task parent: auri-14wv tags: - from-linear created_at: 2024-07-22T12:17:37.050Z updated_at: 2024-07-23T15:13:07.967Z --- ## Linear Metadata - **Linear**: [ZCO-630](https://linear.app/actionsdotwork/issue/ZCO-630) - **Project**: Actions URI - **Milestone**: 1.6.0 - **Branch**: \`feature/zco-630-add-create-headline-if-not-found-option-for-noteappend\` --- Options: * `skip` * `add-headline`s6 * return error --- ## Linked Commits - [`4e94c35`](https://github.com/czottmann/obsidian-actions-uri/commit/4e94c35d59df587cc3190fc584d589581bf78365) — [NEW] Adds 1.6 stuff - [`d42fb01`](https://github.com/czottmann/obsidian-actions-uri/commit/d42fb017869decb8ca9b2f7478b37cf3653c24dc) — [NEW] Adds `?if-headline-missing=` to `/note/append`, `/note/prepend` ================================================ FILE: .beans/zco-633--fix-note-create-in-1-6-0.md ================================================ --- # zco-633 title: "Fix `/note/create` in 1.6.0" status: completed type: bug parent: auri-vp9j tags: - from-linear created_at: 2024-07-25T07:26:11.904Z updated_at: 2024-07-25T07:54:57.154Z --- ## Linear Metadata - **Linear**: [ZCO-633](https://linear.app/actionsdotwork/issue/ZCO-633) - **Project**: Actions URI - **Milestone**: 1.6.1 - **Branch**: \`feature/zco-633-fix-notecreate-in-160\` --- #558 Actions For Obsidian feedback - Jose Gemez: [https://secure.helpscout.net/conversation/2661538959/558?viewId=7423770](https://#558 Actions For Obsidian feedback - Jose Gemez) ================================================ FILE: .beans/zco-634--fix-headline-block-append-prepend.md ================================================ --- # zco-634 title: "Fix headline block append/prepend" status: completed type: bug parent: auri-slfp tags: - from-linear created_at: 2024-07-26T09:25:11.951Z updated_at: 2024-07-29T15:51:13.445Z --- ## Linear Metadata - **Linear**: [ZCO-634](https://linear.app/actionsdotwork/issue/ZCO-634) - **Project**: Actions URI - **Milestone**: 1.6.2 - **Branch**: \`feature/zco-634-fix-headline-block-appendprepend\` --- Request: [Marco :prami: (@esamecar@social.lol)](https://social.lol/@esamecar/112851925966484997) Both `prependNoteBelowHeadline()` and `appendNoteBelowHeadline()` use a sloppy "does this section start with this headline" check: ``` const newContent = res.result .split(/(?=^#+ )/m) .map((section) => { if (!section.startsWith(belowHeadline)) { return section; } ``` If `belowHeadline` is "## A", then this would check `true` if the section starts with "## ABC", too. Also, if `belowHeadline` ending in whitespace might fail the check. Make the check use a regex: `/^## Mittwoch\s*\n/.test(section)` --- ## Linked Commits - [`d9eac50`](https://github.com/czottmann/obsidian-actions-uri/commit/d9eac50736ec1f13ab0084aaaddea413bc9512e5) — [FIX] Fixes sloppy "below headline" matching and appending/prepending ================================================ FILE: .beans/zco-636--fix-error-code-on-note-get-from-500-back-to-404-to-prevent-a.md ================================================ --- # zco-636 title: "Fix error code on `/note/get` from 500 back to 404 to prevent AFO \"Check For Existence of Note\" failing" status: completed type: bug parent: auri-slfp tags: - from-linear created_at: 2024-07-29T08:47:06.739Z updated_at: 2024-07-29T15:44:39.782Z --- ## Linear Metadata - **Linear**: [ZCO-636](https://linear.app/actionsdotwork/issue/ZCO-636) - **Project**: Actions URI - **Milestone**: 1.6.2 - **Branch**: \`feature/zco-636-fix-error-code-on-noteget-from-500-back-to-404-to-prevent\` --- ## Linked Commits - [`8640bdb`](https://github.com/czottmann/obsidian-actions-uri/commit/8640bdb73fe9c5ae1046d2a01882cda557bcffaf) — [FIX] Fixes wrong error code on files not found ================================================ FILE: .beans/zco-642--templater-path-parameter-doesn-t-work-as-assumed-intended.md ================================================ --- # zco-642 title: "Templater path parameter doesn't work as assumed/intended" status: completed type: bug parent: auri-civ4 tags: - from-linear created_at: 2024-08-02T09:11:56.999Z updated_at: 2024-08-02T12:14:56.416Z --- ## Linear Metadata - **Linear**: [ZCO-642](https://linear.app/actionsdotwork/issue/ZCO-642) - **Project**: Actions URI - **Milestone**: 1.6.3 - **Branch**: \`feature/zco-642-templater-path-parameter-doesnt-work-as-assumedintended\` --- Via [Help Scout #562](https://secure.helpscout.net/conversation/2668200764/562): > Carlo, > Since the Actions URI updates, I have not been able to create my Day Note with AFO. The action fails with an error that indicates the template file is not found. However, I am able to use *Create Daily Note* with no problem in Obsidian via the ribbon button (or command). It must have something to do with the recent changes. --- Only an issue with **Create PN + Templater + template-file**! --- ## Linked Commits - [`a2f0fa0`](https://github.com/czottmann/obsidian-actions-uri/commit/a2f0fa0f28004015af96ec36328e91cf5431ce75) — [FIX] Makes `template-file` parameter resolve correctly again ================================================ FILE: .beans/zco-645--replace-vault-modify-w-vault-process-to-prevent-race-conditi.md ================================================ --- # zco-645 title: "Replace `vault.modify()` w/ `vault.process()` to prevent race conditions" status: scrapped type: bug tags: - from-linear created_at: 2024-08-05T13:10:03.596Z updated_at: 2024-08-06T10:57:19.442Z --- ## Linear Metadata - **Linear**: [ZCO-645](https://linear.app/actionsdotwork/issue/ZCO-645) - **Project**: Actions URI - **Branch**: \`feature/zco-645-replace-vaultmodify-w-vaultprocess-to-prevent-race\` --- [Vault - Developer Documentation](https://docs.obsidian.md/Plugins/Vault#Modify+files) When appending text a non-focussed note, e.g. under a headline in the weekly note, the last modification before the update seems to be rolled back before the current modification is applied. Not sure yet it's an AURI issue, though. **UPDATE: Not an AURI issue.** The plugin "Check Please!" misbehaves. ================================================ FILE: .beans/zco-651--add-debugging-info-output-to-settings-page.md ================================================ --- # zco-651 title: "Add debugging info output to Settings page" status: scrapped type: task parent: auri-ujej tags: - from-linear created_at: 2024-08-14T15:42:22.743Z updated_at: 2025-05-22T11:36:12.638Z --- ## Linear Metadata - **Linear**: [ZCO-651](https://linear.app/actionsdotwork/issue/ZCO-651) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-651-add-debugging-info-output-to-settings-page\` --- The settings tab should get a debugging section which lets the customer copy debugging output to the clipboard: * vault name * system path of the vault (?) * plugin version * Obsidian version (?) * … Maybe there's a way to gather the official debugging output of Obsidian behind the scenes? --- ## Progress ### 2025-05-22 (Carlo Zottmann) > Eh, it's unnecessary. ================================================ FILE: .beans/zco-692--insert-below-headline-fails-if-the-headline-is-the-last-line.md ================================================ --- # zco-692 title: "\"Insert below headline\" fails if the headline is the last line of the note and doesn't end in a newline" status: completed type: bug parent: auri-p05h tags: - from-linear created_at: 2024-09-20T07:52:05.317Z updated_at: 2024-09-20T14:30:49.325Z --- ## Linear Metadata - **Linear**: [ZCO-692](https://linear.app/actionsdotwork/issue/ZCO-692) - **Project**: Actions URI - **Milestone**: 1.6.4 - **Branch**: \`feature/zco-692-insert-below-headline-fails-if-the-headline-is-the-last-line\` --- ## Linked Commits - [`2ae98d1`](https://github.com/czottmann/obsidian-actions-uri/commit/2ae98d1fe641ada6010680b1927bf75f957e0c87) — [FIX] Fixes append/prepend below headline at the end of the file ================================================ FILE: .beans/zco-694--add-optional-logging-to-file.md ================================================ --- # zco-694 title: "Add optional logging to file" status: draft type: feature tags: - from-linear created_at: 2024-09-20T14:35:27.945Z updated_at: 2024-09-20T14:35:27.945Z --- ## Linear Metadata - **Linear**: [ZCO-694](https://linear.app/actionsdotwork/issue/ZCO-694) - **Project**: Actions URI - **Branch**: \`feature/zco-694-add-optional-logging-to-file\` --- Might be a worthwhile addition to the plugin: A switch which logs everything that usually just goes to the console. Basically Logstravaganza light – just for Actions URI, NDJSON only, no further user configuration possible. ================================================ FILE: .beans/zco-695--fix-empty-note-when-trying-to-append-prepend-content-to-non-.md ================================================ --- # zco-695 title: "Fix empty note when trying to append/prepend content to non-existing periodic note which has to be created first" status: completed type: bug tags: - from-linear - help-scout created_at: 2024-09-23T09:23:29.780Z updated_at: 2024-10-09T11:11:32.296Z --- ## Linear Metadata - **Linear**: [ZCO-695](https://linear.app/actionsdotwork/issue/ZCO-695) - **Project**: Actions URI - **Branch**: \`feature/zco-695-fix-empty-note-when-trying-to-appendprepend-content-to-non\` --- Via [Help Scout #589](https://secure.helpscout.net/conversation/2711901364/589): > `obsidian://actions-uri/note/append/?vault=Full%20Life&create-if-not-found=true&periodic-note=daily&content=hello` > > This action does correctly create the daily note, if it doesn’t exist. It also correctly appends the content to the note. > > However, it does not apply the daily note template when creating the daily note. The result of this URI is that the daily note is created, but only contains the word “hello”. > > This means I can’t use the below headline option, because the headline is in the template, and the template doesn’t get applied. My shortcut has to contain a separate create daily note action, which does apply the template when creating the note. 💭 I think due to some internal changes in Obsidian itself the periodic note plugin(s) and Actions URI are now interfering with each other. --- ## Linked Commits - [`f615958`](https://github.com/czottmann/obsidian-actions-uri/commit/f61595878dc547cc1f6ae39b6d335da7f7dd4b17) — [CHG] Updates note open/focus by path for Obsidian 1.7 - [`901b26b`](https://github.com/czottmann/obsidian-actions-uri/commit/901b26bbf18e5aaff596fca3d519c731b14233e4) — [NEW] Adds ZCO-695 to changelog - [`c14ca8c`](https://github.com/czottmann/obsidian-actions-uri/commit/c14ca8c24282658188d195ac621769d8b7fcc7ea) — [FIX] Ensures append/prepend on periodic notes creates the right type of note if requested ================================================ FILE: .beans/zco-707--check-plugin-against-new-obsidian-guidelines.md ================================================ --- # zco-707 title: "Check plugin against new Obsidian guidelines" status: completed type: task tags: - from-linear - tooling created_at: 2024-10-04T18:48:58.287Z updated_at: 2025-05-19T11:12:04.308Z --- ## Linear Metadata - **Linear**: [ZCO-707](https://linear.app/actionsdotwork/issue/ZCO-707) - **Project**: Actions URI - **Branch**: \`feature/zco-707-check-plugin-against-new-obsidian-guidelines\` --- [Obsidian October O_O 2024 plugin self-critique checklist - Developer Documentation](https://docs.obsidian.md/oo24/plugin) ================================================ FILE: .beans/zco-918--make-dataview-list-result-consistent.md ================================================ --- # zco-918 title: "Make Dataview LIST result consistent" status: completed type: bug parent: zco-919 tags: - from-linear created_at: 2024-11-25T14:26:48.381Z updated_at: 2025-02-04T11:58:55.693Z --- ## Linear Metadata - **Linear**: [ZCO-918](https://linear.app/actionsdotwork/issue/ZCO-918) - **Project**: Actions URI - **Milestone**: 1.7.0 - **Branch**: \`feature/zco-918-make-dataview-list-result-consistent\` --- [Get DV list without ID + inline → Json string couldn't be decoded](https://forum.actions.work/t/get-dv-list-without-id-inline-json-string-couldnt-be-decoded/554/4) For LIST queries, DV will return a two-dimensional array instead of a one-dimensional one *if* one of the queried files returns more than one hit. Example: If you query for an inline field (`whatever::`), and one file contains of it, e.g. `whatever:: something 1` and `whatever:: something 2`, while another file contains `whatever:: something 3`, DV will return: ``` [ ["something 1", "something 2"], "something 3" ] ``` This is inconsistent, and AFO will nope out. So we'll make it consistent. --- ## Linked Commits - [`6a3d653`](https://github.com/czottmann/obsidian-actions-uri/commit/6a3d6535b9e7cc7386d4f8013d091346f7d0df6e) — [CHG] Rewrote inline docs - [`7f25a90`](https://github.com/czottmann/obsidian-actions-uri/commit/7f25a902988cc1674d4435d7c3b855c58f9dcbe2) — [FIX] Fixes inconsistencies in `LIST` query results ================================================ FILE: .beans/zco-937--opening-a-note-in-another-vault-only-opens-the-requested-vau.md ================================================ --- # zco-937 title: "Opening a note in *another* vault only opens the requested vault, but not the requested note" status: todo type: bug tags: - from-linear - ios-only created_at: 2024-12-06T17:34:28.494Z updated_at: 2025-05-15T09:39:52.878Z --- ## Linear Metadata - **Linear**: [ZCO-937](https://linear.app/actionsdotwork/issue/ZCO-937) - **Project**: Actions URI - **Branch**: \`feature/zco-937-opening-a-note-in-another-vault-only-opens-the-requested\` --- [HS#648 Re: Actions For Obsidian feedback, note bug - Mario Mazzoli](https://secure.helpscout.net/conversation/2782096382/648?viewId=7423770): > I have a shortcut to open a specific note in a vault. An example, vault A is the vault of the shortcut, vault B is another vault. If Obsidian was last opened in vault B and in the vault A a different note was opened, when I launch the shortcut, Obsidian opens in vault A but on the different note. If then I launch again the shortcut, it switches to the correct note. ================================================ FILE: .beans/zco-976--fix-wonky-frontmatter-parser.md ================================================ --- # zco-976 title: "Fix wonky frontmatter parser" status: completed type: bug parent: auri-48pt tags: - from-linear - help-scout created_at: 2025-01-29T18:03:47.109Z updated_at: 2025-01-30T11:23:44.042Z --- ## Linear Metadata - **Linear**: [ZCO-976](https://linear.app/actionsdotwork/issue/ZCO-976) - **Project**: Actions URI - **Milestone**: 1.7.0 - **Branch**: \`feature/zco-976-fix-wonky-frontmatter-parser\` --- When the frontmatter block contains any string like `"---"`, the parser fails because it assumes this string is the end boundary of the FM. See [https://secure.helpscout.net/conversation/2833345523/672?viewId=7423770](https://secure.helpscout.net/conversation/2833345523/672?viewId=7423770) --- ## Linked Commits - [`3f7de7c`](https://github.com/czottmann/obsidian-actions-uri/commit/3f7de7cc567f0dd70de9b66f9e41d36a5510c780) — [CHG] Fixes triple hyphens in frontmatter strings breaking FM parsing ================================================ FILE: .beans/zco-977--remove-dedicated-periodic-note-related-routes.md ================================================ --- # zco-977 title: "Remove dedicated periodic note-related routes" status: completed type: task parent: auri-ujej tags: - from-linear created_at: 2025-01-30T11:29:05.033Z updated_at: 2025-05-22T11:40:10.343Z --- ## Linear Metadata - **Linear**: [ZCO-977](https://linear.app/actionsdotwork/issue/ZCO-977) - **Project**: Actions URI - **Milestone**: 1.8.1 - **Branch**: \`feature/zco-977-remove-dedicated-periodic-note-related-routes\` --- As mentioned in the deprecation notice in v1.6.0: > All dedicated periodic note-related routes (`/daily-note/*`, `/weekly-note/*`, `/monthly-note/*`, `/quarterly-note/*`, `/yearly-note/*`) are officially deprecated, and will be removed in early 2025. Please update your scripts accordingly. --- ## Linked Commits - [`92d8a9d`](https://github.com/czottmann/obsidian-actions-uri/commit/92d8a9dc842889d221fe8d166cd79f8259d36917) — [DEL] Removes deprecated periodic note routes ================================================ FILE: .beans/zco-985--dataview-list-results-return-unexpected-nested-array.md ================================================ --- # zco-985 title: "DataView LIST results return unexpected nested array" status: completed type: bug parent: auri-fzy4 tags: - from-linear created_at: 2025-02-03T09:16:22.164Z updated_at: 2025-02-04T12:31:40.378Z --- ## Linear Metadata - **Linear**: [ZCO-985](https://linear.app/actionsdotwork/issue/ZCO-985) - **Project**: Actions URI - **Milestone**: 1.7.1 - **Branch**: \`feature/zco-985-dataview-list-results-return-unexpected-nested-array\` --- Regression of / related to [ZCO-918](https://linear.app/actionsdotwork/issue/ZCO-918/make-dataview-list-result-consistent) Reproducible error! Via [Help Scout #678](https://secure.helpscout.net/conversation/2837335579/678): > For the past 2-3 days, I keep getting this error. Any idea what the issue might be? > > ![untitled.png](assets/zco-985-untitled.png) > > \------ > > Actions For Obsidian 2024.2.3 (7402) > > macOS 15.3.0 The underlying issue is that in 1.7.0, I changed `LIST` results to be 2D string arrays when they are supposed to be 1D. The fix is to return 1D. --- ## Linked Commits - [`92d90a8`](https://github.com/czottmann/obsidian-actions-uri/commit/92d90a8974e4c112e97462ff517ea2d1e9af650f) — [FIX] Fixes LIST return value issues ================================================ FILE: .beans/zco-987--fix-periodic-notes-coming-out-raw-when-using-templater.md ================================================ --- # zco-987 title: "Fix periodic notes coming out \"raw\" when using Templater" status: completed type: bug parent: auri-plwy tags: - from-linear created_at: 2025-02-10T09:17:45.017Z updated_at: 2025-02-10T09:38:26.270Z --- ## Linear Metadata - **Linear**: [ZCO-987](https://linear.app/actionsdotwork/issue/ZCO-987) - **Project**: Actions URI - **Milestone**: 1.7.2 - **Branch**: \`feature/zco-987-fix-periodic-notes-coming-out-raw-when-using-templater\` --- Reports: * [https://secure.helpscout.net/conversation/2834156734/673?viewId=7423770](https://secure.helpscout.net/conversation/2834156734/673?viewId=7423770) * [Creating Periodic Note doesn't apply Templater templates - Actions For Obsidian - ActionsDotWork Forum](https://forum.actions.work/t/creating-periodic-note-doesnt-apply-templater-templates/590/2) Race condition just like the one with general notes. Need to apply a pause after creation here, too. ================================================ FILE: .beans.yml ================================================ beans: path: .beans prefix: auri- id_length: 4 default_status: todo default_type: task ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = spaces indent_size = 2 tab_width = 2 ================================================ FILE: .eslintignore ================================================ npm node_modules build ================================================ FILE: .eslintrc ================================================ { "root": true, "parser": "@typescript-eslint/parser", "env": { "node": true }, "plugins": [ "@typescript-eslint" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "parserOptions": { "sourceType": "module" }, "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off" } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: ["https://actions.work/store/#sponsoring"] ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Obsidian Plugin on: push: # Sequence of patterns matched against refs/tags tags: - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - uses: actions/setup-node@v3 with: node-version: "23.x" # You might need to adjust this value to your own version # Build the plugin - name: Build run: | yarn yarn run build # Get the version number and put it in a variable - name: Get version info id: version run: | echo "name=$(git describe --abbrev=0 --tags)" >> $GITHUB_OUTPUT # Package the required files into a zip - name: Package plugin archive run: | mkdir ${{ github.event.repository.name }} cp main.js manifest.json README.md ${{ github.event.repository.name }} zip -r ${{ github.event.repository.name }}-${{ steps.version.outputs.name }}.zip ${{ github.event.repository.name }} # Create the release on github - name: Create release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: draft: true files: | ${{ github.event.repository.name }}-${{ steps.version.outputs.name }}.zip main.js manifest.json name: ${{ steps.version.outputs.name }} prerelease: false tag_name: ${{ github.ref }} token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # vscode .vscode # Intellij *.iml .idea # npm node_modules # Don't include the compiled main.js file in the repo. # They should be uploaded to GitHub releases instead. /main.js **/actions-uri/main.js # Exclude sourcemaps *.map # Exclude macOS Finder (System Explorer) View States .DS_Store # Exclude files used for local development x-callback-test.txt sandbox.js ================================================ FILE: .mise.toml ================================================ [tools] deno = "latest" ================================================ FILE: .npmrc ================================================ auto-install-peers=true tag-version-prefix="" ================================================ FILE: .prettierrc ================================================ { "arrowParens": "always", "bracketSameLine": false, "bracketSpacing": true, "embeddedLanguageFormatting": "auto", "htmlWhitespaceSensitivity": "css", "insertPragma": false, "printWidth": 80, "proseWrap": "preserve", "quoteProps": "preserve", "requirePragma": false, "semi": true, "singleAttributePerLine": false, "singleQuote": false, "trailingComma": "all", "vueIndentScriptAndStyle": false, "withNodeModules": false } ================================================ FILE: AGENTS.md ================================================ # AGENTS.md This file provides guidance to LLM agents when working with code in this repository. ## Common Development Commands ### Building and Development - **Development**: `pnpm dev` or `npm run dev` - Compiles with esbuild and watches for changes - **Production build**: `pnpm build` or `npm run build` - TypeScript type checking + production build - **Testing**: `pnpm test` or `npm test` - Runs Jest tests (requires build first) ### Single Test Execution Use Jest's pattern matching: `npx jest noteCreate.test.ts` or `npx jest --testNamePattern="specific test name"` ## Architecture Overview This is an Obsidian plugin that extends the built-in URI scheme with additional x-callback-url endpoints for advanced automation. The plugin follows a modular routing architecture: ### Core Components **Main Plugin (`src/main.ts`)**: - Extends Obsidian's `Plugin` class as `ActionsURI` - Registers URI handlers dynamically from route definitions - Handles parameter validation using Zod schemas - Manages x-callback-url responses and error handling **Routing System (`src/routes.ts`, `src/routes/`)**: - Each route module exports a `routePath` object defining available endpoints - Routes are organized by functionality (note, file, vault, search, etc.) - Each route has a Zod schema for validation and a handler function - Route pattern: `/actions-uri/{category}/{action}` **Schema Validation (`src/schemata.ts`, `src/utils/zod.ts`)**: - All incoming parameters validated with Zod - Base parameters include vault, action, debug-mode, x-success/x-error callbacks - Note targeting supports file paths, UIDs, or periodic notes **Result Handling (`src/utils/results-handling.ts`)**: - Standardized success/failure result objects - Automatic x-callback-url responses - Error codes and user-friendly messages ### Key Patterns **Handler Functions**: Each route handler follows the pattern: ```typescript async function handler(params: ValidatedParams): Promise ``` **Parameter Processing**: 1. Raw parameters → Zod validation → Type-safe handler parameters 2. File path resolution using Obsidian's vault API 3. Support for targeting notes by file path, UID (frontmatter), or periodic note type **Error Handling**: - Zod validation errors converted to user-friendly messages - Handler exceptions caught and converted to standard error responses - Optional UI notices (can be suppressed with `hide-ui-notice-on-error`) ## File Organization - `src/routes/` - Individual route handlers organized by functionality - `src/utils/` - Shared utilities (file handling, search, UI, callbacks) - `src/types/` - TypeScript type definitions split by domain - `tests/` - Jest tests with mock Obsidian environment - `docs/` - Documentation (deployed to GitHub Pages) ## Testing Architecture Tests use a custom setup that mocks the Obsidian environment: - `tests/global-setup.ts` - Initializes mock environment and callback server - Tests are serialized (`maxConcurrency: 1`) to avoid callback server conflicts - Test vault in `tests/plugin-test-vault.original/` with sample notes and templates ## Dependencies - **Zod**: Runtime type validation and schema definition - **Obsidian API**: Plugin API, file system, and UI integration - **Jest + ts-jest**: Testing framework with TypeScript support - **esbuild**: Fast bundling for development and production ================================================ FILE: CHANGELOG.md ================================================ # Release history ## 1.8.4, 2025-11-18 Chore release, updating dependencies. ## 1.8.3, 2025-08-05 ### No longer broken - [`/note-properties/set`](https://zottmann.dev/obsidian-actions-uri/routes/note-properties/#note-propertiesset): Sometimes, randomly, _updating_ file properties would fail, and the updated keys would become the only remaining frontmatter. This should be fixed now. Thanks to Marco for the heads-up. ### Changes - `/*/rename`: Renaming now uses Obsidian's [`renameFile()`](https://docs.obsidian.md/Reference/TypeScript+API/FileManager/renameFile) function now, which updates all links to the renamed file depending on the user's preferences. Before, Actions URI would _just_ rename the note/ file/ folder, and be done with it. ## 1.8.1, 2025-05-22 ### New stuff - All [`/note` routes](https://zottmann.dev/obsidian-actions-uri/routes/note/) that return note information (like path, UID, body, etc.) now also return a `result-uri-path` and `result-uri-uid` (where available), containing URLs that can be used to link to the note from other places on the same device. - The new standard, optional boolean parameter [`hide-ui-notice-on-error`](https://zottmann.dev/obsidian-actions-uri/parameters/) can be used to suppress the UI notice on errors like "note not found". Defaults to `false`, so by default the UI notice will be shown as usual. ### Removals As announced last year (see 1.6.0, 2024-07-23), the periodic note routes are now gone for good. All their functionality is now part of the [`/note`](https://zottmann.dev/obsidian-actions-uri/routes/note/) routes. ### No longer broken - `x-success` results will no longer wrap undefined values as strings, so let's bid farewell to `result-some-value=undefined`. ### Changes - Minor cleanups of the settings tab. ### Development Adds a E2E test suite, see `tests/README.md` for details. This is a work in progress, and will be expanded over time. ## 1.7.3, 2025-04-30 ### No longer broken - [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate): Using `if-exists=skip&silent=false` would not open an already existing note in Obsidian. ([#99](https://github.com/czottmann/obsidian-actions-uri/issues/99)) ## 1.7.2, 2025-02-10 ### No longer broken - When using Templater with periodic notes, the created notes would sometimes end up with Templater placeholders still unreplaced or content not applied. ## 1.7.1, 2025-02-04 ### No longer broken - Fixed output of [`/dataview/list-query`](https://zottmann.dev/obsidian-actions-uri/routes/dataview/#dataviewlist-query) to return a one-dimensional array. (Regression introduced in 1.7.0.) ## 1.7.0, 2025-01-30 ### Changes - Adjusted [`/dataview/list-query`](https://zottmann.dev/obsidian-actions-uri/routes/dataview/#dataviewlist-query) to consistently return a two-dimensional array. ### No longer broken - Frontmatter containing a string with three or more hyphens in a row would break frontmatter parsing. This is now fixed. ## 1.6.5, 2024-10-10 ### No longer broken - For some users, both [`/note/append`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteappend) and [`/note/prepend`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteprepend) would create empty periodic notes when the `create-if-not-found` parameter was used. ### Changes - Adjusted opening/focussing notes for API changes in Obsidian 1.7. ## 1.6.4, 2024-09-20 ### No longer broken - Both [`/note/append`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteappend) and [`/note/prepend`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteprepend) now correctly work with headlines at the end of a note which don't have a trailing newline. # Release history ## 1.6.3, 2024-08-02 ### No longer broken - When calling `/*-note/create` with a `template-file` parameter, the template wouldn't be found unless the template path contained both folder and file extension. Fixed! ## 1.6.2, 2024-07-29 ### No longer broken - When a note couldn't be found, the wrong error code would be returned (500 instead of 404). - Appending/prepending below a headline could fail if either the input headline or the headline in the note contained trailing whitespace. This is now fixed by ignoring trailing whitespace when checking, appending, and prepending. ## 1.6.1, 2024-07-25 ### No longer broken - Under some circumstances, [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate) with `apply=content` would not create a note. Sorry about that! ## 1.6.0, 2024-07-23 ### New stuff - All [`/note` routes](https://zottmann.dev/obsidian-actions-uri/routes/note/) gained support for working with periodic notes (daily, weekly, etc.). - All [`/note` routes](https://zottmann.dev/obsidian-actions-uri/routes/note/) gained support for UID-based note references. Notes can now be referenced by their UID instead of their file path, so if you're storing a UID in your front matter, give it a go. Make sure to check that the correct frontmatter key is configured in the … - New settings UI. The plugin now has a settings page in Obsidian's settings. - [`/note/get-active-note`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget-active) returns the current selection as part of its result. (Plain-text only for now.) Thanks to [@FelipeRearden](https://github.com/FelipeRearden) for the suggestion! [#90] - [`/note-properties`](https://zottmann.dev/obsidian-actions-uri/routes/note-properties/) now supports working with current periodic notes (daily, weekly, etc.) - When using [`/note/append`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteappend) or [/note/prepend](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteprepend) to insert text below a headline, the route no longer joyfully reports success even when the headline couldn't be found. Instead, the routes now support a conditional `if-headline-missing` parameter to specify what to do in that case: report an error (the new default), skip the operation (the old default behavior), or add the headline to the end of the note. Thanks to [@vitaly-rudenko](https://github.com/vitaly-rudenko) for the bug report! [#91] ### Deprecation notice All dedicated periodic note-related routes (`/daily-note/*`, `/weekly-note/*`, `/monthly-note/*`, `/quarterly-note/*`, `/yearly-note/*`) are officially deprecated, and will be removed in early 2025. Please update your scripts accordingly. There will be no further work on these routes going forward. ### No longer broken - Fixes search/replace in notes, which wouldn't work if the search term was a string but contained regex-like characters (`$`, `^`, etc.) - [`/note-properties/get`](https://zottmann.dev/obsidian-actions-uri/routes/note-properties/#note-propertiesget) won't automatically open the note anymore. - Fixes template path handling in [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate), which was very strict, and didn't allow for specifying a template file outside the configured template folder. ## 1.5.3, 2024-06-18 ### No longer broken - Fixes the plugin not sending out return calls when the requested vault wasn't loaded yet, e.g. when Obsidian wasn't running or when another vault was active. ## 1.5.2, 2024-05-11 House keeping release, no new features. ### Changes - Replaces deprecated `global.app` references - Sets minimum Obsidian version to 1.5.0 ## 1.5.1, 2024-03-27 ### Changes - `/note/create/`, `/periodic-note/create`: If the file name passed in `template-file` can't be found, the plugin will now check the template folder set in Templates or Templater, respectively, before returning an error. - Console output will now print the paths contained in the incoming params, instead of their internal file references. This prevents circular references and "max call stack" errors related to files when using [Logstravaganza](https://github.com/czottmann/obsidian-logstravaganza). ## 1.5.0, 2024-02-19 ### New stuff - New route path: [`/file`](https://zottmann.dev/obsidian-actions-uri/routes/file/) for working with files. The Obsidian API doesn't allow for uploading attachment files, but now you can at least handle them. (#85) - `/file/list`: Returns the paths of all files in the vault. - `/file/get-active`: Return the path of the currently active/ focussed file. - `/file/open`: Opens a file in Obsidian. - `/file/delete`: Deletes a file. - `/file/trash`: Moves a file to the trash. - `/file/rename`: Renames a file. - New route: [`/note/get-active`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget-active) returns the currently active/ focussed note. - New route: [`/note/get-first-named`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget-first-named) returns the first note found with a given name. - New route: [`/note/touch`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notetouch) sets the modification date of a note to the current date and time (side effect: it makes Obsidian reload it in views/ embeddings). ### Changes - Appending below headline: Now inserts before any trailing new lines in a section instead of after them. - Changed route: [`/command/execute`](https://zottmann.dev/obsidian-actions-uri/routes/command/#commandexecute) no longer requires the `x-success` and `x-error` parameters to be present. If they are, they will be used, but they are optional now. (#84) - Removed route: `/vault/list-folders` was marked as deprecated in 0.16, and is now gone for good. Use [`/folder/list`](https://zottmann.dev/obsidian-actions-uri/routes/folder/#folderlist) instead. ## 1.4.2, 2023-12-12 ### No longer broken - Resolves problems with applying templates of the core plugin Templates. - Fixes broken handling of `silent` parameter in `note/*`, `daily-note/*`, `weekly-note/*`, `monthly-note/*`, `quarterly-note/*`, and `yearly-note/*` routes ## 1.4.0, 2023-11-22 ### New stuff #### Support for Note Properties (ZCO-28) [Properties](https://help.obsidian.md/Editing+and+formatting/Properties) are a core feature of Obsidian: structured data containing information about a note. Actions URI now supports them in a variety of ways. Please see the new route docs for details: - [`/note-properties`](https://zottmann.dev/obsidian-actions-uri/routes/note-properties/) Existing routes which return note content now also return note properties, if present. Their documentation has been updated accordingly. See Changes, below. ### Changes Actions URI now requires Obsidian 1.4+. The following routes return an additional `result-properties` parameter if the note contains properties: - [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate) - [`/note/get`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget) - [`/daily-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-notecreate) - [`/daily-note/get-current`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteget-current) - [`/daily-note/get-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteget-most-recent) - [`/weekly-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/weekly-note/#weekly-notecreate) - [`/weekly-note/get-current`](https://zottmann.dev/obsidian-actions-uri/routes/weekly-note/#weekly-noteget-current) - [`/weekly-note/get-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/weekly-note/#weekly-noteget-most-recent) - [`/monthly-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/monthly-note/#monthly-notecreate) - [`/monthly-note/get-current`](https://zottmann.dev/obsidian-actions-uri/routes/monthly-note/#monthly-noteget-current) - [`/monthly-note/get-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/monthly-note/#monthly-noteget-most-recent) - [`/quarterly-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/quarterly-note/#quarterly-notecreate) - [`/quarterly-note/get-current`](https://zottmann.dev/obsidian-actions-uri/routes/quarterly-note/#quarterly-noteget-current) - [`/quarterly-note/get-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/quarterly-note/#quarterly-noteget-most-recent) - [`/yearly-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/yearly-note/#yearly-notecreate) - [`/yearly-note/get-current`](https://zottmann.dev/obsidian-actions-uri/routes/yearly-note/#yearly-noteget-current) - [`/yearly-note/get-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/yearly-note/#yearly-noteget-most-recent) ## 1.3.1, 2023-10-09 ### Fixes - Append/prepend to a periodic note that had to be created first would create the note but fail to append/prepend the text. This is now fixed. ## 1.3.0, 2023-09-04 ### New stuff #### Support for triggering Obsidian commands (#77) Please see the new route docs for details: - [`/command`](https://zottmann.dev/obsidian-actions-uri/routes/command/) #### Support for Weekly, Monthly, Quarterly, and Yearly Notes (#75) All of Action UR's existing [`/daily-note`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/) functionality is now also available for anything supported by the [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes) community plugin! Please see the new route docs for details: - [`/weekly-note`](https://zottmann.dev/obsidian-actions-uri/routes/weekly-note/) - [`/monthly-note`](https://zottmann.dev/obsidian-actions-uri/routes/monthly-note/) - [`/quarterly-note`](https://zottmann.dev/obsidian-actions-uri/routes/quarterly-note/) - [`/yearly-note`](https://zottmann.dev/obsidian-actions-uri/routes/yearly-note/) > [!IMPORTANT] > **Known issue:** At the time of writing, the Periodic Notes plugin seems to have a bug that (for some people) prevents creating a new weekly note on any day other than Sunday. [liamcain/obsidian-periodic-notes · #185 · Open weekly note only works on Sunday](https://github.com/liamcain/obsidian-periodic-notes/issues/185). Since Actions URI uses the Periodic Notes plugin's API, this is not something I can fix. Please follow the issue for updates. ### Changes - Actions URI now requires Obsidian 1.3+ . ### Fixes - Adds missing return calls to `/vault/open` and `/vault/close`. (#76) - For some Dataview `TABLE` queries, the results would be wrapped in an extra array, this has been fixed. (#79) ### Housekeeping - Updates esbuild and @typescript-eslint packages. ## 1.2.5, 2023-08-29 ### Fixes - Ensures Dataview `TABLE` results are correctly nested. (#79) ## 1.2.4, 2023-08-07 ### Fixes - Appending/prepending below headlines no longer fails if there is no empty line below the headline. (#73) - When using a file path ending in `.canvas`, Actions URI will no longer add `.md` to it. (#74) ## 1.2.3, 2023-07-25 ### Fixes - Attempting to use the Templates core plugin on iOS would result in an error. This is now fixed. ## 1.2.2, 2023-07-13 ### Fixes - Adjusts the behavior of [`/note/get`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteget) so it no longer breaks Actions for Obsidian's "Check if note exists" action. 😬 - Fixes a bug in note creation where the default behavior regarding content insertion was not respected. ## 1.2.0, 2023-07-12 ### New stuff #### [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate) - Adds support for applying a Templates (core plugin) template or Templater (community plugin) template after note creation (#69) #### [`/note/append`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteappend) - Adds an optional `create-if-not-found` parameter for avoiding errors if the note doesn't exist yet (#67) - Adds an optional `below-headline` parameter for appending text not to the end of a file but to a section below a heading (#68) #### [`/note/prepend`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteprepend) - Adds an optional `create-if-not-found` parameter for avoiding errors if the note doesn't exist yet (#67) - Adds an optional `below-headline` parameter for prepending text not to the beginning of a file but to a section below a heading (#68) #### [`/daily-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-notecreate) - Adds support for applying a Templates (core plugin) template or Templater (community plugin) template after note creation (#69) #### [`/daily-note/append`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteappend) - Adds an optional `create-if-not-found` parameter for avoiding errors if the note doesn't exist yet (#67) - Adds an optional `below-headline` parameter for appending text not to the end of a file but to a section below a heading (#68) #### [`/daily-note/prepend`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteprepend) - Adds an optional `create-if-not-found` parameter for avoiding errors if the note doesn't exist yet (#67) - Adds an optional `below-headline` parameter for prepending text not to the beginning of a file but to a section below a heading (#68) ### Changes - Incoming, malformed calls are now answered if possible: if an `x-error` parameter was passed in, it will be used now, instead of Actions URI just doing nothing. (#72) - `file` & `folder` parameter validation is now more strict where the parameter is supposed to reference an existing path, and will return a "bad request" error if the referenced file/folder couldn't be found. Examples for clarification: `file` in `/note/rename`, `folder` in `/folder/delete`; but **not** `file` in `/note/create` (as here the parameter references a file yet to be created). (#72) ### Removals - The deprecations made in 0.18.0 are now feasting with the Gods. ## 1.1.2, 2023-05-10 This is a minor release aimed at fixing an issue with opening notes after creation that came up during the [Actions for Obsidian](https://obsidian.actions.work) iOS TestFlight. - [FIX] Cleans up opening/focussing notes - [DEL] Removes outdated API references ## 1.1.0, 2023-05-04 The plugin is stable enough and used in production as the companion plugin to my macOS/iOS app [Actions for Obsidian](https://obsidian.actions.work). So the version number took a big leap to bring it mostly in line with the app. **Nothing else will change,** Actions URI will remain FOSS under a MIT License. ### New stuff - If you're unhappy with the global search, and use Omnisearch, you'll be delighted about the [new `/omnisearch` routes](https://zottmann.dev/obsidian-actions-uri/routes/omnisearch/) (#59) - Actions URI should now handle unexpected exceptions outside its control more graceful (#60) - I've added table of contents to [the route pages in the documentation](https://zottmann.dev/obsidian-actions-uri/routes/). ### No longer broken - Adds code for preventing a race condition in vaults w/ Templater enabled (#61) ## 0.18.0, 2023-04-14 ### New stuff The [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate) route has a new optional `if-exists` parameter to specify a strategy for dealing with an existing note. It overrides the default behavior (creating a new note by appending a numeric suffix to the base name) and can be set to `skip` or `overwrite`. `if-exists=skip` will not create another note and instead return the named note as-is. `if-exists=overwrite` will replace the existing note with a new one. The[`/daily-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-notecreate) route has a new optional `if-exists` parameter to specify a strategy for dealing with an existing current daily note. It overrides the default behavior (returning an error) and can be set to `skip` or `overwrite`. `if-exists=skip` will pretend the existing note was just created and return it. `if-exists=overwrite` will trash the existing note and create a new daily note from scratch. ### Changes - [`/note/create`](https://zottmann.dev/obsidian-actions-uri/routes/note/#notecreate): the `overwrite` parameter is deprecated and will be removed in a future release. Use `if-exists=overwrite` instead. - [`/daily-note/create`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-notecreate): the `overwrite` parameter is deprecated and will be removed in a future release. Use `if-exists=overwrite` instead. ## 0.17.0, 2023-04-12 - [FIX] Normalizes leading/trailing whitespace in path segments (#54) - [CHG] Moves route `/open/search` → [`/search/open`](https://zottmann.dev/obsidian-actions-uri/routes/search/) (#17) - [DOC] Marks route `/open/search` as deprecated (#17) - [DOC] Removes docs for previously removed `/open/*` routes (#17) - [DOC] Corrects docs to reflect reality (#55) - [DOC] Corrects docs where callbacks are optional, not required (#55) - [`/note/open`](https://zottmann.dev/obsidian-actions-uri/routes/note/#noteopen-v012) - [`/daily-note/open-current`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteopen-current-v012) - [`/daily-note/open-most-recent`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/#daily-noteopen-most-recent-v012) ## 0.16.4, 2023-02-15 - [CHG] Increases time waiting for search results in `/search/all-notes` to 2s (#50) ## 0.16.3, 2023-02-06 - [FIX] Fixes handling of backslashes and colon characters in file names (#43) ## 0.16.2, 2023-01-30 - [CHG] Shortens `*/rename` error messages ## 0.16.1, 2023-01-28 - [FIX] Adds graceful handling of the default "Default location for new notes" configuration setting (#41) ## 0.16.0, 2023-01-26 - [NEW] Adds [route `/note/delete`](https://zottmann.dev/obsidian-actions-uri/routes/note/) for deleting a note (#30) - [NEW] Adds [route `/note/rename`](https://zottmann.dev/obsidian-actions-uri/routes/note/) for renaming or moving a note (#30) - [NEW] Adds [route `/note/trash`](https://zottmann.dev/obsidian-actions-uri/routes/note/) for moving a note to the trash (#30) - [NEW] Adds [route `/folder/list`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) for fetching the list of available folders (#30) - [NEW] Adds [route `/folder/create`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) for creating a new folder or folder structure (#30) - [NEW] Adds [route `/folder/delete`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) for deleting a folder and all its contents (#30) - [NEW] Adds [route `/folder/rename`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) for renaming or moving a folder (#30) - [NEW] Adds [route `/folder/trash`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) for moving a folder to the trash (#30) - [DEL] Deprecates route [`/vault/list-folders`](https://zottmann.dev/obsidian-actions-uri/routes/vault/) in favor of [`/folder/list`](https://zottmann.dev/obsidian-actions-uri/routes/folder/) (#30) ## 0.15.0, 2022-12-31 - [NEW] Adds extra return params for use by [Actions for Obsidian](https://obsidian.actions.work) (#26) - [CHG] Root routes (e.g., `/`, `/note`, `/daily-note`) return a non-empty result message - [FIX] Addresses endless loop in string search/replace routes which would occur when the replacement included the search term (#28) Have a wondrous 2023, people 🚀 ## 0.14.2, 2022-12-16 - [NEW] Adds [Dataview support for `TABLE` and `LIST` queries](https://zottmann.dev/obsidian-actions-uri/routes/dataview/) (#4) - [NEW] Adds [route `/tags/list`](https://zottmann.dev/obsidian-actions-uri/routes/tags/) for fetching a list of all existing tags (#16) - [NEW] Adds [route `/note/list`](https://zottmann.dev/obsidian-actions-uri/routes/note/) for fetching the list of available Markdown files (#24) - [NEW] Adds [route `/vault/list-folders`](https://zottmann.dev/obsidian-actions-uri/routes/vault/) for fetching the list of available folders (#24) - [NEW] Adds [route `/vault/list-non-notes-files`](https://zottmann.dev/obsidian-actions-uri/routes/vault/) for fetching list of non-Markdown files (#24) - [NEW] Adds [route `/vault/list-all-files`](https://zottmann.dev/obsidian-actions-uri/routes/vault/) for fetching all files present in a vault (#24) - [CHG] Adjusts [route `/daily-note/list`](https://zottmann.dev/obsidian-actions-uri/routes/daily-note/) to return the same structure as its `/note/list` counterpart - [FIX] Notes returned will now always contain the four return parameters `result-content`, `result-body`, `result-front-matter` and `result-filepath` (#22) - [FIX] Adds missing links to route docs detail pages ## 0.13.0, 2022-12-07 - [NEW] Adds `tags/list` route (#16) - [NEW] Adds `vault/info` route (#20) - [CHG] Makes `vault/close` desktop-only (due to the different Obsidian foundations on mobile and desktop) - [CHG] Replaces Twitter links w/ Mastodon links in docs ## 0.12.1, 2022-11-23 - [NEW] Added `/vault/open` and `/vault/close` routes (#18) - [CHG] Error callbacks now carry two parameters, `errorCode` and `errorMessage`, instead of just `error`. - [CHG] The routes `note/open` and `daily-note/open` supersede `open/note` and `open/daily-note` respectively. The old routes have been removed. - [FIX] Creating a note would sometimes result in the creation of a folder and an error (#16) - [FIX] Searching/replacing a string would result in an error if the search term looked like a regex (#15) ## 0.11.0, 2022-11-07 - [NEW] Refactors error callback parameters (#12) - [CHG] Replaces all occurrences of `global.app` (#14) - [CHG] Changes spaces in callback URLs from plus-sign- to percent-encoding (#11) - [DEL] Removes support for `call-id` parameter (#7) - [CHG] Drops support for Obsidian # Actions URI This plugin adds additional `x-callback-url` endpoints to [Obsidian](https://obsidian.md) for common actions — it's a clean, super-charged addition to the built-in [Obsidian URIs](https://help.obsidian.md/Advanced+topics/Using+obsidian+URI#Using+Obsidian+URIs), for working with [daily notes, notes, getting search results](https://czottmann.github.io/obsidian-actions-uri/routes/) etc. ## Documentation For information about available features and routes please see the [documentation](https://czottmann.github.io/obsidian-actions-uri/). Bug reports and feature requests are welcome, feel free to [open an issue](https://github.com/czottmann/obsidian-actions-uri/issues) here on GitHub. For discussions, please visit the [Plugin Forum](https://forum.actions.work/c/obsidian-actions-uri/6) ("Log in with GitHub" is enabled). ## Plugin Project Status ![GitHub release (latest by date)](https://img.shields.io/github/v/release/czottmann/obsidian-actions-uri?label=current+release&color=09f) ![Maturity: Stable](https://img.shields.io/badge/maturity-stable-09f) ![Development: Active](https://img.shields.io/badge/development-active-09f) ![Support: Active](https://img.shields.io/badge/support-active-09f) (Please see Don McCurdy's post ["Healthy expectations in open source"](https://www.donmccurdy.com/2023/07/03/expectations-in-open-source/) for information about the different statuses.) ## Installation 1. Search for "Actions URI" in Obsidian's community plugins browser. ([This link should bring it up.](https://obsidian.md/plugins?id=zottmann)) 2. Install it. 3. Enable the plugin in your Obsidian settings under "Community plugins". That's it. ## Installation via BRAT (for pre-releases or betas) 1. Install [BRAT](https://github.com/TfTHacker/obsidian42-brat). 2. Add "Actions URI" to BRAT: 1. Open "Obsidian42 - BRAT" via Settings → Community Plugins 2. Click "Add Beta plugin" 3. Use the repository address `czottmann/obsidian-actions-uri` 3. Enable "Actions URI" under Settings → Options → Community Plugins ## Development Clone the repository, run `pnpm install` OR `npm install` to install the dependencies. Afterwards, run `pnpm dev` OR `npm run dev` to compile and have it watch for file changes. ## Author Carlo Zottmann, , https://c.zottmann.dev, https://github.com/czottmann ## Projects using Actions URI - [Actions for Obsidian](https://obsidian.actions.work/): Useful new Obsidian actions for the Shortcuts app on macOS and iOS, bridging the gap between your notes and your workflows. Want to see your project here? Drop me a line! (See "Author" section.) ## Thanks to … - the [obsidian-tasks](https://github.com/obsidian-tasks-group/obsidian-tasks) crew for the "starter templates" for the GitHub Action workflow and the handy `release.sh` script ## License MIT, see [LICENSE.md](LICENSE.md). ================================================ FILE: bin/prepare-route-docs.sh ================================================ #!/bin/bash # Dependencies: https://github.com/chmln/sd for F in src/routes/*.ts; do ls $F grep ' \/\/' $F \ | sd ' //' '' \ | sd '^\s*\}' '' \ | sd '^\s*\{' "\n| Parameter | Value | optional | |\n| --- | --- | --- |" \ | sd '^\s*"*(.+?)"*(\?*): (.+);' '| `$1` | $3 | $2 |' \ | sd --string-mode '| undefined ' '' \ | sd --string-mode '| ? |' '| yes |' \ | sd '^ +' '' \ > docs/route--$(basename $F .ts).md done ================================================ FILE: bin/tag-release.fish ================================================ #!/opt/homebrew/bin/fish --login function allow_or_exit read -P "$argv[1] Continue? [y/n] " -l response switch $response case y Y echo # We're good to go case '*' echo "Aborting!" exit 0 end end argparse \ "platform=" "patch-version=" "obsidian-version=" help \ -- $argv or return if test -n "$_flag_help" echo " Only works in `release/` branches, e.g. `release/1.0.x` or `release/2.1.x`. Commits the current changes and tags the commit, effectively marking the commit as the release commit for the version contained in the branch name. EXAMPLE: - If the branch name is `release/1.2.x`, and the patch version is '3', then the tag `1.2.3` will be created. FLAGS: --patch-version Will be added to the branch release number. REQUIRED. --obsidian-version The minimum obsidian version for this release. REQUIRED. --help This usage description. " exit 1 end if test -z "$_flag_patch_version" echo "ERROR: --patch-version must be set, exiting" exit 1 end if test -z "$_flag_obsidian_version" echo "ERROR: --obsidian-version must be set, exiting" exit 1 end set git_branch (git branch --show-current) set release_tag ( echo $git_branch | cut -d "/" -f 2 | string replace ".x" ".$_flag_patch_version" ) allow_or_exit "New tag will be named '$release_tag', minimum Obsidian version is $_flag_obsidian_version." echo "Updating package.json" set TEMP_FILE (mktemp) jq ".version |= \"$release_tag\"" package.json >"$TEMP_FILE"; or exit 1 mv "$TEMP_FILE" package.json echo "Updating manifest.json" set TEMP_FILE (mktemp) jq ".version |= \"$release_tag\" | .minAppVersion |= \"$_flag_obsidian_version\"" \ manifest.json >"$TEMP_FILE"; or exit 1 mv "$TEMP_FILE" manifest.json echo "Updating versions.json" set TEMP_FILE (mktemp) jq ". += {\"$release_tag\": \"$_flag_obsidian_version\"}" \ versions.json >"$TEMP_FILE"; or exit 1 mv "$TEMP_FILE" versions.json echo "Updating src/plugin-info.json & src/plugin-info.ts" set TEMP_FILE (mktemp) set DATE_NOW (date +%FT%T%z) jq ".pluginVersion |= \"$release_tag\" | .pluginReleasedAt |= \"$DATE_NOW\"" \ src/plugin-info.json >"$TEMP_FILE"; or exit 1 mv "$TEMP_FILE" src/plugin-info.json echo -n "/* File will be overwritten by bin/release.sh! */ export const PLUGIN_INFO = " >src/plugin-info.ts cat src/plugin-info.json >>src/plugin-info.ts echo "Committing the following files with a message of '[REL] Release $release_tag':" echo git status --porcelain | sed -E "s/^/ /" echo allow_or_exit git commit -m "[REL] Release $release_tag" -a git tag $release_tag echo "Done!" echo allow_or_exit "Now pushing the commit and tag to the remote …" git push --tags echo "Done!" echo allow_or_exit "Now merging branch '$git_branch' into 'main' …" git checkout main git pull --tags git merge -m "[MRG] Merges release '$release_tag'" --no-edit --no-ff $git_branch allow_or_exit "Push main to remote?" git push echo "Done!" echo ================================================ FILE: docs/404.md ================================================ --- title: Page Not Found description: Whatever you thought was here, isn't. The plot thickens, the game is afoot! permalink: /404.html nav_exclude: true --- Whatever you thought was here, isn't. The plot thickens, the game is afoot! `404 Not Found` ================================================ FILE: docs/_config.yml ================================================ remote_theme: just-the-docs/just-the-docs title: Actions URI description: A plugin for Obsidian.md adding additional `x-callback-url` endpoints to the app for common actions — it's a clean, super-charged addition to Obsidian URI. baseurl: "/obsidian-actions-uri" url: "https://zottmann.dev" permalink: pretty aux_links: "Actions URI on GitHub": - "https://github.com/czottmann/obsidian-actions-uri" # External navigation links nav_external_links: - title: "Actions URI on GitHub" url: "https://github.com/czottmann/obsidian-actions-uri" - title: "Actions URI in Community Plugins" url: "https://obsidian.md/plugins?id=actions-uri" - title: "🪳 Issues or Bugs?" url: "https://github.com/czottmann/obsidian-actions-uri/issues" - title: "💡 Ideas & suggestions?" url: "https://forum.actions.work/c/obsidian-actions-uri/6" include: - license.md # Back to top link back_to_top: false back_to_top_text: "Back to top" footer_content: 'Copyright © 2022 Carlo Zottmann. MIT licensed. This plugin and its author are neither affiliated with nor endorsed by Obsidian.' ================================================ FILE: docs/_includes/head_custom.html ================================================ ================================================ FILE: docs/anatomy.md ================================================ --- nav_order: 5 --- # Anatomy of an Actions URI… URL An Action URI-provided URL doesn't look much different from a standard Obsidian URI. Its host "actions-uri" tells Obsidian which plugin is taking care of the incoming call: > obsidian://**actions-uri**/daily-note/get-current?parameter=value … and the path (a.k.a. a route) specifies what to do: > obsidian://actions-uri/**daily-note/get-current**?parameter=value Both data and configuration are passed as URL search parameters: > obsidian://actions-uri/daily-note/get-current?**parameter=value** **Please note:** all parameter data must be properly encoded (see [Wikipedia](https://en.wikipedia.org/wiki/Percent-encoding) for a short intro), as Actions URI makes no attempts to correct malformed input. ================================================ FILE: docs/callbacks.md ================================================ --- nav_order: 4 --- # Getting data back from Actions URI All routes support return calls back to the sender. This is done by passing callback URLs as parameters, e.g.: ``` obsidian://actions-uri/note/get ?vault=My%20Vault &file=My%20super%20note &x-success=my-app%3A%2F%2Fsuccess%3Frequest-id%3D123456789 &x-error=my-app%3A%2F%2Ferror ``` This example call, formatted for better readability, contains four parameters: `vault`, `file`, `x-success` and `x-error`. The latter two are used to provide callbacks to the sender. - `x-success` contains a base URL for returning success information — in the above example, that's `my-app://success?request-id=123456789` - `x-error` contains a base URL for returning failure information — in the above example, that's `my-app://error` When Actions URI has completed the work requested by the incoming call, it'll build a callback URL from the value of either `x-success` or `x-error`. The search parameters containing the requested data (prefixed with `result-`) will be added to the URL, then the outgoing call is made. The `x-success`/`x-error` URL may contain a path and/or parameters, those will be used as-is. Let's continue with the above example. Assuming the file `My super note.md` exists in vault `My Vault` and contains both front matter and the note body *"Actions URI is ready for action!"*, Actions URI would make a callback to the following URL, formatted for better readability: ``` my-app://success ?request-id=123456789 &result-body=%0AActions+URI+is+ready+for+action%21 &result-content=---%0Atags%3A+test%0A---%0A%0AActions+URI+is+ready+for+action%21 &result-filepath=My+super+note.md &result-front-matter=tags%3A+test%0A ``` The successful callback contains the full note content (`result-content`), the note body (`result-body`), the note's path (`result-filepath`) and its front matter (`result-front-matter`). Assuming the note does **not** exist, the resulting call would be: ``` my-app://error ?errorCode=404 &errorMessage=Note+couldn%27t+be+found ``` `errorCode` contains a HTTP status, `errorMessage` contains a simple explanation. ## Important note on callback parameters **The on-success callback parameter structure varies depending on the endpoints.** See the relevant [routes descriptions](routes.md) for details. On-error callbacks always have the same parameter structure. ## Debug mode With `debug-mode` enabled in the incoming request (see ["Parameters required in/ accepted by all calls"](parameters.md)), the on-success callback of the above example would look like this: ``` my-app://success ?request-id=123456789 &result-body=%0AActions+URI+is+ready+for+action%21 &result-content=---%0Atags%3A+test%0A---%0A%0AActions+URI+is+ready+for+action%21 &result-filepath=My+super+note.md &result-front-matter=tags%3A+test%0A &input-action=actions-uri%2Fnote%2Fget &input-file=My+super+note.md &input-silent=false &input-vault=Testbed ``` It's called "debug mode" because it's helpful when developing an external *whatever* communicating with Obsidian via Actions URI. In production you'll probably want to pair the callbacks to your original requests, that's where the `request-id` parameter (or something similar) in the `x-success` URL comes into play. I'm not aware of any drawbacks keeping debug mode on in live code, however. You do you! 🖖🏼 ================================================ FILE: docs/changes.md ================================================ # Release history Please see [CHANGELOG.md](https://github.com/czottmann/obsidian-actions-uri/blob/main/CHANGELOG.md). ================================================ FILE: docs/faq.md ================================================ --- nav_order: 99 --- # FAQ ## Why does this exist? One major reason is an upcoming project of mine, for which I need a way to access my vault data from "the outside". The existing options either didn't fully cut it — like [Obsidian URI](https://help.obsidian.md/Advanced+topics/Using+obsidian+URI) — or were pretty full of features but left me wanting anyways, like [Advanced URI](https://github.com/Vinzent03/obsidian-advanced-uri) which does *a lot* but in a way and format that didn't quite gel with me. (Additionally, its author doesn't actually use it anymore themselves[^1] which in my eyes makes it a gamble to rely on it for a new project.) This is not meant as a diss, mind; it's just not the right thing for me, personally. [^1]: Source: [vinzent03.github.io/obsidian-advanced-uri](https://vinzent03.github.io/obsidian-advanced-uri/) So, here we are! 😀 ## *"I have an idea for this!"* Cool! If you want to discuss it, either [post it to the Ideas discussion board](https://github.com/czottmann/obsidian-actions-uri/discussions/categories/ideas) or [hit me up on Mastodon](https://actions.work/@obsidian). I'm all ears! 👂🏼 ## *"There's a bug!"*, *"There's something wrong"* etc. Oh no! Please [file a bug report](https://github.com/czottmann/obsidian-actions-uri/issues) here or (if you're unsure about it) [ping me on Mastodon](https://actions.work/@obsidian). --- ================================================ FILE: docs/index.md ================================================ --- nav_order: 0 --- Plugin logo thingie: an app icon, a two-way communications icon, a note icon # Actions URI Obsidian natively supports a custom URI protocol `obsidian://` which can trigger various actions within the app. This is commonly used on macOS and mobile apps for automation and cross-app workflows. **This plugin adds new `x-callback-url` endpoints** to Obsidian so that external sources can better interact with an Obsidian instance by making `GET` requests to a `obsidian://actions-uri/*` URL. All new routes support `x-success` and `x-error` parameters as a way of communicating back to the sender. It's a clean, somewhat super-charged addition to Obsidian's [own URI scheme](https://help.obsidian.md/Advanced+topics/Using+obsidian+URI#Using+Obsidian+URIs). ## Author Carlo Zottmann, - GitHub: [@czottmann](https://github.com/czottmann) - Mastodon: - [@czottmann@norden.social](https://norden.social/@czottmann) - [@actionsdotwork@pkm.social/](https://pkm.social/@actionsdotwork) - Bluesky: [@zottmann.dev](https://bsky.app/profile/zottmann.dev) - Obsidian: [@czottmann](https://forum.obsidian.md/u/czottmann) - Website: [c.zottmann.dev](https://c.zottmann.dev/) ## Projects using Actions URI - [Actions for Obsidian](https://obsidian.actions.work/): Useful new Obsidian actions for the Shortcuts app on macOS and iOS, bridging the gap between your notes and your workflows. Want to see your project here? Drop me a line! (See "Author" section.) ## Plugin project status ![GitHub release (latest by date)](https://img.shields.io/github/v/release/czottmann/obsidian-actions-uri?label=current+release&color=09f) ![Maturity: Stable](https://img.shields.io/badge/maturity-stable-09f) ![Development: Active](https://img.shields.io/badge/development-active-09f) ![Support: Active](https://img.shields.io/badge/support-active-09f) (Please see Don McCurdy's post ["Healthy expectations in open source"](https://www.donmccurdy.com/2023/07/03/expectations-in-open-source/) for information about the different statuses.) ================================================ FILE: docs/installation.md ================================================ --- nav_order: 1 --- # Installation 1. Search for "Actions URI" in Obsidian's community plugins browser and install it. ([This link should bring it up.](https://obsidian.md/plugins?id=zottmann)) 2. Enable the plugin in your Obsidian settings under "Community plugins". That's it. # Installation via BRAT (for pre-releases or betas) 1. Install [BRAT](https://github.com/TfTHacker/obsidian42-brat). 2. Add "Actions URI" to BRAT: 1. Open "Obsidian42 - BRAT" via Settings → Community Plugins 2. Click "Add Beta plugin" 3. Use the repository address `czottmann/obsidian-actions-uri` 3. Enable "Actions URI" under Settings → Options → Community Plugins # Development Clone the repository, run `pnpm install` OR `npm install` to install the dependencies. Afterwards, run `pnpm dev` OR `npm run dev` to compile and have it watch for file changes. ================================================ FILE: docs/license.md ================================================ # MIT License Please see [LICENSE.md](https://github.com/czottmann/obsidian-actions-uri/blob/main/LICENSE.md). ================================================ FILE: docs/parameters.md ================================================ --- nav_order: 3 --- # Parameters required & accepted by all endpoints | Parameter | Value type | Optional? | Description | ------------------------- | ---------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------- | `vault` | string | | The name of the target vault. | `x-success` | string | mostly | Base URL for on-success callbacks, see [Getting data back from Actions URI](callbacks.md). | `x-error` | string | mostly | Base URL for on-error callbacks, see [Getting data back from Actions URI](callbacks.md). | `debug-mode` | boolean | yes | When enabled, Actions URI will include all parameters of the original request in the return calls, prefixed with `input-`. Defaults to `false`. | `hide-ui-notice-on-error` | boolean | yes | v1.8+ When enabled, the UI notice will not be shown on "note not found" errors etc. Defaults to `false`. ## Notes about parameters
"mostly"
optional unless specified otherwise in the detailed route description
"boolean"
Actions URI uses what I call "benevolent booleans": the absence of the parameter, an empty string or the string "false" are considered false, everything else is true
================================================ FILE: docs/routes/command.md ================================================ --- parent: New Routes --- # `/command` v1.3+ These routes deal with getting the list of available Obsidian commands (think Command Palette) and executing them. Their URLs start with `obsidian://actions-uri/command`.
  ## Root, i.e. `/command` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/command/list` Returns list of all Obsidian Commands available in the queried vault. | Parameter | Value | Optional? | Description | | ----------- | ------ | :-------: | --------------------------------- | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ----------------- | ------------------------------------------------------------ | | `result-commands` | JSON-encoded array of objects (`{id: string, name: string}`) | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/command/execute` Triggers the passed-in command or commands in sequence, in the specified vault. | Parameter | Value | Optional? | Description | | --------------- | ------ | :-------: | ---------------------------------------------------------------- | | `commands` | string | | Comma-separated list of command IDs. | | `pause-in-secs` | number | optional | Length of the pause in seconds between commands. Default: `0.2`. | | `x-success` | string | optional | base URL for on-success callbacks | | `x-error` | string | optional | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/dataview.md ================================================ --- parent: New Routes --- # `/dataview` v0.14+ These routes allow for running [Dataview DQL queries](https://blacksmithgu.github.io/obsidian-dataview/queries/structure/). Their URLs start with `obsidian://actions-uri/dataview`. Currently, only [`LIST`](https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#list-queries) and [`TABLE`](https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#table-queries) DQL queries are supported.
  ## Root, i.e. `/dataview` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/dataview/list-query` ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ------------------- | | `dql` | string | | A DQL `LIST` query. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------- | | `result-data` | An array containing strings (the list results), encoded as JSON string. Every result is returned as string array. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/dataview/table-query` ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | -------------------- | | `dql` | string | | A DQL `TABLE` query. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------- | --------------------------------------------------------------------------------- | | `result-data` | An array containing arrays of strings (the result table), encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/file.md ================================================ --- parent: New Routes --- # `/file` v1.5+ These routes deal with reading, writing and updating files (i.e., any file, not just notes). Their URLs start with `obsidian://actions-uri/file/…`.
  ## Root, i.e. `/file` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/file/list` Returns a list of all files (i.e. everything, not just notes) in the vault. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | -------------- | ------------------------------------------------------- | | `result-paths` | Array containing all file paths encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/file/get-active` Returns the currently active/focussed file. If there is no open file, an error 404 is returned. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------------ | ----------------------------------------------------------- | | `result-filepath` | The path to the file relative from the vault's root folder. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/file/open` Opens a specific file in Obsidian. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------- | | `file` | string | | The path of the file, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/file/rename` Renames or moves a file. If the new file path already exists, an error will be returned. If the new file path is the same as the original one, nothing will happen. You can move a file to a different folder by specifying the new file path with a different folder name. For example, this will move the file "image.jpg" from its position at the vault root into "another-folder" while keeping the file name: - `file`: "image.jpg" - `new-filename`: "another-folder/image.jpg" Any folder structure in `new-filename` will **not** be created automatically. If a folder is specified that does not exist, an error will be returned. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | -------------- | ---------- | :-------: | -------------------------------------------------------------------------------------------------- | | `file` | string | | The path of the file, relative from the vault's root. | | `new-filename` | string | | The new path of the file, relative from the vault's root. | | `silent` | boolean | optional | *"After updating the file, do **not** open it in Obsidian."* Defaults to `false`. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/file/delete` Immediately deletes a specific file. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------- | | `file` | string | | The path of the file, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/file/trash` Moves a specific file to the trash (either vault-local trash or system trash, depending on the configuration made in _Settings_ → _Files & Links_ → _Deleted Files_). ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------- | | `file` | string | | The path of the file, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/folder.md ================================================ --- parent: New Routes --- # `/folder` v0.16+ These routes deal with folders. Their URLs start with `obsidian://actions-uri/folder/…`.
  ## Root, i.e. `/folder` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/folder/list` Returns a list of folder paths. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). | Parameter | Value type | Optional? | Description | | ----------- | ---------- | :-------: | --------------------------------- | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | -------------- | --------------------------------------------------------- | | `result-paths` | Array containing all folder paths encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/folder/create` Creates a new folder or folder structure. In case the folder already exists, nothing will happen. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ------------------------------------------------ | | `folder` | string | | The folder path, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/folder/rename` Renames or moves a folder. If the new folder path already exists, an error will be returned. If the new folder path is the same as the original one, nothing will happen. Any folder structure in `new-foldername` will **not** be created automatically. If a folder is specified that does not exist, an error will be returned. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | ---------------- | ---------- | :-------: | ---------------------------------------------------- | | `folder` | string | | The folder path, relative from the vault's root. | | `new-foldername` | string | | The new folder path, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/folder/delete` Immediately deletes a folder and all its contents. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ------------------------------------------------ | | `folder` | string | | The folder path, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/folder/trash` Moves a folder to the trash (either vault-local trash or system trash, depending on the configuration made in _Settings_ → _Files & Links_ → _Deleted Files_). ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------- | ---------- | :-------: | ------------------------------------------------ | | `folder` | string | | The folder path, relative from the vault's root. | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | ------------------------ | | `result-message` | A short success message. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/info.md ================================================ --- parent: New Routes --- # `/info` These routes deal with plugin & Obsidian environment info. Their URLs start with `obsidian://actions-uri/info`.
  ## `/info` Returns information about the plugin and the current Obsidian instance. | Parameter | Value | Optional? | Description | | ----------- | ------ | :-------: | --------------------------------- | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------------------------- | --------------------------------------------------------------------------------------------------- | | `result-plugin-version` | The version of the responding Action URI plugin | | `result-plugin-released-at` | The release timestamp of the responding Action URI plugin (ISO 8601) | | `result-api-version` | The API version of the app, which follows the release cycle of the desktop app | | `result-node-version` | The version of Node running the plugin, e.g. "16.13.2" | | `result-os` | OS information gathered from Obsidian's user agent string, e.g. "Macintosh; Intel Mac OS X 10_15_7" | | `result-platform` | Returns "macOS", "Windows/Linux" "iOS" or "Android" | ================================================ FILE: docs/routes/note-properties.md ================================================ --- parent: New Routes --- # `/note-properties` v1.4+ These routes deal with reading, writing and updating [note properties](https://help.obsidian.md/Editing+and+formatting/Properties). Their URLs start with `obsidian://actions-uri/note-properties/…`. Please keep in mind that setting new properties will effectively rewrite a note's front matter.
  ## Root, i.e. `/note-properties` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/note-properties/get` Returns a note's properties. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | string | | **Mutually exclusive with `periodic-note`.** The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | | `periodic-note` | string | | v1.6+ **Mutually exclusive with `file`.** Allowed values: `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------------- | --------------------------------------------- | | `result-properties` | The file's properties encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/note-properties/set` Overwrites or updates a note's properties. When **overwriting**, all of the note's properties will be replaced with the new ones. When **updating**, the properties specified in the `properties` parameter will replace existing keys with the same name, leaving the rest untouched. In absence of a dedicated Obsidian API method for writing properties (AFAICT), Actions URI will translate the `properties` parameter into front matter YAML, and then replace the old front matter. Obsidian will pick up the file change and populate the note's properties from the changed front matter. _How_ Obsidian interprets those values is up to you, and it can only be specified in Obsidian itself — please see [the official Property doc page for more details](https://help.obsidian.md/Editing+and+formatting/Properties#Property%20types). The `properties` parameter will only accept object values with valid types (i.e., string, list of strings, number, and boolean). Date and Date & Time properties are represented as string values. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | string | | **Mutually exclusive with `periodic-note`.** The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | | `periodic-note` | string | | v1.6+ **Mutually exclusive with `file`.** Allowed values: `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | | `properties` | string | | The new properties encoded as JSON string. | | `mode` | string | optional | Either `overwrite` or `update`. Defaults to `overwrite`. | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------------------- | ------------------------------------------------------------------------------------ | | `result-body` | The note body, i.e. the note file content minus possible front matter. | | `result-content` | The entire content of the note file. | | `result-filepath` | The file path of the note, relative from the vault root folder. | | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | | `result-properties` | The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/note-properties/clear` Removes the entirety of a note's properties (and therefore, its front matter). ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | string | | **Mutually exclusive with `periodic-note`.** The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | | `periodic-note` | string | | v1.6+ **Mutually exclusive with `file`.** Allowed values: `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | | `properties` | string | | The new properties encoded as JSON string. | | `mode` | string | optional | Either `overwrite` or `update`. Defaults to `overwrite`. | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------------------- | ------------------------------------------------------------------------------------ | | `result-body` | The note body, i.e. the note file content minus possible front matter. | | `result-content` | The entire content of the note file. | | `result-filepath` | The file path of the note, relative from the vault root folder. | | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | | `result-properties` | The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/note-properties/remove-keys` Remove one or more keys from a note's properties (and therefore, its front matter). The `keys` parameter is a JSON-encoded array of strings, e.g. `["createdAt", "aliases"]`, because keys in a note's properties may contain commas etc., which prevented using a simpler CSV-type parameter like "createdAt,aliases". 🤷🏻‍♂️ ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | | --------------- | ---------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | string | | **Mutually exclusive with `periodic-note`.** The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | | `periodic-note` | string | | v1.6+ **Mutually exclusive with `file`.** Allowed values: `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | | `keys` | string | | The list of keys to remove, as a JSON-encoded array of strings. | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------------------- | ------------------------------------------------------------------------------------ | | `result-body` | The note body, i.e. the note file content minus possible front matter. | | `result-content` | The entire content of the note file. | | `result-filepath` | The file path of the note, relative from the vault root folder. | | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | | `result-properties` | The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/note.md ================================================ --- parent: New Routes --- # `/note` These routes deal with reading, writing and updating notes and periodic notes (daily, weekly, etc.). Their URLs start with `obsidian://actions-uri/note/…`.
  ## Root, i.e. `/note` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done.   ## `/note/list` v0.14+ Returns a path list of either all Markdown files in the vault, or just the subset of all notes that are of a specific Periodic Note type. Default is to return all available notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | --------------------------------- | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | optional | | `x-success` | string | | base URL for on-success callbacks | `x-error` | string | | base URL for on-error callbacks ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | -------------- | ------------------------------------------------------- | `result-paths` | Array containing all file paths encoded as JSON string. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/get` Returns a specific note. ### Parameters A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `x-success` | string | | base URL for on-success callbacks | `x-error` | string | | base URL for on-error callbacks | `silent` | boolean | optional | *"Do **not** open the note in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). **Please note:** `result-properties` might be empty if Obsidian can't process the note's front matter. This can happen if the front matter is malformed or if the note contains a YAML block that is not front matter. On success: | Parameter | Description | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | `result-body` | The note body, i.e. the note file content minus possible front matter. | `result-content` | The entire content of the note file. | `result-filepath` | The file path of the note, relative from the vault root folder. | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | `result-properties` | v1.4+ The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | `result-uid` | v1.6+ The note's UID, if available | `result-uri-path` | v1.8+ The note's URI, by path. Can be used to link to the note from other places on the same device. | `result-uri-uid` | v1.8+ The note's URI, by UID (if available). Can be used to link to the note from other places on the same device. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/get-first-named` Returns the first note with the specified name. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | ----------- | ------------------------------------------------------------------------------------------------- | :-------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | `file` | string | | The name of the note. The extension `.md` can be omitted. | `sort-by` | `best-guess` \|`path-asc` \|`path-desc` \|`ctime-asc` \|`ctime-desc` \|`mtime-asc` \|`mtime-desc` | optional | In case there are multiple notes with the same name, they will be sorted by this criterion before the first is picked from the resulting list. Example options: `best-guess` (using Obsidian's link resolution (starting from root folder), default), `path-asc` (full path alphabetically), `ctime-asc` (creation time, oldest first), `mtime-asc` (modification time, oldest first). | `x-success` | string | | base URL for on-success callbacks | `x-error` | string | | base URL for on-error callbacks | `silent` | boolean | optional | *"Do **not** open the note in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). **Please note:** `result-properties` might be empty if Obsidian can't process the note's front matter. This can happen if the front matter is malformed or if the note contains a YAML block that is not front matter. On success: | Parameter | Description | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | `result-body` | The note body, i.e. the note file content minus possible front matter. | `result-content` | The entire content of the note file. | `result-filepath` | The file path of the note, relative from the vault root folder. | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | `result-properties` | v1.4+ The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | `result-uid` | v1.6+ The note's UID, if available | `result-uri-path` | v1.8+ The note's URI, by path. Can be used to link to the note from other places on the same device. | `result-uri-uid` | v1.8+ The note's URI, by UID (if available). Can be used to link to the note from other places on the same device. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/get-active` v1.5+ Returns the currently focussed note. If there is no open note or the currently focussed file is not a note, an error 404 is returned. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | ----------- | ---------- | :-------: | --------------------------------- | `x-success` | string | | base URL for on-success callbacks | `x-error` | string | | base URL for on-error callbacks ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | `result-body` | The note body, i.e. the note file content minus possible front matter. | `result-content` | The entire content of the note file. | `result-filepath` | The file path of the note, relative from the vault root folder. | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | `result-properties` | The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | `result-uid` | v1.6+ The note's UID, if available | `result-selection` | v1.6+ The current text selection, if available. (Plain text, no formatting.) | `result-uri-path` | v1.8+ The note's URI, by path. Can be used to link to the note from other places on the same device. | `result-uri-uid` | v1.8+ The note's URI, by UID (if available). Can be used to link to the note from other places on the same device. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/open` v0.12+ Opens a specific note in Obsidian. ### Parameters A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` \| `recent-daily` \| `recent-weekly` \| `recent-monthly` \| `recent-quarterly` \| `recent-yearly` | see above | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/create` Creates a new note. The default behavior in case there's already a note with the same name / at the requested file path, the base file name will be suffixed with a number. For example, if the desired file name is `My Note.md` but that file already exists, the note will be saved as `My Note 1.md`; if the desired file `a/Folder/Another Note 17.md` already exists, the note will be saved under `a/Folder/Another Note 18.md`. This route allows one of two **mutually exclusive** targeting parameters: `file` or `periodic-note`. - `file=some/note/path.md`: create a note at a specific file path. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. Depending on which one is used, additional parameters become available. ### When using the `file` parameter The `apply` parameter allows you to specify what to add to the note after creation. Available options are `content` (implied default) for adding a string, `templates` (for using the Template core plugin), `templater` (for using the Templater community plugin). Depending on the `apply` parameter's value, the following additional parameters are allowed: - `apply=content` - `content`: initial body of the note - `apply=templater`: - `template-file`: path of the template file to apply - `apply=templates`: - `template-file`: path of the template file to apply Examples: - `file=new%20note.md&apply=content&content=Hello%20world!` - `file=new%20note.md&content=Hello%20world!` (as `apply=content` is the default) - `file=new%20note.md&apply=templater&template-file=Templates/Meeting%20notes.md` - `file=new%20note.md&apply=templates&template-file=Templates/Meeting%20notes.md` ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------: | ------------------------------------------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | +- `apply` | `content` \| `templater` \| `templates` | optional | What to add to the note after creation. Available options: `content` (implied default), `templates`, `templater`. | +— `content` | string | optional | The initial body of the note. **Prerequisite:** no `apply` parameter or `apply=content`. | +— `template-file` | string | optional | The path of the template file to apply. **Prerequisite:** `apply=templater` or `apply=templates`. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` \| `recent-daily` \| `recent-weekly` \| `recent-monthly` \| `recent-quarterly` \| `recent-yearly` | see above | | `if-exists` | `skip` \| `overwrite` | optional | What to do if the specified note exists. Set to `overwrite` for replacing the note or `skip` for using the existing note as-is. | `silent` | boolean | optional | *"After creating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). v1.4+ **Please note:** `result-properties` might be empty if Obsidian can't process the note's front matter. This can happen if the front matter is malformed or if the note contains a YAML block that is not front matter. On success: | Parameter | Description | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | `result-body` | The note body, i.e. the note file content minus possible front matter. | `result-content` | The entire content of the note file. | `result-filepath` | The file path of the note, relative from the vault root folder. | `result-front-matter` | The note's front matter, i.e. the note file content minus the note body. | `result-properties` | v1.4+ The note's [properties](https://help.obsidian.md/Editing+and+formatting/Properties). | `result-uid` | v1.6+ The note's UID, if available | `result-uri-path` | v1.8+ The note's URI, by path. Can be used to link to the note from other places on the same device. | `result-uri-uid` | v1.8+ The note's URI, by UID (if available). Can be used to link to the note from other places on the same device. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/append` Appends text to a note, either to the very end of the note (default) or to the section below a particular headline in a note. When you want to append text to a section below a headline, the headline must be entered *exactly* as it appears in the note: headline levels, capitalization, punctuation etc. For example, "## My Headline", "### My Headline", and "## my headline" are not identical. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------------- | ----------------------------------------------------------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `content` | string | | The text to be added at the end of the note. | `below-headline` | string | optional | v1.2+ Appends text below the given headline, before the next headline or EOF, whatever comes first. | `if-headline-missing` | `error` \| `skip` \| `add-headline` | optional | v1.6+ Only available together with `below-headline`. If the requested headline is missing, return an error, do nothing (`skip`), or add the headline to the end of the note. Default: `error`. | `create-if-not-found` | boolean | optional | *"If the note does not exist, create it before appending."* Defaults to `false`. v1.2+ | `ensure-newline` | boolean | optional | *"Make sure the note ends with a line break."* Defaults to `false`. | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/prepend` Prepends text to a note, either to the very beginning of the note (default) or to the section below a particular headline in a note. If the very beginning of the note is prepended, then the front matter will be honored (i.e. the new text will be added to the note body below the front matter) unless explicitly stated otherwise. When you prepend text to a section below a heading, the headline must be entered *exactly* as it appears in the note: headline levels, capitalization, punctuation etc. For example, "## My Headline", "### My Headline", and "## my headline" are not identical. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------------- | ----------------------------------------------------------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `content` | string | | The text to be added at the beginning of the note. | `below-headline` | string | optional | Prepends text below the given headline, before the next headline or EOF, whatever comes first. v1.2+ | `if-headline-missing` | `error` \| `skip` \| `add-headline` | optional | v1.6+ Only available together with `below-headline`. If the requested headline is missing, return an error, do nothing (`skip`), or add the headline to the end of the note. Default: `error`. | `create-if-not-found` | boolean | optional | *"If the note does not exist, create it before prepending."* Defaults to `false`. v1.2+ | `ensure-newline` | boolean | optional | *"Make sure the note ends with a line break."* Defaults to `false`. | `ignore-front-matter` | boolean | optional | *"Put the text at the very beginning of the note file, even if there is front matter."* Defaults to `false`. | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/touch` v1.5+ Sets the modification time of the note to now. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. | `x-success` | string | optional | base URL for on-success callbacks | `x-error` | string | optional | base URL for on-error callbacks ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/rename` v0.16+ Renames or moves a note. If the new file path already exists, an error will be returned. If the new file path is the same as the original one, nothing will happen. You can move a note to a different folder by specifying the new file path with a different folder name. For example, this will move the file "my-note.md" from its position at the vault root into "another-folder" while keeping the file name: - `file`: "my-note" - `new-filename`: "another-folder/my-note" Any folder structure in `new-filename` will **not** be created automatically. If a folder is specified that does not exist, an error will be returned. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | -------------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `new-filename` | string | | The new file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | ------------------------ | `result-message` | A short success message. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/search-string-and-replace` Does text replacement in a note. The search term is used as-is, i.e. it's a string search. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `search` | string | | Text string that should be replaced. | `replace` | string | | Replacement text. | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/search-regex-and-replace` Does a text replacement in a note. The search term is used as a pattern, i.e. it's a regular expression search. Capturing is supported. Example: the note contains the text *"and it was good"*, the `search` value is `/(it) (was)/` and the `replace` value is `$2 $1` — after the replacement the note would be changed to *"and was it good"*. Modifiers for case-insensitive and global search (`/…/i`, `/…/g`, `/…/gi`) are supported as well. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#using_the_global_and_ignorecase_flags_with_replace) for examples. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | | `search` | string | | Text pattern that should be replaced. | `replace` | string | | Replacement text. | `silent` | boolean | optional | *"After updating the note, do **not** open it in Obsidian."* Defaults to `false`. ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | --------------------------------- | `result-message` | A short summary of what was done. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/delete` v0.16+ Immediately deletes a specific note. A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | ------------------------ | `result-message` | A short success message. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong.   ## `/note/trash` v0.16+ Moves a specific note to the trash (either vault-local trash or system trash, depending on the configuration made in _Settings_ → _Files & Links_ → _Deleted Files_). A note can be targeted by one of three **mutually exclusive** targeting parameters: `file`, `uid`, or `periodic-note`. - `file`: a full file path. - v1.6+ `uid`: a unique identifier in the note's front matter. The key default is `uid`, e.g. "uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV". That key can be changed using the Actions URI settings UI. The URL parameter name will remain the same, i.e. the front matter key might be "id" or "uuid", but the URL parameter will still be `uid`. - v1.6+ `periodic-note`: a current periodic note (daily, weekly, etc.). Requires either the core Daily Notes plugin needs to be active, or the community plugin, Periodic Notes, must have its Daily Note feature enabled. Working with Weekly, Monthly, Quarterly or Yearly Notes requires the community plugin Periodic Notes. ### Parameters In addition to the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)): | Parameter | Value type | Optional? | Description | --------------- | ----------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------- | `file` | string | see above | The file path of the note, relative from the vault's root. The extension `.md` can be omitted. | `uid` | string | see above | Note ID as stored in a front matter key. Default key: "uid", configurable in Settings UI. | `periodic-note` | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly` | see above | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | ---------------- | ------------------------ | `result-message` | A short success message. On failure: | Parameter | Description | -------------- | ----------------------------------- | `errorCode` | A HTTP status code. | `errorMessage` | A short summary of what went wrong. ================================================ FILE: docs/routes/omnisearch.md ================================================ --- parent: New Routes --- # `/omnisearch` v1.1+ These routes deal with running searches through the [Omnisearch plugin](https://publish.obsidian.md/omnisearch/Index) in Obsidian. Their URLs start with `obsidian://actions-uri/omnisearch/…`. (Omnisearch isn't installed by default, but it is a superior choice for searching through your vault.)
  ## Root, i.e. `/omnisearch` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/omnisearch/all-notes` Returns Omnisearch results (file paths) for a given search query. | Parameter | Value | Optional? | Description | | ----------- | ------ | :-------: | --------------------------------- | | `query` | string | | A valid Omnisearch query | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------- | --------------------------------------------------- | | `result-hits` | Array with found file paths encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/omnisearch/open` Opens Omnisearch for a given query in Obsidian. | Parameter | Value | Optional? | Description | | --------- | ------ | :-------: | ------------------------ | | `query` | string | | A valid Omnisearch query | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. | ================================================ FILE: docs/routes/root.md ================================================ --- parent: New Routes --- # `/` All URLs start with `obsidian://actions-uri`.
  ## `obsidian://actions-uri` Does nothing but say hello (display a wee Notice toast in Obsidian.) ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. | ================================================ FILE: docs/routes/search.md ================================================ --- parent: New Routes --- # `/search` These routes deal with running searches in Obsidian. Their URLs start with `obsidian://actions-uri/search/…`.
  ## Root, i.e. `/search` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/search/all-notes` Desktop only Returns search results (file paths) for a given search query. | Parameter | Value | Optional? | Description | | ----------- | ------ | :-------: | --------------------------------- | | `query` | string | | | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------- | -------------------------------------------------------------------------------------------------------------- | | `result-hits` | Array with found file paths encoded as JSON string. (The max number of results varies, in my tests it was 36.) | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/search/open` Opens the search for a given query in Obsidian. | Parameter | Value | Optional? | Description | | --------- | ------ | :-------: | ----------------------------- | | `query` | string | | A valid Obsidian search query | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. | ================================================ FILE: docs/routes/tags.md ================================================ --- parent: New Routes --- # `/tags` v0.13+ These routes deal with a vault's tags. Their URLs start with `obsidian://actions-uri/tags`.
  ## Root, i.e. `/tags` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/tags/list` Returns list of all tags used in the queried vault. | Parameter | Value | Optional? | Description | | ----------- | ------ | :-------: | --------------------------------- | | `x-success` | string | | base URL for on-success callbacks | | `x-error` | string | | base URL for on-error callbacks | ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------- | -------------------------------------------------------------------------------------------------------------- | | `result-tags` | JSON-encoded string array, sorted alphabetically. The tags are returned as-is, i.e. including the leading `#`. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes/vault.md ================================================ --- parent: New Routes --- # `/vault` v0.12+ These routes deal with handling an Obsidian vault. Their URLs start with `obsidian://actions-uri/vault`.
  ## Root, i.e. `/vault` Does nothing but say hello. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ---------------- | --------------------------------- | | `result-message` | A short summary of what was done. |   ## `/vault/open` Opens a specific vault. For this to work, the vault must be in the list of vaults that Obsidian knows about and Actions URI needs to be active in that vault. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------- | ----------- | | / | | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/vault/close` Desktop only Closes a specific vault. For this to work, the vault must be in the list of vaults that Obsidian knows about and Actions URI needs to be active in that vault. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | --------- | ----------- | | / | | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/vault/info` v0.13+ Returns the full filesystem paths for the vault, its media folder and the "new note" folder. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | ------------------------------- | ---------------------------------------------------------- | | `result-base-path` | The full filesystem path to the vault. | | `result-attachment-folder-path` | The full filesystem path to the vault's media folder. | | `result-new-file-folder-path` | The full filesystem path to the vault's "new note" folder. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/vault/list-all-files` v0.14+ Returns a list of all files in the vault. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | -------------- | ------------------------------------------------------- | | `result-paths` | Array containing all file paths encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. |   ## `/vault/list-non-notes-files` v0.14+ Returns a list of all non Markdown files in the vault. ### Parameters Only supports the base parameters (see section ["Parameters required in/ accepted by all calls"](../parameters.md)). ### Return values These parameters will be added to the callbacks used for [getting data back from Actions URI](../callbacks.md). On success: | Parameter | Description | | -------------- | ------------------------------------------------------- | | `result-paths` | Array containing all file paths encoded as JSON string. | On failure: | Parameter | Description | | -------------- | ----------------------------------- | | `errorCode` | A HTTP status code. | | `errorMessage` | A short summary of what went wrong. | ================================================ FILE: docs/routes.md ================================================ --- nav_order: 2 has_children: true has_toc: false --- # New Routes - [`/command`](routes/command.md): Querying and triggering Obsidian commands. - [`/dataview`](routes/dataview.md): Running Dataview DQL queries. - [`/file`](routes/file.md): Working with non-note files. - [`/folder`](routes/folder.md): Dealing with folders. - [`/info`](routes/info.md): Plugin & Obsidian environment info. - [`/note`](routes/note.md): Reading, writing, updating notes (including periodic notes). - [`/note-properties`](routes/note-properties.md): - [`/omnisearch`](routes/omnisearch.md): Running Omnisearch searches in Obsidian. - [`/search`](routes/search.md): Running searches in Obsidian. - [`/tags`](routes/tags.md): Reading tags. - [`/vault`](routes/vault.md): Dealing with the current vault. - [`/`](routes/root.md): The root note. Not much is happening here. ================================================ FILE: esbuild.config.mjs ================================================ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; import { exec } from "child_process"; const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; const isProduction = process.argv[2] === "production"; const rsyncPlugin = { name: "rsyncPlugin", setup(build) { build.onEnd((_) => { if (process.env.USER !== "czottmann" || isProduction) { return; } exec( "../bin/sync-current-plugins-to-workbench-vault.fish", (error, _, stderr) => { if (error) { console.log(`exec error: ${error}`); } console.log( stderr ? stderr : "[watch] sync'd via `../bin/sync-current-plugins-to-workbench-vault.fish`", ); }, ); }); }, }; const config = { banner: { js: banner }, bundle: true, entryPoints: ["src/main.ts"], external: [ "obsidian", "electron", "@codemirror/autocomplete", "@codemirror/collab", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr", ...builtins, ], format: "cjs", logLevel: "info", minify: isProduction, outfile: "main.js", plugins: [rsyncPlugin], sourcemap: isProduction ? false : "inline", target: "es2022", treeShaking: true, }; if (isProduction) { await esbuild.build(config); } else { const ctx = await esbuild.context(config); ctx.watch(); await ctx.rebuild().catch(() => process.exit(1)); } ================================================ FILE: jest.config.js ================================================ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { transform: { // Suppresses message TS151001: "If you have issues related to imports, you // should consider setting `esModuleInterop` to `true` …" "^.+\\.ts$": ["ts-jest", { diagnostics: { ignoreCodes: ["TS151001"] } }], }, preset: "ts-jest", testEnvironment: "node", testMatch: ["**/tests/**/*.test.ts"], globalSetup: "./tests/global-setup.ts", globalTeardown: "./tests/global-teardown.ts", // Disables parallelization of tests to avoid "callback server not initialized" errors maxConcurrency: 1, maxWorkers: 1, }; ================================================ FILE: manifest.json ================================================ { "id": "actions-uri", "name": "Actions URI", "version": "1.8.4", "minAppVersion": "1.8.0", "description": "Adds additional `x-callback-url` endpoints to the app for common actions — it's a clean, super-charged addition to Obsidian URI.", "author": "Carlo Zottmann", "authorUrl": "https://github.com/czottmann", "isDesktopOnly": false } ================================================ FILE: package.json ================================================ { "name": "obsidian-actions-uri", "version": "1.8.4", "description": "This plugin for Obsidian (https://obsidian.md) adds additional `x-callback-url` endpoints to the app for common actions — it's a clean, super-charged addition to Obsidian URI.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs ", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "test": "npm run build && jest" }, "keywords": [], "author": "Carlo Zottmann, https://github.com/czottmann/", "license": "MIT", "devDependencies": { "@jest/globals": "^29.7.0", "@types/jest": "^29.5.14", "@types/node": "^18.19.67", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.17.0", "builtin-modules": "^3.3.0", "chokidar": "^4.0.3", "esbuild": "^0.25.0", "filter-obj": "^5.1.0", "jest": "^29.7.0", "moment": "^2.30.1", "obsidian": "^1.8.0", "obsidian-daily-notes-interface": "github:czottmann/obsidian-daily-notes-interface", "obsidian-dataview": "^0.5.68", "ts-jest": "^29.3.3", "tslib": "2.5.2", "typescript": "^5.0.4", "zod": "^3.23.8" }, "imports": { "#*": "./*.ts" }, "engines": { "node": ">=20.0.0" } } ================================================ FILE: src/constants.ts ================================================ export const URI_NAMESPACE = "actions-uri"; export const STRINGS = { append_done: "Note was appended", command_not_found: (command: string) => `Unknown command ${command}`, daily_note: { create_note_already_exists: "Daily note already exists", create_note_no_content: "Daily note couldn't be overwritten, no content specified", feature_not_available: "Daily Notes feature is not active", }, dataview_dql_must_start_with_list: 'DQL must start with "LIST"', dataview_dql_must_start_with_table: 'DQL must start with "TABLE"', dataview_plugin_not_available: "Dataview plugin is not active", delete_done: "Successfully deleted", faulty_apply_parameter: "Unexpected 'apply' parameter", faulty_note_targeting: "Either 'file', 'uid', or 'periodic-note' must be provided", file_not_found: "File couldn't be found", file_opened: "File opened", folder_created: "Folder created", global_search_feature_not_available: "Global Search plugin is not active", headline_not_found: "Headline not found", monthly_note: { create_note_already_exists: "Monthly note already exists", create_note_no_content: "Monthly note couldn't be overwritten, no content specified", feature_not_available: "Periodic Notes' Monthly feature is not active", }, not_available_on_mobile: "This action is not available on mobile", not_found: "Not found", note_not_found: "Note couldn't be found", note_opened: "Note opened", omnisearch_plugin_not_available: "Omnisearch plugin is not active", prepend_done: "Note was prepended", properties: { unable_to_update: "Unable to update properties", key_not_found: "Key not found", }, quarterly_note: { create_note_already_exists: "Quarterly note already exists", create_note_no_content: "Quarterly note couldn't be overwritten, no content specified", feature_not_available: "Periodic Notes' Quarterly feature is not active", }, rename_done: "Note was renamed/moved", replacement_done: "Replacement done, note updated", search_pattern_empty: "Search pattern is empty", search_pattern_invalid: "Search pattern must start with a forward slash", search_pattern_not_found: "Search pattern wasn't found, nothing replaced", search_pattern_unparseable: "Search pattern is not correctly formed", search_string_not_found: "Search string wasn't found, nothing replaced", template_not_found: "Template not found", templater: { feature_not_available: "Templater plugin is not active", }, templates: { feature_not_available: "Templates core plugin is not active", }, touch_done: "Successfully touched", trash_done: "Successfully moved to trash", unable_to_read_note: "Can't read note file", unable_to_write_note: "Can't write note file", vault_internals_not_found: "Vault didn't return config info", weekly_note: { create_note_already_exists: "Weekly note already exists", create_note_no_content: "Weekly note couldn't be overwritten, no content specified", feature_not_available: "Periodic Notes' Weekly feature is not active", }, yearly_note: { create_note_already_exists: "Yearly note already exists", create_note_no_content: "Yearly note couldn't be overwritten, no content specified", feature_not_available: "Periodic Notes' Yearly feature is not active", }, }; export const XCALLBACK_RESULT_PREFIX = "result"; ================================================ FILE: src/main.ts ================================================ import { normalizePath, ObsidianProtocolData, Plugin, TAbstractFile, } from "obsidian"; import { z, ZodError } from "zod"; import { STRINGS, URI_NAMESPACE } from "src/constants"; import { AnyParams, RoutePath, routes } from "src/routes"; import { SettingsTab } from "src/settings"; import { AnyHandlerResult, AnyHandlerSuccess, HandlerFailure, HandlerFileSuccess, HandlerFunction, PluginSettings, ProcessingResult, StringResultObject, } from "src/types"; import { sendUrlCallback } from "src/utils/callbacks"; import { self } from "src/utils/self"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { focusOrOpenNote, logErrorToConsole, logToConsole, showBrandedNotice, } from "src/utils/ui"; export default class ActionsURI extends Plugin { // @ts-ignore settings: PluginSettings; defaultSettings: PluginSettings = { frontmatterKey: "uid", }; async onload() { self(this); await this.loadSettings(); this.registerRoutes(routes); this.addSettingTab(new SettingsTab(this.app, this)); } async loadSettings() { this.settings = { ...this.defaultSettings, ...await this.loadData() }; } async saveSettings() { await this.saveData(this.settings); } /** * Takes a list of routes and registers them together with their handlers in * Obsidian. * * Each incoming call is first validated against the route's schema; if it * passes, its handler is called, then a `x-success`/`x-error` callback is * sent out (if necessary) and the processed note is opened in Obsidian (if * necessary). If the validation fails, an error message is shown to the * user. * * @param routeTree - A `RoutePath` object containing information about the * route tree */ private registerRoutes(routeTree: RoutePath) { const registeredRoutes: string[] = []; for (const [routePath, routeSubpaths] of Object.entries(routeTree)) { for (const route of routeSubpaths) { const { path, schema, handler } = route; const fullPath = normalizePath(`${URI_NAMESPACE}/${routePath}/${path}`) .replace(/\/$/, ""); this.registerObsidianProtocolHandler( fullPath, async (incomingParams) => { const res = await schema.safeParseAsync(incomingParams); res.success ? await this.handleIncomingCall( handler, res.data as z.infer, ) : this.handleParseError(res.error, incomingParams); }, ); registeredRoutes.push(fullPath); } } logToConsole("Registered URI handlers:", registeredRoutes); } /** * This function deals with valid incoming calls. It calls the responsible * handler and sends out a callback (if necessary) and opens the processed * note (if necessary). * * @param handlerFunc - A route handler function * @param params - Parameters from the incoming `x-callback-url` call after * being parsed & validated by Zod * * @returns A `ProcessingResult` object containing the incoming parameters, * results from the handler, the callback sending and the note opening */ private async handleIncomingCall( handlerFunc: HandlerFunction, params: AnyParams, ): Promise { let handlerResult: AnyHandlerResult; try { handlerResult = await handlerFunc.bind(this)(params); } catch (error) { const msg = `Handler error: ${( error).message}`; handlerResult = failure(ErrorCode.handlerError, msg); if (!params["hide-ui-notice-on-error"]) showBrandedNotice(msg); logErrorToConsole(msg); } const res = { params: this.prepParamsForConsole(params), handlerResult, sendCallbackResult: this.sendUrlCallbackIfNeeded(handlerResult, params), openResult: await this.openFileIfNeeded(handlerResult, params), }; logToConsole("Call handled:", res); return res; } /** * To prevent circular references and max call stack errors related to files, * we'll to convert all `TAbstractFile` instances to path strings which is what * they were in the original incoming call anyways. */ private prepParamsForConsole(params: AnyParams): AnyParams { const newParams: any = { ...params }; Object.keys(params).forEach((key) => { const value = ( params)[key]; newParams[key] = value instanceof TAbstractFile ? value.path : value; }); return newParams; } /** * When the parameters of an incoming `x-callback-url` call fail to parse or * validate, and thus can't be further processed, we have to inform the user, * conveying the error message. * * @param parseError - The error object returned from Zod's `.safeParse` * method * @param params - Parameters from the incoming `x-callback-url` call after * being parsed & validated by Zod */ private handleParseError(parseError: ZodError, params: ObsidianProtocolData) { const msg = [ "Incoming call failed", parseError.errors.map((e) => { // Some zod errors are too verbose, from them we strip everything but // the important part. const message = e.message.replace(/^.+(Expected )/g, "$1"); return e.path.length > 0 ? `- ${e.path.join(".")}: ${message}` : `- ${message}`; }), ] .flat() .join("\n"); logErrorToConsole(msg); if (!params["hide-ui-notice-on-error"]) showBrandedNotice(msg); if (!params["x-error"]) return; // If there's a "note not found" error, that's the biggest issue, we'll // return only that const error404 = parseError.errors .find((e) => e.message === STRINGS.note_not_found); if (error404) { sendUrlCallback( params["x-error"], failure(ErrorCode.notFound, `[Not found] ${error404.path.join(", ")}`), params, ); return; } const msg2 = "[Bad request] " + parseError.errors .map((e) => { return e.path.length > 0 ? `${e.path.join(", ")}: ${e.message}` : e.message; }) .join("; "); sendUrlCallback( params["x-error"], failure(ErrorCode.handlerError, msg2), params, ); } /** * Using the passed-in result object the method determines whether we should * or even can send a URL callback to the original sender. * * If the original call contained a non-empty `silent` parameter, we don't * send a callback. * * Otherwise, we trigger callback sending if … * - the result object contains a success and a `x-success` parameter * - the result object contains a failure and a `x-error` parameter * * @param handlerRes - A `*Result` object returned by a route handler * * @see {@link sendUrlCallback} */ private sendUrlCallbackIfNeeded( handlerRes: AnyHandlerResult, params: AnyParams, ): StringResultObject { if (handlerRes.isSuccess) { return params["x-success"] ? sendUrlCallback( params["x-success"], handlerRes, params, ) : success("No `x-error` callback URL provided"); } return params["x-error"] ? sendUrlCallback(params["x-error"], handlerRes, params) : success("No `x-error` callback URL provided"); } /** * @param handlerResult - Any handler result object * @param params - Parameters from the incoming `x-callback-url` call after * being parsed & validated by Zod * * @returns A successful `StringResultObject` object, the `result` prop * containing information on what was done. This function won't return a * failure. */ private async openFileIfNeeded( handlerResult: AnyHandlerResult, params: AnyParams, ): Promise { // Do we need to open anything in general? if (!handlerResult.isSuccess) { return success("No file to open, the handler failed"); } if (( params).silent) { return success("No file to open, the `silent` parameter was set"); } // Do we have information what to open? const { processedFilepath } = handlerResult; if (!processedFilepath) { return success( "No file to open, handler didn't return a `processedFilepath` property", ); } return await focusOrOpenNote(processedFilepath); } } ================================================ FILE: src/plugin-info.json ================================================ { "pluginVersion": "1.8.4", "pluginReleasedAt": "2025-11-18T12:48:42+0100" } ================================================ FILE: src/plugin-info.ts ================================================ /* File will be overwritten by bin/release.sh! */ export const PLUGIN_INFO = { "pluginVersion": "1.8.4", "pluginReleasedAt": "2025-11-18T12:48:42+0100" } ================================================ FILE: src/routes/command.ts ================================================ import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerCommandsExecutionSuccess, HandlerCommandsSuccess, HandlerFailure, RealLifePlugin, } from "src/types"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; import { pause } from "src/utils/time"; import { zodCommaSeparatedStrings } from "src/utils/zod"; // SCHEMATA ---------------------------------------- const listParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); const executeParams = incomingBaseParams.extend({ commands: zodCommaSeparatedStrings, "pause-in-secs": z.coerce.number().optional(), }); // TYPES ---------------------------------------- type ListParams = z.infer; type ExecuteParams = z.infer; export type AnyLocalParams = | ListParams | ExecuteParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/command": [ helloRoute(), { path: "/list", schema: listParams, handler: handleList }, { path: "/execute", schema: executeParams, handler: handleExecute, }, ], }; // HANDLERS ---------------------------------------- async function handleList( this: RealLifePlugin, params: ListParams, ): Promise { const commands = this.app.commands .listCommands() .map((cmd) => ({ id: cmd.id, name: cmd.name })); return success({ commands: JSON.stringify(commands) }); } async function handleExecute( this: RealLifePlugin, params: ExecuteParams, ): Promise { const { commands } = params; const pauseInMilliseconds = (params["pause-in-secs"] || 0.2) * 1000; for (let idx = 0; idx < commands.length; idx++) { const cmd = commands[idx]; const wasSuccess = this.app.commands.executeCommandById(cmd); // If this call wasn't successful, stop the sequence and return an error. if (!wasSuccess) { return failure(ErrorCode.notFound, STRINGS.command_not_found(cmd)); } // Unless this was the last command of the sequence, put in a short pause. if (idx < commands.length - 1) { await pause(pauseInMilliseconds); } } return success({}); } ================================================ FILE: src/routes/dataview.ts ================================================ import { DataviewApi, getAPI, isPluginEnabled as isDataviewEnabled, } from "obsidian-dataview"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerDataviewSuccess, HandlerFailure, RealLifePlugin, } from "src/types"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; // SCHEMATA ---------------------------------------- const readParams = incomingBaseParams.extend({ "dql": z.string(), "x-error": z.string().url(), "x-success": z.string().url(), }); // TYPES ---------------------------------------- type ReadParams = z.infer; export type AnyLocalParams = ReadParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/dataview": [ helloRoute(), { path: "/table-query", schema: readParams, handler: handleTableQuery }, { path: "/list-query", schema: readParams, handler: handleListQuery }, // { path: "/task-query", schema: readParams, handler: handleTaskQuery }, ], }; // HANDLERS ---------------------------------------- async function handleTableQuery( this: RealLifePlugin, params: ReadParams, ): Promise { return await executeDataviewQuery.bind(this)("table", params); } async function handleListQuery( this: RealLifePlugin, params: ReadParams, ): Promise { return await executeDataviewQuery.bind(this)("list", params); } // HELPERS ---------------------------------------- function dqlValuesMapper(dataview: DataviewApi, v: any): any { return Array.isArray(v) ? v.map((v1) => dqlValuesMapper(dataview, v1)) : dataview.value.toString(v); } async function executeDataviewQuery( this: RealLifePlugin, type: "table" | "list", params: ReadParams, ): Promise { const dataview = getAPI(this.app); if (!isDataviewEnabled(this.app) || !dataview) { return failure( ErrorCode.featureUnavailable, STRINGS.dataview_plugin_not_available, ); } const dql = params.dql.trim() + "\n"; if (!dql.toLowerCase().startsWith(type)) { return failure( ErrorCode.invalidInput, STRINGS[`dataview_dql_must_start_with_${type}`], ); } const res = await dataview.query(dql); if (!res.successful) { return failure(ErrorCode.unknownError, res.error); } // For some TABLE queries, DV will return a three-dimensional array instead of // a two-dimensional one. Not sure what's the cause but I'll need to account // for this. (https://github.com/czottmann/obsidian-actions-uri/issues/79) if (type === "table") { return (getArrayDimensions(res.value.values) > 2) ? success({ data: dqlValuesMapper(dataview, res.value.values[0]) }) : success({ data: dqlValuesMapper(dataview, res.value.values) }); } // For LIST queries, DV will return a two-dimensional array instead of a one- // dimensional one *if* one of the queried files returns more than one hit. // This is inconsistent, and AFO will nope out. So we'll need to make it // consistent before rendering out the result. // // Example: If you query for an inline field (`whatever::`), and one file // contains two of this field, e.g. `whatever:: something 1` and // `whatever:: something 2`, while another file contains just one // (e.g., `whatever:: something 3`), DV will return: // // [ // ["something 1", "something 2"], // "something 3" // ] if (type === "list") { res.value.values = res.value.values .map((v: any) => Array.isArray(v) ? v : [v]); return success({ data: dqlValuesMapper(dataview, res.value.values) .map((v: any) => v.join(", ")), }); } return failure(ErrorCode.invalidInput, "Neither LIST nor TABLE query"); } function getArrayDimensions(input: any[]) { if (!Array.isArray(input)) { return 0; } let dimensions = 1; input.forEach((item) => { if (Array.isArray(item)) { dimensions = Math.max(dimensions, getArrayDimensions(item) + 1); } }); return dimensions; } ================================================ FILE: src/routes/file.ts ================================================ import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerFilePathSuccess, HandlerPathsSuccess, HandlerTextSuccess, RealLifePlugin, } from "src/types"; import { getFile, renameFilepath, trashFilepath, } from "src/utils/file-handling"; import { helloRoute } from "src/utils/routing"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { zodExistingFilePath, zodOptionalBoolean, zodSanitizedFilePath, } from "src/utils/zod"; // SCHEMATA ---------------------------------------- const defaultParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); const openParams = incomingBaseParams.extend({ file: zodExistingFilePath, }); const deleteParams = incomingBaseParams.extend({ file: zodExistingFilePath, }); const renameParams = incomingBaseParams.extend({ file: zodExistingFilePath, "new-filename": zodSanitizedFilePath, silent: zodOptionalBoolean, }); // TYPES ---------------------------------------- type DefaultParams = z.infer; type OpenParams = z.infer; type DeleteParams = z.infer; type RenameParams = z.infer; export type AnyLocalParams = | DefaultParams | OpenParams | DeleteParams | RenameParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/file": [ helloRoute(), { path: "/list", schema: defaultParams, handler: handleList }, { path: "/get-active", schema: defaultParams, handler: handleGetActive }, { path: "/open", schema: openParams, handler: handleOpen }, { path: "/delete", schema: deleteParams, handler: handleDelete }, { path: "/trash", schema: deleteParams, handler: handleTrash }, { path: "/rename", schema: renameParams, handler: handleRename }, ], }; // HANDLERS ---------------------------------------- async function handleList( this: RealLifePlugin, params: DefaultParams, ): Promise { return success({ paths: this.app.vault.getFiles().map((t) => t.path).sort(), }); } async function handleGetActive( this: RealLifePlugin, params: DefaultParams, ): Promise { const res = this.app.workspace.getActiveFile(); return res ? success({ filepath: res.path }) : failure(ErrorCode.notFound, "No active file"); } async function handleOpen( params: OpenParams, ): Promise { const { file } = params; const res = await getFile(file.path); return res.isSuccess ? success({ message: STRINGS.file_opened }, file.path) : res; } async function handleDelete( params: DeleteParams, ): Promise { const { file } = params; const res = await trashFilepath(file.path, true); return res.isSuccess ? success({ message: res.result }, file.path) : res; } async function handleTrash( params: DeleteParams, ): Promise { const { file } = params; const res = await trashFilepath(file.path); return res.isSuccess ? success({ message: res.result }, file.path) : res; } async function handleRename( params: RenameParams, ): Promise { const { file } = params; const res = await renameFilepath(file.path, params["new-filename"]); return res.isSuccess ? success({ message: res.result }, file.path) : res; } ================================================ FILE: src/routes/folder.ts ================================================ import { TFolder } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerPathsSuccess, HandlerTextSuccess, } from "src/types"; import { createFolderIfNecessary, getFileMap, renameFilepath, trashFilepath, } from "src/utils/file-handling"; import { helloRoute } from "src/utils/routing"; import { zodExistingFolderPath, zodSanitizedFolderPath } from "src/utils/zod"; import { success } from "src/utils/results-handling"; // SCHEMATA ---------------------------------------- const listParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); const createParams = incomingBaseParams.extend({ folder: zodSanitizedFolderPath, }); const deleteParams = incomingBaseParams.extend({ folder: zodExistingFolderPath, }); const renameParams = incomingBaseParams.extend({ folder: zodExistingFolderPath, "new-foldername": zodSanitizedFolderPath, }); // TYPES ---------------------------------------- type ListParams = z.infer; type CreateParams = z.infer; type DeleteParams = z.infer; type RenameParams = z.infer; export type AnyLocalParams = | ListParams | CreateParams | DeleteParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/folder": [ helloRoute(), { path: "/list", schema: listParams, handler: handleList }, { path: "/create", schema: createParams, handler: handleCreate }, { path: "/rename", schema: renameParams, handler: handleRename }, { path: "/delete", schema: deleteParams, handler: handleDelete }, { path: "/trash", schema: deleteParams, handler: handleTrash }, ], }; // HANDLERS ---------------------------------------- async function handleList( params: ListParams, ): Promise { return success({ paths: getFileMap() .filter((t) => t instanceof TFolder) .map((t) => t.path.endsWith("/") ? t.path : `${t.path}/`).sort(), }); } async function handleCreate( params: CreateParams, ): Promise { const { folder } = params; await createFolderIfNecessary(folder); return success({ message: STRINGS.folder_created }, folder); } async function handleRename( params: RenameParams, ): Promise { const { folder } = params; const res = await renameFilepath(folder.path, params["new-foldername"]); return res.isSuccess ? success({ message: res.result }, folder.path) : res; } async function handleDelete( params: DeleteParams, ): Promise { const { folder } = params; const res = await trashFilepath(folder.path, true); return res.isSuccess ? success({ message: res.result }, folder.path) : res; } async function handleTrash( params: DeleteParams, ): Promise { const { folder } = params; const res = await trashFilepath(folder.path); return res.isSuccess ? success({ message: res.result }, folder.path) : res; } ================================================ FILE: src/routes/info.ts ================================================ import { apiVersion, Platform } from "obsidian"; import { z } from "zod"; import { PLUGIN_INFO } from "src/plugin-info"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerInfoSuccess } from "src/types"; import { success } from "src/utils/results-handling"; // SCHEMATA -------------------- const defaultParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); type DefaultParams = z.infer; export type AnyLocalParams = DefaultParams; // ROUTES -------------------- export const routePath: RoutePath = { "/info": [ { path: "/", schema: defaultParams, handler: handleInfo }, ], }; // HANDLERS -------------------- async function handleInfo( params: DefaultParams, ): Promise { const uaMatch = navigator.userAgent.match(/\((.+?)\)/); const os: string = uaMatch ? uaMatch[1] : "unknown"; const { isAndroidApp, isDesktopApp, isIosApp, isMacOS } = Platform; let platform = ""; if (isDesktopApp && isMacOS) { platform = "macOS"; } else if (isDesktopApp) { platform = "Windows/Linux"; } else if (isIosApp) { platform = "iOS"; } else if (isAndroidApp) { platform = "Android"; } return success({ ...PLUGIN_INFO, apiVersion, nodeVersion: window.process?.version?.replace(/^v/, "") || "N/A", platform, os, }); } ================================================ FILE: src/routes/note/create.ts ================================================ import { TAbstractFile } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerFileSuccess, Prettify, RealLifePlugin, TFileResultObject, } from "src/types"; import { applyCorePluginTemplate, createNote, createOrOverwriteNote, getNote, getNoteDetails, sanitizeFilePath, trashFilepath, } from "src/utils/file-handling"; import { createPeriodicNote, PeriodicNoteType, } from "src/utils/periodic-notes-handling"; import { getEnabledCommunityPlugin, getEnabledCorePlugin, } from "src/utils/plugins"; import { mergeResolvedData, ResolvedNoteTargetingValues, resolveNoteTargeting, } from "src/utils/parameters"; import { ErrorCode, failure } from "src/utils/results-handling"; import { self } from "src/utils/self"; import { focusOrOpenNote } from "src/utils/ui"; import { zodOptionalBoolean } from "src/utils/zod"; // TYPES ---------------------------------------- export enum CreateApplyParameterValue { Content = "content", Templater = "templater", Templates = "templates", } enum IfExistsParameterValue { Default = "", Overwrite = "overwrite", Skip = "skip", } // SCHEMAS ---------------------------------------- const optionalIfExists = z.nativeEnum(IfExistsParameterValue).optional(); const createNoteApplyContentParams = incomingBaseParams .extend({ file: z.string(), // This sets the default value for `apply` to `content`. The default fallback // only works when the `apply` is missing from the input; if it's there but // empty, the default won't be applied, and the route will return an error. apply: z.literal(CreateApplyParameterValue.Content) .optional() .default(CreateApplyParameterValue.Content), content: z.string().optional(), "if-exists": optionalIfExists, silent: zodOptionalBoolean, }); const createNoteApplyTemplateParams = incomingBaseParams .extend({ file: z.string(), apply: z.enum([ CreateApplyParameterValue.Templater, CreateApplyParameterValue.Templates, ]), "template-file": z.string(), "if-exists": optionalIfExists, silent: zodOptionalBoolean, }) .transform(resolveTemplatePathStrict); const createPeriodicNoteParams = incomingBaseParams .extend({ "periodic-note": z.nativeEnum(PeriodicNoteType), "if-exists": optionalIfExists, silent: zodOptionalBoolean, }); export const createParams = z.union([ createNoteApplyContentParams, createNoteApplyTemplateParams, createPeriodicNoteParams, ]) .transform(resolveNoteTargeting); // TYPES ---------------------------------------- export type CreateParams = Prettify>; export type CreateNoteApplyContentParams = Prettify< & z.infer & ResolvedNoteTargetingValues >; export type CreateNoteApplyTemplateParams = Prettify< & z.infer & ResolvedNoteTargetingValues & ResolvedTemplatePathValues >; export type AnyCreateNoteApplyParams = | CreateNoteApplyContentParams | CreateNoteApplyTemplateParams; export type CreatePeriodicNoteParams = Prettify< & z.infer & ResolvedNoteTargetingValues >; // HANDLERS ---------------------------------------- export async function _handleCreatePeriodicNote( this: RealLifePlugin, params: CreatePeriodicNoteParams, ): Promise { const { _resolved: { inputPath }, ["if-exists"]: ifExists, ["periodic-note"]: periodicNoteType, silent, } = params; const shouldFocusNote = !silent; // If there already is a note with that name or at that path, deal with it. const resNoteExists = await getNote(inputPath); if (resNoteExists.isSuccess) { switch (ifExists) { // `skip` == Leave not as-is, we just return the existing note. case IfExistsParameterValue.Skip: if (shouldFocusNote) await focusOrOpenNote(inputPath); return await getNoteDetails(inputPath); // Overwrite the existing note. case IfExistsParameterValue.Overwrite: // Delete existing note, but keep going afterwards. await trashFilepath(inputPath); break; default: return failure( ErrorCode.noteAlreadyExists, STRINGS[`${periodicNoteType}_note`].create_note_already_exists, ); } } const newNote = await createPeriodicNote(periodicNoteType); if (!newNote) { return failure( ErrorCode.unableToCreateNote, STRINGS.unable_to_write_note, ); } if (shouldFocusNote) await focusOrOpenNote(inputPath); return await getNoteDetails(inputPath); } export async function _handleCreateNoteFromContent( this: RealLifePlugin, params: CreateNoteApplyContentParams, ): Promise { const { _resolved: { inputPath }, ["if-exists"]: ifExists, content, silent, } = params; const shouldFocusNote = !silent; // If there already is a note with that name or at that path, deal with it. let resCreate: TFileResultObject | undefined; const resNoteExists = await getNote(inputPath); if (resNoteExists.isSuccess) { switch (ifExists) { // `skip` == Leave not as-is, we just return the existing note. case IfExistsParameterValue.Skip: if (shouldFocusNote) await focusOrOpenNote(inputPath); return await getNoteDetails(inputPath); case IfExistsParameterValue.Overwrite: resCreate = await createOrOverwriteNote(inputPath, ""); break; // Overwrite with suffix default: resCreate = await createNote(inputPath, ""); break; } } // // The note doesn't exist yet, so we create it. else { resCreate = await createNote(inputPath, ""); } if (!resCreate?.isSuccess) return resCreate!; const newNote = resCreate.result; await this.app.vault.modify(newNote, content || ""); if (shouldFocusNote) await focusOrOpenNote(newNote.path); return await getNoteDetails(newNote.path); } export async function _handleCreateNoteFromTemplate( this: RealLifePlugin, params: CreateNoteApplyTemplateParams, ): Promise { const { _resolved: { inputPath, templateFile }, ["if-exists"]: ifExists, apply, silent, } = params; const shouldFocusNote = !silent; // If there already is a note with that name or at that path, deal with it. let resCreate: TFileResultObject | undefined; const resNoteExists = await getNote(inputPath); if (resNoteExists.isSuccess) { switch (ifExists) { // `skip` == Leave not as-is, we just return the existing note. case IfExistsParameterValue.Skip: if (shouldFocusNote) await focusOrOpenNote(inputPath); return await getNoteDetails(inputPath); case IfExistsParameterValue.Overwrite: resCreate = await createOrOverwriteNote(inputPath, ""); break; // Overwrite with suffix default: resCreate = await createNote(inputPath, ""); break; } } // // The note doesn't exist yet, so we create it. else { resCreate = await createNote(inputPath, ""); } if (!resCreate?.isSuccess) return resCreate!; const newNote = resCreate.result; // We need to check if the relevant plugin is available, and if not, we return // from here. Testing for existence of template file is done by a zod transform, // so we can be sure the file exists. switch (apply) { case CreateApplyParameterValue.Templater: const resPlugin1 = getEnabledCommunityPlugin("templater-obsidian"); if (!resPlugin1.isSuccess) return resPlugin1; await resPlugin1.result.templater .write_template_to_file(templateFile!, newNote); break; case CreateApplyParameterValue.Templates: const resPlugin2 = getEnabledCorePlugin("templates"); if (!resPlugin2.isSuccess) return resPlugin2; await applyCorePluginTemplate(templateFile!, newNote); break; } if (shouldFocusNote) await focusOrOpenNote(newNote.path); return await getNoteDetails(newNote.path); } // RESOLVERS ---------------------------------------- type ResolvedTemplatePathValues = Readonly<{ _resolved: { templatePath: string; templateFile: TAbstractFile | undefined; }; }>; /** * Validates the `template-file` parameter of a note and adds computed values to * the input object (under the `_resolved` key). * * This function resolves the input parameter into a `TAbstractFile` instance. * The file is looked up in the templates folder specified in the Templates or * Templater settings, depending on the `apply` parameter. If the file is not * found, a Zod validation error is triggered. * * @param data - The input data containing the `template-file` key. * @param ctx - The Zod refinement context used for adding validation issues. * @returns The input object augmented with computed values if validation * succeeds; otherwise, it triggers a Zod validation error. * @throws {ZodError} When more than one or none of the targeting parameters are provided. * * @template T - The type of the input data. */ export function resolveTemplatePathStrict( data: T, ctx: z.RefinementCtx, ): T & ResolvedTemplatePathValues { const { "template-file": input, apply } = data as unknown as CreateNoteApplyTemplateParams; let folder = ""; switch (apply) { case CreateApplyParameterValue.Templater: const resTemplater = getEnabledCommunityPlugin("templater-obsidian"); if (!resTemplater.isSuccess) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.templater.feature_not_available, }); return z.NEVER; } folder = resTemplater.result.settings?.templates_folder || ""; break; case CreateApplyParameterValue.Templates: const resTemplates = getEnabledCorePlugin("templates"); if (!resTemplates.isSuccess) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.templates.feature_not_available, }); return z.NEVER; } folder = resTemplates.result.options?.folder || ""; break; default: ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.faulty_apply_parameter, }); return z.NEVER; } // Check if the file exists in the specified folder. We try two paths: the // input prefixed with the template folder as configured in the relevant plugin, // and the input as is (in case the input already is a full path). const { vault } = self().app; const templateFile = vault.getFileByPath(sanitizeFilePath(`${folder}/${input}`)) || vault.getFileByPath(sanitizeFilePath(input)); if (!templateFile) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.template_not_found, }); return z.NEVER; } return mergeResolvedData(data, { templatePath: templateFile.path, templateFile, }); } ================================================ FILE: src/routes/note-properties.ts ================================================ import { stringifyYaml } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { incomingBaseParams, noteTargetingParams } from "src/schemata"; import { HandlerFailure, HandlerFileSuccess, HandlerPropertiesSuccess, Prettify, } from "src/types"; import { getNote, getNoteDetails, propertiesForFile, updateNote, } from "src/utils/file-handling"; import { resolveNoteTargetingStrict } from "src/utils/parameters"; import { helloRoute } from "src/utils/routing"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { self } from "src/utils/self"; import { zodJsonPropertiesObject, zodJsonStringArray, zodOptionalBoolean, } from "src/utils/zod"; // SCHEMATA ---------------------------------------- const getParams = incomingBaseParams .merge(noteTargetingParams) .extend({ silent: zodOptionalBoolean, "x-error": z.string().url(), "x-success": z.string().url(), }) .transform(resolveNoteTargetingStrict); const setParams = incomingBaseParams .merge(noteTargetingParams) .extend({ properties: zodJsonPropertiesObject, mode: z.enum(["overwrite", "update"]).optional(), }) .transform(resolveNoteTargetingStrict); const removeKeysParams = incomingBaseParams .merge(noteTargetingParams) .extend({ keys: zodJsonStringArray, }) .transform(resolveNoteTargetingStrict); // TYPES ---------------------------------------- type GetParams = Prettify>; type SetParams = Prettify>; type RemoveKeysParams = Prettify>; export type AnyLocalParams = | GetParams | SetParams | RemoveKeysParams; // ROUTES -------------------- export const routePath: RoutePath = { "/note-properties": [ helloRoute(), { path: "/get", schema: getParams, handler: handleGet }, { path: "/set", schema: setParams, handler: handleSet }, { path: "/clear", schema: getParams, handler: handleClear }, { path: "/remove-keys", schema: removeKeysParams, handler: handleRemoveKeys, }, ], }; // HANDLERS -------------------- async function handleGet( params: GetParams, ): Promise { const { _resolved: { inputFile } } = params; return success( { properties: await propertiesForFile(inputFile!) }, inputFile?.path, ); } async function handleSet( params: SetParams, ): Promise { const { _resolved: { inputFile }, mode, properties } = params; const { path } = inputFile!; if (mode === "update") { const resNote = await getNote(path); if (!resNote.isSuccess) { return resNote; } try { self().app.fileManager.processFrontMatter( resNote.result, (frontmatter) => Object.assign(frontmatter, properties), ); return getNoteDetails(path); } catch (err) { return failure( ErrorCode.unableToWrite, STRINGS.properties.unable_to_update, ); } } else { return updateNote(path, sanitizedStringifyYaml(properties)); } } async function handleClear( params: GetParams, ): Promise { const { _resolved: { inputPath: path } } = params; return updateNote(path, ""); } async function handleRemoveKeys( params: RemoveKeysParams, ): Promise { const { _resolved: { inputPath: path, inputFile }, keys } = params; const props = await propertiesForFile(inputFile!)!; ( keys).forEach((key) => delete props[key]); return updateNote(path, sanitizedStringifyYaml(props)); } function sanitizedStringifyYaml(props: any): string { return Object.keys(props).length > 0 ? stringifyYaml(props).trim() : ""; } ================================================ FILE: src/routes/note.ts ================================================ import { MarkdownView, TFile } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { NoteTargetingParameterKey, RoutePath } from "src/routes"; import { _handleCreateNoteFromContent, _handleCreateNoteFromTemplate, _handleCreatePeriodicNote, AnyCreateNoteApplyParams, CreateApplyParameterValue, CreateNoteApplyContentParams, CreateNoteApplyTemplateParams, CreateParams, createParams, CreatePeriodicNoteParams, } from "src/routes/note/create"; import { incomingBaseParams, noteTargetingParams, noteTargetingWithRecentsParams, } from "src/schemata"; import { HandlerFailure, HandlerFileSuccess, HandlerPathsSuccess, HandlerTextSuccess, Prettify, RealLifePlugin, StringResultObject, } from "src/types"; import { appendNote, appendNoteBelowHeadline, createNote, getNote, getNoteContent, getNoteDetails, prependNote, prependNoteBelowHeadline, renameFilepath, sanitizeFilePath, searchAndReplaceInNote, touchNote, trashFilepath, } from "src/utils/file-handling"; import { resolveNoteTargeting, resolveNoteTargetingStrict, } from "src/utils/parameters"; import { checkForEnabledPeriodicNoteFeature, createPeriodicNote, getAllPeriodicNotes, PeriodicNoteType, } from "src/utils/periodic-notes-handling"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; import { escapeRegExpChars, parseStringIntoRegex, } from "src/utils/string-handling"; import { focusOrOpenNote } from "src/utils/ui"; import { zodOptionalBoolean, zodSanitizedNotePath } from "src/utils/zod"; // SCHEMATA ---------------------------------------- enum IfHeadlineMissingParameterValue { Error = "error", AddHeadline = "add-headline", Skip = "skip", } const listParams = incomingBaseParams .extend({ "periodic-note": z.nativeEnum(PeriodicNoteType).optional(), "x-error": z.string().url(), "x-success": z.string().url(), }); const getParams = incomingBaseParams .merge(noteTargetingWithRecentsParams) .extend({ silent: zodOptionalBoolean, "x-error": z.string().url(), "x-success": z.string().url(), }) .transform(resolveNoteTargetingStrict); const getActiveParams = incomingBaseParams .extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); const readNamedParams = incomingBaseParams .extend({ file: zodSanitizedNotePath, "sort-by": z.enum([ "best-guess", "path-asc", "path-desc", "ctime-asc", "ctime-desc", "mtime-asc", "mtime-desc", "", ]).optional(), "x-error": z.string().url(), "x-success": z.string().url(), }); const openParams = incomingBaseParams .merge(noteTargetingWithRecentsParams) .transform(resolveNoteTargetingStrict); const appendParams = incomingBaseParams .merge(noteTargetingParams) .extend({ content: z.string(), silent: zodOptionalBoolean, "below-headline": z.string().optional(), "create-if-not-found": zodOptionalBoolean, "ensure-newline": zodOptionalBoolean, "if-headline-missing": z.nativeEnum(IfHeadlineMissingParameterValue) .optional() .default(IfHeadlineMissingParameterValue.Error), }) .transform(resolveNoteTargeting); const prependParams = incomingBaseParams .merge(noteTargetingParams) .extend({ content: z.string(), silent: zodOptionalBoolean, "below-headline": z.string().optional(), "create-if-not-found": zodOptionalBoolean, "ensure-headline": zodOptionalBoolean, "ensure-newline": zodOptionalBoolean, "if-headline-missing": z.nativeEnum(IfHeadlineMissingParameterValue) .optional() .default(IfHeadlineMissingParameterValue.Error), "ignore-front-matter": zodOptionalBoolean, }) .transform(resolveNoteTargeting); const touchParams = incomingBaseParams .merge(noteTargetingParams) .extend({ silent: zodOptionalBoolean, }) .transform(resolveNoteTargetingStrict); const searchAndReplaceParams = incomingBaseParams .merge(noteTargetingParams) .extend({ silent: zodOptionalBoolean, search: z.string().min(1, { message: "can't be empty" }), replace: z.string(), }) .transform(resolveNoteTargetingStrict); const deleteParams = incomingBaseParams .merge(noteTargetingParams) .transform(resolveNoteTargetingStrict); const renameParams = incomingBaseParams .merge(noteTargetingParams) .extend({ "new-filename": zodSanitizedNotePath, silent: zodOptionalBoolean, }) .transform(resolveNoteTargetingStrict); // TYPES ---------------------------------------- type ListParams = Prettify>; type GetParams = Prettify>; type GetActiveParams = Prettify>; type ReadFirstNamedParams = Prettify>; type OpenParams = Prettify>; type AppendParams = Prettify>; type PrependParams = Prettify>; type TouchParams = Prettify>; type SearchAndReplaceParams = Prettify>; type DeleteParams = Prettify>; type RenameParams = Prettify>; export type AnyLocalParams = | ListParams | GetParams | GetActiveParams | ReadFirstNamedParams | OpenParams | CreateParams | AppendParams | PrependParams | TouchParams | SearchAndReplaceParams | DeleteParams | RenameParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/note": [ helloRoute(), { path: "/list", schema: listParams, handler: handleList }, { path: "/get", schema: getParams, handler: handleGet }, { path: "/get-first-named", schema: readNamedParams, handler: handleGetNamed, }, { path: "/get-active", schema: getActiveParams, handler: handleGetActive, }, { path: "/open", schema: openParams, handler: handleOpen }, { path: "/create", schema: createParams, handler: handleCreate }, { path: "/append", schema: appendParams, handler: handleAppend }, { path: "/prepend", schema: prependParams, handler: handlePrepend }, { path: "/touch", schema: touchParams, handler: handleTouch }, { path: "/delete", schema: deleteParams, handler: handleDelete }, { path: "/trash", schema: deleteParams, handler: handleTrash }, { path: "/rename", schema: renameParams, handler: handleRename }, { path: "/search-string-and-replace", schema: searchAndReplaceParams, handler: handleSearchStringAndReplace, }, { path: "/search-regex-and-replace", schema: searchAndReplaceParams, handler: handleSearchRegexAndReplace, }, ], }; // HANDLERS ---------------------------------------- async function handleList( this: RealLifePlugin, params: ListParams, ): Promise { const { "periodic-note": periodicNoteType } = params; // If no periodic note type is specified, we return all notes. if (!periodicNoteType) { return success({ paths: this.app.vault.getMarkdownFiles().map((t) => t.path).sort(), }); } // If a periodic note type is specified, we return all notes of that type. if (!checkForEnabledPeriodicNoteFeature(periodicNoteType)) { return failure( ErrorCode.featureUnavailable, STRINGS[`${periodicNoteType}_note`].feature_not_available, ); } const notes = getAllPeriodicNotes(periodicNoteType); return success({ paths: Object.keys(notes).sort().reverse().map((k) => notes[k].path), }); } /** * Handler for `/note/get`. Existence of note is checked by the schema, i.e. the * handler won't be called if the file doesn't exist. */ async function handleGet( params: GetParams, ): Promise { const { _resolved: { inputPath }, silent } = params; const res = await getNoteDetails(inputPath); if (res.isSuccess && !silent) await focusOrOpenNote(inputPath); return res; } async function handleGetActive( this: RealLifePlugin, params: GetActiveParams, ): Promise { const res = this.app.workspace.getActiveFile(); if (res?.extension !== "md") { return failure(ErrorCode.notFound, "No active note"); } const res1 = await getNoteDetails(res.path); if (!res1.isSuccess) { return failure(ErrorCode.notFound, "No active note"); } const mdView = this.app.workspace.getActiveViewOfType(MarkdownView); const selection = mdView ? (mdView.currentMode as any).getSelection() : undefined; return selection ? success({ ...res1.result, selection }) : res1; } async function handleGetNamed( this: RealLifePlugin, params: ReadFirstNamedParams, ): Promise { const { file } = params; const sortBy = params["sort-by"] || "best-guess"; // "Best guess" means utilizing Obsidian's internal link resolution to find // the right note. If it's not found, we return a 404. if (sortBy === "best-guess") { const res = this.app.metadataCache .getFirstLinkpathDest(sanitizeFilePath(file), "/"); return res ? await getNoteDetails(res.path) : failure(ErrorCode.notFound, "No note found with that name"); } // If we're here, we're sorting by something else. We need to find all notes // with that name, sort them as requested, and return the first one. const sortFns = { "path-asc": (a: TFile, b: TFile) => a.path.localeCompare(b.path), "path-desc": (a: TFile, b: TFile) => b.path.localeCompare(a.path), "ctime-asc": (a: TFile, b: TFile) => a.stat.ctime - b.stat.ctime, "ctime-desc": (a: TFile, b: TFile) => b.stat.ctime - a.stat.ctime, "mtime-asc": (a: TFile, b: TFile) => a.stat.mtime - b.stat.mtime, "mtime-desc": (a: TFile, b: TFile) => b.stat.mtime - a.stat.mtime, }; const res = this.app.vault.getMarkdownFiles() .sort(sortFns[sortBy]) .find((tf) => tf.name === file); if (!res) return failure(ErrorCode.notFound, "No note found with that name"); return await getNoteDetails(res.path); } /** * Handler for `/note/open`. Existence of note is checked by the schema, i.e. the * handler won't be called if the file doesn't exist. */ async function handleOpen( params: OpenParams, ): Promise { const { _resolved: { inputPath } } = params; const res = await getNote(inputPath); return res.isSuccess ? success({ message: STRINGS.note_opened }, res.result.path) : res; } async function handleCreate( this: RealLifePlugin, params: CreateParams, ): Promise { const { _resolved: { inputKey } } = params; if (inputKey === NoteTargetingParameterKey.PeriodicNote) { return _handleCreatePeriodicNote .bind(this)(params as CreatePeriodicNoteParams); } const applyValue = (params as AnyCreateNoteApplyParams).apply; switch (applyValue) { case CreateApplyParameterValue.Content: return _handleCreateNoteFromContent .bind(this)(params as CreateNoteApplyContentParams); case CreateApplyParameterValue.Templater: case CreateApplyParameterValue.Templates: return _handleCreateNoteFromTemplate .bind(this)(params as CreateNoteApplyTemplateParams); } } async function handleAppend( this: RealLifePlugin, params: AppendParams, ): Promise { const { _resolved: { inputKey, inputFile, inputPath }, ["below-headline"]: belowHeadline, ["create-if-not-found"]: shouldCreateNote, ["ensure-newline"]: shouldEnsureNewline, ["if-headline-missing"]: ifHeadlineMissing, ["periodic-note"]: periodicNoteType, content, silent, uid, } = params; // If the note was requested via UID, doesn't exist but should be created, // we'll use the UID as path. Otherwise, we'll use the resolved path as it was // passed in. const path = ( !inputFile && inputKey === NoteTargetingParameterKey.UID && shouldCreateNote ) ? uid! : inputPath; async function appendAsRequested() { if (belowHeadline) { const resHeadline = await prepareNoteForHeadlineBlockManipulation( path, belowHeadline, ifHeadlineMissing, ); if (!resHeadline.isSuccess) { return resHeadline; } return await appendNoteBelowHeadline(path, belowHeadline, content); } return await appendNote(path, content, shouldEnsureNewline); } // If the file doesn't exist … if (!inputFile) { // … check if we're supposed to create it. If not, back off. if (!shouldCreateNote) { return failure(ErrorCode.notFound, STRINGS.note_not_found); } // We're supposed to create the note! // The requested note is a periodic note. if (inputKey === NoteTargetingParameterKey.PeriodicNote) { const newNote = await createPeriodicNote(periodicNoteType!); if (!newNote) { return failure( ErrorCode.unableToCreateNote, STRINGS.unable_to_write_note, ); } } // // The requested note is not a periodic note. else { const resCreate = await createNote(path, ""); if (!resCreate.isSuccess) return resCreate; // If the note was requested via UID, we need to set the UID in the front // matter of the newly created note. if (inputKey === NoteTargetingParameterKey.UID) { await this.app.fileManager.processFrontMatter( resCreate.result, (fm) => fm[this.settings.frontmatterKey] = uid!, ); } } } // Manipulate the file. const resAppend = await appendAsRequested(); if (resAppend.isSuccess) { if (!silent) await focusOrOpenNote(path); return success({ message: resAppend.result }, path); } return resAppend; } async function handlePrepend( this: RealLifePlugin, params: PrependParams, ): Promise { const { _resolved: { inputKey, inputFile, inputPath }, ["below-headline"]: belowHeadline, ["create-if-not-found"]: shouldCreateNote, ["ensure-newline"]: shouldEnsureNewline, ["ignore-front-matter"]: shouldIgnoreFrontMatter, ["if-headline-missing"]: ifHeadlineMissing, ["periodic-note"]: periodicNoteType, content, silent, uid, } = params; // If the note was requested via UID, doesn't exist but should be created, // we'll use the UID as path. Otherwise, we'll use the resolved path as it was // passed in. const path = ( !inputFile && inputKey === NoteTargetingParameterKey.UID && shouldCreateNote ) ? uid! : inputPath; async function prependAsRequested() { if (belowHeadline) { if (belowHeadline) { const resHeadline = await prepareNoteForHeadlineBlockManipulation( path, belowHeadline, ifHeadlineMissing, ); if (!resHeadline.isSuccess) { return resHeadline; } return await prependNoteBelowHeadline( path, belowHeadline, content, shouldEnsureNewline, ); } } return await prependNote( path, content, shouldEnsureNewline, shouldIgnoreFrontMatter, ); } // If the file doesn't exist … if (!inputFile) { // … check if we're supposed to create it. If not, back off. if (!shouldCreateNote) { return failure(ErrorCode.notFound, STRINGS.note_not_found); } // We're supposed to create the note! // The requested note is a periodic note. if (inputKey === NoteTargetingParameterKey.PeriodicNote) { const newNote = await createPeriodicNote(periodicNoteType!); if (!newNote) { return failure( ErrorCode.unableToCreateNote, STRINGS.unable_to_write_note, ); } } // // The requested note is not a periodic note. else { const resCreate = await createNote(path, ""); if (!resCreate.isSuccess) return resCreate; // If the note was requested via UID, we need to set the UID in the front // matter of the newly created note. if (inputKey === NoteTargetingParameterKey.UID) { await this.app.fileManager.processFrontMatter( resCreate.result, (fm) => fm[this.settings.frontmatterKey] = uid!, ); } } } // Manipulate the file. const resPrepend = await prependAsRequested(); if (resPrepend.isSuccess) { if (!silent) await focusOrOpenNote(path); return success({ message: resPrepend.result }, path); } return resPrepend; } async function handleTouch( params: TouchParams, ): Promise { const { _resolved: { inputPath }, silent } = params; const res = await touchNote(inputPath); if (!res.isSuccess) return res; if (!silent) await focusOrOpenNote(inputPath); return success({ message: STRINGS.touch_done }, inputPath); } async function handleSearchStringAndReplace( params: SearchAndReplaceParams, ): Promise { const { _resolved: { inputPath }, search, replace, silent } = params; const res = await searchAndReplaceInNote(inputPath, search, replace); if (!res.isSuccess) return res; if (!silent) await focusOrOpenNote(inputPath); return success({ message: res.result }, inputPath); } async function handleSearchRegexAndReplace( params: SearchAndReplaceParams, ): Promise { const { _resolved: { inputPath }, search, replace, silent } = params; const resSir = parseStringIntoRegex(search); if (!resSir.isSuccess) return resSir; const res = await searchAndReplaceInNote(inputPath, resSir.result, replace); if (!res.isSuccess) return res; if (!silent) await focusOrOpenNote(inputPath); return success({ message: res.result }, inputPath); } async function handleDelete( params: DeleteParams, ): Promise { const { _resolved: { inputPath } } = params; const res = await trashFilepath(inputPath, true); return res.isSuccess ? success({ message: res.result }, inputPath) : res; } async function handleTrash( params: DeleteParams, ): Promise { const { _resolved: { inputPath } } = params; const res = await trashFilepath(inputPath); return res.isSuccess ? success({ message: res.result }, inputPath) : res; } async function handleRename( params: RenameParams, ): Promise { const { _resolved: { inputPath }, ["new-filename"]: newPath, } = params; const res = await renameFilepath(inputPath, newPath); return res.isSuccess ? success({ message: res.result }, inputPath) : res; } // HELPERS ---------------------------------------- async function prepareNoteForHeadlineBlockManipulation( path: string, headline: string, ifHeadlineMissing: IfHeadlineMissingParameterValue, ): Promise { // Test if the headline exists in the note. const resNote = await getNoteContent(path); if (!resNote.isSuccess) return resNote; const trimmedHeadline = headline.trim(); const headlineRegex = new RegExp( `^${escapeRegExpChars(trimmedHeadline)}\\s*$`, "m", ); if (headlineRegex.test(resNote.result)) { return success("Note contains headline"); } // The note doesn't contain the headline! switch (ifHeadlineMissing) { case IfHeadlineMissingParameterValue.AddHeadline: return await appendNote(path, `\n${trimmedHeadline}`, true); case IfHeadlineMissingParameterValue.Skip: return success(STRINGS.headline_not_found); case IfHeadlineMissingParameterValue.Error: return failure(ErrorCode.notFound, STRINGS.headline_not_found); } } ================================================ FILE: src/routes/omnisearch.ts ================================================ import { z } from "zod"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerSearchSuccess, HandlerTextSuccess, RealLifePlugin, } from "src/types"; import { success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; import { doOmnisearch } from "src/utils/search"; // SCHEMATA -------------------- const defaultParams = incomingBaseParams.extend({ query: z.string().min(1, { message: "can't be empty" }), "x-error": z.string().url(), "x-success": z.string().url(), }); const openParams = incomingBaseParams.extend({ query: z.string().min(1, { message: "can't be empty" }), }); // TYPES ---------------------------------------- type DefaultParams = z.infer; type OpenParams = z.infer; export type AnyLocalParams = | DefaultParams | OpenParams; // ROUTES -------------------- export const routePath: RoutePath = { "/omnisearch": [ helloRoute(), { path: "/all-notes", schema: defaultParams, handler: handleSearch }, { path: "/open", schema: openParams, handler: handleOpen }, ], }; // HANDLERS -------------------- async function handleSearch( params: DefaultParams, ): Promise { const res = await doOmnisearch(params.query); return res.isSuccess ? success(res.result) : res; } async function handleOpen( this: RealLifePlugin, params: DefaultParams, ): Promise { // Let's open the search in the simplest way possible. window.open( "obsidian://omnisearch?" + "vault=" + encodeURIComponent(this.app.vault.getName()) + "&query=" + encodeURIComponent(params.query.trim()), ); return success({ message: "Opened search" }); } ================================================ FILE: src/routes/root.ts ================================================ import { RoutePath } from "src/routes"; import { helloRoute } from "src/utils/routing"; // ROUTES -------------------- export const routePath: RoutePath = { "/": [ helloRoute(), ], }; ================================================ FILE: src/routes/search.ts ================================================ import { z } from "zod"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerSearchSuccess, HandlerTextSuccess, RealLifePlugin, } from "src/types"; import { success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; import { doSearch } from "src/utils/search"; // SCHEMATA -------------------- const defaultParams = incomingBaseParams.extend({ query: z.string().min(1, { message: "can't be empty" }), "x-error": z.string().url(), "x-success": z.string().url(), }); const openParams = incomingBaseParams.extend({ query: z.string().min(1, { message: "can't be empty" }), }); // TYPES ---------------------------------------- type DefaultParams = z.infer; type OpenParams = z.infer; export type AnyLocalParams = | DefaultParams | OpenParams; // ROUTES -------------------- export const routePath: RoutePath = { "/search": [ helloRoute(), { path: "/all-notes", schema: defaultParams, handler: handleSearch }, { path: "/open", schema: openParams, handler: handleOpen }, ], }; // HANDLERS -------------------- async function handleSearch( params: DefaultParams, ): Promise { const res = await doSearch(params.query); return res.isSuccess ? success(res.result) : res; } async function handleOpen( this: RealLifePlugin, params: DefaultParams, ): Promise { // Let's open the search in the simplest way possible. window.open( "obsidian://search?" + "vault=" + encodeURIComponent(this.app.vault.getName()) + "&query=" + encodeURIComponent(params.query.trim()), ); return success({ message: "Opened search" }); } ================================================ FILE: src/routes/settings.ts ================================================ import { z } from "zod"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerTextSuccess, RealLifePlugin } from "src/types"; import { success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; // SCHEMATA -------------------- const defaultParams = incomingBaseParams; // TYPES ---------------------------------------- type DefaultParams = z.infer; export type AnyLocalParams = DefaultParams; // ROUTES -------------------- export const routePath: RoutePath = { "/settings": [ helloRoute(), { path: "/open", schema: defaultParams, handler: handleOpen }, ], }; // HANDLERS -------------------- async function handleOpen( this: RealLifePlugin, params: DefaultParams, ): Promise { const setting = this.app.setting; setting.open(); setting.openTabById(this.manifest.id); return success({ message: "Opened settings UI" }); } ================================================ FILE: src/routes/tags.ts ================================================ import { z } from "zod"; import { RoutePath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerTagsSuccess, RealLifePlugin } from "src/types"; import { success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; // SCHEMATA ---------------------------------------- const listParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); // TYPES ---------------------------------------- type ListParams = z.infer; export type AnyLocalParams = ListParams; // ROUTES ---------------------------------------- export const routePath: RoutePath = { "/tags": [ helloRoute(), { path: "/list", schema: listParams, handler: handleList }, ], }; // HANDLERS ---------------------------------------- async function handleList( this: RealLifePlugin, params: ListParams, ): Promise { return success({ tags: Object.keys(this.app.metadataCache.getTags()) .sort((a, b) => a.localeCompare(b)), }); } ================================================ FILE: src/routes/vault.ts ================================================ import { Platform } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { RoutePath } from "src/routes"; import { IncomingBaseParams, incomingBaseParams } from "src/schemata"; import { HandlerFailure, HandlerPathsSuccess, HandlerVaultInfoSuccess, HandlerVaultSuccess, RealLifeDataAdapter, RealLifePlugin, RealLifeVault, } from "src/types"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { helloRoute } from "src/utils/routing"; // SCHEMATA -------------------- const defaultParams = incomingBaseParams.extend({ "x-error": z.string().url(), "x-success": z.string().url(), }); // TYPES ---------------------------------------- type DefaultParams = z.infer; export type AnyLocalParams = DefaultParams; // ROUTES -------------------- export const routePath: RoutePath = { "/vault": [ helloRoute(), { path: "/open", schema: incomingBaseParams, handler: handleOpen }, { path: "/close", schema: incomingBaseParams, handler: handleClose }, { path: "/info", schema: defaultParams, handler: handleInfo }, { path: "/list-all-files", schema: defaultParams, handler: handleListFiles, }, { path: "/list-non-notes-files", schema: defaultParams, handler: handleListFilesExceptNotes, }, ], }; // HANDLERS -------------------- async function handleOpen( params: IncomingBaseParams, ): Promise { // If we're here, then the vault is already open. return success({}); } async function handleClose( params: IncomingBaseParams, ): Promise { if (Platform.isMobileApp) { return failure( ErrorCode.featureUnavailable, STRINGS.not_available_on_mobile, ); } // This feels wonky, like a race condition waiting to happen. window.setTimeout(window.close, 600); return success({}); } async function handleInfo( this: RealLifePlugin, params: DefaultParams, ): Promise { const { vault } = this.app; const { config } = vault; const basePath = ( vault.adapter).basePath; if (!config || !basePath) { return failure(ErrorCode.notFound, STRINGS.vault_internals_not_found); } return success({ basePath, attachmentFolderPath: `${basePath}/${config.attachmentFolderPath}` .replace(/\/$/, ""), newFileFolderPath: ( config.newFileLocation === "folder" ? `${basePath}/${config.newFileFolderPath}`.replace(/\/$/, "") : basePath ), }); } async function handleListFiles( this: RealLifePlugin, params: DefaultParams, ): Promise { return success({ paths: this.app.vault.getFiles().map((t) => t.path).sort(), }); } async function handleListFilesExceptNotes( this: RealLifePlugin, params: DefaultParams, ): Promise { const { vault } = this.app; const files = vault.getFiles().map((t) => t.path); const notes = vault.getMarkdownFiles().map((t) => t.path); return success({ paths: files.filter((path) => !notes.includes(path)).sort(), }); } ================================================ FILE: src/routes.ts ================================================ import { z } from "zod"; import { AnyLocalParams as AnyCommandParams, routePath as commandRoutes, } from "src/routes/command"; import { AnyLocalParams as AnyDataviewParams, routePath as dataviewRoutes, } from "src/routes/dataview"; import { AnyLocalParams as AnyFileParams, routePath as fileRoutes, } from "src/routes/file"; import { AnyLocalParams as AnyFolderParams, routePath as folderRoutes, } from "src/routes/folder"; import { AnyLocalParams as AnyInfoParams, routePath as infoRoutes, } from "src/routes/info"; import { AnyLocalParams as AnyNoteParams, routePath as noteRoutes, } from "src/routes/note"; import { AnyLocalParams as AnyNotePropertiesParams, routePath as notePropertiesRoutes, } from "src/routes/note-properties"; import { AnyLocalParams as AnyOmnisearchParams, routePath as omnisearchRoutes, } from "src/routes/omnisearch"; import { routePath as rootRoutes } from "src/routes/root"; import { AnyLocalParams as AnySearchParams, routePath as searchRoutes, } from "src/routes/search"; import { AnyLocalParams as AnySettingsParams, routePath as settingsRoutes, } from "src/routes/settings"; import { AnyLocalParams as AnyVaultParams, routePath as vaultRoutes, } from "src/routes/vault"; import { AnyLocalParams as AnyTagsParams, routePath as tagsRoutes, } from "src/routes/tags"; import { IncomingBaseParams } from "src/schemata"; import { HandlerFunction } from "src/types"; export const routes: RoutePath = { ...rootRoutes, ...commandRoutes, ...dataviewRoutes, ...fileRoutes, ...folderRoutes, ...infoRoutes, ...notePropertiesRoutes, ...noteRoutes, ...omnisearchRoutes, ...searchRoutes, ...settingsRoutes, ...tagsRoutes, ...vaultRoutes, }; /** * A `RoutePath` describes a routing branch coming off from the root node (`/`). * It's an object with properties, each key containing a route `path` and its * related value is an array of object each describing a sub-path. * * Example: * * { "daily-note": [ * { path: "create", schema: …, handler: … }, * { path: "get", schema: …, handler: … }, … * ] } * => builds routes `/daily-note/create` and `/daily-note/get` * * A `RouteSubpath` describes a sub-path (`path`), the Zod schema for validation * and a `handler` function. It defines which handler function is responsible * for which route path, and what data structure the function can expect. */ export type RoutePath = { [path: string]: RouteSubpath[]; }; export type RouteSubpath = { path: string; schema: | z.AnyZodObject | z.ZodDiscriminatedUnion | z.ZodEffects | z.ZodUnion; handler: HandlerFunction; }; export type AnyParams = | AnyCommandParams | AnyDataviewParams | AnyFileParams | AnyFolderParams | AnyInfoParams | AnyNoteParams | AnyNotePropertiesParams | AnyOmnisearchParams | AnySearchParams | AnySettingsParams | AnyTagsParams | AnyVaultParams | IncomingBaseParams; export enum NoteTargetingParameterKey { File = "file", UID = "uid", PeriodicNote = "periodic-note", } ================================================ FILE: src/schemata.ts ================================================ import { z } from "zod"; import { zodOptionalBoolean, zodSanitizedNotePath } from "src/utils/zod"; import { PeriodicNoteType, PeriodicNoteTypeWithRecents, } from "src/utils/periodic-notes-handling"; export const incomingBaseParams = z.object({ action: z.string(), vault: z.string().min(1, { message: "can't be empty" }), // When enabled, the plugin will return all input call parameters as part of // its `x-success` or `x-error` callbacks. "debug-mode": zodOptionalBoolean, // When enabled, the plugin will not show any error notices in Obsidian. For // example, if a requested note isn't available, the plugin would normally // show a notice in Obsidian. This can be disabled by setting this to `true`. "hide-ui-notice-on-error": zodOptionalBoolean, "x-error": z.string().url().optional(), "x-success": z.string().url().optional(), "x-source": z.string().optional(), }); export type IncomingBaseParams = z.output; export const noteTargetingParams = z.object({ file: zodSanitizedNotePath.optional(), uid: z.string().optional(), "periodic-note": z.nativeEnum(PeriodicNoteType).optional(), }); export type NoteTargetingParams = z.output; export const noteTargetingWithRecentsParams = z.object({ file: zodSanitizedNotePath.optional(), uid: z.string().optional(), "periodic-note": z.nativeEnum(PeriodicNoteTypeWithRecents).optional(), }); export type NoteTargetingWithRecentsParams = z.output< typeof noteTargetingWithRecentsParams >; ================================================ FILE: src/settings.ts ================================================ import { App, debounce, PluginSettingTab, Setting } from "obsidian"; import ActionsURI from "src/main"; export class SettingsTab extends PluginSettingTab { plugin: ActionsURI; constructor(app: App, plugin: ActionsURI) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl, plugin, plugin: { settings, defaultSettings }, } = this; const debounceOnChange = debounce( async (val: string) => { settings.frontmatterKey = val.trim() || defaultSettings.frontmatterKey; await plugin.saveSettings(); }, 400, ); containerEl.empty(); new Setting(containerEl) .setName("UID frontmatter key") .setDesc(` Actions URI is able to find notes by their UID. This unique identifier is stored in the note's frontmatter. The plugin needs to know under which frontmatter key it can find the UID. (Default: "uid".) `) .addText((input) => { input .setPlaceholder(defaultSettings.frontmatterKey) .setValue(settings.frontmatterKey) .onChange(debounceOnChange); }); // Sponsoring const afoURL = "https://actions.work/actions-for-obsidian?ref=plugin-actions-uri"; containerEl.createEl("div", { attr: { style: ` border-radius: 0.5rem; border: 1px dashed var(--text-muted); color: var(--text-muted); display: grid; font-size: 85%; grid-gap: 1rem; grid-template-columns: auto 1fr; margin-top: 4rem; opacity: 0.75; padding: 1rem; `, }, }) .innerHTML = ` Actions for Obsidian icon, a cog wheel on a glossy black background Actions URI is brought to you by Actions for Obsidian, a macOS/iOS app made by the same developer as this plugin. AFO is the missing link between Obsidian and macOS / iOS: 50+ Shortcuts actions to bring your notes and your automations together. Take a look! `; } } ================================================ FILE: src/types/handlers.d.ts ================================================ /** * A handler function is a function that is responsible for dealing with a * particular route. It takes a payload (i.e. the parameters from the incoming * `x-callback-url` call) and a vault and returns any handler result object. * * @param incomingParams - The parameters from the incoming `x-callback-url` * * @returns A handler result object */ export type HandlerFunction = ( incomingParams: any, ) => Promise; type HandlerResult = { isSuccess: boolean; }; type HandlerSuccess = & HandlerResult & { processedFilepath?: string }; export type HandlerFailure = Readonly< & HandlerResult & { errorCode: number; errorMessage: string; } >; export type HandlerTextSuccess = Readonly< & HandlerSuccess & { result: { message: string; }; } >; export type HandlerFileSuccess = Readonly< & HandlerSuccess & { result: { content: string; filepath: string; uriPath: string; uriUID?: string; body?: string; frontMatter?: string; properties?: NoteProperties; selection?: string; uid?: string | string[]; }; } >; export type HandlerFilePathSuccess = Readonly< & HandlerSuccess & { result: { filepath: string; }; } >; export type HandlerDataviewSuccess = Readonly< & HandlerSuccess & { result: { data: string; }; } >; export type HandlerPathsSuccess = Readonly< & HandlerSuccess & { result: { paths: string[]; }; } >; export type HandlerSearchSuccess = Readonly< & HandlerSuccess & { result: { hits: string[]; }; } >; export type HandlerTagsSuccess = Readonly< & HandlerSuccess & { result: { tags: string[]; }; } >; export type HandlerInfoSuccess = Readonly< & HandlerSuccess & { result: { pluginVersion: string; pluginReleasedAt: string; apiVersion: string; nodeVersion: string; os: string; platform: string; }; } >; export type HandlerVaultSuccess = Readonly< & HandlerSuccess & { result: {} } >; export type HandlerVaultInfoSuccess = Readonly< & HandlerSuccess & { result: { basePath: string; attachmentFolderPath: string; newFileFolderPath: string; }; } >; export type HandlerCommandsSuccess = Readonly< & HandlerSuccess & { result: { commands: string; }; } >; export type HandlerCommandsExecutionSuccess = Readonly< & HandlerSuccess & { result: {} } >; export type HandlerPropertiesSuccess = Readonly< & HandlerSuccess & { result: { properties: NoteProperties; }; } >; export type NoteProperties = Record< string, string | string[] | number | boolean | null >; export type AnyHandlerSuccess = | HandlerCommandsExecutionSuccess | HandlerCommandsSuccess | HandlerDataviewSuccess | HandlerFileSuccess | HandlerFilePathSuccess | HandlerInfoSuccess | HandlerPathsSuccess | HandlerPropertiesSuccess | HandlerSearchSuccess | HandlerTextSuccess | HandlerVaultSuccess; export type AnyHandlerResult = | AnyHandlerSuccess | HandlerFailure; ================================================ FILE: src/types/obsidian-objects.d.ts ================================================ import { App, CachedMetadata, Command, DataAdapter, MetadataCache, PluginManifest, TAbstractFile, TFile, Vault, } from "obsidian"; import { PluginSettings } from "src/types"; export interface RealLifePlugin extends App { app: RealLifeApp; manifest: PluginManifest; settings: PluginSettings; vault: RealLifeVault; } export interface RealLifeApp extends App { commands: { executeCommandById(id: string): boolean; listCommands(): Command[]; }; internalPlugins: any; metadataCache: RealLifeMetadataCache; plugins: any; setting: { open: () => void; openTabById: (pluginName: string) => void; }; } export interface RealLifeVault extends Vault { fileMap: Record; config: { attachmentFolderPath: string; newFileLocation: "root" | "current" | "folder"; newFileFolderPath: string; }; } export interface RealLifeDataAdapter extends DataAdapter { basePath: string; } export interface RealLifeMetadataCache extends MetadataCache { getTags(): Record; /** * The default type signature is `getFileCache(file: TFile): CachedMetadata | null;`. * However, I've checked the actual implementation, and the function in `app.js` * only accesses `file.path` – which is present in both `TFile` and `TAbstractFile`. * * *"Bold move, Cotton. Let's see if it pays off."* */ getFileCache(file: TFile | TAbstractFile): CachedMetadata | null; fileCache: Record; metadataCache: Record }>; } ================================================ FILE: src/types/plugins.d.ts ================================================ // Source: https://publish.obsidian.md/omnisearch/Public+API+%26+URL+Scheme export type OmnisearchAPI = { // Returns a promise that will contain the same results as the Vault modal search: (query: string) => Promise; // Refreshes the index refreshIndex: () => Promise; // Register a callback that will be called when the indexing is done registerOnIndexed: (callback: () => void) => void; // Unregister a callback that was previously registered unregisterOnIndexed: (callback: () => void) => void; }; type OmnisearchResultNoteApi = { score: number; path: string; basename: string; foundWords: string[]; matches: OmnisearchSearchMatchApi[]; }; type OmnisearchSearchMatchApi = { match: string; offset: number; }; ================================================ FILE: src/types/results.d.ts ================================================ import { TFile } from "obsidian"; import { AnyParams } from "src/routes"; import { AnyHandlerResult, NoteProperties } from "src/types"; type ErrorObject = { isSuccess: false; errorCode: number; errorMessage: string; }; type ResultObject = { isSuccess: true; result: T; processedFilepath?: string; }; export type TFileResultObject = ResultObject | ErrorObject; export type RegexResultObject = ResultObject | ErrorObject; export type SearchResultObject = | ResultObject<{ hits: string[] }> | ErrorObject; export type StringResultObject = ResultObject | ErrorObject; export type PluginResultObject = ResultObject | ErrorObject; export type BooleanResultObject = ResultObject | ErrorObject; export type ProcessingResult = { params: AnyParams; handlerResult: AnyHandlerResult; sendCallbackResult: StringResultObject; openResult: StringResultObject; }; export type NoteDetailsResultObject = | ResultObject<{ filepath: string; content: string; body: string; frontMatter: string; properties: NoteProperties; uriPath: string; uriUID?: string; uid?: string | string[]; }> | ErrorObject; ================================================ FILE: src/types.d.ts ================================================ export * from "src/types/handlers"; export * from "src/types/obsidian-objects"; export * from "src/types/plugins"; export * from "src/types/results"; export type PluginSettings = { frontmatterKey: string; }; /** * A TypeScript type alias called `Prettify`. * It takes a type as its argument and returns a new type that has the same properties as the original type, * but the properties are not intersected. This means that the new type is easier to read and understand. */ export type Prettify = & { [K in keyof T]: T[K] extends object ? Prettify : T[K] } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents & unknown; ================================================ FILE: src/utils/callbacks.ts ================================================ import { ObsidianProtocolData, requestUrl, TAbstractFile } from "obsidian"; import { excludeKeys } from "filter-obj"; import { XCALLBACK_RESULT_PREFIX } from "src/constants"; import { PLUGIN_INFO } from "src/plugin-info"; import { AnyParams } from "src/routes"; import { AnyHandlerResult, AnyHandlerSuccess, HandlerFailure, StringResultObject, } from "src/types"; import { success } from "src/utils/results-handling"; import { toKebabCase } from "src/utils/string-handling"; /** * @param baseURL - The base `x-callback-url` of the receiver, e.g. * "another-app://", "another-app://x-callback-url/success" or * "another-app://success" * @param handlerRes - Any route handler result object * * @returns A `StringResultObject` with the `result` property set to the called * URL * * @see {@link AnyHandlerResult} */ export function sendUrlCallback( baseURL: string, handlerRes: AnyHandlerResult, params: AnyParams | ObsidianProtocolData, ): StringResultObject { const url = new URL(baseURL); if (handlerRes.isSuccess) { addObjectToUrlSearchParams(( handlerRes).result, url); } else { const { errorCode, errorMessage } = handlerRes; url.searchParams.set("errorCode", errorCode.toString()); url.searchParams.set("errorMessage", errorMessage); } if (params["x-source"] && /actions for obsidian/i.test(params["x-source"])) { url.searchParams.set("pv", PLUGIN_INFO.pluginVersion); } const returnParams: Record = params["debug-mode"] ? excludeKeys( params, [ "debug-mode", "x-success", "x-error", "_computed", ]) : {}; addObjectToUrlSearchParams(returnParams, url, "input"); const callbackURL = url.toString().replace(/\+/g, "%20"); sendCallbackResult(callbackURL); return success(callbackURL); } /** * Adds properties of an object as search params to a `URL` instance. The keys * of the object will be normalized to kebab case. * * @param obj - An object whose properties are to be added an `URL` object as * search parameters * @param url - The `URL` target object * @param prefix - An optional prefix to be added to the parameter names, * defaults to `XCALLBACK_RESULT_PREFIX` */ function addObjectToUrlSearchParams( obj: Record, url: URL, prefix: string = XCALLBACK_RESULT_PREFIX, ) { const sortedKeys = Object.keys(obj).sort(); for (const key of sortedKeys) { if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; let val: string | undefined; if (typeof obj[key] === "string") { val = obj[key]; } else if (obj[key] instanceof TAbstractFile) { val = ( obj[key]).path; } else if (typeof obj[key] !== "undefined") { val = JSON.stringify(obj[key]); } if (val === undefined) continue; url.searchParams.set(toKebabCase(`${prefix}-${key}`), val); } } /** * Sends a XCU callback, i.e. makes a request to the given URI. * * If the URL is a HTTP/HTTPS one, it is assumed the callback is slated for the * HTTP server of the testing setup, and Obsidian's own `requestUrl()` is used. * * In production mode (outside testing) the URI is passed to the OS using * `window.open()`, which passes them to the registered apps. (In testing, we * use a HTTP server, and `window.open()` would have the OS pass the URI to the * default browser, which is not what we want.) * * @param uri - The URI to call */ function sendCallbackResult(uri: string) { if (/^https?:\/\//.test(uri)) { requestUrl(uri); } else { window.open(uri); } } ================================================ FILE: src/utils/file-handling.ts ================================================ import { MarkdownView, normalizePath, TAbstractFile, TFile } from "obsidian"; import { STRINGS } from "src/constants"; import { self } from "src/utils/self"; import { getEnabledCorePlugin, isCommunityPluginEnabled, isCorePluginEnabled, } from "src/utils/plugins"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { endStringWithNewline, escapeRegExpChars, extractNoteContentParts, } from "src/utils/string-handling"; import { pause } from "src/utils/time"; import { BooleanResultObject, NoteDetailsResultObject, NoteProperties, RealLifeVault, StringResultObject, TFileResultObject, } from "src/types"; import { focusOrOpenNote, logErrorToConsole, showBrandedNotice, } from "src/utils/ui"; /** * Create a new note. If the note already exists, find a available numeric * suffix for the filename and create a new note with that suffix. After creation, * there will be a short pause to allow other plugins to do their thing (Templates, * Templater, etc.). * * @example * - `test.md` exists → `test 1.md` * - `test 1.md` exists → `test 2.md` * * @param filepath - A full filename, relative from vault root * root * @param content - The body of the note to be created * * @returns The created file * * @remarks * The `filename` parameter will be sanitized and suffixed with the file * extension `.md` if it is not already present. */ export async function createNote( filepath: string, content: string, ): Promise { filepath = sanitizeFilePath(filepath); const vault = self().app.vault; let file = vault.getFileByPath(filepath); let doesFileExist = file instanceof TFile; if (doesFileExist) { // Add a numeric suffix to the filename (w/o its extension), and see whether // the filename is available. Make sure to honor an existing suffix by // starting to increment from there, eg. `test.md` → `test 1.md`, // `test 17.md` → `test 18.md`, etc. const currentNumSuffix: string | undefined = ( filepath.match(/( (\d+))?\.md$/))[2]; let numSuffix = currentNumSuffix ? +currentNumSuffix : 0; do { numSuffix++; filepath = filepath.replace(/( \d+)?\.md$/, ` ${numSuffix}.md`); file = vault.getFileByPath(filepath); doesFileExist = file instanceof TFile; } while (doesFileExist); } // Create folder if necessary await createFolderIfNecessary(dirname(filepath)); // Create the new note await createAndPause(filepath, content); const newFile = vault.getFileByPath(filepath); return (newFile instanceof TFile) ? success(newFile) : failure(ErrorCode.unableToWrite, STRINGS.unable_to_write_note); } /** * Create a new note. If the note already exists, overwrite its content. * * @param filepath - A full filename, including the path relative from vault * root * @param content - The body of the note to be created * * @returns The created file * * @remarks * The `filename` parameter will be sanitized and suffixed with the file * extension `.md` if it is not already present. */ export async function createOrOverwriteNote( filepath: string, content: string, ): Promise { filepath = sanitizeFilePath(filepath); const { vault } = self().app; const file = vault.getFileByPath(filepath); // Update the file if it already exists, but give any other creation-hooked // functions some time to do their things first if (file instanceof TFile) { await pause(500); await vault.modify(file, content); return success( vault.getFileByPath(filepath)); } // Create the new note await createFolderIfNecessary(dirname(filepath)); await createAndPause(filepath, content); const newFile = vault.getFileByPath(filepath); return (newFile instanceof TFile) ? success(newFile) : failure(ErrorCode.unableToWrite, STRINGS.unable_to_write_note); } /** * Fetches an existing note and returns its content. * * @param filepath - A full filename, relative from vault root * * @returns A result object. Success case: note body, failure case: readable * error message */ export async function getNoteContent( filepath: string, ): Promise { const vault = self().app.vault; const res = await getNote(filepath); if (!res.isSuccess) { return res; } const noteContent = await vault.read(res.result); return (typeof noteContent === "string") ? success(noteContent) : failure(ErrorCode.unableToWrite, STRINGS.unable_to_read_note); } /** * Fetches an existing note and returns its split-up contents. * * @param filepath - A full filename, relative from vault root * * @returns A result object. Success case: note path, content, body and front * matter; failure case: readable error message */ export async function getNoteDetails( filepath: string, ): Promise { const res = await getNote(filepath); if (!res.isSuccess) { return res; } const res2 = await getNoteContent(filepath); if (!res2.isSuccess) { return res2; } const file = res.result; const content = res2.result; const { body, frontMatter } = extractNoteContentParts(content); const properties = await propertiesForFile(file); const vaultName = self().app.vault.getName(); const uid = getFirstUID(properties); return success({ filepath, content, uriPath: getUriPath(filepath, vaultName), uriUID: getUriUID(uid, vaultName), body, frontMatter, properties, uid, }); } /** * Make sure user-submitted file paths are relative to the vault root and the * path is normalized and sanitized. Returned paths will never start with dots * or slashes. * * @param filename - A full file path * @param isNote - Whether the path is a note; if `true`, ensure the path ends * in `.md`/`.canvas`, otherwise leave the path alone. * Default: `true` * * @returns A normalized file path relative to the vault root */ export function sanitizeFilePath( filename: string, isNote: boolean = true, ): string { filename = filename.replace(/[:#^[]|]/g, "-"); filename = normalizePath(filename) .split("/") .map((seg) => seg.trim()) .join("/") .replace(/^[\/\.]+/g, ""); return (isNote && !/\.(md|canvas)/i.test(extname(filename))) ? `${filename}.md` : filename; } /** * Make sure user-submitted file paths are relative to the vault root and the * path is normalized and sanitized. Returned paths will never start with dots * or slashes. * * @param filename - A full file path * @param isNote - Whether the path is a note; if `true`, ensure the path ends * in `.md`/`.canvas`, otherwise leave the path alone. * Default: `true` */ export function sanitizeFilePathAndGetAbstractFile( path: string, isNote: boolean = true, ): TAbstractFile | null { return self().app.vault.getAbstractFileByPath(sanitizeFilePath(path, isNote)); } /** * Replaces the front matter and/or body of an existing note and returns its new * split-up contents. * * @param filepath - A full filename, relative from vault root * @param newFrontMatter - The new front matter to use. If not specified, the * existing front matter will be kept. An empty string * will clear any existing front matter. * @param newBody - The new body to use. If not specified, the existing body * will be kept. An empty string will remove the existing body. * * @returns A result object. Success case: note path, content, body and front * matter; failure case: readable error message */ export async function updateNote( filepath: string, newFrontMatter?: string, newBody?: string, ): Promise { const res = await getNote(filepath); if (!res.isSuccess) { return res; } const res2 = await getNoteDetails(filepath); if (!res2.isSuccess) { return res2; } // If both newFrontMatter and newBody are undefined, there's nothing to do. if (typeof newFrontMatter !== "string" && typeof newBody !== "string") { return res2; } const vaultName = self().app.vault.getName(); const file = res.result; const noteDetails = res2.result; const body = (typeof newBody === "string") ? newBody : noteDetails.body; let frontMatter = (typeof newFrontMatter === "string") ? newFrontMatter : noteDetails.frontMatter; frontMatter = frontMatter.trim(); const newNoteContent = frontMatter !== "" ? ["---", frontMatter, "---", body].join("\n") : body; await self().app.vault.modify(file, newNoteContent); const properties = await propertiesForFile(file); const uid = getFirstUID(properties); return success({ filepath, content: newNoteContent, uriPath: getUriPath(vaultName, filepath), uriUID: getUriUID(uid, vaultName), body, frontMatter, properties, uid, }); } function getUriPath(filepath: string, vaultName: string): string { return `obsidian://actions-uri/note/open` + "?vault=" + encodeURIComponent(vaultName) + "&file=" + encodeURIComponent(filepath); } function getUriUID( uid: string | undefined, vaultName: string, ): string | undefined { if (!uid) return undefined; return `obsidian://actions-uri/note/open` + "?vault=" + encodeURIComponent(vaultName) + "&uid=" + encodeURIComponent(uid); } /** * Returns the first UID from the frontmatter properties of a note. */ function getFirstUID(properties: NoteProperties): string | undefined { const uid = properties[self().settings.frontmatterKey] as | string | string[] | undefined; return Array.isArray(uid) ? uid[0] : uid; } /** * Sets the modification time of the file to now. * * @param filepath - A full filename, relative from vault root * * @returns A `StringResultObject` object containing either an `error` string or * `result` string */ export async function touchNote( filepath: string, ): Promise { const res = await getNote(filepath); if (!res.isSuccess) return res; const res2 = await getNoteDetails(filepath); if (!res2.isSuccess) return res2; await self().app.vault.modify(res.result, res2.result.content); return res; } /** * @param filepath - A full filename, relative from vault root * @param searchTerm - The term to search for * @param replacement - The term to replace the search term with * @returns A `StringResultObject` object containing either an `error` string or * `result` string */ export async function searchAndReplaceInNote( filepath: string, searchTerm: string | RegExp, replacement: string, ): Promise { const res = await getNoteContent(filepath); if (!res.isSuccess) { return res; } const noteContent = res.result; const newContent = (typeof searchTerm === "string") ? noteContent.replaceAll(searchTerm, replacement) : noteContent.replace(searchTerm, replacement); if (noteContent === newContent) { return (typeof searchTerm === "string") ? success(STRINGS.search_string_not_found) : success(STRINGS.search_pattern_not_found); } const resFile = await createOrOverwriteNote(filepath, newContent); return resFile.isSuccess ? success(STRINGS.replacement_done) : resFile; } export async function appendNote( filepath: string, textToAppend: string, shouldEnsureNewline: boolean = false, ): Promise { const res = await getNoteContent(filepath); if (!res.isSuccess) { return res; } const newContent = res.result + (shouldEnsureNewline ? endStringWithNewline(textToAppend) : textToAppend); const resFile = await createOrOverwriteNote(filepath, newContent); if (resFile.isSuccess) { return success(STRINGS.append_done); } return resFile; } export async function appendNoteBelowHeadline( filepath: string, belowHeadline: string, textToAppend: string, ): Promise { const resTFile = await getNote(filepath); if (!resTFile.isSuccess) { return resTFile; } const headlineRegex = new RegExp( `^${escapeRegExpChars(belowHeadline.trim())}\\s*\\n`, "s", ); const resProcess = await self().app.vault.process( resTFile.result, (contents) => { // Split into sections by headline, find the section below the specified // headline, and append the text to that section return endStringWithNewline(contents) .split(/(?=^#+ )/m) .map((section) => { if (!headlineRegex.test(section)) { return section; } // Rebuild the section by trimming it, appending the text, and adding back // the original number of consecutive newlines return endStringWithNewline( section.trim() + "\n" + textToAppend + section.match(/\n+$/)?.[0] || "", ); }) .join(""); }, ); return resProcess ? success(STRINGS.append_done) : failure(ErrorCode.unableToWrite, STRINGS.unable_to_write_note); } export async function prependNote( filepath: string, textToPrepend: string, shouldEnsureNewline: boolean = false, shouldIgnoreFrontMatter: boolean = false, ): Promise { const res = await getNoteContent(filepath); if (!res.isSuccess) { return res; } const noteContent = res.result; let newContent: string; if (shouldEnsureNewline) { textToPrepend = endStringWithNewline(textToPrepend); } if (shouldIgnoreFrontMatter) { newContent = textToPrepend + noteContent; } else { const { frontMatter, body } = extractNoteContentParts(noteContent); newContent = `---\n${frontMatter}---\n` + textToPrepend + body; } const resFile = await createOrOverwriteNote(filepath, newContent); return resFile.isSuccess ? success(STRINGS.prepend_done) : resFile; } export async function prependNoteBelowHeadline( filepath: string, belowHeadline: string, textToPrepend: string, shouldEnsureNewline: boolean = false, ): Promise { const resTFile = await getNote(filepath); if (!resTFile.isSuccess) { return resTFile; } const headlineRegex = new RegExp( `^${escapeRegExpChars(belowHeadline.trim())}\\s*\\n`, "s", ); const resProcess = await self().app.vault.process( resTFile.result, (contents) => { // Split into sections by headline, find the section below the specified // headline, and prepend the text to that section return endStringWithNewline(contents) .split(/(?=^#+ )/m) .map((section) => { if (!headlineRegex.test(section)) { return section; } if (shouldEnsureNewline) { textToPrepend = endStringWithNewline(textToPrepend); } const prependedSection = section.split("\n"); prependedSection[1] = textToPrepend + (prependedSection[1] || ""); const newSection = prependedSection.join("\n"); return endStringWithNewline(newSection); }) .join(""); }, ); return resProcess ? success(STRINGS.append_done) : failure(ErrorCode.unableToWrite, STRINGS.unable_to_write_note); } /** * Gets the list of all files and folders in the vault. * * @returns An array of `TFile` instances */ export function getFileMap(): TFile[] { const vault = self().app.vault; const { fileMap } = vault; return Object.values(fileMap); } /** * Checks whether a particular file exists and when it does, returns its `TFile` * instance. * * @param filepath - A full filename * * @returns A result object containing either an error or the `TFile`. */ export async function getFile( filepath: string, ): Promise { const cleanPath = sanitizeFilePath(filepath, false); const file = self().app.vault.getFileByPath(cleanPath); return file instanceof TFile ? success(file) : failure(ErrorCode.notFound, STRINGS.note_not_found); } /** * Checks whether a particular note file exists and when it does, returns its * `TFile` instance. * * @param filepath - A full filename * * @returns A result object containing either an error or the `TFile`. */ export async function getNote( filepath: string, ): Promise { const cleanPath = sanitizeFilePath(filepath); const file = self().app.vault.getFileByPath(cleanPath); return file instanceof TFile ? success(file) : failure(ErrorCode.notFound, STRINGS.note_not_found); } /** * Opens or focusses a particular note, then applies a template to it, using the * core Templates plugin. * * @param templateFile - The template file to apply * @param note - The note to apply the template to * @returns A result object containing either an error or `true`. */ export async function applyCorePluginTemplate( templateFile: TAbstractFile, note: TFile, ): Promise { const pluginRes = getEnabledCorePlugin("templates"); if (!pluginRes.isSuccess) return pluginRes; const pluginInstance = pluginRes.result; // The core plugin will only apply a template to the open, focussed, and // editable note ¯\_(ツ)_/¯ await focusOrOpenNote(note.path); try { // Ensure the view is in source mode const activeView = self().app.workspace.getActiveViewOfType(MarkdownView); if (activeView && activeView?.getMode() !== "source") { await activeView.setState( { ...activeView.getState(), mode: "source" }, { history: false }, ); } } catch (error) { const msg = ( error).message; showBrandedNotice(msg); logErrorToConsole(msg); return failure(ErrorCode.handlerError, msg); } await pause(200); await pluginInstance.insertTemplate(templateFile); return success(true); } /** * Moves a particular file/folder to the trash or deletes it right away. * * @param filepath - A full filename * @param deleteImmediately - Whether the file should be deleted immediately * (`true`) or moved to the preferred trash location (`false`, default) * * @returns A result object containing either an error or a success message. */ export async function trashFilepath( filepath: string, deleteImmediately: boolean = false, ): Promise { const vault = self().app.vault; const fileOrFolder = vault.getAbstractFileByPath(filepath); if (!fileOrFolder) { return failure(ErrorCode.notFound, STRINGS.not_found); } if (deleteImmediately) { await vault.delete(fileOrFolder, true); } else { const isSystemTrashPreferred = ( vault).config?.trashOption === "system"; await vault.trash(fileOrFolder, isSystemTrashPreferred); } return success(STRINGS.trash_done); } /** * Renames or moves a file/folder. * * @param filepath - A full filename * @param newFilepath - A full filename * * @returns A result object containing either an error or a success message. */ export async function renameFilepath( filepath: string, newFilepath: string, ): Promise { const vault = self().app.vault; const fileOrFolder = vault.getAbstractFileByPath(filepath); if (!fileOrFolder) { return failure(ErrorCode.notFound, STRINGS.not_found); } try { await self().app.fileManager.renameFile(fileOrFolder, newFilepath); } catch (error) { const msg = ( error).message; return failure( ErrorCode.notFound, msg.contains("no such file or directory") ? "No such file or folder" : msg, ); } return success(STRINGS.rename_done); } /** * Creates a folder but checks for its existence before attempting creation. * We're civilized people here. * * @param folder - A folder path relative from the vault root */ export async function createFolderIfNecessary(folder: string) { const vault = self().app.vault; folder = sanitizeFilePath(folder, false); if (folder === "" || folder === ".") return; // Back off if the folder already exists if (vault.getFolderByPath(folder)) return; await vault.createFolder(folder); } /** * Returns the frontmatter properties for a given file. * @param file - The file to retrieve properties for. * @returns An object containing the frontmatter properties of the file, or an * empty object if none exist. */ export async function propertiesForFile( file: TAbstractFile, ): Promise { // Without this delay, more often than not, `propertiesForFile()` will return // outdated properties. await pause(200); return self().app.metadataCache.getFileCache(file)?.frontmatter || {}; } // HELPERS ---------------------------------------- /** * Necessary for preventing a race condition when creating an empty note in a * folder that is being watched by either templates plugin. * * @param filepath - A full filename, including the path relative from vault * root * @param content - The body of the note to be created * * @remarks * See issue #61 at https://github.com/czottmann/obsidian-actions-uri/issues/61 */ async function createAndPause(filepath: string, content: string) { // Create the new note await self().app.vault.create(filepath, content); if ( isCorePluginEnabled("templates") || isCommunityPluginEnabled("templater-obsidian") ) { await pause(500); } } /** * Returns the directory name of a `path`, as a bare-bones replacement for * Node's `path.dirname`. * * @param path - A file path * * @returns Directory name of the input `path` */ function dirname(path: string) { path = normalizePath(path); return path.indexOf("/") === -1 ? "." : path.replace(/\/[^/]*$/, ""); } /** * Returns the extension of the `path`, from the last occurrence of the `.` * (period) character to end of string in the last portion of the `path`. If * there is no `.` in the last portion of the `path`, or if there are no `.` * characters other than the first character of the basename of `path`, an empty * string is returned. * * @param path - A file path * * @returns Filename extension of the input `path` */ function extname(path: string) { const filename = normalizePath(path).split("/").pop() || ""; return filename.includes(".") ? `.${filename.split(".").pop()}` : ""; } ================================================ FILE: src/utils/parameters.ts ================================================ import { parseFrontMatterEntry, TAbstractFile } from "obsidian"; import { z } from "zod"; import { STRINGS } from "src/constants"; import { sanitizeFilePathAndGetAbstractFile } from "src/utils/file-handling"; import { self } from "src/utils/self"; import { checkForEnabledPeriodicNoteFeature, getCurrentPeriodicNotePath, getMostRecentPeriodicNotePath, PeriodicNoteType, PeriodicNoteTypeWithRecents, } from "src/utils/periodic-notes-handling"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { NoteTargetingParameterKey } from "src/routes"; import { NoteTargetingParams } from "src/schemata"; import { StringResultObject } from "src/types.d"; // TYPES ---------------------------------------- type ResolvedData = { _resolved: Record; }; export type ResolvedNoteTargetingValues = Readonly<{ _resolved: { inputKey: NoteTargetingParameterKey; inputPath: string; inputFile: TAbstractFile | undefined; }; }>; // RESOLVERS ---------------------------------------- /** * Validates the note targeting parameters and adds computed values to the * input object (under the `_resolved` key). * * This function ensures that exactly one of the specified targeting parameters * (`file`, `uid`, or `periodic-note`) is provided. If the validation passes, * it gets the requested note path based on the input and appends it to the * returned object. * * @param data - The input data containing targeting parameters. * @param ctx - The Zod refinement context used for adding validation issues. * @param throwOnMissingNote - Whether to throw a Zod validation error if the * requested note path does not exist. Defaults to `false`. * @returns The input object augmented with computed values if validation * succeeds; otherwise, it triggers a Zod validation error. * @throws {ZodError} When more than one or none of the targeting parameters are provided. * * @template T - The type of the input data. */ export function resolveNoteTargeting( data: T, ctx: z.RefinementCtx, throwOnMissingNote: boolean = false, ): T & ResolvedNoteTargetingValues { const input = data as NoteTargetingParams; // Validate that only one of the three keys is present const keysCount = [ NoteTargetingParameterKey.File, NoteTargetingParameterKey.UID, NoteTargetingParameterKey.PeriodicNote, ] .filter((key) => key in input) .length; if (keysCount !== 1) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.faulty_note_targeting, }); return z.NEVER; } // Get the requested file path let inputKey: NoteTargetingParameterKey; let inputPath = ""; if (NoteTargetingParameterKey.File in input) { const val = input[NoteTargetingParameterKey.File]!; inputKey = NoteTargetingParameterKey.File; inputPath = val; } // else if (NoteTargetingParameterKey.UID in input) { const val = input[NoteTargetingParameterKey.UID]!; inputKey = NoteTargetingParameterKey.UID; const res = filepathForUID(val); inputPath = res.isSuccess ? res.result : ""; } // else if (input[NoteTargetingParameterKey.PeriodicNote]) { const val = input[ NoteTargetingParameterKey.PeriodicNote ]! as unknown as PeriodicNoteTypeWithRecents; inputKey = NoteTargetingParameterKey.PeriodicNote; const periodicNoteType = val.replace(/^recent-/, "") as PeriodicNoteType; const shouldFindMostRecent = val.startsWith("recent-"); // Normalize "recent-daily" into "daily" etc. then check feature availability const isPluginAvailable = checkForEnabledPeriodicNoteFeature( periodicNoteType, ); if (!isPluginAvailable) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS[`${periodicNoteType}_note`].feature_not_available, }); return z.NEVER; } if (shouldFindMostRecent) { // Get the most recent note path const resRPN = getMostRecentPeriodicNotePath(periodicNoteType); inputPath = resRPN.isSuccess ? resRPN.result : ""; } else { // Get the current note path inputPath = getCurrentPeriodicNotePath(periodicNoteType); } } // Validate that the requested note path exists let inputFile: TAbstractFile | undefined; if (inputPath != "") { const resFileTest = sanitizeFilePathAndGetAbstractFile(inputPath); if (resFileTest) { inputFile = resFileTest; inputPath = resFileTest.path; } } if (!inputFile && throwOnMissingNote) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: STRINGS.note_not_found, path: [inputPath], }); return z.NEVER; } // Return original object plus resolved values return mergeResolvedData(data, { inputKey: inputKey!, inputPath, inputFile, }); } /** * Validates the note targeting parameters and adds computed values. Triggers a * Zod validation error if the requested note path does not exist. * * This function ensures that exactly one of the specified targeting parameters * (`file`, `uid`, or `periodic-note`) is provided. If the validation passes, * it gets the requested note path based on the input and appends it to the * returned object. * * @param data - The input data containing targeting parameters. * @param ctx - The Zod refinement context used for adding validation issues. * @returns The input object augmented with computed values if validation * succeeds; otherwise, it triggers a Zod validation error. Also triggers a * Zod validation error if the note path does not exist. * @throws {ZodError} If more than one or none of the targeting parameters are * provided. * * @template T - The type of the input data. */ export function resolveNoteTargetingStrict( data: T, ctx: z.RefinementCtx, ): T & ResolvedNoteTargetingValues { return resolveNoteTargeting(data, ctx, true); } // HELPERS ---------------------------------------- /** * Merges the resolved values into the input object. If the input object already * contains a `_resolved` key, the new values are merged into it. */ export function mergeResolvedData( data: T, resolved: U, ): T & { _resolved: U } { return { ...data, _resolved: { ...(data as T & ResolvedData)._resolved || {}, ...resolved, }, }; } function filepathForUID(uid: string): StringResultObject { const path = self().app.vault .getMarkdownFiles() .find((note) => { let uidValues = parseFrontMatterEntry( self().app.metadataCache.getFileCache(note)?.frontmatter, self().settings.frontmatterKey, ); return [uidValues].flat().map((u) => `${u}`).includes(uid); }) ?.path; return path ? success(path) : failure(ErrorCode.notFound, STRINGS.note_not_found); } ================================================ FILE: src/utils/periodic-notes-handling.ts ================================================ import { moment, TFile } from "obsidian"; import { appHasDailyNotesPluginLoaded, appHasMonthlyNotesPluginLoaded, appHasQuarterlyNotesPluginLoaded, appHasWeeklyNotesPluginLoaded, appHasYearlyNotesPluginLoaded, createDailyNote, createMonthlyNote, createQuarterlyNote, createWeeklyNote, createYearlyNote, getAllDailyNotes, getAllMonthlyNotes, getAllQuarterlyNotes, getAllWeeklyNotes, getAllYearlyNotes, getDailyNote, getDailyNoteSettings, getMonthlyNote, getMonthlyNoteSettings, getQuarterlyNote, getQuarterlyNoteSettings, getWeeklyNote, getWeeklyNoteSettings, getYearlyNote, getYearlyNoteSettings, } from "obsidian-daily-notes-interface"; import { STRINGS } from "src/constants"; import { StringResultObject } from "src/types"; import { sanitizeFilePath } from "src/utils/file-handling"; import { isCommunityPluginEnabled, isCorePluginEnabled, } from "src/utils/plugins"; import { ErrorCode, failure, success } from "src/utils/results-handling"; import { pause } from "src/utils/time"; // TYPES ---------------------------------------- export enum PeriodicNoteType { DailyNote = "daily", WeeklyNote = "weekly", MonthlyNote = "monthly", QuarterlyNote = "quarterly", YearlyNote = "yearly", } export enum PeriodicNoteTypeWithRecents { DailyNote = "daily", WeeklyNote = "weekly", MonthlyNote = "monthly", QuarterlyNote = "quarterly", YearlyNote = "yearly", RecentDailyNote = "recent-daily", RecentWeeklyNote = "recent-weekly", RecentMonthlyNote = "recent-monthly", RecentQuarterlyNote = "recent-quarterly", RecentYearlyNote = "recent-yearly", } // FUNCTIONS ---------------------------------------- export function getCurrentPeriodicNotePath( periodicNoteType: PeriodicNoteType, ): string { let getSettingsFn: Function; switch (periodicNoteType) { case PeriodicNoteType.DailyNote: getSettingsFn = getDailyNoteSettings; break; case PeriodicNoteType.WeeklyNote: getSettingsFn = getWeeklyNoteSettings; break; case PeriodicNoteType.MonthlyNote: getSettingsFn = getMonthlyNoteSettings; break; case PeriodicNoteType.QuarterlyNote: getSettingsFn = getQuarterlyNoteSettings; break; case PeriodicNoteType.YearlyNote: getSettingsFn = getYearlyNoteSettings; break; } const { format, folder } = getSettingsFn(); const title = moment().format(format); return sanitizeFilePath(`${folder}/${title}.md`); } export function getMostRecentPeriodicNotePath( periodicNoteType: PeriodicNoteType, ): StringResultObject { const notes = getAllPeriodicNotes(periodicNoteType); const mostRecentKey = Object.keys(notes).sort().last(); return mostRecentKey ? success(notes[mostRecentKey].path) : failure(ErrorCode.notFound, STRINGS.note_not_found); } export function getCurrentPeriodicNote( periodicNoteType: PeriodicNoteType, ): TFile | undefined { const now = moment(); switch (periodicNoteType) { case PeriodicNoteType.DailyNote: return getDailyNote(now, getAllDailyNotes()); case PeriodicNoteType.WeeklyNote: return getWeeklyNote(now, getAllWeeklyNotes()); case PeriodicNoteType.MonthlyNote: return getMonthlyNote(now, getAllMonthlyNotes()); case PeriodicNoteType.QuarterlyNote: return getQuarterlyNote(now, getAllQuarterlyNotes()); case PeriodicNoteType.YearlyNote: return getYearlyNote(now, getAllYearlyNotes()); } } export function getAllPeriodicNotes( periodicNoteType: PeriodicNoteType, ): Record { switch (periodicNoteType) { case PeriodicNoteType.DailyNote: return getAllDailyNotes(); case PeriodicNoteType.WeeklyNote: return getAllWeeklyNotes(); case PeriodicNoteType.MonthlyNote: return getAllMonthlyNotes(); case PeriodicNoteType.QuarterlyNote: return getAllQuarterlyNotes(); case PeriodicNoteType.YearlyNote: return getAllYearlyNotes(); } } export function checkForEnabledPeriodicNoteFeature( periodicNoteType: PeriodicNoteType, ): boolean { switch (periodicNoteType) { case PeriodicNoteType.DailyNote: return appHasDailyNotesPluginLoaded(); case PeriodicNoteType.WeeklyNote: return appHasWeeklyNotesPluginLoaded(); case PeriodicNoteType.MonthlyNote: return appHasMonthlyNotesPluginLoaded(); case PeriodicNoteType.QuarterlyNote: return appHasQuarterlyNotesPluginLoaded(); case PeriodicNoteType.YearlyNote: return appHasYearlyNotesPluginLoaded(); } } export async function createPeriodicNote( periodicNoteType: PeriodicNoteType, ): Promise { const now = moment(); let newFile: Promise; switch (periodicNoteType) { case PeriodicNoteType.DailyNote: newFile = createDailyNote(now); break; case PeriodicNoteType.WeeklyNote: newFile = createWeeklyNote(now); break; case PeriodicNoteType.MonthlyNote: newFile = createMonthlyNote(now); break; case PeriodicNoteType.QuarterlyNote: newFile = createQuarterlyNote(now); break; case PeriodicNoteType.YearlyNote: newFile = createYearlyNote(now); break; } if ( isCorePluginEnabled("templates") || isCommunityPluginEnabled("templater-obsidian") ) { await pause(500); } return newFile; } /** * Checks if the daily/weekly/monthly/etc periodic note feature is available, * and gets the path to the current related note. * * @returns Successful `StringResultObject` containing the path if the PN * functionality is available and there is a current daily note. Unsuccessful * `StringResultObject` if it isn't. */ export function getExistingPeriodicNotePathIfPluginIsAvailable( periodicNoteType: PeriodicNoteType, ): StringResultObject { if (!checkForEnabledPeriodicNoteFeature(periodicNoteType)) { return failure( ErrorCode.featureUnavailable, STRINGS[`${periodicNoteType}_note`].feature_not_available, ); } const pNote = getCurrentPeriodicNote(periodicNoteType); return pNote ? success(pNote.path) : failure(ErrorCode.notFound, STRINGS.note_not_found); } ================================================ FILE: src/utils/plugins.ts ================================================ import { PluginResultObject } from "src/types"; import { self } from "src/utils/self"; import { ErrorCode, failure, success } from "src/utils/results-handling"; /** * Returns sorted list of the string IDs of the enabled community plugins. * * @returns {string[]} - A sorted list of enabled community plugins. */ function enabledCommunityPlugins(): string[] { const list: string[] = Array.from( self().app.plugins?.enabledPlugins || [], ); return list.sort(); } /** * Checks if a specific community plugin is enabled. * * @param {string} pluginID - The ID of the plugin to check. * * @returns {boolean} - True if the plugin is enabled, false otherwise. */ export function isCommunityPluginEnabled(pluginID: string): boolean { return enabledCommunityPlugins().contains(pluginID); } /** * Gets the enabled community plugin with the specified ID. * * @param {string} pluginID The ID of the community plugin to retrieve. * * @returns {PluginResultObject} A result object containing the plugin if available. */ export function getEnabledCommunityPlugin( pluginID: string, ): PluginResultObject { return isCommunityPluginEnabled(pluginID) ? success(self().app.plugins.getPlugin(pluginID)) : failure( ErrorCode.featureUnavailable, `Community plugin ${pluginID} is not enabled.`, ); } /** * Checks if a specific core plugin is enabled. * * @param {string} pluginID - The ID of the plugin to check. * * @returns {boolean} - True if the plugin is enabled, false otherwise. */ export function isCorePluginEnabled(pluginID: string): boolean { return !!self().app.internalPlugins?.getEnabledPluginById(pluginID); } /** * Gets the enabled core plugin with the specified ID. * * @param {string} pluginID The ID of the core plugin to retrieve. * * @returns {PluginResultObject} A result object containing the plugin if available. */ export function getEnabledCorePlugin(pluginID: string): PluginResultObject { const plugin = self().app.internalPlugins?.getEnabledPluginById(pluginID); return plugin ? success(plugin) : failure( ErrorCode.featureUnavailable, `Core plugin ${pluginID} is not enabled.`, ); } ================================================ FILE: src/utils/results-handling.ts ================================================ import { ErrorObject, ResultObject } from "src/types"; /** * Returns a `ResultObject` based on the passed-in parameters. * * @param result The `ResultObject`'s `result` key * @param processedFilepath Optional, the `ResultObject`'s `processedFilepath` key * @returns A `ResultObject` with the `isSuccess` key set to `true` */ export function success( result: T, processedFilepath?: string, ): ResultObject { return { isSuccess: true, result, processedFilepath }; } /** * Returns an `ErrorObject` based on the passed-in parameters. * * @param errorCode The `ErrorObject`'s `errorCode` key * @param errorMessage The `ErrorObject`'s `errorMessage` key * @returns An `ErrorObject` with the `isSuccess` key set to `false` */ export function failure( errorCode: ErrorCode, errorMessage: string, ): ErrorObject { return { isSuccess: false, errorCode, errorMessage }; } export enum ErrorCode { notFound = 404, pluginUnavailable = 424, featureUnavailable = 424, unableToCreateNote = 400, unableToWrite = 400, invalidInput = 406, noteAlreadyExists = 409, handlerError = 500, unknownError = 500, } ================================================ FILE: src/utils/routing.ts ================================================ import { success } from "src/utils/results-handling"; import { AnyParams, RouteSubpath } from "src/routes"; import { incomingBaseParams } from "src/schemata"; import { HandlerTextSuccess } from "src/types"; import { showBrandedNotice } from "src/utils/ui"; export function helloRoute(path: string = "/"): RouteSubpath { return { path, schema: incomingBaseParams.extend({}), handler: handleHello }; } async function handleHello(data: AnyParams): Promise { showBrandedNotice("… is ready for action 🚀"); return success({ message: "Hello!" }); } ================================================ FILE: src/utils/search.ts ================================================ import { TFile } from "obsidian"; import { getEnabledCommunityPlugin, getEnabledCorePlugin, } from "src/utils/plugins"; import { pause } from "src/utils/time"; import { STRINGS } from "src/constants"; import { OmnisearchAPI, SearchResultObject } from "src/types"; import { self } from "src/utils/self"; import { ErrorCode, failure, success } from "src/utils/results-handling"; /** * Executes a global search for the specified query and returns the search * results (= file paths) as a `SearchResultObject`. * * @param {string} query The search query string. */ export async function doSearch(query: string): Promise { // Get the global search plugin instance const res = getEnabledCorePlugin("global-search"); // If the plugin instance is not available, return an error response if (!res.isSuccess) { return failure( ErrorCode.featureUnavailable, STRINGS.global_search_feature_not_available, ); } // Open the global search panel and wait for it to load const pluginInstance = res.result; pluginInstance.openGlobalSearch(query); const searchLeaf = self().app.workspace.getLeavesOfType("search")[0]; const searchView = await searchLeaf.open(searchLeaf.view); await pause(2000); // Extract the search result hits const rawSearchResult: Map = ( searchView).dom.resultDomLookup; const hits = Array.from(rawSearchResult.keys()).map((tfile) => tfile.path); // Return the search result as a `SearchResultObject` return success({ hits }); } /** * Executes an Omnisearch …search for the specified query and returns the * results (= file paths) as a `SearchResultObject`. * * @param {string} query The search query string. */ export async function doOmnisearch(query: string): Promise { // Get the Omnisearch plugin instance or back off const res = getEnabledCommunityPlugin("omnisearch"); if (!res.isSuccess) { return failure( ErrorCode.featureUnavailable, STRINGS.omnisearch_plugin_not_available, ); } // Execute the Omnisearch query const plugin = res.result.api; const results = await plugin.search(query); const hits = results.map((result) => result.path); // Return the search result as a `SearchResultObject` return success({ hits }); } ================================================ FILE: src/utils/self.ts ================================================ import type ActionsURI from "src/main"; import { RealLifePlugin } from "src/types"; let _self: RealLifePlugin; export function self(): RealLifePlugin; export function self(pluginInstance: ActionsURI): RealLifePlugin; export function self(pluginInstance?: ActionsURI): RealLifePlugin { if (pluginInstance) _self = pluginInstance as unknown as RealLifePlugin; if (!_self) throw new Error("Plugin instance not set"); return _self; } ================================================ FILE: src/utils/string-handling.ts ================================================ import { getFrontMatterInfo } from "obsidian"; import { STRINGS } from "src/constants"; import { RegexResultObject } from "src/types"; import { ErrorCode, failure, success } from "src/utils/results-handling"; /** * Makes sure the passed-in string ends in a newline. * * @param str - The string that should end in a newline * @returns String ending in a newline */ export function endStringWithNewline(str: string = ""): string { return str.endsWith("\n") ? str : `${str}\n`; } /** * Tries to parse a regular expression stored as a string into an actual, real * `RegExp` object. * * @param search - A string containing a full regular expression, e.g. "/^herp/i" * * @returns A `RegexResultObject` object containing either an `error` string or * a `RegExp` object */ export function parseStringIntoRegex(search: string): RegexResultObject { if (!search.startsWith("/")) { return failure(ErrorCode.invalidInput, STRINGS.search_pattern_invalid); } // Starts to look like a regex, let's try to parse it. let re = search.slice(1); const lastSlashIdx = re.lastIndexOf("/"); if (lastSlashIdx === 0) { return failure(ErrorCode.invalidInput, STRINGS.search_pattern_empty); } let searchPattern: RegExp; let flags = re.slice(lastSlashIdx + 1); re = re.slice(0, lastSlashIdx); try { searchPattern = new RegExp(re, flags); } catch (e) { return failure(ErrorCode.invalidInput, STRINGS.search_pattern_unparseable); } return success(searchPattern); } /** * Escapes special characters in a string that are used in regular expressions. * This function is useful when a string is to be treated as a literal pattern * inside a regular expression, rather than as part of the regular expression * syntax. * * @param string - The string to be escaped. * @returns The escaped string, with special regular expression characters prefixed * with a backslash. This makes the string safe to use within a RegExp constructor * or function. */ export function escapeRegExpChars(string: string) { return string.replace(/([.*+?^${}()|[\]\\])/g, "\\$1"); } /** * Extracts front matter and body from a passed-in string. * * @param noteContent - The content of the note to be searched * * @returns An object containing both `frontMatter` and `body` of the note as * keys. When no front matter was found, `frontMatter` will be an empty string * while `note` will contain the input string * * @see {@link https://help.obsidian.md/Advanced+topics/YAML+front+matter | Obsidian's YAML front matter documentation} */ export function extractNoteContentParts( noteContent: string, ): { frontMatter: string; body: string } { const info = getFrontMatterInfo(noteContent); return { frontMatter: info.frontmatter, body: noteContent.slice(info.contentStart), }; } /** * Returns the kebab-cased version of a passed-in string. * * @param text - The text to be turned kebab-case * * @returns Text in kebab-case * * @example "hello you veryNice" -> "hello-you-very-nice" */ export function toKebabCase(text: string): string { return text .replace(/([a-z])([A-Z])/g, "$1-$2") .replace(/\s+/g, "-") .toLowerCase(); } ================================================ FILE: src/utils/time.ts ================================================ export async function pause(milliseconds: number): Promise { return new Promise((resolve) => { setTimeout( () => resolve(), milliseconds, ); }); } ================================================ FILE: src/utils/ui.ts ================================================ import { FileView, Notice, requireApiVersion } from "obsidian"; import { STRINGS } from "src/constants"; import { StringResultObject } from "src/types"; import { getFile } from "src/utils/file-handling"; import { self } from "src/utils/self"; import { ErrorCode, failure, success } from "src/utils/results-handling"; /** * Displays a `Notice` inside Obsidian. The notice is prefixed with * "[Actions URI]" so the sender is clear to the receiving user. * * @param msg - The message to be shown in the notice */ export function showBrandedNotice(msg: string) { new Notice(`[Actions URI] ${msg}`); } /** * Logs anything to the console, prefixed with "[Actions URI]" so the sender is * clear. Standard log level. * * @param data - Anything that can be logged, really */ export function logToConsole(...data: any[]) { console.log("[Actions URI]", ...data); } /** * Logs anything to the console, prefixed with "[Actions URI]" so the sender is * clear. Error log level. * * @param data - Anything that can be logged, really */ export function logErrorToConsole(...data: any[]) { console.error("[Actions URI]", ...data); } /** * Given a file path, the function will check whether the note file is already * open and then focus it, or it'll open the note. * * @param filepath - The path to the file to be focussed or opened * * @returns A positive string result object specifying the action taken */ export async function focusOrOpenNote( filepath: string, ): Promise { // Is this file open already? If so, can we just focus it? const res = await revealLeafWithFilePath(filepath); if (res.isSuccess) { return res; } const res1 = await getFile(filepath); if (res1.isSuccess) { self().app.workspace.getLeaf(true).openFile(res1.result); return success(STRINGS.note_opened); } return failure(ErrorCode.notFound, STRINGS.note_not_found); } /** * Finds an open note with the passed-in filepath. If it's found, it'll be * revealed, otherwise nothing happens. * * @param filepath - The path to the file to be focussed * * @returns Success when note could be found and focussed, error otherwise */ async function revealLeafWithFilePath( filepath: string, ): Promise { for (let leaf of self().app.workspace.getLeavesOfType("markdown")) { // See https://publish.obsidian.md/dev-docs-test/Plugins/Guides/Understanding+deferred+views if (requireApiVersion("1.7.2")) { // @ts-ignore await leaf.loadIfDeferred(); } if (leaf.view instanceof FileView && leaf.view.file?.path === filepath) { await self().app.workspace.revealLeaf(leaf); return success("Open file found and focussed"); } } return failure(ErrorCode.notFound, "File currently not open"); } ================================================ FILE: src/utils/zod.ts ================================================ import { z } from "zod"; import { TAbstractFile, TFile, TFolder } from "obsidian"; import { self } from "src/utils/self"; import { sanitizeFilePath, sanitizeFilePathAndGetAbstractFile, } from "src/utils/file-handling"; import { getEnabledCommunityPlugin, getEnabledCorePlugin, } from "src/utils/plugins"; // The absence of a parameter `blah`, a `blah=false` and a value-less `blah=` // should all be treated as `false`. My reign shall be merciful. export const zodOptionalBoolean = z.preprocess( (param: unknown): boolean => typeof param === "string" && param !== "false" && param !== "", z.boolean().optional(), ); export const zodSanitizedNotePath = z.string() .min(1, { message: "can't be empty" }) .transform((file) => sanitizeFilePath(file)); export const zodSanitizedFilePath = z.string() .min(1, { message: "can't be empty" }) .transform((file) => sanitizeFilePath(file, false)); export const zodSanitizedFolderPath = z.string() .min(1, { message: "can't be empty" }) .transform((file) => sanitizeFilePath(file, false)); /** * A schema which expects a string containing a JSON-encoded array of strings, * and which will return the parsed array of strings. */ export const zodJsonStringArray = z.string() .refine((str) => { try { const value = JSON.parse(str); return Array.isArray(value) && value.every((item) => typeof item === "string"); } catch (error) { return false; } }, { message: "Input must be a JSON-encoded string array.", }) .transform((str) => JSON.parse(str)); /** * A schema which expects a string containing a JSON-encoded object containing * only values of type `string`, `string[]`, `number`, `boolean` or `null`. * Return the object if valid. */ export const zodJsonPropertiesObject = z.string() .refine((str) => { try { const value = JSON.parse(str); if (typeof value !== "object") { return false; } const isValid = Object.values(value) .every((item) => { const type = typeof item; if (["string", "number", "boolean"].includes(type) || item === null) { return true; } if (Array.isArray(item)) { return item.every((subItem) => typeof subItem === "string"); } return false; }); return isValid; } catch (error) { return false; } }, { message: "Input must be a JSON-encoded object containing only values of type string, string array, number, boolean or null.", }) .transform((str) => JSON.parse(str)); /** * A schema which expects a comma-separated list of strings, and which will * return the parsed array of strings. */ export const zodCommaSeparatedStrings = z.string() .min(1, { message: "can't be empty" }) .transform((str) => str.split(",").map((item) => item.trim())); /** * A schema which tests the passed-in string to see if it's a valid path to an * existing template. If it is, returns a `TFile` instance. */ export const zodExistingTemplaterPath = z.preprocess( lookupAbstractFileForTemplaterPath, z.instanceof(TFile, { message: "Template doesn't exist or Templater isn't enabled", }), ); /** * A schema which tests the passed-in string to see if it's a valid path to an * existing template. If it is, returns a `TFile` instance. */ export const zodExistingTemplatesPath = z.preprocess( lookupAbstractFileForTemplatesPath, z.instanceof(TFile, { message: "Template doesn't exist or Templates isn't enabled", }), ); /** * A schema which tests the passed-in string to see if it's a valid path to an * existing file. If it is, returns a `TFile` instance. */ export const zodExistingFilePath = z.preprocess( lookupAbstractFileForFilePath, z.instanceof(TFile, { message: "File doesn't exist" }), ); /** * A schema which tests the passed-in string to see if it's a valid path to an * existing folder. If it is, returns a `TFolder` instance. */ export const zodExistingFolderPath = z.preprocess( lookupAbstractFolderForPath, z.instanceof(TFolder, { message: "Folder doesn't exist" }), ); /** * A schema which expects an undefined value (i.e. no parameter passed in), and * returns a default value instead. * * @param defaultValue The default value to return if the parameter is undefined */ export const zodUndefinedChangedToDefaultValue = (defaultValue: any) => z.undefined() .refine((val) => val === undefined) .transform(() => defaultValue); /** * A schema which expects an empty string, and overwrites it with a given value. * * @param defaultString The default value to return if the parameter is undefined */ export const zodEmptyStringChangedToDefaultString = (defaultString: string) => z.literal("") .refine((val) => val === "") .transform(() => defaultString); // HELPERS ---------------------------------------- /** * Takes an incoming parameter and returns the corresponding `TAbstractFile` if * the parameter is a string and the string corresponds to an existing file or * folder. Otherwise returns `null`. * * @param path Any incoming zod parameter */ function lookupAbstractFileForFilePath(path: any): TAbstractFile | null { return (typeof path === "string" && path.length > 0) ? sanitizeFilePathAndGetAbstractFile(path, false) : null; } /** * Takes an incoming parameter and returns the corresponding `TAbstractFile` if * the parameter is a string and the string corresponds to an existing file or * folder. Otherwise returns `null`. * * @param path Any incoming zod parameter */ function lookupAbstractFolderForPath(path: any): TAbstractFile | null { return (typeof path === "string" && path.length > 0) ? self().app.vault.getFolderByPath(path) : null; } /** * Takes an incoming parameter and returns the corresponding `TAbstractFile` if * the parameter is a string and the string corresponds to an existing template * file. If the passed in path can't be found, the function will also check * Templater's template folder path for the file. Returns `null` when the search * came up empty. * * @param path Any incoming zod parameter * @returns */ function lookupAbstractFileForTemplaterPath(path: any): TAbstractFile | null { if (typeof path !== "string" || !path) { return null; } const abstractFile = sanitizeFilePathAndGetAbstractFile(path, true); if (abstractFile) return abstractFile; const res = getEnabledCommunityPlugin("templater-obsidian"); if (res.isSuccess) { const folder = res.result.settings?.templates_folder; return sanitizeFilePathAndGetAbstractFile(`${folder}/${path}`, true) || sanitizeFilePathAndGetAbstractFile(`${folder}/${path}.md`, true); } return null; } /** * Takes an incoming parameter and returns the corresponding `TAbstractFile` if * the parameter is a string and the string corresponds to an existing template * file. If the passed in path can't be found, the function will also check * Templates' template folder path for the file. Returns `null` when the search * came up empty. * * @param path Any incoming zod parameter * @returns */ function lookupAbstractFileForTemplatesPath(path: any): TAbstractFile | null { if (typeof path !== "string" || !path) { return null; } const abstractFile = sanitizeFilePathAndGetAbstractFile(path, true); if (abstractFile) return abstractFile; const res = getEnabledCorePlugin("templates"); if (res.isSuccess) { const folder = res.result.options?.folder; return sanitizeFilePathAndGetAbstractFile(`${folder}/${path}`, true) || sanitizeFilePathAndGetAbstractFile(`${folder}/${path}.md`, true); } return null; } ================================================ FILE: tests/README.md ================================================ # Test Setup Documentation This document explains the setup and structure of the test environment for the Obsidian Actions URI plugin. ## Prerequisites / Assumptions for Testing The test suite assumes that Obsidian is installed and configured to know about a test vault named "plugin-test-vault" located at `~/tmp/plugin-test-vault`. This vault should be configured as needed for testing various plugin features. A blueprint of this test vault is stored in this repository at `tests/plugin-test-vault.original`. ## Running Tests [Jest](https://jestjs.io) is used as the test runner. The test suite utilizes global setup and teardown scripts to manage the test environment. A test run performs the following steps: ### 1. Global Setup (`tests/global-setup.ts`) - Ensures the `~/tmp` directory exists. - Removes any existing test vault at `~/tmp/plugin-test-vault`. - Copies the blueprint vault from `tests/plugin-test-vault.original` to `~/tmp/plugin-test-vault`. - Ensures the Actions URI plugin is enabled in the copied vault's `community-plugins.json`. - Copies the compiled plugin files (`main.js`, `manifest.json`) into the test vault's plugin directory. - Opens the copied vault in Obsidian using a `obsidian://open` URI. - Starts a local HTTP callback server on port 3000 (`tests/callback-server.ts`). ### 2. Test Execution (`*.test.ts` files) - Jest runs the test files. - Tests interact with the Obsidian plugin by sending `obsidian://actions-uri/…` URIs. - Tests use helper functions (`tests/helpers.ts`) to send URIs and wait for callbacks. ### 3. Global Teardown (`tests/global-teardown.ts`) - Signals Obsidian to close the test vault using a `obsidian://actions-uri/vault/close` URI. - Removes the temporary test vault directory at `~/tmp/plugin-test-vault`. - Stops the local HTTP callback server. **Important:** The original vault at `tests/plugin-test-vault.original` is **not** modified by the tests. ## XCU Call Flow in Tests The test suite simulates user interaction by sending Obsidian Actions URIs. The process involves the following steps: 1. **Initiating the Call:** A test case calls the `callObsidian()` helper function (`tests/helpers.ts`), providing the desired route path and any necessary payload parameters. 2. **URI Construction:** The `callObsidian()` function constructs a full `obsidian://actions-uri/…` URI. This URI includes the test vault name and sets the `x-success` and `x-error` callback parameters to point to the `/success` and `/failure` endpoints of the local callback server running on port 3000. 3. **Sending the URI:** The constructed URI is opened using the `sendUri()` helper function, which uses OS-specific commands (`open`, `start`, `xdg-open`) to trigger Obsidian to handle the URI. 4. **Obsidian Processing:** Obsidian receives the URI and the Actions URI plugin processes the requested route. 5. **Sending Callback:** Based on the outcome of processing the route, the Actions URI plugin sends an HTTP GET request to either the `x-success` or `x-error` URL specified in the original URI. Depending on the environment, that request is sent either using `window.open()` (live) or `fetch()` (testing). (See `sendCallbackResult()` in `src/utils/callbacks.ts`.) 6. **Receiving Callback:** The local callback server (`tests/callback-server.ts`) receives the HTTP request on either the `/success` or `/failure` endpoint. 7. **Resolving Promise:** The `waitForCallback()` method in the `CallbackServer` instance, which `callObsidian()` is awaiting, receives the data from the incoming request and resolves the promise. 8. **Processing Result:** The `callObsidian()` function receives the data from the resolved promise, determines if it was a success or failure callback based on the received data structure, and returns a `Result` object (`tests/types.d.ts`) containing the outcome. 9. **Assertions:** The test case then uses the returned `Result` object to make assertions about the success or failure of the call and the received data. ## Structure of the Test Vault and Test Files Test files (`*.test.ts`) and their related Markdown notes (`*.md`) are organized within the `tests/plugin-test-vault.original/` directory. The folder structure within `plugin-test-vault.original` mirrors the Actions URI routes being tested. For example, files related to testing the `/note/get` route are located in `tests/plugin-test-vault.original/note/get/`. This directory contains the test file (`noteGet.test.ts`) and any Markdown notes (`.md` files) required for those specific tests. ``` tests/ plugin-test-vault.original/ note/ get/ noteGet.test.ts // Test cases for the `/note/get` route first-note.md // Markdown note used in noteGet.test.ts second-note.md // Another Markdown note used in noteGet.test.ts dataview/ list/ dataviewList.test.ts // Test cases for the `/dataview/list` route // … any necessary files for `dataview/list` tests // … other routes ``` The test files are typically named after the route they are testing (e.g., `noteGet.test.ts` for the `/note/get` route). ### Plugins The vault is preconfigured with the following community plugins: - Actions URI: The plugin being tested, built and copied into the test vault during setup. - [Dataview](https://github.com/blacksmithgu/obsidian-dataview): A data index and query language over Markdown files. - [Logstravaganza](https://github.com/czottmann/obsidian-logstravaganza): Captures developer tool console logs into a `.ndjson` file in the vault's root directory. This is useful for debugging and understanding the flow of the plugin during tests. - [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes): For managing periodic notes. - [Templater](https://github.com/SilentVoid13/Templater): For creating and managing templates. ## Key Components - **`tests/plugin-test-vault.original/`**: The blueprint of the Obsidian vault used for testing. Copied to a temporary location before each test run. - **`tests/global-setup.ts`**: Jest global setup script. Handles creating and configuring the temporary test vault and starting the callback server. - **`tests/global-teardown.ts`**: Jest global teardown script. Handles cleaning up the temporary test vault and stopping the callback server. - **`tests/callback-server.ts`**: A simple HTTP server that listens for `x-success` (`/success`) and `x-error` (`/failure`) callbacks from the Obsidian plugin. - **`tests/helpers.ts`**: Contains helper functions for the tests, including `sendUri` (to open Obsidian URIs) and `callObsidian` (to send an Actions URI and wait for a callback, returning a `Result` type). - **`tests/types.d.ts`**: Defines custom types used in the tests, such as the `Result` type for handling success and error outcomes. ## TODO - [ ] add function for looking up files in the vault folder which correlates to the route being tested ================================================ FILE: tests/callback-server.ts ================================================ import * as http from "http"; import { URL } from "url"; import { CallbackData } from "./types"; const TEST_PORT = 3000; export class CallbackServer { private server: http.Server; private callbackData: CallbackData | null = null; private resolve: ((data: CallbackData) => void) | null = null; private reject: ((error: Error) => void) | null = null; public baseURL: string = `http://localhost:${TEST_PORT}`; constructor() { this.server = http.createServer(async (req, res) => { const url = new URL(req.url || "/", this.baseURL); const params = Object.fromEntries(url.searchParams.entries()); if (url.pathname.startsWith("/success")) { this.callbackData = { success: params }; if (this.resolve) { this.resolve(this.callbackData); this.reset(); } res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Success callback received"); } else if (url.pathname.startsWith("/failure")) { this.callbackData = { error: params }; if (this.resolve) { this.resolve(this.callbackData); this.reset(); } res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Failure callback received"); } else { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not Found"); } }); } start(): Promise { return new Promise((resolve, reject) => { this.server.listen(TEST_PORT, () => { console.log(`- Callback server listening on port ${TEST_PORT}`); resolve(); }); this.server.on("error", reject); }); } stop(): Promise { return new Promise((resolve, reject) => { this.server.close((err) => { if (err) { reject(err); } else { console.log("- Callback server stopped"); resolve(); } }); }); } waitForCallback(timeout = 3000): Promise { return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; const timer = setTimeout(() => { this.reset(); reject(new Error("Callback timeout")); }, timeout); // Override resolve and reject to clear the timer const originalResolve = resolve; const originalReject = reject; this.resolve = (data) => { clearTimeout(timer); originalResolve(data); this.reset(); // Reset after resolving }; this.reject = (error) => { clearTimeout(timer); originalReject(error); this.reset(); // Reset after rejecting }; }); } private reset() { this.callbackData = null; this.resolve = null; this.reject = null; } } ================================================ FILE: tests/global-setup.ts ================================================ import * as fs from "fs/promises"; import * as path from "path"; import * as os from "os"; import chokidar from "chokidar"; import { CallbackServer } from "./callback-server"; import { asyncExec, pause } from "./helpers"; import { id as pluginID } from "../manifest.json"; /** * The name of the vault used for testing. The value of the constant is the same * as the "blueprint" test vault stored in the `__tests__/` folder, sans the * extension, i.e. `plugin-test-vault` (value) instead of * `plugin-test-vault.original` (the folder). * * This constant is used in setting up the actual test vault (see the * `setup-vault.ts` script), and for deciding how XCU callbacks are made (see * `src/utils/callbacks.ts`). */ const testVaultName = "plugin-test-vault"; /** * Sets up a temporary Obsidian vault for testing purposes. * * This function performs the following steps: * 1. Ensures the parent directory for the test vault exists. * 2. Removes any existing test vault at the target location. * 3. Copies a blueprint vault to the test location. * 4. Ensures the plugin is enabled in the vault's `community-plugins.json`. * 5. Copies the compiled plugin files into the vault's plugin directory. * 6. Opens the copied vault in Obsidian. * 7. Starts the global callback server. */ export default async function globalSetup() { console.log("\nSetting up test vault…"); const blueprintVaultPath = path.join(__dirname, `${testVaultName}.original`); const testVaultDir = path.join(os.homedir(), "tmp"); const testVaultPath = path.join(testVaultDir, testVaultName); const obsidianDir = path.join(testVaultPath, ".obsidian"); const pluginDir = path.join(obsidianDir, "plugins", pluginID); // Ensure the parent directory for the test vault exists console.log("- Creating temp vault…"); await createTestVault(testVaultDir, testVaultPath, blueprintVaultPath); // Ensure the plugin is in the community-plugins.json and compiled files are copied console.log(`- Ensuring ${pluginID} plugin is enabled…`); await ensureTestPluginIsEnabled(pluginDir, obsidianDir); console.log("- Copying new plugin build into test vault…"); copyNewPluginBuildIntoTestVault(pluginDir); console.log(`- Opening test vault in Obsidian…`); await openTestVaultInObsidian(); console.log(`- Starting the global callback server…`); const httpServer = await startHTTPServer(); console.log("- Finding NDJSON console log file and setting up watcher…"); const consoleLogFile = await locateLogstravaganzaLogFile(testVaultPath); console.log(`- Watching ${consoleLogFile} for new log entries…`); const { logPath, logWatcher } = await startLogFileWatcher( testVaultPath, consoleLogFile, ); global.httpServer = httpServer; global.testVault = { logPath, logRows: [], logWatcher, name: testVaultName, path: testVaultPath, }; console.log("Test vault set up!\n"); } async function startLogFileWatcher( testVaultPath: string, consoleLogFile: string, ) { const logPath = path.join(testVaultPath, consoleLogFile); // Use polling as the file might be written to by another process const logWatcher = chokidar.watch(logPath, { persistent: true, usePolling: true, interval: 50, }); // `lastSize` keeps track of the last read position, so we can read only new // lines, starting from the moment the vault setup is complete. let lastSize = (await fs.stat(logPath)).size; logWatcher.on("change", async () => { try { const stats = await fs.stat(logPath); const currentSize = stats.size; if (currentSize > lastSize) { const fileHandle = await fs.open(logPath, "r"); const buffer = Buffer.alloc(currentSize - lastSize); await fileHandle.read(buffer, 0, currentSize - lastSize, lastSize); await fileHandle.close(); const newContent = buffer.toString(); const lines = newContent.split("\n").filter((line) => line.length); global.testVault.logRows.push(...lines); lastSize = currentSize; } } catch (e) { console.error("Error reading new lines from console log file:", e); } }); return { logPath, logWatcher }; } async function locateLogstravaganzaLogFile( testVaultPath: string, ): Promise { const vaultFiles = await fs.readdir(testVaultPath); const consoleLogFile = vaultFiles.find((file) => file.endsWith(".ndjson")); if (!consoleLogFile) { throw new Error(`No NDJSON file found in test vault at ${testVaultPath}`); } return consoleLogFile; } async function startHTTPServer() { const httpServer = new CallbackServer(); await httpServer.start(); return httpServer; } /** * Open the vault in Obsidian and give it a moment to load. */ async function openTestVaultInObsidian() { await asyncExec(`open "obsidian://open?vault=${testVaultName}"`); await pause(2000); } /** * Copy the compiled plugin files from the project root to the vault's plugin * directory. */ function copyNewPluginBuildIntoTestVault(pluginDir: string) { ["main.js", "manifest.json"] .forEach(async (file) => { try { await fs.copyFile(file, path.join(pluginDir, file)); } catch (e) { throw new Error(`Failed to copy ${file}: ${e}`); } }); } async function ensureTestPluginIsEnabled( pluginDir: string, obsidianDir: string, ) { await fs.mkdir(pluginDir, { recursive: true }); // Update community-plugins.json to ensure the plugin is enabled const communityPluginsPath = path.join(obsidianDir, "community-plugins.json"); let communityPlugins: string[] = []; try { const content = await fs.readFile(communityPluginsPath, "utf-8"); communityPlugins = JSON.parse(content); } catch (e) { // File might not exist, start with empty array } if (!communityPlugins.includes(pluginID)) { communityPlugins.push(pluginID); await fs.writeFile(communityPluginsPath, JSON.stringify(communityPlugins)); } } async function createTestVault( testVaultDir: string, testVaultPath: string, blueprintVaultPath: string, ) { await fs.mkdir(testVaultDir, { recursive: true }); // Remove existing test vault if it exists try { await fs.rm(testVaultPath, { recursive: true, force: true }); } catch (e) { // Ignore if it doesn't exist } // Copy the blueprint vault await fs.cp(blueprintVaultPath, testVaultPath, { recursive: true }); } ================================================ FILE: tests/global-teardown.ts ================================================ import * as fs from "fs/promises"; import { asyncExec, pause } from "./helpers"; /** * Tears down (removes) the specified test vault directory. */ export default async function globalTeardown() { console.log("\nTearing down test vault…"); await asyncExec( `open "obsidian://actions-uri/vault/close?vault=${global.testVault.name}"`, ); console.log("- Signalled Obsidian to close the vault…"); // Wait for a moment to ensure the vault is closed, including all open files await pause(500); // Close the console log watcher if (global.testVault.logWatcher) { await global.testVault.logWatcher.close(); console.log("- Quit the console log watcher"); } if (global.testVault.path) { // We don't remove the parent directory anymore, only the vault itself await fs.rm(global.testVault.path, { recursive: true, force: true }); console.log(`- Removed temp vault at ${global.testVault.path}`); } else { console.warn("- No vault path found in `global.testVault.path`!"); } if (global.httpServer) { await global.httpServer.stop(); console.log("- Stopped HTTP callback server"); } else { console.warn("- No HTTP server instance found!"); } } ================================================ FILE: tests/helpers.ts ================================================ import { exec } from "child_process"; import { randomUUID } from "crypto"; import { platform } from "os"; import { promisify } from "util"; import { LogEntry, Result } from "./types"; export const asyncExec = promisify(exec); export function sendUri(uri: string): Promise { let command: string; const osType = platform(); switch (osType) { // macOS case "darwin": command = `open "${uri}"`; break; // Windows case "win32": command = `start "" "${uri}"`; break; // Linux case "linux": command = `xdg-open "${uri}"`; break; default: return Promise.reject(new Error(`Unsupported OS: ${osType}`)); } return new Promise((resolve, reject) => { exec( command, (error) => error ? reject(error) : resolve(), ); }); } /** A simple wait-for-n-ms function. */ export async function pause(milliseconds: number): Promise { return new Promise((resolve) => { setTimeout( () => resolve(), milliseconds, ); }); } /** * Calls an Obsidian Actions URI endpoint and waits for a callback from the test callback server. * * This function constructs an Obsidian URI with the specified route path and payload parameters. * It automatically includes the required 'vault', 'x-success', and 'x-error' parameters, * setting 'x-success' to the '/success' endpoint and 'x-error' to the '/failure' endpoint * of the local test callback server (http://localhost:3000). * * After sending the URI to Obsidian, the function waits for a response from the callback server. * The response is returned as a `Result` object, containing either the success value (`ok: true`) * or an error object (`ok: false`), based on which callback endpoint was invoked by the Obsidian plugin. * * @template T - The expected type of the success value. * @template E - The expected type of the error object. * * @param path - The route path of the Actions URI endpoint to call (e.g., "note/get", "file/create"). * @param payload - An optional object containing key-value pairs for the endpoint's URL parameters. * @param timeout - The maximum time to wait for a callback from the server (default: 3000 ms). * @returns A Promise that resolves with a `Result` object. * - If the '/success' callback is received, `result.ok` is true and `result.value` contains the received data. * - If the '/failure' callback is received, `result.ok` is false and `result.error` contains the received error data. * - If a timeout or unknown error occurs, `result.ok` is false and `result.error` contains the error. */ export async function callObsidian( path: string, payload: Record = {}, timeout: number = 3000, ): Promise> { const cbServer = global.httpServer!; const uri = constructObsidianURI(path, payload); const cbPromise = cbServer.waitForCallback(timeout); await sendUri(uri); let callbackRes; try { callbackRes = await cbPromise; } catch (error) { // Handle timeout or other errors from `waitForCallback()` return { ok: false, error: error as E, log: await collectRecentLogEntries(), }; } const logEntries = await collectRecentLogEntries(); if (callbackRes.success) { try { // Attempt to parse success data if it's a JSON string const parsedValue = JSON.parse(callbackRes.success); return { ok: true, value: parsedValue as T, log: logEntries }; } catch (e) { // If parsing fails, return the raw string return { ok: true, value: callbackRes.success as T, log: logEntries }; } } else if (callbackRes.error) { // Assuming error data is always an object with errorCode and errorMessage return { ok: false, error: callbackRes.error as E, log: logEntries }; } // Should not happen if `waitForCallback()` works correctly return { ok: false, error: new Error("Unknown callback data received") as E, log: logEntries, }; } /** * Collects recent log entries from the test vault and clears the log rows. * This function is used to gather log entries after a callback is received. * * @returns A Promise that resolves with an array of log entries. */ async function collectRecentLogEntries(): Promise { await pause(100); const logEntries = global.testVault.logRows.map((l) => JSON.parse(l)); global.testVault.logRows = []; return logEntries; } /** * Constructs an Obsidian URI with the specified route path and payload parameters. * Automatically includes the required 'vault', 'x-success', and 'x-error' parameters. * * @param path - The route path of the Actions URI endpoint to call (e.g., "note/get", "file/create"). * @param payload - An object containing key-value pairs for the endpoint's URL parameters. * @returns A string representing the constructed Obsidian URI. */ function constructObsidianURI( path: string, payload: Record, ): string { // Generate a unique identifier for the callback so it's easier to track const uuid = randomUUID(); const cbServer = global.httpServer; const url = new URL(`obsidian://actions-uri/${path}`); // Set required parameters url.searchParams.set("vault", global.testVault.name); // Allow for custom x-success parameter, even if rarely used if ( Object.hasOwn(payload, "x-success") && typeof payload["x-success"] !== "undefined" ) { url.searchParams.set("x-success", payload["x-success"]); } else { url.searchParams.set("x-success", `${cbServer.baseURL}/success/${uuid}`); } // Allow for custom x-error parameter, even if rarely used if ( Object.hasOwn(payload, "x-error") && typeof payload["x-error"] !== "undefined" ) { url.searchParams.set("x-error", payload["x-error"]); } else { url.searchParams.set("x-error", `${cbServer.baseURL}/failure/${uuid}`); } // Add payload parameters for (const key in payload) { if (Object.prototype.hasOwnProperty.call(payload, key)) { url.searchParams.set(key, String(payload[key])); } } return url.toString(); } ================================================ FILE: tests/periodic-notes.ts ================================================ import * as moment from "moment"; import { PeriodicNoteSet, RecentPeriodicNoteSet } from "#tests/types.d"; export const periodicNotes: PeriodicNoteSet[] = [ { key: "daily", dateString: moment().format("YYYY-MM-DD") }, { key: "weekly", dateString: moment().format("gggg-[W]ww") }, { key: "monthly", dateString: moment().format("YYYY-MM") }, { key: "quarterly", dateString: moment().format("YYYY-[Q]Q") }, { key: "yearly", dateString: moment().format("YYYY") }, ]; export const recentPeriodicNotes: RecentPeriodicNoteSet[] = [ { key: "recent-daily", dateString: "2025-05-18" }, { key: "recent-weekly", dateString: "2025-W20" }, { key: "recent-monthly", dateString: "2025-04" }, { key: "recent-quarterly", dateString: "2025-Q1" }, { key: "recent-yearly", dateString: "2024" }, ]; ================================================ FILE: tests/plugin-test-vault.original/.obsidian/app.json ================================================ {} ================================================ FILE: tests/plugin-test-vault.original/.obsidian/appearance.json ================================================ {} ================================================ FILE: tests/plugin-test-vault.original/.obsidian/community-plugins.json ================================================ [ "logstravaganza", "periodic-notes", "auto-periodic-notes", "templater-obsidian", "actions-uri" ] ================================================ FILE: tests/plugin-test-vault.original/.obsidian/core-plugins.json ================================================ { "file-explorer": true, "global-search": false, "switcher": false, "graph": false, "backlink": false, "canvas": false, "outgoing-link": false, "tag-pane": false, "properties": false, "page-preview": false, "daily-notes": true, "templates": true, "note-composer": false, "command-palette": false, "slash-command": false, "editor-status": true, "bookmarks": false, "markdown-importer": false, "zk-prefixer": false, "random-note": false, "outline": false, "word-count": false, "slides": false, "audio-recorder": false, "workspaces": false, "file-recovery": false, "publish": false, "sync": false, "webviewer": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/graph.json ================================================ { "collapse-filter": true, "search": "", "showTags": false, "showAttachments": false, "hideUnresolved": false, "showOrphans": true, "collapse-color-groups": true, "colorGroups": [], "collapse-display": true, "showArrow": false, "textFadeMultiplier": 0, "nodeSizeMultiplier": 1, "lineSizeMultiplier": 1, "collapse-forces": true, "centerStrength": 0.518713248970312, "repelStrength": 10, "linkStrength": 1, "linkDistance": 250, "scale": 1, "close": true } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/actions-uri/manifest.json ================================================ { "id": "actions-uri", "name": "Actions URI", "version": "1.7.3", "minAppVersion": "1.5.11", "description": "Adds additional `x-callback-url` endpoints to the app for common actions — it's a clean, super-charged addition to Obsidian URI.", "author": "Carlo Zottmann", "authorUrl": "https://github.com/czottmann", "isDesktopOnly": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/auto-periodic-notes/data.json ================================================ { "alwaysOpen": false, "daily": { "available": true, "enabled": true, "closeExisting": false, "openAndPin": false, "excludeWeekends": false }, "weekly": { "available": true, "enabled": true, "closeExisting": false, "openAndPin": false }, "monthly": { "available": true, "enabled": true, "closeExisting": false, "openAndPin": false }, "quarterly": { "available": true, "enabled": true, "closeExisting": false, "openAndPin": false }, "yearly": { "available": true, "enabled": true, "closeExisting": false, "openAndPin": false } } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/auto-periodic-notes/main.js ================================================ /* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the GitHub repository of this plugin. */ var Ce=Object.create;var B=Object.defineProperty;var We=Object.getOwnPropertyDescriptor;var Ye=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Ue=Object.prototype.hasOwnProperty;var qe=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),He=(n,e)=>{for(var t in e)B(n,t,{get:e[t],enumerable:!0})},le=(n,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ye(e))!Ue.call(n,i)&&i!==t&&B(n,i,{get:()=>e[i],enumerable:!(o=We(e,i))||o.enumerable});return n};var x=(n,e,t)=>(t=n!=null?Ce(Re(n)):{},le(e||!n||!n.__esModule?B(t,"default",{value:n,enumerable:!0}):t,n)),$e=n=>le(B({},"__esModule",{value:!0}),n);var E=qe(r=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var d=require("obsidian"),te="YYYY-MM-DD",ne="gggg-[W]ww",de="YYYY-MM",ue="YYYY-[Q]Q",pe="YYYY";function _(n){var t,o;let e=window.app.plugins.getPlugin("periodic-notes");return e&&((o=(t=e.settings)==null?void 0:t[n])==null?void 0:o.enabled)}function M(){var n,e,t,o;try{let{internalPlugins:i,plugins:a}=window.app;if(_("daily")){let{format:l,folder:m,template:f}=((e=(n=a.getPlugin("periodic-notes"))==null?void 0:n.settings)==null?void 0:e.daily)||{};return{format:l||te,folder:(m==null?void 0:m.trim())||"",template:(f==null?void 0:f.trim())||""}}let{folder:s,format:c,template:u}=((o=(t=i.getPluginById("daily-notes"))==null?void 0:t.instance)==null?void 0:o.options)||{};return{format:c||te,folder:(s==null?void 0:s.trim())||"",template:(u==null?void 0:u.trim())||""}}catch(i){console.info("No custom daily note settings found!",i)}}function L(){var n,e,t,o,i,a,s;try{let c=window.app.plugins,u=(n=c.getPlugin("calendar"))==null?void 0:n.options,l=(t=(e=c.getPlugin("periodic-notes"))==null?void 0:e.settings)==null?void 0:t.weekly;if(_("weekly"))return{format:l.format||ne,folder:((o=l.folder)==null?void 0:o.trim())||"",template:((i=l.template)==null?void 0:i.trim())||""};let m=u||{};return{format:m.weeklyNoteFormat||ne,folder:((a=m.weeklyNoteFolder)==null?void 0:a.trim())||"",template:((s=m.weeklyNoteTemplate)==null?void 0:s.trim())||""}}catch(c){console.info("No custom weekly note settings found!",c)}}function C(){var e,t,o,i;let n=window.app.plugins;try{let a=_("monthly")&&((t=(e=n.getPlugin("periodic-notes"))==null?void 0:e.settings)==null?void 0:t.monthly)||{};return{format:a.format||de,folder:((o=a.folder)==null?void 0:o.trim())||"",template:((i=a.template)==null?void 0:i.trim())||""}}catch(a){console.info("No custom monthly note settings found!",a)}}function W(){var e,t,o,i;let n=window.app.plugins;try{let a=_("quarterly")&&((t=(e=n.getPlugin("periodic-notes"))==null?void 0:e.settings)==null?void 0:t.quarterly)||{};return{format:a.format||ue,folder:((o=a.folder)==null?void 0:o.trim())||"",template:((i=a.template)==null?void 0:i.trim())||""}}catch(a){console.info("No custom quarterly note settings found!",a)}}function Y(){var e,t,o,i;let n=window.app.plugins;try{let a=_("yearly")&&((t=(e=n.getPlugin("periodic-notes"))==null?void 0:e.settings)==null?void 0:t.yearly)||{};return{format:a.format||pe,folder:((o=a.folder)==null?void 0:o.trim())||"",template:((i=a.template)==null?void 0:i.trim())||""}}catch(a){console.info("No custom yearly note settings found!",a)}}function ge(...n){let e=[];for(let o=0,i=n.length;o{let Z=o(),ee=n.clone().set({hour:Z.get("hour"),minute:Z.get("minute"),second:Z.get("second")});return T&&ee.add(parseInt(h,10),g),p?ee.format(p.substring(1).trim()):ee.format(a)}).replace(/{{\s*yesterday\s*}}/gi,n.clone().subtract(1,"day").format(a)).replace(/{{\s*tomorrow\s*}}/gi,n.clone().add(1,"d").format(a)));return e.foldManager.save(f,u),f}catch(f){console.error(`Failed to create file: '${m}'`,f),new d.Notice("Unable to create new file.")}}function Be(n,e){var t;return(t=e[N(n,"day")])!=null?t:null}function Ge(){let{vault:n}=window.app,{folder:e}=M(),t=n.getAbstractFileByPath(d.normalizePath(e));if(!t)throw new oe("Failed to find daily notes folder");let o={};return d.Vault.recurseChildren(t,i=>{if(i instanceof d.TFile){let a=D(i,"day");if(a){let s=N(a,"day");o[s]=i}}}),o}var ie=class extends Error{};function Je(){let{moment:n}=window,e=n.localeData()._week.dow,t=["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];for(;e;)t.push(t.shift()),e--;return t}function Ke(n){return Je().indexOf(n.toLowerCase())}async function he(n){let{vault:e}=window.app,{template:t,format:o,folder:i}=L(),[a,s]=await A(t),c=n.format(o),u=await R(i,c);try{let l=await e.create(u,a.replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,(m,f,P,b,T,h)=>{let g=window.moment(),p=n.clone().set({hour:g.get("hour"),minute:g.get("minute"),second:g.get("second")});return P&&p.add(parseInt(b,10),T),h?p.format(h.substring(1).trim()):p.format(o)}).replace(/{{\s*title\s*}}/gi,c).replace(/{{\s*time\s*}}/gi,window.moment().format("HH:mm")).replace(/{{\s*(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\s*:(.*?)}}/gi,(m,f,P)=>{let b=Ke(f);return n.weekday(b).format(P.trim())}));return window.app.foldManager.save(l,s),l}catch(l){console.error(`Failed to create file: '${u}'`,l),new d.Notice("Unable to create new file.")}}function Xe(n,e){var t;return(t=e[N(n,"week")])!=null?t:null}function Ze(){let n={};if(!Ne())return n;let{vault:e}=window.app,{folder:t}=L(),o=e.getAbstractFileByPath(d.normalizePath(t));if(!o)throw new ie("Failed to find weekly notes folder");return d.Vault.recurseChildren(o,i=>{if(i instanceof d.TFile){let a=D(i,"week");if(a){let s=N(a,"week");n[s]=i}}}),n}var ae=class extends Error{};async function we(n){let{vault:e}=window.app,{template:t,format:o,folder:i}=C(),[a,s]=await A(t),c=n.format(o),u=await R(i,c);try{let l=await e.create(u,a.replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,(m,f,P,b,T,h)=>{let g=window.moment(),p=n.clone().set({hour:g.get("hour"),minute:g.get("minute"),second:g.get("second")});return P&&p.add(parseInt(b,10),T),h?p.format(h.substring(1).trim()):p.format(o)}).replace(/{{\s*date\s*}}/gi,c).replace(/{{\s*time\s*}}/gi,window.moment().format("HH:mm")).replace(/{{\s*title\s*}}/gi,c));return window.app.foldManager.save(l,s),l}catch(l){console.error(`Failed to create file: '${u}'`,l),new d.Notice("Unable to create new file.")}}function et(n,e){var t;return(t=e[N(n,"month")])!=null?t:null}function tt(){let n={};if(!Pe())return n;let{vault:e}=window.app,{folder:t}=C(),o=e.getAbstractFileByPath(d.normalizePath(t));if(!o)throw new ae("Failed to find monthly notes folder");return d.Vault.recurseChildren(o,i=>{if(i instanceof d.TFile){let a=D(i,"month");if(a){let s=N(a,"month");n[s]=i}}}),n}var re=class extends Error{};async function nt(n){let{vault:e}=window.app,{template:t,format:o,folder:i}=W(),[a,s]=await A(t),c=n.format(o),u=await R(i,c);try{let l=await e.create(u,a.replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,(m,f,P,b,T,h)=>{let g=window.moment(),p=n.clone().set({hour:g.get("hour"),minute:g.get("minute"),second:g.get("second")});return P&&p.add(parseInt(b,10),T),h?p.format(h.substring(1).trim()):p.format(o)}).replace(/{{\s*date\s*}}/gi,c).replace(/{{\s*time\s*}}/gi,window.moment().format("HH:mm")).replace(/{{\s*title\s*}}/gi,c));return window.app.foldManager.save(l,s),l}catch(l){console.error(`Failed to create file: '${u}'`,l),new d.Notice("Unable to create new file.")}}function ot(n,e){var t;return(t=e[N(n,"quarter")])!=null?t:null}function it(){let n={};if(!be())return n;let{vault:e}=window.app,{folder:t}=W(),o=e.getAbstractFileByPath(d.normalizePath(t));if(!o)throw new re("Failed to find quarterly notes folder");return d.Vault.recurseChildren(o,i=>{if(i instanceof d.TFile){let a=D(i,"quarter");if(a){let s=N(a,"quarter");n[s]=i}}}),n}var se=class extends Error{};async function at(n){let{vault:e}=window.app,{template:t,format:o,folder:i}=Y(),[a,s]=await A(t),c=n.format(o),u=await R(i,c);try{let l=await e.create(u,a.replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,(m,f,P,b,T,h)=>{let g=window.moment(),p=n.clone().set({hour:g.get("hour"),minute:g.get("minute"),second:g.get("second")});return P&&p.add(parseInt(b,10),T),h?p.format(h.substring(1).trim()):p.format(o)}).replace(/{{\s*date\s*}}/gi,c).replace(/{{\s*time\s*}}/gi,window.moment().format("HH:mm")).replace(/{{\s*title\s*}}/gi,c));return window.app.foldManager.save(l,s),l}catch(l){console.error(`Failed to create file: '${u}'`,l),new d.Notice("Unable to create new file.")}}function rt(n,e){var t;return(t=e[N(n,"year")])!=null?t:null}function st(){let n={};if(!Te())return n;let{vault:e}=window.app,{folder:t}=Y(),o=e.getAbstractFileByPath(d.normalizePath(t));if(!o)throw new se("Failed to find yearly notes folder");return d.Vault.recurseChildren(o,i=>{if(i instanceof d.TFile){let a=D(i,"year");if(a){let s=N(a,"year");n[s]=i}}}),n}function lt(){var o,i;let{app:n}=window,e=n.internalPlugins.plugins["daily-notes"];if(e&&e.enabled)return!0;let t=n.plugins.getPlugin("periodic-notes");return t&&((i=(o=t.settings)==null?void 0:o.daily)==null?void 0:i.enabled)}function Ne(){var t,o;let{app:n}=window;if(n.plugins.getPlugin("calendar"))return!0;let e=n.plugins.getPlugin("periodic-notes");return e&&((o=(t=e.settings)==null?void 0:t.weekly)==null?void 0:o.enabled)}function Pe(){var t,o;let{app:n}=window,e=n.plugins.getPlugin("periodic-notes");return e&&((o=(t=e.settings)==null?void 0:t.monthly)==null?void 0:o.enabled)}function be(){var t,o;let{app:n}=window,e=n.plugins.getPlugin("periodic-notes");return e&&((o=(t=e.settings)==null?void 0:t.quarterly)==null?void 0:o.enabled)}function Te(){var t,o;let{app:n}=window,e=n.plugins.getPlugin("periodic-notes");return e&&((o=(t=e.settings)==null?void 0:t.yearly)==null?void 0:o.enabled)}function ct(n){let e={day:M,week:L,month:C,quarter:W,year:Y}[n];return e()}function dt(n,e){return{day:ye,month:we,week:he}[n](e)}r.DEFAULT_DAILY_NOTE_FORMAT=te;r.DEFAULT_MONTHLY_NOTE_FORMAT=de;r.DEFAULT_QUARTERLY_NOTE_FORMAT=ue;r.DEFAULT_WEEKLY_NOTE_FORMAT=ne;r.DEFAULT_YEARLY_NOTE_FORMAT=pe;r.appHasDailyNotesPluginLoaded=lt;r.appHasMonthlyNotesPluginLoaded=Pe;r.appHasQuarterlyNotesPluginLoaded=be;r.appHasWeeklyNotesPluginLoaded=Ne;r.appHasYearlyNotesPluginLoaded=Te;r.createDailyNote=ye;r.createMonthlyNote=we;r.createPeriodicNote=dt;r.createQuarterlyNote=nt;r.createWeeklyNote=he;r.createYearlyNote=at;r.getAllDailyNotes=Ge;r.getAllMonthlyNotes=tt;r.getAllQuarterlyNotes=it;r.getAllWeeklyNotes=Ze;r.getAllYearlyNotes=st;r.getDailyNote=Be;r.getDailyNoteSettings=M;r.getDateFromFile=D;r.getDateFromPath=Ve;r.getDateUID=N;r.getMonthlyNote=et;r.getMonthlyNoteSettings=C;r.getPeriodicNoteSettings=ct;r.getQuarterlyNote=ot;r.getQuarterlyNoteSettings=W;r.getTemplateInfo=A;r.getWeeklyNote=Xe;r.getWeeklyNoteSettings=L;r.getYearlyNote=rt;r.getYearlyNoteSettings=Y});var pt={};He(pt,{default:()=>K});module.exports=$e(pt);var X=require("obsidian");var ce="auto-periodic-notes:settings-updated";var G=require("obsidian");var Oe=require("obsidian"),S=x(E());var w=class{};var Se="day",U=class extends w{constructor(){super(...arguments);this.date=(0,Oe.moment)()}getAllPaths(){let t=(0,S.getAllDailyNotes)();return Object.entries(t).map(([o,i])=>i.path)}isPresent(){let t=this.date.clone().startOf(Se),o=(0,S.getAllDailyNotes)();return!!(0,S.getDailyNote)(t,o)}async create(){let t=this.date.clone().startOf(Se);return(0,S.createDailyNote)(t)}getCurrent(){return(0,S.getDailyNote)(this.date,(0,S.getAllDailyNotes)())}};var ke=require("obsidian"),O=x(E());var Fe="month",q=class extends w{constructor(){super(...arguments);this.date=(0,ke.moment)()}getAllPaths(){let t=(0,O.getAllMonthlyNotes)();return Object.entries(t).map(([o,i])=>i.path)}isPresent(){let t=this.date.clone().startOf(Fe),o=(0,O.getAllMonthlyNotes)();return!!(0,O.getMonthlyNote)(t,o)}async create(){let t=this.date.clone().startOf(Fe);return(0,O.createMonthlyNote)(t)}getCurrent(){return(0,O.getMonthlyNote)(this.date,(0,O.getAllMonthlyNotes)())}};var ve=require("obsidian"),F=x(E());var Ie="quarter",H=class extends w{constructor(){super(...arguments);this.date=(0,ve.moment)()}getAllPaths(){let t=(0,F.getAllQuarterlyNotes)();return Object.entries(t).map(([o,i])=>i.path)}isPresent(){let t=this.date.clone().startOf(Ie),o=(0,F.getAllQuarterlyNotes)();return!!(0,F.getQuarterlyNote)(t,o)}async create(){let t=this.date.clone().startOf(Ie);return(0,F.createQuarterlyNote)(t)}getCurrent(){return(0,F.getQuarterlyNote)(this.date,(0,F.getAllQuarterlyNotes)())}};var De=require("obsidian"),k=x(E());var Ae="week",$=class extends w{constructor(){super(...arguments);this.date=(0,De.moment)()}getAllPaths(){let t=(0,k.getAllWeeklyNotes)();return Object.entries(t).map(([o,i])=>i.path)}isPresent(){let t=this.date.clone().startOf(Ae),o=(0,k.getAllWeeklyNotes)();return!!(0,k.getWeeklyNote)(t,o)}async create(){let t=this.date.clone().startOf(Ae);return(0,k.createWeeklyNote)(t)}getCurrent(){return(0,k.getWeeklyNote)(this.date,(0,k.getAllWeeklyNotes)())}};var xe=require("obsidian"),I=x(E());var Ee="year",j=class extends w{constructor(){super(...arguments);this.date=(0,xe.moment)()}getAllPaths(){let t=(0,I.getAllYearlyNotes)();return Object.entries(t).map(([o,i])=>i.path)}isPresent(){let t=this.date.clone().startOf(Ee),o=(0,I.getAllYearlyNotes)();return!!(0,I.getYearlyNote)(t,o)}async create(){let t=this.date.clone().startOf(Ee);return(0,I.createYearlyNote)(t)}getCurrent(){return(0,I.getYearlyNote)(this.date,(0,I.getAllYearlyNotes)())}};function y(n){console.debug(`[APN] ${n}`)}var Q=class{constructor(e){this.workspace=e}async checkAndCreateNotes(e){y("Checking if any new notes need to be created"),this.workspaceLeaves={},await this.checkAndCreateSingleNote(e.yearly,new j,"yearly",e.alwaysOpen),await this.checkAndCreateSingleNote(e.quarterly,new H,"quarterly",e.alwaysOpen),await this.checkAndCreateSingleNote(e.monthly,new q,"monthly",e.alwaysOpen),await this.checkAndCreateSingleNote(e.weekly,new $,"weekly",e.alwaysOpen),await this.checkAndCreateSingleNote(e.daily,new U,"daily",e.alwaysOpen)}async checkAndCreateSingleNote(e,t,o,i){if(e.available&&e.enabled)if(y(`Checking if ${o} note needs to be created`),t.isPresent()){if(i){y(`Set to always open notes, getting current ${o} note and checking if it needs to be opened`);let a=t.getCurrent();await this.handleClose(e,t,a),await this.handleOpen(e,a)}}else{if(o==="daily"&&e.excludeWeekends){let s=(0,G.moment)();if(s.format("dd")==="Sa"||s.format("dd")==="Su"){y("Not creating new note as it is a weekend");return}}y(`Creating new ${o} note`);let a=await t.create();new G.Notice(`Today's ${o} note has been created.`,5e3),await this.handleClose(e,t,a),await this.handleOpen(e,a)}}getOpenWorkspaceLeaves(){return Object.keys(this.workspaceLeaves).length||this.workspace.iterateRootLeaves(e=>{e.view.getState()&&typeof e.view.getState().file!="undefined"&&(this.workspaceLeaves[e.view.getState().file]=e)}),this.workspaceLeaves}async handleClose(e,t,o){if(e.closeExisting){y("Checking for any existing notes to close");let i=t.getAllPaths(),a=[];if(Object.entries(this.getOpenWorkspaceLeaves()).forEach(([s,c])=>{i.indexOf(s)>-1&&a.push(c)}),Object.keys(this.getOpenWorkspaceLeaves()).indexOf(o.path)===-1){y("Found "+a.length+" tab(s) to close");for(let s of a)s.detach()}await Promise.all([setTimeout(()=>{},1e3)])}}async handleOpen(e,t){var o;e.openAndPin&&Object.keys(this.getOpenWorkspaceLeaves()).indexOf(t.path)===-1&&(y("Opening new note"),await this.workspace.getLeaf(!0).openFile(t),(o=this.workspace.getMostRecentLeaf())==null||o.setPinned(!0))}};var _e="periodic-notes",Me="periodic-notes:settings-updated",J=class{constructor(e){this.app=e}isEnabled(){return this.app.plugins.enabledPlugins.has(_e)}getPlugin(){return this.app.plugins.getPlugin(_e)}getSettings(){return this.getPlugin().settings||{}}convertSettings(e,t){return e.daily.available=t.daily.enabled,e.weekly.available=t.weekly.enabled,e.monthly.available=t.monthly.enabled,e.quarterly.available=t.quarterly.enabled,e.yearly.available=t.yearly.enabled,e}};var z=Object.freeze({available:!1,enabled:!1,closeExisting:!1,openAndPin:!1}),ut=Object.freeze({alwaysOpen:!1,daily:{...z,excludeWeekends:!1},weekly:{...z},monthly:{...z},quarterly:{...z},yearly:{...z}});function Le(n){return Object.assign({},ut,n)}var v=require("obsidian"),V=class extends v.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){this.containerEl.empty();let e=this.plugin.settings,t=["daily","weekly","monthly","quarterly","yearly"];if(!e.daily.available&&!e.weekly.available&&!e.monthly.available&&!e.quarterly.available&&!e.yearly.available){let o=this.containerEl.createDiv({cls:"settings-banner"});new v.Setting(o).setName("No periodic notes enabled").setHeading().setDesc("No periodic notes settings are enabled. You must turn on one of daily, weekly, monthly, quarterly or yearly notes within the Periodic Notes plugin settings to be able to configure them to generate automatically.")}this.containerEl.createEl("h3",{text:"All periodic notes"}),new v.Setting(this.containerEl).setName("Always open periodic notes").setDesc("When opening Obsidian or checking notes, always open your periodic notes even when they haven't just been created. This can be useful for maintaining a consistent workspace with pinned notes each time you start your day.").addToggle(o=>{o.setValue(e.alwaysOpen).onChange(async i=>{e.alwaysOpen=i,await this.plugin.updateSettings(e)})});for(let o of t)e[o].available&&(this.containerEl.createEl("h3",{text:`Automatic ${o} notes`}),new v.Setting(this.containerEl).setName(`Enable automatic ${o} notes`).setDesc(`Create new ${o} notes automatically using periodic notes location and template.`).addToggle(i=>{i.setValue(e[o].enabled).onChange(async a=>{e[o].enabled=a,await this.plugin.updateSettings(e)})}),o==="daily"&&new v.Setting(this.containerEl).setName("Exclude weekends").setDesc("Only create new daily notes Monday - Friday, excluding Saturdays and Sundays.").addToggle(i=>{i.setValue(e[o].excludeWeekends).onChange(async a=>{e[o].excludeWeekends=a,await this.plugin.updateSettings(e)})}),new v.Setting(this.containerEl).setName(`Open and pin new ${o} notes`).setDesc("When enabled, whether to automatically open the new note and pin it to your tabs.").addToggle(i=>{i.setValue(e[o].openAndPin).onChange(async a=>{e[o].openAndPin=a,await this.plugin.updateSettings(e)})}),new v.Setting(this.containerEl).setName(`Close older ${o} notes`).setDesc(`When creating new notes, automatically close any older and open ${o} notes.`).addToggle(i=>{i.setValue(e[o].closeExisting).onChange(async a=>{e[o].closeExisting=a,await this.plugin.updateSettings(e)})}))}};var K=class extends X.Plugin{constructor(e,t){super(e,t),this.settings={},this.periodicNotesPlugin=new J(e),this.notes=new Q(e.workspace)}async onload(){this.updateSettings=this.updateSettings.bind(this),await this.loadSettings(),this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this))}onLayoutReady(){if(!this.periodicNotesPlugin.isEnabled()){new X.Notice("The Periodic Notes plugin must be installed and available for Auto Periodic Notes to work.",1e4);return}let e=this.app.workspace;this.registerEvent(e.on(Me,this.syncPeriodicNotesSettings.bind(this))),this.syncPeriodicNotesSettings(),this.addSettingTab(new V(this.app,this)),this.registerInterval(window.setInterval(()=>{this.notes.checkAndCreateNotes(this.settings)},3e5)),this.notes.checkAndCreateNotes(this.settings)}async loadSettings(){this.settings=Le(await this.loadData()),y("Loaded settings: "+JSON.stringify(this.settings))}async updateSettings(e){this.settings=e,await this.saveData(this.settings),this.onSettingsUpdate(),y("Saved settings: "+JSON.stringify(this.settings))}syncPeriodicNotesSettings(){y("Received new settings from Periodic Notes plugin"),this.updateSettings(this.periodicNotesPlugin.convertSettings(this.settings,this.periodicNotesPlugin.getSettings()))}onSettingsUpdate(){this.app.workspace.trigger(ce)}}; /* nosourcemap */ ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/auto-periodic-notes/manifest.json ================================================ { "id": "auto-periodic-notes", "name": "Auto Periodic Notes", "version": "0.2.3", "minAppVersion": "1.6.6", "description": "Creates new periodic notes automatically in the background and allows these to be pinned in your open tabs, requires the Periodic Notes plugin.", "author": "Jamie Hurst", "authorUrl": "https://jamiehurst.co.uk", "isDesktopOnly": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/logstravaganza/data.json ================================================ { "fileNameContainsDate": false, "formatterID": "ndjson", "outputFolder": "/", "logLevel": "debug", "debounceWrites": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/logstravaganza/main.js ================================================ /* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { default: () => Logstravaganza }); module.exports = __toCommonJS(main_exports); var import_obsidian4 = require("obsidian"); // src/console-proxy.ts var import_obsidian2 = require("obsidian"); // src/utils.ts var import_obsidian = require("obsidian"); function getDeviceName(app) { const syncPlugin = app.internalPlugins?.plugins["sync"]?.instance; if (!syncPlugin) { return "Unknown device"; } return syncPlugin.deviceName ? syncPlugin.deviceName : syncPlugin.getDefaultDeviceName(); } function prefixMsg(msg) { return `[Logstravaganza] ${msg}`; } function createQueue(onPush, debounceWrites = true) { const callback = debounceWrites ? (0, import_obsidian.debounce)(onPush, 1e3) : onPush; const queue = []; const handler = { get(target, prop) { if (prop === "push" || prop.description === "push") { callback(); } return target[prop]; } }; return new Proxy(queue, handler); } async function getFile(vault, filename, initialContent) { const note = vault.getAbstractFileByPath(filename); return note instanceof import_obsidian.TFile ? note : await vault.create(filename, (initialContent ?? "") + "\n"); } function getObsidianURI(vault, path) { const v = encodeURIComponent(vault.getName()); const p = encodeURIComponent(path); return `obsidian://open?vault=${v}&file=${p}`; } function logLevelFilter(logEvent, logLevel) { switch (logLevel) { case "debug": return true; case "info": return logEvent.level !== "debug"; case "warn": return !["debug", "info", "log"].includes(logEvent.level); case "error": return !["debug", "info", "log", "warn"].includes(logEvent.level); default: break; } return true; } // src/console-proxy.ts var WINDOW_CONSOLE = window.console; var ConsoleProxy = class { constructor(queue) { this.queue = queue; } // Installing the console proxy object and the listener for uncaught errors setup() { const self = this; const handler = { get(target, prop) { const property = target[prop]; if (typeof property === "function") { return (...args) => { const sender = new Error().stack?.split("\n").at(2)?.replace(/^.+\((.+?)\).*$/, "$1").replace("app://obsidian.md/", "").trim(); self.storeEvent(prop.toString(), sender, args); return property.apply(target, args); }; } return property; } }; const consoleProxy = new Proxy(window.console, handler); window.console = consoleProxy; window.addEventListener("error", this.onWindowError.bind(this)); window.addEventListener( "unhandledrejection", this.onWindowUnhandledRejection.bind(this) ); return this; } // Removing the console proxy object and the listener for uncaught errors teardown() { window.console = WINDOW_CONSOLE; window.removeEventListener("error", this.onWindowError.bind(this)); window.removeEventListener( "unhandledrejection", this.onWindowUnhandledRejection.bind(this) ); console.info(prefixMsg("Proxy removed")); } /** * Adds log events with the specified level and arguments to the log event * queue. * * @param level - The level of the log event. * @param sender - The sender of the log event (e.g., "plugin:whatever"). * @param args - The argument(s) to be logged, optional. */ storeEvent(level, sender, ...args) { this.queue.push({ timestamp: /* @__PURE__ */ new Date(), level, sender, args: args.map(this.rewriteForLogging.bind(this)) }); } /** * Tries to prevent "max. call stack exceeded" errors by replacing certain * objects with a string representation. */ rewriteForLogging(value) { if (value instanceof import_obsidian2.TFolder) { return `[TFolder] ${value.path}`; } else if (value instanceof import_obsidian2.TFile) { return `[TFile] ${value.path}`; } else if (value instanceof import_obsidian2.TAbstractFile) { return `[TAbstractFile] ${value.path}`; } else if (value instanceof import_obsidian2.App) { return "[App]"; } else if (value instanceof import_obsidian2.Vault) { return "[Vault]"; } else if (value instanceof import_obsidian2.Workspace) { return "[Workspace]"; } else if (Array.isArray(value)) { return value.map((item) => this.rewriteForLogging(item)); } else if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([key, val]) => [key, this.rewriteForLogging(val)]) ); } return value; } /** * Event handler for window errors. Adds a "fatal"-level log event to the log * event queue. * * @param event - The error event object. */ onWindowError(event) { const { message, colno, lineno, filename, error } = event; this.storeEvent( "fatal", `${filename}:${lineno}:${colno}`, error.name, message, error.stack || "(stack trace unavailable)" ); } /** * Event handler for unhandled exceptions happening in promises. Adds a * "fatal"-level log event to the log event queue. * * @param event - The error event object. */ onWindowUnhandledRejection(event) { const error = event.reason; if (typeof error === "string") { this.storeEvent( "fatal", "sender:unknown", "Uncaught (in promise)", error ); } else { const { colno, lineno, filename } = error; const sender = filename && lineno && colno ? `${filename}:${lineno}:${colno}` : error.stack?.match(/at eval \((.+?)\)/)?.[1] ?? "(undetermined)"; this.storeEvent( "fatal", sender, "Uncaught (in promise)", error.stack || "(stack trace unavailable)" ); } } }; // src/formatters/mdcodeblocks.ts var mdcodeblocks_default = { id: "mdcodeblocks", title: "Markdown Code Blocks", description: "Generates a Markdown file containing code blocks.", fileExt: "md", format: ({ timestamp, level, sender, args }) => { const logMsg = args.map((arg) => { if (typeof arg === "string") { return arg; } if (Array.isArray(arg) && arg.length <= 1) { if (typeof arg[0] === "string") { return arg[0]; } return JSON.stringify(arg[0], null, 2); } return JSON.stringify(arg, null, 2); }); return [ "```", `time: ${timestamp.toISOString()}`, `from: ${sender ?? ""}`, `level: ${level}`, logMsg, "```", "" ].join("\n"); } }; // src/formatters/mdtable.ts var mdtable_default = { id: "mdtable", title: "Markdown Table", description: "Generates a Markdown file containing a table", fileExt: "md", contentHead: [ "| Timestamp | Originator | Level | Message |", "| --------- | ---------- | ----- | ------- |" ].join("\n"), format: ({ timestamp, level, sender, args }) => { const logMsg = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).map(escapeForMdTable).join(" "); return [ "", timestamp.toISOString(), escapeForMdTable(sender ?? ""), level, logMsg, "" ].join(" | ").trim(); } }; var escapeForMdTable = (str) => str.replace(/([\|\[<])/sg, "\\$1").replace(/\n/g, "
"); // src/formatters/ndjson.ts var ndjson_default = { id: "ndjson", title: "NDJSON", description: "Generates a newline-delimited JSON file", fileExt: "ndjson", format: (logEvent) => JSON.stringify(logEvent) }; // src/formatters.ts var formatters = [ mdtable_default, ndjson_default, mdcodeblocks_default ].sort((a, b) => a.title.localeCompare(b.title)); function findFormatterByID(id) { return formatters.find((formatter) => formatter.id === id); } // src/plugin-info.ts var PLUGIN_INFO = { "pluginVersion": "2.2.0", "pluginReleasedAt": "2025-05-21T13:23:45+0200" }; // src/settings.ts var import_obsidian3 = require("obsidian"); var LogstravaganzaSettingTab = class extends import_obsidian3.PluginSettingTab { plugin; constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } display() { const { containerEl, plugin } = this; containerEl.empty(); new import_obsidian3.Setting(containerEl).setName("Output format").setDesc(` This plugin intercepts developer console messages, and saves them to a file in your vault. Select the output file format here.`).addDropdown((dropdown) => { dropdown.addOptions(this.allFormatters()).setValue(plugin.settings.formatterID).onChange( async (value) => { plugin.settings.formatterID = value; await plugin.saveSettings(); this.display(); } ); }); const ul = containerEl.createEl("ul", { cls: "setting-item-description", attr: { style: "margin-block-start: 0; padding-inline-start: 2em;" } }); formatters.forEach((f) => { ul.createEl("li", { attr: { style: "margin-bottom: 0.5rem;" } }).innerHTML = ` ${f.title}: ${f.description}.
File extension: .${f.fileExt} `; }); new import_obsidian3.Setting(containerEl).setName("Output folder").setDesc("Where to save the log files.").addDropdown((dropdown) => { dropdown.addOptions(this.allFolders()).setValue(plugin.settings.outputFolder).onChange( async (value) => { plugin.settings.outputFolder = value; await plugin.saveSettings(); this.display(); } ); }); new import_obsidian3.Setting(containerEl).setName("Include current date in filename").setDesc("Adds the YYYY-MM-DD timestamp to the output filename.").addToggle((toggle) => { toggle.setValue(plugin.settings.fileNameContainsDate).onChange(async (value) => { plugin.settings.fileNameContainsDate = value; await plugin.saveSettings(); this.display(); }); }); new import_obsidian3.Setting(containerEl).setName("Log level to render").setDesc(` Only print out the log level equal to or above what you set here. `).addDropdown((dropdown) => { dropdown.addOption("debug", "debug (print everything)").addOption("info", "info").addOption("warn", "warn").addOption("error", "error (only print error)").setValue(plugin.settings.logLevel).onChange(async (value) => { plugin.settings.logLevel = value; await plugin.saveSettings(); this.display(); }); }); new import_obsidian3.Setting(containerEl).setName("Debounce writing to output file").setDesc(` Disabling this setting will cause Logstravaganza to write everything to the output file as it happens, and as such will impact performance. Usually, you'll want to keep this setting enabled. `).addToggle((toggle) => { toggle.setValue(plugin.settings.debounceWrites).onChange(async (value) => { plugin.settings.debounceWrites = value; await plugin.saveSettings(); this.display(); }); }); const fileExt = formatters.find((f) => f.id === plugin.settings.formatterID).fileExt; const filename = plugin.getOutputFilename(fileExt); const link = getObsidianURI(this.app.vault, filename); containerEl.createEl("h5", { text: "Output file" }); containerEl.createEl("p", { text: "\u2192 " }).createEl("a", { text: filename, attr: { href: link } }); const afoURL = "https://actions.work/actions-for-obsidian?ref=plugin-logstravaganza"; containerEl.createEl("div", { attr: { style: ` border-radius: 0.5rem; border: 1px dashed var(--text-muted); color: var(--text-muted); display: grid; font-size: 85%; grid-gap: 1rem; grid-template-columns: auto 1fr; margin-top: 4rem; opacity: 0.75; padding: 1rem; ` } }).innerHTML = ` Actions for Obsidian icon, a cog wheel on a glossy black background Logstravaganza is brought to you by Actions for Obsidian, a macOS/iOS app made by the same developer as this plugin. AFO is the missing link between Obsidian and macOS / iOS: 50+ Shortcuts actions to bring your notes and your automations together. Take a look! `; } allFolders() { return this.app.vault.getAllLoadedFiles().filter((f) => f instanceof import_obsidian3.TFolder).map((f) => ({ name: `/${f.path}`.replace(/^\/+/, "/"), path: f.path })).sort((a, b) => b.name.localeCompare(a.name)).reduce( (obj, f) => ({ [f.path]: f.name, ...obj }), {} ); } allFormatters() { return formatters.reduce( (obj, f) => ({ [f.id]: f.title, ...obj }), {} ); } }; // src/main.ts var DEFAULT_SETTINGS = { fileNameContainsDate: false, formatterID: "mdtable", outputFolder: "/", logLevel: "debug", debounceWrites: true }; var Logstravaganza = class extends import_obsidian4.Plugin { queue; proxy; deviceName = getDeviceName(this.app); settings; outputFileBasename = `console-log.${this.deviceName}`; async onload() { await this.loadSettings(); this.queue = createQueue( this.writeToFile.bind(this), this.settings.debounceWrites ); this.proxy = new ConsoleProxy(this.queue).setup(); this.proxy.storeEvent( "info", "plugin:logstravaganza", prefixMsg(`Proxy set up (v${PLUGIN_INFO.pluginVersion})`) ); this.addSettingTab(new LogstravaganzaSettingTab(this.app, this)); new import_obsidian4.Notice("Logstravaganza is enabled!"); } onunload() { this.proxy.teardown(); new import_obsidian4.Notice("Logstravaganza is disabled"); } async loadSettings() { this.settings = { ...DEFAULT_SETTINGS, ...await this.loadData() }; } async saveSettings() { await this.saveData(this.settings); } getOutputFilename(ext) { const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const filename = this.settings.fileNameContainsDate ? `${this.outputFileBasename}.${currentDate}.${ext}` : `${this.outputFileBasename}.${ext}`; return (0, import_obsidian4.normalizePath)(`${this.settings.outputFolder}/${filename}`); } /** * This function writes the log event to the file. It is called by the queue * when new log events have been intercepted. */ async writeToFile() { const { vault, workspace } = this.app; workspace.onLayoutReady(async () => { const formatter = findFormatterByID(this.settings.formatterID); const filename = this.getOutputFilename(formatter.fileExt); const file = await getFile(vault, filename, formatter.contentHead); let logEvent; while (logEvent = this.queue.shift()) { if (logLevelFilter(logEvent, this.settings.logLevel)) { let line = formatter.format(logEvent) + "\n"; await vault.append(file, line); } } }); } }; /* nosourcemap */ ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/logstravaganza/manifest.json ================================================ { "id": "logstravaganza", "name": "Logstravaganza", "version": "2.2.0", "minAppVersion": "1.8.0", "description": "A simple proxy for `console.*()` calls which copies log messages and uncaught exceptions to a file.", "author": "Carlo Zottmann", "authorUrl": "https://zottmann.dev", "isDesktopOnly": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/periodic-notes/data.json ================================================ { "showGettingStartedBanner": true, "hasMigratedDailyNoteSettings": false, "hasMigratedWeeklyNoteSettings": false, "daily": { "format": "", "template": "_templates/Daily Note.md", "folder": "", "enabled": true }, "weekly": { "format": "", "template": "_templates/Weekly Note.md", "folder": "", "enabled": true }, "monthly": { "format": "", "template": "_templates/Monthly Note.md", "folder": "", "enabled": true }, "quarterly": { "format": "", "template": "_templates/Quarterly Note.md", "folder": "", "enabled": true }, "yearly": { "format": "", "template": "_templates/Yearly Note.md", "folder": "", "enabled": true } } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/periodic-notes/main.js ================================================ 'use strict'; var obsidian = require('obsidian'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var obsidian__default = /*#__PURE__*/_interopDefaultLegacy(obsidian); const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD"; const DEFAULT_WEEKLY_NOTE_FORMAT = "gggg-[W]ww"; const DEFAULT_MONTHLY_NOTE_FORMAT = "YYYY-MM"; const DEFAULT_QUARTERLY_NOTE_FORMAT = "YYYY-[Q]Q"; const DEFAULT_YEARLY_NOTE_FORMAT = "YYYY"; function shouldUsePeriodicNotesSettings(periodicity) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = window.app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.[periodicity]?.enabled; } /** * Read the user settings for the `daily-notes` plugin * to keep behavior of creating a new note in-sync. */ function getDailyNoteSettings() { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { internalPlugins, plugins } = window.app; if (shouldUsePeriodicNotesSettings("daily")) { const { format, folder, template } = plugins.getPlugin("periodic-notes")?.settings?.daily || {}; return { format: format || DEFAULT_DAILY_NOTE_FORMAT, folder: folder?.trim() || "", template: template?.trim() || "", }; } const { folder, format, template } = internalPlugins.getPluginById("daily-notes")?.instance?.options || {}; return { format: format || DEFAULT_DAILY_NOTE_FORMAT, folder: folder?.trim() || "", template: template?.trim() || "", }; } catch (err) { console.info("No custom daily note settings found!", err); } } /** * Read the user settings for the `weekly-notes` plugin * to keep behavior of creating a new note in-sync. */ function getWeeklyNoteSettings() { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pluginManager = window.app.plugins; const calendarSettings = pluginManager.getPlugin("calendar")?.options; const periodicNotesSettings = pluginManager.getPlugin("periodic-notes")?.settings?.weekly; if (shouldUsePeriodicNotesSettings("weekly")) { return { format: periodicNotesSettings.format || DEFAULT_WEEKLY_NOTE_FORMAT, folder: periodicNotesSettings.folder?.trim() || "", template: periodicNotesSettings.template?.trim() || "", }; } const settings = calendarSettings || {}; return { format: settings.weeklyNoteFormat || DEFAULT_WEEKLY_NOTE_FORMAT, folder: settings.weeklyNoteFolder?.trim() || "", template: settings.weeklyNoteTemplate?.trim() || "", }; } catch (err) { console.info("No custom weekly note settings found!", err); } } /** * Read the user settings for the `periodic-notes` plugin * to keep behavior of creating a new note in-sync. */ function getMonthlyNoteSettings() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pluginManager = window.app.plugins; try { const settings = (shouldUsePeriodicNotesSettings("monthly") && pluginManager.getPlugin("periodic-notes")?.settings?.monthly) || {}; return { format: settings.format || DEFAULT_MONTHLY_NOTE_FORMAT, folder: settings.folder?.trim() || "", template: settings.template?.trim() || "", }; } catch (err) { console.info("No custom monthly note settings found!", err); } } /** * Read the user settings for the `periodic-notes` plugin * to keep behavior of creating a new note in-sync. */ function getQuarterlyNoteSettings() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pluginManager = window.app.plugins; try { const settings = (shouldUsePeriodicNotesSettings("quarterly") && pluginManager.getPlugin("periodic-notes")?.settings?.quarterly) || {}; return { format: settings.format || DEFAULT_QUARTERLY_NOTE_FORMAT, folder: settings.folder?.trim() || "", template: settings.template?.trim() || "", }; } catch (err) { console.info("No custom quarterly note settings found!", err); } } /** * Read the user settings for the `periodic-notes` plugin * to keep behavior of creating a new note in-sync. */ function getYearlyNoteSettings() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pluginManager = window.app.plugins; try { const settings = (shouldUsePeriodicNotesSettings("yearly") && pluginManager.getPlugin("periodic-notes")?.settings?.yearly) || {}; return { format: settings.format || DEFAULT_YEARLY_NOTE_FORMAT, folder: settings.folder?.trim() || "", template: settings.template?.trim() || "", }; } catch (err) { console.info("No custom yearly note settings found!", err); } } // Credit: @creationix/path.js function join(...partSegments) { // Split the inputs into a list of path commands. let parts = []; for (let i = 0, l = partSegments.length; i < l; i++) { parts = parts.concat(partSegments[i].split("/")); } // Interpret the path commands to get the new resolved path. const newParts = []; for (let i = 0, l = parts.length; i < l; i++) { const part = parts[i]; // Remove leading and trailing slashes // Also remove "." segments if (!part || part === ".") continue; // Push new path segments. else newParts.push(part); } // Preserve the initial slash if there was one. if (parts[0] === "") newParts.unshift(""); // Turn back into a single string path. return newParts.join("/"); } async function ensureFolderExists(path) { const dirs = path.replace(/\\/g, "/").split("/"); dirs.pop(); // remove basename if (dirs.length) { const dir = join(...dirs); if (!window.app.vault.getAbstractFileByPath(dir)) { await window.app.vault.createFolder(dir); } } } async function getNotePath(directory, filename) { if (!filename.endsWith(".md")) { filename += ".md"; } const path = obsidian__default['default'].normalizePath(join(directory, filename)); await ensureFolderExists(path); return path; } async function getTemplateInfo(template) { const { metadataCache, vault } = window.app; const templatePath = obsidian__default['default'].normalizePath(template); if (templatePath === "/") { return Promise.resolve(["", null]); } try { const templateFile = metadataCache.getFirstLinkpathDest(templatePath, ""); const contents = await vault.cachedRead(templateFile); // eslint-disable-next-line @typescript-eslint/no-explicit-any const IFoldInfo = window.app.foldManager.load(templateFile); return [contents, IFoldInfo]; } catch (err) { console.error(`Failed to read the daily note template '${templatePath}'`, err); new obsidian__default['default'].Notice("Failed to read the daily note template"); return ["", null]; } } /** * dateUID is a way of weekly identifying daily/weekly/monthly notes. * They are prefixed with the granularity to avoid ambiguity. */ function getDateUID(date, granularity = "day") { const ts = date.clone().startOf(granularity).format(); return `${granularity}-${ts}`; } function removeEscapedCharacters(format) { return format.replace(/\[[^\]]*\]/g, ""); // remove everything within brackets } /** * XXX: When parsing dates that contain both week numbers and months, * Moment choses to ignore the week numbers. For the week dateUID, we * want the opposite behavior. Strip the MMM from the format to patch. */ function isFormatAmbiguous(format, granularity) { if (granularity === "week") { const cleanFormat = removeEscapedCharacters(format); return (/w{1,2}/i.test(cleanFormat) && (/M{1,4}/.test(cleanFormat) || /D{1,4}/.test(cleanFormat))); } return false; } function getDateFromFile(file, granularity) { return getDateFromFilename(file.basename, granularity); } function getDateFromFilename(filename, granularity) { const getSettings = { day: getDailyNoteSettings, week: getWeeklyNoteSettings, month: getMonthlyNoteSettings, quarter: getQuarterlyNoteSettings, year: getYearlyNoteSettings, }; const format = getSettings[granularity]().format.split("/").pop(); const noteDate = window.moment(filename, format, true); if (!noteDate.isValid()) { return null; } if (isFormatAmbiguous(format, granularity)) { if (granularity === "week") { const cleanFormat = removeEscapedCharacters(format); if (/w{1,2}/i.test(cleanFormat)) { return window.moment(filename, // If format contains week, remove day & month formatting format.replace(/M{1,4}/g, "").replace(/D{1,4}/g, ""), false); } } } return noteDate; } class DailyNotesFolderMissingError extends Error { } /** * This function mimics the behavior of the daily-notes plugin * so it will replace {{date}}, {{title}}, and {{time}} with the * formatted timestamp. * * Note: it has an added bonus that it's not 'today' specific. */ async function createDailyNote(date) { const app = window.app; const { vault } = app; const moment = window.moment; const { template, format, folder } = getDailyNoteSettings(); const [templateContents, IFoldInfo] = await getTemplateInfo(template); const filename = date.format(format); const normalizedPath = await getNotePath(folder, filename); try { const createdFile = await vault.create(normalizedPath, templateContents .replace(/{{\s*date\s*}}/gi, filename) .replace(/{{\s*time\s*}}/gi, moment().format("HH:mm")) .replace(/{{\s*title\s*}}/gi, filename) .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { const now = moment(); const currentDate = date.clone().set({ hour: now.get("hour"), minute: now.get("minute"), second: now.get("second"), }); if (calc) { currentDate.add(parseInt(timeDelta, 10), unit); } if (momentFormat) { return currentDate.format(momentFormat.substring(1).trim()); } return currentDate.format(format); }) .replace(/{{\s*yesterday\s*}}/gi, date.clone().subtract(1, "day").format(format)) .replace(/{{\s*tomorrow\s*}}/gi, date.clone().add(1, "d").format(format))); // eslint-disable-next-line @typescript-eslint/no-explicit-any app.foldManager.save(createdFile, IFoldInfo); return createdFile; } catch (err) { console.error(`Failed to create file: '${normalizedPath}'`, err); new obsidian__default['default'].Notice("Unable to create new file."); } } function getDailyNote(date, dailyNotes) { return dailyNotes[getDateUID(date, "day")] ?? null; } function getAllDailyNotes() { /** * Find all daily notes in the daily note folder */ const { vault } = window.app; const { folder } = getDailyNoteSettings(); const dailyNotesFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); if (!dailyNotesFolder) { throw new DailyNotesFolderMissingError("Failed to find daily notes folder"); } const dailyNotes = {}; obsidian__default['default'].Vault.recurseChildren(dailyNotesFolder, (note) => { if (note instanceof obsidian__default['default'].TFile) { const date = getDateFromFile(note, "day"); if (date) { const dateString = getDateUID(date, "day"); dailyNotes[dateString] = note; } } }); return dailyNotes; } class WeeklyNotesFolderMissingError extends Error { } function getDaysOfWeek() { const { moment } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any let weekStart = moment.localeData()._week.dow; const daysOfWeek = [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", ]; while (weekStart) { daysOfWeek.push(daysOfWeek.shift()); weekStart--; } return daysOfWeek; } function getDayOfWeekNumericalValue(dayOfWeekName) { return getDaysOfWeek().indexOf(dayOfWeekName.toLowerCase()); } async function createWeeklyNote(date) { const { vault } = window.app; const { template, format, folder } = getWeeklyNoteSettings(); const [templateContents, IFoldInfo] = await getTemplateInfo(template); const filename = date.format(format); const normalizedPath = await getNotePath(folder, filename); try { const createdFile = await vault.create(normalizedPath, templateContents .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { const now = window.moment(); const currentDate = date.clone().set({ hour: now.get("hour"), minute: now.get("minute"), second: now.get("second"), }); if (calc) { currentDate.add(parseInt(timeDelta, 10), unit); } if (momentFormat) { return currentDate.format(momentFormat.substring(1).trim()); } return currentDate.format(format); }) .replace(/{{\s*title\s*}}/gi, filename) .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) .replace(/{{\s*(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\s*:(.*?)}}/gi, (_, dayOfWeek, momentFormat) => { const day = getDayOfWeekNumericalValue(dayOfWeek); return date.weekday(day).format(momentFormat.trim()); })); // eslint-disable-next-line @typescript-eslint/no-explicit-any window.app.foldManager.save(createdFile, IFoldInfo); return createdFile; } catch (err) { console.error(`Failed to create file: '${normalizedPath}'`, err); new obsidian__default['default'].Notice("Unable to create new file."); } } function getWeeklyNote(date, weeklyNotes) { return weeklyNotes[getDateUID(date, "week")] ?? null; } function getAllWeeklyNotes() { const weeklyNotes = {}; if (!appHasWeeklyNotesPluginLoaded()) { return weeklyNotes; } const { vault } = window.app; const { folder } = getWeeklyNoteSettings(); const weeklyNotesFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); if (!weeklyNotesFolder) { throw new WeeklyNotesFolderMissingError("Failed to find weekly notes folder"); } obsidian__default['default'].Vault.recurseChildren(weeklyNotesFolder, (note) => { if (note instanceof obsidian__default['default'].TFile) { const date = getDateFromFile(note, "week"); if (date) { const dateString = getDateUID(date, "week"); weeklyNotes[dateString] = note; } } }); return weeklyNotes; } class MonthlyNotesFolderMissingError extends Error { } /** * This function mimics the behavior of the daily-notes plugin * so it will replace {{date}}, {{title}}, and {{time}} with the * formatted timestamp. * * Note: it has an added bonus that it's not 'today' specific. */ async function createMonthlyNote(date) { const { vault } = window.app; const { template, format, folder } = getMonthlyNoteSettings(); const [templateContents, IFoldInfo] = await getTemplateInfo(template); const filename = date.format(format); const normalizedPath = await getNotePath(folder, filename); try { const createdFile = await vault.create(normalizedPath, templateContents .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { const now = window.moment(); const currentDate = date.clone().set({ hour: now.get("hour"), minute: now.get("minute"), second: now.get("second"), }); if (calc) { currentDate.add(parseInt(timeDelta, 10), unit); } if (momentFormat) { return currentDate.format(momentFormat.substring(1).trim()); } return currentDate.format(format); }) .replace(/{{\s*date\s*}}/gi, filename) .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) .replace(/{{\s*title\s*}}/gi, filename)); // eslint-disable-next-line @typescript-eslint/no-explicit-any window.app.foldManager.save(createdFile, IFoldInfo); return createdFile; } catch (err) { console.error(`Failed to create file: '${normalizedPath}'`, err); new obsidian__default['default'].Notice("Unable to create new file."); } } function getMonthlyNote(date, monthlyNotes) { return monthlyNotes[getDateUID(date, "month")] ?? null; } function getAllMonthlyNotes() { const monthlyNotes = {}; if (!appHasMonthlyNotesPluginLoaded()) { return monthlyNotes; } const { vault } = window.app; const { folder } = getMonthlyNoteSettings(); const monthlyNotesFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); if (!monthlyNotesFolder) { throw new MonthlyNotesFolderMissingError("Failed to find monthly notes folder"); } obsidian__default['default'].Vault.recurseChildren(monthlyNotesFolder, (note) => { if (note instanceof obsidian__default['default'].TFile) { const date = getDateFromFile(note, "month"); if (date) { const dateString = getDateUID(date, "month"); monthlyNotes[dateString] = note; } } }); return monthlyNotes; } class QuarterlyNotesFolderMissingError extends Error { } /** * This function mimics the behavior of the daily-notes plugin * so it will replace {{date}}, {{title}}, and {{time}} with the * formatted timestamp. * * Note: it has an added bonus that it's not 'today' specific. */ async function createQuarterlyNote(date) { const { vault } = window.app; const { template, format, folder } = getQuarterlyNoteSettings(); const [templateContents, IFoldInfo] = await getTemplateInfo(template); const filename = date.format(format); const normalizedPath = await getNotePath(folder, filename); try { const createdFile = await vault.create(normalizedPath, templateContents .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { const now = window.moment(); const currentDate = date.clone().set({ hour: now.get("hour"), minute: now.get("minute"), second: now.get("second"), }); if (calc) { currentDate.add(parseInt(timeDelta, 10), unit); } if (momentFormat) { return currentDate.format(momentFormat.substring(1).trim()); } return currentDate.format(format); }) .replace(/{{\s*date\s*}}/gi, filename) .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) .replace(/{{\s*title\s*}}/gi, filename)); // eslint-disable-next-line @typescript-eslint/no-explicit-any window.app.foldManager.save(createdFile, IFoldInfo); return createdFile; } catch (err) { console.error(`Failed to create file: '${normalizedPath}'`, err); new obsidian__default['default'].Notice("Unable to create new file."); } } function getQuarterlyNote(date, quarterly) { return quarterly[getDateUID(date, "quarter")] ?? null; } function getAllQuarterlyNotes() { const quarterly = {}; if (!appHasQuarterlyNotesPluginLoaded()) { return quarterly; } const { vault } = window.app; const { folder } = getQuarterlyNoteSettings(); const quarterlyFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); if (!quarterlyFolder) { throw new QuarterlyNotesFolderMissingError("Failed to find quarterly notes folder"); } obsidian__default['default'].Vault.recurseChildren(quarterlyFolder, (note) => { if (note instanceof obsidian__default['default'].TFile) { const date = getDateFromFile(note, "quarter"); if (date) { const dateString = getDateUID(date, "quarter"); quarterly[dateString] = note; } } }); return quarterly; } class YearlyNotesFolderMissingError extends Error { } /** * This function mimics the behavior of the daily-notes plugin * so it will replace {{date}}, {{title}}, and {{time}} with the * formatted timestamp. * * Note: it has an added bonus that it's not 'today' specific. */ async function createYearlyNote(date) { const { vault } = window.app; const { template, format, folder } = getYearlyNoteSettings(); const [templateContents, IFoldInfo] = await getTemplateInfo(template); const filename = date.format(format); const normalizedPath = await getNotePath(folder, filename); try { const createdFile = await vault.create(normalizedPath, templateContents .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { const now = window.moment(); const currentDate = date.clone().set({ hour: now.get("hour"), minute: now.get("minute"), second: now.get("second"), }); if (calc) { currentDate.add(parseInt(timeDelta, 10), unit); } if (momentFormat) { return currentDate.format(momentFormat.substring(1).trim()); } return currentDate.format(format); }) .replace(/{{\s*date\s*}}/gi, filename) .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) .replace(/{{\s*title\s*}}/gi, filename)); // eslint-disable-next-line @typescript-eslint/no-explicit-any window.app.foldManager.save(createdFile, IFoldInfo); return createdFile; } catch (err) { console.error(`Failed to create file: '${normalizedPath}'`, err); new obsidian__default['default'].Notice("Unable to create new file."); } } function getYearlyNote(date, yearlyNotes) { return yearlyNotes[getDateUID(date, "year")] ?? null; } function getAllYearlyNotes() { const yearlyNotes = {}; if (!appHasYearlyNotesPluginLoaded()) { return yearlyNotes; } const { vault } = window.app; const { folder } = getYearlyNoteSettings(); const yearlyNotesFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); if (!yearlyNotesFolder) { throw new YearlyNotesFolderMissingError("Failed to find yearly notes folder"); } obsidian__default['default'].Vault.recurseChildren(yearlyNotesFolder, (note) => { if (note instanceof obsidian__default['default'].TFile) { const date = getDateFromFile(note, "year"); if (date) { const dateString = getDateUID(date, "year"); yearlyNotes[dateString] = note; } } }); return yearlyNotes; } function appHasDailyNotesPluginLoaded() { const { app } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any const dailyNotesPlugin = app.internalPlugins.plugins["daily-notes"]; if (dailyNotesPlugin && dailyNotesPlugin.enabled) { return true; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.daily?.enabled; } /** * XXX: "Weekly Notes" live in either the Calendar plugin or the periodic-notes plugin. * Check both until the weekly notes feature is removed from the Calendar plugin. */ function appHasWeeklyNotesPluginLoaded() { const { app } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any if (app.plugins.getPlugin("calendar")) { return true; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.weekly?.enabled; } function appHasMonthlyNotesPluginLoaded() { const { app } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.monthly?.enabled; } function appHasQuarterlyNotesPluginLoaded() { const { app } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.quarterly?.enabled; } function appHasYearlyNotesPluginLoaded() { const { app } = window; // eslint-disable-next-line @typescript-eslint/no-explicit-any const periodicNotes = app.plugins.getPlugin("periodic-notes"); return periodicNotes && periodicNotes.settings?.yearly?.enabled; } var DEFAULT_DAILY_NOTE_FORMAT_1 = DEFAULT_DAILY_NOTE_FORMAT; var DEFAULT_MONTHLY_NOTE_FORMAT_1 = DEFAULT_MONTHLY_NOTE_FORMAT; var DEFAULT_QUARTERLY_NOTE_FORMAT_1 = DEFAULT_QUARTERLY_NOTE_FORMAT; var DEFAULT_WEEKLY_NOTE_FORMAT_1 = DEFAULT_WEEKLY_NOTE_FORMAT; var DEFAULT_YEARLY_NOTE_FORMAT_1 = DEFAULT_YEARLY_NOTE_FORMAT; var appHasDailyNotesPluginLoaded_1 = appHasDailyNotesPluginLoaded; var createDailyNote_1 = createDailyNote; var createMonthlyNote_1 = createMonthlyNote; var createQuarterlyNote_1 = createQuarterlyNote; var createWeeklyNote_1 = createWeeklyNote; var createYearlyNote_1 = createYearlyNote; var getAllDailyNotes_1 = getAllDailyNotes; var getAllMonthlyNotes_1 = getAllMonthlyNotes; var getAllQuarterlyNotes_1 = getAllQuarterlyNotes; var getAllWeeklyNotes_1 = getAllWeeklyNotes; var getAllYearlyNotes_1 = getAllYearlyNotes; var getDailyNote_1 = getDailyNote; var getDateFromFile_1 = getDateFromFile; var getMonthlyNote_1 = getMonthlyNote; var getQuarterlyNote_1 = getQuarterlyNote; var getWeeklyNote_1 = getWeeklyNote; var getYearlyNote_1 = getYearlyNote; const wrapAround = (value, size) => { return ((value % size) + size) % size; }; function orderedValues(unordered) { return Object.keys(unordered) .sort() .reduce((acc, key) => { acc.push(unordered[key]); return acc; }, []); } function getCalendarPlugin() { // eslint-disable-next-line @typescript-eslint/no-explicit-any return window.app.plugins.getPlugin("calendar"); } function getDailyNotesPlugin() { var _a; // eslint-disable-next-line @typescript-eslint/no-explicit-any const { internalPlugins } = window.app; return (_a = internalPlugins.getPluginById("daily-notes")) === null || _a === void 0 ? void 0 : _a.instance; } function capitalize(text) { return text.charAt(0).toUpperCase() + text.slice(1); } function hasLegacyDailyNoteSettings() { var _a; if (!appHasDailyNotesPluginLoaded_1()) { return false; } const options = (_a = getDailyNotesPlugin()) === null || _a === void 0 ? void 0 : _a.options; return !!(options.format || options.folder || options.template); } function getLegacyDailyNoteSettings() { var _a, _b; const options = getDailyNotesPlugin().options || {}; return { format: options.format, folder: (_a = options.folder) === null || _a === void 0 ? void 0 : _a.trim(), template: (_b = options.template) === null || _b === void 0 ? void 0 : _b.trim(), }; } function hasLegacyWeeklyNoteSettings() { const calendarPlugin = getCalendarPlugin(); if (!calendarPlugin) { return false; } const options = calendarPlugin.options || {}; return !!(options.weeklyNoteFormat || options.weeklyNoteFolder || options.weeklyNoteTemplate); } function getLegacyWeeklyNoteSettings() { var _a, _b; const options = getCalendarPlugin().options || {}; return { format: options.weeklyNoteFormat || "", folder: ((_a = options.weeklyNoteFolder) === null || _a === void 0 ? void 0 : _a.trim()) || "", template: ((_b = options.weeklyNoteTemplate) === null || _b === void 0 ? void 0 : _b.trim()) || "", }; } function isMacOS() { return navigator.appVersion.indexOf("Mac") !== -1; } function isMetaPressed(e) { return isMacOS() ? e.metaKey : e.ctrlKey; } const periodConfigs = { daily: { unitOfTime: "day", relativeUnit: "today", createNote: createDailyNote_1, getNote: getDailyNote_1, getAllNotes: getAllDailyNotes_1, }, weekly: { unitOfTime: "week", relativeUnit: "this week", createNote: createWeeklyNote_1, getNote: getWeeklyNote_1, getAllNotes: getAllWeeklyNotes_1, }, monthly: { unitOfTime: "month", relativeUnit: "this month", createNote: createMonthlyNote_1, getNote: getMonthlyNote_1, getAllNotes: getAllMonthlyNotes_1, }, quarterly: { unitOfTime: "quarter", relativeUnit: "this quarter", createNote: createQuarterlyNote_1, getNote: getQuarterlyNote_1, getAllNotes: getAllQuarterlyNotes_1, }, yearly: { unitOfTime: "year", relativeUnit: "this year", createNote: createYearlyNote_1, getNote: getYearlyNote_1, getAllNotes: getAllYearlyNotes_1, }, }; async function openPeriodicNote(periodicity, date, inNewSplit) { const config = periodConfigs[periodicity]; const startOfPeriod = date.clone().startOf(config.unitOfTime); let allNotes; try { allNotes = config.getAllNotes(); } catch (err) { console.error(`failed to find your ${periodicity} notes folder`, err); new obsidian.Notice(`Failed to find your ${periodicity} notes folder`); return; } let periodicNote = config.getNote(startOfPeriod, allNotes); if (!periodicNote) { periodicNote = await config.createNote(startOfPeriod); } await openFile(periodicNote, inNewSplit); } function getActiveFile() { const { workspace } = window.app; const activeView = workspace.getActiveViewOfType(obsidian.MarkdownView); return activeView === null || activeView === void 0 ? void 0 : activeView.file; } async function openFile(file, inNewSplit) { const { workspace } = window.app; const leaf = inNewSplit ? workspace.splitActiveLeaf() : workspace.getUnpinnedLeaf(); await leaf.openFile(file, { active: true }); } async function openNextNote(periodicity) { const config = periodConfigs[periodicity]; const activeFile = getActiveFile(); try { const allNotes = orderedValues(config.getAllNotes()); const activeNoteIndex = allNotes.findIndex((file) => file === activeFile); const nextNote = allNotes[activeNoteIndex + 1]; if (nextNote) { await openFile(nextNote, false); } } catch (err) { console.error(`failed to find your ${periodicity} notes folder`, err); new obsidian.Notice(`Failed to find your ${periodicity} notes folder`); } } async function openPrevNote(periodicity) { const config = periodConfigs[periodicity]; const activeFile = getActiveFile(); try { const allNotes = orderedValues(config.getAllNotes()); const activeNoteIndex = allNotes.findIndex((file) => file === activeFile); const prevNote = allNotes[activeNoteIndex - 1]; if (prevNote) { await openFile(prevNote, false); } } catch (err) { console.error(`failed to find your ${periodicity} notes folder`, err); new obsidian.Notice(`Failed to find your ${periodicity} notes folder`); } } function getCommands(periodicity) { const config = periodConfigs[periodicity]; return [ { id: `open-${periodicity}-note`, name: `Open ${periodicity} note`, callback: () => openPeriodicNote(periodicity, window.moment(), false), }, { id: `next-${periodicity}-note`, name: `Open next ${periodicity} note`, checkCallback: (checking) => { if (checking) { const activeFile = getActiveFile(); return !!(activeFile && getDateFromFile_1(activeFile, config.unitOfTime)); } openNextNote(periodicity); }, }, { id: `prev-${periodicity}-note`, name: `Open previous ${periodicity} note`, checkCallback: (checking) => { if (checking) { const activeFile = getActiveFile(); return !!(activeFile && getDateFromFile_1(activeFile, config.unitOfTime)); } openPrevNote(periodicity); }, }, ]; } const SETTINGS_UPDATED = "periodic-notes:settings-updated"; const calendarDayIcon = ` `; const calendarWeekIcon = ` `; const calendarMonthIcon = ` `; const calendarQuarterIcon = ` `; const calendarYearIcon = ` `; function showFileMenu(app, settings, position) { const contextMenu = new obsidian.Menu(app); ["daily", "weekly", "monthly"] .filter((periodicity) => settings[periodicity].enabled) .forEach((periodicity) => { const config = periodConfigs[periodicity]; contextMenu.addItem((item) => item .setTitle(`Open ${config.relativeUnit}`) .setIcon(`calendar-${config.unitOfTime}`) .onClick(() => { openPeriodicNote(periodicity, window.moment(), false); })); }); contextMenu.showAtPosition(position); } function noop() { } const identity = x => x; function run(fn) { return fn(); } function blank_object() { return Object.create(null); } function run_all(fns) { fns.forEach(run); } function is_function(thing) { return typeof thing === 'function'; } function safe_not_equal(a, b) { return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); } function is_empty(obj) { return Object.keys(obj).length === 0; } function subscribe(store, ...callbacks) { if (store == null) { return noop; } const unsub = store.subscribe(...callbacks); return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; } function component_subscribe(component, store, callback) { component.$$.on_destroy.push(subscribe(store, callback)); } function set_store_value(store, ret, value = ret) { store.set(value); return ret; } const is_client = typeof window !== 'undefined'; let now = is_client ? () => window.performance.now() : () => Date.now(); let raf = is_client ? cb => requestAnimationFrame(cb) : noop; const tasks = new Set(); function run_tasks(now) { tasks.forEach(task => { if (!task.c(now)) { tasks.delete(task); task.f(); } }); if (tasks.size !== 0) raf(run_tasks); } /** * Creates a new task that runs on each raf frame * until it returns a falsy value or is aborted */ function loop(callback) { let task; if (tasks.size === 0) raf(run_tasks); return { promise: new Promise(fulfill => { tasks.add(task = { c: callback, f: fulfill }); }), abort() { tasks.delete(task); } }; } function append(target, node) { target.appendChild(node); } function insert(target, node, anchor) { target.insertBefore(node, anchor || null); } function detach(node) { node.parentNode.removeChild(node); } function destroy_each(iterations, detaching) { for (let i = 0; i < iterations.length; i += 1) { if (iterations[i]) iterations[i].d(detaching); } } function element(name) { return document.createElement(name); } function svg_element(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } function text(data) { return document.createTextNode(data); } function space() { return text(' '); } function empty() { return text(''); } function listen(node, event, handler, options) { node.addEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options); } function attr(node, attribute, value) { if (value == null) node.removeAttribute(attribute); else if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value); } function children(element) { return Array.from(element.childNodes); } function set_data(text, data) { data = '' + data; if (text.wholeText !== data) text.data = data; } function set_input_value(input, value) { input.value = value == null ? '' : value; } function toggle_class(element, name, toggle) { element.classList[toggle ? 'add' : 'remove'](name); } function custom_event(type, detail) { const e = document.createEvent('CustomEvent'); e.initCustomEvent(type, false, false, detail); return e; } const active_docs = new Set(); let active = 0; // https://github.com/darkskyapp/string-hash/blob/master/index.js function hash$2(str) { let hash = 5381; let i = str.length; while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); return hash >>> 0; } function create_rule(node, a, b, duration, delay, ease, fn, uid = 0) { const step = 16.666 / duration; let keyframes = '{\n'; for (let p = 0; p <= 1; p += step) { const t = a + (b - a) * ease(p); keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; } const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; const name = `__svelte_${hash$2(rule)}_${uid}`; const doc = node.ownerDocument; active_docs.add(doc); const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style')).sheet); const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {}); if (!current_rules[name]) { current_rules[name] = true; stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); } const animation = node.style.animation || ''; node.style.animation = `${animation ? `${animation}, ` : ''}${name} ${duration}ms linear ${delay}ms 1 both`; active += 1; return name; } function delete_rule(node, name) { const previous = (node.style.animation || '').split(', '); const next = previous.filter(name ? anim => anim.indexOf(name) < 0 // remove specific animation : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations ); const deleted = previous.length - next.length; if (deleted) { node.style.animation = next.join(', '); active -= deleted; if (!active) clear_rules(); } } function clear_rules() { raf(() => { if (active) return; active_docs.forEach(doc => { const stylesheet = doc.__svelte_stylesheet; let i = stylesheet.cssRules.length; while (i--) stylesheet.deleteRule(i); doc.__svelte_rules = {}; }); active_docs.clear(); }); } let current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error('Function called outside component initialization'); return current_component; } function onMount(fn) { get_current_component().$$.on_mount.push(fn); } function onDestroy(fn) { get_current_component().$$.on_destroy.push(fn); } const dirty_components = []; const binding_callbacks = []; const render_callbacks = []; const flush_callbacks = []; const resolved_promise = Promise.resolve(); let update_scheduled = false; function schedule_update() { if (!update_scheduled) { update_scheduled = true; resolved_promise.then(flush); } } function add_render_callback(fn) { render_callbacks.push(fn); } let flushing = false; const seen_callbacks = new Set(); function flush() { if (flushing) return; flushing = true; do { // first, call beforeUpdate functions // and update components for (let i = 0; i < dirty_components.length; i += 1) { const component = dirty_components[i]; set_current_component(component); update(component.$$); } set_current_component(null); dirty_components.length = 0; while (binding_callbacks.length) binding_callbacks.pop()(); // then, once components are updated, call // afterUpdate functions. This may cause // subsequent updates... for (let i = 0; i < render_callbacks.length; i += 1) { const callback = render_callbacks[i]; if (!seen_callbacks.has(callback)) { // ...so guard against infinite loops seen_callbacks.add(callback); callback(); } } render_callbacks.length = 0; } while (dirty_components.length); while (flush_callbacks.length) { flush_callbacks.pop()(); } update_scheduled = false; flushing = false; seen_callbacks.clear(); } function update($$) { if ($$.fragment !== null) { $$.update(); run_all($$.before_update); const dirty = $$.dirty; $$.dirty = [-1]; $$.fragment && $$.fragment.p($$.ctx, dirty); $$.after_update.forEach(add_render_callback); } } let promise; function wait() { if (!promise) { promise = Promise.resolve(); promise.then(() => { promise = null; }); } return promise; } function dispatch(node, direction, kind) { node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); } const outroing = new Set(); let outros; function group_outros() { outros = { r: 0, c: [], p: outros // parent group }; } function check_outros() { if (!outros.r) { run_all(outros.c); } outros = outros.p; } function transition_in(block, local) { if (block && block.i) { outroing.delete(block); block.i(local); } } function transition_out(block, local, detach, callback) { if (block && block.o) { if (outroing.has(block)) return; outroing.add(block); outros.c.push(() => { outroing.delete(block); if (callback) { if (detach) block.d(1); callback(); } }); block.o(local); } } const null_transition = { duration: 0 }; function create_in_transition(node, fn, params) { let config = fn(node, params); let running = false; let animation_name; let task; let uid = 0; function cleanup() { if (animation_name) delete_rule(node, animation_name); } function go() { const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition; if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++); tick(0, 1); const start_time = now() + delay; const end_time = start_time + duration; if (task) task.abort(); running = true; add_render_callback(() => dispatch(node, true, 'start')); task = loop(now => { if (running) { if (now >= end_time) { tick(1, 0); dispatch(node, true, 'end'); cleanup(); return running = false; } if (now >= start_time) { const t = easing((now - start_time) / duration); tick(t, 1 - t); } } return running; }); } let started = false; return { start() { if (started) return; delete_rule(node); if (is_function(config)) { config = config(); wait().then(go); } else { go(); } }, invalidate() { started = false; }, end() { if (running) { cleanup(); running = false; } } }; } function create_out_transition(node, fn, params) { let config = fn(node, params); let running = true; let animation_name; const group = outros; group.r += 1; function go() { const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition; if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css); const start_time = now() + delay; const end_time = start_time + duration; add_render_callback(() => dispatch(node, false, 'start')); loop(now => { if (running) { if (now >= end_time) { tick(0, 1); dispatch(node, false, 'end'); if (!--group.r) { // this will result in `end()` being called, // so we don't need to clean up here run_all(group.c); } return false; } if (now >= start_time) { const t = easing((now - start_time) / duration); tick(1 - t, t); } } return running; }); } if (is_function(config)) { wait().then(() => { // @ts-ignore config = config(); go(); }); } else { go(); } return { end(reset) { if (reset && config.tick) { config.tick(1, 0); } if (running) { if (animation_name) delete_rule(node, animation_name); running = false; } } }; } function create_component(block) { block && block.c(); } function mount_component(component, target, anchor, customElement) { const { fragment, on_mount, on_destroy, after_update } = component.$$; fragment && fragment.m(target, anchor); if (!customElement) { // onMount happens before the initial afterUpdate add_render_callback(() => { const new_on_destroy = on_mount.map(run).filter(is_function); if (on_destroy) { on_destroy.push(...new_on_destroy); } else { // Edge case - component was destroyed immediately, // most likely as a result of a binding initialising run_all(new_on_destroy); } component.$$.on_mount = []; }); } after_update.forEach(add_render_callback); } function destroy_component(component, detaching) { const $$ = component.$$; if ($$.fragment !== null) { run_all($$.on_destroy); $$.fragment && $$.fragment.d(detaching); // TODO null out other refs, including component.$$ (but need to // preserve final state?) $$.on_destroy = $$.fragment = null; $$.ctx = []; } } function make_dirty(component, i) { if (component.$$.dirty[0] === -1) { dirty_components.push(component); schedule_update(); component.$$.dirty.fill(0); } component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); } function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { const parent_component = current_component; set_current_component(component); const $$ = component.$$ = { fragment: null, ctx: null, // state props, update: noop, not_equal, bound: blank_object(), // lifecycle on_mount: [], on_destroy: [], on_disconnect: [], before_update: [], after_update: [], context: new Map(parent_component ? parent_component.$$.context : []), // everything else callbacks: blank_object(), dirty, skip_bound: false }; let ready = false; $$.ctx = instance ? instance(component, options.props || {}, (i, ret, ...rest) => { const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (ready) make_dirty(component, i); } return ret; }) : []; $$.update(); ready = true; run_all($$.before_update); // `false` as a special case of no DOM component $$.fragment = create_fragment ? create_fragment($$.ctx) : false; if (options.target) { if (options.hydrate) { const nodes = children(options.target); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment.l(nodes); nodes.forEach(detach); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment.c(); } if (options.intro) transition_in(component.$$.fragment); mount_component(component, options.target, options.anchor, options.customElement); flush(); } set_current_component(parent_component); } /** * Base class for Svelte components. Used when dev=false. */ class SvelteComponent { $destroy() { destroy_component(this, 1); this.$destroy = noop; } $on(type, callback) { const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); callbacks.push(callback); return () => { const index = callbacks.indexOf(callback); if (index !== -1) callbacks.splice(index, 1); }; } $set($$props) { if (this.$$set && !is_empty($$props)) { this.$$.skip_bound = true; this.$$set($$props); this.$$.skip_bound = false; } } } const subscriber_queue = []; /** * Create a `Writable` store that allows both updating and reading by subscription. * @param {*=}value initial value * @param {StartStopNotifier=}start start and stop notifications for subscriptions */ function writable(value, start = noop) { let stop; const subscribers = []; function set(new_value) { if (safe_not_equal(value, new_value)) { value = new_value; if (stop) { // store is ready const run_queue = !subscriber_queue.length; for (let i = 0; i < subscribers.length; i += 1) { const s = subscribers[i]; s[1](); subscriber_queue.push(s, value); } if (run_queue) { for (let i = 0; i < subscriber_queue.length; i += 2) { subscriber_queue[i][0](subscriber_queue[i + 1]); } subscriber_queue.length = 0; } } } } function update(fn) { set(fn(value)); } function subscribe(run, invalidate = noop) { const subscriber = [run, invalidate]; subscribers.push(subscriber); if (subscribers.length === 1) { stop = start(set) || noop; } run(value); return () => { const index = subscribers.indexOf(subscriber); if (index !== -1) { subscribers.splice(index, 1); } if (subscribers.length === 0) { stop(); stop = null; } }; } return { set, update, subscribe }; } function cubicOut(t) { const f = t - 1.0; return f * f * f + 1.0; } function slide(node, { delay = 0, duration = 400, easing = cubicOut } = {}) { const style = getComputedStyle(node); const opacity = +style.opacity; const height = parseFloat(style.height); const padding_top = parseFloat(style.paddingTop); const padding_bottom = parseFloat(style.paddingBottom); const margin_top = parseFloat(style.marginTop); const margin_bottom = parseFloat(style.marginBottom); const border_top_width = parseFloat(style.borderTopWidth); const border_bottom_width = parseFloat(style.borderBottomWidth); return { delay, duration, easing, css: t => 'overflow: hidden;' + `opacity: ${Math.min(t * 20, 1) * opacity};` + `height: ${t * height}px;` + `padding-top: ${t * padding_top}px;` + `padding-bottom: ${t * padding_bottom}px;` + `margin-top: ${t * margin_top}px;` + `margin-bottom: ${t * margin_bottom}px;` + `border-top-width: ${t * border_top_width}px;` + `border-bottom-width: ${t * border_bottom_width}px;` }; } /* src/settings/Checkmark.svelte generated by Svelte v3.35.0 */ function add_css$1() { var style = element("style"); style.id = "svelte-1q3q9tf-style"; style.textContent = ".check.svelte-1q3q9tf{margin-left:6px;width:12px;height:12px}"; append(document.head, style); } function create_fragment$5(ctx) { let svg; let path; return { c() { svg = svg_element("svg"); path = svg_element("path"); attr(path, "fill", "currentColor"); attr(path, "d", "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"); attr(svg, "aria-hidden", "true"); attr(svg, "focusable", "false"); attr(svg, "class", "check svelte-1q3q9tf"); attr(svg, "data-icon", "check"); attr(svg, "role", "img"); attr(svg, "xmlns", "http://www.w3.org/2000/svg"); attr(svg, "viewBox", "0 0 512 512"); }, m(target, anchor) { insert(target, svg, anchor); append(svg, path); }, p: noop, i: noop, o: noop, d(detaching) { if (detaching) detach(svg); } }; } class Checkmark extends SvelteComponent { constructor(options) { super(); if (!document.getElementById("svelte-1q3q9tf-style")) add_css$1(); init(this, options, null, create_fragment$5, safe_not_equal, {}); } } /* src/settings/GettingStartedBanner.svelte generated by Svelte v3.35.0 */ function add_css() { var style = element("style"); style.id = "svelte-1alo0m9-style"; style.textContent = "button.svelte-1alo0m9{display:flex;align-items:center}"; append(document.head, style); } // (20:2) {#if hasDailyNoteSettings} function create_if_block_3(ctx) { let div2; let div0; let h4; let t1; let t2; let div1; let current_block_type_index; let if_block1; let current; function select_block_type(ctx, dirty) { if (/*$settings*/ ctx[5].hasMigratedDailyNoteSettings) return create_if_block_5; return create_else_block_2; } let current_block_type = select_block_type(ctx); let if_block0 = current_block_type(ctx); const if_block_creators = [create_if_block_4, create_else_block_1]; const if_blocks = []; function select_block_type_1(ctx, dirty) { if (/*$settings*/ ctx[5].hasMigratedDailyNoteSettings) return 0; return 1; } current_block_type_index = select_block_type_1(ctx); if_block1 = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); return { c() { div2 = element("div"); div0 = element("div"); h4 = element("h4"); h4.textContent = "Daily Notes plugin is enabled"; t1 = space(); if_block0.c(); t2 = space(); div1 = element("div"); if_block1.c(); attr(div0, "class", "setting-item-info"); attr(div1, "class", "setting-item-control"); attr(div2, "class", "setting-item"); }, m(target, anchor) { insert(target, div2, anchor); append(div2, div0); append(div0, h4); append(div0, t1); if_block0.m(div0, null); append(div2, t2); append(div2, div1); if_blocks[current_block_type_index].m(div1, null); current = true; }, p(ctx, dirty) { if (current_block_type !== (current_block_type = select_block_type(ctx))) { if_block0.d(1); if_block0 = current_block_type(ctx); if (if_block0) { if_block0.c(); if_block0.m(div0, null); } } let previous_block_index = current_block_type_index; current_block_type_index = select_block_type_1(ctx); if (current_block_type_index === previous_block_index) { if_blocks[current_block_type_index].p(ctx, dirty); } else { group_outros(); transition_out(if_blocks[previous_block_index], 1, 1, () => { if_blocks[previous_block_index] = null; }); check_outros(); if_block1 = if_blocks[current_block_type_index]; if (!if_block1) { if_block1 = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); if_block1.c(); } else { if_block1.p(ctx, dirty); } transition_in(if_block1, 1); if_block1.m(div1, null); } }, i(local) { if (current) return; transition_in(if_block1); current = true; }, o(local) { transition_out(if_block1); current = false; }, d(detaching) { if (detaching) detach(div2); if_block0.d(); if_blocks[current_block_type_index].d(); } }; } // (31:8) {:else} function create_else_block_2(ctx) { let p; return { c() { p = element("p"); p.textContent = "You are currently using the core Daily Notes plugin. You can migrate\n those settings over to Periodic Notes to enjoy the same\n functionality as well as some notable improvements"; attr(p, "class", "setting-item-description"); }, m(target, anchor) { insert(target, p, anchor); }, d(detaching) { if (detaching) detach(p); } }; } // (24:8) {#if $settings.hasMigratedDailyNoteSettings} function create_if_block_5(ctx) { let p; return { c() { p = element("p"); p.innerHTML = `You have successfully migrated your daily notes settings. You can now disable the Daily Notes core plugin to avoid any confusion.
If you have an custom hotkeys for daily notes, make sure to update them to use the new "Periodic Notes" commands.`; attr(p, "class", "setting-item-description"); }, m(target, anchor) { insert(target, p, anchor); }, d(detaching) { if (detaching) detach(p); } }; } // (42:8) {:else} function create_else_block_1(ctx) { let button; let mounted; let dispose; return { c() { button = element("button"); button.textContent = "Migrate"; attr(button, "class", "mod-cta svelte-1alo0m9"); }, m(target, anchor) { insert(target, button, anchor); if (!mounted) { dispose = listen(button, "click", function () { if (is_function(/*migrateDailyNoteSettings*/ ctx[2])) /*migrateDailyNoteSettings*/ ctx[2].apply(this, arguments); }); mounted = true; } }, p(new_ctx, dirty) { ctx = new_ctx; }, i: noop, o: noop, d(detaching) { if (detaching) detach(button); mounted = false; dispose(); } }; } // (40:8) {#if $settings.hasMigratedDailyNoteSettings} function create_if_block_4(ctx) { let button; let t; let checkmark; let current; checkmark = new Checkmark({}); return { c() { button = element("button"); t = text("Migrated "); create_component(checkmark.$$.fragment); button.disabled = true; attr(button, "class", "svelte-1alo0m9"); }, m(target, anchor) { insert(target, button, anchor); append(button, t); mount_component(checkmark, button, null); current = true; }, p: noop, i(local) { if (current) return; transition_in(checkmark.$$.fragment, local); current = true; }, o(local) { transition_out(checkmark.$$.fragment, local); current = false; }, d(detaching) { if (detaching) detach(button); destroy_component(checkmark); } }; } // (51:2) {#if hasWeeklyNoteSettings} function create_if_block_1$2(ctx) { let div2; let div0; let t3; let div1; let current_block_type_index; let if_block; let current; const if_block_creators = [create_if_block_2, create_else_block]; const if_blocks = []; function select_block_type_2(ctx, dirty) { if (/*$settings*/ ctx[5].hasMigratedWeeklyNoteSettings) return 0; return 1; } current_block_type_index = select_block_type_2(ctx); if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); return { c() { div2 = element("div"); div0 = element("div"); div0.innerHTML = `

Weekly Note settings migrated

Your existing weekly-note settings from the Calendar plugin have been migrated over automatically. The functionality will be removed from the Calendar plugin in the future.

`; t3 = space(); div1 = element("div"); if_block.c(); attr(div0, "class", "setting-item-info"); attr(div1, "class", "setting-item-control"); attr(div2, "class", "setting-item"); }, m(target, anchor) { insert(target, div2, anchor); append(div2, div0); append(div2, t3); append(div2, div1); if_blocks[current_block_type_index].m(div1, null); current = true; }, p(ctx, dirty) { let previous_block_index = current_block_type_index; current_block_type_index = select_block_type_2(ctx); if (current_block_type_index !== previous_block_index) { group_outros(); transition_out(if_blocks[previous_block_index], 1, 1, () => { if_blocks[previous_block_index] = null; }); check_outros(); if_block = if_blocks[current_block_type_index]; if (!if_block) { if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); if_block.c(); } transition_in(if_block, 1); if_block.m(div1, null); } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if (detaching) detach(div2); if_blocks[current_block_type_index].d(); } }; } // (67:8) {:else} function create_else_block(ctx) { let button; return { c() { button = element("button"); button.textContent = "Migrate"; attr(button, "class", "mod-cta svelte-1alo0m9"); }, m(target, anchor) { insert(target, button, anchor); }, i: noop, o: noop, d(detaching) { if (detaching) detach(button); } }; } // (62:8) {#if $settings.hasMigratedWeeklyNoteSettings} function create_if_block_2(ctx) { let button; let t; let checkmark; let current; checkmark = new Checkmark({}); return { c() { button = element("button"); t = text("Migrated\n "); create_component(checkmark.$$.fragment); button.disabled = true; attr(button, "class", "svelte-1alo0m9"); }, m(target, anchor) { insert(target, button, anchor); append(button, t); mount_component(checkmark, button, null); current = true; }, i(local) { if (current) return; transition_in(checkmark.$$.fragment, local); current = true; }, o(local) { transition_out(checkmark.$$.fragment, local); current = false; }, d(detaching) { if (detaching) detach(button); destroy_component(checkmark); } }; } // (74:2) {#if !hasDailyNoteSettings && !hasWeeklyNoteSettings} function create_if_block$4(ctx) { let p; return { c() { p = element("p"); p.textContent = "With this plugin, you can quickly create and navigate to daily, weekly,\n and monthly notes. Enable them below to get started."; }, m(target, anchor) { insert(target, p, anchor); }, d(detaching) { if (detaching) detach(p); } }; } function create_fragment$4(ctx) { let div; let h3; let t1; let t2; let t3; let t4; let button; let div_outro; let current; let mounted; let dispose; let if_block0 = /*hasDailyNoteSettings*/ ctx[3] && create_if_block_3(ctx); let if_block1 = /*hasWeeklyNoteSettings*/ ctx[4] && create_if_block_1$2(ctx); let if_block2 = !/*hasDailyNoteSettings*/ ctx[3] && !/*hasWeeklyNoteSettings*/ ctx[4] && create_if_block$4(); return { c() { div = element("div"); h3 = element("h3"); h3.textContent = "Getting Started"; t1 = space(); if (if_block0) if_block0.c(); t2 = space(); if (if_block1) if_block1.c(); t3 = space(); if (if_block2) if_block2.c(); t4 = space(); button = element("button"); button.textContent = "Dismiss"; attr(button, "class", "svelte-1alo0m9"); attr(div, "class", "settings-banner"); }, m(target, anchor) { insert(target, div, anchor); append(div, h3); append(div, t1); if (if_block0) if_block0.m(div, null); append(div, t2); if (if_block1) if_block1.m(div, null); append(div, t3); if (if_block2) if_block2.m(div, null); append(div, t4); append(div, button); current = true; if (!mounted) { dispose = listen(button, "click", function () { if (is_function(/*handleTeardown*/ ctx[1])) /*handleTeardown*/ ctx[1].apply(this, arguments); }); mounted = true; } }, p(new_ctx, [dirty]) { ctx = new_ctx; if (/*hasDailyNoteSettings*/ ctx[3]) { if (if_block0) { if_block0.p(ctx, dirty); if (dirty & /*hasDailyNoteSettings*/ 8) { transition_in(if_block0, 1); } } else { if_block0 = create_if_block_3(ctx); if_block0.c(); transition_in(if_block0, 1); if_block0.m(div, t2); } } else if (if_block0) { group_outros(); transition_out(if_block0, 1, 1, () => { if_block0 = null; }); check_outros(); } if (/*hasWeeklyNoteSettings*/ ctx[4]) { if (if_block1) { if_block1.p(ctx, dirty); if (dirty & /*hasWeeklyNoteSettings*/ 16) { transition_in(if_block1, 1); } } else { if_block1 = create_if_block_1$2(ctx); if_block1.c(); transition_in(if_block1, 1); if_block1.m(div, t3); } } else if (if_block1) { group_outros(); transition_out(if_block1, 1, 1, () => { if_block1 = null; }); check_outros(); } if (!/*hasDailyNoteSettings*/ ctx[3] && !/*hasWeeklyNoteSettings*/ ctx[4]) { if (if_block2) ; else { if_block2 = create_if_block$4(); if_block2.c(); if_block2.m(div, t4); } } else if (if_block2) { if_block2.d(1); if_block2 = null; } }, i(local) { if (current) return; transition_in(if_block0); transition_in(if_block1); if (div_outro) div_outro.end(1); current = true; }, o(local) { transition_out(if_block0); transition_out(if_block1); div_outro = create_out_transition(div, slide, {}); current = false; }, d(detaching) { if (detaching) detach(div); if (if_block0) if_block0.d(); if (if_block1) if_block1.d(); if (if_block2) if_block2.d(); if (detaching && div_outro) div_outro.end(); mounted = false; dispose(); } }; } function instance$4($$self, $$props, $$invalidate) { let $settings, $$unsubscribe_settings = noop, $$subscribe_settings = () => ($$unsubscribe_settings(), $$unsubscribe_settings = subscribe(settings, $$value => $$invalidate(5, $settings = $$value)), settings); $$self.$$.on_destroy.push(() => $$unsubscribe_settings()); let { settings } = $$props; $$subscribe_settings(); let { handleTeardown } = $$props; let { migrateDailyNoteSettings } = $$props; let hasDailyNoteSettings; let hasWeeklyNoteSettings; $$self.$$set = $$props => { if ("settings" in $$props) $$subscribe_settings($$invalidate(0, settings = $$props.settings)); if ("handleTeardown" in $$props) $$invalidate(1, handleTeardown = $$props.handleTeardown); if ("migrateDailyNoteSettings" in $$props) $$invalidate(2, migrateDailyNoteSettings = $$props.migrateDailyNoteSettings); }; { $$invalidate(3, hasDailyNoteSettings = hasLegacyDailyNoteSettings()); $$invalidate(4, hasWeeklyNoteSettings = hasLegacyWeeklyNoteSettings()); } return [ settings, handleTeardown, migrateDailyNoteSettings, hasDailyNoteSettings, hasWeeklyNoteSettings, $settings ]; } class GettingStartedBanner extends SvelteComponent { constructor(options) { super(); if (!document.getElementById("svelte-1alo0m9-style")) add_css(); init(this, options, instance$4, create_fragment$4, safe_not_equal, { settings: 0, handleTeardown: 1, migrateDailyNoteSettings: 2 }); } } function getBasename(format) { const isTemplateNested = format.indexOf("/") !== -1; return isTemplateNested ? format.split("/").pop() : format; } function isValidFilename(filename) { const illegalRe = /[?<>\\:*|"]/g; const controlRe = /[\x00-\x1f\x80-\x9f]/g; const reservedRe = /^\.+$/; const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; return (!illegalRe.test(filename) && !controlRe.test(filename) && !reservedRe.test(filename) && !windowsReservedRe.test(filename)); } function validateFormat(format, periodicity) { if (!format) { return ""; } if (!isValidFilename(format)) { return "Format contains illegal characters"; } if (periodicity === "daily" && !["m", "d", "y"].every((requiredChar) => getBasename(format) .replace(/\[[^\]]*\]/g, "") // remove everything within brackets .toLowerCase() .indexOf(requiredChar) !== -1)) { return "Filename must be unique"; } } function validateTemplate(template) { if (!template) { return ""; } const { metadataCache } = window.app; const file = metadataCache.getFirstLinkpathDest(template, ""); if (!file) { return "Template file not found"; } return ""; } function validateFolder(folder) { if (!folder || folder === "/") { return ""; } const { vault } = window.app; if (!vault.getAbstractFileByPath(obsidian.normalizePath(folder))) { return "Folder not found in vault"; } return ""; } /* src/settings/NoteFormatSetting.svelte generated by Svelte v3.35.0 */ function create_if_block_1$1(ctx) { let div; let t0; let strong0; let t1; let br; let t2; let strong1; let t3; return { c() { div = element("div"); t0 = text("New files will be created at "); strong0 = element("strong"); t1 = text(/*value*/ ctx[2]); br = element("br"); t2 = text("\n Format: "); strong1 = element("strong"); t3 = text(/*basename*/ ctx[7]); }, m(target, anchor) { insert(target, div, anchor); append(div, t0); append(div, strong0); append(strong0, t1); append(div, br); append(div, t2); append(div, strong1); append(strong1, t3); }, p(ctx, dirty) { if (dirty & /*value*/ 4) set_data(t1, /*value*/ ctx[2]); if (dirty & /*basename*/ 128) set_data(t3, /*basename*/ ctx[7]); }, d(detaching) { if (detaching) detach(div); } }; } // (56:4) {#if error} function create_if_block$3(ctx) { let div; let t; return { c() { div = element("div"); t = text(/*error*/ ctx[5]); attr(div, "class", "has-error"); }, m(target, anchor) { insert(target, div, anchor); append(div, t); }, p(ctx, dirty) { if (dirty & /*error*/ 32) set_data(t, /*error*/ ctx[5]); }, d(detaching) { if (detaching) detach(div); } }; } function create_fragment$3(ctx) { let div5; let div3; let div0; let t1; let div2; let a; let t3; let div1; let t4; let b; let t5_value = window.moment().format(/*value*/ ctx[2] || /*defaultFormat*/ ctx[8]) + ""; let t5; let t6; let t7; let t8; let div4; let input; let mounted; let dispose; let if_block0 = /*isTemplateNested*/ ctx[6] && create_if_block_1$1(ctx); let if_block1 = /*error*/ ctx[5] && create_if_block$3(ctx); return { c() { div5 = element("div"); div3 = element("div"); div0 = element("div"); div0.textContent = "Format"; t1 = space(); div2 = element("div"); a = element("a"); a.textContent = "Syntax Reference"; t3 = space(); div1 = element("div"); t4 = text("Your current syntax looks like this: "); b = element("b"); t5 = text(t5_value); t6 = space(); if (if_block0) if_block0.c(); t7 = space(); if (if_block1) if_block1.c(); t8 = space(); div4 = element("div"); input = element("input"); attr(div0, "class", "setting-item-name"); attr(a, "href", "https://momentjs.com/docs/#/displaying/format/"); attr(b, "class", "u-pop"); attr(div2, "class", "setting-item-description"); attr(div3, "class", "setting-item-info"); attr(input, "type", "text"); attr(input, "spellcheck", false); attr(input, "placeholder", /*defaultFormat*/ ctx[8]); toggle_class(input, "has-error", !!/*error*/ ctx[5]); attr(div4, "class", "setting-item-control"); attr(div5, "class", "setting-item"); }, m(target, anchor) { insert(target, div5, anchor); append(div5, div3); append(div3, div0); append(div3, t1); append(div3, div2); append(div2, a); append(div2, t3); append(div2, div1); append(div1, t4); append(div1, b); append(b, t5); append(div2, t6); if (if_block0) if_block0.m(div2, null); append(div3, t7); if (if_block1) if_block1.m(div3, null); append(div5, t8); append(div5, div4); append(div4, input); set_input_value(input, /*$settings*/ ctx[3][/*periodicity*/ ctx[1]].format); /*input_binding*/ ctx[12](input); if (!mounted) { dispose = [ listen(input, "input", /*input_input_handler*/ ctx[11]), listen(input, "change", /*onChange*/ ctx[10]), listen(input, "input", /*clearError*/ ctx[9]) ]; mounted = true; } }, p(ctx, [dirty]) { if (dirty & /*value*/ 4 && t5_value !== (t5_value = window.moment().format(/*value*/ ctx[2] || /*defaultFormat*/ ctx[8]) + "")) set_data(t5, t5_value); if (/*isTemplateNested*/ ctx[6]) { if (if_block0) { if_block0.p(ctx, dirty); } else { if_block0 = create_if_block_1$1(ctx); if_block0.c(); if_block0.m(div2, null); } } else if (if_block0) { if_block0.d(1); if_block0 = null; } if (/*error*/ ctx[5]) { if (if_block1) { if_block1.p(ctx, dirty); } else { if_block1 = create_if_block$3(ctx); if_block1.c(); if_block1.m(div3, null); } } else if (if_block1) { if_block1.d(1); if_block1 = null; } if (dirty & /*$settings, periodicity*/ 10 && input.value !== /*$settings*/ ctx[3][/*periodicity*/ ctx[1]].format) { set_input_value(input, /*$settings*/ ctx[3][/*periodicity*/ ctx[1]].format); } if (dirty & /*error*/ 32) { toggle_class(input, "has-error", !!/*error*/ ctx[5]); } }, i: noop, o: noop, d(detaching) { if (detaching) detach(div5); if (if_block0) if_block0.d(); if (if_block1) if_block1.d(); /*input_binding*/ ctx[12](null); mounted = false; run_all(dispose); } }; } function instance$3($$self, $$props, $$invalidate) { let $settings, $$unsubscribe_settings = noop, $$subscribe_settings = () => ($$unsubscribe_settings(), $$unsubscribe_settings = subscribe(settings, $$value => $$invalidate(3, $settings = $$value)), settings); $$self.$$.on_destroy.push(() => $$unsubscribe_settings()); let { settings } = $$props; $$subscribe_settings(); let { periodicity } = $$props; const DEFAULT_FORMATS = { daily: DEFAULT_DAILY_NOTE_FORMAT_1, weekly: DEFAULT_WEEKLY_NOTE_FORMAT_1, monthly: DEFAULT_MONTHLY_NOTE_FORMAT_1, quarterly: DEFAULT_QUARTERLY_NOTE_FORMAT_1, yearly: DEFAULT_YEARLY_NOTE_FORMAT_1 }; const defaultFormat = DEFAULT_FORMATS[periodicity]; let inputEl; let value; let error; let isTemplateNested; let basename; onMount(() => { $$invalidate(5, error = validateFormat(inputEl.value, periodicity)); }); function clearError() { $$invalidate(5, error = ""); } function onChange() { $$invalidate(5, error = validateFormat(inputEl.value, periodicity)); } function input_input_handler() { $settings[periodicity].format = this.value; settings.set($settings); $$invalidate(1, periodicity); } function input_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { inputEl = $$value; $$invalidate(4, inputEl); }); } $$self.$$set = $$props => { if ("settings" in $$props) $$subscribe_settings($$invalidate(0, settings = $$props.settings)); if ("periodicity" in $$props) $$invalidate(1, periodicity = $$props.periodicity); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*$settings, periodicity, value*/ 14) { { $$invalidate(2, value = $settings[periodicity].format || ""); $$invalidate(6, isTemplateNested = value.indexOf("/") !== -1); $$invalidate(7, basename = getBasename(value)); } } }; return [ settings, periodicity, value, $settings, inputEl, error, isTemplateNested, basename, defaultFormat, clearError, onChange, input_input_handler, input_binding ]; } class NoteFormatSetting extends SvelteComponent { constructor(options) { super(); init(this, options, instance$3, create_fragment$3, safe_not_equal, { settings: 0, periodicity: 1 }); } } var top = 'top'; var bottom = 'bottom'; var right = 'right'; var left = 'left'; var auto = 'auto'; var basePlacements = [top, bottom, right, left]; var start = 'start'; var end = 'end'; var clippingParents = 'clippingParents'; var viewport = 'viewport'; var popper = 'popper'; var reference = 'reference'; var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) { return acc.concat([placement + "-" + start, placement + "-" + end]); }, []); var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) { return acc.concat([placement, placement + "-" + start, placement + "-" + end]); }, []); // modifiers that need to read the DOM var beforeRead = 'beforeRead'; var read = 'read'; var afterRead = 'afterRead'; // pure-logic modifiers var beforeMain = 'beforeMain'; var main = 'main'; var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state) var beforeWrite = 'beforeWrite'; var write = 'write'; var afterWrite = 'afterWrite'; var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; function getNodeName(element) { return element ? (element.nodeName || '').toLowerCase() : null; } function getWindow(node) { if (node == null) { return window; } if (node.toString() !== '[object Window]') { var ownerDocument = node.ownerDocument; return ownerDocument ? ownerDocument.defaultView || window : window; } return node; } function isElement(node) { var OwnElement = getWindow(node).Element; return node instanceof OwnElement || node instanceof Element; } function isHTMLElement(node) { var OwnElement = getWindow(node).HTMLElement; return node instanceof OwnElement || node instanceof HTMLElement; } function isShadowRoot(node) { // IE 11 has no ShadowRoot if (typeof ShadowRoot === 'undefined') { return false; } var OwnElement = getWindow(node).ShadowRoot; return node instanceof OwnElement || node instanceof ShadowRoot; } // and applies them to the HTMLElements such as popper and arrow function applyStyles(_ref) { var state = _ref.state; Object.keys(state.elements).forEach(function (name) { var style = state.styles[name] || {}; var attributes = state.attributes[name] || {}; var element = state.elements[name]; // arrow is optional + virtual elements if (!isHTMLElement(element) || !getNodeName(element)) { return; } // Flow doesn't support to extend this property, but it's the most // effective way to apply styles to an HTMLElement // $FlowFixMe[cannot-write] Object.assign(element.style, style); Object.keys(attributes).forEach(function (name) { var value = attributes[name]; if (value === false) { element.removeAttribute(name); } else { element.setAttribute(name, value === true ? '' : value); } }); }); } function effect$2(_ref2) { var state = _ref2.state; var initialStyles = { popper: { position: state.options.strategy, left: '0', top: '0', margin: '0' }, arrow: { position: 'absolute' }, reference: {} }; Object.assign(state.elements.popper.style, initialStyles.popper); state.styles = initialStyles; if (state.elements.arrow) { Object.assign(state.elements.arrow.style, initialStyles.arrow); } return function () { Object.keys(state.elements).forEach(function (name) { var element = state.elements[name]; var attributes = state.attributes[name] || {}; var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them var style = styleProperties.reduce(function (style, property) { style[property] = ''; return style; }, {}); // arrow is optional + virtual elements if (!isHTMLElement(element) || !getNodeName(element)) { return; } Object.assign(element.style, style); Object.keys(attributes).forEach(function (attribute) { element.removeAttribute(attribute); }); }); }; } // eslint-disable-next-line import/no-unused-modules var applyStyles$1 = { name: 'applyStyles', enabled: true, phase: 'write', fn: applyStyles, effect: effect$2, requires: ['computeStyles'] }; function getBasePlacement(placement) { return placement.split('-')[0]; } function getBoundingClientRect(element) { var rect = element.getBoundingClientRect(); return { width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, x: rect.left, y: rect.top }; } // means it doesn't take into account transforms. function getLayoutRect(element) { var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed. // Fixes https://github.com/popperjs/popper-core/issues/1223 var width = element.offsetWidth; var height = element.offsetHeight; if (Math.abs(clientRect.width - width) <= 1) { width = clientRect.width; } if (Math.abs(clientRect.height - height) <= 1) { height = clientRect.height; } return { x: element.offsetLeft, y: element.offsetTop, width: width, height: height }; } function contains(parent, child) { var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method if (parent.contains(child)) { return true; } // then fallback to custom implementation with Shadow DOM support else if (rootNode && isShadowRoot(rootNode)) { var next = child; do { if (next && parent.isSameNode(next)) { return true; } // $FlowFixMe[prop-missing]: need a better way to handle this... next = next.parentNode || next.host; } while (next); } // Give up, the result is false return false; } function getComputedStyle$1(element) { return getWindow(element).getComputedStyle(element); } function isTableElement(element) { return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0; } function getDocumentElement(element) { // $FlowFixMe[incompatible-return]: assume body is always available return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing] element.document) || window.document).documentElement; } function getParentNode(element) { if (getNodeName(element) === 'html') { return element; } return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle // $FlowFixMe[incompatible-return] // $FlowFixMe[prop-missing] element.assignedSlot || // step into the shadow DOM of the parent of a slotted node element.parentNode || ( // DOM Element detected isShadowRoot(element) ? element.host : null) || // ShadowRoot detected // $FlowFixMe[incompatible-call]: HTMLElement is a Node getDocumentElement(element) // fallback ); } function getTrueOffsetParent(element) { if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837 getComputedStyle$1(element).position === 'fixed') { return null; } return element.offsetParent; } // `.offsetParent` reports `null` for fixed elements, while absolute elements // return the containing block function getContainingBlock(element) { var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') !== -1; var currentNode = getParentNode(element); while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) { var css = getComputedStyle$1(currentNode); // This is non-exhaustive but covers the most common CSS properties that // create a containing block. // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') { return currentNode; } else { currentNode = currentNode.parentNode; } } return null; } // Gets the closest ancestor positioned element. Handles some edge cases, // such as table ancestors and cross browser bugs. function getOffsetParent(element) { var window = getWindow(element); var offsetParent = getTrueOffsetParent(element); while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') { offsetParent = getTrueOffsetParent(offsetParent); } if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static')) { return window; } return offsetParent || getContainingBlock(element) || window; } function getMainAxisFromPlacement(placement) { return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y'; } var max = Math.max; var min = Math.min; var round = Math.round; function within(min$1, value, max$1) { return max(min$1, min(value, max$1)); } function getFreshSideObject() { return { top: 0, right: 0, bottom: 0, left: 0 }; } function mergePaddingObject(paddingObject) { return Object.assign({}, getFreshSideObject(), paddingObject); } function expandToHashMap(value, keys) { return keys.reduce(function (hashMap, key) { hashMap[key] = value; return hashMap; }, {}); } var toPaddingObject = function toPaddingObject(padding, state) { padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, { placement: state.placement })) : padding; return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); }; function arrow(_ref) { var _state$modifiersData$; var state = _ref.state, name = _ref.name, options = _ref.options; var arrowElement = state.elements.arrow; var popperOffsets = state.modifiersData.popperOffsets; var basePlacement = getBasePlacement(state.placement); var axis = getMainAxisFromPlacement(basePlacement); var isVertical = [left, right].indexOf(basePlacement) >= 0; var len = isVertical ? 'height' : 'width'; if (!arrowElement || !popperOffsets) { return; } var paddingObject = toPaddingObject(options.padding, state); var arrowRect = getLayoutRect(arrowElement); var minProp = axis === 'y' ? top : left; var maxProp = axis === 'y' ? bottom : right; var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len]; var startDiff = popperOffsets[axis] - state.rects.reference[axis]; var arrowOffsetParent = getOffsetParent(arrowElement); var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is // outside of the popper bounds var min = paddingObject[minProp]; var max = clientSize - arrowRect[len] - paddingObject[maxProp]; var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; var offset = within(min, center, max); // Prevents breaking syntax highlighting... var axisProp = axis; state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$); } function effect$1(_ref2) { var state = _ref2.state, options = _ref2.options; var _options$element = options.element, arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element; if (arrowElement == null) { return; } // CSS selector if (typeof arrowElement === 'string') { arrowElement = state.elements.popper.querySelector(arrowElement); if (!arrowElement) { return; } } if (process.env.NODE_ENV !== "production") { if (!isHTMLElement(arrowElement)) { console.error(['Popper: "arrow" element must be an HTMLElement (not an SVGElement).', 'To use an SVG arrow, wrap it in an HTMLElement that will be used as', 'the arrow.'].join(' ')); } } if (!contains(state.elements.popper, arrowElement)) { if (process.env.NODE_ENV !== "production") { console.error(['Popper: "arrow" modifier\'s `element` must be a child of the popper', 'element.'].join(' ')); } return; } state.elements.arrow = arrowElement; } // eslint-disable-next-line import/no-unused-modules var arrow$1 = { name: 'arrow', enabled: true, phase: 'main', fn: arrow, effect: effect$1, requires: ['popperOffsets'], requiresIfExists: ['preventOverflow'] }; var unsetSides = { top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' }; // Round the offsets to the nearest suitable subpixel based on the DPR. // Zooming can change the DPR, but it seems to report a value that will // cleanly divide the values into the appropriate subpixels. function roundOffsetsByDPR(_ref) { var x = _ref.x, y = _ref.y; var win = window; var dpr = win.devicePixelRatio || 1; return { x: round(round(x * dpr) / dpr) || 0, y: round(round(y * dpr) / dpr) || 0 }; } function mapToStyles(_ref2) { var _Object$assign2; var popper = _ref2.popper, popperRect = _ref2.popperRect, placement = _ref2.placement, offsets = _ref2.offsets, position = _ref2.position, gpuAcceleration = _ref2.gpuAcceleration, adaptive = _ref2.adaptive, roundOffsets = _ref2.roundOffsets; var _ref3 = roundOffsets === true ? roundOffsetsByDPR(offsets) : typeof roundOffsets === 'function' ? roundOffsets(offsets) : offsets, _ref3$x = _ref3.x, x = _ref3$x === void 0 ? 0 : _ref3$x, _ref3$y = _ref3.y, y = _ref3$y === void 0 ? 0 : _ref3$y; var hasX = offsets.hasOwnProperty('x'); var hasY = offsets.hasOwnProperty('y'); var sideX = left; var sideY = top; var win = window; if (adaptive) { var offsetParent = getOffsetParent(popper); var heightProp = 'clientHeight'; var widthProp = 'clientWidth'; if (offsetParent === getWindow(popper)) { offsetParent = getDocumentElement(popper); if (getComputedStyle$1(offsetParent).position !== 'static') { heightProp = 'scrollHeight'; widthProp = 'scrollWidth'; } } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it offsetParent = offsetParent; if (placement === top) { sideY = bottom; // $FlowFixMe[prop-missing] y -= offsetParent[heightProp] - popperRect.height; y *= gpuAcceleration ? 1 : -1; } if (placement === left) { sideX = right; // $FlowFixMe[prop-missing] x -= offsetParent[widthProp] - popperRect.width; x *= gpuAcceleration ? 1 : -1; } } var commonStyles = Object.assign({ position: position }, adaptive && unsetSides); if (gpuAcceleration) { var _Object$assign; return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) < 2 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); } return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2)); } function computeStyles(_ref4) { var state = _ref4.state, options = _ref4.options; var _options$gpuAccelerat = options.gpuAcceleration, gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, _options$adaptive = options.adaptive, adaptive = _options$adaptive === void 0 ? true : _options$adaptive, _options$roundOffsets = options.roundOffsets, roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; if (process.env.NODE_ENV !== "production") { var transitionProperty = getComputedStyle$1(state.elements.popper).transitionProperty || ''; if (adaptive && ['transform', 'top', 'right', 'bottom', 'left'].some(function (property) { return transitionProperty.indexOf(property) >= 0; })) { console.warn(['Popper: Detected CSS transitions on at least one of the following', 'CSS properties: "transform", "top", "right", "bottom", "left".', '\n\n', 'Disable the "computeStyles" modifier\'s `adaptive` option to allow', 'for smooth transitions, or remove these properties from the CSS', 'transition declaration on the popper element if only transitioning', 'opacity or background-color for example.', '\n\n', 'We recommend using the popper element as a wrapper around an inner', 'element that can have any CSS property transitioned for animations.'].join(' ')); } } var commonStyles = { placement: getBasePlacement(state.placement), popper: state.elements.popper, popperRect: state.rects.popper, gpuAcceleration: gpuAcceleration }; if (state.modifiersData.popperOffsets != null) { state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { offsets: state.modifiersData.popperOffsets, position: state.options.strategy, adaptive: adaptive, roundOffsets: roundOffsets }))); } if (state.modifiersData.arrow != null) { state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { offsets: state.modifiersData.arrow, position: 'absolute', adaptive: false, roundOffsets: roundOffsets }))); } state.attributes.popper = Object.assign({}, state.attributes.popper, { 'data-popper-placement': state.placement }); } // eslint-disable-next-line import/no-unused-modules var computeStyles$1 = { name: 'computeStyles', enabled: true, phase: 'beforeWrite', fn: computeStyles, data: {} }; var passive = { passive: true }; function effect(_ref) { var state = _ref.state, instance = _ref.instance, options = _ref.options; var _options$scroll = options.scroll, scroll = _options$scroll === void 0 ? true : _options$scroll, _options$resize = options.resize, resize = _options$resize === void 0 ? true : _options$resize; var window = getWindow(state.elements.popper); var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); if (scroll) { scrollParents.forEach(function (scrollParent) { scrollParent.addEventListener('scroll', instance.update, passive); }); } if (resize) { window.addEventListener('resize', instance.update, passive); } return function () { if (scroll) { scrollParents.forEach(function (scrollParent) { scrollParent.removeEventListener('scroll', instance.update, passive); }); } if (resize) { window.removeEventListener('resize', instance.update, passive); } }; } // eslint-disable-next-line import/no-unused-modules var eventListeners = { name: 'eventListeners', enabled: true, phase: 'write', fn: function fn() {}, effect: effect, data: {} }; var hash$1 = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; function getOppositePlacement(placement) { return placement.replace(/left|right|bottom|top/g, function (matched) { return hash$1[matched]; }); } var hash = { start: 'end', end: 'start' }; function getOppositeVariationPlacement(placement) { return placement.replace(/start|end/g, function (matched) { return hash[matched]; }); } function getWindowScroll(node) { var win = getWindow(node); var scrollLeft = win.pageXOffset; var scrollTop = win.pageYOffset; return { scrollLeft: scrollLeft, scrollTop: scrollTop }; } function getWindowScrollBarX(element) { // If has a CSS width greater than the viewport, then this will be // incorrect for RTL. // Popper 1 is broken in this case and never had a bug report so let's assume // it's not an issue. I don't think anyone ever specifies width on // anyway. // Browsers where the left scrollbar doesn't cause an issue report `0` for // this (e.g. Edge 2019, IE11, Safari) return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; } function getViewportRect(element) { var win = getWindow(element); var html = getDocumentElement(element); var visualViewport = win.visualViewport; var width = html.clientWidth; var height = html.clientHeight; var x = 0; var y = 0; // NB: This isn't supported on iOS <= 12. If the keyboard is open, the popper // can be obscured underneath it. // Also, `html.clientHeight` adds the bottom bar height in Safari iOS, even // if it isn't open, so if this isn't available, the popper will be detected // to overflow the bottom of the screen too early. if (visualViewport) { width = visualViewport.width; height = visualViewport.height; // Uses Layout Viewport (like Chrome; Safari does not currently) // In Chrome, it returns a value very close to 0 (+/-) but contains rounding // errors due to floating point numbers, so we need to check precision. // Safari returns a number <= 0, usually < -1 when pinch-zoomed // Feature detection fails in mobile emulation mode in Chrome. // Math.abs(win.innerWidth / visualViewport.scale - visualViewport.width) < // 0.001 // Fallback here: "Not Safari" userAgent if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { x = visualViewport.offsetLeft; y = visualViewport.offsetTop; } } return { width: width, height: height, x: x + getWindowScrollBarX(element), y: y }; } // of the `` and `` rect bounds if horizontally scrollable function getDocumentRect(element) { var _element$ownerDocumen; var html = getDocumentElement(element); var winScroll = getWindowScroll(element); var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); var x = -winScroll.scrollLeft + getWindowScrollBarX(element); var y = -winScroll.scrollTop; if (getComputedStyle$1(body || html).direction === 'rtl') { x += max(html.clientWidth, body ? body.clientWidth : 0) - width; } return { width: width, height: height, x: x, y: y }; } function isScrollParent(element) { // Firefox wants us to check `-x` and `-y` variations as well var _getComputedStyle = getComputedStyle$1(element), overflow = _getComputedStyle.overflow, overflowX = _getComputedStyle.overflowX, overflowY = _getComputedStyle.overflowY; return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); } function getScrollParent(node) { if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) { // $FlowFixMe[incompatible-return]: assume body is always available return node.ownerDocument.body; } if (isHTMLElement(node) && isScrollParent(node)) { return node; } return getScrollParent(getParentNode(node)); } /* given a DOM element, return the list of all scroll parents, up the list of ancesors until we get to the top window object. This list is what we attach scroll listeners to, because if any of these parent elements scroll, we'll need to re-calculate the reference element's position. */ function listScrollParents(element, list) { var _element$ownerDocumen; if (list === void 0) { list = []; } var scrollParent = getScrollParent(element); var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); var win = getWindow(scrollParent); var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; var updatedList = list.concat(target); return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here updatedList.concat(listScrollParents(getParentNode(target))); } function rectToClientRect(rect) { return Object.assign({}, rect, { left: rect.x, top: rect.y, right: rect.x + rect.width, bottom: rect.y + rect.height }); } function getInnerBoundingClientRect(element) { var rect = getBoundingClientRect(element); rect.top = rect.top + element.clientTop; rect.left = rect.left + element.clientLeft; rect.bottom = rect.top + element.clientHeight; rect.right = rect.left + element.clientWidth; rect.width = element.clientWidth; rect.height = element.clientHeight; rect.x = rect.left; rect.y = rect.top; return rect; } function getClientRectFromMixedType(element, clippingParent) { return clippingParent === viewport ? rectToClientRect(getViewportRect(element)) : isHTMLElement(clippingParent) ? getInnerBoundingClientRect(clippingParent) : rectToClientRect(getDocumentRect(getDocumentElement(element))); } // A "clipping parent" is an overflowable container with the characteristic of // clipping (or hiding) overflowing elements with a position different from // `initial` function getClippingParents(element) { var clippingParents = listScrollParents(getParentNode(element)); var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle$1(element).position) >= 0; var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; if (!isElement(clipperElement)) { return []; } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414 return clippingParents.filter(function (clippingParent) { return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body'; }); } // Gets the maximum area that the element is visible in due to any number of // clipping parents function getClippingRect(element, boundary, rootBoundary) { var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary); var clippingParents = [].concat(mainClippingParents, [rootBoundary]); var firstClippingParent = clippingParents[0]; var clippingRect = clippingParents.reduce(function (accRect, clippingParent) { var rect = getClientRectFromMixedType(element, clippingParent); accRect.top = max(rect.top, accRect.top); accRect.right = min(rect.right, accRect.right); accRect.bottom = min(rect.bottom, accRect.bottom); accRect.left = max(rect.left, accRect.left); return accRect; }, getClientRectFromMixedType(element, firstClippingParent)); clippingRect.width = clippingRect.right - clippingRect.left; clippingRect.height = clippingRect.bottom - clippingRect.top; clippingRect.x = clippingRect.left; clippingRect.y = clippingRect.top; return clippingRect; } function getVariation(placement) { return placement.split('-')[1]; } function computeOffsets(_ref) { var reference = _ref.reference, element = _ref.element, placement = _ref.placement; var basePlacement = placement ? getBasePlacement(placement) : null; var variation = placement ? getVariation(placement) : null; var commonX = reference.x + reference.width / 2 - element.width / 2; var commonY = reference.y + reference.height / 2 - element.height / 2; var offsets; switch (basePlacement) { case top: offsets = { x: commonX, y: reference.y - element.height }; break; case bottom: offsets = { x: commonX, y: reference.y + reference.height }; break; case right: offsets = { x: reference.x + reference.width, y: commonY }; break; case left: offsets = { x: reference.x - element.width, y: commonY }; break; default: offsets = { x: reference.x, y: reference.y }; } var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; if (mainAxis != null) { var len = mainAxis === 'y' ? 'height' : 'width'; switch (variation) { case start: offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2); break; case end: offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2); break; } } return offsets; } function detectOverflow(state, options) { if (options === void 0) { options = {}; } var _options = options, _options$placement = _options.placement, placement = _options$placement === void 0 ? state.placement : _options$placement, _options$boundary = _options.boundary, boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, _options$rootBoundary = _options.rootBoundary, rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, _options$elementConte = _options.elementContext, elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, _options$altBoundary = _options.altBoundary, altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, _options$padding = _options.padding, padding = _options$padding === void 0 ? 0 : _options$padding; var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); var altContext = elementContext === popper ? reference : popper; var referenceElement = state.elements.reference; var popperRect = state.rects.popper; var element = state.elements[altBoundary ? altContext : elementContext]; var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary); var referenceClientRect = getBoundingClientRect(referenceElement); var popperOffsets = computeOffsets({ reference: referenceClientRect, element: popperRect, strategy: 'absolute', placement: placement }); var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets)); var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect // 0 or negative = within the clipping rect var overflowOffsets = { top: clippingClientRect.top - elementClientRect.top + paddingObject.top, bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, left: clippingClientRect.left - elementClientRect.left + paddingObject.left, right: elementClientRect.right - clippingClientRect.right + paddingObject.right }; var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element if (elementContext === popper && offsetData) { var offset = offsetData[placement]; Object.keys(overflowOffsets).forEach(function (key) { var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x'; overflowOffsets[key] += offset[axis] * multiply; }); } return overflowOffsets; } function computeAutoPlacement(state, options) { if (options === void 0) { options = {}; } var _options = options, placement = _options.placement, boundary = _options.boundary, rootBoundary = _options.rootBoundary, padding = _options.padding, flipVariations = _options.flipVariations, _options$allowedAutoP = _options.allowedAutoPlacements, allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; var variation = getVariation(placement); var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) { return getVariation(placement) === variation; }) : basePlacements; var allowedPlacements = placements$1.filter(function (placement) { return allowedAutoPlacements.indexOf(placement) >= 0; }); if (allowedPlacements.length === 0) { allowedPlacements = placements$1; if (process.env.NODE_ENV !== "production") { console.error(['Popper: The `allowedAutoPlacements` option did not allow any', 'placements. Ensure the `placement` option matches the variation', 'of the allowed placements.', 'For example, "auto" cannot be used to allow "bottom-start".', 'Use "auto-start" instead.'].join(' ')); } } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions... var overflows = allowedPlacements.reduce(function (acc, placement) { acc[placement] = detectOverflow(state, { placement: placement, boundary: boundary, rootBoundary: rootBoundary, padding: padding })[getBasePlacement(placement)]; return acc; }, {}); return Object.keys(overflows).sort(function (a, b) { return overflows[a] - overflows[b]; }); } function getExpandedFallbackPlacements(placement) { if (getBasePlacement(placement) === auto) { return []; } var oppositePlacement = getOppositePlacement(placement); return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; } function flip(_ref) { var state = _ref.state, options = _ref.options, name = _ref.name; if (state.modifiersData[name]._skip) { return; } var _options$mainAxis = options.mainAxis, checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, _options$altAxis = options.altAxis, checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, specifiedFallbackPlacements = options.fallbackPlacements, padding = options.padding, boundary = options.boundary, rootBoundary = options.rootBoundary, altBoundary = options.altBoundary, _options$flipVariatio = options.flipVariations, flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, allowedAutoPlacements = options.allowedAutoPlacements; var preferredPlacement = state.options.placement; var basePlacement = getBasePlacement(preferredPlacement); var isBasePlacement = basePlacement === preferredPlacement; var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) { return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, { placement: placement, boundary: boundary, rootBoundary: rootBoundary, padding: padding, flipVariations: flipVariations, allowedAutoPlacements: allowedAutoPlacements }) : placement); }, []); var referenceRect = state.rects.reference; var popperRect = state.rects.popper; var checksMap = new Map(); var makeFallbackChecks = true; var firstFittingPlacement = placements[0]; for (var i = 0; i < placements.length; i++) { var placement = placements[i]; var _basePlacement = getBasePlacement(placement); var isStartVariation = getVariation(placement) === start; var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; var len = isVertical ? 'width' : 'height'; var overflow = detectOverflow(state, { placement: placement, boundary: boundary, rootBoundary: rootBoundary, altBoundary: altBoundary, padding: padding }); var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; if (referenceRect[len] > popperRect[len]) { mainVariationSide = getOppositePlacement(mainVariationSide); } var altVariationSide = getOppositePlacement(mainVariationSide); var checks = []; if (checkMainAxis) { checks.push(overflow[_basePlacement] <= 0); } if (checkAltAxis) { checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); } if (checks.every(function (check) { return check; })) { firstFittingPlacement = placement; makeFallbackChecks = false; break; } checksMap.set(placement, checks); } if (makeFallbackChecks) { // `2` may be desired in some cases – research later var numberOfChecks = flipVariations ? 3 : 1; var _loop = function _loop(_i) { var fittingPlacement = placements.find(function (placement) { var checks = checksMap.get(placement); if (checks) { return checks.slice(0, _i).every(function (check) { return check; }); } }); if (fittingPlacement) { firstFittingPlacement = fittingPlacement; return "break"; } }; for (var _i = numberOfChecks; _i > 0; _i--) { var _ret = _loop(_i); if (_ret === "break") break; } } if (state.placement !== firstFittingPlacement) { state.modifiersData[name]._skip = true; state.placement = firstFittingPlacement; state.reset = true; } } // eslint-disable-next-line import/no-unused-modules var flip$1 = { name: 'flip', enabled: true, phase: 'main', fn: flip, requiresIfExists: ['offset'], data: { _skip: false } }; function getSideOffsets(overflow, rect, preventedOffsets) { if (preventedOffsets === void 0) { preventedOffsets = { x: 0, y: 0 }; } return { top: overflow.top - rect.height - preventedOffsets.y, right: overflow.right - rect.width + preventedOffsets.x, bottom: overflow.bottom - rect.height + preventedOffsets.y, left: overflow.left - rect.width - preventedOffsets.x }; } function isAnySideFullyClipped(overflow) { return [top, right, bottom, left].some(function (side) { return overflow[side] >= 0; }); } function hide(_ref) { var state = _ref.state, name = _ref.name; var referenceRect = state.rects.reference; var popperRect = state.rects.popper; var preventedOffsets = state.modifiersData.preventOverflow; var referenceOverflow = detectOverflow(state, { elementContext: 'reference' }); var popperAltOverflow = detectOverflow(state, { altBoundary: true }); var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); state.modifiersData[name] = { referenceClippingOffsets: referenceClippingOffsets, popperEscapeOffsets: popperEscapeOffsets, isReferenceHidden: isReferenceHidden, hasPopperEscaped: hasPopperEscaped }; state.attributes.popper = Object.assign({}, state.attributes.popper, { 'data-popper-reference-hidden': isReferenceHidden, 'data-popper-escaped': hasPopperEscaped }); } // eslint-disable-next-line import/no-unused-modules var hide$1 = { name: 'hide', enabled: true, phase: 'main', requiresIfExists: ['preventOverflow'], fn: hide }; function distanceAndSkiddingToXY(placement, rects, offset) { var basePlacement = getBasePlacement(placement); var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, { placement: placement })) : offset, skidding = _ref[0], distance = _ref[1]; skidding = skidding || 0; distance = (distance || 0) * invertDistance; return [left, right].indexOf(basePlacement) >= 0 ? { x: distance, y: skidding } : { x: skidding, y: distance }; } function offset(_ref2) { var state = _ref2.state, options = _ref2.options, name = _ref2.name; var _options$offset = options.offset, offset = _options$offset === void 0 ? [0, 0] : _options$offset; var data = placements.reduce(function (acc, placement) { acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset); return acc; }, {}); var _data$state$placement = data[state.placement], x = _data$state$placement.x, y = _data$state$placement.y; if (state.modifiersData.popperOffsets != null) { state.modifiersData.popperOffsets.x += x; state.modifiersData.popperOffsets.y += y; } state.modifiersData[name] = data; } // eslint-disable-next-line import/no-unused-modules var offset$1 = { name: 'offset', enabled: true, phase: 'main', requires: ['popperOffsets'], fn: offset }; function popperOffsets(_ref) { var state = _ref.state, name = _ref.name; // Offsets are the actual position the popper needs to have to be // properly positioned near its reference element // This is the most basic placement, and will be adjusted by // the modifiers in the next step state.modifiersData[name] = computeOffsets({ reference: state.rects.reference, element: state.rects.popper, strategy: 'absolute', placement: state.placement }); } // eslint-disable-next-line import/no-unused-modules var popperOffsets$1 = { name: 'popperOffsets', enabled: true, phase: 'read', fn: popperOffsets, data: {} }; function getAltAxis(axis) { return axis === 'x' ? 'y' : 'x'; } function preventOverflow(_ref) { var state = _ref.state, options = _ref.options, name = _ref.name; var _options$mainAxis = options.mainAxis, checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, _options$altAxis = options.altAxis, checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, boundary = options.boundary, rootBoundary = options.rootBoundary, altBoundary = options.altBoundary, padding = options.padding, _options$tether = options.tether, tether = _options$tether === void 0 ? true : _options$tether, _options$tetherOffset = options.tetherOffset, tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; var overflow = detectOverflow(state, { boundary: boundary, rootBoundary: rootBoundary, padding: padding, altBoundary: altBoundary }); var basePlacement = getBasePlacement(state.placement); var variation = getVariation(state.placement); var isBasePlacement = !variation; var mainAxis = getMainAxisFromPlacement(basePlacement); var altAxis = getAltAxis(mainAxis); var popperOffsets = state.modifiersData.popperOffsets; var referenceRect = state.rects.reference; var popperRect = state.rects.popper; var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, { placement: state.placement })) : tetherOffset; var data = { x: 0, y: 0 }; if (!popperOffsets) { return; } if (checkMainAxis || checkAltAxis) { var mainSide = mainAxis === 'y' ? top : left; var altSide = mainAxis === 'y' ? bottom : right; var len = mainAxis === 'y' ? 'height' : 'width'; var offset = popperOffsets[mainAxis]; var min$1 = popperOffsets[mainAxis] + overflow[mainSide]; var max$1 = popperOffsets[mainAxis] - overflow[altSide]; var additive = tether ? -popperRect[len] / 2 : 0; var minLen = variation === start ? referenceRect[len] : popperRect[len]; var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go // outside the reference bounds var arrowElement = state.elements.arrow; var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { width: 0, height: 0 }; var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject(); var arrowPaddingMin = arrowPaddingObject[mainSide]; var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want // to include its full size in the calculation. If the reference is small // and near the edge of a boundary, the popper can overflow even if the // reference is not overflowing as well (e.g. virtual elements with no // width or height) var arrowLen = within(0, referenceRect[len], arrowRect[len]); var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - tetherOffsetValue : minLen - arrowLen - arrowPaddingMin - tetherOffsetValue; var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + tetherOffsetValue : maxLen + arrowLen + arrowPaddingMax + tetherOffsetValue; var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; var offsetModifierValue = state.modifiersData.offset ? state.modifiersData.offset[state.placement][mainAxis] : 0; var tetherMin = popperOffsets[mainAxis] + minOffset - offsetModifierValue - clientOffset; var tetherMax = popperOffsets[mainAxis] + maxOffset - offsetModifierValue; if (checkMainAxis) { var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1); popperOffsets[mainAxis] = preventedOffset; data[mainAxis] = preventedOffset - offset; } if (checkAltAxis) { var _mainSide = mainAxis === 'x' ? top : left; var _altSide = mainAxis === 'x' ? bottom : right; var _offset = popperOffsets[altAxis]; var _min = _offset + overflow[_mainSide]; var _max = _offset - overflow[_altSide]; var _preventedOffset = within(tether ? min(_min, tetherMin) : _min, _offset, tether ? max(_max, tetherMax) : _max); popperOffsets[altAxis] = _preventedOffset; data[altAxis] = _preventedOffset - _offset; } } state.modifiersData[name] = data; } // eslint-disable-next-line import/no-unused-modules var preventOverflow$1 = { name: 'preventOverflow', enabled: true, phase: 'main', fn: preventOverflow, requiresIfExists: ['offset'] }; function getHTMLElementScroll(element) { return { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop }; } function getNodeScroll(node) { if (node === getWindow(node) || !isHTMLElement(node)) { return getWindowScroll(node); } else { return getHTMLElementScroll(node); } } // Composite means it takes into account transforms as well as layout. function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { if (isFixed === void 0) { isFixed = false; } var documentElement = getDocumentElement(offsetParent); var rect = getBoundingClientRect(elementOrVirtualElement); var isOffsetParentAnElement = isHTMLElement(offsetParent); var scroll = { scrollLeft: 0, scrollTop: 0 }; var offsets = { x: 0, y: 0 }; if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078 isScrollParent(documentElement)) { scroll = getNodeScroll(offsetParent); } if (isHTMLElement(offsetParent)) { offsets = getBoundingClientRect(offsetParent); offsets.x += offsetParent.clientLeft; offsets.y += offsetParent.clientTop; } else if (documentElement) { offsets.x = getWindowScrollBarX(documentElement); } } return { x: rect.left + scroll.scrollLeft - offsets.x, y: rect.top + scroll.scrollTop - offsets.y, width: rect.width, height: rect.height }; } function order(modifiers) { var map = new Map(); var visited = new Set(); var result = []; modifiers.forEach(function (modifier) { map.set(modifier.name, modifier); }); // On visiting object, check for its dependencies and visit them recursively function sort(modifier) { visited.add(modifier.name); var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); requires.forEach(function (dep) { if (!visited.has(dep)) { var depModifier = map.get(dep); if (depModifier) { sort(depModifier); } } }); result.push(modifier); } modifiers.forEach(function (modifier) { if (!visited.has(modifier.name)) { // check for visited object sort(modifier); } }); return result; } function orderModifiers(modifiers) { // order based on dependencies var orderedModifiers = order(modifiers); // order based on phase return modifierPhases.reduce(function (acc, phase) { return acc.concat(orderedModifiers.filter(function (modifier) { return modifier.phase === phase; })); }, []); } function debounce(fn) { var pending; return function () { if (!pending) { pending = new Promise(function (resolve) { Promise.resolve().then(function () { pending = undefined; resolve(fn()); }); }); } return pending; }; } function format(str) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return [].concat(args).reduce(function (p, c) { return p.replace(/%s/, c); }, str); } var INVALID_MODIFIER_ERROR = 'Popper: modifier "%s" provided an invalid %s property, expected %s but got %s'; var MISSING_DEPENDENCY_ERROR = 'Popper: modifier "%s" requires "%s", but "%s" modifier is not available'; var VALID_PROPERTIES = ['name', 'enabled', 'phase', 'fn', 'effect', 'requires', 'options']; function validateModifiers(modifiers) { modifiers.forEach(function (modifier) { Object.keys(modifier).forEach(function (key) { switch (key) { case 'name': if (typeof modifier.name !== 'string') { console.error(format(INVALID_MODIFIER_ERROR, String(modifier.name), '"name"', '"string"', "\"" + String(modifier.name) + "\"")); } break; case 'enabled': if (typeof modifier.enabled !== 'boolean') { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"enabled"', '"boolean"', "\"" + String(modifier.enabled) + "\"")); } case 'phase': if (modifierPhases.indexOf(modifier.phase) < 0) { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"phase"', "either " + modifierPhases.join(', '), "\"" + String(modifier.phase) + "\"")); } break; case 'fn': if (typeof modifier.fn !== 'function') { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"fn"', '"function"', "\"" + String(modifier.fn) + "\"")); } break; case 'effect': if (typeof modifier.effect !== 'function') { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"effect"', '"function"', "\"" + String(modifier.fn) + "\"")); } break; case 'requires': if (!Array.isArray(modifier.requires)) { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"requires"', '"array"', "\"" + String(modifier.requires) + "\"")); } break; case 'requiresIfExists': if (!Array.isArray(modifier.requiresIfExists)) { console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"requiresIfExists"', '"array"', "\"" + String(modifier.requiresIfExists) + "\"")); } break; case 'options': case 'data': break; default: console.error("PopperJS: an invalid property has been provided to the \"" + modifier.name + "\" modifier, valid properties are " + VALID_PROPERTIES.map(function (s) { return "\"" + s + "\""; }).join(', ') + "; but \"" + key + "\" was provided."); } modifier.requires && modifier.requires.forEach(function (requirement) { if (modifiers.find(function (mod) { return mod.name === requirement; }) == null) { console.error(format(MISSING_DEPENDENCY_ERROR, String(modifier.name), requirement, requirement)); } }); }); }); } function uniqueBy(arr, fn) { var identifiers = new Set(); return arr.filter(function (item) { var identifier = fn(item); if (!identifiers.has(identifier)) { identifiers.add(identifier); return true; } }); } function mergeByName(modifiers) { var merged = modifiers.reduce(function (merged, current) { var existing = merged[current.name]; merged[current.name] = existing ? Object.assign({}, existing, current, { options: Object.assign({}, existing.options, current.options), data: Object.assign({}, existing.data, current.data) }) : current; return merged; }, {}); // IE11 does not support Object.values return Object.keys(merged).map(function (key) { return merged[key]; }); } var INVALID_ELEMENT_ERROR = 'Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.'; var INFINITE_LOOP_ERROR = 'Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash.'; var DEFAULT_OPTIONS = { placement: 'bottom', modifiers: [], strategy: 'absolute' }; function areValidElements() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return !args.some(function (element) { return !(element && typeof element.getBoundingClientRect === 'function'); }); } function popperGenerator(generatorOptions) { if (generatorOptions === void 0) { generatorOptions = {}; } var _generatorOptions = generatorOptions, _generatorOptions$def = _generatorOptions.defaultModifiers, defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, _generatorOptions$def2 = _generatorOptions.defaultOptions, defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; return function createPopper(reference, popper, options) { if (options === void 0) { options = defaultOptions; } var state = { placement: 'bottom', orderedModifiers: [], options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), modifiersData: {}, elements: { reference: reference, popper: popper }, attributes: {}, styles: {} }; var effectCleanupFns = []; var isDestroyed = false; var instance = { state: state, setOptions: function setOptions(options) { cleanupModifierEffects(); state.options = Object.assign({}, defaultOptions, state.options, options); state.scrollParents = { reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [], popper: listScrollParents(popper) }; // Orders the modifiers based on their dependencies and `phase` // properties var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers state.orderedModifiers = orderedModifiers.filter(function (m) { return m.enabled; }); // Validate the provided modifiers so that the consumer will get warned // if one of the modifiers is invalid for any reason if (process.env.NODE_ENV !== "production") { var modifiers = uniqueBy([].concat(orderedModifiers, state.options.modifiers), function (_ref) { var name = _ref.name; return name; }); validateModifiers(modifiers); if (getBasePlacement(state.options.placement) === auto) { var flipModifier = state.orderedModifiers.find(function (_ref2) { var name = _ref2.name; return name === 'flip'; }); if (!flipModifier) { console.error(['Popper: "auto" placements require the "flip" modifier be', 'present and enabled to work.'].join(' ')); } } var _getComputedStyle = getComputedStyle$1(popper), marginTop = _getComputedStyle.marginTop, marginRight = _getComputedStyle.marginRight, marginBottom = _getComputedStyle.marginBottom, marginLeft = _getComputedStyle.marginLeft; // We no longer take into account `margins` on the popper, and it can // cause bugs with positioning, so we'll warn the consumer if ([marginTop, marginRight, marginBottom, marginLeft].some(function (margin) { return parseFloat(margin); })) { console.warn(['Popper: CSS "margin" styles cannot be used to apply padding', 'between the popper and its reference element or boundary.', 'To replicate margin, use the `offset` modifier, as well as', 'the `padding` option in the `preventOverflow` and `flip`', 'modifiers.'].join(' ')); } } runModifierEffects(); return instance.update(); }, // Sync update – it will always be executed, even if not necessary. This // is useful for low frequency updates where sync behavior simplifies the // logic. // For high frequency updates (e.g. `resize` and `scroll` events), always // prefer the async Popper#update method forceUpdate: function forceUpdate() { if (isDestroyed) { return; } var _state$elements = state.elements, reference = _state$elements.reference, popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements // anymore if (!areValidElements(reference, popper)) { if (process.env.NODE_ENV !== "production") { console.error(INVALID_ELEMENT_ERROR); } return; } // Store the reference and popper rects to be read by modifiers state.rects = { reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'), popper: getLayoutRect(popper) }; // Modifiers have the ability to reset the current update cycle. The // most common use case for this is the `flip` modifier changing the // placement, which then needs to re-run all the modifiers, because the // logic was previously ran for the previous placement and is therefore // stale/incorrect state.reset = false; state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier // is filled with the initial data specified by the modifier. This means // it doesn't persist and is fresh on each update. // To ensure persistent data, use `${name}#persistent` state.orderedModifiers.forEach(function (modifier) { return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); }); var __debug_loops__ = 0; for (var index = 0; index < state.orderedModifiers.length; index++) { if (process.env.NODE_ENV !== "production") { __debug_loops__ += 1; if (__debug_loops__ > 100) { console.error(INFINITE_LOOP_ERROR); break; } } if (state.reset === true) { state.reset = false; index = -1; continue; } var _state$orderedModifie = state.orderedModifiers[index], fn = _state$orderedModifie.fn, _state$orderedModifie2 = _state$orderedModifie.options, _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, name = _state$orderedModifie.name; if (typeof fn === 'function') { state = fn({ state: state, options: _options, name: name, instance: instance }) || state; } } }, // Async and optimistically optimized update – it will not be executed if // not necessary (debounced to run at most once-per-tick) update: debounce(function () { return new Promise(function (resolve) { instance.forceUpdate(); resolve(state); }); }), destroy: function destroy() { cleanupModifierEffects(); isDestroyed = true; } }; if (!areValidElements(reference, popper)) { if (process.env.NODE_ENV !== "production") { console.error(INVALID_ELEMENT_ERROR); } return instance; } instance.setOptions(options).then(function (state) { if (!isDestroyed && options.onFirstUpdate) { options.onFirstUpdate(state); } }); // Modifiers have the ability to execute arbitrary code before the first // update cycle runs. They will be executed in the same order as the update // cycle. This is useful when a modifier adds some persistent data that // other modifiers need to use, but the modifier is run after the dependent // one. function runModifierEffects() { state.orderedModifiers.forEach(function (_ref3) { var name = _ref3.name, _ref3$options = _ref3.options, options = _ref3$options === void 0 ? {} : _ref3$options, effect = _ref3.effect; if (typeof effect === 'function') { var cleanupFn = effect({ state: state, name: name, instance: instance, options: options }); var noopFn = function noopFn() {}; effectCleanupFns.push(cleanupFn || noopFn); } }); } function cleanupModifierEffects() { effectCleanupFns.forEach(function (fn) { return fn(); }); effectCleanupFns = []; } return instance; }; } var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1]; var createPopper = /*#__PURE__*/popperGenerator({ defaultModifiers: defaultModifiers }); // eslint-disable-next-line import/no-unused-modules class Suggest { constructor(owner, containerEl, scope) { this.owner = owner; this.containerEl = containerEl; containerEl.on("click", ".suggestion-item", this.onSuggestionClick.bind(this)); containerEl.on("mousemove", ".suggestion-item", this.onSuggestionMouseover.bind(this)); scope.register([], "ArrowUp", (event) => { if (!event.isComposing) { this.setSelectedItem(this.selectedItem - 1, true); return false; } }); scope.register([], "ArrowDown", (event) => { if (!event.isComposing) { this.setSelectedItem(this.selectedItem + 1, true); return false; } }); scope.register([], "Enter", (event) => { if (!event.isComposing) { this.useSelectedItem(event); return false; } }); } onSuggestionClick(event, el) { event.preventDefault(); const item = this.suggestions.indexOf(el); this.setSelectedItem(item, false); this.useSelectedItem(event); } onSuggestionMouseover(_event, el) { const item = this.suggestions.indexOf(el); this.setSelectedItem(item, false); } setSuggestions(values) { this.containerEl.empty(); const suggestionEls = []; values.forEach((value) => { const suggestionEl = this.containerEl.createDiv("suggestion-item"); this.owner.renderSuggestion(value, suggestionEl); suggestionEls.push(suggestionEl); }); this.values = values; this.suggestions = suggestionEls; this.setSelectedItem(0, false); } useSelectedItem(event) { const currentValue = this.values[this.selectedItem]; if (currentValue) { this.owner.selectSuggestion(currentValue, event); } } setSelectedItem(selectedIndex, scrollIntoView) { const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); const prevSelectedSuggestion = this.suggestions[this.selectedItem]; const selectedSuggestion = this.suggestions[normalizedIndex]; prevSelectedSuggestion === null || prevSelectedSuggestion === void 0 ? void 0 : prevSelectedSuggestion.removeClass("is-selected"); selectedSuggestion === null || selectedSuggestion === void 0 ? void 0 : selectedSuggestion.addClass("is-selected"); this.selectedItem = normalizedIndex; if (scrollIntoView) { selectedSuggestion.scrollIntoView(false); } } } class TextInputSuggest { constructor(app, inputEl) { this.app = app; this.inputEl = inputEl; this.scope = new obsidian.Scope(); this.suggestEl = createDiv("suggestion-container"); const suggestion = this.suggestEl.createDiv("suggestion"); this.suggest = new Suggest(this, suggestion, this.scope); this.scope.register([], "Escape", this.close.bind(this)); this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); this.inputEl.addEventListener("blur", this.close.bind(this)); this.suggestEl.on("mousedown", ".suggestion-container", (event) => { event.preventDefault(); }); } onInputChanged() { const inputStr = this.inputEl.value; const suggestions = this.getSuggestions(inputStr); if (suggestions.length > 0) { this.suggest.setSuggestions(suggestions); // eslint-disable-next-line @typescript-eslint/no-explicit-any this.open(this.app.dom.appContainerEl, this.inputEl); } } open(container, inputEl) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.app.keymap.pushScope(this.scope); container.appendChild(this.suggestEl); this.popper = createPopper(inputEl, this.suggestEl, { placement: "bottom-start", modifiers: [ { name: "sameWidth", enabled: true, fn: ({ state, instance }) => { // Note: positioning needs to be calculated twice - // first pass - positioning it according to the width of the popper // second pass - position it with the width bound to the reference element // we need to early exit to avoid an infinite loop const targetWidth = `${state.rects.reference.width}px`; if (state.styles.popper.width === targetWidth) { return; } state.styles.popper.width = targetWidth; instance.update(); }, phase: "beforeWrite", requires: ["computeStyles"], }, ], }); } close() { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.app.keymap.popScope(this.scope); this.suggest.setSuggestions([]); this.popper.destroy(); this.suggestEl.detach(); } } class FileSuggest extends TextInputSuggest { getSuggestions(inputStr) { const abstractFiles = this.app.vault.getAllLoadedFiles(); const files = []; const lowerCaseInputStr = inputStr.toLowerCase(); abstractFiles.forEach((file) => { if (file instanceof obsidian.TFile && file.extension === "md" && file.path.toLowerCase().contains(lowerCaseInputStr)) { files.push(file); } }); return files; } renderSuggestion(file, el) { el.setText(file.path); } selectSuggestion(file) { this.inputEl.value = file.path; this.inputEl.trigger("input"); this.close(); } } class FolderSuggest extends TextInputSuggest { getSuggestions(inputStr) { const abstractFiles = this.app.vault.getAllLoadedFiles(); const folders = []; const lowerCaseInputStr = inputStr.toLowerCase(); abstractFiles.forEach((folder) => { if (folder instanceof obsidian.TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr)) { folders.push(folder); } }); return folders; } renderSuggestion(file, el) { el.setText(file.path); } selectSuggestion(file) { this.inputEl.value = file.path; this.inputEl.trigger("input"); this.close(); } } /* src/settings/NoteTemplateSetting.svelte generated by Svelte v3.35.0 */ function create_if_block$2(ctx) { let div; let t; return { c() { div = element("div"); t = text(/*error*/ ctx[3]); attr(div, "class", "has-error"); }, m(target, anchor) { insert(target, div, anchor); append(div, t); }, p(ctx, dirty) { if (dirty & /*error*/ 8) set_data(t, /*error*/ ctx[3]); }, d(detaching) { if (detaching) detach(div); } }; } function create_fragment$2(ctx) { let div4; let div2; let div0; let t0_value = capitalize(/*periodicity*/ ctx[1]) + ""; let t0; let t1; let t2; let div1; let t4; let t5; let div3; let input; let mounted; let dispose; let if_block = /*error*/ ctx[3] && create_if_block$2(ctx); return { c() { div4 = element("div"); div2 = element("div"); div0 = element("div"); t0 = text(t0_value); t1 = text(" Note Template"); t2 = space(); div1 = element("div"); div1.textContent = "Choose the file to use as a template"; t4 = space(); if (if_block) if_block.c(); t5 = space(); div3 = element("div"); input = element("input"); attr(div0, "class", "setting-item-name"); attr(div1, "class", "setting-item-description"); attr(div2, "class", "setting-item-info"); attr(input, "type", "text"); attr(input, "spellcheck", false); attr(input, "placeholder", "Example: folder/note"); toggle_class(input, "has-error", !!/*error*/ ctx[3]); attr(div3, "class", "setting-item-control"); attr(div4, "class", "setting-item"); }, m(target, anchor) { insert(target, div4, anchor); append(div4, div2); append(div2, div0); append(div0, t0); append(div0, t1); append(div2, t2); append(div2, div1); append(div2, t4); if (if_block) if_block.m(div2, null); append(div4, t5); append(div4, div3); append(div3, input); /*input_binding*/ ctx[7](input); set_input_value(input, /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].template); if (!mounted) { dispose = [ listen(input, "input", /*input_input_handler*/ ctx[8]), listen(input, "change", /*validateOnBlur*/ ctx[5]), listen(input, "input", /*clearError*/ ctx[6]) ]; mounted = true; } }, p(ctx, [dirty]) { if (dirty & /*periodicity*/ 2 && t0_value !== (t0_value = capitalize(/*periodicity*/ ctx[1]) + "")) set_data(t0, t0_value); if (/*error*/ ctx[3]) { if (if_block) { if_block.p(ctx, dirty); } else { if_block = create_if_block$2(ctx); if_block.c(); if_block.m(div2, null); } } else if (if_block) { if_block.d(1); if_block = null; } if (dirty & /*$settings, periodicity*/ 6 && input.value !== /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].template) { set_input_value(input, /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].template); } if (dirty & /*error*/ 8) { toggle_class(input, "has-error", !!/*error*/ ctx[3]); } }, i: noop, o: noop, d(detaching) { if (detaching) detach(div4); if (if_block) if_block.d(); /*input_binding*/ ctx[7](null); mounted = false; run_all(dispose); } }; } function instance$2($$self, $$props, $$invalidate) { let $settings, $$unsubscribe_settings = noop, $$subscribe_settings = () => ($$unsubscribe_settings(), $$unsubscribe_settings = subscribe(settings, $$value => $$invalidate(2, $settings = $$value)), settings); $$self.$$.on_destroy.push(() => $$unsubscribe_settings()); let { settings } = $$props; $$subscribe_settings(); let { periodicity } = $$props; let error; let inputEl; function validateOnBlur() { $$invalidate(3, error = validateTemplate(inputEl.value)); } function clearError() { $$invalidate(3, error = ""); } onMount(() => { $$invalidate(3, error = validateTemplate(inputEl.value)); new FileSuggest(window.app, inputEl); }); function input_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { inputEl = $$value; $$invalidate(4, inputEl); }); } function input_input_handler() { $settings[periodicity].template = this.value; settings.set($settings); $$invalidate(1, periodicity); } $$self.$$set = $$props => { if ("settings" in $$props) $$subscribe_settings($$invalidate(0, settings = $$props.settings)); if ("periodicity" in $$props) $$invalidate(1, periodicity = $$props.periodicity); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*$settings, periodicity*/ 6) { $settings[periodicity].template || ""; } }; return [ settings, periodicity, $settings, error, inputEl, validateOnBlur, clearError, input_binding, input_input_handler ]; } class NoteTemplateSetting extends SvelteComponent { constructor(options) { super(); init(this, options, instance$2, create_fragment$2, safe_not_equal, { settings: 0, periodicity: 1 }); } } /* src/settings/NoteFolderSetting.svelte generated by Svelte v3.35.0 */ function create_if_block$1(ctx) { let div; let t; return { c() { div = element("div"); t = text(/*error*/ ctx[4]); attr(div, "class", "has-error"); }, m(target, anchor) { insert(target, div, anchor); append(div, t); }, p(ctx, dirty) { if (dirty & /*error*/ 16) set_data(t, /*error*/ ctx[4]); }, d(detaching) { if (detaching) detach(div); } }; } function create_fragment$1(ctx) { let div4; let div2; let div0; let t1; let div1; let t2; let t3; let t4; let t5; let t6; let div3; let input; let mounted; let dispose; let if_block = /*error*/ ctx[4] && create_if_block$1(ctx); return { c() { div4 = element("div"); div2 = element("div"); div0 = element("div"); div0.textContent = "Note Folder"; t1 = space(); div1 = element("div"); t2 = text("New "); t3 = text(/*periodicity*/ ctx[1]); t4 = text(" notes will be placed here"); t5 = space(); if (if_block) if_block.c(); t6 = space(); div3 = element("div"); input = element("input"); attr(div0, "class", "setting-item-name"); attr(div1, "class", "setting-item-description"); attr(div2, "class", "setting-item-info"); attr(input, "type", "text"); attr(input, "spellcheck", false); attr(input, "placeholder", "Example: folder 1/folder 2"); toggle_class(input, "has-error", !!/*error*/ ctx[4]); attr(div3, "class", "setting-item-control"); attr(div4, "class", "setting-item"); }, m(target, anchor) { insert(target, div4, anchor); append(div4, div2); append(div2, div0); append(div2, t1); append(div2, div1); append(div1, t2); append(div1, t3); append(div1, t4); append(div2, t5); if (if_block) if_block.m(div2, null); append(div4, t6); append(div4, div3); append(div3, input); set_input_value(input, /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].folder); /*input_binding*/ ctx[8](input); if (!mounted) { dispose = [ listen(input, "input", /*input_input_handler*/ ctx[7]), listen(input, "change", /*onChange*/ ctx[5]), listen(input, "input", /*clearError*/ ctx[6]) ]; mounted = true; } }, p(ctx, [dirty]) { if (dirty & /*periodicity*/ 2) set_data(t3, /*periodicity*/ ctx[1]); if (/*error*/ ctx[4]) { if (if_block) { if_block.p(ctx, dirty); } else { if_block = create_if_block$1(ctx); if_block.c(); if_block.m(div2, null); } } else if (if_block) { if_block.d(1); if_block = null; } if (dirty & /*$settings, periodicity*/ 6 && input.value !== /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].folder) { set_input_value(input, /*$settings*/ ctx[2][/*periodicity*/ ctx[1]].folder); } if (dirty & /*error*/ 16) { toggle_class(input, "has-error", !!/*error*/ ctx[4]); } }, i: noop, o: noop, d(detaching) { if (detaching) detach(div4); if (if_block) if_block.d(); /*input_binding*/ ctx[8](null); mounted = false; run_all(dispose); } }; } function instance$1($$self, $$props, $$invalidate) { let $settings, $$unsubscribe_settings = noop, $$subscribe_settings = () => ($$unsubscribe_settings(), $$unsubscribe_settings = subscribe(settings, $$value => $$invalidate(2, $settings = $$value)), settings); $$self.$$.on_destroy.push(() => $$unsubscribe_settings()); let { settings } = $$props; $$subscribe_settings(); let { periodicity } = $$props; let inputEl; let error; function onChange() { $$invalidate(4, error = validateFolder(inputEl.value)); } function clearError() { $$invalidate(4, error = ""); } onMount(() => { $$invalidate(4, error = validateFolder(inputEl.value)); new FolderSuggest(window.app, inputEl); }); function input_input_handler() { $settings[periodicity].folder = this.value; settings.set($settings); $$invalidate(1, periodicity); } function input_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { inputEl = $$value; $$invalidate(3, inputEl); }); } $$self.$$set = $$props => { if ("settings" in $$props) $$subscribe_settings($$invalidate(0, settings = $$props.settings)); if ("periodicity" in $$props) $$invalidate(1, periodicity = $$props.periodicity); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*$settings, periodicity*/ 6) { $settings[periodicity].folder || ""; } }; return [ settings, periodicity, $settings, inputEl, error, onChange, clearError, input_input_handler, input_binding ]; } class NoteFolderSetting extends SvelteComponent { constructor(options) { super(); init(this, options, instance$1, create_fragment$1, safe_not_equal, { settings: 0, periodicity: 1 }); } } /* src/settings/SettingsTab.svelte generated by Svelte v3.35.0 */ function get_each_context(ctx, list, i) { const child_ctx = ctx.slice(); child_ctx[9] = list[i]; return child_ctx; } // (33:0) {#if $settingsStore.showGettingStartedBanner} function create_if_block_1(ctx) { let gettingstartedbanner; let current; gettingstartedbanner = new GettingStartedBanner({ props: { migrateDailyNoteSettings: /*migrateDailyNoteSettings*/ ctx[2], settings: /*settingsStore*/ ctx[1], handleTeardown: /*func*/ ctx[6] } }); return { c() { create_component(gettingstartedbanner.$$.fragment); }, m(target, anchor) { mount_component(gettingstartedbanner, target, anchor); current = true; }, p(ctx, dirty) { const gettingstartedbanner_changes = {}; if (dirty & /*$settingsStore*/ 1) gettingstartedbanner_changes.handleTeardown = /*func*/ ctx[6]; gettingstartedbanner.$set(gettingstartedbanner_changes); }, i(local) { if (current) return; transition_in(gettingstartedbanner.$$.fragment, local); current = true; }, o(local) { transition_out(gettingstartedbanner.$$.fragment, local); current = false; }, d(detaching) { destroy_component(gettingstartedbanner, detaching); } }; } // (62:2) {#if $settingsStore[periodicity].enabled} function create_if_block(ctx) { let div; let noteformatsetting; let t0; let notetemplatesetting; let t1; let notefoldersetting; let t2; let div_intro; let div_outro; let current; noteformatsetting = new NoteFormatSetting({ props: { periodicity: /*periodicity*/ ctx[9], settings: /*settingsStore*/ ctx[1] } }); notetemplatesetting = new NoteTemplateSetting({ props: { periodicity: /*periodicity*/ ctx[9], settings: /*settingsStore*/ ctx[1] } }); notefoldersetting = new NoteFolderSetting({ props: { periodicity: /*periodicity*/ ctx[9], settings: /*settingsStore*/ ctx[1] } }); return { c() { div = element("div"); create_component(noteformatsetting.$$.fragment); t0 = space(); create_component(notetemplatesetting.$$.fragment); t1 = space(); create_component(notefoldersetting.$$.fragment); t2 = space(); }, m(target, anchor) { insert(target, div, anchor); mount_component(noteformatsetting, div, null); append(div, t0); mount_component(notetemplatesetting, div, null); append(div, t1); mount_component(notefoldersetting, div, null); append(div, t2); current = true; }, p: noop, i(local) { if (current) return; transition_in(noteformatsetting.$$.fragment, local); transition_in(notetemplatesetting.$$.fragment, local); transition_in(notefoldersetting.$$.fragment, local); add_render_callback(() => { if (div_outro) div_outro.end(1); if (!div_intro) div_intro = create_in_transition(div, slide, {}); div_intro.start(); }); current = true; }, o(local) { transition_out(noteformatsetting.$$.fragment, local); transition_out(notetemplatesetting.$$.fragment, local); transition_out(notefoldersetting.$$.fragment, local); if (div_intro) div_intro.invalidate(); div_outro = create_out_transition(div, slide, {}); current = false; }, d(detaching) { if (detaching) detach(div); destroy_component(noteformatsetting); destroy_component(notetemplatesetting); destroy_component(notefoldersetting); if (detaching && div_outro) div_outro.end(); } }; } // (42:0) {#each periodicities as periodicity} function create_each_block(ctx) { let div4; let div1; let div0; let h3; let t0_value = capitalize(/*periodicity*/ ctx[9]) + ""; let t0; let t1; let t2; let div3; let div2; let t3; let if_block_anchor; let current; let mounted; let dispose; function click_handler() { return /*click_handler*/ ctx[7](/*periodicity*/ ctx[9]); } let if_block = /*$settingsStore*/ ctx[0][/*periodicity*/ ctx[9]].enabled && create_if_block(ctx); return { c() { div4 = element("div"); div1 = element("div"); div0 = element("div"); h3 = element("h3"); t0 = text(t0_value); t1 = text(" Notes"); t2 = space(); div3 = element("div"); div2 = element("div"); t3 = space(); if (if_block) if_block.c(); if_block_anchor = empty(); attr(div0, "class", "setting-item-name"); attr(div1, "class", "setting-item-info"); attr(div2, "class", "checkbox-container"); toggle_class(div2, "is-enabled", /*$settingsStore*/ ctx[0][/*periodicity*/ ctx[9]].enabled); attr(div3, "class", "setting-item-control"); attr(div4, "class", "setting-item setting-item-heading"); }, m(target, anchor) { insert(target, div4, anchor); append(div4, div1); append(div1, div0); append(div0, h3); append(h3, t0); append(h3, t1); append(div4, t2); append(div4, div3); append(div3, div2); insert(target, t3, anchor); if (if_block) if_block.m(target, anchor); insert(target, if_block_anchor, anchor); current = true; if (!mounted) { dispose = listen(div2, "click", click_handler); mounted = true; } }, p(new_ctx, dirty) { ctx = new_ctx; if (dirty & /*$settingsStore, periodicities*/ 9) { toggle_class(div2, "is-enabled", /*$settingsStore*/ ctx[0][/*periodicity*/ ctx[9]].enabled); } if (/*$settingsStore*/ ctx[0][/*periodicity*/ ctx[9]].enabled) { if (if_block) { if_block.p(ctx, dirty); if (dirty & /*$settingsStore*/ 1) { transition_in(if_block, 1); } } else { if_block = create_if_block(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); } } else if (if_block) { group_outros(); transition_out(if_block, 1, 1, () => { if_block = null; }); check_outros(); } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if (detaching) detach(div4); if (detaching) detach(t3); if (if_block) if_block.d(detaching); if (detaching) detach(if_block_anchor); mounted = false; dispose(); } }; } function create_fragment(ctx) { let t; let each_1_anchor; let current; let if_block = /*$settingsStore*/ ctx[0].showGettingStartedBanner && create_if_block_1(ctx); let each_value = /*periodicities*/ ctx[3]; let each_blocks = []; for (let i = 0; i < each_value.length; i += 1) { each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); } const out = i => transition_out(each_blocks[i], 1, 1, () => { each_blocks[i] = null; }); return { c() { if (if_block) if_block.c(); t = space(); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].c(); } each_1_anchor = empty(); }, m(target, anchor) { if (if_block) if_block.m(target, anchor); insert(target, t, anchor); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].m(target, anchor); } insert(target, each_1_anchor, anchor); current = true; }, p(ctx, [dirty]) { if (/*$settingsStore*/ ctx[0].showGettingStartedBanner) { if (if_block) { if_block.p(ctx, dirty); if (dirty & /*$settingsStore*/ 1) { transition_in(if_block, 1); } } else { if_block = create_if_block_1(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(t.parentNode, t); } } else if (if_block) { group_outros(); transition_out(if_block, 1, 1, () => { if_block = null; }); check_outros(); } if (dirty & /*periodicities, settingsStore, $settingsStore, capitalize*/ 11) { each_value = /*periodicities*/ ctx[3]; let i; for (i = 0; i < each_value.length; i += 1) { const child_ctx = get_each_context(ctx, each_value, i); if (each_blocks[i]) { each_blocks[i].p(child_ctx, dirty); transition_in(each_blocks[i], 1); } else { each_blocks[i] = create_each_block(child_ctx); each_blocks[i].c(); transition_in(each_blocks[i], 1); each_blocks[i].m(each_1_anchor.parentNode, each_1_anchor); } } group_outros(); for (i = each_value.length; i < each_blocks.length; i += 1) { out(i); } check_outros(); } }, i(local) { if (current) return; transition_in(if_block); for (let i = 0; i < each_value.length; i += 1) { transition_in(each_blocks[i]); } current = true; }, o(local) { transition_out(if_block); each_blocks = each_blocks.filter(Boolean); for (let i = 0; i < each_blocks.length; i += 1) { transition_out(each_blocks[i]); } current = false; }, d(detaching) { if (if_block) if_block.d(detaching); if (detaching) detach(t); destroy_each(each_blocks, detaching); if (detaching) detach(each_1_anchor); } }; } function instance($$self, $$props, $$invalidate) { let $settingsStore; let { settings } = $$props; let { onUpdateSettings } = $$props; let settingsStore = writable(settings); component_subscribe($$self, settingsStore, value => $$invalidate(0, $settingsStore = value)); const unsubscribeFromSettings = settingsStore.subscribe(onUpdateSettings); function migrateDailyNoteSettings() { const dailyNoteSettings = getLegacyDailyNoteSettings(); settingsStore.update(old => Object.assign(Object.assign({}, old), { daily: Object.assign(Object.assign({}, dailyNoteSettings), { enabled: true }), hasMigratedDailyNoteSettings: true })); } const periodicities = ["daily", "weekly", "monthly", "quarterly", "yearly"]; onDestroy(() => { unsubscribeFromSettings(); }); const func = () => { set_store_value(settingsStore, $settingsStore.showGettingStartedBanner = false, $settingsStore); }; const click_handler = periodicity => { set_store_value(settingsStore, $settingsStore[periodicity].enabled = !$settingsStore[periodicity].enabled, $settingsStore); }; $$self.$$set = $$props => { if ("settings" in $$props) $$invalidate(4, settings = $$props.settings); if ("onUpdateSettings" in $$props) $$invalidate(5, onUpdateSettings = $$props.onUpdateSettings); }; return [ $settingsStore, settingsStore, migrateDailyNoteSettings, periodicities, settings, onUpdateSettings, func, click_handler ]; } class SettingsTab extends SvelteComponent { constructor(options) { super(); init(this, options, instance, create_fragment, safe_not_equal, { settings: 4, onUpdateSettings: 5 }); } } const DEFAULT_SETTINGS = Object.freeze({ format: "", template: "", folder: "", }); class PeriodicNotesSettingsTab extends obsidian.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } display() { this.containerEl.empty(); this.view = new SettingsTab({ target: this.containerEl, props: { settings: this.plugin.settings, onUpdateSettings: this.plugin.updateSettings, }, }); } } class PeriodicNotesPlugin extends obsidian.Plugin { async onload() { this.ribbonEl = null; this.updateSettings = this.updateSettings.bind(this); await this.loadSettings(); this.addSettingTab(new PeriodicNotesSettingsTab(this.app, this)); this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this)); obsidian.addIcon("calendar-day", calendarDayIcon); obsidian.addIcon("calendar-week", calendarWeekIcon); obsidian.addIcon("calendar-month", calendarMonthIcon); obsidian.addIcon("calendar-quarter", calendarQuarterIcon); obsidian.addIcon("calendar-year", calendarYearIcon); } onLayoutReady() { // If the user has Calendar Weekly Notes settings, migrate them automatically, // since the functionality will be deprecated. if (this.isInitialLoad && hasLegacyWeeklyNoteSettings()) { this.migrateWeeklySettings(); this.settings.weekly.enabled = true; } this.configureRibbonIcons(); this.configureCommands(); } migrateWeeklySettings() { const calendarSettings = getLegacyWeeklyNoteSettings(); this.updateSettings(Object.assign(Object.assign({}, this.settings), { weekly: Object.assign(Object.assign({}, calendarSettings), { enabled: true }), hasMigratedWeeklyNoteSettings: true, })); } configureRibbonIcons() { var _a; (_a = this.ribbonEl) === null || _a === void 0 ? void 0 : _a.detach(); const configuredPeriodicities = [ "daily", "weekly", "monthly", "quarterly", "yearly", ].filter((periodicity) => this.settings[periodicity].enabled); if (configuredPeriodicities.length) { const periodicity = configuredPeriodicities[0]; const config = periodConfigs[periodicity]; this.ribbonEl = this.addRibbonIcon(`calendar-${config.unitOfTime}`, `Open ${config.relativeUnit}`, (event) => openPeriodicNote(periodicity, window.moment(), isMetaPressed(event))); this.ribbonEl.addEventListener("contextmenu", (ev) => { showFileMenu(this.app, this.settings, { x: ev.pageX, y: ev.pageY, }); }); } } configureCommands() { // Remove disabled commands ["daily", "weekly", "monthly", "quarterly", "yearly"] .filter((periodicity) => !this.settings[periodicity].enabled) .forEach((periodicity) => { getCommands(periodicity).forEach((command) => // eslint-disable-next-line @typescript-eslint/no-explicit-any this.app.commands.removeCommand(`periodic-notes:${command.id}`)); }); // register enabled commands ["daily", "weekly", "monthly", "quarterly", "yearly"] .filter((periodicity) => this.settings[periodicity].enabled) .forEach((periodicity) => { getCommands(periodicity).forEach(this.addCommand.bind(this)); }); } async loadSettings() { const settings = await this.loadData(); if (!settings) { this.isInitialLoad = true; } this.settings = Object.assign({}, { showGettingStartedBanner: true, hasMigratedDailyNoteSettings: false, hasMigratedWeeklyNoteSettings: false, daily: Object.assign({}, DEFAULT_SETTINGS), weekly: Object.assign({}, DEFAULT_SETTINGS), monthly: Object.assign({}, DEFAULT_SETTINGS), quarterly: Object.assign({}, DEFAULT_SETTINGS), yearly: Object.assign({}, DEFAULT_SETTINGS), }, settings || {}); } onSettingsUpdate() { this.configureCommands(); this.configureRibbonIcons(); // Integrations (i.e. Calendar Plugin) can listen for changes to settings this.app.workspace.trigger(SETTINGS_UPDATED); } async updateSettings(val) { this.settings = val; await this.saveData(this.settings); this.onSettingsUpdate(); } } module.exports = PeriodicNotesPlugin; /* nosourcemap */ ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/periodic-notes/manifest.json ================================================ { "id": "periodic-notes", "name": "Periodic Notes", "description": "Create/manage your daily, weekly, and monthly notes", "version": "0.0.17", "author": "Liam Cain", "authorUrl": "https://github.com/liamcain/", "isDesktopOnly": false, "minAppVersion": "0.10.11" } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/periodic-notes/styles.css ================================================ .periodic-modal { min-width: 40vw; } .settings-banner { background-color: var(--background-primary-alt); border-radius: 8px; border: 1px solid var(--background-modifier-border); margin-bottom: 1em; margin-top: 1em; padding: 1.5em; text-align: left; } .settings-banner h3 { margin-top: 0; } .settings-banner h4 { margin-bottom: 0.25em; } .has-error { color: var(--text-error); } input.has-error { color: var(--text-error); border-color: var(--text-error); } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/templater-obsidian/main.js ================================================ /* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ var Aa=Object.create;var Ln=Object.defineProperty;var _a=Object.getOwnPropertyDescriptor;var xa=Object.getOwnPropertyNames;var ya=Object.getPrototypeOf,ja=Object.prototype.hasOwnProperty;var Yi=n=>Ln(n,"__esModule",{value:!0});var va=(n,e)=>{Yi(n);for(var t in e)Ln(n,t,{get:e[t],enumerable:!0})},wa=(n,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of xa(e))!ja.call(n,r)&&r!=="default"&&Ln(n,r,{get:()=>e[r],enumerable:!(t=_a(e,r))||t.enumerable});return n},X=n=>wa(Yi(Ln(n!=null?Aa(ya(n)):{},"default",n&&n.__esModule&&"default"in n?{get:()=>n.default,enumerable:!0}:{value:n,enumerable:!0})),n);var Ui=(()=>{for(var n=new Uint8Array(128),e=0;e<64;e++)n[e<26?e+65:e<52?e+71:e<62?e-4:e*4-205]=e;return t=>{for(var r=t.length,i=new Uint8Array((r-(t[r-1]=="=")-(t[r-2]=="="))*3/4|0),o=0,a=0;o>4,i[a++]=c<<4|d>>2,i[a++]=d<<6|m}return i}})();va(exports,{default:()=>Oi});var jr=X(require("obsidian"));var L=X(require("obsidian"));var Gi=X(require("obsidian"));function oe(n){let e=new Gi.Notice("",8e3);n instanceof O&&n.console_msg?(e.noticeEl.innerHTML=`Templater Error:
${n.message}
Check console for more information`,console.error("Templater Error:",n.message,` `,n.console_msg)):e.noticeEl.innerHTML=`Templater Error:
${n.message}`}var O=class extends Error{constructor(e,t){super(e);this.console_msg=t;this.name=this.constructor.name,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}};async function Te(n,e){try{return await n()}catch(t){return t instanceof O?oe(t):oe(new O(e,t.message)),null}}function ke(n,e){try{return n()}catch(t){return oe(new O(e,t.message)),null}}var re=function(){function n(){}return n.explainIfInvalidTSDocTagName=function(e){if(e[0]!=="@")return'A TSDoc tag name must start with an "@" symbol';if(!n._tsdocTagNameRegExp.test(e))return"A TSDoc tag name must start with a letter and contain only letters and numbers"},n.validateTSDocTagName=function(e){var t=n.explainIfInvalidTSDocTagName(e);if(t)throw new Error(t)},n.explainIfInvalidLinkUrl=function(e){if(e.length===0)return"The URL cannot be empty";if(!n._urlSchemeRegExp.test(e))return'An @link URL must begin with a scheme comprised only of letters and numbers followed by "://". (For general URLs, use an HTML "" tag instead.)';if(!n._urlSchemeAfterRegExp.test(e))return'An @link URL must have at least one character after "://"'},n.explainIfInvalidHtmlName=function(e){if(!n._htmlNameRegExp.test(e))return"An HTML name must be an ASCII letter followed by zero or more letters, digits, or hyphens"},n.validateHtmlName=function(e){var t=n.explainIfInvalidHtmlName(e);if(t)throw new Error(t)},n.explainIfInvalidPackageName=function(e){if(e.length===0)return"The package name cannot be an empty string";if(!n._validPackageNameRegExp.test(e))return"The package name ".concat(JSON.stringify(e)," is not a valid package name")},n.explainIfInvalidImportPath=function(e,t){if(e.length>0){if(e.indexOf("//")>=0)return'An import path must not contain "//"';if(e[e.length-1]==="/")return'An import path must not end with "/"';if(!t&&e[0]==="/")return'An import path must not start with "/" unless prefixed by a package name'}},n.isSystemSelector=function(e){return n._systemSelectors.has(e)},n.explainIfInvalidUnquotedIdentifier=function(e){if(e.length===0)return"The identifier cannot be an empty string";if(n._identifierBadCharRegExp.test(e))return"The identifier cannot non-word characters";if(n._identifierNumberStartRegExp.test(e))return"The identifier must not start with a number"},n.explainIfInvalidUnquotedMemberIdentifier=function(e){var t=n.explainIfInvalidUnquotedIdentifier(e);if(t!==void 0)return t;if(n.isSystemSelector(e))return'The identifier "'.concat(e,'" must be quoted because it is a TSDoc system selector name')},n._tsdocTagNameRegExp=/^@[a-z][a-z0-9]*$/i,n._urlSchemeRegExp=/^[a-z][a-z0-9]*\:\/\//i,n._urlSchemeAfterRegExp=/^[a-z][a-z0-9]*\:\/\/./i,n._htmlNameRegExp=/^[a-z]+[a-z0-9\-]*$/i,n._identifierBadCharRegExp=/[^a-z0-9_$]/i,n._identifierNumberStartRegExp=/^[0-9]/,n._validPackageNameRegExp=/^(?:@[a-z0-9\-_\.]+\/)?[a-z0-9\-_\.]+$/i,n._systemSelectors=new Set(["instance","static","constructor","class","enum","function","interface","namespace","type","variable"]),n}();var kr=function(){function n(){this._docNodeDefinitionsByKind=new Map,this._docNodeDefinitionsByConstructor=new Map}return n.prototype.registerDocNodes=function(e,t){var r=re.explainIfInvalidPackageName(e);if(r)throw new Error("Invalid NPM package name: "+r);for(var i=0,o=t;i0&&i.appendNodes(r),i}return Object.defineProperty(e.prototype,"nodes",{get:function(){return this._nodes},enumerable:!1,configurable:!0}),e.prototype.appendNode=function(t){if(!this.configuration.docNodeManager.isAllowedChild(this.kind,t.kind))throw new Error("The TSDocConfiguration does not allow a ".concat(this.kind," node to")+" contain a node of type ".concat(t.kind));this._nodes.push(t)},e.prototype.appendNodes=function(t){for(var r=0,i=t;r0){var i=this.nodes[this.nodes.length-1];i.kind===g.Paragraph&&(r=i)}r||(r=new at({configuration:this.configuration}),this.appendNode(r)),r.appendNode(t)},e.prototype.appendNodesInParagraph=function(t){for(var r=0,i=t;r1){var e=this._chunks.join("");this._chunks.length=1,this._chunks[0]=e}return this._chunks[0]},n}();var zi=function(){function n(){}return n.transform=function(e){for(var t=[],r=!1,i=[],o=[],a=!1,l=0,c=e.nodes;l0&&(r&&(i.push(" "),r=!1),i.push(P),o.push(d),a=!0),E&&a&&(r=!0);break;case g.SoftBreak:a&&(r=!0),o.push(d);break;default:r&&(i.push(" "),r=!1),i.length>0&&(t.push(new Ge({configuration:e.configuration,text:i.join("")})),i.length=0,o.length=0),t.push(d),a=!0}}i.length>0&&(t.push(new Ge({configuration:e.configuration,text:i.join("")})),i.length=0,o.length=0);var k=new at({configuration:e.configuration});return k.appendNodes(t),k},n}();var Or=function(){function n(){}return n.trimSpacesInParagraph=function(e){return zi.transform(e)},n}();var Mr=function(n,e,t){if(t||arguments.length===2)for(var r=0,i=e.length,o;r0&&(this._ensureLineSkipped(),this._renderNodes(a.modifierTagSet.nodes));break;case g.DeclarationReference:var l=e;this._writeContent(l.packageName),this._writeContent(l.importPath),(l.packageName!==void 0||l.importPath!==void 0)&&this._writeContent("#"),this._renderNodes(l.memberReferences);break;case g.ErrorText:var c=e;this._writeContent(c.text);break;case g.EscapedText:var d=e;this._writeContent(d.encodedText);break;case g.FencedCode:var m=e;this._ensureAtStartOfLine(),this._writeContent("```"),this._writeContent(m.language),this._writeNewline(),this._writeContent(m.code),this._writeContent("```"),this._writeNewline(),this._writeNewline();break;case g.HtmlAttribute:var y=e;this._writeContent(y.name),this._writeContent(y.spacingAfterName),this._writeContent("="),this._writeContent(y.spacingAfterEquals),this._writeContent(y.value),this._writeContent(y.spacingAfterValue);break;case g.HtmlEndTag:var b=e;this._writeContent("");break;case g.HtmlStartTag:var E=e;this._writeContent("<"),this._writeContent(E.name),this._writeContent(E.spacingAfterName);for(var P=E.spacingAfterName===void 0||E.spacingAfterName.length===0,k=0,w=E.htmlAttributes;k":">");break;case g.InheritDocTag:var $=e;this._renderInlineTag($,function(){$.declarationReference&&(t._writeContent(" "),t._renderNode($.declarationReference))});break;case g.InlineTag:var K=e;this._renderInlineTag(K,function(){K.tagContent.length>0&&(t._writeContent(" "),t._writeContent(K.tagContent))});break;case g.LinkTag:var C=e;this._renderInlineTag(C,function(){(C.urlDestination!==void 0||C.codeDestination!==void 0)&&(C.urlDestination!==void 0?(t._writeContent(" "),t._writeContent(C.urlDestination)):C.codeDestination!==void 0&&(t._writeContent(" "),t._renderNode(C.codeDestination))),C.linkText!==void 0&&(t._writeContent(" "),t._writeContent("|"),t._writeContent(" "),t._writeContent(C.linkText))});break;case g.MemberIdentifier:var H=e;H.hasQuotes?(this._writeContent('"'),this._writeContent(H.identifier),this._writeContent('"')):this._writeContent(H.identifier);break;case g.MemberReference:var I=e;I.hasDot&&this._writeContent("."),I.selector&&this._writeContent("("),I.memberSymbol?this._renderNode(I.memberSymbol):this._renderNode(I.memberIdentifier),I.selector&&(this._writeContent(":"),this._renderNode(I.selector),this._writeContent(")"));break;case g.MemberSelector:var J=e;this._writeContent(J.selector);break;case g.MemberSymbol:var te=e;this._writeContent("["),this._renderNode(te.symbolReference),this._writeContent("]");break;case g.Section:var ne=e;this._renderNodes(ne.nodes);break;case g.Paragraph:var Q=Or.trimSpacesInParagraph(e);Q.nodes.length>0&&(this._hangingParagraph?this._hangingParagraph=!1:this._ensureLineSkipped(),this._renderNodes(Q.nodes),this._writeNewline());break;case g.ParamBlock:var h=e;this._ensureLineSkipped(),this._renderNode(h.blockTag),this._writeContent(" "),this._writeContent(h.parameterName),this._writeContent(" - "),this._hangingParagraph=!0,this._renderNode(h.content),this._hangingParagraph=!1;break;case g.ParamCollection:var S=e;this._renderNodes(S.blocks);break;case g.PlainText:var f=e;this._writeContent(f.text);break}},n.prototype._renderInlineTag=function(e,t){this._writeContent("{"),this._writeContent(e.tagName),t(),this._writeContent("}")},n.prototype._renderNodes=function(e){for(var t=0,r=e;t1){for(var r=!0,i=0,o=t;i0?this.params:void 0,this.typeParams.count>0?this.typeParams:void 0,this.returnsBlock],this.customBlocks,!0),this.seeBlocks,!0),[this.inheritDocTag],!1),this.modifierTagSet.nodes,!0)},e.prototype.emitAsTsdoc=function(){var t=new st,r=new ct;return r.renderComment(t,this),t.toString()},e}(T);var Ma=function(){var n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(r,i){r.__proto__=i}||function(r,i){for(var o in i)Object.prototype.hasOwnProperty.call(i,o)&&(r[o]=i[o])},n(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");n(e,t);function r(){this.constructor=e}e.prototype=t===null?Object.create(t):(r.prototype=t.prototype,new r)}}(),Ba=function(n,e,t){if(t||arguments.length===2)for(var r=0,i=e.length,o;r=t},n._scanTextContent=function(e,t,r){for(var i=0,o=e;i=t||(r+=n._scanTextContent(a.getChildNodes(),t,r),r>=t))break}return r},n._countNonSpaceCharacters=function(e){for(var t=0,r=e.length,i=0;ithis.buffer.length)return{line:0,column:0};for(var t=1,r=1,i=0;ithis.buffer.length)throw new Error("TextRange.pos cannot exceed the associated text buffer length");if(this.end>this.buffer.length)throw new Error("TextRange.end cannot exceed the associated text buffer length")},n.empty=new n("",0,0),n}();var mn=function(){function n(e){this.messageId=e.messageId,this.unformattedText=e.messageText,this.textRange=e.textRange,this.tokenSequence=e.tokenSequence,this.docNode=e.docNode,this._text=void 0}return n._formatMessageText=function(e,t){if(e||(e="An unknown error occurred"),t.pos!==0||t.end!==0){var r=t.getLocation(t.pos);if(r.line)return"(".concat(r.line,",").concat(r.column,"): ")+e}return e},Object.defineProperty(n.prototype,"text",{get:function(){return this._text===void 0&&(this._text=n._formatMessageText(this.unformattedText,this.textRange)),this._text},enumerable:!1,configurable:!0}),n.prototype.toString=function(){return this.text},n}();var Br=function(){function n(){this._messages=[]}return Object.defineProperty(n.prototype,"messages",{get:function(){return this._messages},enumerable:!1,configurable:!0}),n.prototype.addMessage=function(e){this._messages.push(e)},n.prototype.addMessageForTextRange=function(e,t,r){this.addMessage(new mn({messageId:e,messageText:t,textRange:r}))},n.prototype.addMessageForTokenSequence=function(e,t,r,i){this.addMessage(new mn({messageId:e,messageText:t,textRange:r.getContainingTextRange(),tokenSequence:r,docNode:i}))},n.prototype.addMessageForDocErrorText=function(e){var t;e.textExcerpt?t=e.textExcerpt:t=e.errorLocation,this.addMessage(new mn({messageId:e.messageId,messageText:e.errorMessage,textRange:t.getContainingTextRange(),tokenSequence:t,docNode:e}))},n}();var Fr=function(){function n(e,t){this.commentRange=We.empty,this.lines=[],this.tokens=[],this.configuration=e,this.sourceRange=t,this.docComment=new Kn({configuration:this.configuration}),this.log=new Br}return n}();var xt=function(){function n(e){this.parserContext=e.parserContext,this._startIndex=e.startIndex,this._endIndex=e.endIndex,this._validateBounds()}return n.createEmpty=function(e){return new n({parserContext:e,startIndex:0,endIndex:0})},Object.defineProperty(n.prototype,"startIndex",{get:function(){return this._startIndex},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"endIndex",{get:function(){return this._endIndex},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"tokens",{get:function(){return this.parserContext.tokens.slice(this._startIndex,this._endIndex)},enumerable:!1,configurable:!0}),n.prototype.getNewSequence=function(e,t){return new n({parserContext:this.parserContext,startIndex:e,endIndex:t})},n.prototype.getContainingTextRange=function(){return this.isEmpty()?We.empty:this.parserContext.sourceRange.getNewRange(this.parserContext.tokens[this._startIndex].range.pos,this.parserContext.tokens[this._endIndex-1].range.end)},n.prototype.isEmpty=function(){return this._startIndex===this._endIndex},n.prototype.toString=function(){return this.tokens.map(function(e){return e.toString()}).join("")},n.prototype._validateBounds=function(){if(this.startIndex<0)throw new Error("TokenSequence.startIndex cannot be negative");if(this.endIndex<0)throw new Error("TokenSequence.endIndex cannot be negative");if(this.endIndexthis.parserContext.tokens.length)throw new Error("TokenSequence.startIndex cannot exceed the associated token array");if(this.endIndex>this.parserContext.tokens.length)throw new Error("TokenSequence.endIndex cannot exceed the associated token array")},n}();var pe;(function(n){n[n.BeginComment1=0]="BeginComment1",n[n.BeginComment2=1]="BeginComment2",n[n.CollectingFirstLine=2]="CollectingFirstLine",n[n.CollectingLine=3]="CollectingLine",n[n.AdvancingLine=4]="AdvancingLine",n[n.Done=5]="Done"})(pe||(pe={}));var Xi=function(){function n(){}return n.extract=function(e){for(var t=e.sourceRange,r=t.buffer,i=0,o=0,a=0,l=0,c=t.pos,d=pe.BeginComment1,m=[];d!==pe.Done;){if(c>=t.end)switch(d){case pe.BeginComment1:case pe.BeginComment2:return e.log.addMessageForTextRange(v.CommentNotFound,'Expecting a "/**" comment',t),!1;default:return e.log.addMessageForTextRange(v.CommentMissingClosingDelimiter,"Unexpected end of input",t),!1}var y=r[c],b=c;++c;var E=ca)&&m.push(t.getNewRange(a,l)),a=c,l=c,d=pe.AdvancingLine):y==="*"&&E==="/"?(l>a&&m.push(t.getNewRange(a,l)),a=0,l=0,++c,o=c,d=pe.Done):n._whitespaceCharacterRegExp.test(y)||(l=c);break;case pe.AdvancingLine:y==="*"?E==="/"?(a=0,l=0,++c,o=c,d=pe.Done):(E===" "&&++c,a=c,l=c,d=pe.CollectingLine):y===` `?(m.push(t.getNewRange(b,b)),a=c):n._whitespaceCharacterRegExp.test(y)||(l=c,d=pe.CollectingLine);break}}return e.commentRange=t.getNewRange(i,o),e.lines=m,!0},n._whitespaceCharacterRegExp=/^\s$/,n}();var gn=function(){function n(){}return n.readTokens=function(e){n._ensureInitialized();for(var t=[],r=void 0,i=0,o=e;i":u.GreaterThan,"=":u.Equals,"'":u.SingleQuote,'"':u.DoubleQuote,"/":u.Slash,"-":u.Hyphen,"@":u.AtSign,"{":u.LeftCurlyBracket,"}":u.RightCurlyBracket,"`":u.Backtick,".":u.Period,":":u.Colon,",":u.Comma,"[":u.LeftSquareBracket,"]":u.RightSquareBracket,"|":u.Pipe,"(":u.LeftParenthesis,")":u.RightParenthesis,"#":u.PoundSymbol,"+":u.Plus,$:u.DollarSign},o=0,a=Object.getOwnPropertyNames(i);o?@[\\]^`{|}~",n._wordCharacters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",n}();var Ir=function(){function n(e,t){if(this._parserContext=e,this.tokens=e.tokens,t){if(t.parserContext!==this._parserContext)throw new Error("The embeddedTokenSequence must use the same parser context");this._readerStartIndex=t.startIndex,this._readerEndIndex=t.endIndex}else this._readerStartIndex=0,this._readerEndIndex=this.tokens.length;this._currentIndex=this._readerStartIndex,this._accumulatedStartIndex=this._readerStartIndex}return n.prototype.extractAccumulatedSequence=function(){if(this._accumulatedStartIndex===this._currentIndex)throw new Error("Parser assertion failed: The queue should not be empty when extractAccumulatedSequence() is called");var e=new xt({parserContext:this._parserContext,startIndex:this._accumulatedStartIndex,endIndex:this._currentIndex});return this._accumulatedStartIndex=this._currentIndex,e},n.prototype.isAccumulatedSequenceEmpty=function(){return this._accumulatedStartIndex===this._currentIndex},n.prototype.tryExtractAccumulatedSequence=function(){if(!this.isAccumulatedSequenceEmpty())return this.extractAccumulatedSequence()},n.prototype.assertAccumulatedSequenceIsEmpty=function(){if(!this.isAccumulatedSequenceEmpty()){var e=new xt({parserContext:this._parserContext,startIndex:this._accumulatedStartIndex,endIndex:this._currentIndex}),t=e.tokens.map(function(r){return r.toString()});throw new Error(`Parser assertion failed: The queue should be empty, but it contains: `+JSON.stringify(t))}},n.prototype.peekToken=function(){return this.tokens[this._currentIndex]},n.prototype.peekTokenKind=function(){return this._currentIndex>=this._readerEndIndex?u.EndOfInput:this.tokens[this._currentIndex].kind},n.prototype.peekTokenAfterKind=function(){return this._currentIndex+1>=this._readerEndIndex?u.EndOfInput:this.tokens[this._currentIndex+1].kind},n.prototype.peekTokenAfterAfterKind=function(){return this._currentIndex+2>=this._readerEndIndex?u.EndOfInput:this.tokens[this._currentIndex+2].kind},n.prototype.readToken=function(){if(this._currentIndex>=this._readerEndIndex)throw new Error("Cannot read past end of stream");var e=this.tokens[this._currentIndex];if(e.kind===u.EndOfInput)throw new Error("The EndOfInput token cannot be read");return this._currentIndex++,e},n.prototype.peekPreviousTokenKind=function(){return this._currentIndex===0?u.EndOfInput:this.tokens[this._currentIndex-1].kind},n.prototype.createMarker=function(){return this._currentIndex},n.prototype.backtrackToMarker=function(e){if(e>this._currentIndex)throw new Error("The marker has expired");this._currentIndex=e,e" character should be escaped using a backslash to avoid confusion with an HTML tag'));break;case u.Backtick:this._pushAccumulatedPlainText(e),e.peekTokenAfterKind()===u.Backtick&&e.peekTokenAfterAfterKind()===u.Backtick?this._pushNode(this._parseFencedCode(e)):this._pushNode(this._parseCodeSpan(e));break;default:e.readToken();break}this._pushAccumulatedPlainText(e),this._performValidationChecks()},n.prototype._performValidationChecks=function(){var e=this._parserContext.docComment;e.deprecatedBlock&&(ir.hasAnyTextContent(e.deprecatedBlock)||this._parserContext.log.addMessageForTokenSequence(v.MissingDeprecationMessage,"The ".concat(e.deprecatedBlock.blockTag.tagName," block must include a deprecation message,")+" e.g. describing the recommended alternative",e.deprecatedBlock.blockTag.getTokenSequence(),e.deprecatedBlock)),e.inheritDocTag&&(e.remarksBlock&&this._parserContext.log.addMessageForTokenSequence(v.InheritDocIncompatibleTag,'A "'.concat(e.remarksBlock.blockTag.tagName,'" block must not be used, because that')+" content is provided by the @inheritDoc tag",e.remarksBlock.blockTag.getTokenSequence(),e.remarksBlock.blockTag),ir.hasAnyTextContent(e.summarySection)&&this._parserContext.log.addMessageForTextRange(v.InheritDocIncompatibleSummary,"The summary section must not have any content, because that content is provided by the @inheritDoc tag",this._parserContext.commentRange))},n.prototype._validateTagDefinition=function(e,t,r,i,o){if(e){var a=e.syntaxKind===R.InlineTag;a!==r?r?this._parserContext.log.addMessageForTokenSequence(v.TagShouldNotHaveBraces,'The TSDoc tag "'.concat(t,'" is not an inline tag; it must not be enclosed in "{ }" braces'),i,o):this._parserContext.log.addMessageForTokenSequence(v.InlineTagMissingBraces,'The TSDoc tag "'.concat(t,'" is an inline tag; it must be enclosed in "{ }" braces'),i,o):this._parserContext.configuration.validation.reportUnsupportedTags&&(this._parserContext.configuration.isTagSupported(e)||this._parserContext.log.addMessageForTokenSequence(v.UnsupportedTag,'The TSDoc tag "'.concat(t,'" is not supported by this tool'),i,o))}else this._parserContext.configuration.validation.ignoreUndefinedTags||this._parserContext.log.addMessageForTokenSequence(v.UndefinedTag,'The TSDoc tag "'.concat(t,'" is not defined in this configuration'),i,o)},n.prototype._pushAccumulatedPlainText=function(e){e.isAccumulatedSequenceEmpty()||this._pushNode(new Ge({parsed:!0,configuration:this._configuration,textExcerpt:e.extractAccumulatedSequence()}))},n.prototype._parseAndPushBlock=function(e){var t=this._parserContext.docComment,r=this._parserContext.configuration,i=t.modifierTagSet,o=this._parseBlockTag(e);if(o.kind!==g.BlockTag){this._pushNode(o);return}var a=o,l=r.tryGetTagDefinitionWithUpperCase(a.tagNameWithUpperCase);if(this._validateTagDefinition(l,a.tagName,!1,a.getTokenSequence(),a),l)switch(l.syntaxKind){case R.BlockTag:if(a.tagNameWithUpperCase===G.param.tagNameWithUpperCase){var c=this._parseParamBlock(e,a,G.param.tagName);this._parserContext.docComment.params.add(c),this._currentSection=c.content;return}else if(a.tagNameWithUpperCase===G.typeParam.tagNameWithUpperCase){var c=this._parseParamBlock(e,a,G.typeParam.tagName);this._parserContext.docComment.typeParams.add(c),this._currentSection=c.content;return}else{var d=new Gt({configuration:this._configuration,blockTag:a});this._addBlockToDocComment(d),this._currentSection=d.content}return;case R.ModifierTag:i.addTag(a);return}this._pushNode(a)},n.prototype._addBlockToDocComment=function(e){var t=this._parserContext.docComment;switch(e.blockTag.tagNameWithUpperCase){case G.remarks.tagNameWithUpperCase:t.remarksBlock=e;break;case G.privateRemarks.tagNameWithUpperCase:t.privateRemarks=e;break;case G.deprecated.tagNameWithUpperCase:t.deprecatedBlock=e;break;case G.returns.tagNameWithUpperCase:t.returnsBlock=e;break;case G.see.tagNameWithUpperCase:t._appendSeeBlock(e);break;default:t.appendCustomBlock(e)}},n.prototype._tryParseJSDocTypeOrValueRest=function(e,t,r,i){for(var o,a=1;a>0;){var l=e.peekTokenKind();switch(l){case t:o===void 0&&a++;break;case r:o===void 0&&a--;break;case u.Backslash:o!==void 0&&(e.readToken(),l=e.peekTokenKind());break;case u.DoubleQuote:case u.SingleQuote:case u.Backtick:o===l?o=void 0:o===void 0&&(o=l);break}if(l===u.EndOfInput){e.backtrackToMarker(i);return}e.readToken()}return e.tryExtractAccumulatedSequence()},n.prototype._tryParseUnsupportedJSDocType=function(e,t,r){if(e.assertAccumulatedSequenceIsEmpty(),!(e.peekTokenKind()!==u.LeftCurlyBracket||e.peekTokenAfterKind()===u.AtSign)){var i=e.createMarker();e.readToken();var o=this._tryParseJSDocTypeOrValueRest(e,u.LeftCurlyBracket,u.RightCurlyBracket,i);if(o){this._parserContext.log.addMessageForTokenSequence(v.ParamTagWithInvalidType,"The "+r+" block should not include a JSDoc-style '{type}'",o,t);var a=this._tryReadSpacingAndNewlines(e);a&&(o=o.getNewSequence(o.startIndex,a.endIndex))}return o}},n.prototype._tryParseJSDocOptionalNameRest=function(e){if(e.assertAccumulatedSequenceIsEmpty(),e.peekTokenKind()!==u.EndOfInput){var t=e.createMarker();return this._tryParseJSDocTypeOrValueRest(e,u.LeftSquareBracket,u.RightSquareBracket,t)}},n.prototype._parseParamBlock=function(e,t,r){var i=e.createMarker(),o=this._tryReadSpacingAndNewlines(e),a=this._tryParseUnsupportedJSDocType(e,t,r),l;e.peekTokenKind()===u.LeftSquareBracket&&(e.readToken(),l=e.extractAccumulatedSequence());for(var c="",d=!1;!d;)switch(e.peekTokenKind()){case u.AsciiWord:case u.Period:case u.DollarSign:c+=e.readToken();break;default:d=!0;break}var m=re.explainIfInvalidUnquotedIdentifier(c);if(m!==void 0){e.backtrackToMarker(i);var y=new dn({configuration:this._configuration,blockTag:t,parameterName:""}),b=c.length>0?"The "+r+" block should be followed by a valid parameter name: "+m:"The "+r+" block should be followed by a parameter name";return this._parserContext.log.addMessageForTokenSequence(v.ParamTagWithInvalidName,b,t.getTokenSequence(),t),y}var E=e.extractAccumulatedSequence(),P;if(l){P=this._tryParseJSDocOptionalNameRest(e);var k=l;P&&(k=t.getTokenSequence().getNewSequence(l.startIndex,P.endIndex)),this._parserContext.log.addMessageForTokenSequence(v.ParamTagWithInvalidOptionalName,"The "+r+" should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.",k,t)}var w=this._tryReadSpacingAndNewlines(e),M=this._tryParseUnsupportedJSDocType(e,t,r),$,K,C;return e.peekTokenKind()===u.Hyphen?(e.readToken(),$=e.extractAccumulatedSequence(),K=this._tryReadSpacingAndNewlines(e),C=this._tryParseUnsupportedJSDocType(e,t,r)):this._parserContext.log.addMessageForTokenSequence(v.ParamTagMissingHyphen,"The "+r+" block should be followed by a parameter name and then a hyphen",t.getTokenSequence(),t),new dn({parsed:!0,configuration:this._configuration,blockTag:t,spacingBeforeParameterNameExcerpt:o,unsupportedJsdocTypeBeforeParameterNameExcerpt:a,unsupportedJsdocOptionalNameOpenBracketExcerpt:l,parameterNameExcerpt:E,parameterName:c,unsupportedJsdocOptionalNameRestExcerpt:P,spacingAfterParameterNameExcerpt:w,unsupportedJsdocTypeAfterParameterNameExcerpt:M,hyphenExcerpt:$,spacingAfterHyphenExcerpt:K,unsupportedJsdocTypeAfterHyphenExcerpt:C})},n.prototype._pushNode=function(e){this._configuration.docNodeManager.isAllowedChild(g.Paragraph,e.kind)?this._currentSection.appendNodeInParagraph(e):this._currentSection.appendNode(e)},n.prototype._parseBackslashEscape=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker();if(e.readToken(),e.peekTokenKind()===u.EndOfInput)return this._backtrackAndCreateError(e,t,v.UnnecessaryBackslash,"A backslash must precede another character that is being escaped");var r=e.readToken();if(!gn.isPunctuation(r.kind))return this._backtrackAndCreateError(e,t,v.UnnecessaryBackslash,"A backslash can only be used to escape a punctuation character");var i=e.extractAccumulatedSequence();return new Yn({parsed:!0,configuration:this._configuration,escapeStyle:rr.CommonMarkBackslash,encodedTextExcerpt:i,decodedText:r.toString()})},n.prototype._parseBlockTag=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker();if(e.peekTokenKind()!==u.AtSign)return this._backtrackAndCreateError(e,t,v.MissingTag,'Expecting a TSDoc tag starting with "@"');switch(e.peekPreviousTokenKind()){case u.EndOfInput:case u.Spacing:case u.Newline:break;default:return this._backtrackAndCreateError(e,t,v.AtSignInWord,'The "@" character looks like part of a TSDoc tag; use a backslash to escape it')}var r=e.readToken().toString();if(e.peekTokenKind()!==u.AsciiWord)return this._backtrackAndCreateError(e,t,v.AtSignWithoutTagName,'Expecting a TSDoc tag name after "@"; if it is not a tag, use a backslash to escape this character');for(var i=e.createMarker();e.peekTokenKind()===u.AsciiWord;)r+=e.readToken().toString();switch(e.peekTokenKind()){case u.Spacing:case u.Newline:case u.EndOfInput:break;default:var o=e.peekToken().range.toString()[0];return this._backtrackAndCreateError(e,t,v.CharactersAfterBlockTag,'The token "'.concat(r,'" looks like a TSDoc tag but contains an invalid character')+" ".concat(JSON.stringify(o),'; if it is not a tag, use a backslash to escape the "@"'))}if(re.explainIfInvalidTSDocTagName(r)){var a=this._createFailureForTokensSince(e,v.MalformedTagName,"A TSDoc tag name must start with a letter and contain only letters and numbers",i);return this._backtrackAndCreateErrorForFailure(e,t,"",a)}return new Hn({parsed:!0,configuration:this._configuration,tagName:r,tagNameExcerpt:e.extractAccumulatedSequence()})},n.prototype._parseInlineTag=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker();if(e.peekTokenKind()!==u.LeftCurlyBracket)return this._backtrackAndCreateError(e,t,v.MissingTag,'Expecting a TSDoc tag starting with "{"');e.readToken();var r=e.extractAccumulatedSequence(),i=e.createMarker();if(e.peekTokenKind()!==u.AtSign)return this._backtrackAndCreateError(e,t,v.MalformedInlineTag,'Expecting a TSDoc tag starting with "{@"');for(var o=e.readToken().toString();e.peekTokenKind()===u.AsciiWord;)o+=e.readToken().toString();if(o==="@"){var a=this._createFailureForTokensSince(e,v.MalformedInlineTag,'Expecting a TSDoc inline tag name after the "{@" characters',i);return this._backtrackAndCreateErrorRangeForFailure(e,t,i,"",a)}if(re.explainIfInvalidTSDocTagName(o)){var a=this._createFailureForTokensSince(e,v.MalformedTagName,"A TSDoc tag name must start with a letter and contain only letters and numbers",i);return this._backtrackAndCreateErrorRangeForFailure(e,t,i,"",a)}var l=e.extractAccumulatedSequence(),c=this._tryReadSpacingAndNewlines(e);if(c===void 0&&e.peekTokenKind()!==u.RightCurlyBracket){var d=e.peekToken().range.toString()[0],a=this._createFailureForToken(e,v.CharactersAfterInlineTag,"The character ".concat(JSON.stringify(d)," cannot appear after the TSDoc tag name; expecting a space"));return this._backtrackAndCreateErrorRangeForFailure(e,t,i,"",a)}for(var m=!1;!m;)switch(e.peekTokenKind()){case u.EndOfInput:return this._backtrackAndCreateErrorRange(e,t,i,v.InlineTagMissingRightBrace,'The TSDoc inline tag name is missing its closing "}"');case u.Backslash:if(e.readToken(),!gn.isPunctuation(e.peekTokenKind())){var a=this._createFailureForToken(e,v.UnnecessaryBackslash,"A backslash can only be used to escape a punctuation character");return this._backtrackAndCreateErrorRangeForFailure(e,t,i,"Error reading inline TSDoc tag: ",a)}e.readToken();break;case u.LeftCurlyBracket:{var a=this._createFailureForToken(e,v.InlineTagUnescapedBrace,'The "{" character must be escaped with a backslash when used inside a TSDoc inline tag');return this._backtrackAndCreateErrorRangeForFailure(e,t,i,"",a)}case u.RightCurlyBracket:m=!0;break;default:e.readToken();break}var y=e.tryExtractAccumulatedSequence();e.readToken();var b=e.extractAccumulatedSequence(),E={parsed:!0,configuration:this._configuration,openingDelimiterExcerpt:r,tagNameExcerpt:l,tagName:o,spacingAfterTagNameExcerpt:c,tagContentExcerpt:y,closingDelimiterExcerpt:b},P=o.toUpperCase(),k=new Ir(this._parserContext,y||xt.createEmpty(this._parserContext)),w;switch(P){case G.inheritDoc.tagNameWithUpperCase:w=this._parseInheritDocTag(E,k);break;case G.link.tagNameWithUpperCase:w=this._parseLinkTag(E,k);break;default:w=new Wt(E)}var M=this._parserContext.configuration.tryGetTagDefinitionWithUpperCase(P);return this._validateTagDefinition(M,o,!0,l,w),w},n.prototype._parseInheritDocTag=function(e,t){var r=new Wt(e),i=or({},e);if(t.peekTokenKind()!==u.EndOfInput){if(i.declarationReference=this._parseDeclarationReference(t,e.tagNameExcerpt,r),!i.declarationReference)return r;if(t.peekTokenKind()!==u.EndOfInput)return t.readToken(),this._parserContext.log.addMessageForTokenSequence(v.InheritDocTagSyntax,"Unexpected character after declaration reference",t.extractAccumulatedSequence(),r),r}return new un(i)},n.prototype._parseLinkTag=function(e,t){var r=new Wt(e),i=or({},e);if(!e.tagContentExcerpt)return this._parserContext.log.addMessageForTokenSequence(v.LinkTagEmpty,"The @link tag content is missing",i.tagNameExcerpt,r),r;for(var o=t.peekTokenKind()===u.Slash&&t.peekTokenAfterKind()===u.Slash,a=t.createMarker(),l=o;!l;)switch(t.peekTokenKind()){case u.AsciiWord:case u.Period:case u.Hyphen:case u.Plus:t.readToken();break;case u.Colon:t.readToken(),o=t.peekTokenKind()===u.Slash&&t.peekTokenAfterKind()===u.Slash,l=!0;break;default:l=!0}if(t.backtrackToMarker(a),o){if(!this._parseLinkTagUrlDestination(t,i,e.tagNameExcerpt,r))return r}else if(!this._parseLinkTagCodeDestination(t,i,e.tagNameExcerpt,r))return r;if(t.peekTokenKind()===u.Spacing)throw new Error("Unconsumed spacing encountered after construct");if(t.peekTokenKind()===u.Pipe){t.readToken(),i.pipeExcerpt=t.extractAccumulatedSequence(),i.spacingAfterPipeExcerpt=this._tryReadSpacingAndNewlines(t),l=!1;for(var c=void 0;!l;)switch(t.peekTokenKind()){case u.EndOfInput:l=!0;break;case u.Pipe:case u.LeftCurlyBracket:var d=t.readToken().toString();return this._parserContext.log.addMessageForTokenSequence(v.LinkTagUnescapedText,'The "'.concat(d,'" character may not be used in the link text without escaping it'),t.extractAccumulatedSequence(),r),r;case u.Spacing:case u.Newline:t.readToken();break;default:c=t.createMarker()+1,t.readToken()}var m=t.tryExtractAccumulatedSequence();m&&(c===void 0?i.spacingAfterLinkTextExcerpt=m:c>=m.endIndex?i.linkTextExcerpt=m:(i.linkTextExcerpt=m.getNewSequence(m.startIndex,c),i.spacingAfterLinkTextExcerpt=m.getNewSequence(c,m.endIndex)))}else if(t.peekTokenKind()!==u.EndOfInput)return t.readToken(),this._parserContext.log.addMessageForTokenSequence(v.LinkTagDestinationSyntax,"Unexpected character after link destination",t.extractAccumulatedSequence(),r),r;return new zn(i)},n.prototype._parseLinkTagUrlDestination=function(e,t,r,i){for(var o="",a=!1;!a;)switch(e.peekTokenKind()){case u.Spacing:case u.Newline:case u.EndOfInput:case u.Pipe:case u.RightCurlyBracket:a=!0;break;default:o+=e.readToken();break}if(o.length===0)throw new Error("Missing URL in _parseLinkTagUrlDestination()");var l=e.extractAccumulatedSequence(),c=re.explainIfInvalidLinkUrl(o);return c?(this._parserContext.log.addMessageForTokenSequence(v.LinkTagInvalidUrl,c,l,i),!1):(t.urlDestinationExcerpt=l,t.spacingAfterDestinationExcerpt=this._tryReadSpacingAndNewlines(e),!0)},n.prototype._parseLinkTagCodeDestination=function(e,t,r,i){return t.codeDestination=this._parseDeclarationReference(e,r,i),!!t.codeDestination},n.prototype._parseDeclarationReference=function(e,t,r){e.assertAccumulatedSequenceIsEmpty();for(var i=e.createMarker(),o=!1,a=!0,l=!1,c=!1;!c;)switch(e.peekTokenKind()){case u.DoubleQuote:case u.EndOfInput:case u.LeftCurlyBracket:case u.LeftParenthesis:case u.LeftSquareBracket:case u.Newline:case u.Pipe:case u.RightCurlyBracket:case u.RightParenthesis:case u.RightSquareBracket:case u.SingleQuote:case u.Spacing:c=!0;break;case u.PoundSymbol:o=!0,c=!0;break;case u.Slash:case u.AtSign:a&&(l=!0),e.readToken();break;case u.AsciiWord:case u.Period:case u.Hyphen:e.readToken();break;default:a=!1,e.readToken()}if(!o&&l){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingHash,'The declaration reference appears to contain a package name or import path, but it is missing the "#" delimiter',e.extractAccumulatedSequence(),r);return}e.backtrackToMarker(i);var d,m,y,b;if(o){if(e.peekTokenKind()!==u.Period){var E=e.peekTokenKind()===u.AtSign,P=!1;for(c=!1;!c;)switch(e.peekTokenKind()){case u.EndOfInput:throw new Error("Expecting pound symbol");case u.Slash:E&&!P?(e.readToken(),P=!0):c=!0;break;case u.PoundSymbol:c=!0;break;default:e.readToken()}if(!e.isAccumulatedSequenceEmpty()){d=e.extractAccumulatedSequence();var k=re.explainIfInvalidPackageName(d.toString());if(k){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMalformedPackageName,k,d,r);return}}}for(c=!1;!c;)switch(e.peekTokenKind()){case u.EndOfInput:throw new Error("Expecting pound symbol");case u.PoundSymbol:c=!0;break;default:e.readToken()}if(!e.isAccumulatedSequenceEmpty()){m=e.extractAccumulatedSequence();var k=re.explainIfInvalidImportPath(m.toString(),!!d);if(k){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMalformedImportPath,k,m,r);return}}if(e.peekTokenKind()!==u.PoundSymbol)throw new Error("Expecting pound symbol");if(e.readToken(),y=e.extractAccumulatedSequence(),b=this._tryReadSpacingAndNewlines(e),d===void 0&&m===void 0){this._parserContext.log.addMessageForTokenSequence(v.ReferenceHashSyntax,"The hash character must be preceded by a package name or import path",y,r);return}}var w=[];for(c=!1;!c;)switch(e.peekTokenKind()){case u.Period:case u.LeftParenthesis:case u.AsciiWord:case u.Colon:case u.LeftSquareBracket:case u.DoubleQuote:var M=w.length>0,$=this._parseMemberReference(e,M,t,r);if(!$)return;w.push($);break;default:c=!0}if(d===void 0&&m===void 0&&w.length===0){this._parserContext.log.addMessageForTokenSequence(v.MissingReference,"Expecting a declaration reference",t,r);return}return new Rn({parsed:!0,configuration:this._configuration,packageNameExcerpt:d,importPathExcerpt:m,importHashExcerpt:y,spacingAfterImportHashExcerpt:b,memberReferences:w})},n.prototype._parseMemberReference=function(e,t,r,i){var o={parsed:!0,configuration:this._configuration};if(t){if(e.peekTokenKind()!==u.Period){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingDot,"Expecting a period before the next component of a declaration reference",r,i);return}e.readToken(),o.dotExcerpt=e.extractAccumulatedSequence(),o.spacingAfterDotExcerpt=this._tryReadSpacingAndNewlines(e)}if(e.peekTokenKind()===u.LeftParenthesis&&(e.readToken(),o.leftParenthesisExcerpt=e.extractAccumulatedSequence(),o.spacingAfterLeftParenthesisExcerpt=this._tryReadSpacingAndNewlines(e)),e.peekTokenKind()===u.LeftSquareBracket){if(o.memberSymbol=this._parseMemberSymbol(e,i),!o.memberSymbol)return}else if(o.memberIdentifier=this._parseMemberIdentifier(e,r,i),!o.memberIdentifier)return;if(o.spacingAfterMemberExcerpt=this._tryReadSpacingAndNewlines(e),e.peekTokenKind()===u.Colon){if(e.readToken(),o.colonExcerpt=e.extractAccumulatedSequence(),o.spacingAfterColonExcerpt=this._tryReadSpacingAndNewlines(e),!o.leftParenthesisExcerpt){this._parserContext.log.addMessageForTokenSequence(v.ReferenceSelectorMissingParens,"Syntax error in declaration reference: the member selector must be enclosed in parentheses",o.colonExcerpt,i);return}if(o.selector=this._parseMemberSelector(e,o.colonExcerpt,i),!o.selector)return;o.spacingAfterSelectorExcerpt=this._tryReadSpacingAndNewlines(e)}else if(o.leftParenthesisExcerpt){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingColon,"Expecting a colon after the identifier because the expression is in parentheses",o.leftParenthesisExcerpt,i);return}if(o.leftParenthesisExcerpt){if(e.peekTokenKind()!==u.RightParenthesis){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingRightParen,"Expecting a matching right parenthesis",o.leftParenthesisExcerpt,i);return}e.readToken(),o.rightParenthesisExcerpt=e.extractAccumulatedSequence(),o.spacingAfterRightParenthesisExcerpt=this._tryReadSpacingAndNewlines(e)}return new Jn(o)},n.prototype._parseMemberSymbol=function(e,t){if(e.peekTokenKind()!==u.LeftSquareBracket)throw new Error('Expecting "["');e.readToken();var r=e.extractAccumulatedSequence(),i=this._tryReadSpacingAndNewlines(e),o=this._parseDeclarationReference(e,r,t);if(!o){this._parserContext.log.addMessageForTokenSequence(v.ReferenceSymbolSyntax,"Missing declaration reference in symbol reference",r,t);return}if(e.peekTokenKind()!==u.RightSquareBracket){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingRightBracket,"Missing closing square bracket for symbol reference",r,t);return}e.readToken();var a=e.extractAccumulatedSequence();return new Xn({parsed:!0,configuration:this._configuration,leftBracketExcerpt:r,spacingAfterLeftBracketExcerpt:i,symbolReference:o,rightBracketExcerpt:a})},n.prototype._parseMemberIdentifier=function(e,t,r){var i=void 0,o=void 0;if(e.peekTokenKind()===u.DoubleQuote){for(e.readToken(),i=e.extractAccumulatedSequence();e.peekTokenKind()!==u.DoubleQuote;){if(e.peekTokenKind()===u.EndOfInput){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingQuote,"Unexpected end of input inside quoted member identifier",i,r);return}e.readToken()}if(e.isAccumulatedSequenceEmpty()){this._parserContext.log.addMessageForTokenSequence(v.ReferenceEmptyIdentifier,"The quoted identifier cannot be empty",i,r);return}var a=e.extractAccumulatedSequence();return e.readToken(),o=e.extractAccumulatedSequence(),new fn({parsed:!0,configuration:this._configuration,leftQuoteExcerpt:i,identifierExcerpt:a,rightQuoteExcerpt:o})}else{for(var l=!1;!l;)switch(e.peekTokenKind()){case u.AsciiWord:case u.DollarSign:e.readToken();break;default:l=!0;break}if(e.isAccumulatedSequenceEmpty()){this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingIdentifier,"Syntax error in declaration reference: expecting a member identifier",t,r);return}var a=e.extractAccumulatedSequence(),c=a.toString(),d=re.explainIfInvalidUnquotedMemberIdentifier(c);if(d){this._parserContext.log.addMessageForTokenSequence(v.ReferenceUnquotedIdentifier,d,a,r);return}return new fn({parsed:!0,configuration:this._configuration,leftQuoteExcerpt:i,identifierExcerpt:a,rightQuoteExcerpt:o})}},n.prototype._parseMemberSelector=function(e,t,r){e.peekTokenKind()!==u.AsciiWord&&this._parserContext.log.addMessageForTokenSequence(v.ReferenceMissingLabel,"Expecting a selector label after the colon",t,r);var i=e.readToken().toString(),o=e.extractAccumulatedSequence(),a=new Qn({parsed:!0,configuration:this._configuration,selectorExcerpt:o,selector:i});if(a.errorMessage){this._parserContext.log.addMessageForTokenSequence(v.ReferenceSelectorSyntax,a.errorMessage,o,r);return}return a},n.prototype._parseHtmlStartTag=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker(),r=e.readToken();if(r.kind!==u.LessThan)throw new Error('Expecting an HTML tag starting with "<"');var i=e.extractAccumulatedSequence(),o=this._parseHtmlName(e);if(hn(o))return this._backtrackAndCreateErrorForFailure(e,t,"Invalid HTML element: ",o);for(var a=this._tryReadSpacingAndNewlines(e),l=[];e.peekTokenKind()===u.AsciiWord;){var c=this._parseHtmlAttribute(e);if(hn(c))return this._backtrackAndCreateErrorForFailure(e,t,"The HTML element has an invalid attribute: ",c);l.push(c)}e.assertAccumulatedSequenceIsEmpty();var d=e.createMarker(),m=!1;if(e.peekTokenKind()===u.Slash&&(e.readToken(),m=!0),e.peekTokenKind()!==u.GreaterThan){var y=this._createFailureForTokensSince(e,v.HtmlTagMissingGreaterThan,'Expecting an attribute or ">" or "/>"',d);return this._backtrackAndCreateErrorForFailure(e,t,"The HTML tag has invalid syntax: ",y)}e.readToken();var b=e.extractAccumulatedSequence();return new Vn({parsed:!0,configuration:this._configuration,openingDelimiterExcerpt:i,nameExcerpt:o,spacingAfterNameExcerpt:a,htmlAttributes:l,selfClosingTag:m,closingDelimiterExcerpt:b})},n.prototype._parseHtmlAttribute=function(e){e.assertAccumulatedSequenceIsEmpty();var t=this._parseHtmlName(e);if(hn(t))return t;var r=this._tryReadSpacingAndNewlines(e);if(e.peekTokenKind()!==u.Equals)return this._createFailureForToken(e,v.HtmlTagMissingEquals,'Expecting "=" after HTML attribute name');e.readToken();var i=e.extractAccumulatedSequence(),o=this._tryReadSpacingAndNewlines(e),a=this._parseHtmlString(e);if(hn(a))return a;var l=e.extractAccumulatedSequence(),c=this._tryReadSpacingAndNewlines(e);return new Gn({parsed:!0,configuration:this._configuration,nameExcerpt:t,spacingAfterNameExcerpt:r,equalsExcerpt:i,spacingAfterEqualsExcerpt:o,valueExcerpt:l,spacingAfterValueExcerpt:c})},n.prototype._parseHtmlString=function(e){var t=e.createMarker(),r=e.peekTokenKind();if(r!==u.DoubleQuote&&r!==u.SingleQuote)return this._createFailureForToken(e,v.HtmlTagMissingString,"Expecting an HTML string starting with a single-quote or double-quote character");e.readToken();for(var i="";;){var o=e.peekTokenKind();if(o===r){e.readToken();break}if(o===u.EndOfInput||o===u.Newline)return this._createFailureForToken(e,v.HtmlStringMissingQuote,"The HTML string is missing its closing quote",t);i+=e.readToken().toString()}return e.peekTokenKind()===u.AsciiWord?this._createFailureForToken(e,v.TextAfterHtmlString,"The next character after a closing quote must be spacing or punctuation"):i},n.prototype._parseHtmlEndTag=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker(),r=e.peekToken();if(r.kind!==u.LessThan)return this._backtrackAndCreateError(e,t,v.MissingHtmlEndTag,'Expecting an HTML tag starting with "" for the HTML tag');return this._backtrackAndCreateErrorForFailure(e,t,"",c)}e.readToken();var d=e.extractAccumulatedSequence();return new Wn({parsed:!0,configuration:this._configuration,openingDelimiterExcerpt:o,nameExcerpt:a,spacingAfterNameExcerpt:l,closingDelimiterExcerpt:d})},n.prototype._parseHtmlName=function(e){var t=e.createMarker();if(e.peekTokenKind()===u.Spacing)return this._createFailureForTokensSince(e,v.MalformedHtmlName,"A space is not allowed here",t);for(var r=!1;!r;)switch(e.peekTokenKind()){case u.Hyphen:case u.Period:case u.AsciiWord:e.readToken();break;default:r=!0;break}var i=e.tryExtractAccumulatedSequence();if(!i)return this._createFailureForToken(e,v.MalformedHtmlName,"Expecting an HTML name");var o=i.toString(),a=re.explainIfInvalidHtmlName(o);return a?this._createFailureForTokensSince(e,v.MalformedHtmlName,a,t):this._configuration.validation.reportUnsupportedHtmlElements&&!this._configuration.isHtmlElementSupported(o)?this._createFailureForToken(e,v.UnsupportedHtmlElementName,"The HTML element name ".concat(JSON.stringify(o)," is not defined by your TSDoc configuration"),t):i},n.prototype._parseFencedCode=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker(),r=t+2;switch(e.peekPreviousTokenKind()){case u.Newline:case u.EndOfInput:break;default:return this._backtrackAndCreateErrorRange(e,t,r,v.CodeFenceOpeningIndent,"The opening backtick for a code fence must appear at the start of the line")}var i="";if(i+=e.readToken(),i+=e.readToken(),i+=e.readToken(),i!=="```")throw new Error("Expecting three backticks");for(var o=e.extractAccumulatedSequence();e.peekTokenKind()===u.Spacing;)e.readToken();for(var a=e.tryExtractAccumulatedSequence(),l=!1,c=void 0;!l;)switch(e.peekTokenKind()){case u.Spacing:case u.Newline:c===void 0&&(c=e.createMarker()),e.peekTokenKind()===u.Newline&&(l=!0),e.readToken();break;case u.Backtick:var d=this._createFailureForToken(e,v.CodeFenceSpecifierSyntax,"The language specifier cannot contain backtick characters");return this._backtrackAndCreateErrorRangeForFailure(e,t,r,"Error parsing code fence: ",d);case u.EndOfInput:var m=this._createFailureForToken(e,v.CodeFenceMissingDelimiter,"Missing closing delimiter");return this._backtrackAndCreateErrorRangeForFailure(e,t,r,"Error parsing code fence: ",m);default:c=void 0,e.readToken();break}var y=e.extractAccumulatedSequence(),b=y.getNewSequence(y.startIndex,c),E=y.getNewSequence(c,y.endIndex),P=-1,k=-1;l=!1;for(var w;!l;)switch(e.peekTokenKind()){case u.EndOfInput:var m=this._createFailureForToken(e,v.CodeFenceMissingDelimiter,"Missing closing delimiter");return this._backtrackAndCreateErrorRangeForFailure(e,t,r,"Error parsing code fence: ",m);case u.Newline:for(w=e.readToken(),P=e.createMarker();e.peekTokenKind()===u.Spacing;)w=e.readToken();if(e.peekTokenKind()!==u.Backtick||(k=e.createMarker(),e.readToken(),e.peekTokenKind()!==u.Backtick)||(e.readToken(),e.peekTokenKind()!==u.Backtick))break;e.readToken(),l=!0;break;default:e.readToken();break}w.kind!==u.Newline&&this._parserContext.log.addMessageForTextRange(v.CodeFenceClosingIndent,"The closing delimiter for a code fence must not be indented",w.range);var M=e.extractAccumulatedSequence(),$=M.getNewSequence(M.startIndex,P),K=M.getNewSequence(P,k),C=M.getNewSequence(k,M.endIndex);for(l=!1;!l;)switch(e.peekTokenKind()){case u.Spacing:e.readToken();break;case u.Newline:l=!0,e.readToken();break;case u.EndOfInput:l=!0;break;default:this._parserContext.log.addMessageForTextRange(v.CodeFenceClosingSyntax,"Unexpected characters after closing delimiter for code fence",e.peekToken().range),l=!0;break}var H=e.tryExtractAccumulatedSequence();return new Un({parsed:!0,configuration:this._configuration,openingFenceExcerpt:o,spacingAfterOpeningFenceExcerpt:a,languageExcerpt:b,spacingAfterLanguageExcerpt:E,codeExcerpt:$,spacingBeforeClosingFenceExcerpt:K,closingFenceExcerpt:C,spacingAfterClosingFenceExcerpt:H})},n.prototype._parseCodeSpan=function(e){e.assertAccumulatedSequenceIsEmpty();var t=e.createMarker();if(e.peekTokenKind()!==u.Backtick)throw new Error('Expecting a code span starting with a backtick character "`"');e.readToken();for(var r=e.extractAccumulatedSequence(),i=void 0,o=void 0;;){var a=e.peekTokenKind();if(a===u.Backtick){if(e.isAccumulatedSequenceEmpty())return this._backtrackAndCreateErrorRange(e,t,t+1,v.CodeSpanEmpty,"A code span must contain at least one character between the backticks");i=e.extractAccumulatedSequence(),e.readToken(),o=e.extractAccumulatedSequence();break}if(a===u.EndOfInput||a===u.Newline)return this._backtrackAndCreateError(e,t,v.CodeSpanMissingDelimiter,"The code span is missing its closing backtick");e.readToken()}return new $n({parsed:!0,configuration:this._configuration,openingDelimiterExcerpt:r,codeExcerpt:i,closingDelimiterExcerpt:o})},n.prototype._tryReadSpacingAndNewlines=function(e){var t=!1;do switch(e.peekTokenKind()){case u.Spacing:case u.Newline:e.readToken();break;default:t=!0;break}while(!t);return e.tryExtractAccumulatedSequence()},n.prototype._createError=function(e,t,r){e.readToken();var i=e.extractAccumulatedSequence(),o=new St({parsed:!0,configuration:this._configuration,textExcerpt:i,messageId:t,errorMessage:r,errorLocation:i});return this._parserContext.log.addMessageForDocErrorText(o),o},n.prototype._backtrackAndCreateError=function(e,t,r,i){return e.backtrackToMarker(t),this._createError(e,r,i)},n.prototype._backtrackAndCreateErrorRange=function(e,t,r,i,o){for(e.backtrackToMarker(t);e.createMarker()!==r;)e.readToken();e.peekTokenKind()!==u.EndOfInput&&e.readToken();var a=e.extractAccumulatedSequence(),l=new St({parsed:!0,configuration:this._configuration,textExcerpt:a,messageId:i,errorMessage:o,errorLocation:a});return this._parserContext.log.addMessageForDocErrorText(l),l},n.prototype._backtrackAndCreateErrorForFailure=function(e,t,r,i){e.backtrackToMarker(t),e.readToken();var o=e.extractAccumulatedSequence(),a=new St({parsed:!0,configuration:this._configuration,textExcerpt:o,messageId:i.failureMessageId,errorMessage:r+i.failureMessage,errorLocation:i.failureLocation});return this._parserContext.log.addMessageForDocErrorText(a),a},n.prototype._backtrackAndCreateErrorRangeForFailure=function(e,t,r,i,o){for(e.backtrackToMarker(t);e.createMarker()!==r;)e.readToken();e.peekTokenKind()!==u.EndOfInput&&e.readToken();var a=e.extractAccumulatedSequence(),l=new St({parsed:!0,configuration:this._configuration,textExcerpt:a,messageId:o.failureMessageId,errorMessage:i+o.failureMessage,errorLocation:o.failureLocation});return this._parserContext.log.addMessageForDocErrorText(l),l},n.prototype._createFailureForToken=function(e,t,r,i){i||(i=e.createMarker());var o=new xt({parserContext:this._parserContext,startIndex:i,endIndex:i+1});return{failureMessageId:t,failureMessage:r,failureLocation:o}},n.prototype._createFailureForTokensSince=function(e,t,r,i){var o=e.createMarker();if(osetTimeout(e,n))}function no(n){return n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ro(){return/(<%(?:-|_)?\s*[*~]{0,1})\+((?:.|\s)*?%>)/g}function Za(n,e){e=(0,Ve.normalizePath)(e);let t=n.vault.getAbstractFileByPath(e);if(!t)throw new O(`Folder "${e}" doesn't exist`);if(!(t instanceof Ve.TFolder))throw new O(`${e} is a file, not a folder`);return t}function Dt(n,e){e=(0,Ve.normalizePath)(e);let t=n.vault.getAbstractFileByPath(e);if(!t)throw new O(`File "${e}" doesn't exist`);if(!(t instanceof Ve.TFile))throw new O(`${e} is a folder, not a file`);return t}function ze(n,e){let t=Za(n,e),r=[];return Ve.Vault.recurseChildren(t,i=>{i instanceof Ve.TFile&&r.push(i)}),r.sort((i,o)=>i.path.localeCompare(o.path)),r}async function io(n,e){return await Promise.all(e.map(async r=>{let i=await n.vault.cachedRead(r);return es(r,i)}))}function es(n,e){let r=new qr().parseString(e),i=new Lr(n);return i.description=ts(r.docComment.summarySection),i.returns=ns(r.docComment.returnsBlock),i.arguments=rs(r.docComment.params),i}function ts(n){try{return n.nodes.map(t=>t.getChildNodes().filter(r=>r instanceof Ge).map(r=>r.text).join(` `)).join(` `)}catch(e){throw console.error("Failed to parse sumamry section"),e}}function ns(n){if(!n)return"";try{return n.content.nodes[0].getChildNodes()[0].text.trim()}catch{return""}}function rs(n){try{return n.blocks.map(r=>{let i=r.parameterName,o=r.content.getChildNodes()[0].getChildNodes().filter(a=>a instanceof Ge).map(a=>a.text).join(" ");return new Hr(i,o)})}catch{return[]}}function Pt(n,e,t){if(t<0||t===n.length)return;let r=n[e];n[e]=n[t],n[t]=r}function Jt(n){return n.workspace.activeEditor?.file??n.workspace.getActiveFile()}function oo(n){let e=n.lastIndexOf("/");return e!==-1?n.slice(0,e):""}function $r(n){return n!==null&&typeof n=="object"}function ao(n){let e=n.toString(),t=e.indexOf("(");return e.substring(t+1,e.indexOf(")")).replace(/ /g,"").split(",")}function Kr(n,e,t){let r=n instanceof HTMLOListElement?"li":"p",i=n.createEl(r),o=n.createEl("b",{text:e});return i.appendChild(o),i.appendChild(document.createTextNode(`: ${t}`)),i}var Po=X(require("obsidian"));var Co=X(require("obsidian"));var ie="top",ue="bottom",ce="right",ae="left",sr="auto",yt=[ie,ue,ce,ae],lt="start",Nt="end",so="clippingParents",cr="viewport",Qt="popper",co="reference",Rr=yt.reduce(function(n,e){return n.concat([e+"-"+lt,e+"-"+Nt])},[]),lr=[].concat(yt,[sr]).reduce(function(n,e){return n.concat([e,e+"-"+lt,e+"-"+Nt])},[]),is="beforeRead",os="read",as="afterRead",ss="beforeMain",cs="main",ls="afterMain",ps="beforeWrite",us="write",fs="afterWrite",lo=[is,os,as,ss,cs,ls,ps,us,fs];function de(n){return n?(n.nodeName||"").toLowerCase():null}function Z(n){if(n==null)return window;if(n.toString()!=="[object Window]"){var e=n.ownerDocument;return e&&e.defaultView||window}return n}function De(n){var e=Z(n).Element;return n instanceof e||n instanceof Element}function fe(n){var e=Z(n).HTMLElement;return n instanceof e||n instanceof HTMLElement}function Xt(n){if(typeof ShadowRoot=="undefined")return!1;var e=Z(n).ShadowRoot;return n instanceof e||n instanceof ShadowRoot}function ds(n){var e=n.state;Object.keys(e.elements).forEach(function(t){var r=e.styles[t]||{},i=e.attributes[t]||{},o=e.elements[t];!fe(o)||!de(o)||(Object.assign(o.style,r),Object.keys(i).forEach(function(a){var l=i[a];l===!1?o.removeAttribute(a):o.setAttribute(a,l===!0?"":l)}))})}function ms(n){var e=n.state,t={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,t.popper),e.styles=t,e.elements.arrow&&Object.assign(e.elements.arrow.style,t.arrow),function(){Object.keys(e.elements).forEach(function(r){var i=e.elements[r],o=e.attributes[r]||{},a=Object.keys(e.styles.hasOwnProperty(r)?e.styles[r]:t[r]),l=a.reduce(function(c,d){return c[d]="",c},{});!fe(i)||!de(i)||(Object.assign(i.style,l),Object.keys(o).forEach(function(c){i.removeAttribute(c)}))})}}var po={name:"applyStyles",enabled:!0,phase:"write",fn:ds,effect:ms,requires:["computeStyles"]};function me(n){return n.split("-")[0]}var Fe=Math.max,Ot=Math.min,pt=Math.round;function Zt(){var n=navigator.userAgentData;return n!=null&&n.brands&&Array.isArray(n.brands)?n.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function An(){return!/^((?!chrome|android).)*safari/i.test(Zt())}function Pe(n,e,t){e===void 0&&(e=!1),t===void 0&&(t=!1);var r=n.getBoundingClientRect(),i=1,o=1;e&&fe(n)&&(i=n.offsetWidth>0&&pt(r.width)/n.offsetWidth||1,o=n.offsetHeight>0&&pt(r.height)/n.offsetHeight||1);var a=De(n)?Z(n):window,l=a.visualViewport,c=!An()&&t,d=(r.left+(c&&l?l.offsetLeft:0))/i,m=(r.top+(c&&l?l.offsetTop:0))/o,y=r.width/i,b=r.height/o;return{width:y,height:b,top:m,right:d+y,bottom:m+b,left:d,x:d,y:m}}function Mt(n){var e=Pe(n),t=n.offsetWidth,r=n.offsetHeight;return Math.abs(e.width-t)<=1&&(t=e.width),Math.abs(e.height-r)<=1&&(r=e.height),{x:n.offsetLeft,y:n.offsetTop,width:t,height:r}}function _n(n,e){var t=e.getRootNode&&e.getRootNode();if(n.contains(e))return!0;if(t&&Xt(t)){var r=e;do{if(r&&n.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function ve(n){return Z(n).getComputedStyle(n)}function Yr(n){return["table","td","th"].indexOf(de(n))>=0}function he(n){return((De(n)?n.ownerDocument:n.document)||window.document).documentElement}function ut(n){return de(n)==="html"?n:n.assignedSlot||n.parentNode||(Xt(n)?n.host:null)||he(n)}function uo(n){return!fe(n)||ve(n).position==="fixed"?null:n.offsetParent}function gs(n){var e=/firefox/i.test(Zt()),t=/Trident/i.test(Zt());if(t&&fe(n)){var r=ve(n);if(r.position==="fixed")return null}var i=ut(n);for(Xt(i)&&(i=i.host);fe(i)&&["html","body"].indexOf(de(i))<0;){var o=ve(i);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||e&&o.willChange==="filter"||e&&o.filter&&o.filter!=="none")return i;i=i.parentNode}return null}function Ie(n){for(var e=Z(n),t=uo(n);t&&Yr(t)&&ve(t).position==="static";)t=uo(t);return t&&(de(t)==="html"||de(t)==="body"&&ve(t).position==="static")?e:t||gs(n)||e}function Bt(n){return["top","bottom"].indexOf(n)>=0?"x":"y"}function Ft(n,e,t){return Fe(n,Ot(e,t))}function fo(n,e,t){var r=Ft(n,e,t);return r>t?t:r}function xn(){return{top:0,right:0,bottom:0,left:0}}function yn(n){return Object.assign({},xn(),n)}function jn(n,e){return e.reduce(function(t,r){return t[r]=n,t},{})}var hs=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,yn(typeof e!="number"?e:jn(e,yt))};function As(n){var e,t=n.state,r=n.name,i=n.options,o=t.elements.arrow,a=t.modifiersData.popperOffsets,l=me(t.placement),c=Bt(l),d=[ae,ce].indexOf(l)>=0,m=d?"height":"width";if(!(!o||!a)){var y=hs(i.padding,t),b=Mt(o),E=c==="y"?ie:ae,P=c==="y"?ue:ce,k=t.rects.reference[m]+t.rects.reference[c]-a[c]-t.rects.popper[m],w=a[c]-t.rects.reference[c],M=Ie(o),$=M?c==="y"?M.clientHeight||0:M.clientWidth||0:0,K=k/2-w/2,C=y[E],H=$-b[m]-y[P],I=$/2-b[m]/2+K,J=Ft(C,I,H),te=c;t.modifiersData[r]=(e={},e[te]=J,e.centerOffset=J-I,e)}}function _s(n){var e=n.state,t=n.options,r=t.element,i=r===void 0?"[data-popper-arrow]":r;i!=null&&(typeof i=="string"&&(i=e.elements.popper.querySelector(i),!i)||!_n(e.elements.popper,i)||(e.elements.arrow=i))}var mo={name:"arrow",enabled:!0,phase:"main",fn:As,effect:_s,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Ne(n){return n.split("-")[1]}var xs={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ys(n,e){var t=n.x,r=n.y,i=e.devicePixelRatio||1;return{x:pt(t*i)/i||0,y:pt(r*i)/i||0}}function go(n){var e,t=n.popper,r=n.popperRect,i=n.placement,o=n.variation,a=n.offsets,l=n.position,c=n.gpuAcceleration,d=n.adaptive,m=n.roundOffsets,y=n.isFixed,b=a.x,E=b===void 0?0:b,P=a.y,k=P===void 0?0:P,w=typeof m=="function"?m({x:E,y:k}):{x:E,y:k};E=w.x,k=w.y;var M=a.hasOwnProperty("x"),$=a.hasOwnProperty("y"),K=ae,C=ie,H=window;if(d){var I=Ie(t),J="clientHeight",te="clientWidth";if(I===Z(t)&&(I=he(t),ve(I).position!=="static"&&l==="absolute"&&(J="scrollHeight",te="scrollWidth")),I=I,i===ie||(i===ae||i===ce)&&o===Nt){C=ue;var ne=y&&I===H&&H.visualViewport?H.visualViewport.height:I[J];k-=ne-r.height,k*=c?1:-1}if(i===ae||(i===ie||i===ue)&&o===Nt){K=ce;var Q=y&&I===H&&H.visualViewport?H.visualViewport.width:I[te];E-=Q-r.width,E*=c?1:-1}}var h=Object.assign({position:l},d&&xs),S=m===!0?ys({x:E,y:k},Z(t)):{x:E,y:k};if(E=S.x,k=S.y,c){var f;return Object.assign({},h,(f={},f[C]=$?"0":"",f[K]=M?"0":"",f.transform=(H.devicePixelRatio||1)<=1?"translate("+E+"px, "+k+"px)":"translate3d("+E+"px, "+k+"px, 0)",f))}return Object.assign({},h,(e={},e[C]=$?k+"px":"",e[K]=M?E+"px":"",e.transform="",e))}function js(n){var e=n.state,t=n.options,r=t.gpuAcceleration,i=r===void 0?!0:r,o=t.adaptive,a=o===void 0?!0:o,l=t.roundOffsets,c=l===void 0?!0:l,d={placement:me(e.placement),variation:Ne(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:i,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,go(Object.assign({},d,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:a,roundOffsets:c})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,go(Object.assign({},d,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})}var ho={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:js,data:{}};var pr={passive:!0};function vs(n){var e=n.state,t=n.instance,r=n.options,i=r.scroll,o=i===void 0?!0:i,a=r.resize,l=a===void 0?!0:a,c=Z(e.elements.popper),d=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&d.forEach(function(m){m.addEventListener("scroll",t.update,pr)}),l&&c.addEventListener("resize",t.update,pr),function(){o&&d.forEach(function(m){m.removeEventListener("scroll",t.update,pr)}),l&&c.removeEventListener("resize",t.update,pr)}}var Ao={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:vs,data:{}};var ws={left:"right",right:"left",bottom:"top",top:"bottom"};function en(n){return n.replace(/left|right|bottom|top/g,function(e){return ws[e]})}var bs={start:"end",end:"start"};function ur(n){return n.replace(/start|end/g,function(e){return bs[e]})}function It(n){var e=Z(n),t=e.pageXOffset,r=e.pageYOffset;return{scrollLeft:t,scrollTop:r}}function qt(n){return Pe(he(n)).left+It(n).scrollLeft}function Ur(n,e){var t=Z(n),r=he(n),i=t.visualViewport,o=r.clientWidth,a=r.clientHeight,l=0,c=0;if(i){o=i.width,a=i.height;var d=An();(d||!d&&e==="fixed")&&(l=i.offsetLeft,c=i.offsetTop)}return{width:o,height:a,x:l+qt(n),y:c}}function Gr(n){var e,t=he(n),r=It(n),i=(e=n.ownerDocument)==null?void 0:e.body,o=Fe(t.scrollWidth,t.clientWidth,i?i.scrollWidth:0,i?i.clientWidth:0),a=Fe(t.scrollHeight,t.clientHeight,i?i.scrollHeight:0,i?i.clientHeight:0),l=-r.scrollLeft+qt(n),c=-r.scrollTop;return ve(i||t).direction==="rtl"&&(l+=Fe(t.clientWidth,i?i.clientWidth:0)-o),{width:o,height:a,x:l,y:c}}function Lt(n){var e=ve(n),t=e.overflow,r=e.overflowX,i=e.overflowY;return/auto|scroll|overlay|hidden/.test(t+i+r)}function fr(n){return["html","body","#document"].indexOf(de(n))>=0?n.ownerDocument.body:fe(n)&&Lt(n)?n:fr(ut(n))}function jt(n,e){var t;e===void 0&&(e=[]);var r=fr(n),i=r===((t=n.ownerDocument)==null?void 0:t.body),o=Z(r),a=i?[o].concat(o.visualViewport||[],Lt(r)?r:[]):r,l=e.concat(a);return i?l:l.concat(jt(ut(a)))}function tn(n){return Object.assign({},n,{left:n.x,top:n.y,right:n.x+n.width,bottom:n.y+n.height})}function Es(n,e){var t=Pe(n,!1,e==="fixed");return t.top=t.top+n.clientTop,t.left=t.left+n.clientLeft,t.bottom=t.top+n.clientHeight,t.right=t.left+n.clientWidth,t.width=n.clientWidth,t.height=n.clientHeight,t.x=t.left,t.y=t.top,t}function _o(n,e,t){return e===cr?tn(Ur(n,t)):De(e)?Es(e,t):tn(Gr(he(n)))}function Ts(n){var e=jt(ut(n)),t=["absolute","fixed"].indexOf(ve(n).position)>=0,r=t&&fe(n)?Ie(n):n;return De(r)?e.filter(function(i){return De(i)&&_n(i,r)&&de(i)!=="body"}):[]}function Wr(n,e,t,r){var i=e==="clippingParents"?Ts(n):[].concat(e),o=[].concat(i,[t]),a=o[0],l=o.reduce(function(c,d){var m=_o(n,d,r);return c.top=Fe(m.top,c.top),c.right=Ot(m.right,c.right),c.bottom=Ot(m.bottom,c.bottom),c.left=Fe(m.left,c.left),c},_o(n,a,r));return l.width=l.right-l.left,l.height=l.bottom-l.top,l.x=l.left,l.y=l.top,l}function vn(n){var e=n.reference,t=n.element,r=n.placement,i=r?me(r):null,o=r?Ne(r):null,a=e.x+e.width/2-t.width/2,l=e.y+e.height/2-t.height/2,c;switch(i){case ie:c={x:a,y:e.y-t.height};break;case ue:c={x:a,y:e.y+e.height};break;case ce:c={x:e.x+e.width,y:l};break;case ae:c={x:e.x-t.width,y:l};break;default:c={x:e.x,y:e.y}}var d=i?Bt(i):null;if(d!=null){var m=d==="y"?"height":"width";switch(o){case lt:c[d]=c[d]-(e[m]/2-t[m]/2);break;case Nt:c[d]=c[d]+(e[m]/2-t[m]/2);break;default:}}return c}function qe(n,e){e===void 0&&(e={});var t=e,r=t.placement,i=r===void 0?n.placement:r,o=t.strategy,a=o===void 0?n.strategy:o,l=t.boundary,c=l===void 0?so:l,d=t.rootBoundary,m=d===void 0?cr:d,y=t.elementContext,b=y===void 0?Qt:y,E=t.altBoundary,P=E===void 0?!1:E,k=t.padding,w=k===void 0?0:k,M=yn(typeof w!="number"?w:jn(w,yt)),$=b===Qt?co:Qt,K=n.rects.popper,C=n.elements[P?$:b],H=Wr(De(C)?C:C.contextElement||he(n.elements.popper),c,m,a),I=Pe(n.elements.reference),J=vn({reference:I,element:K,strategy:"absolute",placement:i}),te=tn(Object.assign({},K,J)),ne=b===Qt?te:I,Q={top:H.top-ne.top+M.top,bottom:ne.bottom-H.bottom+M.bottom,left:H.left-ne.left+M.left,right:ne.right-H.right+M.right},h=n.modifiersData.offset;if(b===Qt&&h){var S=h[i];Object.keys(Q).forEach(function(f){var Me=[ce,ue].indexOf(f)>=0?1:-1,be=[ie,ue].indexOf(f)>=0?"y":"x";Q[f]+=S[be]*Me})}return Q}function Vr(n,e){e===void 0&&(e={});var t=e,r=t.placement,i=t.boundary,o=t.rootBoundary,a=t.padding,l=t.flipVariations,c=t.allowedAutoPlacements,d=c===void 0?lr:c,m=Ne(r),y=m?l?Rr:Rr.filter(function(P){return Ne(P)===m}):yt,b=y.filter(function(P){return d.indexOf(P)>=0});b.length===0&&(b=y);var E=b.reduce(function(P,k){return P[k]=qe(n,{placement:k,boundary:i,rootBoundary:o,padding:a})[me(k)],P},{});return Object.keys(E).sort(function(P,k){return E[P]-E[k]})}function ks(n){if(me(n)===sr)return[];var e=en(n);return[ur(n),e,ur(e)]}function Ss(n){var e=n.state,t=n.options,r=n.name;if(!e.modifiersData[r]._skip){for(var i=t.mainAxis,o=i===void 0?!0:i,a=t.altAxis,l=a===void 0?!0:a,c=t.fallbackPlacements,d=t.padding,m=t.boundary,y=t.rootBoundary,b=t.altBoundary,E=t.flipVariations,P=E===void 0?!0:E,k=t.allowedAutoPlacements,w=e.options.placement,M=me(w),$=M===w,K=c||($||!P?[en(w)]:ks(w)),C=[w].concat(K).reduce(function(B,D){return B.concat(me(D)===sr?Vr(e,{placement:D,boundary:m,rootBoundary:y,padding:d,flipVariations:P,allowedAutoPlacements:k}):D)},[]),H=e.rects.reference,I=e.rects.popper,J=new Map,te=!0,ne=C[0],Q=0;Q=0,be=Me?"width":"height",Ae=qe(e,{placement:h,boundary:m,rootBoundary:y,altBoundary:b,padding:d}),_e=Me?f?ce:ae:f?ue:ie;H[be]>I[be]&&(_e=en(_e));var Ke=en(_e),Ee=[];if(o&&Ee.push(Ae[S]<=0),l&&Ee.push(Ae[_e]<=0,Ae[Ke]<=0),Ee.every(function(B){return B})){ne=h,te=!1;break}J.set(h,Ee)}if(te)for(var $t=P?3:1,Re=function(D){var q=C.find(function(ee){var et=J.get(ee);if(et)return et.slice(0,D).every(function(W){return W})});if(q)return ne=q,"break"},Ze=$t;Ze>0;Ze--){var xe=Re(Ze);if(xe==="break")break}e.placement!==ne&&(e.modifiersData[r]._skip=!0,e.placement=ne,e.reset=!0)}}var xo={name:"flip",enabled:!0,phase:"main",fn:Ss,requiresIfExists:["offset"],data:{_skip:!1}};function yo(n,e,t){return t===void 0&&(t={x:0,y:0}),{top:n.top-e.height-t.y,right:n.right-e.width+t.x,bottom:n.bottom-e.height+t.y,left:n.left-e.width-t.x}}function jo(n){return[ie,ce,ue,ae].some(function(e){return n[e]>=0})}function Cs(n){var e=n.state,t=n.name,r=e.rects.reference,i=e.rects.popper,o=e.modifiersData.preventOverflow,a=qe(e,{elementContext:"reference"}),l=qe(e,{altBoundary:!0}),c=yo(a,r),d=yo(l,i,o),m=jo(c),y=jo(d);e.modifiersData[t]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:m,hasPopperEscaped:y},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":m,"data-popper-escaped":y})}var vo={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Cs};function Ds(n,e,t){var r=me(n),i=[ae,ie].indexOf(r)>=0?-1:1,o=typeof t=="function"?t(Object.assign({},e,{placement:n})):t,a=o[0],l=o[1];return a=a||0,l=(l||0)*i,[ae,ce].indexOf(r)>=0?{x:l,y:a}:{x:a,y:l}}function Ps(n){var e=n.state,t=n.options,r=n.name,i=t.offset,o=i===void 0?[0,0]:i,a=lr.reduce(function(m,y){return m[y]=Ds(y,e.rects,o),m},{}),l=a[e.placement],c=l.x,d=l.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=c,e.modifiersData.popperOffsets.y+=d),e.modifiersData[r]=a}var wo={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Ps};function Ns(n){var e=n.state,t=n.name;e.modifiersData[t]=vn({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})}var bo={name:"popperOffsets",enabled:!0,phase:"read",fn:Ns,data:{}};function zr(n){return n==="x"?"y":"x"}function Os(n){var e=n.state,t=n.options,r=n.name,i=t.mainAxis,o=i===void 0?!0:i,a=t.altAxis,l=a===void 0?!1:a,c=t.boundary,d=t.rootBoundary,m=t.altBoundary,y=t.padding,b=t.tether,E=b===void 0?!0:b,P=t.tetherOffset,k=P===void 0?0:P,w=qe(e,{boundary:c,rootBoundary:d,padding:y,altBoundary:m}),M=me(e.placement),$=Ne(e.placement),K=!$,C=Bt(M),H=zr(C),I=e.modifiersData.popperOffsets,J=e.rects.reference,te=e.rects.popper,ne=typeof k=="function"?k(Object.assign({},e.rects,{placement:e.placement})):k,Q=typeof ne=="number"?{mainAxis:ne,altAxis:ne}:Object.assign({mainAxis:0,altAxis:0},ne),h=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,S={x:0,y:0};if(!!I){if(o){var f,Me=C==="y"?ie:ae,be=C==="y"?ue:ce,Ae=C==="y"?"height":"width",_e=I[C],Ke=_e+w[Me],Ee=_e-w[be],$t=E?-te[Ae]/2:0,Re=$===lt?J[Ae]:te[Ae],Ze=$===lt?-te[Ae]:-J[Ae],xe=e.elements.arrow,B=E&&xe?Mt(xe):{width:0,height:0},D=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:xn(),q=D[Me],ee=D[be],et=Ft(0,J[Ae],B[Ae]),W=K?J[Ae]/2-$t-et-q-Q.mainAxis:Re-et-q-Q.mainAxis,ye=K?-J[Ae]/2+$t+et+ee+Q.mainAxis:Ze+et+ee+Q.mainAxis,bt=e.elements.arrow&&Ie(e.elements.arrow),Sn=bt?C==="y"?bt.clientTop||0:bt.clientLeft||0:0,tt=(f=h==null?void 0:h[C])!=null?f:0,nt=_e+W-tt-Sn,gt=_e+ye-tt,Et=Ft(E?Ot(Ke,nt):Ke,_e,E?Fe(Ee,gt):Ee);I[C]=Et,S[C]=Et-_e}if(l){var Cn,Dn=C==="x"?ie:ae,Pn=C==="x"?ue:ce,rt=I[H],Kt=H==="y"?"height":"width",Nn=rt+w[Dn],On=rt-w[Pn],an=[ie,ae].indexOf(M)!==-1,Tt=(Cn=h==null?void 0:h[H])!=null?Cn:0,Mn=an?Nn:rt-J[Kt]-te[Kt]-Tt+Q.altAxis,Ye=an?rt+J[Kt]+te[Kt]-Tt-Q.altAxis:On,se=E&&an?fo(Mn,rt,Ye):Ft(E?Mn:Nn,rt,E?Ye:On);I[H]=se,S[H]=se-rt}e.modifiersData[r]=S}}var Eo={name:"preventOverflow",enabled:!0,phase:"main",fn:Os,requiresIfExists:["offset"]};function Jr(n){return{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}}function Qr(n){return n===Z(n)||!fe(n)?It(n):Jr(n)}function Ms(n){var e=n.getBoundingClientRect(),t=pt(e.width)/n.offsetWidth||1,r=pt(e.height)/n.offsetHeight||1;return t!==1||r!==1}function Xr(n,e,t){t===void 0&&(t=!1);var r=fe(e),i=fe(e)&&Ms(e),o=he(e),a=Pe(n,i,t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(r||!r&&!t)&&((de(e)!=="body"||Lt(o))&&(l=Qr(e)),fe(e)?(c=Pe(e,!0),c.x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=qt(o))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function Bs(n){var e=new Map,t=new Set,r=[];n.forEach(function(o){e.set(o.name,o)});function i(o){t.add(o.name);var a=[].concat(o.requires||[],o.requiresIfExists||[]);a.forEach(function(l){if(!t.has(l)){var c=e.get(l);c&&i(c)}}),r.push(o)}return n.forEach(function(o){t.has(o.name)||i(o)}),r}function Zr(n){var e=Bs(n);return lo.reduce(function(t,r){return t.concat(e.filter(function(i){return i.phase===r}))},[])}function ei(n){var e;return function(){return e||(e=new Promise(function(t){Promise.resolve().then(function(){e=void 0,t(n())})})),e}}function ti(n){var e=n.reduce(function(t,r){var i=t[r.name];return t[r.name]=i?Object.assign({},i,r,{options:Object.assign({},i.options,r.options),data:Object.assign({},i.data,r.data)}):r,t},{});return Object.keys(e).map(function(t){return e[t]})}var To={placement:"bottom",modifiers:[],strategy:"absolute"};function ko(){for(var n=arguments.length,e=new Array(n),t=0;t(n%e+e)%e,Do=class{constructor(e,t,r){this.owner=e,this.containerEl=t,t.on("click",".suggestion-item",this.onSuggestionClick.bind(this)),t.on("mousemove",".suggestion-item",this.onSuggestionMouseover.bind(this)),r.register([],"ArrowUp",i=>{if(!i.isComposing)return this.setSelectedItem(this.selectedItem-1,!0),!1}),r.register([],"ArrowDown",i=>{if(!i.isComposing)return this.setSelectedItem(this.selectedItem+1,!0),!1}),r.register([],"Enter",i=>{if(!i.isComposing)return this.useSelectedItem(i),!1})}onSuggestionClick(e,t){e.preventDefault();let r=this.suggestions.indexOf(t);this.setSelectedItem(r,!1),this.useSelectedItem(e)}onSuggestionMouseover(e,t){let r=this.suggestions.indexOf(t);this.setSelectedItem(r,!1)}setSuggestions(e){this.containerEl.empty();let t=[];e.forEach(r=>{let i=this.containerEl.createDiv("suggestion-item");this.owner.renderSuggestion(r,i),t.push(i)}),this.values=e,this.suggestions=t,this.setSelectedItem(0,!1)}useSelectedItem(e){let t=this.values[this.selectedItem];t&&this.owner.selectSuggestion(t,e)}setSelectedItem(e,t){let r=Is(e,this.suggestions.length),i=this.suggestions[this.selectedItem],o=this.suggestions[r];i?.removeClass("is-selected"),o?.addClass("is-selected"),this.selectedItem=r,t&&o.scrollIntoView(!1)}},wn=class{constructor(e,t){this.app=e,this.inputEl=t,this.scope=new Co.Scope,this.suggestEl=createDiv("suggestion-container");let r=this.suggestEl.createDiv("suggestion");this.suggest=new Do(this,r,this.scope),this.scope.register([],"Escape",this.close.bind(this)),this.inputEl.addEventListener("input",this.onInputChanged.bind(this)),this.inputEl.addEventListener("focus",this.onInputChanged.bind(this)),this.inputEl.addEventListener("blur",this.close.bind(this)),this.suggestEl.on("mousedown",".suggestion-container",i=>{i.preventDefault()})}onInputChanged(){let e=this.inputEl.value,t=this.getSuggestions(e);if(!t){this.close();return}t.length>0?(this.suggest.setSuggestions(t),this.open(this.app.dom.appContainerEl,this.inputEl)):this.close()}open(e,t){this.app.keymap.pushScope(this.scope),e.appendChild(this.suggestEl),this.popper=ni(t,this.suggestEl,{placement:"bottom-start",modifiers:[{name:"sameWidth",enabled:!0,fn:({state:r,instance:i})=>{let o=`${r.rects.reference.width}px`;r.styles.popper.width!==o&&(r.styles.popper.width=o,i.update())},phase:"beforeWrite",requires:["computeStyles"]}]})}close(){this.app.keymap.popScope(this.scope),this.suggest.setSuggestions([]),this.popper&&this.popper.destroy(),this.suggestEl.detach()}};var Le;(function(t){t[t.TemplateFiles=0]="TemplateFiles",t[t.ScriptFiles=1]="ScriptFiles"})(Le||(Le={}));var nn=class extends wn{constructor(e,t,r){super(t.app,e);this.inputEl=e;this.plugin=t;this.mode=r}get_folder(e){switch(e){case 0:return this.plugin.settings.templates_folder;case 1:return this.plugin.settings.user_scripts_folder}}get_error_msg(e){switch(e){case 0:return"Templates folder doesn't exist";case 1:return"User Scripts folder doesn't exist"}}getSuggestions(e){let t=ke(()=>ze(this.plugin.app,this.get_folder(this.mode)),this.get_error_msg(this.mode));if(!t)return[];let r=[],i=e.toLowerCase();return t.forEach(o=>{o instanceof Po.TFile&&o.extension==="md"&&o.path.toLowerCase().contains(i)&&r.push(o)}),r.slice(0,1e3)}renderSuggestion(e,t){t.setText(e.path)}selectSuggestion(e){this.inputEl.value=e.path,this.inputEl.trigger("input"),this.close()}};var No=X(require("obsidian"));var bn=class extends wn{constructor(e,t){super(e,t)}getSuggestions(e){let t=this.app.vault.getAllLoadedFiles(),r=[],i=e.toLowerCase();return t.forEach(o=>{o instanceof No.TFolder&&o.path.toLowerCase().contains(i)&&r.push(o)}),r.slice(0,1e3)}renderSuggestion(e,t){t.setText(e.path)}selectSuggestion(e){this.inputEl.value=e.path,this.inputEl.trigger("input"),this.close()}};var ft;(function(o){o[o.Off=0]="Off",o[o.RenderDescriptionParameterReturn=1]="RenderDescriptionParameterReturn",o[o.RenderDescriptionParameterList=2]="RenderDescriptionParameterList",o[o.RenderDescriptionReturn=3]="RenderDescriptionReturn",o[o.RenderDescriptionOnly=4]="RenderDescriptionOnly"})(ft||(ft={}));function Oo(n){return isBoolean(n)?n:[1,3].includes(n)}function Mo(n){return isBoolean(n)?n:[1,2].includes(n)}function Bo(n){return isBoolean(n)?n:n!=0}var Fo={command_timeout:5,templates_folder:"",templates_pairs:[["",""]],trigger_on_file_creation:!1,auto_jump_to_cursor:!1,enable_system_commands:!1,shell_path:"",user_scripts_folder:"",enable_folder_templates:!0,folder_templates:[{folder:"",template:""}],enable_file_templates:!1,file_templates:[{regex:".*",template:""}],syntax_highlighting:!0,syntax_highlighting_mobile:!1,enabled_templates_hotkeys:[""],startup_templates:[""],intellisense_render:ft.RenderDescriptionParameterReturn},ri=class extends L.PluginSettingTab{constructor(e){super(e.app,e);this.plugin=e}display(){this.containerEl.empty(),this.add_template_folder_setting(),this.add_internal_functions_setting(),this.add_syntax_highlighting_settings(),this.add_auto_jump_to_cursor(),this.add_trigger_on_new_file_creation_setting(),this.plugin.settings.trigger_on_file_creation&&(this.add_folder_templates_setting(),this.add_file_templates_setting()),this.add_templates_hotkeys_setting(),this.add_startup_templates_setting(),this.add_user_script_functions_setting(),this.add_user_system_command_functions_setting(),this.add_donating_setting()}add_template_folder_setting(){new L.Setting(this.containerEl).setName("Template folder location").setDesc("Files in this folder will be available as templates.").addSearch(e=>{new bn(this.app,e.inputEl),e.setPlaceholder("Example: folder1/folder2").setValue(this.plugin.settings.templates_folder).onChange(t=>{t=t.trim(),t=t.replace(/\/$/,""),this.plugin.settings.templates_folder=t,this.plugin.save_settings()}),e.containerEl.addClass("templater_search")})}add_internal_functions_setting(){let e=document.createDocumentFragment();e.append("Templater provides multiples predefined variables / functions that you can use.",e.createEl("br"),"Check the ",e.createEl("a",{href:"https://silentvoid13.github.io/Templater/",text:"documentation"})," to get a list of all the available internal variables / functions."),new L.Setting(this.containerEl).setName("Internal variables and functions").setDesc(e)}add_syntax_highlighting_settings(){let e=document.createDocumentFragment();e.append("Adds syntax highlighting for Templater commands in edit mode.");let t=document.createDocumentFragment();t.append("Adds syntax highlighting for Templater commands in edit mode on mobile. Use with caution: this may break live preview on mobile platforms."),new L.Setting(this.containerEl).setName("Syntax highlighting on desktop").setDesc(e).addToggle(r=>{r.setValue(this.plugin.settings.syntax_highlighting).onChange(i=>{this.plugin.settings.syntax_highlighting=i,this.plugin.save_settings(),this.plugin.event_handler.update_syntax_highlighting()})}),new L.Setting(this.containerEl).setName("Syntax highlighting on mobile").setDesc(t).addToggle(r=>{r.setValue(this.plugin.settings.syntax_highlighting_mobile).onChange(i=>{this.plugin.settings.syntax_highlighting_mobile=i,this.plugin.save_settings(),this.plugin.event_handler.update_syntax_highlighting()})})}add_auto_jump_to_cursor(){let e=document.createDocumentFragment();e.append("Automatically triggers ",e.createEl("code",{text:"tp.file.cursor"})," after inserting a template.",e.createEl("br"),"You can also set a hotkey to manually trigger ",e.createEl("code",{text:"tp.file.cursor"}),"."),new L.Setting(this.containerEl).setName("Automatic jump to cursor").setDesc(e).addToggle(t=>{t.setValue(this.plugin.settings.auto_jump_to_cursor).onChange(r=>{this.plugin.settings.auto_jump_to_cursor=r,this.plugin.save_settings()})})}add_trigger_on_new_file_creation_setting(){let e=document.createDocumentFragment();e.append("Templater will listen for the new file creation event, and, if it matches a rule you've set, replace every command it finds in the new file's content. ","This makes Templater compatible with other plugins like the Daily note core plugin, Calendar plugin, Review plugin, Note refactor plugin, etc. ",e.createEl("br"),e.createEl("br"),"Make sure to set up rules under either folder templates or file regex template below.",e.createEl("br"),e.createEl("br"),e.createEl("b",{text:"Warning: "}),"This can be dangerous if you create new files with unknown / unsafe content on creation. Make sure that every new file's content is safe on creation."),new L.Setting(this.containerEl).setName("Trigger Templater on new file creation").setDesc(e).addToggle(t=>{t.setValue(this.plugin.settings.trigger_on_file_creation).onChange(r=>{this.plugin.settings.trigger_on_file_creation=r,this.plugin.save_settings(),this.plugin.event_handler.update_trigger_file_on_creation(),this.display()})})}add_templates_hotkeys_setting(){new L.Setting(this.containerEl).setName("Template hotkeys").setHeading();let e=document.createDocumentFragment();e.append("Template hotkeys allows you to bind a template to a hotkey."),new L.Setting(this.containerEl).setDesc(e),this.plugin.settings.enabled_templates_hotkeys.forEach((t,r)=>{new L.Setting(this.containerEl).addSearch(o=>{new nn(o.inputEl,this.plugin,Le.TemplateFiles),o.setPlaceholder("Example: folder1/template_file").setValue(t).onChange(a=>{if(a&&this.plugin.settings.enabled_templates_hotkeys.contains(a)){oe(new O("This template is already bound to a hotkey"));return}this.plugin.command_handler.add_template_hotkey(this.plugin.settings.enabled_templates_hotkeys[r],a),this.plugin.settings.enabled_templates_hotkeys[r]=a,this.plugin.save_settings()}),o.containerEl.addClass("templater_search")}).addExtraButton(o=>{o.setIcon("any-key").setTooltip("Configure Hotkey").onClick(()=>{this.app.setting.openTabById("hotkeys");let a=this.app.setting.activeTab;a.searchComponent.inputEl.value=t,a.updateHotkeyVisibility()})}).addExtraButton(o=>{o.setIcon("up-chevron-glyph").setTooltip("Move up").onClick(()=>{Pt(this.plugin.settings.enabled_templates_hotkeys,r,r-1),this.plugin.save_settings(),this.display()})}).addExtraButton(o=>{o.setIcon("down-chevron-glyph").setTooltip("Move down").onClick(()=>{Pt(this.plugin.settings.enabled_templates_hotkeys,r,r+1),this.plugin.save_settings(),this.display()})}).addExtraButton(o=>{o.setIcon("cross").setTooltip("Delete").onClick(()=>{this.plugin.command_handler.remove_template_hotkey(this.plugin.settings.enabled_templates_hotkeys[r]),this.plugin.settings.enabled_templates_hotkeys.splice(r,1),this.plugin.save_settings(),this.display()})}).infoEl.remove()}),new L.Setting(this.containerEl).addButton(t=>{t.setButtonText("Add new hotkey for template").setCta().onClick(()=>{this.plugin.settings.enabled_templates_hotkeys.push(""),this.plugin.save_settings(),this.display()})})}add_folder_templates_setting(){new L.Setting(this.containerEl).setName("Folder templates").setHeading();let e=document.createDocumentFragment();e.append("Folder templates are triggered when a new ",e.createEl("strong",{text:"empty "}),"file is created in a given folder.",e.createEl("br"),"Templater will fill the empty file with the specified template.",e.createEl("br"),"The deepest match is used. A global default template would be defined on the root ",e.createEl("code",{text:"/"}),"."),new L.Setting(this.containerEl).setDesc(e);let t=document.createDocumentFragment();t.append("When enabled, Templater will make use of the folder templates defined below. This option is mutually exclusive with file regex templates below, so enabling one will disable the other."),new L.Setting(this.containerEl).setName("Enable folder templates").setDesc(t).addToggle(r=>{r.setValue(this.plugin.settings.enable_folder_templates).onChange(i=>{this.plugin.settings.enable_folder_templates=i,i&&(this.plugin.settings.enable_file_templates=!1),this.plugin.save_settings(),this.display()})}),!!this.plugin.settings.enable_folder_templates&&(this.plugin.settings.folder_templates.forEach((r,i)=>{new L.Setting(this.containerEl).addSearch(a=>{new bn(this.app,a.inputEl),a.setPlaceholder("Folder").setValue(r.folder).onChange(l=>{if(l&&this.plugin.settings.folder_templates.some(c=>c.folder==l)){oe(new O("This folder already has a template associated with it"));return}this.plugin.settings.folder_templates[i].folder=l,this.plugin.save_settings()}),a.containerEl.addClass("templater_search")}).addSearch(a=>{new nn(a.inputEl,this.plugin,Le.TemplateFiles),a.setPlaceholder("Template").setValue(r.template).onChange(l=>{this.plugin.settings.folder_templates[i].template=l,this.plugin.save_settings()}),a.containerEl.addClass("templater_search")}).addExtraButton(a=>{a.setIcon("up-chevron-glyph").setTooltip("Move up").onClick(()=>{Pt(this.plugin.settings.folder_templates,i,i-1),this.plugin.save_settings(),this.display()})}).addExtraButton(a=>{a.setIcon("down-chevron-glyph").setTooltip("Move down").onClick(()=>{Pt(this.plugin.settings.folder_templates,i,i+1),this.plugin.save_settings(),this.display()})}).addExtraButton(a=>{a.setIcon("cross").setTooltip("Delete").onClick(()=>{this.plugin.settings.folder_templates.splice(i,1),this.plugin.save_settings(),this.display()})}).infoEl.remove()}),new L.Setting(this.containerEl).addButton(r=>{r.setButtonText("Add new folder template").setTooltip("Add additional folder template").setCta().onClick(()=>{this.plugin.settings.folder_templates.push({folder:"",template:""}),this.plugin.save_settings(),this.display()})}))}add_file_templates_setting(){new L.Setting(this.containerEl).setName("File regex templates").setHeading();let e=document.createDocumentFragment();e.append("File regex templates are triggered when a new ",e.createEl("strong",{text:"empty"})," file is created that matches one of them. Templater will fill the empty file with the specified template.",e.createEl("br"),"The first match from the top is used, so the order of the rules is important.",e.createEl("br"),"Use ",e.createEl("code",{text:".*"})," as a final catch-all, if you need it."),new L.Setting(this.containerEl).setDesc(e);let t=document.createDocumentFragment();t.append("When enabled, Templater will make use of the file regex templates defined below. This option is mutually exclusive with folder templates above, so enabling one will disable the other."),new L.Setting(this.containerEl).setName("Enable file regex templates").setDesc(t).addToggle(r=>{r.setValue(this.plugin.settings.enable_file_templates).onChange(i=>{this.plugin.settings.enable_file_templates=i,i&&(this.plugin.settings.enable_folder_templates=!1),this.plugin.save_settings(),this.display()})}),!!this.plugin.settings.enable_file_templates&&(this.plugin.settings.file_templates.forEach((r,i)=>{new L.Setting(this.containerEl).addText(a=>{a.setPlaceholder("File regex").setValue(r.regex).onChange(l=>{this.plugin.settings.file_templates[i].regex=l,this.plugin.save_settings()}),a.inputEl.addClass("templater_search")}).addSearch(a=>{new nn(a.inputEl,this.plugin,Le.TemplateFiles),a.setPlaceholder("Template").setValue(r.template).onChange(l=>{this.plugin.settings.file_templates[i].template=l,this.plugin.save_settings()}),a.containerEl.addClass("templater_search")}).addExtraButton(a=>{a.setIcon("up-chevron-glyph").setTooltip("Move up").onClick(()=>{Pt(this.plugin.settings.file_templates,i,i-1),this.plugin.save_settings(),this.display()})}).addExtraButton(a=>{a.setIcon("down-chevron-glyph").setTooltip("Move down").onClick(()=>{Pt(this.plugin.settings.file_templates,i,i+1),this.plugin.save_settings(),this.display()})}).addExtraButton(a=>{a.setIcon("cross").setTooltip("Delete").onClick(()=>{this.plugin.settings.file_templates.splice(i,1),this.plugin.save_settings(),this.display()})}).infoEl.remove()}),new L.Setting(this.containerEl).addButton(r=>{r.setButtonText("Add new file regex").setTooltip("Add additional file regex").setCta().onClick(()=>{this.plugin.settings.file_templates.push({regex:"",template:""}),this.plugin.save_settings(),this.display()})}))}add_startup_templates_setting(){new L.Setting(this.containerEl).setName("Startup templates").setHeading();let e=document.createDocumentFragment();e.append("Startup templates are templates that will get executed once when Templater starts.",e.createEl("br"),"These templates won't output anything.",e.createEl("br"),"This can be useful to set up templates adding hooks to Obsidian events for example."),new L.Setting(this.containerEl).setDesc(e),this.plugin.settings.startup_templates.forEach((t,r)=>{new L.Setting(this.containerEl).addSearch(o=>{new nn(o.inputEl,this.plugin,Le.TemplateFiles),o.setPlaceholder("Example: folder1/template_file").setValue(t).onChange(a=>{if(a&&this.plugin.settings.startup_templates.contains(a)){oe(new O("This startup template already exist"));return}this.plugin.settings.startup_templates[r]=a,this.plugin.save_settings()}),o.containerEl.addClass("templater_search")}).addExtraButton(o=>{o.setIcon("cross").setTooltip("Delete").onClick(()=>{this.plugin.settings.startup_templates.splice(r,1),this.plugin.save_settings(),this.display()})}).infoEl.remove()}),new L.Setting(this.containerEl).addButton(t=>{t.setButtonText("Add new startup template").setCta().onClick(()=>{this.plugin.settings.startup_templates.push(""),this.plugin.save_settings(),this.display()})})}add_user_script_functions_setting(){new L.Setting(this.containerEl).setName("User script functions").setHeading();let e=document.createDocumentFragment();e.append("All JavaScript files in this folder will be loaded as CommonJS modules, to import custom user functions.",e.createEl("br"),"The folder needs to be accessible from the vault.",e.createEl("br"),"Check the ",e.createEl("a",{href:"https://silentvoid13.github.io/Templater/",text:"documentation"})," for more information."),new L.Setting(this.containerEl).setName("Script files folder location").setDesc(e).addSearch(r=>{new bn(this.app,r.inputEl),r.setPlaceholder("Example: folder1/folder2").setValue(this.plugin.settings.user_scripts_folder).onChange(i=>{this.plugin.settings.user_scripts_folder=i,this.plugin.save_settings()}),r.containerEl.addClass("templater_search")}),new L.Setting(this.containerEl).setName("User script intellisense").setDesc("Determine how you'd like to have user script intellisense render. Note values will not render if not in the script.").addDropdown(r=>{r.addOption("0","Turn off intellisense").addOption("1","Render method description, parameters list, and return").addOption("2","Render method description and parameters list").addOption("3","Render method description and return").addOption("4","Render method description").setValue(this.plugin.settings.intellisense_render.toString()).onChange(i=>{this.plugin.settings.intellisense_render=parseInt(i),this.plugin.save_settings()})}),e=document.createDocumentFragment();let t;if(!this.plugin.settings.user_scripts_folder)t="No user scripts folder set";else{let r=ke(()=>ze(this.app,this.plugin.settings.user_scripts_folder),"User scripts folder doesn't exist");if(!r||r.length===0)t="No user scripts detected";else{let i=0;for(let o of r)o.extension==="js"&&(i++,e.append(e.createEl("li",{text:`tp.user.${o.basename}`})));t=`Detected ${i} User Script(s)`}}new L.Setting(this.containerEl).setName(t).setDesc(e).addExtraButton(r=>{r.setIcon("sync").setTooltip("Refresh").onClick(()=>{this.display()})})}add_user_system_command_functions_setting(){let e=document.createDocumentFragment();if(e.append("Allows you to create user functions linked to system commands.",e.createEl("br"),e.createEl("b",{text:"Warning: "}),"It can be dangerous to execute arbitrary system commands from untrusted sources. Only run system commands that you understand, from trusted sources."),new L.Setting(this.containerEl).setName("User system command functions").setHeading(),new L.Setting(this.containerEl).setName("Enable user system command functions").setDesc(e).addToggle(t=>{t.setValue(this.plugin.settings.enable_system_commands).onChange(r=>{this.plugin.settings.enable_system_commands=r,this.plugin.save_settings(),this.display()})}),this.plugin.settings.enable_system_commands){new L.Setting(this.containerEl).setName("Timeout").setDesc("Maximum timeout in seconds for a system command.").addText(o=>{o.setPlaceholder("Timeout").setValue(this.plugin.settings.command_timeout.toString()).onChange(a=>{let l=Number(a);if(isNaN(l)){oe(new O("Timeout must be a number"));return}this.plugin.settings.command_timeout=l,this.plugin.save_settings()})}),e=document.createDocumentFragment(),e.append("Full path to the shell binary to execute the command with.",e.createEl("br"),"This setting is optional and will default to the system's default shell if not specified.",e.createEl("br"),"You can use forward slashes ('/') as path separators on all platforms if in doubt."),new L.Setting(this.containerEl).setName("Shell binary location").setDesc(e).addText(o=>{o.setPlaceholder("Example: /bin/bash, ...").setValue(this.plugin.settings.shell_path).onChange(a=>{this.plugin.settings.shell_path=a,this.plugin.save_settings()})});let t=1;this.plugin.settings.templates_pairs.forEach(o=>{let a=this.containerEl.createEl("div");a.addClass("templater_div");let l=this.containerEl.createEl("h4",{text:"User function n\xB0"+t});l.addClass("templater_title"),new L.Setting(this.containerEl).addExtraButton(d=>{d.setIcon("cross").setTooltip("Delete").onClick(()=>{let m=this.plugin.settings.templates_pairs.indexOf(o);m>-1&&(this.plugin.settings.templates_pairs.splice(m,1),this.plugin.save_settings(),this.display())})}).addText(d=>{let m=d.setPlaceholder("Function name").setValue(o[0]).onChange(y=>{let b=this.plugin.settings.templates_pairs.indexOf(o);b>-1&&(this.plugin.settings.templates_pairs[b][0]=y,this.plugin.save_settings())});return m.inputEl.addClass("templater_template"),m}).addTextArea(d=>{let m=d.setPlaceholder("System command").setValue(o[1]).onChange(y=>{let b=this.plugin.settings.templates_pairs.indexOf(o);b>-1&&(this.plugin.settings.templates_pairs[b][1]=y,this.plugin.save_settings())});return m.inputEl.setAttr("rows",2),m.inputEl.addClass("templater_cmd"),m}).infoEl.remove(),a.appendChild(l),a.appendChild(this.containerEl.lastChild),t+=1});let r=this.containerEl.createEl("div");r.addClass("templater_div2"),new L.Setting(this.containerEl).addButton(o=>{o.setButtonText("Add new user function").setCta().onClick(()=>{this.plugin.settings.templates_pairs.push(["",""]),this.plugin.save_settings(),this.display()})}).infoEl.remove(),r.appendChild(this.containerEl.lastChild)}}add_donating_setting(){let e=new L.Setting(this.containerEl).setName("Donate").setDesc("If you like this Plugin, consider donating to support continued development."),t=document.createElement("a");t.setAttribute("href","https://github.com/sponsors/silentvoid13"),t.addClass("templater_donating");let r=document.createElement("img");r.src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86",t.appendChild(r);let i=document.createElement("a");i.setAttribute("href","https://www.paypal.com/donate?hosted_button_id=U2SRGAFYXT32Q"),i.addClass("templater_donating");let o=document.createElement("img");o.src="https://img.shields.io/badge/paypal-silentvoid13-yellow?style=social&logo=paypal",i.appendChild(o),e.settingEl.appendChild(t),e.settingEl.appendChild(i)}};var dr=X(require("obsidian"));var rn;(function(t){t[t.InsertTemplate=0]="InsertTemplate",t[t.CreateNoteTemplate=1]="CreateNoteTemplate"})(rn||(rn={}));var ii=class extends dr.FuzzySuggestModal{constructor(e){super(e.app);this.plugin=e,this.setPlaceholder("Type name of a template...")}getItems(){if(!this.plugin.settings.templates_folder)return this.app.vault.getMarkdownFiles();let e=ke(()=>ze(this.plugin.app,this.plugin.settings.templates_folder),`Couldn't retrieve template files from templates folder ${this.plugin.settings.templates_folder}`);return e||[]}getItemText(e){let t=e.path;if(e.path.startsWith(this.plugin.settings.templates_folder)&&(0,dr.normalizePath)(this.plugin.settings.templates_folder)!="/"){let r=this.plugin.settings.templates_folder.length,i=this.plugin.settings.templates_folder.endsWith("/")?r:r+1;t=e.path.slice(i)}return t.split(".").slice(0,-1).join(".")}onChooseItem(e){switch(this.open_mode){case 0:this.plugin.templater.append_template_to_active_file(e);break;case 1:this.plugin.templater.create_new_note_from_template(e,this.creation_folder);break}}start(){try{this.open()}catch(e){oe(e)}}insert_template(){this.open_mode=0,this.start()}create_new_note_from_template(e){this.creation_folder=e,this.open_mode=1,this.start()}};var Io="Error_MobileUnsupportedTemplate",qo='';var Oe=X(require("obsidian"));var dt=X(require("obsidian"));var we=class{constructor(e){this.plugin=e;this.static_functions=new Map;this.dynamic_functions=new Map}getName(){return this.name}async init(){await this.create_static_templates(),this.static_object=Object.fromEntries(this.static_functions)}async generate_object(e){return this.config=e,await this.create_dynamic_templates(),{...this.static_object,...Object.fromEntries(this.dynamic_functions)}}};var oi=class extends we{constructor(){super(...arguments);this.name="date"}async create_static_templates(){this.static_functions.set("now",this.generate_now()),this.static_functions.set("tomorrow",this.generate_tomorrow()),this.static_functions.set("weekday",this.generate_weekday()),this.static_functions.set("yesterday",this.generate_yesterday())}async create_dynamic_templates(){}async teardown(){}generate_now(){return(e="YYYY-MM-DD",t,r,i)=>{if(r&&!(0,dt.moment)(r,i).isValid())throw new O("Invalid reference date format, try specifying one with the argument 'reference_format'");let o;return typeof t=="string"?o=dt.moment.duration(t):typeof t=="number"&&(o=dt.moment.duration(t,"days")),(0,dt.moment)(r,i).add(o).format(e)}}generate_tomorrow(){return(e="YYYY-MM-DD")=>(0,dt.moment)().add(1,"days").format(e)}generate_weekday(){return(e="YYYY-MM-DD",t,r,i)=>{if(r&&!(0,dt.moment)(r,i).isValid())throw new O("Invalid reference date format, try specifying one with the argument 'reference_format'");return(0,dt.moment)(r,i).weekday(t).format(e)}}generate_yesterday(){return(e="YYYY-MM-DD")=>(0,dt.moment)().add(-1,"days").format(e)}};var le=X(require("obsidian"));var Lo=10,ai=class extends we{constructor(){super(...arguments);this.name="file";this.include_depth=0;this.create_new_depth=0;this.linkpath_regex=new RegExp("^\\[\\[(.*)\\]\\]$")}async create_static_templates(){this.static_functions.set("creation_date",this.generate_creation_date()),this.static_functions.set("create_new",this.generate_create_new()),this.static_functions.set("cursor",this.generate_cursor()),this.static_functions.set("cursor_append",this.generate_cursor_append()),this.static_functions.set("exists",this.generate_exists()),this.static_functions.set("find_tfile",this.generate_find_tfile()),this.static_functions.set("folder",this.generate_folder()),this.static_functions.set("include",this.generate_include()),this.static_functions.set("last_modified_date",this.generate_last_modified_date()),this.static_functions.set("move",this.generate_move()),this.static_functions.set("path",this.generate_path()),this.static_functions.set("rename",this.generate_rename()),this.static_functions.set("selection",this.generate_selection())}async create_dynamic_templates(){this.dynamic_functions.set("content",await this.generate_content()),this.dynamic_functions.set("tags",this.generate_tags()),this.dynamic_functions.set("title",this.generate_title())}async teardown(){}async generate_content(){return await this.plugin.app.vault.read(this.config.target_file)}generate_create_new(){return async(e,t,r=!1,i)=>{if(this.create_new_depth+=1,this.create_new_depth>Lo)throw this.create_new_depth=0,new O("Reached create_new depth limit (max = 10)");let o=await this.plugin.templater.create_new_note_from_template(e,i,t,r);return this.create_new_depth-=1,o}}generate_creation_date(){return(e="YYYY-MM-DD HH:mm")=>(0,le.moment)(this.config.target_file.stat.ctime).format(e)}generate_cursor(){return e=>`<% tp.file.cursor(${e??""}) %>`}generate_cursor_append(){return e=>{let t=this.plugin.app.workspace.activeEditor;if(!t||!t.editor){oe(new O("No active editor, can't append to cursor."));return}return t.editor.getDoc().replaceSelection(e),""}}generate_exists(){return async e=>{let t=(0,le.normalizePath)(e);return await this.plugin.app.vault.exists(t)}}generate_find_tfile(){return e=>{let t=(0,le.normalizePath)(e);return this.plugin.app.metadataCache.getFirstLinkpathDest(t,"")}}generate_folder(){return(e=!1)=>{let t=this.config.target_file.parent,r;return e?r=t.path:r=t.name,r}}generate_include(){return async e=>{if(this.include_depth+=1,this.include_depth>Lo)throw this.include_depth-=1,new O("Reached inclusion depth limit (max = 10)");let t;if(e instanceof le.TFile)t=await this.plugin.app.vault.read(e);else{let r;if((r=this.linkpath_regex.exec(e))===null)throw this.include_depth-=1,new O("Invalid file format, provide an obsidian link between quotes.");let{path:i,subpath:o}=(0,le.parseLinktext)(r[1]),a=this.plugin.app.metadataCache.getFirstLinkpathDest(i,"");if(!a)throw this.include_depth-=1,new O(`File ${e} doesn't exist`);if(t=await this.plugin.app.vault.read(a),o){let l=this.plugin.app.metadataCache.getFileCache(a);if(l){let c=(0,le.resolveSubpath)(l,o);c&&(t=t.slice(c.start.offset,c.end?.offset))}}}try{let r=await this.plugin.templater.parser.parse_commands(t,this.plugin.templater.current_functions_object);return this.include_depth-=1,r}catch(r){throw this.include_depth-=1,r}}}generate_last_modified_date(){return(e="YYYY-MM-DD HH:mm")=>(0,le.moment)(this.config.target_file.stat.mtime).format(e)}generate_move(){return async(e,t)=>{let r=t||this.config.target_file,i=(0,le.normalizePath)(`${e}.${r.extension}`),o=i.replace(/\\/g,"/").split("/");if(o.pop(),o.length){let a=o.join("/");this.plugin.app.vault.getAbstractFileByPath(a)||await this.plugin.app.vault.createFolder(a)}return await this.plugin.app.fileManager.renameFile(r,i),""}}generate_path(){return(e=!1)=>{let t="";if(le.Platform.isMobile){let r=this.plugin.app.vault.adapter.fs.uri,i=this.plugin.app.vault.adapter.basePath;t=`${r}/${i}`}else if(this.plugin.app.vault.adapter instanceof le.FileSystemAdapter)t=this.plugin.app.vault.adapter.getBasePath();else throw new O("app.vault is not a FileSystemAdapter instance");return e?this.config.target_file.path:`${t}/${this.config.target_file.path}`}}generate_rename(){return async e=>{if(e.match(/[\\/:]+/g))throw new O("File name cannot contain any of these characters: \\ / :");let t=(0,le.normalizePath)(`${this.config.target_file.parent.path}/${e}.${this.config.target_file.extension}`);return await this.plugin.app.fileManager.renameFile(this.config.target_file,t),""}}generate_selection(){return()=>{let e=this.plugin.app.workspace.activeEditor;if(!e||!e.editor)throw new O("Active editor is null, can't read selection.");return e.editor.getSelection()}}generate_tags(){let e=this.plugin.app.metadataCache.getFileCache(this.config.target_file);return e?(0,le.getAllTags)(e):null}generate_title(){return this.config.target_file.basename}};var Ho=X(require("obsidian"));var si=class extends we{constructor(){super(...arguments);this.name="web"}async create_static_templates(){this.static_functions.set("daily_quote",this.generate_daily_quote()),this.static_functions.set("request",this.generate_request()),this.static_functions.set("random_picture",this.generate_random_picture())}async create_dynamic_templates(){}async teardown(){}async getRequest(e){try{let t=await(0,Ho.requestUrl)(e);if(t.status<200&&t.status>=300)throw new O("Error performing GET request");return t}catch{throw new O("Error performing GET request")}}generate_daily_quote(){return async()=>{try{let t=(await this.getRequest("https://raw.githubusercontent.com/Zachatoo/quotes-database/refs/heads/main/quotes.json")).json,r=t[Math.floor(Math.random()*t.length)],{quote:i,author:o}=r;return`> [!quote] ${i} > \u2014 ${o}`}catch{return new O("Error generating daily quote"),"Error generating daily quote"}}}generate_random_picture(){return async(e,t,r=!1)=>{try{let i=await this.getRequest(`https://templater-unsplash-2.fly.dev/${t?"?q="+t:""}`).then(a=>a.json),o=i.full;if(e&&!r)if(e.includes("x")){let[a,l]=e.split("x");o=o.concat(`&w=${a}&h=${l}`)}else o=o.concat(`&w=${e}`);return r?`![photo by ${i.photog}(${i.photogUrl}) on Unsplash|${e}](${o})`:`![photo by ${i.photog}(${i.photogUrl}) on Unsplash](${o})`}catch{return new O("Error generating random picture"),"Error generating random picture"}}}generate_request(){return async(e,t)=>{try{let i=await(await this.getRequest(e)).json;return t&&i?t.split(".").reduce((o,a)=>{if(o&&o.hasOwnProperty(a))return o[a];throw new Error(`Path ${t} not found in the JSON response`)},i):i}catch(r){throw console.error(r),new O("Error fetching and extracting value")}}}};var ci=class extends we{constructor(){super(...arguments);this.name="hooks";this.event_refs=[]}async create_static_templates(){this.static_functions.set("on_all_templates_executed",this.generate_on_all_templates_executed())}async create_dynamic_templates(){}async teardown(){this.event_refs.forEach(e=>{e.e.offref(e)}),this.event_refs=[]}generate_on_all_templates_executed(){return e=>{let t=this.plugin.app.workspace.on("templater:all-templates-executed",async()=>{await ar(1),e()});t&&this.event_refs.push(t)}}};var li=class extends we{constructor(){super(...arguments);this.name="frontmatter"}async create_static_templates(){}async create_dynamic_templates(){let e=this.plugin.app.metadataCache.getFileCache(this.config.target_file);this.dynamic_functions=new Map(Object.entries(e?.frontmatter||{}))}async teardown(){}};var Je=X(require("obsidian"));var pi=class extends Je.Modal{constructor(e,t,r,i){super(e);this.prompt_text=t;this.default_value=r;this.multi_line=i;this.submitted=!1}onOpen(){this.titleEl.setText(this.prompt_text),this.createForm()}onClose(){this.contentEl.empty(),this.submitted||this.reject(new O("Cancelled prompt"))}createForm(){let e=this.contentEl.createDiv();e.addClass("templater-prompt-div");let t;if(this.multi_line){t=new Je.TextAreaComponent(e);let r=this.contentEl.createDiv();r.addClass("templater-button-div");let i=new Je.ButtonComponent(r);i.buttonEl.addClass("mod-cta"),i.setButtonText("Submit").onClick(o=>{this.resolveAndClose(o)})}else t=new Je.TextComponent(e);this.value=this.default_value??"",t.inputEl.addClass("templater-prompt-input"),t.setPlaceholder("Type text here"),t.setValue(this.value),t.onChange(r=>this.value=r),t.inputEl.focus(),t.inputEl.addEventListener("keydown",r=>this.enterCallback(r))}enterCallback(e){e.isComposing||e.keyCode===229||(this.multi_line?Je.Platform.isDesktop&&e.key==="Enter"&&!e.shiftKey&&this.resolveAndClose(e):e.key==="Enter"&&this.resolveAndClose(e))}resolveAndClose(e){this.submitted=!0,e.preventDefault(),this.resolve(this.value),this.close()}async openAndGetValue(e,t){this.resolve=e,this.reject=t,this.open()}};var $o=X(require("obsidian")),ui=class extends $o.FuzzySuggestModal{constructor(e,t,r,i,o){super(e);this.text_items=t;this.items=r;this.submitted=!1;this.setPlaceholder(i),o&&(this.limit=o)}getItems(){return this.items}onClose(){this.submitted||this.reject(new O("Cancelled prompt"))}selectSuggestion(e,t){this.submitted=!0,this.close(),this.onChooseSuggestion(e,t)}getItemText(e){return this.text_items instanceof Function?this.text_items(e):this.text_items[this.items.indexOf(e)]||"Undefined Text Item"}onChooseItem(e){this.resolve(e)}async openAndGetValue(e,t){this.resolve=e,this.reject=t,this.open()}};var fi=class extends we{constructor(){super(...arguments);this.name="system"}async create_static_templates(){this.static_functions.set("clipboard",this.generate_clipboard()),this.static_functions.set("prompt",this.generate_prompt()),this.static_functions.set("suggester",this.generate_suggester())}async create_dynamic_templates(){}async teardown(){}generate_clipboard(){return async()=>await navigator.clipboard.readText()}generate_prompt(){return async(e,t,r=!1,i=!1)=>{let o=new pi(this.plugin.app,e,t,i),a=new Promise((l,c)=>o.openAndGetValue(l,c));try{return await a}catch(l){if(r)throw l;return null}}}generate_suggester(){return async(e,t,r=!1,i="",o)=>{let a=new ui(this.plugin.app,e,t,i,o),l=new Promise((c,d)=>a.openAndGetValue(c,d));try{return await l}catch(c){if(r)throw c;return null}}}};var di=class extends we{constructor(){super(...arguments);this.name="config"}async create_static_templates(){}async create_dynamic_templates(){}async teardown(){}async generate_object(e){return e}};var mi=class{constructor(e){this.plugin=e;this.modules_array=[];this.modules_array.push(new oi(this.plugin)),this.modules_array.push(new ai(this.plugin)),this.modules_array.push(new si(this.plugin)),this.modules_array.push(new li(this.plugin)),this.modules_array.push(new ci(this.plugin)),this.modules_array.push(new fi(this.plugin)),this.modules_array.push(new di(this.plugin))}async init(){for(let e of this.modules_array)await e.init()}async teardown(){for(let e of this.modules_array)await e.teardown()}async generate_object(e){let t={};for(let r of this.modules_array)t[r.getName()]=await r.generate_object(e);return t}};var En=X(require("obsidian"));var gi=class{constructor(e){this.plugin=e;if(En.Platform.isMobile||!(this.plugin.app.vault.adapter instanceof En.FileSystemAdapter))this.cwd="";else{this.cwd=this.plugin.app.vault.adapter.getBasePath();let{promisify:t}=require("util"),{exec:r}=require("child_process");this.exec_promise=t(r)}}async generate_system_functions(e){let t=new Map,r=await this.plugin.templater.functions_generator.generate_object(e,Qe.INTERNAL);for(let i of this.plugin.settings.templates_pairs){let o=i[0],a=i[1];!o||!a||(En.Platform.isMobile?t.set(o,()=>new Promise(l=>l(Io))):(a=await this.plugin.templater.parser.parse_commands(a,r),t.set(o,async l=>{let c={...process.env,...l},d={timeout:this.plugin.settings.command_timeout*1e3,cwd:this.cwd,env:c,...this.plugin.settings.shell_path&&{shell:this.plugin.settings.shell_path}};try{let{stdout:m}=await this.exec_promise(a,d);return m.trimRight()}catch(m){throw new O(`Error with User Template ${o}`,m)}})))}return t}async generate_object(e){let t=await this.generate_system_functions(e);return Object.fromEntries(t)}};var hi=class{constructor(e){this.plugin=e}async generate_user_script_functions(){let e=new Map,t=ke(()=>ze(this.plugin.app,this.plugin.settings.user_scripts_folder),`Couldn't find user script folder "${this.plugin.settings.user_scripts_folder}"`);if(!t)return new Map;for(let r of t)r.extension.toLowerCase()==="js"&&await this.load_user_script_function(r,e);return e}async load_user_script_function(e,t){let r=c=>window.require&&window.require(c),i={},o={exports:i},a=await this.plugin.app.vault.read(e);try{window.eval("(function anonymous(require, module, exports){"+a+` })`)(r,o,i)}catch(c){throw new O(`Failed to load user script at "${e.path}".`,c.message)}let l=i.default||o.exports;if(!l)throw new O(`Failed to load user script at "${e.path}". No exports detected.`);if(!(l instanceof Function))throw new O(`Failed to load user script at "${e.path}". Default export is not a function.`);t.set(`${e.basename}`,l)}async generate_object(){let e=await this.generate_user_script_functions();return Object.fromEntries(e)}};var Ai=class{constructor(e){this.plugin=e;this.user_system_functions=new gi(e),this.user_script_functions=new hi(e)}async generate_object(e){let t={},r={};return this.plugin.settings.enable_system_commands&&(t=await this.user_system_functions.generate_object(e)),this.plugin.settings.user_scripts_folder&&(r=await this.user_script_functions.generate_object()),{...t,...r}}};var qs=X(require("obsidian")),Qe;(function(t){t[t.INTERNAL=0]="INTERNAL",t[t.USER_INTERNAL=1]="USER_INTERNAL"})(Qe||(Qe={}));var _i=class{constructor(e){this.plugin=e;this.internal_functions=new mi(this.plugin),this.user_functions=new Ai(this.plugin)}async init(){await this.internal_functions.init()}async teardown(){await this.internal_functions.teardown()}additional_functions(){return{app:this.plugin.app,obsidian:qs}}async generate_object(e,t=1){let r={},i=this.additional_functions(),o=await this.internal_functions.generate_object(e),a={};switch(Object.assign(r,i),t){case 0:Object.assign(r,o);break;case 1:a=await this.user_functions.generate_object(e),Object.assign(r,{...o,user:a});break}return r}};var Vs={},N,He=new Array(32).fill(void 0);He.push(void 0,null,!0,!1);function Se(n){return He[n]}var Tn=He.length;function Ls(n){n<36||(He[n]=Tn,Tn=n)}function xi(n){let e=Se(n);return Ls(n),e}var Ko=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});Ko.decode();var mr=new Uint8Array;function gr(){return mr.byteLength===0&&(mr=new Uint8Array(N.memory.buffer)),mr}function vt(n,e){return Ko.decode(gr().subarray(n,n+e))}function wt(n){Tn===He.length&&He.push(He.length+1);let e=Tn;return Tn=He[e],He[e]=n,e}var $e=0,hr=new TextEncoder("utf-8"),Hs=typeof hr.encodeInto=="function"?function(n,e){return hr.encodeInto(n,e)}:function(n,e){let t=hr.encode(n);return e.set(t),{read:n.length,written:t.length}};function mt(n,e,t){if(t===void 0){let l=hr.encode(n),c=e(l.length);return gr().subarray(c,c+l.length).set(l),$e=l.length,c}let r=n.length,i=e(r),o=gr(),a=0;for(;a127)break;o[i+a]=l}if(a!==r){a!==0&&(n=n.slice(a)),i=t(i,r,r=a+n.length*3);let l=gr().subarray(i+a,i+r);a+=Hs(n,l).written}return $e=a,i}function $s(n){return n==null}var Ar=new Int32Array;function Ce(){return Ar.byteLength===0&&(Ar=new Int32Array(N.memory.buffer)),Ar}function yi(n){let e=typeof n;if(e=="number"||e=="boolean"||n==null)return`${n}`;if(e=="string")return`"${n}"`;if(e=="symbol"){let i=n.description;return i==null?"Symbol":`Symbol(${i})`}if(e=="function"){let i=n.name;return typeof i=="string"&&i.length>0?`Function(${i})`:"Function"}if(Array.isArray(n)){let i=n.length,o="[";i>0&&(o+=yi(n[0]));for(let a=1;a1)r=t[1];else return toString.call(n);if(r=="Object")try{return"Object("+JSON.stringify(n)+")"}catch{return"Object"}return n instanceof Error?`${n.name}: ${n.message} ${n.stack}`:r}function Ks(n,e){if(!(n instanceof e))throw new Error(`expected instance of ${e.name}`);return n.ptr}var _r=32;function Rs(n){if(_r==1)throw new Error("out of js stack");return He[--_r]=n,_r}function ji(n,e){try{return n.apply(this,e)}catch(t){N.__wbindgen_exn_store(wt(t))}}var Ht=class{static __wrap(e){let t=Object.create(Ht.prototype);return t.ptr=e,t}__destroy_into_raw(){let e=this.ptr;return this.ptr=0,e}free(){let e=this.__destroy_into_raw();N.__wbg_parserconfig_free(e)}get interpolate(){let e=N.__wbg_get_parserconfig_interpolate(this.ptr);return String.fromCodePoint(e)}set interpolate(e){N.__wbg_set_parserconfig_interpolate(this.ptr,e.codePointAt(0))}get execution(){let e=N.__wbg_get_parserconfig_execution(this.ptr);return String.fromCodePoint(e)}set execution(e){N.__wbg_set_parserconfig_execution(this.ptr,e.codePointAt(0))}get single_whitespace(){let e=N.__wbg_get_parserconfig_single_whitespace(this.ptr);return String.fromCodePoint(e)}set single_whitespace(e){N.__wbg_set_parserconfig_single_whitespace(this.ptr,e.codePointAt(0))}get multiple_whitespace(){let e=N.__wbg_get_parserconfig_multiple_whitespace(this.ptr);return String.fromCodePoint(e)}set multiple_whitespace(e){N.__wbg_set_parserconfig_multiple_whitespace(this.ptr,e.codePointAt(0))}constructor(e,t,r,i,o,a,l){let c=mt(e,N.__wbindgen_malloc,N.__wbindgen_realloc),d=$e,m=mt(t,N.__wbindgen_malloc,N.__wbindgen_realloc),y=$e,b=mt(l,N.__wbindgen_malloc,N.__wbindgen_realloc),E=$e,P=N.parserconfig_new(c,d,m,y,r.codePointAt(0),i.codePointAt(0),o.codePointAt(0),a.codePointAt(0),b,E);return Ht.__wrap(P)}get opening_tag(){try{let r=N.__wbindgen_add_to_stack_pointer(-16);N.parserconfig_opening_tag(r,this.ptr);var e=Ce()[r/4+0],t=Ce()[r/4+1];return vt(e,t)}finally{N.__wbindgen_add_to_stack_pointer(16),N.__wbindgen_free(e,t)}}set opening_tag(e){let t=mt(e,N.__wbindgen_malloc,N.__wbindgen_realloc),r=$e;N.parserconfig_set_opening_tag(this.ptr,t,r)}get closing_tag(){try{let r=N.__wbindgen_add_to_stack_pointer(-16);N.parserconfig_closing_tag(r,this.ptr);var e=Ce()[r/4+0],t=Ce()[r/4+1];return vt(e,t)}finally{N.__wbindgen_add_to_stack_pointer(16),N.__wbindgen_free(e,t)}}set closing_tag(e){let t=mt(e,N.__wbindgen_malloc,N.__wbindgen_realloc),r=$e;N.parserconfig_set_closing_tag(this.ptr,t,r)}get global_var(){try{let r=N.__wbindgen_add_to_stack_pointer(-16);N.parserconfig_global_var(r,this.ptr);var e=Ce()[r/4+0],t=Ce()[r/4+1];return vt(e,t)}finally{N.__wbindgen_add_to_stack_pointer(16),N.__wbindgen_free(e,t)}}set global_var(e){let t=mt(e,N.__wbindgen_malloc,N.__wbindgen_realloc),r=$e;N.parserconfig_set_global_var(this.ptr,t,r)}},on=class{static __wrap(e){let t=Object.create(on.prototype);return t.ptr=e,t}__destroy_into_raw(){let e=this.ptr;return this.ptr=0,e}free(){let e=this.__destroy_into_raw();N.__wbg_renderer_free(e)}constructor(e){Ks(e,Ht);var t=e.ptr;e.ptr=0;let r=N.renderer_new(t);return on.__wrap(r)}render_content(e,t){try{let a=N.__wbindgen_add_to_stack_pointer(-16),l=mt(e,N.__wbindgen_malloc,N.__wbindgen_realloc),c=$e;N.renderer_render_content(a,this.ptr,l,c,Rs(t));var r=Ce()[a/4+0],i=Ce()[a/4+1],o=Ce()[a/4+2];if(o)throw xi(i);return xi(r)}finally{N.__wbindgen_add_to_stack_pointer(16),He[_r++]=void 0}}};async function Ys(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(r){if(n.headers.get("Content-Type")!="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}let t=await n.arrayBuffer();return await WebAssembly.instantiate(t,e)}else{let t=await WebAssembly.instantiate(n,e);return t instanceof WebAssembly.Instance?{instance:t,module:n}:t}}function Us(){let n={};return n.wbg={},n.wbg.__wbindgen_object_drop_ref=function(e){xi(e)},n.wbg.__wbindgen_string_new=function(e,t){let r=vt(e,t);return wt(r)},n.wbg.__wbindgen_string_get=function(e,t){let r=Se(t),i=typeof r=="string"?r:void 0;var o=$s(i)?0:mt(i,N.__wbindgen_malloc,N.__wbindgen_realloc),a=$e;Ce()[e/4+1]=a,Ce()[e/4+0]=o},n.wbg.__wbg_call_97ae9d8645dc388b=function(){return ji(function(e,t){let r=Se(e).call(Se(t));return wt(r)},arguments)},n.wbg.__wbg_new_8d2af00bc1e329ee=function(e,t){let r=new Error(vt(e,t));return wt(r)},n.wbg.__wbg_message_fe2af63ccc8985bc=function(e){let t=Se(e).message;return wt(t)},n.wbg.__wbg_newwithargs_8fe23e3842840c8e=function(e,t,r,i){let o=new Function(vt(e,t),vt(r,i));return wt(o)},n.wbg.__wbg_call_168da88779e35f61=function(){return ji(function(e,t,r){let i=Se(e).call(Se(t),Se(r));return wt(i)},arguments)},n.wbg.__wbg_call_3999bee59e9f7719=function(){return ji(function(e,t,r,i){let o=Se(e).call(Se(t),Se(r),Se(i));return wt(o)},arguments)},n.wbg.__wbindgen_debug_string=function(e,t){let r=yi(Se(t)),i=mt(r,N.__wbindgen_malloc,N.__wbindgen_realloc),o=$e;Ce()[e/4+1]=o,Ce()[e/4+0]=i},n.wbg.__wbindgen_throw=function(e,t){throw new Error(vt(e,t))},n}function Gs(n,e){}function Ws(n,e){return N=n.exports,Ro.__wbindgen_wasm_module=e,Ar=new Int32Array,mr=new Uint8Array,N}async function Ro(n){typeof n=="undefined"&&(n=new URL("rusty_engine_bg.wasm",Vs.url));let e=Us();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n)),Gs(e);let{instance:t,module:r}=await Ys(await n,e);return Ws(t,r)}var Yo=Ro;var Uo=Ui("AGFzbQEAAAABvwEaYAJ/fwBgAn9/AX9gAX8Bf2ADf39/AX9gA39/fwBgAX8AYAV/f39/fwBgBH9/f38AYAR/f39/AX9gAABgBX9/f39/AX9gAX8BfmAAAX9gBn9/f39/fwBgB39/f39/f38AYAV/f35/fwBgBX9/fX9/AGAFf398f38AYAR/fn9/AGAFf35/f38AYAR/fX9/AGAEf3x/fwBgBn9/f39/fwF/YAd/f39/f39/AX9gCn9/f39/f39/f38Bf2ACfn8BfwLkAgsDd2JnGl9fd2JpbmRnZW5fb2JqZWN0X2Ryb3BfcmVmAAUDd2JnFV9fd2JpbmRnZW5fc3RyaW5nX25ldwABA3diZxVfX3diaW5kZ2VuX3N0cmluZ19nZXQAAAN3YmcbX193YmdfY2FsbF85N2FlOWQ4NjQ1ZGMzODhiAAEDd2JnGl9fd2JnX25ld184ZDJhZjAwYmMxZTMyOWVlAAEDd2JnHl9fd2JnX21lc3NhZ2VfZmUyYWY2M2NjYzg5ODViYwACA3diZyJfX3diZ19uZXd3aXRoYXJnc184ZmUyM2UzODQyODQwYzhlAAgDd2JnG19fd2JnX2NhbGxfMTY4ZGE4ODc3OWUzNWY2MQADA3diZxtfX3diZ19jYWxsXzM5OTliZWU1OWU5Zjc3MTkACAN3YmcXX193YmluZGdlbl9kZWJ1Z19zdHJpbmcAAAN3YmcQX193YmluZGdlbl90aHJvdwAAA7kBtwECBwAGAgYEBAcBBQMKCAAEBgYAAwcCAAEADgETAQQXAQICAQAAAwcZAQAFAQwABgACAgAAAgAEBAAGAQAAAAAEBw0CAQUEBQYCDBgAAQAAAAQBAQEAAQABBAQEBgMDBwMJAwQIAAAABQkAAgEAAAAABwAAAgICAgAFBQMEFgoGEQ8QAAUHAwIBAgABBQEBCAACAQEBBQEAAgECAgACAQEBAgAJCQICAgIAAAAAAwMDAQECAgsLCwUEBQFwATs7BQMBABEGCQF/AUGAgMAACwfcBRkGbWVtb3J5AgAXX193YmdfcGFyc2VyY29uZmlnX2ZyZWUAUSJfX3diZ19nZXRfcGFyc2VyY29uZmlnX2ludGVycG9sYXRlAH4iX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19pbnRlcnBvbGF0ZQB3IF9fd2JnX2dldF9wYXJzZXJjb25maWdfZXhlY3V0aW9uAH8gX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19leGVjdXRpb24AeChfX3diZ19nZXRfcGFyc2VyY29uZmlnX3NpbmdsZV93aGl0ZXNwYWNlAIABKF9fd2JnX3NldF9wYXJzZXJjb25maWdfc2luZ2xlX3doaXRlc3BhY2UAeSpfX3diZ19nZXRfcGFyc2VyY29uZmlnX211bHRpcGxlX3doaXRlc3BhY2UAgQEqX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19tdWx0aXBsZV93aGl0ZXNwYWNlAHoQcGFyc2VyY29uZmlnX25ldwBVGHBhcnNlcmNvbmZpZ19vcGVuaW5nX3RhZwBGHHBhcnNlcmNvbmZpZ19zZXRfb3BlbmluZ190YWcAYxhwYXJzZXJjb25maWdfY2xvc2luZ190YWcARxxwYXJzZXJjb25maWdfc2V0X2Nsb3NpbmdfdGFnAGQXcGFyc2VyY29uZmlnX2dsb2JhbF92YXIASBtwYXJzZXJjb25maWdfc2V0X2dsb2JhbF92YXIAZRNfX3diZ19yZW5kZXJlcl9mcmVlAE8McmVuZGVyZXJfbmV3ACAXcmVuZGVyZXJfcmVuZGVyX2NvbnRlbnQAORFfX3diaW5kZ2VuX21hbGxvYwB1El9fd2JpbmRnZW5fcmVhbGxvYwCFAR9fX3diaW5kZ2VuX2FkZF90b19zdGFja19wb2ludGVyAKsBD19fd2JpbmRnZW5fZnJlZQCaARRfX3diaW5kZ2VuX2V4bl9zdG9yZQCfAQllAQBBAQs6mAGdAaoBPzzBAZUBlgFOkgGOAWotYsEBwQFnKl3BAXaIAUyJAYgBhwGQAY8BiQGJAYwBigGLAZgBX8EBaKABXo4BvwG+AYQBOElwoQHBAWioAWCjAVclqQGcAcEBwAEK2dYCtwG8IAIPfwF+IwBBEGsiCyQAAkACQCAAQfUBTwRAQYCAfEEIQQgQlwFBFEEIEJcBakEQQQgQlwFqa0F3cUF9aiICQQBBEEEIEJcBQQJ0ayIBIAEgAksbIABNDQIgAEEEakEIEJcBIQRBrK7AACgCAEUNAUEAIARrIQMCQAJAAn9BACAEQYACSQ0AGkEfIARB////B0sNABogBEEGIARBCHZnIgBrdkEBcSAAQQF0a0E+agsiBkECdEG4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EBBBIAQoAgAiBSAAKAIAaiIGQdSDwAApAAA3AAAgBCAFQRBqNgIAIAZBCGpB3IPAACkAADcAACADQQE2AiwgAyABKAIIQShqIgU2AiggAyAANgIYIANB3ABqQQE2AgAgA0ICNwJMIANB8IPAADYCSCADIANBKGo2AlgCQAJAAkACQAJAAkAgA0EYakGYisAAIANByABqEB5FBEAgAigCACEIAkAgAigCCCIBRQ0AIAFBBHQhCkGQhMAAIQZBACEBQQAhBANAAn8gASAIaiIHQQRqIgkgBygCAEUNABoCQCAERQ0AIANBEGogBCgCACAEKAIEQQAgBiAGLQAAQQJGG0EBEBAgA0EIaiADKAIQIAMoAhRBACAHQQ1qIgQgBC0AAEECRhtBABAQIANBGGogAygCCCADKAIMEBIgA0EBNgI0IANBATYCLCADIAU2AiggAyADQRhqNgIwIAMgADYCRCADQQI2AlwgA0IDNwJMIANBmITAADYCSCADIANBKGo2AlggA0HEAGpBmIrAACADQcgAahAeDQUgAygCHEUNACADKAIYEBULIAdBDmohBgJAIAdBDGotAABFBEAgA0ECNgIsIAMgCTYCKCADIAA2AhggA0EBNgJcIANCAjcCTCADQfSEwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQcgA0ECNgI0IANBoIXAADYCMCADQQE2AiwgAyAFNgIoIAMgADYCGCADQQI2AlwgA0IDNwJMIANBmITAADYCSCADIANBKGo2AlggA0EYakGYisAAIANByABqEB5FDQFBq4HAAEErIANByABqQdiBwABBqIXAABBSAAsgA0ECNgIsIAMgCTYCKCADIAA2AhggA0EBNgJcIANCAjcCTCADQcSEwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQcLQQALIQQgCiABQRBqIgFHDQALIARFDQAgAyAEKAIAIAQoAgRBACAGIAYtAABBAkYbQQEQECADQRhqIAMoAgAgAygCBBASIANBNGpBATYCACADQQE2AiwgAyAFNgIoIAMgA0EYajYCMCADIAA2AkQgA0HcAGpBAjYCACADQgM3AkwgA0GYhMAANgJIIAMgA0EoajYCWCADQcQAakGYisAAIANByABqEB4NBSADKAIcRQ0AIAMoAhgQFQsgAEEEaigCACAAQQhqIgQoAgAiAWtBJ00EQCAAIAFBKBBBIAQoAgAhAQsgBCABQShqNgIAIAAoAgAgAWoiAUHIhcAAKQAANwAAIAFBCGpB0IXAACkAADcAACABQRBqQdiFwAApAAA3AAAgAUEYakHghcAAKQAANwAAIAFBIGpB6IXAACkAADcAACADQTxqQQI2AgAgA0E0akEBNgIAIANBoIXAADYCOCADIAU2AjAgA0EBNgIsIAMgBTYCKCADIAA2AhggA0HcAGoiAUEDNgIAIANCBDcCTCADQZiGwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQUgA0EBNgIsIAMgBTYCKCADIAA2AhggAUEBNgIAIANCAjcCTCADQdCGwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQYgAkEEaigCAARAIAgQFQsgA0HgAGokAA8LQauBwABBKyADQcgAakHYgcAAQYCEwAAQUgALQauBwABBKyADQcgAakHYgcAAQbCEwAAQUgALQauBwABBKyADQcgAakHYgcAAQYSFwAAQUgALQauBwABBKyADQcgAakHYgcAAQdSEwAAQUgALQauBwABBKyADQcgAakHYgcAAQbiFwAAQUgALQauBwABBKyADQcgAakHYgcAAQbiGwAAQUgALQauBwABBKyADQcgAakHYgcAAQeCGwAAQUgAL7QkCCH8GfiMAQdAAayIDJAACQAJAAkAQVCIEBEAgA0EgakIANwMAIANBHGpBkIrAADYCACAEIAQpAwAiC0IBfDcDACADQQA2AhggAyALNwMIIAMgBEEIaikDADcDECADQqeAgIDwBDcDSCADQo2AgICgDjcDQCADQoqAgIDgDTcDOCADQtyAgIDACzcDMCADQQhqIANBMGoQGSADQQA2AjggA0IENwMwIAJFBEAgAEEANgIIIABCATcCAEEEIQRBBCEBDAQLIAEgAmohCEEAIQIDQAJ/IAEsAAAiBEF/SgRAIARB/wFxIQQgAUEBagwBCyABLQABQT9xIQUgBEEfcSEGIARBX00EQCAGQQZ0IAVyIQQgAUECagwBCyABLQACQT9xIAVBBnRyIQUgBEFwSQRAIAUgBkEMdHIhBCABQQNqDAELIAZBEnRBgIDwAHEgAS0AA0E/cSAFQQZ0cnIiBEGAgMQA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ASACIANqQQFqIgM2AhAgAyAHT0EAIAMgDE0bRQRAIAYgA08NAQwDCwsgB0EEELUBAAsgASAGNgIQCyABLQAkIAEoAgAiAiABKAIEIgRHckUNASABQQE6ACUgBCACawshAyAIRQ0AIAIgCGohCyADRQRAQQAhAgwBCyADQX9qIgEgAyABIAtqLQAAQQ1GGyECCyAAIAI2AgQgACALNgIAIAVBEGokAAv+BAEKfyMAQTBrIgMkACADQSRqIAE2AgAgA0EDOgAoIANCgICAgIAENwMIIAMgADYCICADQQA2AhggA0EANgIQAkACQAJAIAIoAggiCkUEQCACQRRqKAIAIgRFDQEgAigCACEBIAIoAhAhACAEQX9qQf////8BcUEBaiIHIQQDQCABQQRqKAIAIgUEQCADKAIgIAEoAgAgBSADKAIkKAIMEQMADQQLIAAoAgAgA0EIaiAAQQRqKAIAEQEADQMgAEEIaiEAIAFBCGohASAEQX9qIgQNAAsMAQsgAkEMaigCACIARQ0AIABBBXQhCyAAQX9qQf///z9xQQFqIQcgAigCACEBA0AgAUEEaigCACIABEAgAygCICABKAIAIAAgAygCJCgCDBEDAA0DCyADIAQgCmoiBUEcai0AADoAKCADIAVBBGopAgBCIIk3AwggBUEYaigCACEGIAIoAhAhCEEAIQlBACEAAkACQAJAIAVBFGooAgBBAWsOAgACAQsgBkEDdCAIaiIMKAIEQTdHDQEgDCgCACgCACEGC0EBIQALIAMgBjYCFCADIAA2AhAgBUEQaigCACEAAkACQAJAIAVBDGooAgBBAWsOAgACAQsgAEEDdCAIaiIGKAIEQTdHDQEgBigCACgCACEAC0EBIQkLIAMgADYCHCADIAk2AhggCCAFKAIAQQN0aiIAKAIAIANBCGogACgCBBEBAA0CIAFBCGohASALIARBIGoiBEcNAAsLQQAhACAHIAIoAgRJIgFFDQEgAygCICACKAIAIAdBA3RqQQAgARsiASgCACABKAIEIAMoAiQoAgwRAwBFDQELQQEhAAsgA0EwaiQAIAALwgQBCH8jAEHQAGsiBCQAIARBEGogASACIAMoAgAgA0EIaigCABAOAkACQAJAAkACQAJAIAQoAhBFBEAgBEEeai0AAA0EIARBxABqKAIAIQYgBCgCQCEHIARBHGot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AkAgAUGAAU8EQCACQQA2AgwgAUGAEEkNASABQYCABE8NAiACIAFBP3FBgAFyOgAOIAIgAUEMdkHgAXI6AAwgAiABQQZ2QT9xQYABcjoADUEDDAMLIAAoAggiAyAAQQRqKAIARgR/IAAgAxBAIAAoAggFIAMLIAAoAgBqIAE6AAAgACAAKAIIQQFqNgIIDAMLIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAELIAIgAUE/cUGAAXI6AA8gAiABQQZ2QT9xQYABcjoADiACIAFBDHZBP3FBgAFyOgANIAIgAUESdkEHcUHwAXI6AAxBBAshASAAQQRqKAIAIABBCGoiBCgCACIDayABSQRAIAAgAyABEEEgBCgCACEDCyAAKAIAIANqIAJBDGogARC5ARogBCABIANqNgIACyACQRBqJABBAAvVAgEDfyMAQRBrIgIkAAJAAn8CQCABQYABTwRAIAJBADYCDCABQYAQTw0BIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAILIAAoAggiAyAAQQRqKAIARgRAIAAgAxBDIAAoAgghAwsgACADQQFqNgIIIAAoAgAgA2ogAToAAAwCCyABQYCABE8EQCACIAFBP3FBgAFyOgAPIAIgAUEGdkE/cUGAAXI6AA4gAiABQQx2QT9xQYABcjoADSACIAFBEnZBB3FB8AFyOgAMQQQMAQsgAiABQT9xQYABcjoADiACIAFBDHZB4AFyOgAMIAIgAUEGdkE/cUGAAXI6AA1BAwshASAAQQRqKAIAIABBCGoiBCgCACIDayABSQRAIAAgAyABEEIgBCgCACEDCyAAKAIAIANqIAJBDGogARC5ARogBCABIANqNgIACyACQRBqJAAL1wIBA38jAEEQayICJAACQAJ/AkACQCABQYABTwRAIAJBADYCDCABQYAQSQ0BIAFBgIAETw0CIAIgAUE/cUGAAXI6AA4gAiABQQx2QeABcjoADCACIAFBBnZBP3FBgAFyOgANQQMMAwsgACgCCCIDIABBBGooAgBGBEAgACADEEMgACgCCCEDCyAAIANBAWo2AgggACgCACADaiABOgAADAMLIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAELIAIgAUE/cUGAAXI6AA8gAiABQQZ2QT9xQYABcjoADiAC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AiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEQayIDJAACQAJAIAEEQCABKAIAIgJBf0YNASABIAJBAWo2AgAgAyABQRBqEGEgASABKAIAQX9qNgIAIAMoAgAhAQJAIAMoAgQiAiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEQayIDJAACQAJAIAEEQCABKAIAIgJBf0YNASABIAJBAWo2AgAgAyABQSxqEGEgASABKAIAQX9qNgIAIAMoAgAhAQJAIAMoAgQiAiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEwayICJAAgAUEEaiEDIAEoAgRFBEAgASgCACEBIAJBEGoiBEEANgIAIAJCATcDCCACIAJBCGo2AhQgAkEoaiABQRBqKQIANwMAIAJBIGogAUEIaikCADcDACACIAEpAgA3AxggAkEUakGAjsAAIAJBGGoQHhogA0EIaiAEKAIANgIAIAMgAikDCDcCAAsgAEHoj8AANgIEIAAgAzYCACACQTBqJAALkAEBAn8CQAJ/AkACQAJAAn9BASIDIAFBAEgNABogAigCCEUNAiACKAIEIgQNASABDQNBAQwECyEDQQAhAQwECyACKAIAIARBASABEJkBDAILIAENAEEBDAELIAFBARCeAQsiAgRAIAAgAjYCBEEAIQMMAQsgACABNgIEQQEhAQsgACADNgIAIABBCGogATYCAAunAQECfwJAAkACQAJAAkACQAJAAn8gAgRAQQEiBCABQQBIDQEaIAMoAghFDQMgAygCBCIFDQIgAQ0EDAYLIAAgATYCBEEBCyEEQQAhAQwGCyADKAIAIAUgAiABEJkBIgNFDQIMBAsgAUUNAgsgASACEJ4BIgMNAgsgACABNgIEIAIhAQwCCyACIQML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AX8jAEEwayICJAAgAiABNgIEIAIgADYCACACQRxqQQI2AgAgAkEsakEDNgIAIAJCAjcCDCACQfSWwAA2AgggAkEDNgIkIAIgAkEgajYCGCACIAJBBGo2AiggAiACNgIgIAJBCGpBhJfAABB0AAtsAQF/IwBBMGsiAyQAIAMgATYCBCADIAA2AgAgA0EcakECNgIAIANBLGpBAzYCACADQgI3AgwgA0HAk8AANgIIIANBAzYCJCADIANBIGo2AhggAyADNgIoIAMgA0EEajYCICADQQhqIAIQdAALVgECfyMAQSBrIgIkACAAQRxqKAIAIQMgACgCGCACQRhqIAFBEGopAgA3AwAgAkEQaiABQQhqKQIANwMAIAIgASkCADcDCCADIAJBCGoQHiACQSBqJAALWQEBfyMAQSBrIgIkACACIAAoAgA2AgQgAkEYaiABQRBqKQIANwMAIAJBEGogAUEIaikCADcDACACIAEpAgA3AwggAkEEakGYisAAIAJBCGoQHiACQSBqJAALWQEBfyMAQSBrIgIkACACIAAoAgA2AgQgAkEYaiABQRBqKQIANwMAIAJBEGogAUEIaikCADcDACACIAEpAgA3AwggAkEEakGAjsAAIAJBCGoQHiACQSBqJAALZwAjAEEwayIBJABB2K3AAC0AAARAIAFBHGpBATYCACABQgI3AgwgAUH0jsAANgIIIAFBAzYCJCABIAA2AiwgASABQSBqNgIYIAEgAUEsajYCICABQQhqQZyPwAAQdAALIAFBMGokAAtZAQF/IwBBIGsiAiQAIAIgACgCADYCBCACQRhqIAFBEGopAgA3AwAgAkEQaiABQQhqKQIANwMAIAIgASkCADcDCCACQQRqQbSRwAAgAkEIahAeIAJBIGokAAtnAQJ/IAEoAgAhAwJAAkACQCABQQhqKAIAIgFFBEBBASECDAELIAFBf0wNASABQQEQngEiAkUNAgsgAiADIAEQuQEhAiAAIAE2AgggACABNgIEIAAgAjYCAA8LEHMACyABQQEQswEAC1YBAX8jAEEgayICJAAgAiAANgIEIAJBGGogAUEQaikCADcDACACQRBqIAFBCGopAgA3AwAgAiABKQIANwMIIAJBBGpBmIrAACACQQhqEB4gAkEgaiQAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQQhqIgMoAgAEQCAAKAIEEBULIAAgATYCBCAAQQA2AgAgAEEMaiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQRRqIgMoAgAEQCAAKAIQEBULIAAgATYCECAAQQA2AgAgAEEYaiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQTBqIgMoAgAEQCAAKAIsEBULIAAgATYCLCAAQQA2AgAgAEE0aiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8jAEEQayIFJAAgASgCACACKAIAIAMoAgAgBCgCABAIIQEgBUEIahCDASAFKAIMIQIgACAFKAIIIgNBAEc2AgAgACACIAEgAxs2AgQgBUEQaiQAC08BAn8gACgCACIDQQRqKAIAIANBCGoiBCgCACIAayACSQRAIAMgACACEEEgBCgCACEACyADKAIAIABqIAEgAhC5ARogBCAAIAJqNgIAQQALTwECfyAAKAIAIgNBBGooAgAgA0EIaiIEKAIAIgBrIAJJBEAgAyAAIAIQQiAEKAIAIQALIAMoAgAgAGogASACELkBGiAEIAAgAmo2AgBBAAtRAQF/IwBBEGsiBCQAIAEoAgAgAigCACADKAIAEAchASAEQQhqEIMBIAQoAgwhAiAAIAQoAggiA0EARzYCACAAIAIgASADGzYCBCAEQRBqJAALSgECfyAAQQRqKAIAIABBCGoiBCgCACIDayACSQRAIAAgAyACEEEgBCgCACEDCyAAKAIAIANqIAEgAhC5ARogBCACIANqNgIAQQALPwEBfyMAQSBrIgAkACAAQRxqQQA2AgAgAEGwkMAANgIYIABCATcCDCAAQcyQwAA2AgggAEEIakGkkcAAEHQAC0MBA38CQCACRQ0AA0AgAC0AACIEIAEtAAAiBUYEQCAAQQFqIQAgAUEBaiEBIAJBf2oiAg0BDAILCyAEIAVrIQMLIAMLTAECfyMAQRBrIgMkACABKAIAIAIoAgAQAyEBIANBCGoQgwEgAygCDCECIAAgAygCCCIEQQBHNgIAIAAgAiABIAQbNgIEIANBEGokAAtLAAJAAn8gAUGAgMQARwRAQQEgACgCGCABIABBHGooAgAoAhARAQANARoLIAINAUEACw8LIAAoAhggAiADIABBHGooAgAoAgwRAwALRwEBfyMAQSBrIgIkACACQRRqQQA2AgAgAkGAk8AANgIQIAJCATcCBCACQSs2AhwgAiAANgIYIAIgAkEYajYCACACIAEQdAALRgECfyABKAIEIQIgASgCACEDQQhBBBCeASIBRQRAQQhBBBCzAQALIAEgAjYCBCABIAM2AgAgAEH4j8AANgIEIAAgATYCAAs5AQF/IAFBEHZAACECIABBADYCCCAAQQAgAUGAgHxxIAJBf0YiARs2AgQgAEEAIAJBEHQgARs2AgALZAEDfyMAQRBrIgEkACAAKAIMIgJFBEBBmI7AAEHIj8AAEG8ACyAAKAIIIgNFBEBBmI7AAEHYj8AAEG8ACyABIAI2AgggASAANgIEIAEgAzYCACABKAIAIAEoAgQgASgCCBBQAAs/AQF/IwBBIGsiACQAIABBHGpBADYCACAAQcyRwAA2AhggAEIBNwIMIABBjJLAADYCCCAAQQhqQZSSwAAQdAALPgEBfyMAQSBrIgIkACACQQE6ABggAiABNgIUIAIgADYCECACQfyTwAA2AgwgAkGAk8AANgIIIAJBCGoQcgALKwACQCAAQXxLDQAgAEUEQEEEDwsgACAAQX1JQQJ0EJ4BIgBFDQAgAA8LAAsiACMAQRBrIgAkACAAQQhqIAEQfSAAQQhqEFMgAEEQaiQACysAAkAgAARAIAAoAgANASAAQQA2AgAgAEEcaiABNgIADwsQrQEACxCuAQALKwACQCAABEAgACgCAA0BIABBADYCACAAQSBqIAE2AgAPCxCtAQALEK4BAAsrAAJAIAAEQCAAKAIADQEgAEEANgIAIABBJGogATYCAA8LEK0BAAsQrgEACysAAkAgAARAIAAoAgANASAAQQA2AgAgAEEoaiABNgIADwsQrQEACxCuAQALQAEBfyMAQRBrIgQkACAEIAM2AgwgBCACNgIIIAQgATYCBCAEIAA2AgAgBCgCACAEKAIEIAQoAgggBCgCDBATAAs3ACAAQQM6ACAgAEKAgICAgAQ3AgAgACABNgIYIABBADYCECAAQQA2AgggAEEcakHEh8AANgIACzUBAX8gASgCGEHDjsAAQQsgAUEcaigCACgCDBEDACECIABBADoABSAAIAI6AAQgACABNgIACyUAAkAgAARAIAAoAgBBf0YNASAAQRxqKAIADwsQrQEACxCuAQALJQACQCAABEAgACgCAEF/Rg0BIABBIGooAgAPCxCtAQALEK4BAAslAAJAIAAEQCAAKAIAQX9GDQEgAEEkaigCAA8LEK0BAAsQrgEACyUAAkAgAARAIAAoAgBBf0YNASAAQShqKAIADwsQrQEACxCuAQALJwAgACAAKAIEQQFxIAFyQQJyNgIEIAAgAWoiACAAKAIEQQFyNgIECzoBAn9B3K3AAC0AACEBQdytwABBADoAAEHgrcAAKAIAIQJB4K3AAEEANgIAIAAgAjYCBCAAIAE2AgALIAEBfwJAIAAoAgQiAUUNACAAQQhqKAIARQ0AIAEQFQsLHwACQCABQXxNBEAgACABQQQgAhCZASIADQELAAsgAAsjACACIAIoAgRBfnE2AgQgACABQQFyNgIEIAAgAWogATYCAAslACAARQRAQciMwABBMBCsAQALIAAgAiADIAQgBSABKAIQEQoACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQEQgACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQEQcACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERUACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERIACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERQACx4AIAAgAUEDcjYCBCAAIAFqIgAgACgCBEEBcjYCBAsUACAAQQRqKAIABEAgACgCABAVCwshACAARQRAQciMwABBMBCsAQALIAAgAiADIAEoAhARBAALHwAgAEUEQEHIjMAAQTAQrAEACyAAIAIgASgCEBEBAAsZAQF/IAAoAhAiAQR/IAEFIABBFGooAgALCxkAIAAoAgAiACgCACAAQQhqKAIAIAEQtwELEgBBAEEZIABBAXZrIABBH0YbCxYAIAAgAUEBcjYCBCAAIAFqIAE2AgALHAAgASgCGEHgpsAAQQUgAUEcaigCACgCDBEDAAsTACAAKAIAIgBBJE8EQCAAEAALCxAAIAAgAWpBf2pBACABa3ELFAAgACgCACAAQQhqKAIAIAEQtwELDAAgACABIAIgAxAYCwsAIAEEQCAAEBULCw8AIABBAXQiAEEAIABrcgsUACAAKAIAIAEg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BAAAAAQAAAARAAAAEgAAABMAAABjYW5ub3QgYWNjZXNzIGEgVGhyZWFkIExvY2FsIFN0b3JhZ2UgdmFsdWUgZHVyaW5nIG9yIGFmdGVyIGRlc3RydWN0aW9uL3J1c3RjL2E1NWRkNzFkNWZiMGVjNWE2YTNhOWU4YzI3YjIxMjdiYTQ5MWNlNTIvbGlicmFyeS9zdGQvc3JjL3RocmVhZC9sb2NhbC5ycwAAAHYFEABPAAAApQEAABoAAAAUAAAAAAAAAAEAAAAVAAAAL3J1c3RjL2E1NWRkNzFkNWZiMGVjNWE2YTNhOWU4YzI3YjIxMjdiYTQ5MWNlNTIvbGlicmFyeS9jb3JlL3NyYy9zdHIvcGF0dGVybi5ycwDoBRAATwAAALcBAAAmAAAAY2xvc3VyZSBpbnZva2VkIHJlY3Vyc2l2ZWx5IG9yIGRlc3Ryb3llZCBhbHJlYWR5SnNWYWx1ZSgpAAAAeAYQAAgAAACABhAAAQAAAG51bGwgcG9pbnRlciBwYXNzZWQgdG8gcnVzdHJlY3Vyc2l2ZSB1c2Ugb2YgYW4gb2JqZWN0IGRldGVjdGVkIHdoaWNoIHdvdWxkIGxlYWQgdG8gdW5zYWZlIGFsaWFzaW5nIGluIHJ1c3QAACQAAAAEAAAABAAAACUAAAAmAAAAJwAAAGNhbGxlZCBgT3B0aW9uOjp1bndyYXAoKWAgb24gYSBgTm9uZWAgdmFsdWVBY2Nlc3NFcnJvcm1lbW9yeSBhbGxvY2F0aW9uIG9mICBieXRlcyBmYWlsZWQKAAAATgcQABUAAABjBxAADgAAAGxpYnJhcnkvc3RkL3NyYy9hbGxvYy5yc4QHEAAYAAAAUgEAAAkAAABsaWJyYXJ5L3N0ZC9zcmMvcGFuaWNraW5nLnJzrAcQABwAAABGAgAAHwAAAKwHEAAcAAAARwIAAB4AAAAoAAAADAAAAAQAAAApAAAAJAAAAAgAAAAEAAAAKgAAACsAAAAQAAAABAAAACwAAAAtAAAAJAAAAAgAAAAEAAAALgAAAC8AAABIYXNoIHRhYmxlIGNhcGFjaXR5IG92ZXJmbG93MAgQABwAAAAvY2FyZ28vcmVnaXN0cnkvc3JjL2dpdGh1Yi5jb20tMWVjYzYyOTlkYjllYzgyMy9oYXNoYnJvd24tMC4xMi4zL3NyYy9yYXcvbW9kLnJzAFQIEABPAAAAWgAAACgAAAAwAAAABAAAAAQAAAAxAAAAMgAAADMAAAAwAAAAAAAAAAEAAAAHAAAAbGlicmFyeS9hbGxvYy9zcmMvcmF3X3ZlYy5yc2NhcGFjaXR5IG92ZXJmbG93AAAA+AgQABEAAADcCBAAHAAAAAYCAAAFAAAAYSBmb3JtYXR0aW5nIHRyYWl0IGltcGxlbWVudGF0aW9uIHJldHVybmVkIGFuIGVycm9ybGlicmFyeS9hbGxvYy9zcmMvZm10LnJzAFcJEAAYAAAAZAIAACAAAAAuLgAAgAkQAAIAAABpbmRleCBvdXQgb2YgYm91bmRzOiB0aGUgbGVuIGlzICBidXQgdGhlIGluZGV4IGlzIAAAjAkQACAAAACsCRAAEgAAAGNhbGxlZCBgT3B0aW9uOjp1bndyYXAoKWAgb24gYSBgTm9uZWAgdmFsdWUAOQAAAAAAAAABAAAAOgAAAGA6IACACRAAAAAAAA0KEAACAAAAfSB9MHgwMDAxMDIwMzA0MDUwNjA3MDgwOTEwMTExMjEzMTQxNTE2MTcxODE5MjAyMTIyMjMyNDI1MjYyNzI4MjkzMDMxMzIzMzM0MzUzNjM3MzgzOTQwNDE0MjQzNDQ0NTQ2NDc0ODQ5NTA1MTUyNTM1NDU1NTY1NzU4NTk2MDYxNjI2MzY0NjU2NjY3Njg2OTcwNzE3MjczNzQ3NTc2Nzc3ODc5ODA4MTgyODM4NDg1ODY4Nzg4ODk5MDkxOTI5Mzk0OTU5Njk3OTg5OXJhbmdlIHN0YXJ0IGluZGV4ICBvdXQgb2YgcmFuZ2UgZm9yIHNsaWNlIG9mIGxlbmd0aCAAAADtChAAEgAAAP8KEAAiAAAAbGlicmFyeS9jb3JlL3NyYy9zbGljZS9pbmRleC5ycwA0CxAAHwAAADQAAAAFAAAAcmFuZ2UgZW5kIGluZGV4IGQLEAAQAAAA/woQACIAAAA0CxAAHwAAAEkAAAAFAAAAc2xpY2UgaW5kZXggc3RhcnRzIGF0ICBidXQgZW5kcyBhdCAAlAsQABYAAACqCxAADQAAADQLEAAfAAAAXAAAAAUAAABsaWJyYXJ5L2NvcmUvc3JjL3N0ci9wYXR0ZXJuLnJzANgLEAAfAAAAGgYAABUAAADYCxAAHwAAAEgGAAAVAAAA2AsQAB8AAABJBgAA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");var vi=class{async init(){await Yo(Uo);let e=new Ht("<%","%>","\0","*","-","_","tR");this.renderer=new on(e)}async parse_commands(e,t){return this.renderer.render_content(e,t)}};var Xe;(function(a){a[a.CreateNewFromTemplate=0]="CreateNewFromTemplate",a[a.AppendActiveFile=1]="AppendActiveFile",a[a.OverwriteFile=2]="OverwriteFile",a[a.OverwriteActiveFile=3]="OverwriteActiveFile",a[a.DynamicProcessor=4]="DynamicProcessor",a[a.StartupTemplate=5]="StartupTemplate"})(Xe||(Xe={}));var kn=class{constructor(e){this.plugin=e;this.functions_generator=new _i(this.plugin),this.parser=new vi}async setup(){this.files_with_pending_templates=new Set,await this.parser.init(),await this.functions_generator.init(),this.plugin.registerMarkdownPostProcessor((e,t)=>this.process_dynamic_templates(e,t))}create_running_config(e,t,r){let i=Jt(this.plugin.app);return{template_file:e,target_file:t,run_mode:r,active_file:i}}async read_and_parse_template(e){let t=await this.plugin.app.vault.read(e.template_file);return this.parse_template(e,t)}async parse_template(e,t){let r=await this.functions_generator.generate_object(e,Qe.USER_INTERNAL);return this.current_functions_object=r,await this.parser.parse_commands(t,r)}start_templater_task(e){this.files_with_pending_templates.add(e)}async end_templater_task(e){this.files_with_pending_templates.delete(e),this.files_with_pending_templates.size===0&&(this.plugin.app.workspace.trigger("templater:all-templates-executed"),await this.functions_generator.teardown())}async create_new_note_from_template(e,t,r,i=!0){if(!t)switch(this.plugin.app.vault.getConfig("newFileLocation")){case"current":{let y=Jt(this.plugin.app);y&&(t=y.parent);break}case"folder":t=this.plugin.app.fileManager.getNewFileParent("");break;case"root":t=this.plugin.app.vault.getRoot();break;default:break}let o=e instanceof Oe.TFile&&e.extension||"md",a=await Te(async()=>{let m=t instanceof Oe.TFolder?t.path:t,y=this.plugin.app.vault.getAvailablePath((0,Oe.normalizePath)(`${m??""}/${r||"Untitled"}`),o),b=oo(y);return b&&!this.plugin.app.vault.getAbstractFileByPathInsensitive(b)&&await this.plugin.app.vault.createFolder(b),this.plugin.app.vault.create(y,"")},`Couldn't create ${o} file.`);if(a==null)return;let{path:l}=a;this.start_templater_task(l);let c,d;if(e instanceof Oe.TFile?(c=this.create_running_config(e,a,0),d=await Te(async()=>this.read_and_parse_template(c),"Template parsing error, aborting.")):(c=this.create_running_config(void 0,a,0),d=await Te(async()=>this.parse_template(c,e),"Template parsing error, aborting.")),d==null){await this.plugin.app.vault.delete(a),await this.end_templater_task(l);return}if(await this.plugin.app.vault.modify(a,d),this.plugin.app.workspace.trigger("templater:new-note-from-template",{file:a,content:d}),i){let m=this.plugin.app.workspace.getLeaf(!1);if(!m){oe(new O("No active leaf"));return}await m.openFile(a,{state:{mode:"source"}}),await this.plugin.editor_handler.jump_to_next_cursor_location(a,!0),m.setEphemeralState({rename:"all"})}return await this.end_templater_task(l),a}async append_template_to_active_file(e){let t=this.plugin.app.workspace.getActiveViewOfType(Oe.MarkdownView),r=this.plugin.app.workspace.activeEditor;if(!r||!r.file||!r.editor){oe(new O("No active editor, can't append templates."));return}let{path:i}=r.file;this.start_templater_task(i);let o=this.create_running_config(e,r.file,1),a=await Te(async()=>this.read_and_parse_template(o),"Template parsing error, aborting.");if(a==null){await this.end_templater_task(i);return}let c=r.editor.getDoc(),d=c.listSelections();c.replaceSelection(a),r.file&&await this.plugin.app.vault.append(r.file,""),this.plugin.app.workspace.trigger("templater:template-appended",{view:t,editor:r,content:a,oldSelections:d,newSelections:c.listSelections()}),await this.plugin.editor_handler.jump_to_next_cursor_location(r.file,!0),await this.end_templater_task(i)}async write_template_to_file(e,t){let{path:r}=t;this.start_templater_task(r);let i=this.plugin.app.workspace.activeEditor,o=Jt(this.plugin.app),a=this.create_running_config(e,t,2),l=await Te(async()=>this.read_and_parse_template(a),"Template parsing error, aborting.");if(l==null){await this.end_templater_task(r);return}await this.plugin.app.vault.modify(t,l),o?.path===t.path&&i&&i.editor&&i.editor.setSelection({line:0,ch:0},{line:0,ch:0}),this.plugin.app.workspace.trigger("templater:new-note-from-template",{file:t,content:l}),await this.plugin.editor_handler.jump_to_next_cursor_location(t,!0),await this.end_templater_task(r)}overwrite_active_file_commands(){let e=this.plugin.app.workspace.activeEditor;if(!e||!e.file){oe(new O("Active editor is null, can't overwrite content"));return}this.overwrite_file_commands(e.file,!0)}async overwrite_file_commands(e,t=!1){let{path:r}=e;this.start_templater_task(r);let i=this.create_running_config(e,e,t?3:2),o=await Te(async()=>this.read_and_parse_template(i),"Template parsing error, aborting.");if(o==null){await this.end_templater_task(r);return}await this.plugin.app.vault.modify(e,o),this.plugin.app.workspace.trigger("templater:overwrite-file",{file:e,content:o}),await this.plugin.editor_handler.jump_to_next_cursor_location(e,!0),await this.end_templater_task(r)}async process_dynamic_templates(e,t){let r=ro(),i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT),o,a=!1,l;for(;o=i.nextNode();){let c=o.nodeValue;if(c!==null){let d=r.exec(c);if(d!==null){let m=this.plugin.app.metadataCache.getFirstLinkpathDest("",t.sourcePath);if(!m||!(m instanceof Oe.TFile))return;if(!a){a=!0;let y=this.create_running_config(m,m,4);l=await this.functions_generator.generate_object(y,Qe.USER_INTERNAL),this.current_functions_object=l}}for(;d!=null;){let m=d[1]+d[2],y=await Te(async()=>await this.parser.parse_commands(m,l),`Command Parsing error in dynamic command '${m}'`);if(y==null)return;let b=r.lastIndex-d[0].length,E=r.lastIndex;c=c.substring(0,b)+y+c.substring(E),r.lastIndex+=y.length-d[0].length,d=r.exec(c)}o.nodeValue=c}}}get_new_file_template_for_folder(e){do{let t=this.plugin.settings.folder_templates.find(r=>r.folder==e.path);if(t&&t.template)return t.template;e=e.parent}while(e)}get_new_file_template_for_file(e){let t=this.plugin.settings.file_templates.find(r=>new RegExp(r.regex).test(e.path));if(t&&t.template)return t.template}static async on_file_creation(e,t,r){if(!(r instanceof Oe.TFile)||r.extension!=="md")return;let i=(0,Oe.normalizePath)(e.plugin.settings.templates_folder);if(!(r.path.includes(i)&&i!=="/")&&(await ar(300),!e.files_with_pending_templates.has(r.path)))if(r.stat.size==0&&e.plugin.settings.enable_folder_templates){let o=e.get_new_file_template_for_folder(r.parent);if(!o)return;let a=await Te(async()=>Dt(t,o),`Couldn't find template ${o}`);if(a==null)return;await e.write_template_to_file(a,r)}else if(r.stat.size==0&&e.plugin.settings.enable_file_templates){let o=e.get_new_file_template_for_file(r);if(!o)return;let a=await Te(async()=>Dt(t,o),`Couldn't find template ${o}`);if(a==null)return;await e.write_template_to_file(a,r)}else r.stat.size<=1e5?await e.overwrite_file_commands(r):console.log(`Templater skipped parsing ${r.path} because file size exceeds 10000`)}async execute_startup_scripts(){for(let e of this.plugin.settings.startup_templates){if(!e)continue;let t=ke(()=>Dt(this.plugin.app,e),`Couldn't find startup template "${e}"`);if(!t)continue;let{path:r}=t;this.start_templater_task(r);let i=this.create_running_config(t,t,5);await Te(async()=>this.read_and_parse_template(i),"Startup Template parsing error, aborting."),await this.end_templater_task(r)}}};var Go=X(require("obsidian")),xr=class{constructor(e,t,r){this.plugin=e;this.templater=t;this.settings=r}setup(){Array.isArray(this.plugin.app.workspace.onLayoutReadyCallbacks)?this.plugin.app.workspace.onLayoutReadyCallbacks.push({pluginId:this.plugin.manifest.id,callback:()=>{this.update_trigger_file_on_creation()}}):this.plugin.app.workspace.onLayoutReady(()=>{this.update_trigger_file_on_creation()}),this.update_syntax_highlighting(),this.update_file_menu()}update_syntax_highlighting(){let e=this.plugin.editor_handler.desktopShouldHighlight(),t=this.plugin.editor_handler.mobileShouldHighlight();e||t?this.plugin.editor_handler.enable_highlighter():this.plugin.editor_handler.disable_highlighter()}update_trigger_file_on_creation(){this.settings.trigger_on_file_creation?(this.trigger_on_file_creation_event=this.plugin.app.vault.on("create",e=>kn.on_file_creation(this.templater,this.plugin.app,e)),this.plugin.registerEvent(this.trigger_on_file_creation_event)):this.trigger_on_file_creation_event&&(this.plugin.app.vault.offref(this.trigger_on_file_creation_event),this.trigger_on_file_creation_event=void 0)}update_file_menu(){this.plugin.registerEvent(this.plugin.app.workspace.on("file-menu",(e,t)=>{t instanceof Go.TFolder&&e.addItem(r=>{r.setTitle("Create new note from template").setIcon("templater-icon").onClick(()=>{this.plugin.fuzzy_suggester.create_new_note_from_template(t)})})}))}};var yr=X(require("obsidian"));var wi=class{constructor(e){this.plugin=e}setup(){this.plugin.addCommand({id:"insert-templater",name:"Open insert template modal",icon:"templater-icon",hotkeys:yr.Platform.isMacOS?void 0:[{modifiers:["Alt"],key:"e"}],callback:()=>{this.plugin.fuzzy_suggester.insert_template()}}),this.plugin.addCommand({id:"replace-in-file-templater",name:"Replace templates in the active file",icon:"templater-icon",hotkeys:yr.Platform.isMacOS?void 0:[{modifiers:["Alt"],key:"r"}],callback:()=>{this.plugin.templater.overwrite_active_file_commands()}}),this.plugin.addCommand({id:"jump-to-next-cursor-location",name:"Jump to next cursor location",icon:"text-cursor",hotkeys:[{modifiers:["Alt"],key:"Tab"}],callback:()=>{this.plugin.editor_handler.jump_to_next_cursor_location()}}),this.plugin.addCommand({id:"create-new-note-from-template",name:"Create new note from template",icon:"templater-icon",hotkeys:yr.Platform.isMacOS?void 0:[{modifiers:["Alt"],key:"n"}],callback:()=>{this.plugin.fuzzy_suggester.create_new_note_from_template()}}),this.register_templates_hotkeys()}register_templates_hotkeys(){this.plugin.settings.enabled_templates_hotkeys.forEach(e=>{e&&this.add_template_hotkey(null,e)})}add_template_hotkey(e,t){this.remove_template_hotkey(e),t&&(this.plugin.addCommand({id:t,name:`Insert ${t}`,icon:"templater-icon",callback:()=>{let r=ke(()=>Dt(this.plugin.app,t),"Couldn't find the template file associated with this hotkey");!r||this.plugin.templater.append_template_to_active_file(r)}}),this.plugin.addCommand({id:`create-${t}`,name:`Create ${t}`,icon:"templater-icon",callback:()=>{let r=ke(()=>Dt(this.plugin.app,t),"Couldn't find the template file associated with this hotkey");!r||this.plugin.templater.create_new_note_from_template(r)}}))}remove_template_hotkey(e){e&&(this.plugin.removeCommand(`${e}`),this.plugin.removeCommand(`create-${e}`))}};var Ci=X(require("obsidian"));var bi=X(require("obsidian"));var Ei=class{constructor(e){this.app=e}async jump_to_next_cursor_location(){let e=this.app.workspace.activeEditor;if(!e||!e.editor)return;let t=e.editor.getValue(),{new_content:r,positions:i}=this.replace_and_get_cursor_positions(t);if(i){let o=e instanceof bi.MarkdownView?e.currentMode.getFoldInfo():null;e.editor.setValue(r),o&&Array.isArray(o.folds)&&(i.forEach(a=>{o.folds=o.folds.filter(l=>l.from>a.line||l.to[0-9]*)\\)\\s*%>","g");for(;(r=i.exec(e))!=null;)t.push(r);if(t.length===0)return{};t.sort((c,d)=>Number(c.groups&&c.groups.order)-Number(d.groups&&d.groups.order));let o=t[0][0];t=t.filter(c=>c[0]===o);let a=[],l=0;for(let c of t){let d=c.index-l;if(a.push(this.get_editor_position_from_index(e,d)),e=e.replace(new RegExp(no(c[0])),""),l+=c[0].length,c[1]==="")break}return{new_content:e,positions:a}}set_cursor_location(e){let t=this.app.workspace.activeEditor;if(!t||!t.editor)return;let r=t.editor,i=[];for(let a of e)i.push({from:a});let o={selections:i};r.transaction(o)}};var Jo=X(require("obsidian"));var Js={app:{name:"app",description:"This module exposes the app instance. Prefer to use this over the global app instance."},user:{name:"user",description:"This module exposes custom made scripts, written by yourself within the script file folder location"},config:{name:"config",description:`This module exposes Templater's running configuration. This is mostly useful when writing scripts requiring some context information. `,functions:{template_file:{name:"template_file",description:"The `TFile` object representing the template file.",definition:"tp.config.template_file"},target_file:{name:"target_file",description:"The `TFile` object representing the target file where the template will be inserted.",definition:"tp.config.target_file"},run_mode:{name:"run_mode",description:"The `RunMode`, representing the way Templater was launched (Create new from template, Append to active file, ...).",definition:"tp.config.run_mode"},active_file:{name:"active_file",description:"The active file (if existing) when launching Templater.",definition:"tp.config.active_file?"}}},date:{name:"date",description:"This module contains every internal function related to dates.",functions:{now:{name:"now",description:"Retrieves the date.",definition:'tp.date.now(format: string = "YYYY-MM-DD", offset?: number\u23AEstring, reference?: string, reference_format?: string)',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'},{name:"offset",description:"Duration to offset the date from. If a number is provided, duration will be added to the date in days. You can also specify the offset as a string using the ISO 8601 format."},{name:"reference",description:"The date referential, e.g. set this to the note's title."},{name:"reference_format",description:"The format for the reference date. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/)."}],examples:[{name:"Date now",example:"<% tp.date.now() %>"},{name:"Date now with format",example:'<% tp.date.now("Do MMMM YYYY") %>'},{name:"Last week",example:'<% tp.date.now("YYYY-MM-DD", -7) %>'},{name:"Next week",example:'<% tp.date.now("YYYY-MM-DD", 7) %>'},{name:"Last month",example:'<% tp.date.now("YYYY-MM-DD", "P-1M") %>'},{name:"Next year",example:'<% tp.date.now("YYYY-MM-DD", "P1Y") %>'},{name:"File's title date + 1 day (tomorrow)",example:'<% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %>'},{name:"File's title date - 1 day (yesterday)",example:'<% tp.date.now("YYYY-MM-DD", -1, tp.file.title, "YYYY-MM-DD") %>'}]},tomorrow:{name:"tomorrow",description:"Retrieves tomorrow's date.",definition:'tp.date.tomorrow(format: string = "YYYY-MM-DD")',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'}],examples:[{name:"Date tomorrow",example:"<% tp.date.tomorrow() %>"},{name:"Date tomorrow with format",example:'<% tp.date.tomorrow("Do MMMM YYYY") %>'}]},yesterday:{name:"yesterday",description:"Retrieves yesterday's date.",definition:'tp.date.yesterday(format: string = "YYYY-MM-DD")',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'}],examples:[{name:"Date yesterday",example:"<% tp.date.yesterday() %>"},{name:"Date yesterday with format",example:'<% tp.date.yesterday("Do MMMM YYYY") %>'}]},weekday:{name:"weekday",description:"",definition:'tp.date.weekday(format: string = "YYYY-MM-DD", weekday: number, reference?: string, reference_format?: string)',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'},{name:"weekday",description:"Week day number. If the locale assigns Monday as the first day of the week, `0` will be Monday, `-7` will be last week's day."},{name:"reference",description:"The date referential, e.g. set this to the note's title."},{name:"reference_format",description:"The format for the reference date. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/)."}],examples:[{name:"This week's Monday",example:'<% tp.date.weekday("YYYY-MM-DD", 0) %>'},{name:"Next Monday",example:'<% tp.date.weekday("YYYY-MM-DD", 7) %>'},{name:"File's title Monday",example:'<% tp.date.weekday("YYYY-MM-DD", 0, tp.file.title, "YYYY-MM-DD") %>'},{name:"File's title previous Monday",example:'<% tp.date.weekday("YYYY-MM-DD", -7, tp.file.title, "YYYY-MM-DD") %>'}]}},momentjs:{examples:[{name:"Date now",example:'<% moment(tp.file.title, "YYYY-MM-DD").format("YYYY-MM-DD") %>'},{name:"Get start of month from note title",example:'<% moment(tp.file.title, "YYYY-MM-DD").startOf("month").format("YYYY-MM-DD") %>'},{name:"Get end of month from note title",example:'<% moment(tp.file.title, "YYYY-MM-DD").endOf("month").format("YYYY-MM-DD") %>'}]}},file:{name:"file",description:"This module contains every internal function related to files.",functions:{content:{name:"content",description:"The string contents of the file at the time that Templater was executed. Manipulating this string will *not* update the current file.",definition:"tp.file.content",examples:[{name:"Retrieve file content",example:"<% tp.file.content %>"}]},create_new:{name:"create_new",description:"Creates a new file using a specified template or with a specified content.",definition:"tp.file.create_new(template: TFile \u23AE string, filename?: string, open_new: boolean = false, folder?: TFolder | string)",args:[{name:"template",description:"Either the template used for the new file content, or the file content as a string. If it is the template to use, you retrieve it with `tp.file.find_tfile(TEMPLATENAME)`."},{name:"filename",description:'The filename of the new file, defaults to "Untitled".'},{name:"open_new",description:"Whether to open or not the newly created file. Warning: if you use this option, since commands are executed asynchronously, the file can be opened first and then other commands are appended to that new file and not the previous file."},{name:"folder",description:'The folder to put the new file in, defaults to Obsidian\'s default location. If you want the file to appear in a different folder, specify it with `"PATH/TO/FOLDERNAME"` or `app.vault.getAbstractFileByPath("PATH/TO/FOLDERNAME")`.'}],examples:[{name:"File creation",example:'<%* await tp.file.create_new("MyFileContent", "MyFilename") %>'},{name:"File creation with template",example:'<%* await tp.file.create_new(tp.file.find_tfile("MyTemplate"), "MyFilename") %>'},{name:"File creation and open created note",example:'<%* await tp.file.create_new("MyFileContent", "MyFilename", true) %>'},{name:"File creation in current folder",example:'<%* await tp.file.create_new("MyFileContent", "MyFilename", false, tp.file.folder(true)) %>'},{name:"File creation in specified folder with string path",example:'<%* await tp.file.create_new("MyFileContent", "MyFilename", false, "Path/To/MyFolder") %>'},{name:"File creation in specified folder with TFolder",example:'<%* await tp.file.create_new("MyFileContent", "MyFilename", false, app.vault.getAbstractFileByPath("MyFolder")) %>'},{name:"File creation and append link to current note",example:'[[<% (await tp.file.create_new("MyFileContent", "MyFilename")).basename %>]]'}]},creation_date:{name:"creation_date",description:"Retrieves the file's creation date.",definition:'tp.file.creation_date(format: string = "YYYY-MM-DD HH:mm")',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD HH:mm"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'}],examples:[{name:"File creation date",example:"<% tp.file.creation_date() %>"},{name:"File creation date with format",example:'<% tp.file.creation_date("dddd Do MMMM YYYY HH:mm") %>'}]},cursor:{name:"cursor",description:`Sets the cursor to this location after the template has been inserted. You can navigate between the different cursors using the configured hotkey in Obsidian settings. `,definition:"tp.file.cursor(order?: number)",args:[{name:"order",description:`The order of the different cursors jump, e.g. it will jump from 1 to 2 to 3, and so on. If you specify multiple tp.file.cursor with the same order, the editor will switch to multi-cursor. `}],examples:[{name:"File cursor",example:"<% tp.file.cursor() %>"},{name:"File multi-cursor",example:"<% tp.file.cursor(1) %>Content<% tp.file.cursor(1) %>"}]},cursor_append:{name:"cursor_append",description:"Appends some content after the active cursor in the file.",definition:"tp.file.cursor_append(content: string)",args:[{name:"content",description:"The content to append after the active cursor."}],examples:[{name:"File cursor append",example:'<% tp.file.cursor_append("Some text") %>'}]},exists:{name:"exists",description:"Check to see if a file exists by it's file path. The full path to the file, relative to the Vault and containing the extension, must be provided.",definition:"tp.file.exists(filepath: string)",args:[{name:"filepath",description:"The full file path of the file we want to check existence for."}],examples:[{name:"File existence",example:'<% await tp.file.exists("MyFolder/MyFile.md") %>'},{name:"File existence of current file",example:'<% await tp.file.exists(tp.file.folder(true) + "/" + tp.file.title + ".md") %>'}]},find_tfile:{name:"find_tfile",description:"Search for a file and returns its `TFile` instance.",definition:"tp.file.find_tfile(filename: string)",args:[{name:"filename",description:"The filename we want to search and resolve as a `TFile`."}],examples:[{name:"File find TFile",example:'<% tp.file.find_tfile("MyFile").basename %>'}]},folder:{name:"folder",description:"Retrieves the file's folder name.",definition:"tp.file.folder(absolute: boolean = false)",args:[{name:"absolute",description:"If set to `true`, returns the vault-absolute path of the folder. If `false`, only returns the basename of the folder (the last part). Defaults to `false`."}],examples:[{name:"File folder (Folder)",example:"<% tp.file.folder() %>"},{name:"File folder with vault-absolute path (Path/To/Folder)",example:"<% tp.file.folder(true) %>"}]},include:{name:"include",description:"Includes the file's link content. Templates in the included content will be resolved.",definition:"tp.file.include(include_link: string \u23AE TFile)",args:[{name:"include_link",description:'The link to the file to include, e.g. `"[[MyFile]]"`, or a TFile object. Also supports sections or blocks inclusions.'}],examples:[{name:"File include",example:'<% tp.file.include("[[Template1]]") %>'},{name:"File include TFile",example:'<% tp.file.include(tp.file.find_tfile("MyFile")) %>'},{name:"File include section",example:'<% tp.file.include("[[MyFile#Section1]]") %>'},{name:"File include block",example:'<% tp.file.include("[[MyFile#^block1]]") %>'}]},last_modified_date:{name:"last_modified_date",description:"Retrieves the file's last modification date.",definition:'tp.file.last_modified_date(format: string = "YYYY-MM-DD HH:mm")',args:[{name:"format",description:'The format for the date. Defaults to `"YYYY-MM-DD HH:mm"`. Refer to [format reference](https://momentjs.com/docs/#/displaying/format/).'}],examples:[{name:"File last modified date",example:"<% tp.file.last_modified_date() %>"},{name:"File last modified date with format",example:'<% tp.file.last_modified_date("dddd Do MMMM YYYY HH:mm") %>'}]},move:{name:"move",description:"Moves the file to the desired vault location.",definition:"tp.file.move(new_path: string, file_to_move?: TFile)",args:[{name:"new_path",description:'The new vault relative path of the file, without the file extension. Note: the new path needs to include the folder and the filename, e.g. `"/Notes/MyNote"`.'},{name:"file_to_move",description:"The file to move, defaults to the current file."}],examples:[{name:"File move",example:'<% await tp.file.move("/A/B/" + tp.file.title) %>'},{name:"File move and rename",example:'<% await tp.file.move("/A/B/NewTitle") %>'}]},path:{name:"path",description:"Retrieves the file's absolute path on the system.",definition:"tp.file.path(relative: boolean = false)",args:[{name:"relative",description:"If set to `true`, only retrieves the vault's relative path."}],examples:[{name:"File path",example:"<% tp.file.path() %>"},{name:"File relative path (relative to vault root)",example:"<% tp.file.path(true) %>"}]},rename:{name:"rename",description:"Renames the file (keeps the same file extension).",definition:"tp.file.rename(new_title: string)",args:[{name:"new_title",description:"The new file title."}],examples:[{name:"File rename",example:'<% await tp.file.rename("MyNewName") %>'},{name:"File append a 2 to the file name",example:'<% await tp.file.rename(tp.file.title + "2") %>'}]},selection:{name:"selection",description:"Retrieves the active file's text selection.",definition:"tp.file.selection()",examples:[{name:"File selection",example:"<% tp.file.selection() %>"}]},tags:{name:"tags",description:"Retrieves the file's tags (array of string).",definition:"tp.file.tags",examples:[{name:"File tags",example:"<% tp.file.tags %>"}]},title:{name:"title",definition:"tp.file.title",description:"Retrieves the file's title.",examples:[{name:"File title",example:"<% tp.file.title %>"},{name:"Strip the Zettelkasten ID of title (if space separated)",example:'<% tp.file.title.split(" ")[1] %>'}]}}},frontmatter:{name:"frontmatter",description:"This modules exposes all the frontmatter variables of a file as variables."},hooks:{name:"hooks",description:"This module exposes hooks that allow you to execute code when a Templater event occurs.",functions:{on_all_templates_executed:{name:"on_all_templates_executed",description:"Hooks into when all actively running templates have finished executing. Most of the time this will be a single template, unless you are using `tp.file.include` or `tp.file.create_new`.\n\nMultiple invokations of this method will have their callback functions run in parallel.",definition:"tp.hooks.on_all_templates_executed(callback_function: () => any)",args:[{name:"callback_function",description:"Callback function that will be executed when all actively running templates have finished executing."}]}}},obsidian:{name:"obsidian",description:"This module exposes all the functions and classes from the Obsidian API."},system:{name:"system",description:"This module contains system related functions.",functions:{clipboard:{name:"clipboard",description:"Retrieves the clipboard's content.",definition:"tp.system.clipboard()",examples:[{name:"Clipboard",example:"<% tp.system.clipboard() %>"}]},prompt:{name:"prompt",description:"Spawns a prompt modal and returns the user's input.",definition:"tp.system.prompt(prompt_text?: string, default_value?: string, throw_on_cancel: boolean = false, multiline?: boolean = false)",args:[{name:"prompt_text",description:"Text placed above the input field."},{name:"default_value",description:"A default value for the input field."},{name:"throw_on_cancel",description:"Throws an error if the prompt is canceled, instead of returning a `null` value."},{name:"multiline",description:"If set to `true`, the input field will be a multiline textarea. Defaults to `false`."}],examples:[{name:"Prompt",example:'<% tp.system.prompt("Please enter a value") %>'},{name:"Prompt with default value",example:'<% tp.system.prompt("What is your mood today?", "happy") %>'},{name:"Multiline prompt",example:'<% tp.system.prompt("What is your mood today?", null, false, true) %>'}]},suggester:{name:"suggester",description:"Spawns a suggester prompt and returns the user's chosen item.",definition:'tp.system.suggester(text_items: string[] \u23AE ((item: T) => string), items: T[], throw_on_cancel: boolean = false, placeholder: string = "", limit?: number = undefined)',args:[{name:"text_items",description:"Array of strings representing the text that will be displayed for each item in the suggester prompt. This can also be a function that maps an item to its text representation."},{name:"items",description:"Array containing the values of each item in the correct order."},{name:"throw_on_cancel",description:"Throws an error if the prompt is canceled, instead of returning a `null` value."},{name:"placeholder",description:"Placeholder string of the prompt."},{name:"limit",description:"Limit the number of items rendered at once (useful to improve performance when displaying large lists)."}],examples:[{name:"Suggester",example:'<% tp.system.suggester(["Happy", "Sad", "Confused"], ["Happy", "Sad", "Confused"]) %>'},{name:"Suggester with mapping function (same as above example)",example:'<% tp.system.suggester((item) => item, ["Happy", "Sad", "Confused"]) %>'},{name:"Suggester for files",example:"[[<% (await tp.system.suggester((item) => item.basename, app.vault.getMarkdownFiles())).basename %>]]"},{name:"Suggester for tags",example:'<% tp.system.suggester(item => item, Object.keys(app.metadataCache.getTags()).map(x => x.replace("#", ""))) %>'}]}}},web:{name:"web",description:"This modules contains every internal function related to the web (making web requests).",functions:{daily_quote:{name:"daily_quote",description:"Retrieves and parses the daily quote from `https://github.com/Zachatoo/quotes-database` as a callout.",definition:"tp.web.daily_quote()",examples:[{name:"Daily quote",example:"<% tp.web.daily_quote() %>"}]},random_picture:{name:"random_picture",description:"Gets a random image from `https://unsplash.com/`.",definition:"tp.web.random_picture(size?: string, query?: string, include_size?: boolean)",args:[{name:"size",description:"Image size in the format `x`."},{name:"query",description:"Limits selection to photos matching a search term. Multiple search terms can be passed separated by a comma."},{name:"include_size",description:"Optional argument to include the specified size in the image link markdown. Defaults to false."}],examples:[{name:"Random picture",example:"<% tp.web.random_picture() %>"},{name:"Random picture with size",example:'<% tp.web.random_picture("200x200") %>'},{name:"Random picture with size and query",example:'<% tp.web.random_picture("200x200", "landscape,water") %>'}]},request:{name:"request",description:"Makes a HTTP request to the specified URL. Optionally, you can specify a path to extract specific data from the response.",definition:"tp.web.request(url: string, path?: string)",args:[{name:"url",description:"The URL to which the HTTP request will be made."},{name:"path",description:"A path within the response JSON to extract specific data."}],examples:[{name:"Simple request",example:'<% tp.web.request("https://jsonplaceholder.typicode.com/todos/1") %>'},{name:"Request with path",example:'<% tp.web.request("https://jsonplaceholder.typicode.com/todos", "0.title") %>'}]}}}},Vo={tp:Js};var Qs=["app","config","date","file","frontmatter","hooks","obsidian","system","user","web"],Xs=new Set(Qs);function zo(n){return typeof n=="string"&&Xs.has(n)}function Ti(n){return!!(n.definition||n.returns||n.args)}var ki=class{constructor(e){this.plugin=e;this.documentation=Vo}get_all_modules_documentation(){let e=this.documentation.tp;return(!this.plugin.settings||!this.plugin.settings.user_scripts_folder)&&(e=Object.values(e).filter(t=>t.name!=="user")),Object.values(e).map(t=>(t.queryKey=t.name,t))}async get_all_functions_documentation(e,t){if(e==="app")return this.get_app_functions_documentation(this.plugin.app,t);if(e==="user"){if(!this.plugin.settings||!this.plugin.settings.user_scripts_folder)return;let r=await Te(async()=>{let i=ze(this.plugin.app,this.plugin.settings.user_scripts_folder).filter(a=>a.extension=="js");return await io(this.plugin.app,i)},"User Scripts folder doesn't exist");return!r||r.length===0?void 0:r.reduce((i,o)=>o.extension!=="js"?i:[...i,{name:o.basename,queryKey:o.basename,definition:"",description:o.description,returns:o.returns,args:o.arguments.reduce((l,c)=>(l[c.name]={name:c.name,description:c.description},l),{}),example:""}],[])}if(!!this.documentation.tp[e].functions)return Object.values(this.documentation.tp[e].functions).map(r=>(r.queryKey=r.name,r))}get_app_functions_documentation(e,t){if(!$r(e))return[];let r=t.split(".");if(r.length===0)return[];let i=e;for(let c=0;c[a-z]*)?(?\.(?[a-zA-Z_.]*)?)?$/;this.documentation=new ki(e),this.intellisense_render_setting=e.settings.intellisense_render}onTrigger(e,t,r){let i=t.getRange({line:e.line,ch:0},{line:e.line,ch:e.ch}),o=this.tp_keyword_regex.exec(i);if(!o)return null;let a,l=o.groups&&o.groups.module||"";if(this.module_name=l,o.groups&&o.groups.fn_trigger){if(l==""||!zo(l))return null;this.function_trigger=!0,this.function_name=o.groups.fn||"",a=this.function_name}else this.function_trigger=!1,a=this.module_name;let c={start:{line:e.line,ch:e.ch-a.length},end:{line:e.line,ch:e.ch},query:a};return this.latest_trigger_info=c,c}async getSuggestions(e){let t;return this.module_name&&this.function_trigger?t=await this.documentation.get_all_functions_documentation(this.module_name,this.function_name):t=this.documentation.get_all_modules_documentation(),t?t.filter(r=>r.queryKey.toLowerCase().startsWith(e.query.toLowerCase())):[]}renderSuggestion(e,t){if(t.createEl("b",{text:e.name}),Ti(e)){if(e.args&&this.getNumberOfArguments(e.args)>0&&Mo(this.intellisense_render_setting)){t.createEl("p",{text:"Parameter list:"});let r=t.createEl("ol");for(let[i,o]of Object.entries(e.args))Kr(r,i,o.description)}e.returns&&Oo(this.intellisense_render_setting)&&Kr(t,"Returns",e.returns)}this.function_trigger&&Ti(e)&&t.createEl("code",{text:e.definition}),e.description&&Bo(this.intellisense_render_setting)&&t.createEl("div",{text:e.description})}selectSuggestion(e,t){let r=this.app.workspace.activeEditor;if(!(!r||!r.editor)&&(r.editor.replaceRange(e.queryKey,this.latest_trigger_info.start,this.latest_trigger_info.end),this.latest_trigger_info.start.ch==this.latest_trigger_info.end.ch)){let i=this.latest_trigger_info.end;i.ch+=e.queryKey.length,r.editor.setCursor(i)}}getNumberOfArguments(e){try{return new Map(Object.entries(e)).size}catch{return 0}}updateAutocompleteIntellisenseSetting(e){this.intellisense_render_setting=e}};(function(n){n(window.CodeMirror)})(function(n){"use strict";n.defineMode("javascript",function(e,t){var r=e.indentUnit,i=t.statementIndent,o=t.jsonld,a=t.json||o,l=t.trackScope!==!1,c=t.typescript,d=t.wordCharacters||/[\w$\xa1-\uffff]/,m=function(){function s(je){return{type:je,style:"keyword"}}var p=s("keyword a"),A=s("keyword b"),_=s("keyword c"),F=s("keyword d"),Y=s("operator"),z={type:"atom",style:"atom"};return{if:s("if"),while:p,with:p,else:A,do:A,try:A,finally:A,return:F,break:F,continue:F,new:s("new"),delete:_,void:_,throw:_,debugger:s("debugger"),var:s("var"),const:s("var"),let:s("var"),function:s("function"),catch:s("catch"),for:s("for"),switch:s("switch"),case:s("case"),default:s("default"),in:Y,typeof:Y,instanceof:Y,true:z,false:z,null:z,undefined:z,NaN:z,Infinity:z,this:s("this"),class:s("class"),super:s("atom"),yield:_,export:s("export"),import:s("import"),extends:_,await:_}}(),y=/[+\-*&%=<>!?|~^@]/,b=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;function E(s){for(var p=!1,A,_=!1;(A=s.next())!=null;){if(!p){if(A=="/"&&!_)return;A=="["?_=!0:_&&A=="]"&&(_=!1)}p=!p&&A=="\\"}}var P,k;function w(s,p,A){return P=s,k=A,p}function M(s,p){var A=s.next();if(A=='"'||A=="'")return p.tokenize=$(A),p.tokenize(s,p);if(A=="."&&s.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/))return w("number","number");if(A=="."&&s.match(".."))return w("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(A))return w(A);if(A=="="&&s.eat(">"))return w("=>","operator");if(A=="0"&&s.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/))return w("number","number");if(/\d/.test(A))return s.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/),w("number","number");if(A=="/")return s.eat("*")?(p.tokenize=K,K(s,p)):s.eat("/")?(s.skipToEnd(),w("comment","comment")):Ri(s,p,1)?(E(s),s.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/),w("regexp","string-2")):(s.eat("="),w("operator","operator",s.current()));if(A=="`")return p.tokenize=C,C(s,p);if(A=="#"&&s.peek()=="!")return s.skipToEnd(),w("meta","meta");if(A=="#"&&s.eatWhile(d))return w("variable","property");if(A=="<"&&s.match("!--")||A=="-"&&s.match("->")&&!/\S/.test(s.string.slice(0,s.start)))return s.skipToEnd(),w("comment","comment");if(y.test(A))return(A!=">"||!p.lexical||p.lexical.type!=">")&&(s.eat("=")?(A=="!"||A=="=")&&s.eat("="):/[<>*+\-|&?]/.test(A)&&(s.eat(A),A==">"&&s.eat(A))),A=="?"&&s.eat(".")?w("."):w("operator","operator",s.current());if(d.test(A)){s.eatWhile(d);var _=s.current();if(p.lastType!="."){if(m.propertyIsEnumerable(_)){var F=m[_];return w(F.type,F.style,_)}if(_=="async"&&s.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/,!1))return w("async","keyword",_)}return w("variable","variable",_)}}function $(s){return function(p,A){var _=!1,F;if(o&&p.peek()=="@"&&p.match(b))return A.tokenize=M,w("jsonld-keyword","meta");for(;(F=p.next())!=null&&!(F==s&&!_);)_=!_&&F=="\\";return _||(A.tokenize=M),w("string","string")}}function K(s,p){for(var A=!1,_;_=s.next();){if(_=="/"&&A){p.tokenize=M;break}A=_=="*"}return w("comment","comment")}function C(s,p){for(var A=!1,_;(_=s.next())!=null;){if(!A&&(_=="`"||_=="$"&&s.eat("{"))){p.tokenize=M;break}A=!A&&_=="\\"}return w("quasi","string-2",s.current())}var H="([{}])";function I(s,p){p.fatArrowAt&&(p.fatArrowAt=null);var A=s.string.indexOf("=>",s.start);if(!(A<0)){if(c){var _=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(s.string.slice(s.start,A));_&&(A=_.index)}for(var F=0,Y=!1,z=A-1;z>=0;--z){var je=s.string.charAt(z),Ue=H.indexOf(je);if(Ue>=0&&Ue<3){if(!F){++z;break}if(--F==0){je=="("&&(Y=!0);break}}else if(Ue>=3&&Ue<6)++F;else if(d.test(je))Y=!0;else if(/["'\/`]/.test(je))for(;;--z){if(z==0)return;var ha=s.string.charAt(z-1);if(ha==je&&s.string.charAt(z-2)!="\\"){z--;break}}else if(Y&&!F){++z;break}}Y&&!F&&(p.fatArrowAt=z)}}var J={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,import:!0,"jsonld-keyword":!0};function te(s,p,A,_,F,Y){this.indented=s,this.column=p,this.type=A,this.prev=F,this.info=Y,_!=null&&(this.align=_)}function ne(s,p){if(!l)return!1;for(var A=s.localVars;A;A=A.next)if(A.name==p)return!0;for(var _=s.context;_;_=_.prev)for(var A=_.vars;A;A=A.next)if(A.name==p)return!0}function Q(s,p,A,_,F){var Y=s.cc;for(h.state=s,h.stream=F,h.marked=null,h.cc=Y,h.style=p,s.lexical.hasOwnProperty("align")||(s.lexical.align=!0);;){var z=Y.length?Y.pop():a?W:ee;if(z(A,_)){for(;Y.length&&Y[Y.length-1].lex;)Y.pop()();return h.marked?h.marked:A=="variable"&&ne(s,_)?"variable-2":p}}}var h={state:null,column:null,marked:null,cc:null};function S(){for(var s=arguments.length-1;s>=0;s--)h.cc.push(arguments[s])}function f(){return S.apply(null,arguments),!0}function Me(s,p){for(var A=p;A;A=A.next)if(A.name==s)return!0;return!1}function be(s){var p=h.state;if(h.marked="def",!!l){if(p.context){if(p.lexical.info=="var"&&p.context&&p.context.block){var A=Ae(s,p.context);if(A!=null){p.context=A;return}}else if(!Me(s,p.localVars)){p.localVars=new Ee(s,p.localVars);return}}t.globalVars&&!Me(s,p.globalVars)&&(p.globalVars=new Ee(s,p.globalVars))}}function Ae(s,p){if(p)if(p.block){var A=Ae(s,p.prev);return A?A==p.prev?p:new Ke(A,p.vars,!0):null}else return Me(s,p.vars)?p:new Ke(p.prev,new Ee(s,p.vars),!1);else return null}function _e(s){return s=="public"||s=="private"||s=="protected"||s=="abstract"||s=="readonly"}function Ke(s,p,A){this.prev=s,this.vars=p,this.block=A}function Ee(s,p){this.name=s,this.next=p}var $t=new Ee("this",new Ee("arguments",null));function Re(){h.state.context=new Ke(h.state.context,h.state.localVars,!1),h.state.localVars=$t}function Ze(){h.state.context=new Ke(h.state.context,h.state.localVars,!0),h.state.localVars=null}function xe(){h.state.localVars=h.state.context.vars,h.state.context=h.state.context.prev}xe.lex=!0;function B(s,p){var A=function(){var _=h.state,F=_.indented;if(_.lexical.type=="stat")F=_.lexical.indented;else for(var Y=_.lexical;Y&&Y.type==")"&&Y.align;Y=Y.prev)F=Y.indented;_.lexical=new te(F,h.stream.column(),s,null,_.lexical,p)};return A.lex=!0,A}function D(){var s=h.state;s.lexical.prev&&(s.lexical.type==")"&&(s.indented=s.lexical.indented),s.lexical=s.lexical.prev)}D.lex=!0;function q(s){function p(A){return A==s?f():s==";"||A=="}"||A==")"||A=="]"?S():f(p)}return p}function ee(s,p){return s=="var"?f(B("vardef",p),Er,q(";"),D):s=="keyword a"?f(B("form"),bt,ee,D):s=="keyword b"?f(B("form"),ee,D):s=="keyword d"?h.stream.match(/^\s*$/,!1)?f():f(B("stat"),tt,q(";"),D):s=="debugger"?f(q(";")):s=="{"?f(B("}"),Ze,Bn,D,xe):s==";"?f():s=="if"?(h.state.lexical.info=="else"&&h.state.cc[h.state.cc.length-1]==D&&h.state.cc.pop()(),f(B("form"),bt,ee,D,Fi)):s=="function"?f(At):s=="for"?f(B("form"),Ze,Ii,ee,xe,D):s=="class"||c&&p=="interface"?(h.marked="keyword",f(B("form",s=="class"?s:p),Li,D)):s=="variable"?c&&p=="declare"?(h.marked="keyword",f(ee)):c&&(p=="module"||p=="enum"||p=="type")&&h.stream.match(/^\s*\w/,!1)?(h.marked="keyword",p=="enum"?f(Ki):p=="type"?f(qi,q("operator"),V,q(";")):f(B("form"),Be,q("{"),B("}"),Bn,D,D)):c&&p=="namespace"?(h.marked="keyword",f(B("form"),W,ee,D)):c&&p=="abstract"?(h.marked="keyword",f(ee)):f(B("stat"),On):s=="switch"?f(B("form"),bt,q("{"),B("}","switch"),Ze,Bn,D,D,xe):s=="case"?f(W,q(":")):s=="default"?f(q(":")):s=="catch"?f(B("form"),Re,et,ee,D,xe):s=="export"?f(B("stat"),pa,D):s=="import"?f(B("stat"),ua,D):s=="async"?f(ee):p=="@"?f(W,ee):S(B("stat"),W,q(";"),D)}function et(s){if(s=="(")return f(kt,q(")"))}function W(s,p){return Sn(s,p,!1)}function ye(s,p){return Sn(s,p,!0)}function bt(s){return s!="("?S():f(B(")"),tt,q(")"),D)}function Sn(s,p,A){if(h.state.fatArrowAt==h.stream.start){var _=A?Pn:Dn;if(s=="(")return f(Re,B(")"),se(kt,")"),D,q("=>"),_,xe);if(s=="variable")return S(Re,Be,q("=>"),_,xe)}var F=A?gt:nt;return J.hasOwnProperty(s)?f(F):s=="function"?f(At,F):s=="class"||c&&p=="interface"?(h.marked="keyword",f(B("form"),la,D)):s=="keyword c"||s=="async"?f(A?ye:W):s=="("?f(B(")"),tt,q(")"),D,F):s=="operator"||s=="spread"?f(A?ye:W):s=="["?f(B("]"),da,D,F):s=="{"?sn(Tt,"}",null,F):s=="quasi"?S(Et,F):s=="new"?f(rt(A)):f()}function tt(s){return s.match(/[;\}\)\],]/)?S():S(W)}function nt(s,p){return s==","?f(tt):gt(s,p,!1)}function gt(s,p,A){var _=A==!1?nt:gt,F=A==!1?W:ye;if(s=="=>")return f(Re,A?Pn:Dn,xe);if(s=="operator")return/\+\+|--/.test(p)||c&&p=="!"?f(_):c&&p=="<"&&h.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/,!1)?f(B(">"),se(V,">"),D,_):p=="?"?f(W,q(":"),F):f(F);if(s=="quasi")return S(Et,_);if(s!=";"){if(s=="(")return sn(ye,")","call",_);if(s==".")return f(an,_);if(s=="[")return f(B("]"),tt,q("]"),D,_);if(c&&p=="as")return h.marked="keyword",f(V,_);if(s=="regexp")return h.state.lastType=h.marked="operator",h.stream.backUp(h.stream.pos-h.stream.start-1),f(F)}}function Et(s,p){return s!="quasi"?S():p.slice(p.length-2)!="${"?f(Et):f(tt,Cn)}function Cn(s){if(s=="}")return h.marked="string-2",h.state.tokenize=C,f(Et)}function Dn(s){return I(h.stream,h.state),S(s=="{"?ee:W)}function Pn(s){return I(h.stream,h.state),S(s=="{"?ee:ye)}function rt(s){return function(p){return p=="."?f(s?Nn:Kt):p=="variable"&&c?f(ia,s?gt:nt):S(s?ye:W)}}function Kt(s,p){if(p=="target")return h.marked="keyword",f(nt)}function Nn(s,p){if(p=="target")return h.marked="keyword",f(gt)}function On(s){return s==":"?f(D,ee):S(nt,q(";"),D)}function an(s){if(s=="variable")return h.marked="property",f()}function Tt(s,p){if(s=="async")return h.marked="property",f(Tt);if(s=="variable"||h.style=="keyword"){if(h.marked="property",p=="get"||p=="set")return f(Mn);var A;return c&&h.state.fatArrowAt==h.stream.start&&(A=h.stream.match(/^\s*:\s*/,!1))&&(h.state.fatArrowAt=h.stream.pos+A[0].length),f(Ye)}else{if(s=="number"||s=="string")return h.marked=o?"property":h.style+" property",f(Ye);if(s=="jsonld-keyword")return f(Ye);if(c&&_e(p))return h.marked="keyword",f(Tt);if(s=="[")return f(W,Rt,q("]"),Ye);if(s=="spread")return f(ye,Ye);if(p=="*")return h.marked="keyword",f(Tt);if(s==":")return S(Ye)}}function Mn(s){return s!="variable"?S(Ye):(h.marked="property",f(At))}function Ye(s){if(s==":")return f(ye);if(s=="(")return S(At)}function se(s,p,A){function _(F,Y){if(A?A.indexOf(F)>-1:F==","){var z=h.state.lexical;return z.info=="call"&&(z.pos=(z.pos||0)+1),f(function(je,Ue){return je==p||Ue==p?S():S(s)},_)}return F==p||Y==p?f():A&&A.indexOf(";")>-1?S(s):f(q(p))}return function(F,Y){return F==p||Y==p?f():S(s,_)}}function sn(s,p,A){for(var _=3;_"),V);if(s=="quasi")return S(wr,it)}function na(s){if(s=="=>")return f(V)}function vr(s){return s.match(/[\}\)\]]/)?f():s==","||s==";"?f(vr):S(cn,vr)}function cn(s,p){if(s=="variable"||h.style=="keyword")return h.marked="property",f(cn);if(p=="?"||s=="number"||s=="string")return f(cn);if(s==":")return f(V);if(s=="[")return f(q("variable"),ea,q("]"),cn);if(s=="(")return S(Ut,cn);if(!s.match(/[;\}\)\],]/))return f()}function wr(s,p){return s!="quasi"?S():p.slice(p.length-2)!="${"?f(wr):f(V,ra)}function ra(s){if(s=="}")return h.marked="string-2",h.state.tokenize=C,f(wr)}function br(s,p){return s=="variable"&&h.stream.match(/^\s*[?:]/,!1)||p=="?"?f(br):s==":"?f(V):s=="spread"?f(br):S(V)}function it(s,p){if(p=="<")return f(B(">"),se(V,">"),D,it);if(p=="|"||s=="."||p=="&")return f(V);if(s=="[")return f(V,q("]"),it);if(p=="extends"||p=="implements")return h.marked="keyword",f(V);if(p=="?")return f(V,q(":"),V)}function ia(s,p){if(p=="<")return f(B(">"),se(V,">"),D,it)}function Fn(){return S(V,oa)}function oa(s,p){if(p=="=")return f(V)}function Er(s,p){return p=="enum"?(h.marked="keyword",f(Ki)):S(Be,Rt,ht,sa)}function Be(s,p){if(c&&_e(p))return h.marked="keyword",f(Be);if(s=="variable")return be(p),f();if(s=="spread")return f(Be);if(s=="[")return sn(aa,"]");if(s=="{")return sn(Bi,"}")}function Bi(s,p){return s=="variable"&&!h.stream.match(/^\s*:/,!1)?(be(p),f(ht)):(s=="variable"&&(h.marked="property"),s=="spread"?f(Be):s=="}"?S():s=="["?f(W,q("]"),q(":"),Bi):f(q(":"),Be,ht))}function aa(){return S(Be,ht)}function ht(s,p){if(p=="=")return f(ye)}function sa(s){if(s==",")return f(Er)}function Fi(s,p){if(s=="keyword b"&&p=="else")return f(B("form","else"),ee,D)}function Ii(s,p){if(p=="await")return f(Ii);if(s=="(")return f(B(")"),ca,D)}function ca(s){return s=="var"?f(Er,Yt):s=="variable"?f(Yt):S(Yt)}function Yt(s,p){return s==")"?f():s==";"?f(Yt):p=="in"||p=="of"?(h.marked="keyword",f(W,Yt)):S(W,Yt)}function At(s,p){if(p=="*")return h.marked="keyword",f(At);if(s=="variable")return be(p),f(At);if(s=="(")return f(Re,B(")"),se(kt,")"),D,Mi,ee,xe);if(c&&p=="<")return f(B(">"),se(Fn,">"),D,At)}function Ut(s,p){if(p=="*")return h.marked="keyword",f(Ut);if(s=="variable")return be(p),f(Ut);if(s=="(")return f(Re,B(")"),se(kt,")"),D,Mi,xe);if(c&&p=="<")return f(B(">"),se(Fn,">"),D,Ut)}function qi(s,p){if(s=="keyword"||s=="variable")return h.marked="type",f(qi);if(p=="<")return f(B(">"),se(Fn,">"),D)}function kt(s,p){return p=="@"&&f(W,kt),s=="spread"?f(kt):c&&_e(p)?(h.marked="keyword",f(kt)):c&&s=="this"?f(Rt,ht):S(Be,Rt,ht)}function la(s,p){return s=="variable"?Li(s,p):In(s,p)}function Li(s,p){if(s=="variable")return be(p),f(In)}function In(s,p){if(p=="<")return f(B(">"),se(Fn,">"),D,In);if(p=="extends"||p=="implements"||c&&s==",")return p=="implements"&&(h.marked="keyword"),f(c?V:W,In);if(s=="{")return f(B("}"),ot,D)}function ot(s,p){if(s=="async"||s=="variable"&&(p=="static"||p=="get"||p=="set"||c&&_e(p))&&h.stream.match(/^\s+[\w$\xa1-\uffff]/,!1))return h.marked="keyword",f(ot);if(s=="variable"||h.style=="keyword")return h.marked="property",f(ln,ot);if(s=="number"||s=="string")return f(ln,ot);if(s=="[")return f(W,Rt,q("]"),ln,ot);if(p=="*")return h.marked="keyword",f(ot);if(c&&s=="(")return S(Ut,ot);if(s==";"||s==",")return f(ot);if(s=="}")return f();if(p=="@")return f(W,ot)}function ln(s,p){if(p=="!"||p=="?")return f(ln);if(s==":")return f(V,ht);if(p=="=")return f(ye);var A=h.state.lexical.prev,_=A&&A.info=="interface";return S(_?Ut:At)}function pa(s,p){return p=="*"?(h.marked="keyword",f(Tr,q(";"))):p=="default"?(h.marked="keyword",f(W,q(";"))):s=="{"?f(se(Hi,"}"),Tr,q(";")):S(ee)}function Hi(s,p){if(p=="as")return h.marked="keyword",f(q("variable"));if(s=="variable")return S(ye,Hi)}function ua(s){return s=="string"?f():s=="("?S(W):s=="."?S(nt):S(qn,$i,Tr)}function qn(s,p){return s=="{"?sn(qn,"}"):(s=="variable"&&be(p),p=="*"&&(h.marked="keyword"),f(fa))}function $i(s){if(s==",")return f(qn,$i)}function fa(s,p){if(p=="as")return h.marked="keyword",f(qn)}function Tr(s,p){if(p=="from")return h.marked="keyword",f(W)}function da(s){return s=="]"?f():S(se(ye,"]"))}function Ki(){return S(B("form"),Be,q("{"),B("}"),se(ma,"}"),D,D)}function ma(){return S(Be,ht)}function ga(s,p){return s.lastType=="operator"||s.lastType==","||y.test(p.charAt(0))||/[,.]/.test(p.charAt(0))}function Ri(s,p,A){return p.tokenize==M&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(p.lastType)||p.lastType=="quasi"&&/\{\s*$/.test(s.string.slice(0,s.pos-(A||0)))}return{startState:function(s){var p={tokenize:M,lastType:"sof",cc:[],lexical:new te((s||0)-r,0,"block",!1),localVars:t.localVars,context:t.localVars&&new Ke(null,null,!1),indented:s||0};return t.globalVars&&typeof t.globalVars=="object"&&(p.globalVars=t.globalVars),p},token:function(s,p){if(s.sol()&&(p.lexical.hasOwnProperty("align")||(p.lexical.align=!1),p.indented=s.indentation(),I(s,p)),p.tokenize!=K&&s.eatSpace())return null;var A=p.tokenize(s,p);return P=="comment"?A:(p.lastType=P=="operator"&&(k=="++"||k=="--")?"incdec":P,Q(p,A,P,k,s))},indent:function(s,p){if(s.tokenize==K||s.tokenize==C)return n.Pass;if(s.tokenize!=M)return 0;var A=p&&p.charAt(0),_=s.lexical,F;if(!/^\s*else\b/.test(p))for(var Y=s.cc.length-1;Y>=0;--Y){var z=s.cc[Y];if(z==D)_=_.prev;else if(z!=Fi&&z!=xe)break}for(;(_.type=="stat"||_.type=="form")&&(A=="}"||(F=s.cc[s.cc.length-1])&&(F==nt||F==gt)&&!/^[,\.=+\-*:?[\(]/.test(p));)_=_.prev;i&&_.type==")"&&_.prev.type=="stat"&&(_=_.prev);var je=_.type,Ue=A==je;return je=="vardef"?_.indented+(s.lastType=="operator"||s.lastType==","?_.info.length+1:0):je=="form"&&A=="{"?_.indented:je=="form"?_.indented+r:je=="stat"?_.indented+(ga(s,p)?i||r:0):_.info=="switch"&&!Ue&&t.doubleIndentSwitch!=!1?_.indented+(/^(?:case|default)\b/.test(p)?r:2*r):_.align?_.column+(Ue?0:1):_.indented+(Ue?0:r)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:a?null:"/*",blockCommentEnd:a?null:"*/",blockCommentContinue:a?null:" * ",lineComment:a?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:a?"json":"javascript",jsonldMode:o,jsonMode:a,expressionAllowed:Ri,skipExpression:function(s){Q(s,"atom","atom","true",new n.StringStream("",2,null))}}}),n.registerHelper("wordChars","javascript",/[\w$]/),n.defineMIME("text/javascript","javascript"),n.defineMIME("text/ecmascript","javascript"),n.defineMIME("application/javascript","javascript"),n.defineMIME("application/x-javascript","javascript"),n.defineMIME("application/ecmascript","javascript"),n.defineMIME("application/json",{name:"javascript",json:!0}),n.defineMIME("application/x-json",{name:"javascript",json:!0}),n.defineMIME("application/manifest+json",{name:"javascript",json:!0}),n.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),n.defineMIME("text/typescript",{name:"javascript",typescript:!0}),n.defineMIME("application/typescript",{name:"javascript",typescript:!0})});(function(n){n(window.CodeMirror)})(function(n){"use strict";n.customOverlayMode=function(e,t,r){return{startState:function(){return{base:n.startState(e),overlay:n.startState(t),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(i){return{base:n.copyState(e,i.base),overlay:n.copyState(t,i.overlay),basePos:i.basePos,baseCur:null,overlayPos:i.overlayPos,overlayCur:null}},token:function(i,o){return(i!=o.streamSeen||Math.min(o.basePos,o.overlayPos)0&&(this.activeEditorExtensions.pop(),this.plugin.app.workspace.updateOptions())}async jump_to_next_cursor_location(e=null,t=!1){t&&!this.plugin.settings.auto_jump_to_cursor||e&&Jt(this.plugin.app)!==e||await this.cursor_jumper.jump_to_next_cursor_location()}async registerCodeMirrorMode(){if(!this.desktopShouldHighlight()&&!this.mobileShouldHighlight())return;let e=window.CodeMirror.getMode({},"javascript");if(e.name==="null"){oe(new O("Javascript syntax mode couldn't be found, can't enable syntax highlighting."));return}let t=window.CodeMirror.customOverlayMode;if(t==null){oe(new O("Couldn't find customOverlayMode, can't enable syntax highlighting."));return}window.CodeMirror.defineMode(Zo,function(r){let i={startState:function(){return{...window.CodeMirror.startState(e),inCommand:!1,tag_class:"",freeLine:!1}},copyState:function(o){return{...window.CodeMirror.startState(e),inCommand:o.inCommand,tag_class:o.tag_class,freeLine:o.freeLine}},blankLine:function(o){return o.inCommand?"line-background-templater-command-bg":null},token:function(o,a){if(o.sol()&&a.inCommand&&(a.freeLine=!0),a.inCommand){let c="";if(o.match(/[-_]{0,1}%>/,!0)){a.inCommand=!1,a.freeLine=!1;let m=a.tag_class;return a.tag_class="",`line-${Pi} ${Di} ${ec} ${m}`}let d=e.token&&e.token(o,a);return o.peek()==null&&a.freeLine&&(c+=" line-background-templater-command-bg"),a.freeLine||(c+=` line-${Pi}`),`${c} ${Di} ${d}`}let l=o.match(/<%[-_]{0,1}\s*([*+]{0,1})/,!0);if(l!=null){switch(l[1]){case"*":a.tag_class=nc;break;default:a.tag_class=tc;break}return a.inCommand=!0,`line-${Pi} ${Di} ${Zs} ${a.tag_class}`}for(;o.next()!=null&&!o.match(/<%/,!1););return null}};return t(window.CodeMirror.getMode(r,"hypermd"),i)})}updateEditorIntellisenseSetting(e){this.autocomplete.updateAutocompleteIntellisenseSetting(e)}};var Oi=class extends jr.Plugin{async onload(){await this.load_settings(),this.templater=new kn(this),await this.templater.setup(),this.editor_handler=new Ni(this),await this.editor_handler.setup(),this.fuzzy_suggester=new ii(this),this.event_handler=new xr(this,this.templater,this.settings),this.event_handler.setup(),this.command_handler=new wi(this),this.command_handler.setup(),(0,jr.addIcon)("templater-icon",qo),this.addRibbonIcon("templater-icon","Templater",async()=>{this.fuzzy_suggester.insert_template()}).setAttribute("id","rb-templater-icon"),this.addSettingTab(new ri(this)),this.app.workspace.onLayoutReady(()=>{this.templater.execute_startup_scripts()})}onunload(){this.templater.functions_generator.teardown()}async save_settings(){await this.saveData(this.settings),this.editor_handler.updateEditorIntellisenseSetting(this.settings.intellisense_render)}async load_settings(){this.settings=Object.assign({},Fo,await this.loadData())}}; /* nosourcemap */ ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/templater-obsidian/manifest.json ================================================ { "id": "templater-obsidian", "name": "Templater", "version": "2.11.1", "description": "Create and use templates", "minAppVersion": "1.5.0", "author": "SilentVoid", "authorUrl": "https://github.com/SilentVoid13", "helpUrl": "https://silentvoid13.github.io/Templater/", "isDesktopOnly": false } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/plugins/templater-obsidian/styles.css ================================================ .templater_search { width: calc(100% - 20px); } .templater_div { border-top: 1px solid var(--background-modifier-border); } .templater_div > .setting-item { border-top: none !important; align-self: center; } .templater_div > .setting-item > .setting-item-control { justify-content: space-around; padding: 0; width: 100%; } .templater_div > .setting-item > .setting-item-control > .setting-editor-extra-setting-button { align-self: center; } .templater_donating { margin: 10px; } .templater_title { margin: 0; padding: 0; margin-top: 5px; text-align: center; } .templater_template { align-self: center; margin-left: 5px; margin-right: 5px; width: 70%; } .templater_cmd { margin-left: 5px; margin-right: 5px; font-size: 14px; width: 100%; } .templater_div2 > .setting-item { align-content: center; justify-content: center; } .templater-prompt-div { display: flex; } .templater-prompt-form { display: flex; flex-grow: 1; } .templater-prompt-input { flex-grow: 1; } .templater-button-div { display: flex; flex-direction: column; align-items: center; margin-top: 1rem; } textarea.templater-prompt-input { height: 10rem; } textarea.templater-prompt-input:focus { border-color: var(--interactive-accent); } .cm-s-obsidian .templater-command-bg { left: 0px; right: 0px; background-color: var(--background-primary-alt); } .cm-s-obsidian .cm-templater-command { font-size: 0.85em; font-family: var(--font-monospace); line-height: 1.3; } .cm-s-obsidian .templater-inline .cm-templater-command { background-color: var(--background-primary-alt); } .cm-s-obsidian .cm-templater-command.cm-templater-opening-tag { font-weight: bold; } .cm-s-obsidian .cm-templater-command.cm-templater-closing-tag { font-weight: bold; } .cm-s-obsidian .cm-templater-command.cm-templater-interpolation-tag { color: var(--code-property, #008bff); } .cm-s-obsidian .cm-templater-command.cm-templater-execution-tag { color: var(--code-function, #c0d700); } .cm-s-obsidian .cm-templater-command.cm-keyword { color: var(--code-keyword, #00a7aa); font-weight: normal; } .cm-s-obsidian .cm-templater-command.cm-atom { color: var(--code-normal, #f39b35); } .cm-s-obsidian .cm-templater-command.cm-value, .cm-s-obsidian .cm-templater-command.cm-number, .cm-s-obsidian .cm-templater-command.cm-type { color: var(--code-value, #a06fca); } .cm-s-obsidian .cm-templater-command.cm-def, .cm-s-obsidian .cm-templater-command.cm-type.cm-def { color: var(--code-normal, var(--text-normal)); } .cm-s-obsidian .cm-templater-command.cm-property, .cm-s-obsidian .cm-templater-command.cm-property.cm-def, .cm-s-obsidian .cm-templater-command.cm-attribute { color: var(--code-function, #98e342); } .cm-s-obsidian .cm-templater-command.cm-variable, .cm-s-obsidian .cm-templater-command.cm-variable-2, .cm-s-obsidian .cm-templater-command.cm-variable-3, .cm-s-obsidian .cm-templater-command.cm-meta { color: var(--code-property, #d4d4d4); } .cm-s-obsidian .cm-templater-command.cm-callee, .cm-s-obsidian .cm-templater-command.cm-operator, .cm-s-obsidian .cm-templater-command.cm-qualifier, .cm-s-obsidian .cm-templater-command.cm-builtin { color: var(--code-operator, #fc4384); } .cm-s-obsidian .cm-templater-command.cm-tag { color: var(--code-tag, #fc4384); } .cm-s-obsidian .cm-templater-command.cm-comment, .cm-s-obsidian .cm-templater-command.cm-comment.cm-tag, .cm-s-obsidian .cm-templater-command.cm-comment.cm-attribute { color: var(--code-comment, #696d70); } .cm-s-obsidian .cm-templater-command.cm-string, .cm-s-obsidian .cm-templater-command.cm-string-2 { color: var(--code-string, #e6db74); } .cm-s-obsidian .cm-templater-command.cm-header, .cm-s-obsidian .cm-templater-command.cm-hr { color: var(--code-keyword, #da7dae); } .cm-s-obsidian .cm-templater-command.cm-link { color: var(--code-normal, #696d70); } .cm-s-obsidian .cm-templater-command.cm-error { border-bottom: 1px solid #c42412; } .CodeMirror-hints { position: absolute; z-index: 10; overflow: hidden; list-style: none; margin: 0; padding: 2px; -webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); -moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); border-radius: 3px; border: 1px solid silver; background: white; font-size: 90%; font-family: monospace; max-height: 20em; overflow-y: auto; } .CodeMirror-hint { margin: 0; padding: 0 4px; border-radius: 2px; white-space: pre; color: black; cursor: pointer; } li.CodeMirror-hint-active { background: #08f; color: white; } ================================================ FILE: tests/plugin-test-vault.original/.obsidian/workspace.json ================================================ { "main": { "id": "90ac7104a2c14afa", "type": "split", "children": [ { "id": "26e8f8e844ba644b", "type": "tabs", "children": [ { "id": "a9a8380bedb17107", "type": "leaf", "state": { "type": "markdown", "state": { "file": "Welcome.md", "mode": "source", "source": false, "backlinks": false }, "icon": "lucide-file", "title": "Welcome" } } ] } ], "direction": "vertical" }, "left": { "id": "916340f1f9ab9dc8", "type": "split", "children": [ { "id": "925ec3c27c6026ca", "type": "tabs", "children": [ { "id": "657f9126fd67266c", "type": "leaf", "state": { "type": "file-explorer", "state": { "sortOrder": "alphabetical", "autoReveal": false }, "icon": "lucide-folder-closed", "title": "File explorer" } } ] } ], "direction": "horizontal", "width": 300 }, "right": { "id": "8278f287aacfb41b", "type": "split", "children": [], "direction": "horizontal", "width": 300, "collapsed": true }, "left-ribbon": { "hiddenItems": { "templater-obsidian:Templater": false, "daily-notes:Open today's note": false, "templates:Insert template": false, "periodic-notes:Open today": false } }, "active": "a9a8380bedb17107", "lastOpenFiles": [ "Welcome.md" ] } ================================================ FILE: tests/plugin-test-vault.original/2024.md ================================================ ## Yearly Note: 2024 ================================================ FILE: tests/plugin-test-vault.original/2025-04.md ================================================ ## Monthly Note: 2025-04 ================================================ FILE: tests/plugin-test-vault.original/2025-05-18.md ================================================ ## Daily Note: 2025-05-18 ================================================ FILE: tests/plugin-test-vault.original/2025-Q1.md ================================================ ## Quarterly Note: 2025-Q1 ================================================ FILE: tests/plugin-test-vault.original/2025-W20.md ================================================ ## Weekly Note: 2025-W20 ================================================ FILE: tests/plugin-test-vault.original/Welcome.md ================================================ This is a test vault. ================================================ FILE: tests/plugin-test-vault.original/_templates/Daily Note.md ================================================ ## Daily Note: {{title}} ================================================ FILE: tests/plugin-test-vault.original/_templates/Monthly Note.md ================================================ ## Monthly Note: {{title}} ================================================ FILE: tests/plugin-test-vault.original/_templates/Quarterly Note.md ================================================ ## Quarterly Note: {{title}} ================================================ FILE: tests/plugin-test-vault.original/_templates/Weekly Note.md ================================================ ## Weekly Note: {{title}} ================================================ FILE: tests/plugin-test-vault.original/_templates/Yearly Note.md ================================================ ## Yearly Note: {{title}} ================================================ FILE: tests/plugin-test-vault.original/any/standard-parameters.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("any route", () => { it( "should throw an error on missing/invalid `x-success` parameter", async () => { const res = await callObsidian("/", { "x-success": undefined }, 1000); expect(res.ok).toBe(false); expect(res.log).toBeDefined(); expect( res.log!.find((l) => l.level === "error" && l.sender.startsWith("plugin:actions-uri:") && l.args.join(" ").includes("x-success") ), ).toBeDefined(); }, 10000, ); it( "should throw an error on missing/invalid `x-error` parameter", async () => { const res = await callObsidian("/", { "x-error": undefined }, 1000); expect(res.ok).toBe(false); if (!res.ok) { expect(res.error.message).toBe("Callback timeout"); } expect(res.log).toBeDefined(); expect( res.log!.find((l) => l.level === "error" && l.sender.startsWith("plugin:actions-uri:") && l.args.join(" ").includes("x-error") ), ).toBeDefined(); }, 10000, ); }); ================================================ FILE: tests/plugin-test-vault.original/note/append/noteAppend.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/append", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/create/noteCreate.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/create", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/delete/noteDelete.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/delete", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/get/note-1.md ================================================ --- tags: - one - two id: 01JV9K2XGJA4HH5XVWKC8EPQ4W --- # Hello ================================================ FILE: tests/plugin-test-vault.original/note/get/noteGet.test.ts ================================================ import * as fs from "fs/promises"; import * as path from "path"; import { callObsidian, pause } from "#tests/helpers"; import { periodicNotes, recentPeriodicNotes } from "#tests/periodic-notes"; describe("/note/get", () => { it("should return note content on success", async () => { // console.log(__dirname); const res = await callObsidian("note/get", { file: "note/get/note-1.md" }); expect(res.ok).toBe(true); if (res.ok) { expect(res.value["result-filepath"]).toBe("note/get/note-1.md"); expect(res.value["result-body"]).not.toEqual(""); expect(res.value["result-content"]).toContain("# Hello"); expect(res.value["result-front-matter"]) .toBe("tags:\n - one\n - two\nid: 01JV9K2XGJA4HH5XVWKC8EPQ4W\n"); expect(JSON.parse(res.value["result-properties"])) .toEqual({ tags: ["one", "two"], id: "01JV9K2XGJA4HH5XVWKC8EPQ4W" }); expect(res.value["result-uri-path"]) .toContain("file=note%2Fget%2Fnote-1.md"); } }, 10000); it("should return error on failure", async () => { const res = await callObsidian("note/get", { file: "note/get/invalid.md" }); expect(res.ok).toBe(false); if (!res.ok) { expect(res.error.errorCode).toBe("404"); } }, 10000); it("should return note content for periodic notes", async () => { for (const p of periodicNotes) { const res = await callObsidian( "note/get", { "periodic-note": p.key, silent: true }, ); expect(res.ok).toBe(true); if (res.ok) { expect(res.value["result-filepath"]).toContain(p.dateString); } } }); it("should return note content for recent periodic notes", async () => { expect(global.testVault.path).toBeDefined(); const vaultPath = global.testVault.path!; // Gather the list of current periodic notes (these are created during vault // launch), so we can move them out of the way in order to test the lookup // of recent periodic notes. const renames = periodicNotes.map((p) => { const oldName = path.join(vaultPath, `${p.dateString}.md`); return [oldName, oldName + ".bak"]; }); // Rename the current periodic notes for this test. for (const [oldName, newName] of renames) { try { await fs.rename(oldName, newName); } catch (e) {} } // Give Obsidian a moment to index the changes await pause(1000); try { for (const p of recentPeriodicNotes) { const res = await callObsidian( "note/get", { "periodic-note": p.key, silent: true }, ); expect(res.ok).toBe(true); if (res.ok) { expect(res.value["result-filepath"]).toContain(p.dateString); } } } finally { // Change the current periodic notes back to their original names. for (const [oldName, newName] of renames) { try { await fs.rename(newName, oldName); } catch (e) {} } } }); }); ================================================ FILE: tests/plugin-test-vault.original/note/get-active/noteGetActive.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/get-active", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/get-first-named/noteGetFirstNamed.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/get-first-named", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/list/noteList.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/list", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/open/note-1.md ================================================ ================================================ FILE: tests/plugin-test-vault.original/note/open/note-2.md ================================================ --- uid: 01JVM672TZYJ134Z74M2HY8GNC --- ================================================ FILE: tests/plugin-test-vault.original/note/open/noteOpen.test.ts ================================================ import * as fs from "fs/promises"; import * as path from "path"; import { callObsidian, pause } from "#tests/helpers"; import { periodicNotes, recentPeriodicNotes } from "#tests/periodic-notes"; describe("note/open", () => { it("should open a note by its file name", async () => { const res = await callObsidian( "note/open", { file: "note/open/note-1.md" }, ); expect(res.ok).toBe(true); }); it("should open a note by its UID", async () => { const res = await callObsidian( "note/open", { uid: "01JVM672TZYJ134Z74M2HY8GNC" }, ); expect(res.ok).toBe(true); }); it("should open periodic notes", async () => { for (const p of periodicNotes) { const res = await callObsidian("note/open", { "periodic-note": p.key }); expect(res.ok).toBe(true); } }); it("should open recent periodic notes", async () => { expect(global.testVault.path).toBeDefined(); const vaultPath = global.testVault.path!; // Gather the list of current periodic notes (these are created during vault // launch), so we can move them out of the way in order to test the lookup // of recent periodic notes. const renames = periodicNotes.map((p) => { const oldName = path.join(vaultPath, `${p.dateString}.md`); return [oldName, oldName + ".bak"]; }); // Rename the current periodic notes for this test. for (const [oldName, newName] of renames) { try { await fs.rename(oldName, newName); } catch (e) {} } // Give Obsidian a moment to index the changes await pause(1000); try { for (const p of recentPeriodicNotes) { const res = await callObsidian("note/open", { "periodic-note": p.key }); expect(res.ok).toBe(true); } } finally { // Change the current periodic notes back to their original names. for (const [oldName, newName] of renames) { try { await fs.rename(newName, oldName); } catch (e) {} } } }); it("should return an error when the requested note doesn't exist", async () => { const res1 = await callObsidian("note/open", { uid: "unknown-id" }); expect(res1.ok).toBe(false); if (!res1.ok) { expect(res1.error.errorCode).toBe("404"); } const res2 = await callObsidian("note/open", { file: "missing-note.md" }); expect(res2.ok).toBe(false); }); }); ================================================ FILE: tests/plugin-test-vault.original/note/prepend/notePrepend.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/prepend", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/rename/noteRename.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/rename", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/search-regex-and-replace/noteSearchRegexAndReplace.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/search-regex-and-replace", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/search-string-and-replace/noteSearchStringAndReplace.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/search-string-and-replace", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/touch/noteTouch.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/touch", () => { it.todo("needs testing"); }); ================================================ FILE: tests/plugin-test-vault.original/note/trash/noteTrash.test.ts ================================================ import { callObsidian } from "#tests/helpers"; describe("note/trash", () => { it.todo("needs testing"); }); ================================================ FILE: tests/types.d.ts ================================================ import { CallbackServer } from "./callback-server"; import { FSWatcher } from "chokidar"; type TestVaultConfig = { logPath: string; logRows: string[]; logWatcher: FSWatcher; name: string; path: string; }; // Declare global variable declare global { var httpServer: CallbackServer; var testVault: TestVaultConfig; } export type CallbackData = { success?: any; error?: any; }; export type LogEntry = Record; export type Result = | { ok: true; value: T; log?: LogEntry[] } | { ok: false; error: E; log?: LogEntry[] }; export type PeriodicNoteSet = { /** * The key used to identify the periodic note, e.g. "daily", "weekly", etc. */ key: | "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; /** * The date format used in the periodic note, e.g. "YYYY-MM-DD" or "gggg-[W]ww". */ dateString: string; }; export type RecentPeriodicNoteSet = { /** * The key used to identify the periodic note, e.g. "daily", "weekly", etc. */ key: | "recent-daily" | "recent-weekly" | "recent-monthly" | "recent-quarterly" | "recent-yearly"; /** * A fixed date string used in the periodic note, e.g. "2025-05-19" or "2025-W20". */ dateString: string; }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "allowJs": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "baseUrl": ".", "forceConsistentCasingInFileNames": true, "importHelpers": true, "isolatedModules": true, "lib": [ "DOM", "ES2022", ], "module": "ESNext", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noImplicitReturns": true, "noUnusedLocals": true, "strict": true, "target": "ES2022" }, "include": [ "src/**/*.ts" ] } ================================================ FILE: versions.json ================================================ { "0.9.0": "0.15.0", "0.10.0": "0.15.0", "0.10.1": "0.15.0", "0.10.2": "0.15.0", "0.10.3": "0.15.0", "0.10.4": "0.15.0", "0.10.5": "0.15.0", "0.10.6": "0.15.0", "0.11.0": "1.0.0", "0.12.0": "1.0.0", "0.12.1": "1.0.0", "0.13.0": "1.0.0", "0.14.0": "1.0.0", "0.14.1": "1.0.0", "0.14.2": "1.0.0", "0.15.0": "1.0.0", "0.16.0": "1.1.0", "0.16.1": "1.1.0", "0.16.2": "1.1.0", "0.16.3": "1.1.0", "0.16.4": "1.1.0", "0.17.0": "1.1.0", "0.18.0": "1.1.0", "1.1.0": "1.1.0", "1.1.1": "1.2.0", "1.1.2": "1.2.0", "1.2.0": "1.2.0", "1.2.1": "1.2.0", "1.2.2": "1.2.0", "1.2.3": "1.2.0", "1.2.4": "1.2.0", "1.2.5": "1.3.0", "1.3.0": "1.3.0", "1.3.1": "1.3.0", "1.4.0": "1.4.0", "1.4.1": "1.4.0", "1.4.2": "1.4.0", "1.5.0": "1.4.0", "1.5.1": "1.4.0", "1.5.2": "1.5.0", "1.6.x": "1.5.0", "1.6.0": "1.5.0", "1.6.1": "1.5.0", "1.6.2": "1.5.0", "1.6.3": "1.5.0", "1.6.4": "1.5.0", "1.6.5": "1.5.11", "1.7.0": "1.5.11", "1.7.1": "1.5.11", "post-create-pause": "1.5.11", "post-create-pause.1": "1.5.11", "1.7.2": "1.5.11", "1.7.3": "1.5.11", "1.8.0": "1.8.0", "1.8.1": "1.8.0", "1.8.2": "1.8.0", "1.8.3": "1.8.0", "1.8.4": "1.8.0" }