├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── release.yml ├── .gitignore ├── .husky └── pre-push ├── .npmrc ├── LICENSE ├── README.md ├── bun-fix.d.ts ├── bun.lockb ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── release.sh ├── src ├── createNotice.ts ├── extractTextFromPattern.ts ├── getNewTextFromResults.ts ├── main.ts ├── typings │ ├── obsidian-ex.d.ts │ ├── prettify.d.ts │ └── templater.d.ts ├── ui │ └── settingsTab.ts └── utils │ ├── deepInclude.ts │ ├── evalFromExpression.ts │ ├── ignore-types.ts │ ├── mdast.ts │ ├── obsidian.ts │ ├── regex.ts │ ├── strings.ts │ └── yaml.ts ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hananoshikayomaru] 2 | custom: https://www.buymeacoffee.com/yomaru 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | what is your OS? did you try to narrow the scope of issue? Any other context? 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: oven-sh/setup-bun@v1 16 | with: 17 | bun-version: latest 18 | 19 | - name: Build plugin 20 | run: | 21 | bun install 22 | bun run build 23 | 24 | - name: Create release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | tag="${GITHUB_REF#refs/tags/}" 29 | 30 | gh release create "$tag" \ 31 | --title="$tag" \ 32 | --draft \ 33 | main.js manifest.json 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | obsidian-dataview.d.ts 25 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun tsc -noEmit -skipLibCheck 5 | bun run build 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yeung Man Lung Ken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Run 2 | 3 | Generate markdown from dataview query and javascript. 4 | 5 | - ✅ Powerful, Dead Simple 6 | - ✅ Markdown based, work with every markdown editor / render 7 | - ✅ Works well with Dataview, Templater, Obsidian publish 8 | 9 | demo: 10 | 11 | ## Installation 12 | 13 | ### Plugin marketplace 14 | 15 | You can download this from obsidian plugin store. 16 | 17 | ### Manual Installation 18 | 19 | 1. cd to `.obsidian/plugins` 20 | 2. git clone this repo 21 | 3. `cd obsidian-run && bun install && bun run build` 22 | 4. there you go 🎉 23 | 24 | ## Usage 25 | 26 | 1. install this plugin and install [obsidian-custom-save](https://github.com/HananoshikaYomaru/obsidian-custom-save) 27 | 2. add the `run: run file` command to the custom save actions 28 | 3. define a starting tag 29 | 30 | ```md 31 | %% run start 3+4%% 32 | ``` 33 | 34 | 2. save the file 35 | 3. you markdown will become something like this 36 | 37 | ```md 38 | %% run start 39 | 3+4 40 | %% 41 | 7 42 | %% run end %% 43 | ``` 44 | 45 | ### Syntax 46 | 47 | Each block of run contains three parts: starting tag (required), generated content, ending tag 48 | 49 | #### starting tag (required) 50 | 51 | you define your expression in the starting tag. The expression will be used to calculate the content. It is the only required part for a run block. 52 | 53 | ```md 54 | %% run start %% 55 | ``` 56 | 57 | or you can also write multiple line statements. Notice that if you write in multiple line you must return a value. 58 | 59 | ````md 60 | %% run start 61 | 62 | ```ts|js 63 | 64 | ``` 65 | 66 | %% 67 | ```` 68 | 69 | You can use it with [CodeblockCustomizer](https://github.com/mugiwara85/CodeblockCustomizer), to have folding codeblock. 70 | 71 | ![](https://user-images.githubusercontent.com/43137033/272329457-d278a370-63d6-4dc2-a3f4-68767745ac92.png) 72 | 73 | #### Content 74 | 75 | the generated content 76 | 77 | #### Ending Tag 78 | 79 | ending tag closes the run block. 80 | 81 | ```md 82 | %% run end %% 83 | ``` 84 | 85 | ## Options 86 | 87 | 1. generate ending tag metadata: when enabled, the run block update time and error(if any) will be shown in the ending tag. 88 | 2. ignore folders: the folder listed will be ignored by this plugin 89 | 90 | ## Advanced Usage 91 | 92 | ### Access file object 93 | 94 | ```md 95 | %% run start file.basename %% 96 | ``` 97 | 98 | the file object is the [TFile](https://docs.obsidian.md/Reference/TypeScript+API/TFile/TFile) but it is patched with `file.properties` which is the file yaml properties. 99 | 100 | ### Page level variable 101 | 102 | ```md 103 | --- 104 | bar: "foo" 105 | --- 106 | 107 | %% run start file.properties.bar %% 108 | ``` 109 | 110 | ### Dataview 111 | 112 | you can access the `dv` object if you have dataview plugin installed and enabled. 113 | 114 | ````md 115 | %% run start 116 | 117 | ```ts 118 | return dv.markdownList(dv.pages("#ai/image").map((page) => page.file.link)); 119 | ``` 120 | 121 | %% 122 | ```` 123 | 124 | ### Templater and Reusable user scripts 125 | 126 | you can access the `tp` object if you have templater plugin installed and enabled. 127 | 128 | > Then you need to go to the setting of template and manually set a startup template. The reason of doing this is that `tp` is not initialized by default by templater and it will be undefined. Learn more and see a video: . If you don't want to set a start up template, you can manually run templater once everytime you start up obsidian. As long as templater runs once, the `tp` object will be defined. 129 | 130 | ![](https://share.cleanshot.com/qwTYFCby+) 131 | 132 | Templater allows user to have their user defined functions and scripts. To learn more, checkout . 133 | 134 | ### Function 135 | 136 | you can write complicated function in starting tag codeblock 137 | 138 | ### Async Function 139 | 140 | You can do any kind of async operation in the run block. Async function is non-blocking. Results will be resolved after all sync operation are resolved. You can use the obsidian [request](https://docs.obsidian.md/Reference/TypeScript+API/request) function to fetch data. 141 | 142 | ![](https://share.cleanshot.com/83hQltDB+) 143 | 144 | ### Debug 145 | 146 | you can use `console.log` in the starting tag codeblock. It will output in the developer tool. 147 | 148 | ## Note 149 | 150 | 1. if you want to contribute, please star and open github issue. 151 | 2. this plugin is powerful, but it is still under early development. The syntax is subject to change but will be backward compatible as much as possible 152 | 3. you will want to use this with [CodeblockCustomizer](https://github.com/mugiwara85/CodeblockCustomizer) to collapse your code block. 153 | 4. you will want to save the following codeblock as template so that you can use it easily. 154 | 155 | ````md 156 | %% run start 157 | 158 | ```ts fold 159 | return; 160 | ``` 161 | 162 | %% 163 | ```` 164 | 165 | ## Support 166 | 167 | If you are enjoying this plugin then please support my work and enthusiasm by buying me a coffee on . 168 | 169 | Buy Me A Coffee 170 | -------------------------------------------------------------------------------- /bun-fix.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HananoshikaYomaru/obsidian-run/674023e072b5698d475a878879632e170db499bd/bun.lockb -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ["src/main.ts"], 18 | bundle: true, 19 | external: [ 20 | "obsidian", 21 | "electron", 22 | "@codemirror/autocomplete", 23 | "@codemirror/collab", 24 | "@codemirror/commands", 25 | "@codemirror/language", 26 | "@codemirror/lint", 27 | "@codemirror/search", 28 | "@codemirror/state", 29 | "@codemirror/view", 30 | "@lezer/common", 31 | "@lezer/highlight", 32 | "@lezer/lr", 33 | ...builtins, 34 | ], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "run", 3 | "name": "Run", 4 | "version": "1.0.8", 5 | "minAppVersion": "0.15.0", 6 | "description": "Generate markdown from dataview query and javascript", 7 | "author": "Hananoshika Yomaru", 8 | "authorUrl": "https://yomaru.dev", 9 | "fundingUrl": { 10 | "buymeacoffee": "https://www.buymeacoffee.com/yomaru", 11 | "Github Sponsor": "https://github.com/sponsors/HananoshikaYomaru" 12 | }, 13 | "isDesktopOnly": false 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-run", 3 | "version": "1.0.8", 4 | "description": "Generate markdown from dataview query and javascript", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "bun esbuild.config.mjs", 8 | "build": "bun esbuild.config.mjs production", 9 | "version": "bun version-bump.mjs && git add manifest.json versions.json", 10 | "prepare": "husky install", 11 | "release": "bash ./release.sh", 12 | "typecheck": "tsc -noEmit -skipLibCheck" 13 | }, 14 | "keywords": [ 15 | "obsidian", 16 | "plugin", 17 | "dataview", 18 | "markdown", 19 | "generator", 20 | "javascript" 21 | ], 22 | "author": "Hananoshika Yomaru", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/diff-match-patch": "^1.0.34", 26 | "@types/js-yaml": "^4.0.6", 27 | "@types/node": "^16.11.6", 28 | "@typescript-eslint/eslint-plugin": "5.29.0", 29 | "@typescript-eslint/parser": "5.29.0", 30 | "builtin-modules": "3.3.0", 31 | "bun-types": "^1.0.3", 32 | "esbuild": "0.17.3", 33 | "husky": "^8.0.3", 34 | "obsidian": "latest", 35 | "tslib": "2.4.0", 36 | "typescript": "^5.0.5" 37 | }, 38 | "dependencies": { 39 | "@babel/parser": "^7.23.0", 40 | "@total-typescript/ts-reset": "^0.5.1", 41 | "date-fns": "^2.30.0", 42 | "diff-match-patch": "^1.0.5", 43 | "js-yaml": "^4.1.0", 44 | "mdast-util-from-markdown": "^1.2.0", 45 | "mdast-util-gfm-footnote": "^1.0.1", 46 | "mdast-util-gfm-task-list-item": "^1.0.1", 47 | "mdast-util-math": "^2.0.1", 48 | "micromark-extension-gfm-footnote": "^1.0.4", 49 | "micromark-extension-gfm-task-list-item": "^1.0.3", 50 | "micromark-extension-math": "^2.0.2", 51 | "micromark-util-combine-extensions": "^1.0.0", 52 | "obsidian-dataview": "^0.5.59", 53 | "quick-lru": "^6.1.1", 54 | "recast": "^0.23.4", 55 | "ts-dedent": "^2.2.0", 56 | "unist-util-visit": "^4.1.2", 57 | "zod": "^3.22.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the default update type 4 | UPDATE_TYPE="patch" 5 | 6 | # Parse command-line arguments 7 | while [[ $# -gt 0 ]]; do 8 | key="$1" 9 | case $key in 10 | -m | --minor) 11 | UPDATE_TYPE="minor" 12 | shift 13 | ;; 14 | -M | --major) 15 | UPDATE_TYPE="major" 16 | shift 17 | ;; 18 | *) 19 | echo "Unknown option: $key" 20 | exit 1 21 | ;; 22 | esac 23 | done 24 | 25 | # Get the version number from manifest.json 26 | MANIFEST_VERSION=$(jq -r '.version' manifest.json) 27 | 28 | # Get the version number from package.json 29 | PACKAGE_VERSION=$(node -p -e "require('./package.json').version") 30 | 31 | # Ensure the version from package.json matches the version in manifest.json 32 | if [ "$PACKAGE_VERSION" != "$MANIFEST_VERSION" ]; then 33 | echo "Version mismatch between package.json and manifest.json" 34 | exit 1 35 | fi 36 | 37 | # Increment the version based on the specified update type 38 | if [ "$UPDATE_TYPE" = "minor" ]; then 39 | NEW_VERSION=$(semver $PACKAGE_VERSION -i minor) 40 | elif [ "$UPDATE_TYPE" = "major" ]; then 41 | NEW_VERSION=$(semver $PACKAGE_VERSION -i major) 42 | else 43 | NEW_VERSION=$(semver $PACKAGE_VERSION -i patch) 44 | fi 45 | 46 | echo "Current version: $PACKAGE_VERSION" 47 | echo "New version: $NEW_VERSION" 48 | 49 | # Update the version in package.json 50 | jq --arg version "$NEW_VERSION" '.version = $version' package.json >tmp.json && mv tmp.json package.json 51 | echo "Changed package.json version to $NEW_VERSION" 52 | 53 | # Print the updated version of manifest.json using 'bun' 54 | bun run version 55 | echo "Updated version of manifest using bun. The current version of manifest.json is $(jq -r '.version' manifest.json)" 56 | 57 | # Create a git commit and tag 58 | git add . && git commit -m "release: $NEW_VERSION" 59 | git tag -a "$NEW_VERSION" -m "release: $NEW_VERSION" 60 | echo "Created tag $NEW_VERSION" 61 | 62 | # Push the commit and tag to the remote repository 63 | git push origin "$NEW_VERSION" 64 | echo "Pushed tag $NEW_VERSION to the origin branch $NEW_VERSION" 65 | git push 66 | echo "Pushed to the origin master branch" 67 | -------------------------------------------------------------------------------- /src/createNotice.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | import dedent from "ts-dedent"; 3 | 4 | export function createNotice( 5 | message: string, 6 | color: "white" | "yellow" | "red" = "white", 7 | duration?: number 8 | ) { 9 | const fragment = new DocumentFragment(); 10 | const desc = document.createElement("div"); 11 | desc.setText(dedent`Obsidian Run: 12 | ${message}`); 13 | desc.style.color = color; 14 | fragment.appendChild(desc); 15 | 16 | new Notice(fragment, duration ?? color === "red" ? 10000 : undefined); 17 | } 18 | -------------------------------------------------------------------------------- /src/extractTextFromPattern.ts: -------------------------------------------------------------------------------- 1 | import { Code } from "mdast-util-from-markdown/lib"; 2 | import { Prettify } from "./typings/prettify"; 3 | import { parseTextToAST } from "./utils/mdast"; 4 | import { mapStringToKeyValuePairs } from "./utils/strings"; 5 | 6 | const startPattern = /%%\s*run start\s*([\s\S]*?)%%/g; 7 | const codeBlockPattern = /(```|~~~)([\s\S]*?)\n([\s\S]*?)\n(```|~~~)/; 8 | const endPattern = /%%\s*run end\s*([\s\S]*?)%%/g; 9 | 10 | export const extractCode = (text: string) => { 11 | const matches = new RegExp(codeBlockPattern).exec(text); 12 | if (matches) { 13 | const ast = parseTextToAST(text); 14 | const code = ast.children.find((c) => c.type === "code"); 15 | if (code?.type === "code") 16 | return { 17 | meta: code.meta, 18 | languageCode: code.lang ?? "js", 19 | code: code.value, 20 | }; 21 | } 22 | return undefined; 23 | }; 24 | 25 | /** 26 | * @description 27 | * if the first word is a number, then it is an id. 28 | * remove the id and return the rest of the text 29 | * if the word is not a number, then simply return the text 30 | * 31 | * @example 32 | * "1 dv.pages().length" -> "dv.pages().length" 33 | * "1" -> "1" 34 | * "1 3+3" -> "3+3" 35 | * 36 | * @link 37 | * https://www.typescriptlang.org/play?target=99#code/MYewdgzgLgBApgDygJwIbCgSQCYwLwwAUUiUAXDNMgJZgDmAlPgHwwDeAUAJCiSwC2qKMAAWcCPhgkkAOkHCRhAPQA9QgB1sAagYaIOwgG11EdQGUAugCoGAEiUMA3N2oAzIvNHiYAMh8xPMQgZABs4eigRfDwCABYmTi4uZDgoAFdkMHZuJOpsCgAHVGQIOEwwKEJA8UMARgsAQgYAGhyuaXIAoS8IQwBmRtakgF9nLmHuFPTM7Nz8mDSwbDhXWjhsIfbSIdGOXY5eCBAw0JA6QngkNAwcQgAiWphsADcZIrpxQgZQ8LpIu6YDAO4COJxCZwupGuWGw91qAJgQMOxzgp3OlxQ6BhcJgfS0fQRSJBKLRkKuWNuDyer3en2+YQiUS0uJghKAA 38 | */ 39 | export const extractId = (text: string) => { 40 | const matches = text.match(/^(\d+)(\s+)([\s\S]*)$/); 41 | if (matches && matches.length === 4) { 42 | return { 43 | id: parseInt(matches[1]!), 44 | text: matches[3]!, 45 | }; 46 | } 47 | return { 48 | id: undefined, 49 | text, 50 | }; 51 | }; 52 | 53 | export type SectionBaseData = { 54 | /** 55 | * the id of this section 56 | */ 57 | id: number; 58 | /** 59 | * the starting tag of this section 60 | */ 61 | startingTag: string; 62 | /** 63 | * if the starting tag is a codeblock, this will be an object with the language code and the code 64 | */ 65 | codeBlock?: { 66 | languageCode: string; 67 | code: string; 68 | }; 69 | /** 70 | * the starting tag match 71 | */ 72 | startMatch: RegExpMatchArray; 73 | /** 74 | * the text of the whole section 75 | */ 76 | text: string; 77 | }; 78 | 79 | export type Section = Prettify< 80 | SectionBaseData & 81 | ( 82 | | { 83 | /** 84 | * the content of this section 85 | */ 86 | content: string; 87 | /** 88 | * the ending tag of this section 89 | */ 90 | endingTag: string; 91 | /** 92 | * the ending tag as an object 93 | */ 94 | endingObject: { 95 | [x: string]: string; 96 | }; 97 | /** 98 | * the ending tag match 99 | */ 100 | endMatch: RegExpExecArray; 101 | } 102 | | {} 103 | ) 104 | >; 105 | 106 | // https://www.typescriptlang.org/play?target=99#code/FA4MwVwOwYwFwJYHsoAICmAPOAnAhvAMrrzJQDOAYjkgLYAKecc6OUAFC9gFyrm4IoAcwCUvAN584eHImEAVPEN78cgoQBpUMFCyhwVA4agA+qaABN0YQegtb0UC+sXKpa42cvXbF1AF8AbQBdVHFgVG0Ufj4SRGiJKRk5IVdDD00o-UcDd3VTcycfKDsHJxcldPyvIpsSvyDQgF5UEIBuEEidCjgk2UZmVjQWgHoAUjHUCwA3AFohRz64AB1yACp2QNXlwmC1gH4RCZGhDq7o3rwAGyuAWSYYAAt0clQmrjgAOloHx4BBG7sJYDFhsEQRLIxFgxFp-HD4ACenzANFoQOud1+L3BnVQYCQOFQQKu6F6CDeqAADKg2qhyQAeVDQr4k4RwR40ukAai5qBEYVQwAAkN0Yvxkvc4E8KczAghgsLRb0SthJdKWrLybyAIwKiHnHpLCpCCni2Rqx6BXWfARokQdfWQ3qOCwgoYU8aTGbzRYu1YbLbkHZ7Q7HU6Ol1utifK54fgASSKmFN0nNv0+gisyf2+ypqF5ZrgFsClOCMccQnZZ0iTowTgtFMjTFBUE+WBInCwcHtwpGI0hSBJMaQQiBTcG0djCaTqC0hYtGaTOJrdLARJdDYAhE0WlAIDdUAAyQ91iwLzNYVCMlVF377RdZ-nhUWD9DD0eni33i+YLQ389Jlo4IrgaMQusajb1r8Vplra7A9iBtbdHovQal2nzkFcCAwOg7DzumP41gWqa3lKlqluWbKPGUZ4EUuNpqHaDqIeQcRkOQnwAA4QOQjzsOEiGRIWxoaI6K7ITkvASfopg1FYdSlGJNbgQolSnhBJhycUdhKf4CE1v4GBXKxYRKX2qDxmu7KsOgdKvFASDqcY0iZLg6BMEyzxZChqB4E4Tkmi5vmvN4CkWEprGkNEXE8XxAmCcJqmaEpBoobwoW+KJgkBWkhTyb4un6ZE-gQiVuIQjgpIQGwsRRRQHRlcASpMl2FIAAbAAAxHgCAjAgPwLKg3UAEYjDAY3AAA7qw03TSAEyoAsJT4CwTJKK8haoBMXV4MNQ27WN+0IMAC1LawTC2S5rwultYzzV6cxLUst33VMj2LJtIAAMxcgALPdwAAOwnQ9PpoDdwBTr0ECcRYF28AATJSCNfbM2qUrMlLavICMI9wlIAKzcAjv2fAAnAjABsABaIOgK93pPZ9wA-f9dMg29YPPd9f0A8DC2M76-mQ3G0Ow-DqBIyjaMY1jON44TxOkxTNPswz71oJtNgksAXEyDkER6+yL3AB1TUXKglXkPuqEYNg+BEGx0TUHQUYcC12D8ubFCvu+7BWzbWgB1cLIVsbOLNeQeC0JxJIAEovDbFKBMKkjCkKiWpEoqC8AARKzueiUKIq6DkOeoLngO57OgrFypWduLnUPmOLLCI8jqPo5j2O4-jRMk+TVPU7nwr+EXafF5nrjl-nf3V1o6fSb06W1L4Nfp-X08r-l9Tr0KY+1xPGckRBecF0XJfZDJedV3vQqb2pTeiy3cNt5LHcy938t90rg80yP+8tBHyno-bW6Blitk4vrfQEDUBG0eIXWul80p5W0n4BeddyhJW3mg2uJUFRAA 107 | export function extractSectionsFromPattern(text: string) { 108 | const sections: Section[] = []; 109 | const sectionSummary: { 110 | [startingTag: string]: number; 111 | } = {}; 112 | const allMatches = Array.from(text.matchAll(new RegExp(startPattern))); 113 | 114 | for (let i = 0; i < allMatches.length; i++) { 115 | const startMatch = allMatches[i]!; 116 | const nextMatch = allMatches[i + 1]; 117 | const { text: startingTag } = extractId(startMatch[1]?.trim() ?? ""); 118 | 119 | // if there is the same starting tag, id will be updated 120 | const id = (sectionSummary[startingTag] = 121 | (sectionSummary[startingTag] ?? 0) + 1); 122 | 123 | const codeBlock = extractCode(startingTag); 124 | 125 | const _endPattern = new RegExp(endPattern); 126 | _endPattern.lastIndex = startMatch.index ?? 0 + startMatch[0]?.length; 127 | const endMatch = _endPattern.exec(text); 128 | 129 | // check if there is an ending tag 130 | // it only has ending tag if the ending tag is before the next starting tag 131 | if ( 132 | endMatch !== null && 133 | (nextMatch === undefined || endMatch.index! < nextMatch.index!) 134 | ) { 135 | const endingTag = endMatch[1]?.trim() ?? ""; 136 | const content = text 137 | .slice(startMatch.index! + startMatch[0].length, endMatch.index) 138 | .trim(); 139 | 140 | sections.push({ 141 | startingTag, 142 | id, 143 | codeBlock, 144 | content: content, 145 | endingTag: endingTag, 146 | endingObject: mapStringToKeyValuePairs(endingTag), 147 | startMatch, 148 | endMatch, 149 | text: text.substring( 150 | startMatch.index!, 151 | endMatch.index! + endMatch[0].length 152 | ), 153 | }); 154 | } else { 155 | // If there is no ending tag, treat the content and ending tag as undefined 156 | sections.push({ 157 | id, 158 | startingTag, 159 | startMatch, 160 | codeBlock, 161 | text: text.substring( 162 | startMatch.index!, 163 | startMatch.index! + startMatch[0].length 164 | ), 165 | }); 166 | } 167 | } 168 | return { sections, sectionSummary }; 169 | } 170 | 171 | export type extractSectionsResult = ReturnType< 172 | typeof extractSectionsFromPattern 173 | >; 174 | -------------------------------------------------------------------------------- /src/getNewTextFromResults.ts: -------------------------------------------------------------------------------- 1 | import { Data } from "./utils/obsidian"; 2 | import { Section, extractSectionsResult } from "./extractTextFromPattern"; 3 | import { EvalResult, Primitive } from "./utils/evalFromExpression"; 4 | import dedent from "ts-dedent"; 5 | import { format } from "date-fns"; 6 | import { replaceOccurance } from "./utils/strings"; 7 | 8 | const getEndingTag = ( 9 | generateEndingTagMetadata: boolean, 10 | section: Section, 11 | errorMessage?: string 12 | ) => { 13 | if (errorMessage) 14 | return dedent( 15 | generateEndingTagMetadata 16 | ? dedent` 17 | %% run end 18 | ${_getEndingTag("endingTag" in section ? section.endingObject : {}, { 19 | error: errorMessage, 20 | })} 21 | %% 22 | ` 23 | : `%% run end %%` 24 | ); 25 | 26 | return dedent( 27 | generateEndingTagMetadata 28 | ? dedent` 29 | %% run end 30 | ${_getEndingTag( 31 | {}, 32 | { 33 | "last update": format(new Date(), "yyyy-MM-dd HH:mm:ss"), 34 | } 35 | )} 36 | %%` 37 | : `%% run end %%` 38 | ); 39 | }; 40 | 41 | const _getEndingTag = ( 42 | endingObject: { [x: string]: string }, 43 | newObject: { [x: string]: string } 44 | ) => { 45 | const test = { 46 | ...endingObject, 47 | ...newObject, 48 | }; 49 | 50 | // convert the object to string 51 | const string = Object.entries(test) 52 | .map(([key, value]) => `${key}: ${value}`) 53 | .join("\n"); 54 | return string; 55 | }; 56 | 57 | export function getNewTextFromResults( 58 | data: Data, 59 | results: EvalResult[], 60 | s: extractSectionsResult, 61 | options?: { 62 | generateEndingTagMetadata?: boolean; 63 | } 64 | ) { 65 | let resultedText = data.text; 66 | 67 | const remainingPromises: { 68 | section: Section; 69 | promise: Promise; 70 | }[] = []; 71 | const errors: { section: Section; message: string }[] = []; 72 | 73 | for (let i = 0; i < results.length; i++) { 74 | const result = results[i]!; 75 | const section = s.sections[i]!; 76 | // for each success result, remove all the texts to corresponding end tag and replace it with the result 77 | if (result.success) { 78 | const content = 79 | result.result instanceof Promise ? "Loading..." : result.result; 80 | 81 | const newSectionText = dedent` 82 | %% run start 83 | ${section.startingTag} 84 | %% 85 | ${content} 86 | ${getEndingTag(Boolean(options?.generateEndingTagMetadata), section)} 87 | `; 88 | 89 | resultedText = replaceOccurance( 90 | resultedText, 91 | section.text, 92 | newSectionText, 93 | section.id 94 | ); 95 | 96 | if (result.result instanceof Promise) { 97 | remainingPromises.push({ 98 | section: { 99 | ...section, 100 | text: newSectionText, 101 | }, 102 | promise: result.result, 103 | }); 104 | } 105 | } 106 | 107 | // for each failed result, don't change anything between the start and end tag, but update the ending tag meta to include the error message 108 | else { 109 | console.error(result.error.message); 110 | const newSectionText = dedent` 111 | %% run start 112 | ${section.startingTag} 113 | %% 114 | ${"content" in section ? section.content : ""} 115 | ${getEndingTag( 116 | Boolean(options?.generateEndingTagMetadata), 117 | section, 118 | result.error.message 119 | )} 120 | `; 121 | resultedText = replaceOccurance( 122 | resultedText, 123 | section.text, 124 | newSectionText, 125 | section.id 126 | ); 127 | errors.push({ 128 | section, 129 | message: result.error.message, 130 | }); 131 | } 132 | } 133 | 134 | return { resultedText, remainingPromises, errors }; 135 | } 136 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Editor, MarkdownView, Plugin, TFile } from "obsidian"; 2 | import { getAPI } from "obsidian-dataview"; 3 | import { 4 | Data, 5 | getDataFromTextSync, 6 | isMarkdownFile, 7 | writeFile, 8 | } from "./utils/obsidian"; 9 | import { extractSectionsFromPattern } from "./extractTextFromPattern"; 10 | import { evalFromExpression } from "./utils/evalFromExpression"; 11 | import { getNewTextFromResults } from "./getNewTextFromResults"; 12 | import dedent from "ts-dedent"; 13 | import { createNotice } from "./createNotice"; 14 | import { SettingTab } from "./ui/settingsTab"; 15 | import { Templater } from "./typings/templater"; 16 | 17 | enum YamlKey { 18 | IGNORE = "run-ignore", 19 | } 20 | 21 | const isIgnoredByFolder = (settings: RunPluginSettings, file: TFile) => { 22 | return settings.ignoredFolders.includes(file.parent?.path as string); 23 | }; 24 | 25 | function isFileIgnored(settings: RunPluginSettings, file: TFile, data?: Data) { 26 | if (isIgnoredByFolder(settings, file)) return true; 27 | if (data) { 28 | if (data.yamlObj && data.yamlObj[YamlKey.IGNORE]) return true; 29 | } 30 | return false; 31 | } 32 | 33 | export type RunPluginSettings = { 34 | generateEndingTagMetadata: boolean; 35 | ignoredFolders: string[]; 36 | }; 37 | 38 | export const DEFAULT_SETTINGS: RunPluginSettings = { 39 | generateEndingTagMetadata: false, 40 | ignoredFolders: [], 41 | }; 42 | 43 | export default class RunPlugin extends Plugin { 44 | settings: RunPluginSettings; 45 | 46 | runFileSync(file: TFile, editor: Editor) { 47 | const data = getDataFromTextSync(editor.getValue()); 48 | // recognise the patterns 49 | 50 | const s = extractSectionsFromPattern(data.text); 51 | 52 | // if no sections, we can return early 53 | if (s.sections.length === 0) return; 54 | 55 | // eval all the expressions 56 | const context = { 57 | file: { 58 | ...file, 59 | properties: data.yamlObj, 60 | }, 61 | dv: getAPI(this.app), 62 | tp: ( 63 | this.app.plugins.plugins["templater-obsidian"] as 64 | | (Plugin & { 65 | templater: Templater; 66 | }) 67 | | undefined 68 | )?.templater.current_functions_object, 69 | }; 70 | 71 | const results = s.sections.map(({ startingTag, codeBlock }) => { 72 | return evalFromExpression( 73 | codeBlock ? codeBlock.code : startingTag, 74 | codeBlock ? true : false, 75 | context 76 | ); 77 | }); 78 | 79 | const { 80 | resultedText: newText, 81 | remainingPromises, 82 | errors, 83 | } = getNewTextFromResults(data, results, s, { 84 | generateEndingTagMetadata: this.settings.generateEndingTagMetadata, 85 | }); 86 | 87 | // if new text, write File 88 | if (newText !== data.text) { 89 | writeFile(editor, data.text, newText); 90 | } 91 | 92 | // for each remaining promise, update the text when it resolves 93 | remainingPromises.forEach(({ section, promise }) => { 94 | // the result should be a string 95 | promise 96 | .then((result) => { 97 | const data = getDataFromTextSync(editor.getValue()); 98 | const { resultedText } = getNewTextFromResults( 99 | data, 100 | [{ success: true, result }], 101 | { 102 | sections: [section], 103 | sectionSummary: s.sectionSummary, 104 | }, 105 | { 106 | generateEndingTagMetadata: 107 | this.settings.generateEndingTagMetadata, 108 | } 109 | ); 110 | writeFile(editor, data.text, resultedText); 111 | }) 112 | .catch((e) => { 113 | console.error(e); 114 | const { resultedText } = getNewTextFromResults( 115 | data, 116 | [ 117 | { 118 | success: false, 119 | error: { message: e.message, cause: e }, 120 | }, 121 | ], 122 | { 123 | sections: [section], 124 | sectionSummary: s.sectionSummary, 125 | }, 126 | { 127 | generateEndingTagMetadata: 128 | this.settings.generateEndingTagMetadata, 129 | } 130 | ); 131 | writeFile(editor, data.text, resultedText); 132 | createNotice( 133 | `Error when resolving run ${section.id}: ${e.message}`, 134 | "red" 135 | ); 136 | }); 137 | }); 138 | 139 | createNotice( 140 | dedent` 141 | Completed: ${ 142 | results.length - errors.length - remainingPromises.length 143 | } out of ${results.length} 144 | Promise: ${remainingPromises.length} 145 | Error: ${errors.length} 146 | `, 147 | errors.length > 0 ? "red" : "white" 148 | ); 149 | } 150 | 151 | async onload() { 152 | await this.loadSettings(); 153 | // this.registerEventsAndSaveCallback(); 154 | this.addCommand({ 155 | id: "run-file", 156 | name: "Run file", 157 | editorCheckCallback: this.runFile.bind(this), 158 | }); 159 | 160 | this.addSettingTab(new SettingTab(this.app, this)); 161 | } 162 | 163 | runFile(checking: boolean, editor: Editor, ctx: MarkdownView) { 164 | if (!ctx.file) return; 165 | if (checking) { 166 | return isMarkdownFile(ctx.file); 167 | } 168 | if (!editor) return; 169 | const data = getDataFromTextSync(editor.getValue()); 170 | if (isFileIgnored(this.settings, ctx.file, data)) return; 171 | this.runFileSync(ctx.file, editor); 172 | } 173 | 174 | async saveSettings() { 175 | await this.saveData(this.settings); 176 | } 177 | 178 | async loadSettings() { 179 | this.settings = Object.assign( 180 | {}, 181 | DEFAULT_SETTINGS, 182 | await this.loadData() 183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/typings/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | // this is copied from https://github.com/Fevol/obsidian-typings/blob/e1ff0b68a2d4e6d6ff42b23b5eb18f1e30a4054c/obsidian-ex.d.ts#L1040 2 | 3 | import { 4 | App, 5 | CachedMetadata, 6 | Command, 7 | Constructor, 8 | DataWriteOptions, 9 | EditorPosition, 10 | EditorRange, 11 | EditorSuggest, 12 | EventRef, 13 | Events, 14 | FileView, 15 | KeymapEventHandler, 16 | KeymapEventListener, 17 | KeymapInfo, 18 | Loc, 19 | Modifier, 20 | ObsidianProtocolHandler, 21 | OpenViewState, 22 | PaneType, 23 | Plugin, 24 | Reference, 25 | SplitDirection, 26 | TAbstractFile, 27 | TFile, 28 | TFolder, 29 | View, 30 | ViewState, 31 | WorkspaceLeaf, 32 | WorkspaceMobileDrawer, 33 | WorkspaceSidedock, 34 | WorkspaceSplit, 35 | WorkspaceTabs, 36 | WorkspaceWindow, 37 | WorkspaceWindowInitData, 38 | } from "obsidian"; 39 | import { EditorView } from "@codemirror/view"; 40 | import { EditorState, Extension } from "@codemirror/state"; 41 | 42 | /* eslint-disable @typescript-eslint/no-explicit-any */ 43 | 44 | interface Account { 45 | /** 46 | * The company associated with the activated commercial license 47 | */ 48 | company: string; 49 | /** 50 | * The email address associated with the account 51 | */ 52 | email: string; 53 | /** 54 | * 55 | */ 56 | expiry: number; 57 | /** 58 | * 59 | */ 60 | key: string | undefined; 61 | /** 62 | * 63 | */ 64 | keyValidation: string; 65 | /** 66 | * The license available to the account 67 | */ 68 | license: "" | "insider"; 69 | /** 70 | * Profile name 71 | */ 72 | name: string; 73 | /** 74 | * 75 | */ 76 | seats: number; 77 | /** 78 | * 79 | */ 80 | token: string; 81 | 82 | // TODO: Add Sync and Publish API functions here 83 | } 84 | 85 | // interface AppMenuBarManager { 86 | // /** 87 | // * Reference to App 88 | // */ 89 | // app: App; 90 | // 91 | // /** 92 | // * 93 | // */ 94 | // requestRender: () => void; 95 | // 96 | // /** 97 | // * 98 | // */ 99 | // requestUpdateViewState: () => void; 100 | // } 101 | 102 | interface Commands { 103 | /** 104 | * Reference to App 105 | */ 106 | app: App; 107 | 108 | /** 109 | * Commands *without* editor callback, will always be available in the command palette 110 | * @example `app:open-vault` or `app:reload` 111 | */ 112 | commands: Record; 113 | /** 114 | * Commands *with* editor callback, will only be available when editor is active and callback returns true 115 | * @example `editor:fold-all` or `command-palette:open` 116 | */ 117 | editorCommands: Record; 118 | /** 119 | * Add a command to the command registry 120 | * @param command Command to add 121 | */ 122 | addCommand: (command: Command) => void; 123 | /** 124 | * Execute a command by reference 125 | * @param command Command to execute 126 | */ 127 | executeCommand: (command: Command) => boolean; 128 | /** 129 | * Execute a command by ID 130 | * @param commandId ID of command to execute 131 | */ 132 | executeCommandById: (commandId: string) => boolean; 133 | /** 134 | * Find a command by ID 135 | * @param commandId 136 | */ 137 | findCommand: (commandId: string) => Command | undefined; 138 | /** 139 | * Lists **all** commands, both with and without editor callback 140 | */ 141 | listCommands: () => Command[]; 142 | /** 143 | * Remove a command from the command registry 144 | * @param commandId Command to remove 145 | */ 146 | removeCommand: (commandId: string) => void; 147 | } 148 | 149 | // interface ThemeRecord { 150 | // /** 151 | // * Author of the theme 152 | // */ 153 | // author: string; 154 | // /** 155 | // * @internal 156 | // */ 157 | // authorEl?: HTMLElement; 158 | // /** 159 | // * Amount of downloads the theme 160 | // */ 161 | // downloads: number; 162 | // /** 163 | // * @internal 164 | // */ 165 | // el?: HTMLElement; 166 | // matches: null; 167 | // modes: ['light', 'dark'] | ['light']; 168 | // name: string; 169 | // nameEl?: HTMLElement; 170 | // repo: string; 171 | // score: number; 172 | // screenshot: string; 173 | // updateIconEl?: HTMLElement; 174 | // /** 175 | // * Whether the theme was updated 176 | // */ 177 | // updated: number; 178 | // } 179 | 180 | interface ThemeManifest { 181 | /** 182 | * Name of the author of the theme 183 | */ 184 | author: string; 185 | /** 186 | * URL to the author's website 187 | */ 188 | authorUrl?: string; 189 | /** 190 | * Storage location of the theme relative to the vault root 191 | */ 192 | dir: string; 193 | /** 194 | * URL for funding the author 195 | */ 196 | fundingUrl?: string; 197 | /** 198 | * Minimum Obsidian version compatible with the theme 199 | */ 200 | minAppVersion: string; 201 | /** 202 | * Name of the theme 203 | */ 204 | name: string; 205 | /** 206 | * Version of the theme 207 | * @remark Defaults to "0.0.0" if no theme manifest was provided in the repository 208 | */ 209 | version: "0.0.0" | string; 210 | } 211 | 212 | interface CustomCSS extends Component { 213 | /** 214 | * Reference to App 215 | */ 216 | app: App; 217 | /** 218 | * @internal 219 | */ 220 | boundRaw: () => void; 221 | /** 222 | * @internal Cache of CSS snippet filepath (relative to vault root) to CSS snippet contents 223 | */ 224 | csscache: Map; 225 | /** 226 | * Set of enabled snippet, given by filenames 227 | */ 228 | enabledSnippets: Set; 229 | /** 230 | * @internal 231 | * Contains references to Style elements containing custom CSS snippets 232 | */ 233 | extraStyleEls: HTMLStyleElement[]; 234 | /** 235 | * List of theme names not fully updated to post v1.0.0 theme guidelines 236 | */ 237 | oldThemes: string[]; 238 | /** 239 | * @internal 240 | */ 241 | queue: WeakMap; 242 | /** 243 | * @internal 244 | */ 245 | requestLoadSnippets: () => void; 246 | /** 247 | * @internal 248 | */ 249 | requestLoadTheme: () => void; 250 | /** 251 | * @internal 252 | */ 253 | requestReadThemes: () => void; 254 | /** 255 | * List of snippets detected by Obsidian, given by their filenames 256 | */ 257 | snippets: string[]; 258 | /** 259 | * Currently active theme, given by its name 260 | * @remark "" is the default Obsidian theme 261 | */ 262 | theme: "" | string; 263 | /** 264 | * Mapping of theme names to their manifest 265 | */ 266 | themes: Record; 267 | /** 268 | * @internal 269 | */ 270 | updates: Record; 271 | 272 | /** 273 | * Check whether a specific theme can be updated 274 | * @param themeName - Name of the theme to check 275 | */ 276 | checkForUpdate: (themeName: string) => void; 277 | /** 278 | * Check all themes for updates 279 | */ 280 | checkForUpdates: () => void; 281 | /** 282 | * Disable translucency of application background 283 | */ 284 | disableTranslucency: () => void; 285 | /** 286 | * Fetch legacy theme CSS using the pre-v1.0.0 theme download pipeline 287 | * @returns string obsidian.css contents 288 | */ 289 | downloadLegacyTheme: ({ repo: string }) => Promise; 290 | /** 291 | * Enable translucency of application background 292 | */ 293 | enableTranslucency: () => void; 294 | /** 295 | * Fetch a theme's manifest using repository URL 296 | * @remark Do **not** include github prefix, only `username/repo` 297 | */ 298 | getManifest: (repoUrl: string) => Promise; 299 | /** 300 | * Convert snippet name to its corresponding filepath (relative to vault root) 301 | * @returns string `.obsidian/snippets/${snippetName}.css` 302 | */ 303 | getSnippetPath: (snippetName: string) => string; 304 | /** 305 | * Returns the folder path where snippets are stored (relative to vault root) 306 | */ 307 | getSnippetsFolder: () => string; 308 | /** 309 | * Returns the folder path where themes are stored (relative to vault root) 310 | */ 311 | getThemesFolder: () => string; 312 | /** 313 | * Convert theme name to its corresponding filepath (relative to vault root) 314 | * @returns string `.obsidian/themes/${themeName}/theme.css` 315 | */ 316 | getThemePath: (themeName: string) => string; 317 | /** 318 | * Returns whether there are themes that can be updated 319 | */ 320 | hasUpdates: () => boolean; 321 | /** 322 | * Install a legacy theme using the pre-v1.0.0 theme download pipeline
323 | * Will create a corresponding dummy manifest for the theme 324 | * @remark Name will be used as the folder name for the theme 325 | */ 326 | installLegacyTheme: ({ 327 | name: string, 328 | repo: string, 329 | author: string, 330 | }) => Promise; 331 | /** 332 | * Install a theme using the regular theme download pipeline 333 | */ 334 | installTheme: ( 335 | { name: string, repo: string, author: string }, 336 | version: string 337 | ) => Promise; 338 | /** 339 | * Check whether a specific theme is installed by theme name 340 | */ 341 | isThemeInstalled: (themeName: string) => boolean; 342 | /** 343 | * @internal 344 | */ 345 | onRaw: (e: any) => void; 346 | /** 347 | * @internal 348 | */ 349 | onload: () => void; 350 | /** 351 | * @todo 352 | * @internal 353 | */ 354 | readSnippets: () => void; 355 | /** 356 | * @todo 357 | * @internal 358 | */ 359 | readThemes: () => void; 360 | /** 361 | * Remove a theme by theme name 362 | */ 363 | removeTheme: (themeName: string) => Promise; 364 | /** 365 | * Set the activation status of a snippet by snippet name 366 | */ 367 | setCssEnabledStatus: (snippetName: string, enabled: boolean) => void; 368 | /** 369 | * Set the active theme by theme name 370 | */ 371 | setTheme: (themeName: string) => void; 372 | /** 373 | * Set the translucency of application background 374 | */ 375 | setTranslucency: (translucency: boolean) => void; 376 | } 377 | 378 | interface ObsidianDOM { 379 | /** 380 | * Root element of the application 381 | */ 382 | appContainerEl: HTMLElement; 383 | /** 384 | * Child of `appContainerEl` containing the main content of the application 385 | */ 386 | horizontalMainContainerEl: HTMLElement; 387 | /** 388 | * Status bar element containing word count among other things 389 | */ 390 | statusBarEl: HTMLElement; 391 | /** 392 | * Child of `horizontalMainContainerEl` containing the workspace DOM 393 | */ 394 | workspaceEl: HTMLElement; 395 | } 396 | 397 | // interface EmbedRegistry { 398 | // embedByExtension: Map any>; 399 | // } 400 | 401 | interface PositionedReference extends Reference { 402 | /** 403 | * Position of the reference in the file 404 | */ 405 | position: { 406 | start: Loc; 407 | end: Loc; 408 | }; 409 | } 410 | 411 | interface LinkUpdate { 412 | /** 413 | * Reference to App 414 | */ 415 | app: App; 416 | /** 417 | * Link position in the file 418 | */ 419 | reference: PositionedReference; 420 | /** 421 | * File that was resolved 422 | */ 423 | resolvedFile: TFile; 424 | /** 425 | * Paths the file could have been resolved to 426 | */ 427 | resolvedPaths: string[]; 428 | /** 429 | * File that contains the link 430 | */ 431 | sourceFile: TFile; 432 | } 433 | 434 | interface HotkeyManager { 435 | /** 436 | * Reference to App 437 | */ 438 | app: App; 439 | /** 440 | * @internal Whether hotkeys have been baked (checks completed) 441 | */ 442 | baked: boolean; 443 | /** 444 | * Assigned hotkeys 445 | */ 446 | bakedHotkeys: KeymapInfo[]; 447 | /** 448 | * Array of hotkey index to command ID 449 | */ 450 | bakedIds: string[]; 451 | /** 452 | * Custom (non-Obsidian default) hotkeys, one to many mapping of command ID to assigned hotkey 453 | */ 454 | customKeys: Record; 455 | /** 456 | * Default hotkeys, one to many mapping of command ID to assigned hotkey 457 | */ 458 | defaultKeys: Record; 459 | 460 | /** 461 | * Add a hotkey to the default hotkeys 462 | * @param command - Command ID to add hotkey to 463 | * @param keys - Hotkeys to add 464 | */ 465 | addDefaultHotkeys: (command: string, keys: KeymapInfo[]) => void; 466 | /** 467 | * Get hotkey associated with command ID 468 | * @param command - Command ID to get hotkey for 469 | */ 470 | getDefaultHotkeys: (command: string) => KeymapInfo[]; 471 | /** 472 | * Remove a hotkey from the default hotkeys 473 | * @param command - Command ID to remove hotkey from 474 | */ 475 | removeDefaultHotkeys: (command: string) => void; 476 | /** 477 | * Add a hotkey to the custom hotkeys (overrides default hotkeys) 478 | * @param command - Command ID to add hotkey to 479 | * @param keys - Hotkeys to add 480 | */ 481 | setHotkeys: (command: string, keys: KeymapInfo[]) => void; 482 | /** 483 | * Get hotkey associated with command ID 484 | * @param command - Command ID to get hotkey for 485 | */ 486 | getHotkeys: (command: string) => KeymapInfo[]; 487 | /** 488 | * Remove a hotkey from the custom hotkeys 489 | * @param command - Command ID to remove hotkey from 490 | */ 491 | removeHotkeys: (command: string) => void; 492 | /** 493 | * Pretty-print hotkey of a command 494 | * @param command 495 | */ 496 | printHotkeyForCommand: (command: string) => string; 497 | /** 498 | * Trigger a command by keyboard event 499 | * @param event - Keyboard event to trigger command with 500 | * @param keypress - Pressed key information 501 | */ 502 | onTrigger: (event: KeyboardEvent, keypress: KeymapInfo) => boolean; 503 | /** 504 | * @internal Bake hotkeys (create mapping of pressed key to command ID) 505 | */ 506 | bake: () => void; 507 | /** 508 | * @internal Load hotkeys from storage 509 | */ 510 | load: () => void; 511 | /** 512 | * @internal Save custom hotkeys to storage 513 | */ 514 | save: () => void; 515 | } 516 | 517 | type InternalPlugin = 518 | | "audio-recorder" 519 | | "backlink" 520 | | "bookmarks" 521 | | "canvas" 522 | | "command-palette" 523 | | "daily-notes" 524 | | "editor-status" 525 | | "file-explorer" 526 | | "file-recovery" 527 | | "global-search" 528 | | "graph" 529 | | "markdown-importer" 530 | | "note-composer" 531 | | "outgoing-link" 532 | | "outline" 533 | | "page-preview" 534 | | "properties" 535 | | "publish" 536 | | "random-note" 537 | | "slash-command" 538 | | "slides" 539 | | "starred" 540 | | "switcher" 541 | | "sync" 542 | | "tag-pane" 543 | | "templates" 544 | | "word-count" 545 | | "workspaces" 546 | | "zk-prefixer"; 547 | 548 | interface InternalPlugins extends Events { 549 | /** 550 | * Reference to App 551 | */ 552 | app: App; 553 | /** 554 | * Mapping of whether an internal plugin is enabled 555 | */ 556 | config: Record; 557 | /** 558 | * @internal 559 | */ 560 | migration: boolean; 561 | /** 562 | * Plugin configs for internal plugins 563 | */ 564 | plugins: Record; 565 | /** 566 | * @internal Request save of plugin configs 567 | */ 568 | requestSaveConfig: () => void; 569 | 570 | /** 571 | * Get an enabled internal plugin by ID 572 | * @param id - ID of the plugin to get 573 | */ 574 | getEnabledPluginById: (id: InternalPlugin) => Plugin | null; 575 | /** 576 | * Get all enabled internal plugins 577 | */ 578 | getEnabledPlugins: () => Plugin[]; 579 | /** 580 | * Get an internal plugin by ID 581 | * @param id - ID of the plugin to get 582 | */ 583 | getPluginById: (id: InternalPlugin) => Plugin; 584 | 585 | /** 586 | * @internal - Load plugin configs and enable plugins 587 | */ 588 | enable: () => Promise; 589 | /** 590 | * @internal 591 | */ 592 | loadPlugin: ({ id: string, name: string }) => string; 593 | /** 594 | * @internal 595 | */ 596 | on: (inp: any, cb: () => void, arg: string) => void; 597 | /** 598 | * @internal 599 | */ 600 | onRaw: (cb1: any, cb2: any) => void; 601 | /** 602 | * @internal - Save current plugin configs 603 | */ 604 | saveConfig: () => Promise; 605 | /** 606 | * @internal 607 | */ 608 | trigger: (arg: string) => void; 609 | } 610 | 611 | interface KeyScope { 612 | /** 613 | * Callback of function to execute when key is pressed 614 | */ 615 | func: () => void; 616 | /** 617 | * Key to match 618 | */ 619 | key: string | null; 620 | /** 621 | * Modifiers to match 622 | */ 623 | modifiers: string | null; 624 | /** 625 | * Scope where the key interceptor is registered 626 | */ 627 | scope: EScope; 628 | } 629 | 630 | // interface KeymapManager { 631 | // /** 632 | // * Modifiers pressed within keyscope 633 | // */ 634 | // modifiers: string; 635 | // /** 636 | // * @internal 637 | // */ 638 | // prevScopes: EScope[]; 639 | // /** 640 | // * @internal - Root scope of the application 641 | // */ 642 | // rootScope: EScope; 643 | // 644 | // /** 645 | // * Get the root scope of the application 646 | // */ 647 | // getRootScope: () => EScope; 648 | // /** 649 | // * Check whether a specific modifier was part of the keypress 650 | // */ 651 | // hasModifier: (modifier: Modifier) => boolean; 652 | // /** 653 | // * Check whether event has same modifiers as the current keypress 654 | // */ 655 | // matchModifiers: (event: KeyboardEvent) => boolean; 656 | // /** 657 | // * @internal - On focus keyscope 658 | // */ 659 | // onFocusIn: (event: FocusEvent) => void; 660 | // /** 661 | // * @internal - On keypress find matching keyscope and execute callback 662 | // */ 663 | // onKeyEvent: (event: KeyboardEvent) => void; 664 | // /** 665 | // * @internal - Pop a scope from the prevScopes stack 666 | // */ 667 | // popScope: (scope: EScope) => void; 668 | // /** 669 | // * @internal - Push a scope to the prevScopes stack and set it as the current scope 670 | // */ 671 | // pushScope: (scope: EScope) => void; 672 | // /** 673 | // * @internal - Update last pressed modifiers 674 | // */ 675 | // updateModifiers: (event: KeyboardEvent) => void; 676 | // } 677 | 678 | // interface LoadProgress { 679 | // /** 680 | // * Application's document 681 | // */ 682 | // doc: Document; 683 | // /** 684 | // * @internal Loading bar element 685 | // */ 686 | // el: HTMLElement; 687 | // /** 688 | // * @internal First part of the line 689 | // */ 690 | // line1El: HTMLElement; 691 | // /** 692 | // * @internal Second part of the line 693 | // */ 694 | // line2El: HTMLElement; 695 | // /** 696 | // * @internal Main line element 697 | // */ 698 | // lineEl: HTMLElement; 699 | // /** 700 | // * @internal Message element for the loading bar 701 | // */ 702 | // messageEl: HTMLElement; 703 | // /** 704 | // * @internal Timeout for the loading bar 705 | // */ 706 | // showTimeout: number; 707 | // 708 | // /** 709 | // * @internal Delay showing the loading bar 710 | // */ 711 | // delayedShow: () => LoadProgress; 712 | // /** 713 | // * @internal Hide and remove the loading bar 714 | // */ 715 | // hide: () => LoadProgress; 716 | // /** 717 | // * @internal Update the loading bar message 718 | // * @param message - Message to update to 719 | // */ 720 | // setMessage: (message: string) => LoadProgress; 721 | // /** 722 | // * @internal Update the loading bar progress 723 | // * @param current - Current progress 724 | // * @param max - Maximum progress 725 | // */ 726 | // setProgress: (current: number, max: number) => LoadProgress; 727 | // /** 728 | // * @internal Set the loading bar to unknown progress 729 | // */ 730 | // setUnknownProgress: () => LoadProgress; 731 | // } 732 | 733 | interface BlockCache { 734 | /** 735 | * Reference to App 736 | */ 737 | app: App; 738 | 739 | /** 740 | * @internal 741 | */ 742 | cache: any; 743 | } 744 | 745 | interface FileCacheEntry { 746 | /** 747 | * Hash of file contents 748 | */ 749 | hash: string; 750 | /** 751 | * Last modified time of file 752 | */ 753 | mtime: number; 754 | /** 755 | * Size of file in bytes 756 | */ 757 | size: number; 758 | } 759 | 760 | interface CustomArrayDict { 761 | data: Record; 762 | 763 | add: (key: T, value: Q) => void; 764 | remove: (key: T, value: Q) => void; 765 | removeKey: (key: T) => void; 766 | get: (key: T) => Q; 767 | keys: () => T[]; 768 | clear: (key: T) => void; 769 | clearAll: () => void; 770 | contains: (key: T) => boolean; 771 | count: () => number; 772 | } 773 | 774 | interface PropertyInfo { 775 | /** 776 | * Name of property 777 | */ 778 | name: string; 779 | /** 780 | * Type of property 781 | */ 782 | type: string; 783 | /** 784 | * Usage count of property 785 | */ 786 | count: number; 787 | } 788 | 789 | type PropertyWidgetType = 790 | | "aliases" 791 | | "checkbox" 792 | | "date" 793 | | "datetime" 794 | | "multitext" 795 | | "number" 796 | | "tags" 797 | | "text"; 798 | 799 | interface PropertyWidget { 800 | /** 801 | * @internal 802 | */ 803 | default: () => void; 804 | /** 805 | * Lucide-dev icon associated with the widget 806 | */ 807 | icon: string; 808 | /** 809 | * @internal Name proxy 810 | */ 811 | name: any; 812 | /** 813 | * @internal Render function for the widget 814 | */ 815 | render: ( 816 | element: HTMLElement, 817 | metadataField: any, 818 | property: PropertyInfo 819 | ) => void; 820 | /** 821 | * @internal Reserved keys for the widget 822 | */ 823 | reservedKeys: string[]; 824 | /** 825 | * Widget type 826 | */ 827 | type: string; 828 | /** 829 | * @internal Validate correctness of property input with respects to the widget 830 | */ 831 | validate: (value: any) => boolean; 832 | } 833 | 834 | interface MetadataTypeManager extends Events { 835 | /** 836 | * Reference to App 837 | */ 838 | app: App; 839 | /** 840 | * Registered properties of the vault 841 | */ 842 | properties: Record; 843 | /** 844 | * @internal Registered type widgets 845 | */ 846 | registeredTypeWidgets: Record; 847 | /** 848 | * Associated widget types for each property 849 | */ 850 | types: Record; 851 | 852 | /** 853 | * Get all registered properties of the vault 854 | */ 855 | getAllProperties: () => Record; 856 | /** 857 | * Get assigned widget type for property 858 | */ 859 | getAssignedType: (property: string) => PropertyWidgetType | null; 860 | /** 861 | * Get info for property 862 | */ 863 | getPropertyInfo: (property: string) => PropertyInfo; 864 | /** 865 | * @internal Get expected widget type for property and the one inferred from the property value 866 | */ 867 | getTypeInfo: ({ key: string, type: string, value: any }) => { 868 | inferred: PropertyWidget; 869 | expected: PropertyWidget; 870 | }; 871 | /** 872 | * Get all properties with an assigned widget type 873 | */ 874 | getTypes: () => string[]; 875 | /** 876 | * @internal Load property types from config 877 | */ 878 | loadData: () => Promise; 879 | /** 880 | * @internal 881 | */ 882 | on: (args: any) => void; 883 | /** 884 | * @internal Save property types to config 885 | */ 886 | save: () => Promise; 887 | /** 888 | * @internal Get all properties from metadata cache 889 | */ 890 | savePropertyInfo: () => void; 891 | /** 892 | * @internal Set widget type for property 893 | */ 894 | setType: (property: string, type: PropertyWidgetType) => void; 895 | /** 896 | * @internal 897 | */ 898 | trigger: (e: any) => void; 899 | /** 900 | * @internal Unset widget type for property 901 | */ 902 | unsetType: (property: string) => void; 903 | } 904 | // 905 | // interface MobileNavbar { 906 | // /** 907 | // * Reference to App 908 | // */ 909 | // app: App; 910 | // /** 911 | // * @internal Back button element 912 | // */ 913 | // backButtonEl: HTMLElement; 914 | // /** 915 | // * @internal Container element 916 | // */ 917 | // containerEl: HTMLElement; 918 | // /** 919 | // * @internal Forward button element 920 | // */ 921 | // forwardButtonEl: HTMLElement; 922 | // /** 923 | // * Whether the mobile navbar is currently visible 924 | // */ 925 | // isVisible: boolean; 926 | // /** 927 | // * @internal On ribbon click 928 | // */ 929 | // onRibbonClick: () => void; 930 | // /** 931 | // * @internal Ribbon menu flair element 932 | // */ 933 | // ribbonMenuFlairEl: HTMLElement; 934 | // /** 935 | // * @internal Ribbon menu item element 936 | // */ 937 | // ribbonMenuItemEl: HTMLElement; 938 | // /** 939 | // * @internal Tab button element 940 | // */ 941 | // tabButtonEl: HTMLElement; 942 | // 943 | // /** 944 | // * @internal Hide mobile navbar 945 | // */ 946 | // hide: () => void; 947 | // /** 948 | // * @internal Show mobile navbar 949 | // */ 950 | // show: () => void; 951 | // /** 952 | // * @internal Show ribbon menu 953 | // */ 954 | // showRibbonMenu: () => void; 955 | // /** 956 | // * @internal Update navigation buttons 957 | // */ 958 | // updateNavButtons: () => void; 959 | // /** 960 | // * @internal Update ribbon menu item 961 | // */ 962 | // updateRibbonMenuItem: () => void; 963 | // } 964 | // 965 | // interface MobileToolbar { 966 | // /** 967 | // * Reference to App 968 | // */ 969 | // app: App; 970 | // /** 971 | // * @internal Container element 972 | // */ 973 | // containerEl: HTMLElement; 974 | // /** 975 | // * @internal Last selected command ID 976 | // */ 977 | // lastCommandIds: string; 978 | // /** 979 | // * @internal Options container element 980 | // */ 981 | // optionsContainerEl: HTMLElement; 982 | // 983 | // /** 984 | // * @internal Compile all actions for the toolbar 985 | // */ 986 | // compileToolbar: () => void; 987 | // /** 988 | // * @internal Hide mobile toolbar 989 | // */ 990 | // hide: () => void; 991 | // /** 992 | // * @internal Show mobile toolbar 993 | // */ 994 | // show: () => void; 995 | // } 996 | 997 | interface PluginManifest { 998 | /** 999 | * Name of the author of the plugin 1000 | */ 1001 | author: string; 1002 | /** 1003 | * URL to the author's website 1004 | */ 1005 | authorUrl?: string; 1006 | /** 1007 | * Description of the plugin's functionality 1008 | */ 1009 | description: string; 1010 | /** 1011 | * Storage location of the plugin relative to the vault root 1012 | */ 1013 | dir: string; 1014 | /** 1015 | * URL for funding the author 1016 | */ 1017 | fundingUrl?: string; 1018 | /** 1019 | * Unique identifier of the plugin 1020 | */ 1021 | id: string; 1022 | /** 1023 | * Whether the plugin is designed for desktop use only 1024 | */ 1025 | isDesktopOnly: boolean; 1026 | /** 1027 | * Minimum Obsidian version compatible with the plugin 1028 | */ 1029 | minAppVersion: string; 1030 | /** 1031 | * Name of the plugin 1032 | */ 1033 | name: string; 1034 | /** 1035 | * Version of the plugin 1036 | */ 1037 | version: string; 1038 | } 1039 | 1040 | interface PluginUpdateManifest { 1041 | /** 1042 | * Manifest of the plugin 1043 | */ 1044 | manifest: PluginManifest; 1045 | /** 1046 | * Repository of the plugin 1047 | */ 1048 | repo: string; 1049 | /** 1050 | * New version of the plugin 1051 | */ 1052 | version: string; 1053 | } 1054 | 1055 | interface Plugins { 1056 | /** 1057 | * Reference to App 1058 | */ 1059 | app: App; 1060 | /** 1061 | * Set of enabled plugin IDs 1062 | */ 1063 | enabledPlugins: Set; 1064 | /** 1065 | * @internal Plugin ID that is currently being enabled 1066 | */ 1067 | loadingPluginId: string | null; 1068 | /** 1069 | * Manifests of all the plugins 1070 | */ 1071 | manifests: Record; 1072 | /** 1073 | * Mapping of plugin ID to plugin instance 1074 | */ 1075 | plugins: Record; 1076 | /** 1077 | * Mapping of plugin ID to available updates 1078 | */ 1079 | updates: Map; 1080 | 1081 | /** 1082 | * @internal Check online list for deprecated plugins to automatically disable 1083 | */ 1084 | checkForDeprecations: () => Promise; 1085 | /** 1086 | * Check for plugin updates 1087 | */ 1088 | checkForUpdates: () => Promise; 1089 | /** 1090 | * Unload a plugin by ID 1091 | */ 1092 | disablePlugin: (id: string) => Promise; 1093 | /** 1094 | * Unload a plugin by ID and save config for persistence 1095 | */ 1096 | disablePluginAndSave: (id: string) => Promise; 1097 | /** 1098 | * Enable a plugin by ID 1099 | */ 1100 | enablePlugin: (id: string) => Promise; 1101 | /** 1102 | * Enable a plugin by ID and save config for persistence 1103 | */ 1104 | enablePluginAndSave: (id: string) => Promise; 1105 | /** 1106 | * Get a plugin by ID 1107 | */ 1108 | getPlugin: (id: string) => Plugin | null; 1109 | /** 1110 | * Get the folder where plugins are stored 1111 | */ 1112 | getPluginFolder: () => string; 1113 | /** 1114 | * @internal Load plugin manifests and enable plugins from config 1115 | */ 1116 | initialize: () => Promise; 1117 | /** 1118 | * Install a plugin from a given URL 1119 | */ 1120 | installPlugin: ( 1121 | repo: string, 1122 | manifest: PluginManifest, 1123 | version: string 1124 | ) => Promise; 1125 | /** 1126 | * Check whether a plugin is deprecated 1127 | */ 1128 | isDeprecated: (id: string) => boolean; 1129 | /** 1130 | * Check whether community plugins are enabled 1131 | */ 1132 | isEnabled: () => boolean; 1133 | /** 1134 | * Load a specific plugin's manifest by its ID 1135 | */ 1136 | loadManifest: (id: string) => Promise; 1137 | /** 1138 | * @internal Load all plugin manifests from plugin folder 1139 | */ 1140 | loadManifests: () => Promise; 1141 | /** 1142 | *Load a plugin by its ID 1143 | */ 1144 | loadPlugin: (id: string) => Promise; 1145 | /** 1146 | * @internal 1147 | */ 1148 | onRaw: (e: any) => void; 1149 | /** 1150 | * @internal - Save current plugin configs 1151 | */ 1152 | saveConfig: () => Promise; 1153 | /** 1154 | * @internal Toggle whether community plugins are enabled 1155 | */ 1156 | setEnable: (enabled: boolean) => Promise; 1157 | /** 1158 | * Uninstall a plugin by ID 1159 | */ 1160 | uninstallPlugin: (id: string) => Promise; 1161 | /** 1162 | * Unload a plugin by ID 1163 | */ 1164 | unloadPlugin: (id: string) => Promise; 1165 | } 1166 | 1167 | interface WindowSelection { 1168 | focusEl: HTMLElement; 1169 | range: Range; 1170 | win: Window; 1171 | } 1172 | 1173 | type ConfigItem = 1174 | | "accentColor" 1175 | | "alwaysUpdateLinks" 1176 | | "attachmentFolderPath" 1177 | | "autoConvertHtml" 1178 | | "autoPairBrackets" 1179 | | "autoPairMarkdown" 1180 | | "baseFontSize" 1181 | | "baseFontSizeAction" 1182 | | "cssTheme" 1183 | | "defaultViewMode" 1184 | | "emacsyKeys" 1185 | | "enabledCssSnippets" 1186 | | "fileSortOrder" 1187 | | "focusNewTab" 1188 | | "foldHeading" 1189 | | "foldIndent" 1190 | | "hotkeys" 1191 | | "interfaceFontFamily" 1192 | | "legacyEditor" 1193 | | "livePreview" 1194 | | "mobilePullAction" 1195 | | "mobileQuickRibbonItem" 1196 | | "mobileToolbarCommands" 1197 | | "monospaceFontFamily" 1198 | | "nativeMenus" 1199 | | "newFileFolderPath" 1200 | | "newFileLocation" 1201 | | "newLinkFormat" 1202 | | "pdfExportSettings" 1203 | | "promptDelete" 1204 | | "propertiesInDocument" 1205 | | "readableLineLength" 1206 | | "rightToLeft" 1207 | | "showIndentGuide" 1208 | | "showInlineTitle" 1209 | | "showLineNumber" 1210 | | "showUnsupportedFiles" 1211 | | "showViewHeader" 1212 | | "smartIndentList" 1213 | | "spellcheck" 1214 | | "spellcheckLanguages" 1215 | | "strictLineBreaks" 1216 | | "tabSize" 1217 | | "textFontFamily" 1218 | | "theme" 1219 | | "translucency" 1220 | | "trashOption" 1221 | | "types" 1222 | | "useMarkdownLinks" 1223 | | "useTab" 1224 | | "userIgnoreFilters" 1225 | | "vimMode"; 1226 | 1227 | interface AppVaultConfig { 1228 | /** 1229 | * Appearance > Accent color 1230 | */ 1231 | accentColor: "" | string; 1232 | /** 1233 | * Files & Links > Automatically update internal links 1234 | */ 1235 | alwaysUpdateLinks?: false | boolean; 1236 | /** 1237 | * Files & Links > Attachment folder path 1238 | */ 1239 | attachmentFolderPath?: "/" | string; 1240 | /** 1241 | * Editor > Auto convert HTML 1242 | */ 1243 | autoConvertHtml?: true | boolean; 1244 | /** 1245 | * Editor > Auto pair brackets 1246 | */ 1247 | autoPairBrackets?: true | boolean; 1248 | /** 1249 | * Editor > Auto pair Markdown syntax 1250 | */ 1251 | autoPairMarkdown?: true | boolean; 1252 | /** 1253 | * Appearance > Font size 1254 | */ 1255 | baseFontSize?: 16 | number; 1256 | /** 1257 | * Appearance > Quick font size adjustment 1258 | */ 1259 | baseFontSizeAction?: true | boolean; 1260 | /** 1261 | * Community Plugins > Browse > Sort order 1262 | */ 1263 | communityPluginSortOrder: 1264 | | "download" 1265 | | "update" 1266 | | "release" 1267 | | "alphabetical"; 1268 | /** 1269 | * Themes > Browse > Sort order 1270 | */ 1271 | communityThemeSortOrder: "download" | "update" | "release" | "alphabetical"; 1272 | /** 1273 | * Appearance > Theme 1274 | * @remark "" is the default Obsidian theme 1275 | */ 1276 | cssTheme?: "" | string; 1277 | /** 1278 | * Editor > Default view for new tabs 1279 | */ 1280 | defaultViewMode?: "source" | "preview"; 1281 | /** 1282 | * 1283 | */ 1284 | emacsyKeys?: true | boolean; 1285 | /** 1286 | * Appearance > CSS snippets 1287 | */ 1288 | enabledCssSnippets?: string[]; 1289 | /** 1290 | * 1291 | */ 1292 | fileSortOrder?: "alphabetical"; 1293 | /** 1294 | * Editor > Always focus new tabs 1295 | */ 1296 | focusNewTab?: true | boolean; 1297 | /** 1298 | * Editor > Fold heading 1299 | */ 1300 | foldHeading?: true | boolean; 1301 | /** 1302 | * Editor > Fold indent 1303 | */ 1304 | foldIndent?: true | boolean; 1305 | /** 1306 | * Hotkeys 1307 | * @deprecated Likely not used anymore 1308 | */ 1309 | hotkeys?: Record; 1310 | /** 1311 | * Appearance > Interface font 1312 | */ 1313 | interfaceFontFamily?: "" | string; 1314 | /** 1315 | * Editor > Use legacy editor 1316 | */ 1317 | legacyEditor?: false | boolean; 1318 | /** 1319 | * 1320 | */ 1321 | livePreview?: true | boolean; 1322 | /** 1323 | * Mobile > Configure mobile Quick Action 1324 | */ 1325 | mobilePullAction?: "command-palette:open" | string; 1326 | /** 1327 | * 1328 | */ 1329 | mobileQuickRibbonItem?: "" | string; 1330 | /** 1331 | * Mobile > Manage toolbar options 1332 | */ 1333 | mobileToolbarCommands?: string[]; 1334 | /** 1335 | * 1336 | */ 1337 | monospaceFontFamily?: "" | string; 1338 | /** 1339 | * Appearance > Native menus 1340 | */ 1341 | nativeMenus?: null | boolean; 1342 | /** 1343 | * Files & Links > Default location for new notes | 'folder' > Folder to create new notes in 1344 | */ 1345 | newFileFolderPath?: "/" | string; 1346 | /** 1347 | * Files & Links > Default location for new notes 1348 | */ 1349 | newFileLocation?: "root" | "current" | "folder"; 1350 | /** 1351 | * Files & Links > New link format 1352 | */ 1353 | newLinkFormat?: "shortest" | "relative" | "absolute"; 1354 | /** 1355 | * Saved on executing 'Export to PDF' command 1356 | */ 1357 | pdfExportSettings?: { 1358 | pageSize: "letter" | string; 1359 | landscape: false | boolean; 1360 | margin: "0" | string; 1361 | downscalePercent: 100 | number; 1362 | }; 1363 | /** 1364 | * Files & Links > Confirm line deletion 1365 | */ 1366 | promptDelete?: true | boolean; 1367 | /** 1368 | * Editor > Properties in document 1369 | */ 1370 | propertiesInDocument?: "visible" | "hidden" | "source"; 1371 | /** 1372 | * Editor > Readable line length 1373 | */ 1374 | readableLineLength?: true | boolean; 1375 | /** 1376 | * Editor > Right-to-left (RTL) 1377 | */ 1378 | rightToLeft?: false | boolean; 1379 | /** 1380 | * @deprecated Removed as of version 1.4.3 1381 | */ 1382 | showFrontmatter?: false | boolean; 1383 | /** 1384 | * Editor > Show indentation guides 1385 | */ 1386 | showIndentGuide?: true | boolean; 1387 | /** 1388 | * Editor > Show inline title 1389 | */ 1390 | showInlineTitle?: true | boolean; 1391 | /** 1392 | * Editor > Show line numbers 1393 | */ 1394 | showLineNumber?: false | boolean; 1395 | /** 1396 | * Files & Links > Detect all file extensions 1397 | */ 1398 | showUnsupportedFiles?: false | boolean; 1399 | /** 1400 | * Appearance > Show tab title bar 1401 | */ 1402 | showViewHeader?: false | boolean; 1403 | /** 1404 | * Editor > Smart indent lists 1405 | */ 1406 | smartIndentList?: true | boolean; 1407 | /** 1408 | * Editor > Spellcheck 1409 | */ 1410 | spellcheck?: false | boolean; 1411 | /** 1412 | * @deprecated 1413 | */ 1414 | spellcheckDictionary?: [] | string[]; 1415 | /** 1416 | * Editor > Spellcheck languages 1417 | */ 1418 | spellcheckLanguages?: null | string[]; 1419 | /** 1420 | * Editor > Strict line breaks 1421 | */ 1422 | strictLineBreaks?: false | boolean; 1423 | /** 1424 | * Editor > Tab indent size 1425 | */ 1426 | tabSize?: 4 | number; 1427 | /** 1428 | * Appearance > Text font 1429 | */ 1430 | textFontFamily?: "" | string; 1431 | /** 1432 | * Appearance > Base color scheme 1433 | * @remark Not be confused with cssTheme, this setting is for the light/dark mode 1434 | * @remark "moonstone" is light theme, "obsidian" is dark theme 1435 | */ 1436 | theme?: "moonstone" | "obsidian"; 1437 | /** 1438 | * Appearance > Translucent window 1439 | */ 1440 | translucency?: false | boolean; 1441 | /** 1442 | * Files & Links > Deleted files 1443 | */ 1444 | trashOption?: "system" | "local" | "none"; 1445 | /** 1446 | * @deprecated Probably left-over code from old properties type storage 1447 | */ 1448 | types: object; 1449 | /** 1450 | * Files & Links > Use [[Wikilinks]] 1451 | */ 1452 | useMarkdownLinks?: false | boolean; 1453 | /** 1454 | * Editor > Indent using tabs 1455 | */ 1456 | useTab?: true | boolean; 1457 | /** 1458 | * Files & Links > Excluded files 1459 | */ 1460 | userIgnoreFilters?: null | string[]; 1461 | /** 1462 | * Editor > Vim key bindings 1463 | */ 1464 | vimMode?: false | boolean; 1465 | } 1466 | 1467 | interface FileEntry { 1468 | /** 1469 | * Creation time (if file) 1470 | */ 1471 | ctime?: number; 1472 | /** 1473 | * Modification time (if file) 1474 | */ 1475 | mtime?: number; 1476 | /** 1477 | * Full path to file or folder 1478 | * @remark Might be used for resolving symlinks 1479 | */ 1480 | realpath: string; 1481 | /** 1482 | * Size in bytes (if file) 1483 | */ 1484 | size?: number; 1485 | /** 1486 | * Type of entry 1487 | */ 1488 | type: "file" | "folder"; 1489 | } 1490 | 1491 | interface ViewRegistry extends Events { 1492 | /** 1493 | * Mapping of file extensions to view type 1494 | */ 1495 | typeByExtension: Record; 1496 | /** 1497 | * Mapping of view type to view constructor 1498 | */ 1499 | viewByType: Record View>; 1500 | 1501 | /** 1502 | * Get the view type associated with a file extension 1503 | * @param extension File extension 1504 | */ 1505 | getTypeByExtension: (extension: string) => string; 1506 | /** 1507 | * Get the view constructor associated with a view type 1508 | */ 1509 | getViewCreatorByType: (type: string) => (leaf: WorkspaceLeaf) => View; 1510 | /** 1511 | * Check whether a view type is registered 1512 | */ 1513 | isExtensionRegistered: (extension: string) => boolean; 1514 | /** 1515 | * @internal 1516 | */ 1517 | on: (args: any[]) => EventRef; 1518 | /** 1519 | * Register a view type for a file extension 1520 | * @param extension File extension 1521 | * @param type View type 1522 | * @remark Prefer registering the extension via the Plugin class 1523 | */ 1524 | registerExtensions: (extension: string[], type: string) => void; 1525 | /** 1526 | * Register a view constructor for a view type 1527 | */ 1528 | registerView: ( 1529 | type: string, 1530 | viewCreator: (leaf: WorkspaceLeaf) => View 1531 | ) => void; 1532 | /** 1533 | * Register a view and its associated file extensions 1534 | */ 1535 | registerViewWithExtensions: ( 1536 | extensions: string[], 1537 | type: string, 1538 | viewCreator: (leaf: WorkspaceLeaf) => View 1539 | ) => void; 1540 | /** 1541 | * @internal 1542 | */ 1543 | trigger: (type: string) => void; 1544 | /** 1545 | * Unregister extensions for a view type 1546 | */ 1547 | unregisterExtensions: (extension: string[]) => void; 1548 | /** 1549 | * Unregister a view type 1550 | */ 1551 | unregisterView: (type: string) => void; 1552 | } 1553 | 1554 | interface HoverLinkSource { 1555 | display: string; 1556 | defaultMod: boolean; 1557 | } 1558 | 1559 | interface RecentFileTracker { 1560 | /** 1561 | * List of last opened file paths, limited to 50 1562 | */ 1563 | lastOpenFiles: string[]; 1564 | /** 1565 | * Reference to Vault 1566 | */ 1567 | vault: EVault; 1568 | /** 1569 | * Reference to Workspace 1570 | */ 1571 | workspace: EWorkspace; 1572 | 1573 | /** 1574 | * @internal 1575 | */ 1576 | collect: (file: TFile) => void; 1577 | /** 1578 | * Returns the last 10 opened files 1579 | */ 1580 | getLastOpenFiles: () => string[]; 1581 | /** 1582 | * Get last n files of type (defaults to 10) 1583 | */ 1584 | getRecentFiles: ({ 1585 | showMarkdown: boolean, 1586 | showCanvas: boolean, 1587 | showNonImageAttachments: boolean, 1588 | showImages: boolean, 1589 | maxCount: number, 1590 | }?) => string[]; 1591 | /** 1592 | * Set the last opened files 1593 | */ 1594 | load: (savedFiles: string[]) => void; 1595 | /** 1596 | * @internal On file create, save file to last opened files 1597 | */ 1598 | onFileCreated: (file: TFile) => void; 1599 | /** 1600 | * @internal On file open, save file to last opened files 1601 | */ 1602 | onFileOpen: (prevFile: TFile, file: TFile) => void; 1603 | /** 1604 | * @internal On file rename, update file path in last opened files 1605 | */ 1606 | onRename: (file: TFile, oldPath: string) => void; 1607 | /** 1608 | * @internal Get last opened files 1609 | */ 1610 | serialize: () => string[]; 1611 | } 1612 | 1613 | interface StateHistory { 1614 | /** 1615 | * Ephemeral cursor state within Editor of leaf 1616 | */ 1617 | eState: { 1618 | cursor: EditorRange; 1619 | scroll: number; 1620 | }; 1621 | /** 1622 | * Icon of the leaf 1623 | */ 1624 | icon?: string; 1625 | /** 1626 | * History of previous and future states of leaf 1627 | */ 1628 | leafHistory?: { 1629 | backHistory: StateHistory[]; 1630 | forwardHistory: StateHistory[]; 1631 | }; 1632 | /** 1633 | * Id of parent to which the leaf belonged 1634 | */ 1635 | parentId?: string; 1636 | /** 1637 | * Id of root to which the leaf belonged 1638 | */ 1639 | rootId?: string; 1640 | /** 1641 | * Last state of the leaf 1642 | */ 1643 | state: ViewState; 1644 | /** 1645 | * Title of the leaf 1646 | */ 1647 | title?: string; 1648 | } 1649 | 1650 | interface LeafEntry { 1651 | children?: LeafEntry[]; 1652 | direction?: SplitDirection; 1653 | id: string; 1654 | state?: ViewState; 1655 | type: string; 1656 | width?: number; 1657 | } 1658 | 1659 | interface SerializedWorkspace { 1660 | /** 1661 | * Last active leaf 1662 | */ 1663 | active: string; 1664 | /** 1665 | * Last opened files 1666 | */ 1667 | lastOpenFiles: string[]; 1668 | /** 1669 | * Left opened leaf 1670 | */ 1671 | left: LeafEntry; 1672 | /** 1673 | * Left ribbon 1674 | */ 1675 | leftRibbon: { hiddenItems: Record }; 1676 | /** 1677 | * Main (center) workspace leaf 1678 | */ 1679 | main: LeafEntry; 1680 | /** 1681 | * Right opened leaf 1682 | */ 1683 | right: LeafEntry; 1684 | } 1685 | 1686 | interface ImportedAttachments { 1687 | data: Promise; 1688 | extension: string; 1689 | filename: string; 1690 | name: string; 1691 | } 1692 | 1693 | declare module "obsidian" { 1694 | interface App { 1695 | /** 1696 | * The account signed in to Obsidian 1697 | */ 1698 | account: Account; 1699 | /** 1700 | * ID that uniquely identifies the vault 1701 | * @tutorial Used for implementing device *and* vault-specific 1702 | * data storage using LocalStorage or IndexedDB 1703 | */ 1704 | appId: string; 1705 | // /** 1706 | // * @internal 1707 | // */ 1708 | // appMenuBarManager: AppMenuBarManager; 1709 | /** 1710 | * Contains all registered commands 1711 | * @tutorial Can be used to manually invoke the functionality of a specific command 1712 | */ 1713 | commands: Commands; 1714 | /** 1715 | * Custom CSS (snippets/themes) applied to the application 1716 | * @tutorial Can be used to view which snippets are enabled or available, 1717 | * or inspect loaded-in theme manifests 1718 | */ 1719 | customCss: CustomCSS; 1720 | /** References to important DOM elements of the application */ 1721 | dom: ObsidianDOM; 1722 | // /** 1723 | // * @internal 1724 | // */ 1725 | // dragManager: any; 1726 | // /** 1727 | // * @internal 1728 | // */ 1729 | // embedRegistry: EmbedRegistry; 1730 | /** 1731 | * Manage the creation, deletion and renaming of files from the UI. 1732 | * @remark Prefer using the `vault` API for programmatic file management 1733 | */ 1734 | fileManager: EFileManager; 1735 | // /** 1736 | // * @internal 1737 | // */ 1738 | // foldManager: any; 1739 | /** 1740 | * Manages global hotkeys 1741 | * @tutorial Can be used for manually invoking a command, or finding which hotkey is assigned to a specific key input 1742 | * @remark This should not be used for adding hotkeys to your custom commands, this can easily be done via the official API 1743 | */ 1744 | hotkeyManager: HotkeyManager; 1745 | /** 1746 | * Manager of internal 'core' plugins 1747 | * @tutorial Can be used to check whether a specific internal plugin is enabled, or grab specific parts 1748 | * from its config for simplifying your own plugin's settings 1749 | */ 1750 | internalPlugins: InternalPlugins; 1751 | /** 1752 | * Whether the application is currently running on mobile 1753 | * @remark Prefer usage of `Platform.isMobile` 1754 | * @remark Will be true if `app.emulateMobile()` was enabled 1755 | */ 1756 | isMobile: boolean; 1757 | // /** 1758 | // * @internal 1759 | // */ 1760 | // keymap: KeymapManager; 1761 | // /** 1762 | // * @internal 1763 | // */ 1764 | // loadProgress: LoadProgress; 1765 | /** 1766 | * Manages the gathering and updating of metadata for all files in the vault 1767 | * @tutorial Use for finding tags and backlinks for specific files, grabbing frontmatter properties, ... 1768 | */ 1769 | metadataCache: EMetadataCache; 1770 | /** 1771 | * Manages the frontmatter properties of the vault and the rendering of the properties 1772 | * @tutorial Fetching properties used in all frontmatter fields, may potentially be used for adding custom frontmatter widgets 1773 | */ 1774 | metadataTypeManager: MetadataTypeManager; 1775 | 1776 | // /** 1777 | // * @internal 1778 | // */ 1779 | // mobileNavbar?: MobileNavbar; 1780 | // /** 1781 | // * @internal 1782 | // */ 1783 | // mobileToolbar?: MobileToolbar; 1784 | 1785 | // /** 1786 | // * @internal Events to execute on the next frame 1787 | // */ 1788 | // nextFrameEvents: any[]; 1789 | // /** 1790 | // * @internal Timer for the next frame 1791 | // */ 1792 | // nextFrameTimer: number; 1793 | 1794 | /** 1795 | * Manages loading and enabling of community (non-core) plugins 1796 | * @tutorial Can be used to communicate with other plugins, custom plugin management, ... 1797 | * @remark Be careful when overriding loading logic, as this may result in other plugins not loading 1798 | */ 1799 | plugins: Plugins; 1800 | /** 1801 | * @internal Root keyscope of the application 1802 | */ 1803 | scope: EScope; 1804 | /** 1805 | * Manages the settings modal and its tabs 1806 | * @tutorial Can be used to open the settings modal to a specific tab, extend the settings modal functionality, ... 1807 | * @remark Do not use this to get settings values from other plugins, it is better to do this via `app.plugins.getPlugin(ID)?.settings` (check how the plugin stores its settings) 1808 | */ 1809 | setting: Setting; 1810 | // /** 1811 | // * @internal 1812 | // */ 1813 | // shareReceiver: { app: App } 1814 | // /** 1815 | // * @internal Status bar of the application 1816 | // */ 1817 | // statusBar: { app: App , containerEl: HTMLElement } 1818 | /** 1819 | * Name of the vault with version suffix 1820 | * @remark Formatted as 'NAME - Obsidian vX.Y.Z' 1821 | */ 1822 | title: string; 1823 | /** 1824 | * Manages all file operations for the vault, contains hooks for file changes, and an adapter 1825 | * for low-level file system operations 1826 | * @tutorial Used for creating your own files and folders, renaming, ... 1827 | * @tutorial Use `app.vault.adapter` for accessing files outside the vault (desktop-only) 1828 | * @remark Prefer using the regular `vault` whenever possible 1829 | */ 1830 | vault: Vault; 1831 | /** 1832 | * Manages the construction of appropriate views when opening a file of a certain type 1833 | * @remark Prefer usage of view registration via the Plugin class 1834 | */ 1835 | viewRegistry: ViewRegistry; 1836 | /** 1837 | * Manages the workspace layout, construction, rendering and manipulation of leaves 1838 | * @tutorial Used for accessing the active editor leaf, grabbing references to your views, ... 1839 | */ 1840 | workspace: Workspace; 1841 | 1842 | /** 1843 | * Sets the accent color of the application to the OS preference 1844 | */ 1845 | adaptToSystemTheme: () => void; 1846 | /** 1847 | * Sets the accent color of the application (light/dark mode) 1848 | */ 1849 | changeTheme: (theme: "moonstone" | "obsidian") => void; 1850 | /** 1851 | * Copies Obsidian URI of given file to clipboard 1852 | * @param file File to generate URI for 1853 | */ 1854 | copyObsidianUrl: (file: TFile) => void; 1855 | /** 1856 | * Disables all CSS transitions in the vault (until manually re-enabled) 1857 | */ 1858 | disableCssTransition: () => void; 1859 | /** 1860 | * Restarts Obsidian and renders workspace in mobile mode 1861 | * @tutorial Very useful for testing the rendering of your plugin on mobile devices 1862 | */ 1863 | emulateMobile: (emulate: boolean) => void; 1864 | /** 1865 | * Enables all CSS transitions in the vault 1866 | */ 1867 | enableCssTransition: () => void; 1868 | /** 1869 | * Manually fix all file links pointing towards image/audio/video resources in element 1870 | * @param element Element to fix links in 1871 | */ 1872 | fixFileLinks: (element: HTMLElement) => void; 1873 | /** 1874 | * Applies an obfuscation font to all text characters in the vault 1875 | * @tutorial Useful for hiding sensitive information or sharing pretty screenshots of your vault 1876 | * @remark Uses the `Flow Circular` font 1877 | * @remark You will have to restart the app to get normal text back 1878 | */ 1879 | garbleText: () => void; 1880 | /** 1881 | * Get the accent color of the application 1882 | * @remark Often a better alternative than `app.vault.getConfig('accentColor')` as it returns an empty string if no accent color was set 1883 | */ 1884 | getAccentColor: () => string; 1885 | /** 1886 | * Get the current title of the application 1887 | * @remark The title is based on the currently active leaf 1888 | */ 1889 | getAppTitle: () => string; 1890 | /** 1891 | * Get the URI for opening specified file in Obsidian 1892 | */ 1893 | getObsidianUrl: (file: TFile) => string; 1894 | /** 1895 | * Get currently active spellcheck languages 1896 | * @deprecated Originally spellcheck languages were stored in app settings, 1897 | * languages are now stored in `localStorage.getItem(spellcheck-languages)` 1898 | */ 1899 | getSpellcheckLanguages: () => string[]; 1900 | /** 1901 | * Get the current color scheme of the application 1902 | * @remark Identical to `app.vault.getConfig('theme')` 1903 | */ 1904 | getTheme: () => "moonstone" | "obsidian"; 1905 | /** 1906 | * Import attachments into specified folder 1907 | */ 1908 | importAttachments: ( 1909 | imports: ImportedAttachments[], 1910 | folder: TFolder 1911 | ) => Promise; 1912 | /** 1913 | * @internal Initialize the entire application using the provided FS adapter 1914 | */ 1915 | initializeWithAdapter: (adapter: EDataAdapter) => Promise; 1916 | /** 1917 | * Load a value from the localstorage given key 1918 | * @param key Key of value to load 1919 | * @remark This method is device *and* vault specific 1920 | * @tutorial Use load/saveLocalStorage for saving configuration data that needs to be unique to the current vault 1921 | */ 1922 | loadLocalStorage: (key: string) => string; 1923 | /** 1924 | * @internal Add callback to execute on next frame 1925 | */ 1926 | nextFrame: (callback: () => void) => void; 1927 | /** 1928 | * @internal Add callback to execute on next frame, and remove after execution 1929 | */ 1930 | nextFrameOnceCallback: (callback: () => void) => void; 1931 | /** 1932 | * @internal Add callback to execute on next frame with promise 1933 | */ 1934 | nextFramePromise: (callback: () => Promise) => Promise; 1935 | /** 1936 | * @internal 1937 | */ 1938 | on: (args: any[]) => EventRef; 1939 | /** 1940 | * @internal 1941 | */ 1942 | onMouseEvent: (evt: MouseEvent) => void; 1943 | /** 1944 | * @internal Execute all logged callback (called when next frame is loaded) 1945 | */ 1946 | onNextFrame: (callback: () => void) => void; 1947 | /** 1948 | * Open the help vault (or site if mobile) 1949 | */ 1950 | openHelp: () => void; 1951 | /** 1952 | * Open the vault picker 1953 | */ 1954 | openVaultChooser: () => void; 1955 | /** 1956 | * Open the file with OS defined default file browser application 1957 | */ 1958 | openWithDefaultApp: (path: string) => void; 1959 | /** 1960 | * @internal Register all basic application commands 1961 | */ 1962 | registerCommands: () => void; 1963 | /** 1964 | * @internal Register a hook for saving workspace data before unload 1965 | */ 1966 | registerQuitHook: () => void; 1967 | /** 1968 | * @internal Save attachment at default attachments location 1969 | */ 1970 | saveAttachment: ( 1971 | path: string, 1972 | extension: string, 1973 | data: ArrayBuffer 1974 | ) => Promise; 1975 | /** 1976 | * Save a value to the localstorage given key 1977 | * @param key Key of value to save 1978 | * @param value Value to save 1979 | * @remark This method is device *and* vault specific 1980 | * @tutorial Use load/saveLocalStorage for saving configuration data that needs to be unique to the current vault 1981 | */ 1982 | saveLocalStorage: (key: string, value: any) => void; 1983 | /** 1984 | * Set the accent color of the application 1985 | * @remark Also updates the CSS `--accent` variables 1986 | */ 1987 | setAccentColor: (color: string) => void; 1988 | /** 1989 | * Set the path where attachments should be stored 1990 | */ 1991 | setAttachmentFolder: (path: string) => void; 1992 | /** 1993 | * Set the spellcheck languages 1994 | */ 1995 | setSpellcheckLanguages: (languages: string[]) => void; 1996 | /** 1997 | * Set the current color scheme of the application and reload the CSS 1998 | */ 1999 | setTheme: (theme: "moonstone" | "obsidian") => void; 2000 | /** 2001 | * Open the OS file picker at path location 2002 | */ 2003 | showInFolder: (path: string) => void; 2004 | /** 2005 | * Show the release notes for provided version as a new leaf 2006 | * @param version Version to show release notes for (defaults to current version) 2007 | */ 2008 | showReleaseNotes: (version?: string) => void; 2009 | /** 2010 | * Updates the accent color and reloads the CSS 2011 | */ 2012 | updateAccentColor: () => void; 2013 | /** 2014 | * Update the font family of the application and reloads the CSS 2015 | */ 2016 | updateFontFamily: () => void; 2017 | /** 2018 | * Update the font size of the application and reloads the CSS 2019 | */ 2020 | updateFontSize: () => void; 2021 | /** 2022 | * Update the inline title rendering in notes 2023 | */ 2024 | updateInlineTitleDisplay: () => void; 2025 | /** 2026 | * Update the color scheme of the application and reloads the CSS 2027 | */ 2028 | updateTheme: () => void; 2029 | /** 2030 | * Update the view header display in notes 2031 | */ 2032 | updateViewHeaderDisplay: () => void; 2033 | } 2034 | 2035 | interface Scope { 2036 | /** 2037 | * Overridden keys that exist in this scope 2038 | */ 2039 | keys: KeyScope[]; 2040 | 2041 | /** 2042 | * @internal Scope that this scope is a child of 2043 | */ 2044 | parent: EScope | undefined; 2045 | /** 2046 | * @internal - Callback to execute when scope is matched 2047 | */ 2048 | cb: (() => boolean) | undefined; 2049 | /** 2050 | * @internal 2051 | */ 2052 | tabFocusContainer: HTMLElement | null; 2053 | /** 2054 | * @internal Execute keypress within this scope 2055 | * @param event - Keyboard event 2056 | * @param keypress - Pressed key information 2057 | */ 2058 | handleKey: (event: KeyboardEvent, keypress: KeymapInfo) => any; 2059 | /** 2060 | * @internal 2061 | * @deprecated - Executes same functionality as `Scope.register` 2062 | */ 2063 | registerKey: ( 2064 | modifiers: Modifier[], 2065 | key: string | null, 2066 | func: KeymapEventListener 2067 | ) => KeymapEventHandler; 2068 | /** 2069 | * @internal 2070 | */ 2071 | setTabFocusContainer: (container: HTMLElement) => void; 2072 | } 2073 | 2074 | class MetadataCache { 2075 | /** 2076 | * Reference to App 2077 | */ 2078 | app: App; 2079 | /** 2080 | * @internal 2081 | */ 2082 | blockCache: BlockCache; 2083 | /** 2084 | * @internal IndexedDB database 2085 | */ 2086 | db: IDBDatabase; 2087 | /** 2088 | * @internal File contents cache 2089 | */ 2090 | fileCache: Record; 2091 | /** 2092 | * @internal Amount of tasks currently in progress 2093 | */ 2094 | inProgressTaskCount: number; 2095 | /** 2096 | * @internal Whether the cache is fully loaded 2097 | */ 2098 | initialized: boolean; 2099 | /** 2100 | * @internal 2101 | */ 2102 | linkResolverQueue: any; 2103 | /** 2104 | * @internal File hash to metadata cache entry mapping 2105 | */ 2106 | metadataCache: Record; 2107 | /** 2108 | * @internal Callbacks to execute on cache clean 2109 | */ 2110 | onCleanCacheCallbacks: any[]; 2111 | /** 2112 | * @internal Mapping of filename to collection of files that share the same name 2113 | */ 2114 | uniqueFileLookup: CustomArrayDict; 2115 | /** 2116 | * @internal 2117 | */ 2118 | userIgnoreFilterCache: any; 2119 | /** 2120 | * @internal 2121 | */ 2122 | userIgnoreFilters: any; 2123 | /** 2124 | * @internal 2125 | */ 2126 | userIgnoreFiltersString: string; 2127 | /** 2128 | * Reference to Vault 2129 | */ 2130 | vault: Vault; 2131 | /** 2132 | * @internal 2133 | */ 2134 | workQueue: any; 2135 | /** 2136 | * @internal 2137 | */ 2138 | worker: Worker; 2139 | /** 2140 | * @internal 2141 | */ 2142 | workerResolve: any; 2143 | 2144 | /** 2145 | * Get all property infos of the vault 2146 | */ 2147 | getAllPropertyInfos: () => Record; 2148 | /** 2149 | * Get all backlink information for a file 2150 | */ 2151 | getBacklinksForFile: ( 2152 | file?: TFile 2153 | ) => CustomArrayDict; 2154 | /** 2155 | * Get paths of all files cached in the vault 2156 | */ 2157 | getCachedFiles: () => string[]; 2158 | /** 2159 | * Get an entry from the file cache 2160 | */ 2161 | getFileInfo: (path: string) => FileCacheEntry; 2162 | /** 2163 | * Get property values for frontmatter property key 2164 | */ 2165 | getFrontmatterPropertyValuesForKey: (key: string) => string[]; 2166 | /** 2167 | * Get all links (resolved or unresolved) in the vault 2168 | */ 2169 | getLinkSuggestions: () => { file: TFile | null; path: string }[]; 2170 | /** 2171 | * Get destination of link path 2172 | */ 2173 | getLinkpathDest: (origin: string = "", path: string) => TFile[]; 2174 | /** 2175 | * Get all links within the vault per file 2176 | */ 2177 | getLinks: () => Record; 2178 | /** 2179 | * Get all tags within the vault and their usage count 2180 | */ 2181 | getTags: () => Record; 2182 | 2183 | /** 2184 | * @internal Clear all caches to null values 2185 | */ 2186 | cleanupDeletedCache: () => void; 2187 | /** 2188 | * @internal 2189 | */ 2190 | clear: () => any; 2191 | /** 2192 | * @internal 2193 | */ 2194 | computeMetadataAsync: (e: any) => Promise; 2195 | /** 2196 | * @internal Remove all entries that contain deleted path 2197 | */ 2198 | deletePath: (path: string) => void; 2199 | /** 2200 | * @internal Initialize Database connection and load up caches 2201 | */ 2202 | initialize: () => Promise; 2203 | /** 2204 | * @internal Check whether there are no cache tasks in progress 2205 | */ 2206 | isCacheClean: () => boolean; 2207 | /** 2208 | * @internal Check whether file can support metadata (by checking extension support) 2209 | */ 2210 | isSupportedFile: (file: TFile) => boolean; 2211 | /** 2212 | * @internal Check whether string is part of the user ignore filters 2213 | */ 2214 | isUserIgnored: (filter: any) => boolean; 2215 | /** 2216 | * Iterate over all link references in the vault with callback 2217 | */ 2218 | iterateReferences: (callback: (path: string) => void) => void; 2219 | /** 2220 | * @internal 2221 | */ 2222 | linkResolver: () => void; 2223 | /** 2224 | * @internal Execute onCleanCache callbacks if cache is clean 2225 | */ 2226 | onCleanCache: () => void; 2227 | /** 2228 | * @internal On creation of the cache: update metadata cache 2229 | */ 2230 | onCreate: (file: TFile) => void; 2231 | /** 2232 | * @internal On creation or modification of the cache: update metadata cache 2233 | */ 2234 | onCreateOrModify: (file: TFile) => void; 2235 | /** 2236 | * @internal On deletion of the cache: update metadata cache 2237 | */ 2238 | onDelete: (file: TFile) => void; 2239 | /** 2240 | * @internal 2241 | */ 2242 | onReceiveMessageFromWorker: (e: any) => void; 2243 | /** 2244 | * @internal On rename of the cache: update metadata cache 2245 | */ 2246 | onRename: (file: TFile, oldPath: string) => void; 2247 | /** 2248 | * @internal Check editor for unresolved links and mark these as unresolved 2249 | */ 2250 | resolveLinks: (editor: Element) => void; 2251 | /** 2252 | * @internal Update file cache entry and sync to indexedDB 2253 | */ 2254 | saveFileCache: (path: string, entry: FileCacheEntry) => void; 2255 | /** 2256 | * @internal Update metadata cache entry and sync to indexedDB 2257 | */ 2258 | saveMetaCache: (path: string, entry: CachedMetadata) => void; 2259 | /** 2260 | * @internal Show a notice that the cache is being rebuilt 2261 | */ 2262 | showIndexingNotice: () => void; 2263 | /** 2264 | * @internal 2265 | */ 2266 | trigger: (e: any) => void; 2267 | /** 2268 | * @internal Re-resolve all links for changed path 2269 | */ 2270 | updateRelatedLinks: (path: string) => void; 2271 | /** 2272 | * @internal Update user ignore filters from settings 2273 | */ 2274 | updateUserIgnoreFilters: () => void; 2275 | /** 2276 | * @internal Bind actions to listeners on vault 2277 | */ 2278 | watchVaultChanges: () => void; 2279 | /** 2280 | * @internal Send message to worker to update metadata cache 2281 | */ 2282 | work: (cacheEntry: any) => void; 2283 | } 2284 | 2285 | interface SettingTab { 2286 | /** 2287 | * Unique ID of the tab 2288 | */ 2289 | id: string; 2290 | /** 2291 | * Sidebar name of the tab 2292 | */ 2293 | name: string; 2294 | /** 2295 | * Sidebar navigation element of the tab 2296 | */ 2297 | navEl: HTMLElement; 2298 | /** 2299 | * Reference to the settings modal 2300 | */ 2301 | setting: Setting; 2302 | /** 2303 | * Reference to the plugin that initialised the tab 2304 | * @if Tab is a plugin tab 2305 | */ 2306 | plugin?: Plugin; 2307 | /** 2308 | * Reference to installed plugins element 2309 | * @if Tab is the community plugins tab 2310 | */ 2311 | installedPluginsEl?: HTMLElement; 2312 | 2313 | // TODO: Editor, Files & Links, Appearance and About all have private properties too 2314 | } 2315 | 2316 | interface FileManager { 2317 | /** 2318 | * Reference to App 2319 | */ 2320 | app: App; 2321 | /** 2322 | * Creates a new Markdown file in specified location and opens it in a new view 2323 | * @param path - Path to the file to create (missing folders will be created) 2324 | * @param manner - Where to open the view containing the new file 2325 | */ 2326 | createAndOpenMarkdownFile: ( 2327 | path: string, 2328 | location: PaneType 2329 | ) => Promise; 2330 | /** 2331 | * Create a new file in the vault at specified location 2332 | * @param location - Location to create the file in, defaults to root 2333 | * @param filename - Name of the file to create, defaults to "Untitled" (incremented if file already exists) 2334 | * @param extension - Extension of the file to create, defaults to "md" 2335 | * @param contents - Contents of the file to create, defaults to empty string 2336 | */ 2337 | createNewFile: ( 2338 | location: TFolder = null, 2339 | filename: string = null, 2340 | extension: string = "md", 2341 | contents: string = "" 2342 | ) => Promise; 2343 | /** 2344 | * Creates a new untitled folder in the vault at specified location 2345 | * @param location - Location to create the folder in, defaults to root 2346 | */ 2347 | createNewFolder: (location: TFolder = null) => Promise; 2348 | /** 2349 | * Creates a new Markdown file in the vault at specified location 2350 | */ 2351 | createNewMarkdownFile: ( 2352 | location: TFolder = null, 2353 | filename: string = null, 2354 | contents: string = "" 2355 | ) => Promise; 2356 | /** 2357 | * Creates a new Markdown file based on linktext and path 2358 | * @param filename - Name of the file to create 2359 | * @param path - Path to where the file should be created 2360 | */ 2361 | createNewMarkdownFileFromLinktext: ( 2362 | filename: string, 2363 | path: string 2364 | ) => Promise; 2365 | /** 2366 | * @internal 2367 | */ 2368 | getAllLinkResolutions: () => []; 2369 | /** 2370 | * Gets the folder that new markdown files should be saved to, based on the current settings 2371 | * @param path - The path of the current opened/focused file, used when the user 2372 | * wants new files to be created in the same folder as the current file 2373 | */ 2374 | getMarkdownNewFileParent: (path: string) => TFolder; 2375 | /** 2376 | * Insert text into a file 2377 | * @param file - File to insert text into 2378 | * @param primary_text - Text to insert (will not be inserted if secondary_text exists) 2379 | * @param basename - ??? 2380 | * @param secondary_text - Text to insert (always inserted) 2381 | * @param atStart - Whether to insert text at the start or end of the file 2382 | */ 2383 | insertTextIntoFile: ( 2384 | file: TFile, 2385 | primary_text: string, 2386 | basename: string, 2387 | secondary_text: string, 2388 | atStart: boolean = true 2389 | ) => Promise; 2390 | /** 2391 | * Iterate over all links in the vault with callback 2392 | * @param callback - Callback to execute for each link 2393 | */ 2394 | iterateAllRefs: ( 2395 | callback: (path: string, link: PositionedReference) => void 2396 | ) => void; 2397 | /** 2398 | * Merge two files 2399 | * @param file - File to merge to 2400 | * @param otherFile - File to merge from 2401 | * @param override - If not empty, will override the contents of the file with this string 2402 | * @param atStart - Whether to insert text at the start or end of the file 2403 | */ 2404 | mergeFile: ( 2405 | file: TFile, 2406 | otherFile: TFile, 2407 | override: string, 2408 | atStart: boolean 2409 | ) => Promise; 2410 | /** 2411 | * Prompt the user to delete a file 2412 | */ 2413 | promptForDeletion: (file: TFile) => Promise; 2414 | /** 2415 | * Prompt the user to rename a file 2416 | */ 2417 | promptForFileRename: (file: TFile) => Promise; 2418 | /** 2419 | * @internal 2420 | * Register an extension to be the parent for a specific file type 2421 | */ 2422 | registerFileParentCreator: ( 2423 | extension: string, 2424 | location: TFolder 2425 | ) => void; 2426 | /** 2427 | * @internal 2428 | * @param callback - Callback to execute for each link 2429 | */ 2430 | runAsyncLinkUpdate: (callback: (link: LinkUpdate) => any) => void; 2431 | /** 2432 | * @internal 2433 | * @param path 2434 | * @param data 2435 | */ 2436 | storeTextFileBackup: (path: string, data: string) => void; 2437 | /** 2438 | * Remove a file and put it in the trash (no confirmation modal) 2439 | */ 2440 | trashFile: (file: TFile) => Promise; 2441 | /** 2442 | * @internal: Unregister extension as root input directory for file type 2443 | */ 2444 | unregisterFileCreator: (extension: string) => void; 2445 | /** 2446 | * @internal 2447 | */ 2448 | updateAllLinks: (links: any[]) => Promise; 2449 | /** 2450 | * @internal 2451 | */ 2452 | updateInternalLinks: (data: any) => any; 2453 | 2454 | /** 2455 | * @internal 2456 | */ 2457 | fileParentCreatorByType: Map any>; 2458 | /** 2459 | * @internal 2460 | */ 2461 | inProgressUpdates: null | any[]; 2462 | /** 2463 | * @internal 2464 | */ 2465 | linkUpdaters: Map any>; 2466 | /** 2467 | * @internal 2468 | */ 2469 | updateQueue: Map any>; 2470 | /** 2471 | * Reference to Vault 2472 | */ 2473 | vault: Vault; 2474 | } 2475 | 2476 | interface Modal { 2477 | /** 2478 | * @internal Background applied to application to dim it 2479 | */ 2480 | bgEl: HTMLElement; 2481 | /** 2482 | * @internal Opacity percentage of the background 2483 | */ 2484 | bgOpacity: number; 2485 | /** 2486 | * @internal Whether the background is being dimmed 2487 | */ 2488 | dimBackground: boolean; 2489 | /** 2490 | * @internal Modal container element 2491 | */ 2492 | modalEl: HTMLElement; 2493 | /** 2494 | * @internal Selection logic handler 2495 | */ 2496 | selection: WindowSelection; 2497 | /** 2498 | * Reference to the global Window object 2499 | */ 2500 | win: Window; 2501 | 2502 | /** 2503 | * @internal On escape key press close modal 2504 | */ 2505 | onEscapeKey: () => void; 2506 | /** 2507 | * @internal On closing of the modal 2508 | */ 2509 | onWindowClose: () => void; 2510 | /** 2511 | * @internal Set the background opacity of the dimmed background 2512 | * @param opacity Opacity percentage 2513 | */ 2514 | setBackgroundOpacity: (opacity: string) => this; 2515 | /** 2516 | * @internal Set the content of the modal 2517 | * @param content Content to set 2518 | */ 2519 | setContent: (content: HTMLElement | string) => this; 2520 | /** 2521 | * @internal Set whether the background should be dimmed 2522 | * @param dim Whether the background should be dimmed 2523 | */ 2524 | setDimBackground: (dim: boolean) => this; 2525 | /** 2526 | * @internal Set the title of the modal 2527 | * @param title Title to set 2528 | */ 2529 | setTitle: (title: string) => this; 2530 | } 2531 | 2532 | interface Setting extends Modal { 2533 | /** 2534 | * Current active tab of the settings modal 2535 | */ 2536 | activateTab: ESettingTab; 2537 | /** 2538 | * @internal Container element containing the community plugins 2539 | */ 2540 | communityPluginTabContainer: HTMLElement; 2541 | /** 2542 | * @internal Container element containing the community plugins header 2543 | */ 2544 | communityPluginTabHeaderGroup: HTMLElement; 2545 | /** 2546 | * Previously opened tab ID 2547 | */ 2548 | lastTabId: string; 2549 | /** 2550 | * List of all plugin tabs (core and community, ordered by precedence) 2551 | */ 2552 | pluginTabs: ESettingTab[]; 2553 | /** 2554 | * List of all core settings tabs (editor, files & links, ...) 2555 | */ 2556 | settingTabs: ESettingTab[]; 2557 | /** 2558 | * @internal Container element containing the core settings 2559 | */ 2560 | tabContainer: HTMLElement; 2561 | /** 2562 | * @internal Container for currently active settings tab 2563 | */ 2564 | tabContentContainer: HTMLElement; 2565 | /** 2566 | * @internal Container for all settings tabs 2567 | */ 2568 | tabHeadersEl: HTMLElement; 2569 | 2570 | /** 2571 | * Open a specific tab by ID 2572 | * @param id ID of the tab to open 2573 | */ 2574 | openTabById: (id: string) => void; 2575 | /** 2576 | * @internal Add a new plugin tab to the settings modal 2577 | * @param tab Tab to add 2578 | */ 2579 | addSettingTab: (tab: ESettingTab) => void; 2580 | /** 2581 | * @internal Closes the currently active tab 2582 | */ 2583 | closeActiveTab: () => void; 2584 | /** 2585 | * @internal Check whether tab is a plugin tab 2586 | * @param tab Tab to check 2587 | */ 2588 | isPluginSettingTab: (tab: ESettingTab) => boolean; 2589 | /** 2590 | * @internal Open a specific tab by tab reference 2591 | * @param tab Tab to open 2592 | */ 2593 | openTab: (tab: ESettingTab) => void; 2594 | /** 2595 | * @internal Remove a plugin tab from the settings modal 2596 | * @param tab Tab to remove 2597 | */ 2598 | removeSettingTab: (tab: ESettingTab) => void; 2599 | /** 2600 | * @internal Update the title of the modal 2601 | * @param tab Tab to update the title to 2602 | */ 2603 | updateModalTitle: (tab: ESettingTab) => void; 2604 | /** 2605 | * @internal Update a tab section 2606 | */ 2607 | updatePluginSection: () => void; 2608 | } 2609 | 2610 | interface DataAdapter { 2611 | /** 2612 | * Base OS path for the vault (e.g. /home/user/vault, or C:\Users\user\documents\vault) 2613 | */ 2614 | basePath: string; 2615 | /** 2616 | * @internal 2617 | */ 2618 | btime: { btime: (path: string, btime: number) => void }; 2619 | /** 2620 | * Mapping of file/folder path to vault entry, includes non-MD files 2621 | */ 2622 | files: Record; 2623 | /** 2624 | * Reference to node fs module 2625 | */ 2626 | fs?: fs; 2627 | /** 2628 | * Reference to node fs:promises module 2629 | */ 2630 | fsPromises?: fsPromises; 2631 | /** 2632 | * @internal 2633 | */ 2634 | insensitive: boolean; 2635 | /** 2636 | * Reference to electron ipcRenderer module 2637 | */ 2638 | ipcRenderer?: IpcRenderer; 2639 | /** 2640 | * Reference to node path module 2641 | */ 2642 | path: path; 2643 | /** 2644 | * @internal 2645 | */ 2646 | promise: Promise; 2647 | /** 2648 | * Reference to node URL module 2649 | */ 2650 | url: URL; 2651 | /** 2652 | * @internal 2653 | */ 2654 | watcher: any; 2655 | /** 2656 | * @internal 2657 | */ 2658 | watchers: Record; 2659 | 2660 | /** 2661 | * @internal Apply data write options to file 2662 | * @param normalizedPath Path to file 2663 | * @param options Data write options 2664 | */ 2665 | applyWriteOptions: ( 2666 | normalizedPath: string, 2667 | options: DataWriteOptions 2668 | ) => Promise; 2669 | /** 2670 | * Get base path of vault (OS path) 2671 | */ 2672 | getBasePath: () => string; 2673 | /** 2674 | * Get full path of file (OS path) 2675 | * @param normalizedPath Path to file 2676 | * @return URL path to file 2677 | */ 2678 | getFilePath: (normalizedPath: string) => URL; 2679 | /** 2680 | * Get full path of file (OS path) 2681 | * @param normalizedPath Path to file 2682 | * @return string full path to file 2683 | */ 2684 | getFullPath: (normalizedPath: string) => string; 2685 | /** 2686 | * Get full path of file (OS path) 2687 | * @param normalizedPath Path to file 2688 | * @return string full path to file 2689 | */ 2690 | getFullRealPath: (normalizedPath: string) => string; 2691 | /** 2692 | * @internal Get resource path of file (URL path) 2693 | * @param normalizedPath Path to file 2694 | * @return string URL of form: app://FILEHASH/path/to/file 2695 | */ 2696 | getResourcePath: (normalizedPath: string) => string; 2697 | /** 2698 | * @internal Handles vault events 2699 | */ 2700 | handler: () => void; 2701 | /** 2702 | * @internal Kill file system action due to timeout 2703 | */ 2704 | kill: () => void; 2705 | /** 2706 | * @internal 2707 | */ 2708 | killLastAction: () => void; 2709 | /** 2710 | * @internal Generates `this.files` from the file system 2711 | */ 2712 | listAll: () => Promise; 2713 | /** 2714 | * @internal Generates `this.files` for specific directory of the vault 2715 | */ 2716 | listRecursive: (normalizedPath: string) => Promise; 2717 | /** 2718 | * @internal Helper function for `listRecursive` reads children of directory 2719 | * @param normalizedPath Path to directory 2720 | */ 2721 | listRecursiveChild: (normalizedPath: string) => Promise; 2722 | /** 2723 | * @internal 2724 | */ 2725 | onFileChange: (normalizedPath: string) => void; 2726 | /** 2727 | * @internal 2728 | */ 2729 | queue: (cb: any) => Promise; 2730 | 2731 | /** 2732 | * @internal 2733 | */ 2734 | reconcileDeletion: ( 2735 | normalizedPath: string, 2736 | normalizedNewPath: string, 2737 | option: boolean 2738 | ) => void; 2739 | /** 2740 | * @internal 2741 | */ 2742 | reconcileFile: ( 2743 | normalizedPath: string, 2744 | normalizedNewPath: string, 2745 | option: boolean 2746 | ) => void; 2747 | /** 2748 | * @internal 2749 | */ 2750 | reconcileFileCreation: ( 2751 | normalizedPath: string, 2752 | normalizedNewPath: string, 2753 | option: boolean 2754 | ) => void; 2755 | /** 2756 | * @internal 2757 | */ 2758 | reconcileFileInternal: ( 2759 | normalizedPath: string, 2760 | normalizedNewPath: string 2761 | ) => void; 2762 | /** 2763 | * @internal 2764 | */ 2765 | reconcileFolderCreation: ( 2766 | normalizedPath: string, 2767 | normalizedNewPath: string 2768 | ) => void; 2769 | /** 2770 | * @internal 2771 | */ 2772 | reconcileInternalFile: (normalizedPath: string) => void; 2773 | /** 2774 | * @internal 2775 | */ 2776 | reconcileSymbolicLinkCreation: ( 2777 | normalizedPath: string, 2778 | normalizedNewPath: string 2779 | ) => void; 2780 | /** 2781 | * @internal Remove file from files listing and trigger deletion event 2782 | */ 2783 | removeFile: (normalizedPath: string) => void; 2784 | /** 2785 | * @internal 2786 | */ 2787 | startWatchpath: (normalizedPath: string) => Promise; 2788 | /** 2789 | * @internal Remove all listeners 2790 | */ 2791 | stopWatch: () => void; 2792 | /** 2793 | * @internal Remove listener on specific path 2794 | */ 2795 | stopWatchPath: (normalizedPath: string) => void; 2796 | /** 2797 | * @internal Set whether OS is insensitive to case 2798 | */ 2799 | testInsensitive: () => void; 2800 | /** 2801 | * @internal 2802 | */ 2803 | thingsHappening: () => void; 2804 | /** 2805 | * @internal Trigger an event on handler 2806 | */ 2807 | trigger: (any) => void; 2808 | /** 2809 | * @internal 2810 | */ 2811 | update: (normalizedPath: string) => any; 2812 | /** 2813 | * @internal Add change watcher to path 2814 | */ 2815 | watch: (normalizedPath: string) => Promise; 2816 | /** 2817 | * @internal Watch recursively for changes 2818 | */ 2819 | watchHiddenRecursive: (normalizedPath: string) => Promise; 2820 | } 2821 | 2822 | interface Workspace { 2823 | /** 2824 | * Currently active tab group 2825 | */ 2826 | activeTabGroup: WorkspaceTabs; 2827 | /** 2828 | * Reference to App 2829 | */ 2830 | app: App; 2831 | /** 2832 | * @internal 2833 | */ 2834 | backlinkInDocument?: any; 2835 | /** 2836 | * Registered CodeMirror editor extensions, to be applied to all CM instances 2837 | */ 2838 | editorExtensions: Extension[]; 2839 | /** 2840 | * @internal 2841 | */ 2842 | editorSuggest: { 2843 | currentSuggest?: EditorSuggest; 2844 | suggests: EditorSuggest[]; 2845 | }; 2846 | /** 2847 | * @internal 2848 | */ 2849 | floatingSplit: WorkspaceSplit; 2850 | /** 2851 | * @internal 2852 | */ 2853 | hoverLinkSources: Record; 2854 | /** 2855 | * Last opened file in the vault 2856 | */ 2857 | lastActiveFile: TFile; 2858 | /** 2859 | * @internal 2860 | */ 2861 | lastTabGroupStacked: boolean; 2862 | /** 2863 | * @internal 2864 | */ 2865 | layoutItemQueue: any[]; 2866 | /** 2867 | * Workspace has finished loading 2868 | */ 2869 | layoutReady: boolean; 2870 | /** 2871 | * @internal 2872 | */ 2873 | leftSidebarToggleButtonEl: HTMLElement; 2874 | /** 2875 | * @internal Array of renderCallbacks 2876 | */ 2877 | mobileFileInfos: any[]; 2878 | /** 2879 | * @internal 2880 | */ 2881 | onLayoutReadyCallbacks?: any; 2882 | /** 2883 | * Protocol handlers registered on the workspace 2884 | */ 2885 | protocolHandlers: Map; 2886 | /** 2887 | * Tracks last opened files in the vault 2888 | */ 2889 | recentFileTracker: RecentFileTracker; 2890 | /** 2891 | * @internal 2892 | */ 2893 | rightSidebarToggleButtonEl: HTMLElement; 2894 | /** 2895 | * @internal Keyscope registered to the vault 2896 | */ 2897 | scope: EScope; 2898 | /** 2899 | * List of states that were closed and may be reopened 2900 | */ 2901 | undoHistory: StateHistory[]; 2902 | 2903 | /** 2904 | * @internal Change active leaf and trigger leaf change event 2905 | */ 2906 | activeLeafEvents: () => void; 2907 | /** 2908 | * @internal Add file to mobile file info 2909 | */ 2910 | addMobileFileInfo: (file: any) => void; 2911 | /** 2912 | * @internal Clear layout of workspace and destruct all leaves 2913 | */ 2914 | clearLayout: () => Promise; 2915 | /** 2916 | * @internal Create a leaf in the selected tab group or last used tab group 2917 | * @param tabs Tab group to create leaf in 2918 | */ 2919 | createLeafInTabGroup: (tabs?: WorkspaceTabs) => WorkspaceLeaf; 2920 | /** 2921 | * @internal Deserialize workspace entries into actual Leaf objects 2922 | * @param leaf Leaf entry to deserialize 2923 | * @param ribbon Whether the leaf belongs to the left or right ribbon 2924 | */ 2925 | deserializeLayout: ( 2926 | leaf: LeafEntry, 2927 | ribbon?: "left" | "right" 2928 | ) => Promise; 2929 | /** 2930 | * @internal Reveal leaf in side ribbon with specified view type and state 2931 | * @param type View type of leaf 2932 | * @param ribbon Side ribbon to reveal leaf in 2933 | * @param viewstate Open state of leaf 2934 | */ 2935 | ensureSideLeaf: ( 2936 | type: string, 2937 | ribbon: "left" | "right", 2938 | viewstate: OpenViewState 2939 | ) => void; 2940 | /** 2941 | * Get active file view if exists 2942 | */ 2943 | getActiveFileView: () => FileView | null; 2944 | /** 2945 | * @deprecated Use `getActiveViewOfType` instead 2946 | */ 2947 | getActiveLeafOfViewType(type: Constructor): T | null; 2948 | /** 2949 | * Get adjacent leaf in specified direction 2950 | * @remark Potentially does not work 2951 | */ 2952 | getAdjacentLeafInDirection: ( 2953 | leaf: WorkspaceLeaf, 2954 | direction: "top" | "bottom" | "left" | "right" 2955 | ) => WorkspaceLeaf | null; 2956 | /** 2957 | * @internal Get the direction where the leaf should be dropped on dragevent 2958 | */ 2959 | getDropDirection: ( 2960 | e: DragEvent, 2961 | rect: DOMRect, 2962 | directions: ["left", "right"], 2963 | leaf: WorkspaceLeaf 2964 | ) => "left" | "right" | "top" | "bottom" | "center"; 2965 | /** 2966 | * @internal Get the leaf where the leaf should be dropped on dragevent 2967 | * @param e Drag event 2968 | */ 2969 | getDropLocation: (e: DragEvent) => WorkspaceLeaf | null; 2970 | /** 2971 | * Get the workspace split for the currently focused container 2972 | */ 2973 | getFocusedContainer: () => WorkspaceSplit; 2974 | /** 2975 | * Get n last opened files of type (defaults to 10) 2976 | */ 2977 | getRecentFiles: ({ 2978 | showMarkdown: boolean, 2979 | showCanvas: boolean, 2980 | showNonImageAttachments: boolean, 2981 | showImages: boolean, 2982 | maxCount: number, 2983 | }?) => string[]; 2984 | /** 2985 | * Get leaf in the side ribbon/dock and split if necessary 2986 | * @param sideRibbon Side ribbon to get leaf from 2987 | * @param split Whether to split the leaf if it does not exist 2988 | */ 2989 | getSideLeaf: ( 2990 | sideRibbon: WorkspaceSidedock | WorkspaceMobileDrawer, 2991 | split: boolean 2992 | ) => WorkspaceLeaf; 2993 | /** 2994 | * @internal 2995 | */ 2996 | handleExternalLinkContextMenu: (menu: Menu, linkText: string) => void; 2997 | /** 2998 | * @internal 2999 | */ 3000 | handleLinkContextMenu: ( 3001 | menu: Menu, 3002 | linkText: string, 3003 | sourcePath: string 3004 | ) => void; 3005 | /** 3006 | * @internal Check if leaf has been attached to the workspace 3007 | */ 3008 | isAttached: (leaf?: WorkspaceLeaf) => boolean; 3009 | /** 3010 | * Iterate the leaves of a split 3011 | */ 3012 | iterateLeaves: ( 3013 | split: WorkspaceSplit, 3014 | callback: (leaf: WorkspaceLeaf) => any 3015 | ) => void; 3016 | /** 3017 | * Iterate the tabs of a split till meeting a condition 3018 | */ 3019 | iterateTabs: ( 3020 | tabs: WorkspaceSplit | WorkspaceSplit[], 3021 | cb: (leaf) => boolean 3022 | ) => boolean; 3023 | /** 3024 | * @internal Load workspace from disk and initialize 3025 | */ 3026 | loadLayout: () => Promise; 3027 | /** 3028 | * @internal 3029 | */ 3030 | on: (args: any[]) => EventRef; 3031 | /** 3032 | * @internal Handles drag event on leaf 3033 | */ 3034 | onDragLeaf: (e: DragEvent, leaf: WorkspaceLeaf) => void; 3035 | /** 3036 | * @internal Handles layout change and saves layout to disk 3037 | */ 3038 | onLayoutChange: (leaf?: WorkspaceLeaf) => void; 3039 | /** 3040 | * @internal 3041 | */ 3042 | onLinkContextMenu: (args: any[]) => void; 3043 | /** 3044 | * @internal 3045 | */ 3046 | onQuickPreview: (args: any[]) => void; 3047 | /** 3048 | * @internal 3049 | */ 3050 | onResize: () => void; 3051 | /** 3052 | * @internal 3053 | */ 3054 | onStartLink: (leaf: WorkspaceLeaf) => void; 3055 | /** 3056 | * Open a leaf in a popup window 3057 | * @remark Prefer usage of `app.workspace.openPopoutLeaf` 3058 | */ 3059 | openPopout: (data?: WorkspaceWindowInitData) => WorkspaceWindow; 3060 | /** 3061 | * @internal Push leaf change to history 3062 | */ 3063 | pushUndoHistory: ( 3064 | leaf: WorkspaceLeaf, 3065 | parentID: string, 3066 | rootID: string 3067 | ) => void; 3068 | /** 3069 | * @internal Get drag event target location 3070 | */ 3071 | recursiveGetTarget: ( 3072 | e: DragEvent, 3073 | leaf: WorkspaceLeaf 3074 | ) => WorkspaceTabs | null; 3075 | /** 3076 | * @internal Register a CodeMirror editor extension 3077 | * @remark Prefer registering the extension via the Plugin class 3078 | */ 3079 | registerEditorExtension: (extension: Extension) => void; 3080 | /** 3081 | * @internal Registers hover link source 3082 | */ 3083 | registerHoverLinkSource: (key: string, source: HoverLinkSource) => void; 3084 | /** 3085 | * @internal Registers Obsidian protocol handler 3086 | */ 3087 | registerObsidianProtocolHandler: ( 3088 | protocol: string, 3089 | handler: ObsidianProtocolHandler 3090 | ) => void; 3091 | /** 3092 | * @internal Constructs hook for receiving URI actions 3093 | */ 3094 | registerUriHook: () => void; 3095 | /** 3096 | * @internal Request execution of activeLeaf change events 3097 | */ 3098 | requestActiveLeafEvents: () => void; 3099 | /** 3100 | * @internal Request execution of resize event 3101 | */ 3102 | requestResize: () => void; 3103 | /** 3104 | * @internal Request execution of layout save event 3105 | */ 3106 | requestSaveLayout: () => void; 3107 | /** 3108 | * @internal Request execution of layout update event 3109 | */ 3110 | requestUpdateLayout: () => void; 3111 | /** 3112 | * Save workspace layout to disk 3113 | */ 3114 | saveLayout: () => Promise; 3115 | /** 3116 | * @internal Use deserialized layout data to reconstruct the workspace 3117 | */ 3118 | setLayout: (data: SerializedWorkspace) => Promise; 3119 | /** 3120 | * @internal Split leaves in specified direction 3121 | */ 3122 | splitLeaf: ( 3123 | leaf: WorkspaceLeaf, 3124 | newleaf: WorkspaceLeaf, 3125 | direction?: SplitDirection, 3126 | before?: boolean 3127 | ) => void; 3128 | /** 3129 | * Split provided leaf, or active leaf if none provided 3130 | */ 3131 | splitLeafOrActive: ( 3132 | leaf?: WorkspaceLeaf, 3133 | direction?: SplitDirection 3134 | ) => void; 3135 | /** 3136 | * @internal 3137 | */ 3138 | trigger: (e: any) => void; 3139 | /** 3140 | * @internal Unregister a CodeMirror editor extension 3141 | */ 3142 | unregisterEditorExtension: (extension: Extension) => void; 3143 | /** 3144 | * @internal Unregister hover link source 3145 | */ 3146 | unregisterHoverLinkSource: (key: string) => void; 3147 | /** 3148 | * @internal Unregister Obsidian protocol handler 3149 | */ 3150 | unregisterObsidianProtocolHandler: (protocol: string) => void; 3151 | /** 3152 | * @internal 3153 | */ 3154 | updateFrameless: () => void; 3155 | /** 3156 | * @internal Invoke workspace layout update, redraw and save 3157 | */ 3158 | updateLayout: () => void; 3159 | /** 3160 | * @internal Update visibility of tab group 3161 | */ 3162 | updateMobileVisibleTabGroup: () => void; 3163 | /** 3164 | * Update the internal title of the application 3165 | * @remark This title is shown as the application title in the OS taskbar 3166 | */ 3167 | updateTitle: () => void; 3168 | } 3169 | 3170 | interface Vault { 3171 | /** 3172 | * Low-level file system adapter for read and write operations 3173 | * @tutorial Can be used to read binaries, or files not located directly within the vault 3174 | */ 3175 | adapter: DataAdapter; 3176 | /** 3177 | * @internal Max size of the cache in bytes 3178 | */ 3179 | cacheLimit: number; 3180 | /** 3181 | * Object containing all config settings for the vault (editor, appearance, ... settings) 3182 | * @remark Prefer usage of `app.vault.getConfig(key)` to get settings, config does not contain 3183 | * settings that were not changed from their default value 3184 | */ 3185 | config: AppVaultConfig; 3186 | /** 3187 | * @internal Timestamp of the last config change 3188 | */ 3189 | configTs: number; 3190 | /** 3191 | * @internal Mapping of path to Obsidian folder or file structure 3192 | */ 3193 | fileMap: Record; 3194 | 3195 | on(name: "config-changed", callback: () => void, ctx?: any): EventRef; 3196 | 3197 | /** 3198 | * @internal Add file as child/parent to respective folders 3199 | */ 3200 | addChild: (file: TAbstractFile) => void; 3201 | /** 3202 | * @internal Check whether new file path is available 3203 | */ 3204 | checkForDuplicate: (file: TAbstractFile, newPath: string) => boolean; 3205 | /** 3206 | * @internal Check whether path has valid formatting (no dots/spaces at end, ...) 3207 | */ 3208 | checkPath: (path: string) => boolean; 3209 | /** 3210 | * @internal Remove a vault config file 3211 | */ 3212 | deleteConfigJson: (configFile: string) => Promise; 3213 | /** 3214 | * Check whether a file exists in the vault 3215 | */ 3216 | exists: (file: TAbstractFile, senstive?: boolean) => Promise; 3217 | /** 3218 | * @internal 3219 | */ 3220 | generateFiles: (any) => Promise; 3221 | /** 3222 | * Get an abstract file by path, insensitive to case 3223 | */ 3224 | getAbstractFileByPathInsensitive: ( 3225 | path: string 3226 | ) => TAbstractFile | null; 3227 | /** 3228 | * @internal Get path for file that does not conflict with other existing files 3229 | */ 3230 | getAvailablePath: (path: string, extension: string) => string; 3231 | /** 3232 | * @internal Get path for attachment that does not conflict with other existing files 3233 | */ 3234 | getAvailablePathForAttachments: ( 3235 | filename: string, 3236 | file: TAbstractFile, 3237 | extension: string 3238 | ) => string; 3239 | /** 3240 | * Get value from config by key 3241 | * @remark Default value will be selected if config value was not manually changed 3242 | * @param key Key of config value 3243 | */ 3244 | getConfig: (string: ConfigItem) => any; 3245 | /** 3246 | * Get path to config file (relative to vault root) 3247 | */ 3248 | getConfigFile: (configFile: string) => string; 3249 | /** 3250 | * Get direct parent of file 3251 | * @param file File to get parent of 3252 | */ 3253 | getDirectParent: (file: TAbstractFile) => TFolder | null; 3254 | /** 3255 | * @internal Check whether files map cache is empty 3256 | */ 3257 | isEmpty: () => boolean; 3258 | /** 3259 | * @internal Iterate over the files and read them 3260 | */ 3261 | iterateFiles: (files: TFile[], cachedRead: boolean) => void; 3262 | /** 3263 | * @internal Load vault adapter 3264 | */ 3265 | load: () => Promise; 3266 | /** 3267 | * @internal Listener for all events on the vault 3268 | */ 3269 | onChange: (eventType: string, path: string, x: any, y: any) => void; 3270 | /** 3271 | * Read a config file from the vault and parse it as JSON 3272 | * @param config Name of config file 3273 | */ 3274 | readConfigJson: (config: string) => Promise; 3275 | /** 3276 | * Read a config file (full path) from the vault and parse it as JSON 3277 | * @param path Full path to config file 3278 | */ 3279 | readJson: (path: string) => Promise; 3280 | /** 3281 | * Read a plugin config file (full path relative to vault root) from the vault and parse it as JSON 3282 | * @param path Full path to plugin config file 3283 | */ 3284 | readPluginData: (path: string) => Promise; 3285 | /** 3286 | * Read a file from the vault as a string 3287 | * @param path Path to file 3288 | */ 3289 | readRaw: (path: string) => Promise; 3290 | /** 3291 | * @internal Reload all config files 3292 | */ 3293 | reloadConfig: () => void; 3294 | /** 3295 | * @internal Remove file as child/parent from respective folders 3296 | * @param file File to remove 3297 | */ 3298 | removeChild: (file: TAbstractFile) => void; 3299 | /** 3300 | * @internal Get the file by absolute path 3301 | * @param path Path to file 3302 | */ 3303 | resolveFilePath: (path: string) => TAbstractFile | null; 3304 | /** 3305 | * @internal Get the file by Obsidian URL 3306 | */ 3307 | resolveFileUrl: (url: string) => TAbstractFile | null; 3308 | /** 3309 | * @internal Debounced function for saving config 3310 | */ 3311 | requestSaveConfig: () => void; 3312 | /** 3313 | * @internal Save app and appearance configs to disk 3314 | */ 3315 | saveConfig: () => Promise; 3316 | /** 3317 | * Set value of config by key 3318 | * @param key Key of config value 3319 | * @param value Value to set 3320 | */ 3321 | setConfig: (key: ConfigItem, value: any) => void; 3322 | /** 3323 | * Set where the config files are stored (relative to vault root) 3324 | * @param configDir Path to config files 3325 | */ 3326 | setConfigDir: (configDir: string) => void; 3327 | /** 3328 | * @internal Set file cache limit 3329 | */ 3330 | setFileCacheLimit: (limit: number) => void; 3331 | /** 3332 | * @internal Load all config files into memory 3333 | */ 3334 | setupConfig: () => Promise; 3335 | /** 3336 | * @internal Trigger an event on handler 3337 | */ 3338 | trigger: (type: string) => void; 3339 | /** 3340 | * Write a config file to disk 3341 | * @param config Name of config file 3342 | * @param data Data to write 3343 | */ 3344 | writeConfigJson: (config: string, data: object) => Promise; 3345 | /** 3346 | * Write a config file (full path) to disk 3347 | * @param path Full path to config file 3348 | * @param data Data to write 3349 | * @param pretty Whether to insert tabs or spaces 3350 | */ 3351 | writeJson: ( 3352 | path: string, 3353 | data: object, 3354 | pretty?: boolean 3355 | ) => Promise; 3356 | /** 3357 | * Write a plugin config file (path relative to vault root) to disk 3358 | */ 3359 | writePluginData: (path: string, data: object) => Promise; 3360 | } 3361 | 3362 | // TODO: Add missing elements to other Obsidian interfaces and classes 3363 | 3364 | interface Editor { 3365 | /** 3366 | * CodeMirror editor instance 3367 | */ 3368 | cm: EditorViewI; 3369 | /** 3370 | * HTML instance the CM editor is attached to 3371 | */ 3372 | containerEl: HTMLElement; 3373 | 3374 | /** 3375 | * Make ranges of text highlighted within the editor given specified class (style) 3376 | */ 3377 | addHighlights: ( 3378 | ranges: { from: EditorPosition; to: EditorPosition }[], 3379 | style: "is-flashing" | "obsidian-search-match-highlight", 3380 | remove_previous: boolean, 3381 | x: boolean 3382 | ) => void; 3383 | /** 3384 | * Convert editor position to screen position 3385 | * @param pos Editor position 3386 | * @param mode Relative to the editor or the application window 3387 | */ 3388 | coordsAtPos: ( 3389 | pos: EditorPosition, 3390 | relative_to_editor = false 3391 | ) => { left: number; top: number; bottom: number; right: number }; 3392 | /** 3393 | * Unfolds all folded lines one level up 3394 | * @remark If level 1 and 2 headings are folded, level 2 headings will be unfolded 3395 | */ 3396 | foldLess: () => void; 3397 | /** 3398 | * Folds all the blocks that are of the lowest unfolded level 3399 | * @remark If there is a document with level 1 and 2 headings, level 2 headings will be folded 3400 | */ 3401 | foldMore: () => void; 3402 | /** 3403 | * Get all ranges that can be folded away in the editor 3404 | */ 3405 | getAllFoldableLines: () => { from: number; to: number }[]; 3406 | /** 3407 | * Get a clickable link - if it exists - at specified position 3408 | */ 3409 | getClickableTokenAt: (pos: EditorPosition) => { 3410 | start: EditorPosition; 3411 | end: EditorPosition; 3412 | text: string; 3413 | type: string; 3414 | } | null; 3415 | /** 3416 | * Get all blocks that were folded by their starting character position 3417 | */ 3418 | getFoldOffsets: () => Set; 3419 | /** 3420 | * Checks whether the editor has a highlight of specified class 3421 | * @remark If no style is specified, checks whether the editor has any highlights 3422 | */ 3423 | hasHighlight: (style?: string) => boolean; 3424 | /** 3425 | * Wraps current line around specified characters 3426 | * @remark Was added in a recent Obsidian update (1.4.0 update cycle) 3427 | **/ 3428 | insertBlock: (start: string, end: string) => void; 3429 | /** 3430 | * Get the closest character position to the specified coordinates 3431 | */ 3432 | posAtCoords: (coords: { left: number; top: number }) => EditorPosition; 3433 | /** 3434 | * Removes all highlights of specified class 3435 | */ 3436 | removeHighlights: (style: string) => void; 3437 | /** 3438 | * Adds a search cursor to the editor 3439 | */ 3440 | searchCursor: (searchString: string) => { 3441 | current: () => { from: EditorPosition; to: EditorPosition }; 3442 | findAll: () => { from: EditorPosition; to: EditorPosition }[]; 3443 | findNext: () => { from: EditorPosition; to: EditorPosition }; 3444 | findPrevious: () => { from: EditorPosition; to: EditorPosition }; 3445 | replace: (replacement?: string, origin: string) => void; 3446 | replaceAll: (replacement?: string, origin: string) => void; 3447 | }; 3448 | /** 3449 | * Applies specified markdown syntax to selected text or word under cursor 3450 | */ 3451 | toggleMarkdownFormatting: ( 3452 | syntax: 3453 | | "bold" 3454 | | "italic" 3455 | | "strikethrough" 3456 | | "highlight" 3457 | | "code" 3458 | | "math" 3459 | | "comment" 3460 | ) => void; 3461 | 3462 | /** 3463 | * Clean-up function executed after indenting lists 3464 | */ 3465 | afterIndent: () => void; 3466 | /** 3467 | * Expand text 3468 | * @internal 3469 | */ 3470 | expandText: () => void; 3471 | /** 3472 | * Indents a list by one level 3473 | */ 3474 | indentList: () => void; 3475 | /** 3476 | * Insert a template callout at the current cursor position 3477 | */ 3478 | insertCallout: () => void; 3479 | /** 3480 | * Insert a template code block at the current cursor position 3481 | */ 3482 | insertCodeblock: () => void; 3483 | /** 3484 | * Insert a markdown link at the current cursor position 3485 | */ 3486 | insertLink: () => void; 3487 | /** 3488 | * Insert a mathjax equation block at the current cursor position 3489 | */ 3490 | insertMathJax: () => void; 3491 | /** 3492 | * Insert specified text at the current cursor position 3493 | * @remark Might be broken, inserts at the end of the document 3494 | */ 3495 | insertText: (text: string) => void; 3496 | /** 3497 | * Inserts a new line and continues a markdown bullet point list at the same level 3498 | */ 3499 | newlineAndIndentContinueMarkdownList: () => void; 3500 | /** 3501 | * Inserts a new line at the same indent level 3502 | */ 3503 | newlineAndIndentOnly: () => void; 3504 | /** 3505 | * Get the character position at a mouse event 3506 | */ 3507 | posAtMouse: (e: MouseEvent) => EditorPosition; 3508 | /** 3509 | * Toggles blockquote syntax on paragraph under cursor 3510 | */ 3511 | toggleBlockquote: () => void; 3512 | /** 3513 | * Toggle bullet point list syntax on paragraph under cursor 3514 | */ 3515 | toggleBulletList: () => void; 3516 | /** 3517 | * Toggle checkbox syntax on paragraph under cursor 3518 | */ 3519 | toggleCheckList: () => void; 3520 | /** 3521 | * Toggle numbered list syntax on paragraph under cursor 3522 | */ 3523 | toggleNumberList: () => void; 3524 | /** 3525 | * Convert word under cursor into a wikilink 3526 | * @param embed Whether to embed the link or not 3527 | */ 3528 | triggerWikiLink: (embed: boolean) => void; 3529 | /** 3530 | * Unindents a list by one level 3531 | */ 3532 | unindentList: () => void; 3533 | } 3534 | 3535 | interface View { 3536 | headerEl: HTMLElement; 3537 | titleEl: HTMLElement; 3538 | } 3539 | 3540 | interface WorkspaceLeaf { 3541 | id?: string; 3542 | 3543 | tabHeaderEl: HTMLElement; 3544 | tabHeaderInnerIconEl: HTMLElement; 3545 | tabHeaderInnerTitleEl: HTMLElement; 3546 | } 3547 | 3548 | interface Menu { 3549 | dom: HTMLElement; 3550 | items: MenuItem[]; 3551 | onMouseOver: (evt: MouseEvent) => void; 3552 | hide: () => void; 3553 | } 3554 | 3555 | interface MenuItem { 3556 | callback: () => void; 3557 | dom: HTMLElement; 3558 | setSubmenu: () => Menu; 3559 | onClick: (evt: MouseEvent) => void; 3560 | disabled: boolean; 3561 | } 3562 | 3563 | interface MarkdownPreviewView { 3564 | renderer: ReadViewRenderer; 3565 | } 3566 | 3567 | interface EventRef { 3568 | /** 3569 | * Context applied to the event callback 3570 | */ 3571 | ctx?: any; 3572 | 3573 | /** 3574 | * Events object the event was registered on 3575 | */ 3576 | e: Events; 3577 | 3578 | /** 3579 | * Function to be called on event trigger on the events object 3580 | */ 3581 | fn: (any) => void; 3582 | 3583 | /** 3584 | * Event name the event was registered on 3585 | */ 3586 | name: string; 3587 | } 3588 | } 3589 | 3590 | interface RendererSection { 3591 | el: HTMLElement; 3592 | html: string; 3593 | rendered: boolean; 3594 | } 3595 | 3596 | interface ReadViewRenderer { 3597 | addBottomPadding: boolean; 3598 | lastRender: number; 3599 | lastScroll: number; 3600 | lastText: string; 3601 | previewEl: HTMLElement; 3602 | pusherEl: HTMLElement; 3603 | clear: () => void; 3604 | queueRender: () => void; 3605 | parseSync: () => void; 3606 | parseAsync: () => void; 3607 | set: (text: string) => void; 3608 | text: string; 3609 | sections: RendererSection[]; 3610 | asyncSections: any[]; 3611 | recycledSections: any[]; 3612 | rendered: any[]; 3613 | } 3614 | 3615 | interface CMState extends EditorState { 3616 | vim: { 3617 | inputState: { 3618 | changeQueue: null; 3619 | keyBuffer: []; 3620 | motion: null; 3621 | motionArgs: null; 3622 | motionRepeat: []; 3623 | operator: null; 3624 | operatorArgs: null; 3625 | prefixRepeat: []; 3626 | registerName: null; 3627 | }; 3628 | insertMode: false; 3629 | insertModeRepeat: undefined; 3630 | lastEditActionCommand: undefined; 3631 | lastEditInputState: undefined; 3632 | lastHPos: number; 3633 | lastHSPos: number; 3634 | lastMotion: { 3635 | name?: string; 3636 | }; 3637 | lastPastedText: null; 3638 | lastSelection: null; 3639 | }; 3640 | vimPlugin: { 3641 | lastKeydown: string; 3642 | }; 3643 | } 3644 | 3645 | interface CMView extends EditorView { 3646 | state: CMState; 3647 | } 3648 | 3649 | interface EditorViewI extends EditorView { 3650 | cm?: CMView; 3651 | } 3652 | 3653 | declare global { 3654 | interface Window { 3655 | CodeMirrorAdapter: { 3656 | commands: { 3657 | save(): void; 3658 | }; 3659 | }; 3660 | } 3661 | } 3662 | -------------------------------------------------------------------------------- /src/typings/prettify.d.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = { 2 | [K in keyof T]: T[K]; 3 | } & {}; 4 | -------------------------------------------------------------------------------- /src/typings/templater.d.ts: -------------------------------------------------------------------------------- 1 | export type Templater = { 2 | current_functions_object?: { 3 | config: any; 4 | date: any; 5 | file: any; 6 | frontmatter: any; 7 | obsidian: any; 8 | system: any; 9 | user: any; 10 | web: any; 11 | }; 12 | functions_generator?: any; 13 | parser?: any; 14 | plugin?: any; 15 | }; 16 | -------------------------------------------------------------------------------- /src/ui/settingsTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from "obsidian"; 2 | import RunPlugin from "../main"; 3 | 4 | export class SettingTab extends PluginSettingTab { 5 | plugin: RunPlugin; 6 | 7 | constructor(app: App, plugin: RunPlugin) { 8 | super(app, plugin); 9 | this.plugin = plugin; 10 | } 11 | 12 | async display(): Promise { 13 | const { containerEl } = this; 14 | 15 | containerEl.empty(); 16 | 17 | new Setting(containerEl) 18 | .setName("Generate ending tag metadata") 19 | 20 | .addToggle((toggle) => { 21 | toggle 22 | .setValue(this.plugin.settings.generateEndingTagMetadata) 23 | .onChange(async (value) => { 24 | this.plugin.settings.generateEndingTagMetadata = value; 25 | await this.plugin.saveSettings(); 26 | }); 27 | return toggle; 28 | }); 29 | 30 | new Setting(containerEl) 31 | .setName("Ignore folders") 32 | .setDesc("Folders to ignore. One folder per line.") 33 | .addTextArea((text) => { 34 | text.setPlaceholder("Enter folders to ignore") 35 | .setValue(this.plugin.settings.ignoredFolders.join("\n")) 36 | .onChange(async (_value) => { 37 | const folders = _value 38 | .trim() 39 | .split("\n") 40 | .filter((p) => p !== ""); 41 | 42 | this.plugin.settings.ignoredFolders = folders; 43 | await this.plugin.saveSettings(); 44 | }); 45 | text.inputEl.style.minWidth = text.inputEl.style.maxWidth = 46 | "300px"; 47 | text.inputEl.style.minHeight = "200px"; 48 | return text; 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/deepInclude.ts: -------------------------------------------------------------------------------- 1 | export function deepInclude(obj1: any, obj2: any) { 2 | if (typeof obj1 !== "object" || obj1 === null) { 3 | return obj1 === obj2; 4 | } 5 | if (typeof obj2 !== "object" || obj2 === null) { 6 | return false; 7 | } 8 | for (const key in obj2) { 9 | if (!deepInclude(obj1[key], obj2[key])) { 10 | return false; 11 | } 12 | } 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/evalFromExpression.ts: -------------------------------------------------------------------------------- 1 | import dedent from "ts-dedent"; 2 | import { parse } from "recast"; 3 | import { z } from "zod"; 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction 6 | const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; 7 | 8 | const primativeSchema = z 9 | .string() 10 | .or(z.number()) 11 | .or(z.boolean()) 12 | .or(z.bigint()) 13 | .or(z.date()) 14 | .or(z.undefined()) 15 | .or(z.null()); 16 | 17 | export type Primitive = z.infer; 18 | 19 | export function evalFromExpression( 20 | expression: string, 21 | isFunctionBody: boolean, 22 | context: { 23 | [x: string]: any; 24 | } 25 | ): 26 | | { 27 | success: false; 28 | error: { 29 | cause?: Error; 30 | message: string; 31 | }; 32 | } 33 | // can be a function, a function to return promise, a promise or a primative 34 | | { 35 | success: true; 36 | result: Primitive | Promise; 37 | } { 38 | try { 39 | const ast = parse(expression, { 40 | parser: require("recast/parsers/babel"), 41 | }); 42 | 43 | const func = new ( 44 | JSON.stringify(ast.program.body)?.includes("AwaitExpression") 45 | ? AsyncFunction 46 | : Function 47 | )( 48 | ...Object.keys(context).sort(), 49 | dedent(!isFunctionBody ? `return ${expression}` : expression) 50 | ); 51 | 52 | const result = func( 53 | ...Object.keys(context) 54 | .sort() 55 | .map((key) => context[key]) 56 | ) as Primitive | Promise; 57 | 58 | // for each value in object, make sure it pass the schema, if not, assign error message to the key in sanitizedObject 59 | // const sanitizedResult: SanitizedObject = primativeSchema.parse(object); 60 | 61 | return { 62 | success: true, 63 | result: result, 64 | } as const; 65 | } catch (e) { 66 | return { 67 | success: false, 68 | error: { 69 | cause: e as Error, 70 | message: e.message as string, 71 | }, 72 | } as const; 73 | } 74 | } 75 | 76 | export type EvalResult = ReturnType; 77 | -------------------------------------------------------------------------------- /src/utils/ignore-types.ts: -------------------------------------------------------------------------------- 1 | // copied from https://github.com/platers/obsidian-linter/blob/master/src/utils/ignore-types.ts#L39 2 | 3 | import { 4 | obsidianMultilineCommentRegex, 5 | tagWithLeadingWhitespaceRegex, 6 | wikiLinkRegex, 7 | yamlRegex, 8 | escapeDollarSigns, 9 | genericLinkRegex, 10 | urlRegex, 11 | anchorTagRegex, 12 | templaterCommandRegex, 13 | footnoteDefinitionIndicatorAtStartOfLine, 14 | } from "./regex"; 15 | import { 16 | getAllCustomIgnoreSectionsInText, 17 | getPositions, 18 | MDAstTypes, 19 | } from "./mdast"; 20 | import type { Position } from "unist"; 21 | import { replaceTextBetweenStartAndEndWithNewValue } from "./strings"; 22 | 23 | export type IgnoreResults = { replacedValues: string[]; newText: string }; 24 | export type IgnoreFunction = ( 25 | text: string, 26 | placeholder: string 27 | ) => IgnoreResults; 28 | export type IgnoreType = { 29 | replaceAction: MDAstTypes | RegExp | IgnoreFunction; 30 | placeholder: string; 31 | }; 32 | 33 | export const IgnoreTypes: Record = { 34 | // mdast node types 35 | code: { 36 | replaceAction: MDAstTypes.Code, 37 | placeholder: "{CODE_BLOCK_PLACEHOLDER}", 38 | }, 39 | inlineCode: { 40 | replaceAction: MDAstTypes.InlineCode, 41 | placeholder: "{INLINE_CODE_BLOCK_PLACEHOLDER}", 42 | }, 43 | image: { 44 | replaceAction: MDAstTypes.Image, 45 | placeholder: "{IMAGE_PLACEHOLDER}", 46 | }, 47 | thematicBreak: { 48 | replaceAction: MDAstTypes.HorizontalRule, 49 | placeholder: "{HORIZONTAL_RULE_PLACEHOLDER}", 50 | }, 51 | italics: { 52 | replaceAction: MDAstTypes.Italics, 53 | placeholder: "{ITALICS_PLACEHOLDER}", 54 | }, 55 | bold: { 56 | replaceAction: MDAstTypes.Bold, 57 | placeholder: "{STRONG_PLACEHOLDER}", 58 | }, 59 | list: { replaceAction: MDAstTypes.List, placeholder: "{LIST_PLACEHOLDER}" }, 60 | blockquote: { 61 | replaceAction: MDAstTypes.Blockquote, 62 | placeholder: "{BLOCKQUOTE_PLACEHOLDER}", 63 | }, 64 | math: { replaceAction: MDAstTypes.Math, placeholder: "{MATH_PLACEHOLDER}" }, 65 | inlineMath: { 66 | replaceAction: MDAstTypes.InlineMath, 67 | placeholder: "{INLINE_MATH_PLACEHOLDER}", 68 | }, 69 | html: { replaceAction: MDAstTypes.Html, placeholder: "{HTML_PLACEHOLDER}" }, 70 | // RegExp 71 | yaml: { 72 | replaceAction: yamlRegex, 73 | placeholder: escapeDollarSigns("---\n---"), 74 | }, 75 | wikiLink: { 76 | replaceAction: wikiLinkRegex, 77 | placeholder: "{WIKI_LINK_PLACEHOLDER}", 78 | }, 79 | obsidianMultiLineComments: { 80 | replaceAction: obsidianMultilineCommentRegex, 81 | placeholder: "{OBSIDIAN_COMMENT_PLACEHOLDER}", 82 | }, 83 | footnoteAtStartOfLine: { 84 | replaceAction: footnoteDefinitionIndicatorAtStartOfLine, 85 | placeholder: "{FOOTNOTE_AT_START_OF_LINE_PLACEHOLDER}", 86 | }, 87 | footnoteAfterATask: { 88 | replaceAction: /- \[.] (\[\^\w+\]) ?([,.;!:?])/gm, 89 | placeholder: "{FOOTNOTE_AFTER_A_TASK_PLACEHOLDER}", 90 | }, 91 | url: { replaceAction: urlRegex, placeholder: "{URL_PLACEHOLDER}" }, 92 | anchorTag: { 93 | replaceAction: anchorTagRegex, 94 | placeholder: "{ANCHOR_PLACEHOLDER}", 95 | }, 96 | templaterCommand: { 97 | replaceAction: templaterCommandRegex, 98 | placeholder: "{TEMPLATER_PLACEHOLDER}", 99 | }, 100 | // custom functions 101 | link: { 102 | replaceAction: replaceMarkdownLinks, 103 | placeholder: "{REGULAR_LINK_PLACEHOLDER}", 104 | }, 105 | tag: { replaceAction: replaceTags, placeholder: "#tag-placeholder" }, 106 | customIgnore: { 107 | replaceAction: replaceCustomIgnore, 108 | placeholder: "{CUSTOM_IGNORE_PLACEHOLDER}", 109 | }, 110 | } as const; 111 | 112 | /** 113 | * Replaces all markdown links in the given text with a placeholder. 114 | * @param {string} text The text to replace links in 115 | * @param {string} regularLinkPlaceholder The placeholder to use for regular markdown links 116 | * @return {string} The text with links replaced 117 | * @return {string[]} The regular markdown links replaced 118 | */ 119 | function replaceMarkdownLinks( 120 | text: string, 121 | regularLinkPlaceholder: string 122 | ): IgnoreResults { 123 | const positions: Position[] = getPositions(MDAstTypes.Link, text); 124 | const replacedRegularLinks: string[] = []; 125 | 126 | for (const position of positions) { 127 | if (position == undefined) { 128 | continue; 129 | } 130 | 131 | const regularLink = text.substring( 132 | // @ts-ignore 133 | position.start.offset, 134 | position.end.offset 135 | ); 136 | // skip links that are not in markdown format 137 | if (!regularLink.match(genericLinkRegex)) { 138 | continue; 139 | } 140 | 141 | replacedRegularLinks.push(regularLink); 142 | text = replaceTextBetweenStartAndEndWithNewValue( 143 | text, 144 | // @ts-ignore 145 | position.start.offset, 146 | position.end.offset, 147 | regularLinkPlaceholder 148 | ); 149 | } 150 | 151 | // Reverse the regular links so that they are in the same order as the original text 152 | replacedRegularLinks.reverse(); 153 | 154 | return { newText: text, replacedValues: replacedRegularLinks }; 155 | } 156 | 157 | function replaceTags(text: string, placeholder: string): IgnoreResults { 158 | const replacedValues: string[] = []; 159 | 160 | text = text.replace(tagWithLeadingWhitespaceRegex, (_, whitespace, tag) => { 161 | replacedValues.push(tag); 162 | return whitespace + placeholder; 163 | }); 164 | 165 | return { newText: text, replacedValues: replacedValues }; 166 | } 167 | 168 | function replaceCustomIgnore( 169 | text: string, 170 | customIgnorePlaceholder: string 171 | ): IgnoreResults { 172 | const customIgnorePositions = getAllCustomIgnoreSectionsInText(text); 173 | 174 | const replacedSections: string[] = new Array(customIgnorePositions.length); 175 | let index = 0; 176 | const length = replacedSections.length; 177 | for (const customIgnorePosition of customIgnorePositions) { 178 | replacedSections[length - 1 - index++] = text.substring( 179 | customIgnorePosition.startIndex, 180 | customIgnorePosition.endIndex 181 | ); 182 | text = replaceTextBetweenStartAndEndWithNewValue( 183 | text, 184 | customIgnorePosition.startIndex, 185 | customIgnorePosition.endIndex, 186 | customIgnorePlaceholder 187 | ); 188 | } 189 | 190 | return { newText: text, replacedValues: replacedSections }; 191 | } 192 | -------------------------------------------------------------------------------- /src/utils/mdast.ts: -------------------------------------------------------------------------------- 1 | import { visit } from "unist-util-visit"; 2 | import type { Position } from "unist"; 3 | import type { Root } from "mdast"; 4 | import { 5 | hashString53Bit, 6 | makeSureContentHasEmptyLinesAddedBeforeAndAfter, 7 | replaceTextBetweenStartAndEndWithNewValue, 8 | getStartOfLineIndex, 9 | replaceAt, 10 | countInstances, 11 | } from "./strings"; 12 | import { 13 | genericLinkRegex, 14 | tableRow, 15 | tableSeparator, 16 | tableStartingPipe, 17 | customIgnoreAllStartIndicator, 18 | customIgnoreAllEndIndicator, 19 | checklistBoxStartsTextRegex, 20 | footnoteDefinitionIndicatorAtStartOfLine, 21 | } from "./regex"; 22 | import { gfmFootnote } from "micromark-extension-gfm-footnote"; 23 | import { gfmTaskListItem } from "micromark-extension-gfm-task-list-item"; 24 | import { combineExtensions } from "micromark-util-combine-extensions"; 25 | import { math } from "micromark-extension-math"; 26 | import { mathFromMarkdown } from "mdast-util-math"; 27 | import { fromMarkdown } from "mdast-util-from-markdown"; 28 | import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"; 29 | import { gfmTaskListItemFromMarkdown } from "mdast-util-gfm-task-list-item"; 30 | import QuickLRU from "quick-lru"; 31 | 32 | const LRU = new QuickLRU({ maxSize: 200 }); 33 | 34 | export enum MDAstTypes { 35 | Link = "link", 36 | Footnote = "footnoteDefinition", 37 | Paragraph = "paragraph", 38 | Italics = "emphasis", 39 | Bold = "strong", 40 | ListItem = "listItem", 41 | Code = "code", 42 | InlineCode = "inlineCode", 43 | Image = "image", 44 | List = "list", 45 | Blockquote = "blockquote", 46 | HorizontalRule = "thematicBreak", 47 | Html = "html", 48 | // math types 49 | Math = "math", 50 | InlineMath = "inlineMath", 51 | } 52 | 53 | export function parseTextToAST(text: string): Root { 54 | const textHash = hashString53Bit(text); 55 | if (LRU.has(textHash)) { 56 | return LRU.get(textHash) as Root; 57 | } 58 | 59 | const ast = fromMarkdown(text, { 60 | extensions: [ 61 | combineExtensions([gfmFootnote(), gfmTaskListItem]), 62 | math(), 63 | ], 64 | mdastExtensions: [ 65 | [gfmFootnoteFromMarkdown(), gfmTaskListItemFromMarkdown], 66 | mathFromMarkdown(), 67 | ], 68 | }); 69 | 70 | LRU.set(textHash, ast); 71 | 72 | return ast; 73 | } 74 | 75 | /** 76 | * Gets the positions of the given element type in the given text. 77 | * @param {string} type - The element type to get positions for 78 | * @param {string} text - The markdown text 79 | * @return {Position[]} The positions of the given element type in the given text 80 | */ 81 | export function getPositions(type: MDAstTypes, text: string): Position[] { 82 | const ast = parseTextToAST(text); 83 | const positions: Position[] = []; 84 | visit(ast, type as string, (node) => { 85 | // @ts-ignore 86 | positions.push(node.position); 87 | }); 88 | 89 | // Sort positions by start position in reverse order 90 | // @ts-ignore 91 | positions.sort((a, b) => b.start.offset - a.start.offset); 92 | return positions; 93 | } 94 | 95 | export function getAllCustomIgnoreSectionsInText( 96 | text: string 97 | ): { startIndex: number; endIndex: number }[] { 98 | const positions: { startIndex: number; endIndex: number }[] = []; 99 | const startMatches = [...text.matchAll(customIgnoreAllStartIndicator)]; 100 | if (!startMatches || startMatches.length === 0) { 101 | return positions; 102 | } 103 | 104 | const endMatches = [...text.matchAll(customIgnoreAllEndIndicator)]; 105 | 106 | let iteratorIndex = 0; 107 | startMatches.forEach((startMatch) => { 108 | // @ts-ignore 109 | iteratorIndex = startMatch.index; 110 | 111 | let foundEndingIndicator = false; 112 | let endingPosition = text.length - 1; 113 | // eslint-disable-next-line no-unmodified-loop-condition -- endMatches does not need to be modified with regards to being undefined or null 114 | while (endMatches && endMatches.length !== 0 && !foundEndingIndicator) { 115 | // @ts-ignore 116 | if (endMatches[0].index <= iteratorIndex) { 117 | endMatches.shift(); 118 | } else { 119 | foundEndingIndicator = true; 120 | 121 | const endingIndicator = endMatches[0]; 122 | endingPosition = 123 | // @ts-ignore 124 | endingIndicator.index + endingIndicator[0].length; 125 | } 126 | } 127 | 128 | positions.push({ 129 | startIndex: iteratorIndex, 130 | endIndex: endingPosition, 131 | }); 132 | 133 | if (!endMatches || endMatches.length === 0) { 134 | return; 135 | } 136 | }); 137 | 138 | return positions.reverse(); 139 | } 140 | -------------------------------------------------------------------------------- /src/utils/obsidian.ts: -------------------------------------------------------------------------------- 1 | import { TFolder, TFile, parseYaml, Plugin, Editor } from "obsidian"; 2 | import { stripCr } from "./strings"; 3 | import { getYAMLText, splitYamlAndBody } from "./yaml"; 4 | import { diff_match_patch, DIFF_INSERT, DIFF_DELETE } from "diff-match-patch"; 5 | 6 | export function isMarkdownFile(file: TFile) { 7 | return file && file.extension === "md"; 8 | } 9 | 10 | /** 11 | * recursively get all files in a folder 12 | */ 13 | export function getAllFilesInFolder(startingFolder: TFolder): TFile[] { 14 | const filesInFolder = [] as TFile[]; 15 | const foldersToIterateOver = [startingFolder] as TFolder[]; 16 | for (const folder of foldersToIterateOver) { 17 | for (const child of folder.children) { 18 | if (child instanceof TFile && isMarkdownFile(child)) { 19 | filesInFolder.push(child); 20 | } else if (child instanceof TFolder) { 21 | foldersToIterateOver.push(child); 22 | } 23 | } 24 | } 25 | 26 | return filesInFolder; 27 | } 28 | 29 | /** 30 | * this is the sync version of getDataFromFile 31 | * @param plugin 32 | * @param text 33 | * @returns 34 | */ 35 | export const getDataFromTextSync = (text: string) => { 36 | const yamlText = getYAMLText(text); 37 | 38 | const { body } = splitYamlAndBody(text); 39 | return { 40 | text, 41 | yamlText, 42 | yamlObj: yamlText 43 | ? (parseYaml(yamlText) as { [x: string]: any }) 44 | : null, 45 | body, 46 | }; 47 | }; 48 | 49 | export const getDataFromFile = async (plugin: Plugin, file: TFile) => { 50 | const text = stripCr(await plugin.app.vault.read(file)); 51 | return getDataFromTextSync(text); 52 | }; 53 | 54 | export type Data = Awaited>; 55 | 56 | export function writeFile(editor: Editor, oldText: string, newText: string) { 57 | const dmp = new diff_match_patch(); 58 | const changes = dmp.diff_main(oldText, newText); 59 | let curText = ""; 60 | changes.forEach((change) => { 61 | function endOfDocument(doc: string) { 62 | const lines = doc.split("\n"); 63 | return { 64 | line: lines.length - 1, 65 | // @ts-ignore 66 | ch: lines[lines.length - 1].length, 67 | }; 68 | } 69 | 70 | const [type, value] = change; 71 | 72 | if (type == DIFF_INSERT) { 73 | editor.replaceRange(value, endOfDocument(curText)); 74 | curText += value; 75 | } else if (type == DIFF_DELETE) { 76 | const start = endOfDocument(curText); 77 | let tempText = curText; 78 | tempText += value; 79 | const end = endOfDocument(tempText); 80 | editor.replaceRange("", start, end); 81 | } else { 82 | curText += value; 83 | } 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | import { makeSureContentHasEmptyLinesAddedBeforeAndAfter } from "./strings"; 2 | 3 | // Useful regexes 4 | export const allHeadersRegex = /^([ \t]*)(#+)([ \t]+)([^\n\r]*?)([ \t]+#+)?$/gm; 5 | export const fencedRegexTemplate = 6 | "^XXX\\.*?\n(?:((?:.|\n)*?)\n)?XXX(?=\\s|$)$"; 7 | export const yamlRegex = /^---\n((?:(((?!---)(?:.|\n)*?)\n)?))---(?=\n|$)/; 8 | export const backtickBlockRegexTemplate = fencedRegexTemplate.replaceAll( 9 | "X", 10 | "`" 11 | ); 12 | export const tildeBlockRegexTemplate = fencedRegexTemplate.replaceAll("X", "~"); 13 | export const indentedBlockRegex = "^((\t|( {4})).*\n)+"; 14 | export const codeBlockRegex = new RegExp( 15 | `${backtickBlockRegexTemplate}|${tildeBlockRegexTemplate}|${indentedBlockRegex}`, 16 | "gm" 17 | ); 18 | // based on https://stackoverflow.com/a/26010910/8353749 19 | export const wikiLinkRegex = 20 | /(!?)\[{2}([^\][\n|]+)(\|([^\][\n|]+))?(\|([^\][\n|]+))?\]{2}/g; 21 | // based on https://davidwells.io/snippets/regex-match-markdown-links 22 | export const genericLinkRegex = /(!?)\[([^[]*)\](\(.*\))/g; 23 | export const tagWithLeadingWhitespaceRegex = /(\s|^)(#[^\s#;.,>\\s*)*`; 28 | export const tableSeparator = 29 | /(\|? *:?-{1,}:? *\|?)(\| *:?-{1,}:? *\|?)*( |\t)*$/gm; 30 | export const tableStartingPipe = /^(((>[ ]?)*)|([ ]{0,3}))\|/m; 31 | export const tableRow = /[^\n]*?\|[^\n]*?(\n|$)/m; 32 | // based on https://gist.github.com/skeller88/5eb73dc0090d4ff1249a 33 | export const simpleURIRegex = 34 | /(([a-z\-0-9]+:)\/{2})([^\s/?#]*[^\s")'.?!/]|[/])?(([/?#][^\s")']*[^\s")'.?!])|[/])?/gi; 35 | // generated from https://github.com/spamscanner/url-regex-safe using strict: true, returnString: true, and re2: false as options 36 | export const urlRegex = 37 | /(?:(?:(?:[a-z]+:)?\/\/)|www\.)(?:localhost|(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?|(?:(?:[a-z\u00a1-\uffff0-9][-_]*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:(?:[/?#][^\s")']*[^\s")'.?!])|[/])?/gi; 38 | export const anchorTagRegex = /]+)>((?:.(?!<\/a>))*.)<\/a>/g; 39 | export const wordRegex = /[\p{L}\p{N}\p{Pc}\p{M}\-'’`]+/gu; 40 | // regex from https://stackoverflow.com/a/26128757/8353749 41 | export const htmlEntitiesRegex = /&[^\s]+;$/im; 42 | 43 | export const customIgnoreAllStartIndicator = 44 | generateHTMLLinterCommentWithSpecificTextAndWhitespaceRegexMatch(true); 45 | export const customIgnoreAllEndIndicator = 46 | generateHTMLLinterCommentWithSpecificTextAndWhitespaceRegexMatch(false); 47 | 48 | export const smartDoubleQuoteRegex = /[“”„«»]/g; 49 | export const smartSingleQuoteRegex = /[‘’‚‹›]/g; 50 | 51 | export const templaterCommandRegex = /<%[^]*?%>/g; 52 | // checklist regex 53 | export const checklistBoxIndicator = "\\[.\\]"; 54 | export const checklistBoxStartsTextRegex = new RegExp( 55 | `^${checklistBoxIndicator}` 56 | ); 57 | export const indentedOrBlockquoteNestedChecklistIndicatorRegex = new RegExp( 58 | `^${lineStartingWithWhitespaceOrBlockquoteTemplate}- ${checklistBoxIndicator} ` 59 | ); 60 | export const nonBlockquoteChecklistRegex = new RegExp( 61 | `^\\s*- ${checklistBoxIndicator} ` 62 | ); 63 | 64 | export const footnoteDefinitionIndicatorAtStartOfLine = 65 | /^(\[\^\w+\]) ?([,.;!:?])/gm; 66 | export const calloutRegex = /^(>\s*)+\[![^\s]*\]/m; 67 | 68 | // https://stackoverflow.com/questions/38866071/javascript-replace-method-dollar-signs 69 | // Important to use this for any regex replacements where the replacement string 70 | // could have user constructed dollar signs in it 71 | export function escapeDollarSigns(str: string): string { 72 | return str.replace(/\$/g, "$$$$"); 73 | } 74 | 75 | // https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex 76 | export function escapeRegExp(string: string): string { 77 | return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 78 | } 79 | 80 | /** 81 | * Removes spaces from around the wiki link text 82 | * @param {string} text The text to remove the space from around wiki link text 83 | * @return {string} The text without space around wiki link link text 84 | */ 85 | export function removeSpacesInWikiLinkText(text: string): string { 86 | const linkMatches = text.match(wikiLinkRegex); 87 | if (linkMatches) { 88 | for (const link of linkMatches) { 89 | // wiki link with link text 90 | if (link.includes("|")) { 91 | const startLinkTextPosition = link.indexOf("|"); 92 | const newLink = 93 | link.substring(0, startLinkTextPosition + 1) + 94 | link 95 | .substring(startLinkTextPosition + 1, link.length - 2) 96 | .trim() + 97 | "]]"; 98 | text = text.replace(link, newLink); 99 | } 100 | } 101 | } 102 | 103 | return text; 104 | } 105 | 106 | /** 107 | * Gets the first header one's text from the string provided making sure to convert any links to their display text. 108 | * @param {string} text - The text to have get the first header one's text from. 109 | * @return {string} The text for the first header one if present or an empty string. 110 | */ 111 | export function getFirstHeaderOneText(text: string): string { 112 | const result = text.match(/^#\s+(.*)/m); 113 | if (result && result[1]) { 114 | let headerText = result[1]; 115 | headerText = headerText.replaceAll( 116 | wikiLinkRegex, 117 | (_, _2, $2: string, $3: string) => { 118 | if ($3 != null) { 119 | return $3.replace("|", ""); 120 | } 121 | 122 | return $2; 123 | } 124 | ); 125 | 126 | return headerText.replaceAll(genericLinkRegex, "$2"); 127 | } 128 | 129 | return ""; 130 | } 131 | 132 | export function matchTagRegex(text: string): string[] { 133 | // @ts-ignore 134 | return [...text.matchAll(tagWithLeadingWhitespaceRegex)].map( 135 | (match) => match[2] 136 | ); 137 | } 138 | 139 | export function generateHTMLLinterCommentWithSpecificTextAndWhitespaceRegexMatch( 140 | isStart: boolean 141 | ): RegExp { 142 | const regexTemplate = ""; 143 | let endingText = ""; 144 | 145 | if (isStart) { 146 | endingText += "disable"; 147 | } else { 148 | endingText += "enable"; 149 | } 150 | 151 | return new RegExp(regexTemplate.replace("{ENDING_TEXT}", endingText), "g"); 152 | } 153 | -------------------------------------------------------------------------------- /src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | import { calloutRegex } from "./regex"; 2 | 3 | /** 4 | * Replaces a string by inserting it between the start and end positions provided for a string. 5 | * @param {string} str - The string to replace a value from 6 | * @param {number} start - The position to insert at 7 | * @param {number} end - The position to stop text replacement at 8 | * @param {string} value - The string to insert 9 | * @return {string} The string with the replacement string added over the specified start and stop 10 | */ 11 | export function replaceTextBetweenStartAndEndWithNewValue( 12 | str: string, 13 | start: number, 14 | end: number, 15 | value: string 16 | ): string { 17 | return str.substring(0, start) + value + str.substring(end); 18 | } 19 | 20 | function getStartOfLineWhitespaceOrBlockquoteLevel( 21 | text: string, 22 | startPosition: number 23 | ): [string, number] { 24 | if (startPosition === 0) { 25 | return ["", 0]; 26 | } 27 | 28 | let startOfLine = ""; 29 | let index = startPosition; 30 | while (index >= 0) { 31 | const char = text.charAt(index); 32 | if (char === "\n") { 33 | break; 34 | } else if (char.trim() === "" || char === ">") { 35 | startOfLine = char + startOfLine; 36 | } else { 37 | startOfLine = ""; 38 | } 39 | 40 | index--; 41 | } 42 | 43 | return [startOfLine, index]; 44 | } 45 | 46 | function getEmptyLine(priorLine: string = ""): string { 47 | const [priorLineStart] = getStartOfLineWhitespaceOrBlockquoteLevel( 48 | priorLine, 49 | priorLine.length 50 | ); 51 | 52 | return "\n" + priorLineStart.trim(); 53 | } 54 | 55 | function getEmptyLineForBlockqute( 56 | priorLine: string = "", 57 | isCallout: boolean = false, 58 | blockquoteLevel: number = 1 59 | ): string { 60 | const potentialEmptyLine = getEmptyLine(priorLine); 61 | const previousBlockquoteLevel = countInstances(potentialEmptyLine, ">"); 62 | const dealingWithACallout = isCallout || calloutRegex.test(priorLine); 63 | if (dealingWithACallout && blockquoteLevel === previousBlockquoteLevel) { 64 | return potentialEmptyLine.substring( 65 | 0, 66 | potentialEmptyLine.lastIndexOf(">") 67 | ); 68 | } 69 | 70 | return potentialEmptyLine; 71 | } 72 | 73 | function makeSureContentHasASingleEmptyLineBeforeItUnlessItStartsAFile( 74 | text: string, 75 | startOfContent: number 76 | ): string { 77 | if (startOfContent === 0) { 78 | return text; 79 | } 80 | 81 | let index = startOfContent; 82 | let startOfNewContent = startOfContent; 83 | while (index >= 0) { 84 | const currentChar = text.charAt(index); 85 | if (currentChar.trim() !== "") { 86 | break; // if non-whitespace is encountered, then the line has content 87 | } else if (currentChar === "\n") { 88 | startOfNewContent = index; 89 | } 90 | index--; 91 | } 92 | 93 | if (index < 0 || startOfNewContent === 0) { 94 | return text.substring(startOfContent + 1); 95 | } 96 | 97 | return ( 98 | text.substring(0, startOfNewContent) + 99 | "\n" + 100 | text.substring(startOfContent) 101 | ); 102 | } 103 | 104 | function makeSureContentHasASingleEmptyLineBeforeItUnlessItStartsAFileForBlockquote( 105 | text: string, 106 | startOfLine: string, 107 | startOfContent: number, 108 | isCallout: boolean = false, 109 | addingEmptyLinesAroundBlockquotes: boolean = false 110 | ): string { 111 | if (startOfContent === 0) { 112 | return text; 113 | } 114 | 115 | const nestingLevel = startOfLine.split(">").length - 1; 116 | let index = startOfContent; 117 | let startOfNewContent = startOfContent; 118 | let lineNestingLevel = 0; 119 | let foundABlankLine = false; 120 | let previousChar = ""; 121 | while (index >= 0) { 122 | const currentChar = text.charAt(index); 123 | if (currentChar.trim() !== "" && currentChar !== ">") { 124 | break; // if non-whitespace, non-gt-bracket is encountered, then the line has content 125 | } else if (currentChar === ">") { 126 | // if we go from having a blank line at any point to then having more blockquote content we know we have encountered another blockquote 127 | if (foundABlankLine) { 128 | break; 129 | } 130 | 131 | lineNestingLevel++; 132 | } else if (currentChar === "\n") { 133 | if ( 134 | lineNestingLevel === 0 || 135 | lineNestingLevel === nestingLevel || 136 | lineNestingLevel + 1 === nestingLevel 137 | ) { 138 | startOfNewContent = index; 139 | lineNestingLevel = 0; 140 | 141 | if (previousChar === "\n") { 142 | foundABlankLine = true; 143 | } 144 | } else { 145 | break; 146 | } 147 | } 148 | index--; 149 | previousChar = currentChar; 150 | } 151 | 152 | if (index < 0 || startOfNewContent === 0) { 153 | return text.substring(startOfContent + 1); 154 | } 155 | 156 | const startingEmptyLines = text.substring( 157 | startOfNewContent, 158 | startOfContent 159 | ); 160 | const startsWithEmptyLine = 161 | startingEmptyLines === "\n" || startingEmptyLines.startsWith("\n\n"); 162 | if (startsWithEmptyLine) { 163 | return ( 164 | text.substring(0, startOfNewContent) + 165 | "\n" + 166 | text.substring(startOfContent) 167 | ); 168 | } 169 | 170 | const indexOfLastNewLine = text.lastIndexOf("\n", startOfNewContent - 1); 171 | let priorLine = ""; 172 | if (indexOfLastNewLine === -1) { 173 | priorLine = text.substring(0, startOfNewContent); 174 | } else { 175 | priorLine = text.substring(indexOfLastNewLine, startOfNewContent); 176 | } 177 | 178 | const emptyLine = addingEmptyLinesAroundBlockquotes 179 | ? getEmptyLineForBlockqute(priorLine, isCallout, nestingLevel) 180 | : getEmptyLine(priorLine); 181 | 182 | return ( 183 | text.substring(0, startOfNewContent) + 184 | emptyLine + 185 | text.substring(startOfContent) 186 | ); 187 | } 188 | 189 | function makeSureContentHasASingleEmptyLineAfterItUnlessItEndsAFile( 190 | text: string, 191 | endOfContent: number 192 | ): string { 193 | if (endOfContent === text.length - 1) { 194 | return text; 195 | } 196 | 197 | let index = endOfContent; 198 | let endOfNewContent = endOfContent; 199 | let isFirstNewLine = true; 200 | while (index < text.length) { 201 | const currentChar = text.charAt(index); 202 | if (currentChar.trim() !== "") { 203 | break; // if non-whitespace is encountered, then the line has content 204 | } else if (currentChar === "\n") { 205 | if (isFirstNewLine) { 206 | isFirstNewLine = false; 207 | } else { 208 | endOfNewContent = index; 209 | } 210 | } 211 | index++; 212 | } 213 | 214 | if (index === text.length || endOfNewContent === text.length - 1) { 215 | return text.substring(0, endOfContent); 216 | } 217 | 218 | return ( 219 | text.substring(0, endOfContent) + "\n" + text.substring(endOfNewContent) 220 | ); 221 | } 222 | 223 | function makeSureContentHasASingleEmptyLineAfterItUnlessItEndsAFileForBlockquote( 224 | text: string, 225 | startOfLine: string, 226 | endOfContent: number, 227 | isCallout: boolean = false, 228 | addingEmptyLinesAroundBlockquotes: boolean = false 229 | ): string { 230 | if (endOfContent === text.length - 1) { 231 | return text; 232 | } 233 | 234 | const nestingLevel = startOfLine.split(">").length - 1; 235 | let index = endOfContent; 236 | let endOfNewContent = endOfContent; 237 | let isFirstNewLine = true; 238 | let lineNestingLevel = 0; 239 | let foundABlankLine = false; 240 | let previousChar = ""; 241 | while (index < text.length) { 242 | const currentChar = text.charAt(index); 243 | if (currentChar.trim() !== "" && currentChar !== ">") { 244 | break; // if non-whitespace is encountered, then the line has content 245 | } else if (currentChar === ">") { 246 | // if we go from having a blank line at any point to then having more blockquote content we know we have encountered another blockquote 247 | if (foundABlankLine) { 248 | break; 249 | } 250 | 251 | lineNestingLevel++; 252 | } else if (currentChar === "\n") { 253 | if ( 254 | lineNestingLevel === 0 || 255 | lineNestingLevel === nestingLevel || 256 | lineNestingLevel + 1 === nestingLevel 257 | ) { 258 | lineNestingLevel = 0; 259 | if (isFirstNewLine) { 260 | isFirstNewLine = false; 261 | } else { 262 | endOfNewContent = index; 263 | } 264 | 265 | if (previousChar === "\n") { 266 | foundABlankLine = true; 267 | } 268 | } else { 269 | break; 270 | } 271 | } 272 | index++; 273 | 274 | previousChar = currentChar; 275 | } 276 | 277 | if (index === text.length || endOfNewContent === text.length - 1) { 278 | return text.substring(0, endOfContent); 279 | } 280 | 281 | const endingEmptyLines = text.substring(endOfContent, endOfNewContent); 282 | const endsInEmptyLine = 283 | endingEmptyLines === "\n" || endingEmptyLines.endsWith("\n\n"); 284 | if (endsInEmptyLine) { 285 | return ( 286 | text.substring(0, endOfContent) + 287 | "\n" + 288 | text.substring(endOfNewContent) 289 | ); 290 | } 291 | 292 | const indexOfSecondNewLineAfterContent = text.indexOf( 293 | "\n", 294 | endOfNewContent + 1 295 | ); 296 | let nextLine = ""; 297 | if (indexOfSecondNewLineAfterContent === -1) { 298 | nextLine = text.substring(endOfNewContent); 299 | } else { 300 | nextLine = text.substring( 301 | endOfNewContent + 1, 302 | indexOfSecondNewLineAfterContent 303 | ); 304 | } 305 | 306 | const emptyLine = addingEmptyLinesAroundBlockquotes 307 | ? getEmptyLineForBlockqute(nextLine, isCallout, nestingLevel) 308 | : getEmptyLine(nextLine); 309 | 310 | return ( 311 | text.substring(0, endOfContent) + 312 | emptyLine + 313 | text.substring(endOfNewContent) 314 | ); 315 | } 316 | 317 | /** 318 | * Makes sure that the specified content has an empty line around it so long as it does not start or end a file. 319 | * @param {string} text - The entire file's contents 320 | * @param {number} start - The starting index of the content to escape 321 | * @param {number} end - The ending index of the content to escape 322 | * @param {boolean} addingEmptyLinesAroundBlockquotes - Whether or not the logic is meant to add empty lines around blockquotes. This is something meant to better help with spacing around blockquotes. 323 | * @return {string} The new file contents after the empty lines have been added 324 | */ 325 | export function makeSureContentHasEmptyLinesAddedBeforeAndAfter( 326 | text: string, 327 | start: number, 328 | end: number, 329 | addingEmptyLinesAroundBlockquotes: boolean = false 330 | ): string { 331 | const [startOfLine, startOfLineIndex] = 332 | getStartOfLineWhitespaceOrBlockquoteLevel(text, start); 333 | if (startOfLine.trim() !== "") { 334 | const isCallout = calloutRegex.test(text.substring(start, end)); 335 | const newText = 336 | makeSureContentHasASingleEmptyLineAfterItUnlessItEndsAFileForBlockquote( 337 | text, 338 | startOfLine, 339 | end, 340 | isCallout, 341 | addingEmptyLinesAroundBlockquotes 342 | ); 343 | 344 | return makeSureContentHasASingleEmptyLineBeforeItUnlessItStartsAFileForBlockquote( 345 | newText, 346 | startOfLine, 347 | startOfLineIndex, 348 | isCallout, 349 | addingEmptyLinesAroundBlockquotes 350 | ); 351 | } 352 | 353 | const newText = makeSureContentHasASingleEmptyLineAfterItUnlessItEndsAFile( 354 | text, 355 | end 356 | ); 357 | 358 | return makeSureContentHasASingleEmptyLineBeforeItUnlessItStartsAFile( 359 | newText, 360 | startOfLineIndex 361 | ); 362 | } 363 | 364 | // from https://stackoverflow.com/a/52171480/8353749 365 | export function hashString53Bit(str: string, seed: number = 0): number { 366 | let h1 = 0xdeadbeef ^ seed; 367 | let h2 = 0x41c6ce57 ^ seed; 368 | for (let i = 0, ch; i < str.length; i++) { 369 | ch = str.charCodeAt(i); 370 | h1 = Math.imul(h1 ^ ch, 2654435761); 371 | h2 = Math.imul(h2 ^ ch, 1597334677); 372 | } 373 | 374 | h1 = 375 | Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ 376 | Math.imul(h2 ^ (h2 >>> 13), 3266489909); 377 | h2 = 378 | Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ 379 | Math.imul(h1 ^ (h1 >>> 13), 3266489909); 380 | 381 | return 4294967296 * (2097151 & h2) + (h1 >>> 0); 382 | } 383 | 384 | export function getStartOfLineIndex( 385 | text: string, 386 | indexToStartFrom: number 387 | ): number { 388 | if (indexToStartFrom == 0) { 389 | return indexToStartFrom; 390 | } 391 | 392 | let startOfLineIndex = indexToStartFrom; 393 | while (startOfLineIndex > 0 && text.charAt(startOfLineIndex - 1) !== "\n") { 394 | startOfLineIndex--; 395 | } 396 | 397 | return startOfLineIndex; 398 | } 399 | 400 | /** 401 | * Replace the first instance of the matching search string in the text after the provided starting position. 402 | * @param {string} text - The text in which to do the find and replace given the starting position. 403 | * @param {string} search - The text to search for and replace in the provided string. 404 | * @param {string} replace - The text to replace the search text with in the provided string. 405 | * @param {number} start - The position to start the replace search at. 406 | * @return {string} The new string after replacing the value if found. 407 | */ 408 | export function replaceAt( 409 | text: string, 410 | search: string, 411 | replace: string, 412 | start: number 413 | ): string { 414 | if (start > text.length - 1) { 415 | return text; 416 | } 417 | 418 | return ( 419 | text.slice(0, start) + 420 | text.slice(start, text.length).replace(search, replace) 421 | ); 422 | } 423 | 424 | // based on https://stackoverflow.com/a/21730166/8353749 425 | export function countInstances(text: string, instancesOf: string): number { 426 | let counter = 0; 427 | 428 | for (let i = 0, input_length = text.length; i < input_length; i++) { 429 | const index_of_sub = text.indexOf(instancesOf, i); 430 | 431 | if (index_of_sub > -1) { 432 | counter++; 433 | i = index_of_sub; 434 | } 435 | } 436 | 437 | return counter; 438 | } 439 | 440 | // based on https://stackoverflow.com/a/175787/8353749 441 | export function isNumeric(str: string) { 442 | const type = typeof str; 443 | if (type != "string") return type === "number"; // we only process strings so if the value is not already a number the result is false 444 | return ( 445 | !isNaN(str as unknown as number) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... 446 | !isNaN(parseFloat(str)) 447 | ); // ...and ensure strings of whitespace fail 448 | } 449 | 450 | export function stripCr(text: string) { 451 | return text.replace(/\r/g, ""); 452 | } 453 | 454 | /** 455 | * This function takes in a string and returns an object with key value pairs 456 | */ 457 | export function mapStringToKeyValuePairs(inputString: string) { 458 | const lines = inputString.split("\n"); 459 | const result: { 460 | [x: string]: string; 461 | } = {}; 462 | 463 | lines.forEach((line) => { 464 | const index = line.indexOf(":"); 465 | if (index !== -1) { 466 | const key = line.slice(0, index).trim(); 467 | const value = line.slice(index + 1).trim(); 468 | if (key && value) result[key] = value; 469 | } 470 | }); 471 | 472 | return result; 473 | } 474 | 475 | export const replaceOccurance = ( 476 | text: string, 477 | search: string, 478 | replace: string, 479 | index: number 480 | ) => { 481 | const parts = text.split(search); 482 | 483 | if (parts.length <= index) { 484 | return text; // No match found or index out of range, return original text 485 | } 486 | 487 | let result = parts[0]!; 488 | 489 | for (let i = 1; i < parts.length; i++) { 490 | result += (i === index ? replace : search) + parts[i]; 491 | } 492 | return result; 493 | }; 494 | -------------------------------------------------------------------------------- /src/utils/yaml.ts: -------------------------------------------------------------------------------- 1 | import { load, dump } from "js-yaml"; 2 | import { escapeDollarSigns, yamlRegex } from "./regex"; 3 | import { isNumeric } from "./strings"; 4 | 5 | export const OBSIDIAN_TAG_KEY_SINGULAR = "tag"; 6 | export const OBSIDIAN_TAG_KEY_PLURAL = "tags"; 7 | export const OBSIDIAN_TAG_KEYS = [ 8 | OBSIDIAN_TAG_KEY_SINGULAR, 9 | OBSIDIAN_TAG_KEY_PLURAL, 10 | ]; 11 | export const OBSIDIAN_ALIAS_KEY_SINGULAR = "alias"; 12 | export const OBSIDIAN_ALIAS_KEY_PLURAL = "aliases"; 13 | export const OBSIDIAN_ALIASES_KEYS = [ 14 | OBSIDIAN_ALIAS_KEY_SINGULAR, 15 | OBSIDIAN_ALIAS_KEY_PLURAL, 16 | ]; 17 | export const LINTER_ALIASES_HELPER_KEY = "linter-yaml-title-alias"; 18 | export const DISABLED_RULES_KEY = "disabled rules"; 19 | 20 | export function initYAML(text: string) { 21 | if (text.match(yamlRegex) === null) { 22 | text = "---\n---\n" + text; 23 | } 24 | return text; 25 | } 26 | 27 | export function getYAMLText(text: string) { 28 | const yaml = text.match(yamlRegex); 29 | if (!yaml) { 30 | return null; 31 | } 32 | return yaml[1]; 33 | } 34 | 35 | export function formatYAML(text: string, func: (text: string) => string) { 36 | if (!text.match(yamlRegex)) { 37 | return text; 38 | } 39 | 40 | const oldYaml = text.match(yamlRegex)?.[0]; 41 | if (!oldYaml) return text; 42 | 43 | const newYaml = func(oldYaml); 44 | text = text.replace(oldYaml, escapeDollarSigns(newYaml)); 45 | 46 | return text; 47 | } 48 | 49 | function getYamlSectionRegExp(rawKey: string) { 50 | return new RegExp( 51 | `^([\\t ]*)${rawKey}:[ \\t]*(\\S.*|(?:(?:\\n *- \\S.*)|((?:\\n *- *))*|(\\n([ \\t]+[^\\n]*))*)*)\\n`, 52 | "m" 53 | ); 54 | } 55 | 56 | export function setYamlSection( 57 | yaml: string, 58 | rawKey: string, 59 | rawValue: string 60 | ): string { 61 | const yamlSectionEscaped = `${rawKey}:${rawValue}\n`; 62 | let isReplaced = false; 63 | let result = yaml.replace(getYamlSectionRegExp(rawKey), (_, $1: string) => { 64 | isReplaced = true; 65 | return $1 + yamlSectionEscaped; 66 | }); 67 | if (!isReplaced) { 68 | result = `${yaml}${yamlSectionEscaped}`; 69 | } 70 | return result; 71 | } 72 | 73 | export function getYamlSectionValue( 74 | yaml: string, 75 | rawKey: string 76 | ): string | null { 77 | const match = yaml.match(getYamlSectionRegExp(rawKey)); 78 | const result = match == null ? null : match[2]; 79 | // @ts-ignore 80 | return result; 81 | } 82 | 83 | export function removeYamlSection(yaml: string, rawKey: string): string { 84 | const result = yaml.replace(getYamlSectionRegExp(rawKey), ""); 85 | return result; 86 | } 87 | 88 | export enum TagSpecificArrayFormats { 89 | SingleStringSpaceDelimited = "single string space delimited", 90 | SingleLineSpaceDelimited = "single-line space delimited", 91 | } 92 | 93 | export enum SpecialArrayFormats { 94 | SingleStringToSingleLine = "single string to single-line", 95 | SingleStringToMultiLine = "single string to multi-line", 96 | SingleStringCommaDelimited = "single string comma delimited", 97 | } 98 | 99 | export enum NormalArrayFormats { 100 | SingleLine = "single-line", 101 | MultiLine = "multi-line", 102 | } 103 | 104 | export type QuoteCharacter = "'" | '"'; 105 | 106 | /** 107 | * Formats the YAML array value passed in with the specified format. 108 | * @param {string | string[]} value The value(s) that will be used as the parts of the array that is assumed to already be broken down into the appropriate format to be put in the array. 109 | * @param {NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats} format The format that the array should be converted into. 110 | * @param {string} defaultEscapeCharacter The character escape to use around the value if a specific escape character is not needed. 111 | * @param {boolean} removeEscapeCharactersIfPossibleWhenGoingToMultiLine Whether or not to remove no longer needed escape values when converting to a multi-line format. 112 | * @param {boolean} escapeNumericValues Whether or not to escape any numeric values found in the array. 113 | * @return {string} The formatted array in the specified YAML/obsidian YAML format. 114 | */ 115 | export function formatYamlArrayValue( 116 | value: string | string[], 117 | format: NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats, 118 | defaultEscapeCharacter: QuoteCharacter, 119 | removeEscapeCharactersIfPossibleWhenGoingToMultiLine: boolean, 120 | escapeNumericValues: boolean = false 121 | ): string { 122 | if (typeof value === "string") { 123 | value = [value]; 124 | } 125 | 126 | // handle default values here 127 | if (value == null || value.length === 0) { 128 | return getDefaultYAMLArrayValue(format); 129 | } 130 | 131 | // handle escaping numeric values and the removal of escape characters where applicable for multiline arrays 132 | const shouldRemoveEscapeCharactersIfPossible = 133 | removeEscapeCharactersIfPossibleWhenGoingToMultiLine && 134 | (format == NormalArrayFormats.MultiLine || 135 | (format == SpecialArrayFormats.SingleStringToMultiLine && 136 | value.length > 1)); 137 | if (escapeNumericValues || shouldRemoveEscapeCharactersIfPossible) { 138 | for (let i = 0; i < value.length; i++) { 139 | let currentValue = value[i]; 140 | // @ts-ignore 141 | const valueIsEscaped = isValueEscapedAlready(currentValue); 142 | if (valueIsEscaped) { 143 | // @ts-ignore 144 | currentValue = currentValue.substring( 145 | 1, 146 | // @ts-ignore 147 | currentValue.length - 1 148 | ); 149 | } 150 | 151 | const shouldRequireEscapeOfCurrentValue = 152 | // @ts-ignore 153 | escapeNumericValues && isNumeric(currentValue); 154 | if (valueIsEscaped && shouldRequireEscapeOfCurrentValue) { 155 | continue; // when dealing with numbers that we need escaped, we don't want to remove that escaping for multiline arrays 156 | } else if ( 157 | shouldRequireEscapeOfCurrentValue || 158 | (valueIsEscaped && shouldRemoveEscapeCharactersIfPossible) 159 | ) { 160 | value[i] = escapeStringIfNecessaryAndPossible( 161 | // @ts-ignore 162 | currentValue, 163 | defaultEscapeCharacter, 164 | shouldRequireEscapeOfCurrentValue 165 | ); 166 | } 167 | } 168 | } 169 | 170 | // handle the values that are present based on the format of the array 171 | /* eslint-disable no-fallthrough -- we are falling through here because it makes the most sense for the cases below */ 172 | switch (format) { 173 | case SpecialArrayFormats.SingleStringToSingleLine: 174 | if (value.length === 1) { 175 | return " " + value[0]; 176 | } 177 | case NormalArrayFormats.SingleLine: 178 | return " " + convertStringArrayToSingleLineArray(value); 179 | case SpecialArrayFormats.SingleStringToMultiLine: 180 | if (value.length === 1) { 181 | return " " + value[0]; 182 | } 183 | case NormalArrayFormats.MultiLine: 184 | return convertStringArrayToMultilineArray(value); 185 | case TagSpecificArrayFormats.SingleStringSpaceDelimited: 186 | if (value.length === 1) { 187 | return " " + value[0]; 188 | } 189 | 190 | return " " + value.join(" "); 191 | case SpecialArrayFormats.SingleStringCommaDelimited: 192 | if (value.length === 1) { 193 | return " " + value[0]; 194 | } 195 | 196 | return " " + value.join(", "); 197 | case TagSpecificArrayFormats.SingleLineSpaceDelimited: 198 | if (value.length === 1) { 199 | return " " + value[0]; 200 | } 201 | 202 | return ( 203 | " " + 204 | convertStringArrayToSingleLineArray(value).replaceAll(", ", " ") 205 | ); 206 | } 207 | /* eslint-enable no-fallthrough */ 208 | } 209 | 210 | function getDefaultYAMLArrayValue( 211 | format: NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats 212 | ): string { 213 | /* eslint-disable no-fallthrough */ 214 | switch (format) { 215 | case NormalArrayFormats.SingleLine: 216 | case TagSpecificArrayFormats.SingleLineSpaceDelimited: 217 | case NormalArrayFormats.MultiLine: 218 | return " []"; 219 | case SpecialArrayFormats.SingleStringToSingleLine: 220 | case SpecialArrayFormats.SingleStringToMultiLine: 221 | case TagSpecificArrayFormats.SingleStringSpaceDelimited: 222 | case SpecialArrayFormats.SingleStringCommaDelimited: 223 | return " "; 224 | } 225 | /* eslint-enable no-fallthrough */ 226 | } 227 | 228 | function convertStringArrayToSingleLineArray(arrayItems: string[]): string { 229 | if (arrayItems == null || arrayItems.length === 0) { 230 | return "[]"; 231 | } 232 | 233 | return "[" + arrayItems.join(", ") + "]"; 234 | } 235 | 236 | function convertStringArrayToMultilineArray(arrayItems: string[]): string { 237 | if (arrayItems == null || arrayItems.length === 0) { 238 | return "[]"; 239 | } 240 | 241 | return "\n - " + arrayItems.join("\n - "); 242 | } 243 | 244 | /** 245 | * Parses single-line and multi-line arrays into an array that can be used for formatting down the line 246 | * @param {string} value The value to see about parsing if it is a sing-line or multi-line array 247 | * @return The original value if it was not a single or multi-line array or the an array of the values from the array (multi-line arrays will have empty values removed) 248 | */ 249 | export function splitValueIfSingleOrMultilineArray(value: string) { 250 | if (value == null || value.length === 0) { 251 | return null; 252 | } 253 | 254 | value = value.trimEnd(); 255 | if (value.startsWith("[")) { 256 | value = value.substring(1); 257 | 258 | if (value.endsWith("]")) { 259 | value = value.substring(0, value.length - 1); 260 | } 261 | 262 | // accounts for an empty single line array which can then be converted as needed later on 263 | if (value.length === 0) { 264 | return null; 265 | } 266 | 267 | const arrayItems = convertYAMLStringToArray(value, ","); 268 | if (!arrayItems) return null; 269 | return arrayItems.filter((el: string) => { 270 | return el != ""; 271 | }); 272 | } 273 | 274 | if (value.includes("\n")) { 275 | let arrayItems = value.split(/[ \t]*\n[ \t]*-[ \t]*/); 276 | arrayItems.splice(0, 1); 277 | 278 | arrayItems = arrayItems.filter((el: string) => { 279 | return el != ""; 280 | }); 281 | 282 | if (arrayItems == null || arrayItems.length === 0) { 283 | return null; 284 | } 285 | 286 | return arrayItems; 287 | } 288 | 289 | return value; 290 | } 291 | 292 | /** 293 | * Converts the tag string to the proper split up values based on whether or not it is already an array and if it has delimiters. 294 | * @param {string | string[]} value The value that is already good to go or needs to be split on a comma or spaces. 295 | * @return {string} The converted tag key value that should account for its obsidian formats. 296 | */ 297 | export function convertTagValueToStringOrStringArray(value: string | string[]) { 298 | if (value == null) { 299 | return []; 300 | } 301 | 302 | const tags: string[] = []; 303 | let originalTagValues: string[] = []; 304 | if (Array.isArray(value)) { 305 | originalTagValues = value; 306 | } else if (value.includes(",")) { 307 | originalTagValues = convertYAMLStringToArray(value, ",") ?? []; 308 | } else { 309 | originalTagValues = convertYAMLStringToArray(value, " ") ?? []; 310 | } 311 | 312 | for (const tagValue of originalTagValues) { 313 | tags.push(tagValue.trim()); 314 | } 315 | 316 | return tags; 317 | } 318 | 319 | /** 320 | * Converts the alias over to the appropriate array items for formatting taking into account obsidian formats. 321 | * @param {string | string[]} value The value of the aliases key that may need to be split into the appropriate parts. 322 | */ 323 | export function convertAliasValueToStringOrStringArray( 324 | value: string | string[] 325 | ) { 326 | if (typeof value === "string") { 327 | return convertYAMLStringToArray(value, ","); 328 | } 329 | 330 | return value; 331 | } 332 | 333 | export function convertYAMLStringToArray( 334 | value: string, 335 | delimiter: string = "," 336 | ) { 337 | if (value == "" || value == null) { 338 | return null; 339 | } 340 | 341 | if (delimiter.length > 1) { 342 | throw new Error( 343 | `The delimiter provided is not a single character: ${delimiter}` 344 | ); 345 | } 346 | 347 | const arrayItems: string[] = []; 348 | let currentItem = ""; 349 | let index = 0; 350 | while (index < value.length) { 351 | const currentChar = value.charAt(index); 352 | 353 | if (currentChar === delimiter) { 354 | // case where you find a delimiter 355 | arrayItems.push(currentItem.trim()); 356 | currentItem = ""; 357 | } else if (currentChar === '"' || currentChar === "'") { 358 | // if there is an escape character check to see if there is a closing escape character and if so, skip to it as the next part of the value 359 | const endOfEscapedValue = value.indexOf(currentChar, index + 1); 360 | if (endOfEscapedValue != -1) { 361 | currentItem += value.substring(index, endOfEscapedValue + 1); 362 | index = endOfEscapedValue; 363 | } else { 364 | currentItem += currentChar; 365 | } 366 | } else { 367 | currentItem += currentChar; 368 | } 369 | 370 | index++; 371 | } 372 | 373 | if (currentItem.trim() != "") { 374 | arrayItems.push(currentItem.trim()); 375 | } 376 | 377 | return arrayItems; 378 | } 379 | 380 | /** 381 | * Returns whether or not the YAML string value is already escaped 382 | * @param {string} value The YAML string to check if it is already escaped 383 | * @return {boolean} Whether or not the YAML string value is already escaped 384 | */ 385 | export function isValueEscapedAlready(value: string): boolean { 386 | return ( 387 | value.length > 1 && 388 | ((value.startsWith("'") && value.endsWith("'")) || 389 | (value.startsWith('"') && value.endsWith('"'))) 390 | ); 391 | } 392 | 393 | /** 394 | * Escapes the provided string value if it has a colon with a space after it, a single quote, or a double quote, but not a single and double quote. 395 | * @param {string} value The value to escape if possible 396 | * @param {string} defaultEscapeCharacter The character escape to use around the value if a specific escape character is not needed. 397 | * @param {boolean} forceEscape Whether or not to force the escaping of the value provided. 398 | * @param {boolean} skipValidation Whether or not to ensure that the result string could be unescaped back to the value. 399 | * @return {string} The escaped value if it is either necessary or forced and the provided value if it cannot be escaped, is escaped, 400 | * or does not need escaping and the force escape is not used. 401 | */ 402 | export function escapeStringIfNecessaryAndPossible( 403 | value: string, 404 | defaultEscapeCharacter: QuoteCharacter, 405 | forceEscape: boolean = false, 406 | skipValidation: boolean = false 407 | ): string { 408 | const basicEscape = basicEscapeString( 409 | value, 410 | defaultEscapeCharacter, 411 | forceEscape 412 | ); 413 | if (skipValidation) { 414 | return basicEscape; 415 | } 416 | 417 | try { 418 | const unescaped = load(basicEscape) as string; 419 | if (unescaped === value) { 420 | return basicEscape; 421 | } 422 | } catch { 423 | // invalid YAML 424 | } 425 | 426 | const escapeWithDefaultCharacter = dump(value, { 427 | lineWidth: -1, 428 | quotingType: defaultEscapeCharacter, 429 | forceQuotes: forceEscape, 430 | }).slice(0, -1); 431 | 432 | const escapeWithOtherCharacter = dump(value, { 433 | lineWidth: -1, 434 | quotingType: defaultEscapeCharacter == '"' ? "'" : '"', 435 | forceQuotes: forceEscape, 436 | }).slice(0, -1); 437 | 438 | if ( 439 | escapeWithOtherCharacter === value || 440 | escapeWithOtherCharacter.length < escapeWithDefaultCharacter.length 441 | ) { 442 | return escapeWithOtherCharacter; 443 | } 444 | 445 | return escapeWithDefaultCharacter; 446 | } 447 | 448 | function basicEscapeString( 449 | value: string, 450 | defaultEscapeCharacter: QuoteCharacter, 451 | forceEscape: boolean = false 452 | ): string { 453 | if (isValueEscapedAlready(value)) { 454 | return value; 455 | } 456 | 457 | // if there is no single quote, double quote, or colon to escape, skip this substring 458 | const substringHasSingleQuote = value.includes("'"); 459 | const substringHasDoubleQuote = value.includes('"'); 460 | const substringHasColonWithSpaceAfterIt = value.includes(": "); 461 | if ( 462 | !substringHasSingleQuote && 463 | !substringHasDoubleQuote && 464 | !substringHasColonWithSpaceAfterIt && 465 | !forceEscape 466 | ) { 467 | return value; 468 | } 469 | 470 | // if the substring already has a single quote and a double quote, there is nothing that can be done to escape the substring 471 | if (substringHasSingleQuote && substringHasDoubleQuote) { 472 | return value; 473 | } 474 | 475 | if (substringHasSingleQuote) { 476 | return `"${value}"`; 477 | } else if (substringHasDoubleQuote) { 478 | return `'${value}'`; 479 | } 480 | 481 | // the line must have a colon with a space 482 | return `${defaultEscapeCharacter}${value}${defaultEscapeCharacter}`; 483 | } 484 | 485 | export const splitYamlAndBody = (markdown: string) => { 486 | const parts = markdown.split(/^---$/m); 487 | if (!markdown.startsWith("---") || parts.length === 1) { 488 | return { 489 | yaml: undefined, 490 | body: markdown, 491 | }; 492 | } 493 | if (parts.length < 3) { 494 | return { 495 | yaml: parts[1] as string, 496 | body: parts[2] ?? "", 497 | }; 498 | } 499 | return { 500 | yaml: parts[1], 501 | body: parts.slice(2).join("---"), 502 | }; 503 | }; 504 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "allowSyntheticDefaultImports": true, 15 | "noUncheckedIndexedAccess": true, 16 | "skipLibCheck": true, 17 | "lib": ["DOM", "ES5", "ES6", "ES7", "ESNext"], 18 | "types": ["bun-types"], 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = JSON.parse(readFileSync("package.json", "utf8")).version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.3": "0.15.0", 5 | "1.0.4": "0.15.0", 6 | "1.0.5": "0.15.0", 7 | "1.0.6": "0.15.0", 8 | "1.0.7": "0.15.0", 9 | "1.0.8": "0.15.0" 10 | } --------------------------------------------------------------------------------