├── .editorconfig ├── .envrc ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── FUNDING.yml ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── example ├── daily_note.md ├── nested_dates.png ├── preview.gif └── settings.gif ├── flake.lock ├── flake.nix ├── manifest.json ├── package.json ├── src ├── main.ts ├── note_utils.ts ├── suggest.ts ├── ui.ts └── utils.ts ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake . 2 | -------------------------------------------------------------------------------- /.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/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 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "20.x" 19 | 20 | - name: Build plugin 21 | run: | 22 | npm install 23 | npm run build 24 | 25 | - name: Create release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | tag="${GITHUB_REF#refs/tags/}" 30 | 31 | gh release create "$tag" \ 32 | --title="$tag" \ 33 | --draft \ 34 | main.js manifest.json 35 | -------------------------------------------------------------------------------- /.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 | *error.log 25 | 26 | # Exclude direnv directory for nix development 27 | .direnv 28 | 29 | tests/ 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: objectivist 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dimitar Dimitrov 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 | # Obligator 2 | 3 | Obligator replaces and extends the function of the built in `Daily notes` plugin. 4 | Unchecked to-do items will be copied over to the new daily note, along with all 5 | of the headings and formatting structure you used to organize them, and any 6 | scheduled notes that you've set up. It is a convenient way to manage your 7 | to-do list, and leaves you with running history of notes that you can 8 | reference if you want to. 9 | 10 | ![](example/preview.gif) 11 | 12 | ## How to use Obligator 13 | 14 | Make sure you have fully filled out the settings page, then click the carrot 15 | icon on the sidebar to open your daily note. If today's note doesn't already 16 | exist, a new note file will be made reflecting today's date. If it does exist, 17 | it will simply bring you to the existing file -- so feel free to click that 18 | carrot to your hearts content. 19 | 20 | See the [example template file](example/daily_note.md) for some inspiration on 21 | how to set up your daily note. Here is an animation showing a valid template and 22 | a how to set up the settings properly with that template: 23 | 24 | ![](example/settings.gif) 25 | 26 | ### Nested folder date format 27 | You may use a directory structure in your date format, for example: 28 | `YYYY/MM-MMMM/YYYY-MM-DD`. This will create notes in a structure that looks 29 | something like this: 30 | 31 | ![](example/nested_dates.png) 32 | 33 | The archive function will not delete directories if you use this in combination 34 | with a nested directory structure. I suggest only using a nested structure for 35 | the archive date format. 36 | 37 | ### Template macros 38 | * {{date}}, {{time}}, and {{title}} work as they normally would. 39 | 40 | 41 | * {{previous_note}} and {{previous_note_path}} create back-links to the 42 | previous note from the current note. If there is no previous note, then 43 | these variables will be blank. 44 | 45 | * {{next_note}} and {{next_note_path}} create forward-links to the 46 | next note from the last note. These are added to the last note only when a 47 | new note is made. 48 | 49 | * {{ obligate * * * }} is a very powerful macro. It lets you set up 50 | recurring to-do items in your template. It uses a simplified version of the 51 | cron syntax, where the asterisk represent day-of-the-month, 52 | month-of-the-year, and day-of-the-week in that order. See [crontab.guru](https://crontab.guru) 53 | to play around with the syntax. The obligate macro will add the immediately 54 | proceeding line if you the date matches **for any day between your 55 | last note and today**. See the [example template file](example/daily_note.md) 56 | for plenty of common examples. If you are confused about this and want some 57 | help, just open a GitHub issue. 58 | 59 | 60 | ## Comparison with rollover-daily-todos 61 | I was motivated to create this project because my to-do list is extensive, with 62 | lots of formatting that I wanted to maintain between notes, but rollover-daily-todos 63 | copies *only* the unchecked to-do items and places them all into a specified 64 | header, killing all of the formatting. 65 | 66 | Here is a basic overview of the differences between the two: 67 | 68 | - Obligator copies over all unchecked to-do items as well as all the formatting 69 | that wraps them. 70 | 71 | - Rollover-daily-todos only copies unchecked to-do items, and places them under 72 | one heading. 73 | 74 | - Obligator does not rely on the `Daily notes` or `Periodic notes` plugin. 75 | 76 | - Obligator has the {{ obligate * * * }} macro for automatically scheduling 77 | to-do items in new notes. 78 | 79 | - Obligator has an old note archiving feature to keep your notes directory clean. 80 | 81 | - Obligator has the ability to delete empty headings to keep your daily note clean. 82 | 83 | - Obligator deals with nested to-do items hierarchically, so a checked parent with 84 | any amount of unchecked children will get copied over wholly to preserve the 85 | structure. 86 | 87 | - & More! 88 | 89 | ## Building 90 | * `yarn install` (install dependencies) 91 | * `yarn build` (compile typescript to javascript `main.js`) 92 | * restart Obsidian, or toggle on and off the plugin 93 | * For development, `yarn dev` listens for changes and rebuilds automatically. 94 | 95 | ## Releasing 96 | * `yarn version` 97 | * `yarn release` 98 | 99 | ## Attributions 100 | * File suggestions code taken from [mirnovov](https://github.com/mirnovov/obsidian-homepage/blob/main/src/suggest.ts) 101 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 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 | } -------------------------------------------------------------------------------- /example/daily_note.md: -------------------------------------------------------------------------------- 1 | ## Personal tasks 2 | {{ obligate 1,15 * * }} 3 | - [ ] water the plants (added on the 1st and 15th of every month) 4 | ## Work tasks 5 | {{ obligate * * 3 }} 6 | - [ ] write the weekly email (added every Wednesday) 7 | {{ obligate * * 5 }} 8 | - [ ] send the weekly email (added every Friday) 9 | ## Further Obligator template examples 10 | {{ obligate * * * }} 11 | - [ ] every time a new note is made 12 | {{ obligate 1 2 * }} 13 | - [ ] Add this on the 1st of February 14 | {{ obligate * 1-6 1,5 }} 15 | - [ ] every Monday and Wednesday for the first 6 months of the year 16 | {{ obligate 1-7,15-21 1,3,5,7,9,11 7 }} 17 | - [ ] every other Sunday but only every other month 18 | {{ obligate * 10-12 * }} 19 | - [ ] every day in Q4 20 | {{ obligate 1 1,4,7,10 * }} 21 | - [ ] on the first of every quarter 22 | {{ obligate 20 10 * }} 23 | - [ ] on the 20th of October 24 | {{ obligate 20 10 7 }} 25 | - [ ] on the 20th of October, if it's a Sunday 26 | # 🥕 27 | ---- 28 | 29 | Created on {{date}} {{time}} 30 | Previous note: [[{{previous_note_path}}|{{previous_note}}]] 31 | Next note: [[{{next_note_path}}|{{next_note}}]] 32 | -------------------------------------------------------------------------------- /example/nested_dates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Newbrict/obsidian-obligator/42ea2c6803550c0d38068a1998a6cce8ffb705a8/example/nested_dates.png -------------------------------------------------------------------------------- /example/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Newbrict/obsidian-obligator/42ea2c6803550c0d38068a1998a6cce8ffb705a8/example/preview.gif -------------------------------------------------------------------------------- /example/settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Newbrict/obsidian-obligator/42ea2c6803550c0d38068a1998a6cce8ffb705a8/example/settings.gif -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1697059129, 6 | "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Obligator development environment"; 3 | 4 | # Flake inputs 5 | inputs = { 6 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 7 | }; 8 | 9 | # Flake outputs 10 | outputs = { self, nixpkgs }: 11 | let 12 | # Systems supported 13 | allSystems = [ 14 | "x86_64-linux" # 64-bit Intel/AMD Linux 15 | "aarch64-linux" # 64-bit ARM Linux 16 | "x86_64-darwin" # 64-bit Intel macOS 17 | "aarch64-darwin" # 64-bit ARM macOS 18 | ]; 19 | 20 | # Helper to provide system-specific attributes 21 | forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f { 22 | pkgs = import nixpkgs { inherit system; }; 23 | }); 24 | in { 25 | # Development environment output 26 | devShells = forAllSystems ({ pkgs }: { 27 | default = pkgs.mkShell { 28 | # The Nix packages provided in the environment 29 | packages = with pkgs; [ 30 | nodejs_20 31 | yarn 32 | ]; 33 | }; 34 | }); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obligator", 3 | "name": "Obligator", 4 | "version": "5.2.0", 5 | "minAppVersion": "0.15.0", 6 | "description": "A fully featured replacement for the built-in daily notes plugin. Obligator functions like a virtual bullet journal by copying over unchecked to-do items to your new daily note, along with adding any scheduled items you've set up", 7 | "author": "Dimitar Dimitrov", 8 | "authorUrl": "https://github.com/Newbrict", 9 | "fundingUrl": "https://ko-fi.com/objectivist", 10 | "isDesktopOnly": false 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obligator", 3 | "version": "5.2.0", 4 | "description": "Obligator is a replacement for daily-todos which copies over unchecked todo items under a specified header", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "make-git-tag": "git tag -a $npm_package_version -m '$npm_package_version'", 11 | "push-git-tag": "git push origin $npm_package_version && git push", 12 | "release": "yarn build && yarn make-git-tag && yarn push-git-tag" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/node": "^16.11.6", 19 | "@typescript-eslint/eslint-plugin": "5.29.0", 20 | "@typescript-eslint/parser": "5.29.0", 21 | "builtin-modules": "3.3.0", 22 | "esbuild": "0.17.3", 23 | "obsidian": "latest", 24 | "tslib": "2.4.0", 25 | "typescript": "4.7.4" 26 | }, 27 | "dependencies": { 28 | "@popperjs/core": "^2.11.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Editor, 4 | MarkdownView, 5 | Modal, 6 | Notice, 7 | Plugin, 8 | PluginSettingTab, 9 | Setting, 10 | TFolder, 11 | TFile, 12 | MomentFormatComponent, 13 | ToggleComponent, 14 | normalizePath 15 | } from 'obsidian'; 16 | 17 | import { 18 | FileSuggest, 19 | FolderSuggest 20 | } from "./ui"; 21 | 22 | import { 23 | HEADING_REGEX_GLOBAL, 24 | OBLIGATION_REGEX, 25 | CHECKEDBOX_REGEX, 26 | structurize, 27 | destructure, 28 | merge_structure, 29 | filter_structure, 30 | cron_segment_to_list, 31 | should_trigger_obligation, 32 | strip_frontmatter 33 | } from "./note_utils" 34 | 35 | interface ObligatorSettings { 36 | initial: string; 37 | terminal: string; 38 | date_format: string; 39 | template_path: string; 40 | note_path: string; 41 | archive: boolean; 42 | archive_path: string; 43 | archive_date_format: string; 44 | delete_empty_headings: boolean; 45 | keep_template_headings: boolean; 46 | run_on_startup: boolean; 47 | keep_until_parent_complete: boolean; 48 | } 49 | 50 | const DEFAULT_SETTINGS: ObligatorSettings = { 51 | initial: "", 52 | terminal: "", 53 | date_format: "YYYY-MM-DD", 54 | template_path: "", 55 | note_path: "", 56 | archive: false, 57 | archive_path: "", 58 | archive_date_format: "YYYY/MM-MMMM/YYYY-MM-DD", 59 | delete_empty_headings: true, 60 | keep_template_headings: true, 61 | run_on_startup: false, 62 | keep_until_parent_complete: false 63 | } 64 | 65 | export default class Obligator extends Plugin { 66 | settings: ObligatorSettings; 67 | 68 | async onload() { 69 | await this.loadSettings(); 70 | 71 | const run_obligator = async () => { 72 | 73 | // ---------------------------------------------------------------- 74 | // Basic logical overview 75 | // ---------------------------------------------------------------- 76 | // 1. Check that all of the settings are correctly set, and 77 | // initialize some basic values we'll need later. 78 | // 2. Switch to today's note if it already exists. 79 | // 3. Process the last note, if there is one. 80 | // 4. Process the template file. 81 | // 5. Merge the contents of the last note into the template. 82 | // Do the last_note templates, archiving, and open the new file. 83 | // ---------------------------------------------------------------- 84 | // Step 1 85 | // ---------------------------------------------------------------- 86 | 87 | // Make sure the note path is set, if not, error. 88 | if (["", null].includes(this.settings.note_path)) { 89 | new Notice(`You must specify a note path in the settings.`); 90 | return; 91 | } 92 | 93 | // Make sure the note folder exists 94 | const NOTE_FOLDER = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.note_path)); 95 | if (NOTE_FOLDER == undefined) { 96 | new Notice(`The note path "${this.settings.note_path}" specified in the settings does not exist, aborting...`); 97 | return; 98 | } 99 | 100 | // Make sure that the template file exists 101 | const TEMPLATE_FILE = this.app.vault.getAbstractFileByPath(`${this.settings.template_path}.md`); 102 | if (TEMPLATE_FILE == undefined) { 103 | if (["", null].includes(this.settings.template_path)) { 104 | new Notice(`You must specify a template file in the settings.`); 105 | } else { 106 | new Notice(`The template file "${this.settings.template_path}" specified in the settings does not exist.`); 107 | } 108 | return; 109 | } 110 | if (!(TEMPLATE_FILE instanceof TFile)) { 111 | new Notice(`${this.settings.template_path} is not a regular file! Aborting.`); 112 | return; 113 | } 114 | 115 | // Read the template contents here for checking. It will primarily 116 | // be used in step 4, though. 117 | let template_contents = await this.app.vault.read(TEMPLATE_FILE); 118 | 119 | // Make sure the initial / terminal settings are valid. 120 | if (this.settings.initial === null || this.settings.initial === undefined) { 121 | this.settings.initial = ""; 122 | } 123 | if (this.settings.terminal === null || this.settings.terminal === undefined) { 124 | this.settings.terminal = ""; 125 | } 126 | await this.saveSettings(); 127 | if (this.settings.terminal != "" && this.settings.initial != "") { 128 | const check_template_lines = template_contents.split('\n'); 129 | const initial_index = check_template_lines.indexOf(this.settings.initial); 130 | const terminal_index = check_template_lines.indexOf(this.settings.terminal); 131 | if (terminal_index <= initial_index) { 132 | new Notice("The initial heading must preceed the terminal heading. Aborting."); 133 | return; 134 | } 135 | } 136 | 137 | 138 | // Make sure that the archive path is set if the archive option is on. 139 | if (this.settings.archive) { 140 | if (["", null].includes(this.settings.archive_path)) { 141 | new Notice("The archive path must be specified when the archive option is turned on, aborting."); 142 | return; 143 | } 144 | } 145 | 146 | // Make sure the default value is applied if it's left blank 147 | const NOW = window.moment(); 148 | const DATE_FORMAT = this.settings.date_format || DEFAULT_SETTINGS.date_format; 149 | const ARCHIVE_DATE_FORMAT = this.settings.archive_date_format || DEFAULT_SETTINGS.archive_date_format; 150 | const NOTE_NAME = NOW.format(DATE_FORMAT); 151 | const ACTIVE_LEAF = this.app.workspace.getLeaf(); 152 | 153 | // ---------------------------------------------------------------- 154 | // Step 2 155 | // Context: settings are valid 156 | // ---------------------------------------------------------------- 157 | const NEW_NOTE_PATH = `${this.settings.note_path}/${NOTE_NAME}.md` 158 | let output_file = this.app.vault.getAbstractFileByPath(NEW_NOTE_PATH); 159 | if (output_file != undefined && output_file instanceof TFile) { 160 | await ACTIVE_LEAF.openFile(output_file); 161 | return; 162 | } 163 | 164 | // ---------------------------------------------------------------- 165 | // Step 3 166 | // Context: settings are valid, new note doesn't exist. 167 | // ---------------------------------------------------------------- 168 | // Get a list of all the files in the daily notes directory 169 | const find_all_notes = (path:string):TFile[] => { 170 | const abstract = this.app.vault.getAbstractFileByPath(normalizePath(path)); 171 | let notes: TFile[] = []; 172 | if (abstract instanceof TFile && abstract.extension === "md") { 173 | notes.push(abstract); 174 | } else if (abstract instanceof TFolder) { 175 | for (let child of abstract.children) { 176 | notes = notes.concat(find_all_notes(`${path}/${child.name}`)); 177 | } 178 | } 179 | return notes; 180 | } 181 | const notes = find_all_notes(this.settings.note_path); 182 | notes.sort((a, b) => { 183 | const a_name = a.path.slice(this.settings.note_path.length + 1); 184 | const b_name = b.path.slice(this.settings.note_path.length + 1); 185 | return window.moment(b_name, this.settings.date_format).valueOf() 186 | - window.moment(a_name, this.settings.date_format).valueOf(); 187 | }); 188 | 189 | // Get the last note that's not today's. 190 | let last_note = null; 191 | for (let i=0; i < notes.length; i++) { 192 | // Remove the ".md" extension 193 | const sub_path = notes[i].path.slice(this.settings.note_path.length + 1).slice(0, -3); 194 | // The final boolean makes the moment parse in strict mode 195 | const note_moment = window.moment(sub_path, this.settings.date_format, true); 196 | if (note_moment.isValid() && note_moment.isBefore(NOW, 'day')) { 197 | last_note = notes[i]; 198 | break; 199 | } 200 | } 201 | 202 | let last_note_structure = null; 203 | 204 | if (last_note) { 205 | const last_note_content = await this.app.vault.read(last_note); 206 | const last_note_lines = strip_frontmatter(last_note_content.split('\n')); 207 | let last_note_initial_index = last_note_lines.indexOf(this.settings.initial); 208 | if (this.settings.initial === "") { 209 | last_note_initial_index = 0; 210 | } else if (last_note_initial_index === -1) { 211 | new Notice(`${last_note.basename} does not contain the specified initial heading... aborting.`); 212 | return; 213 | } 214 | let last_note_terminal_index = last_note_lines.indexOf(this.settings.terminal); 215 | if (this.settings.terminal === "") { 216 | last_note_terminal_index = last_note_lines.length; 217 | } else if (last_note_terminal_index === -1) { 218 | new Notice(`${last_note.basename} does not contain the specified terminal heading... aborting.`); 219 | return; 220 | } 221 | last_note_structure = structurize(last_note_lines.slice(last_note_initial_index, last_note_terminal_index)) 222 | } 223 | 224 | // ---------------------------------------------------------------- 225 | // Step 4 226 | // Context: settings are valid, new note doesn't exist, 227 | // last note has been processed 228 | // ---------------------------------------------------------------- 229 | 230 | // ------------------------------------------------------------ 231 | // {{ date }} macro 232 | // ------------------------------------------------------------ 233 | template_contents = template_contents.replace(/{{\s*date:?(.*?)\s*}}/g, (_, format) => { 234 | if (format) { 235 | return NOW.format(format) 236 | } else { 237 | // default format 238 | return NOW.format("YYYY-MM-DD") 239 | } 240 | }); 241 | 242 | // ------------------------------------------------------------ 243 | // {{ time }} macro 244 | // ------------------------------------------------------------ 245 | template_contents = template_contents.replace(/{{\s*time:?(.*?)\s*}}/g, (_, format) => { 246 | if (format) { 247 | return NOW.format(format) 248 | } else { 249 | // default format 250 | return NOW.format("HH:mm") 251 | } 252 | }); 253 | 254 | // ------------------------------------------------------------ 255 | // {{ title }} macro 256 | // ------------------------------------------------------------ 257 | template_contents = template_contents.replace(/{{\s*title\s*}}/g, NOTE_NAME); 258 | 259 | // ------------------------------------------------------------ 260 | // {{ previous_note }} and {{ previous_note_path }} macros 261 | // ------------------------------------------------------------ 262 | if (last_note === null) { 263 | template_contents = template_contents.replace(/{{\s*previous_note\s*}}/g, ""); 264 | template_contents = template_contents.replace(/{{\s*previous_note_path\s*}}/g, ""); 265 | } else { 266 | template_contents = template_contents.replace(/{{\s*previous_note\s*}}/g, last_note.basename); 267 | template_contents = template_contents.replace(/{{\s*previous_note_path\s*}}/g, last_note.path); 268 | } 269 | 270 | // ------------------------------------------------------------ 271 | // {{ obligate }} macro 272 | // ------------------------------------------------------------ 273 | const template_lines = template_contents.split('\n') 274 | let processed_lines = []; 275 | for (let i = 0; i < template_lines.length; i++) { 276 | const line = template_lines[i]; 277 | // If this is an obligator line, then go ahead and run the 278 | // logic which deterimnes if we should now obligate 279 | if (OBLIGATION_REGEX.test(line)) { 280 | // Increment the iterator so we skip over the next line 281 | i++; 282 | let should_trigger = should_trigger_obligation(line, NOW); 283 | // If there is no source note, then we would only 284 | // trigger above. 285 | if (!should_trigger && last_note) { 286 | // Walk forward from the source note date (+1) and 287 | // check every skipped date. 288 | const sub_path = last_note.path.slice(this.settings.note_path.length + 1); 289 | let skipped_moment = window.moment(sub_path, this.settings.date_format); 290 | while (skipped_moment.add(1, 'd').isBefore(NOW) && !should_trigger) { 291 | should_trigger = should_trigger_obligation(line, skipped_moment); 292 | } 293 | } 294 | if (should_trigger) { 295 | // Increment the iterator and get the next line 296 | try { 297 | // This is the NEXT line, since we incremented the iterator above 298 | processed_lines.push(template_lines[i]); 299 | } catch (error) { 300 | new Notice(`Template malformed, "${line}" must be followed by another line`); 301 | return; 302 | } 303 | } 304 | // Not an obligator line, so just add it. 305 | } else { 306 | processed_lines.push(line); 307 | } 308 | } 309 | 310 | let processed_initial_index = processed_lines.indexOf(this.settings.initial); 311 | if (this.settings.initial === "") { 312 | processed_initial_index = 0; 313 | } else if (processed_initial_index === -1) { 314 | new Notice(`${TEMPLATE_FILE.basename} does not contain the specified initial heading... aborting.`); 315 | return; 316 | } 317 | let processed_terminal_index = processed_lines.indexOf(this.settings.terminal); 318 | if (this.settings.terminal === "") { 319 | processed_terminal_index = processed_lines.length; 320 | } else if (processed_terminal_index === -1) { 321 | new Notice(`${TEMPLATE_FILE.basename} does not contain the specified terminal heading... aborting.`); 322 | return; 323 | } 324 | let template_structure = structurize(processed_lines.slice(processed_initial_index, processed_terminal_index)); 325 | const OUTPUT_INITIAL_LINES = processed_lines.slice(0, processed_initial_index) 326 | const OUTPUT_TERMINAL_LINES = processed_lines.slice(processed_terminal_index) 327 | 328 | // ---------------------------------------------------------------- 329 | // Step 5 330 | // Context: settings are valid, new note doesn't exist, 331 | // last note has been processed, 332 | // template has been processed 333 | // ---------------------------------------------------------------- 334 | // Delete from last_note_structure only if this setting is true 335 | if (last_note_structure) { 336 | if (this.settings.keep_template_headings) { 337 | filter_structure(last_note_structure, 338 | this.settings.delete_empty_headings, 339 | this.settings.keep_until_parent_complete); 340 | } 341 | merge_structure( 342 | template_structure, 343 | last_note_structure 344 | ); 345 | } 346 | 347 | // Delete from the merged structure is false 348 | if (!this.settings.keep_template_headings) { 349 | filter_structure(template_structure, 350 | this.settings.delete_empty_headings, 351 | this.settings.keep_until_parent_complete); 352 | } 353 | 354 | let new_note_lines = OUTPUT_INITIAL_LINES.concat(destructure(template_structure)).concat(OUTPUT_TERMINAL_LINES); 355 | 356 | const directories = NEW_NOTE_PATH.split('/').slice(0,-1); 357 | for (let i = 1; i <= directories.length; i++) { 358 | const sub_path = directories.slice(0,i).join('/'); 359 | const abstract = this.app.vault.getAbstractFileByPath(normalizePath(sub_path)); 360 | if (abstract === null) { 361 | this.app.vault.createFolder(sub_path); 362 | } 363 | } 364 | 365 | output_file = await this.app.vault.create(NEW_NOTE_PATH, new_note_lines.join('\n')); 366 | 367 | // Open up the new file 368 | if (output_file != undefined && output_file instanceof TFile) { 369 | await ACTIVE_LEAF.openFile(output_file); 370 | } 371 | 372 | // Apply the next_note and next_note_path macros to the old file 373 | if (last_note instanceof TFile) { 374 | await this.app.vault.process(last_note, (data) => { 375 | if (data !== null && output_file instanceof TFile) { 376 | data = data.replace(/{{\s*next_note\s*}}/g, output_file.basename); 377 | data = data.replace(/{{\s*next_note_path\s*}}/g, output_file.path); 378 | } 379 | return data; 380 | }); 381 | } 382 | 383 | if (this.settings.archive && last_note) { 384 | 385 | const last_note_name = last_note.path.slice(this.settings.note_path.length + 1); 386 | const last_note_moment = window.moment(last_note_name, this.settings.date_format); 387 | const archive_note_name = last_note_moment.format(ARCHIVE_DATE_FORMAT); 388 | const archive_note_path = `${this.settings.archive_path}/${archive_note_name}.md`; 389 | try { 390 | const archive_directories = archive_note_path.split('/').slice(0,-1); 391 | for (let i = 1; i <= archive_directories.length; i++) { 392 | const sub_path = archive_directories.slice(0,i).join('/'); 393 | const abstract = this.app.vault.getAbstractFileByPath(normalizePath(sub_path)); 394 | if (abstract === null) { 395 | await this.app.vault.createFolder(sub_path); 396 | } 397 | } 398 | await this.app.fileManager.renameFile(last_note, archive_note_path); 399 | } catch (error) { 400 | new Notice(`A file called ${archive_note_path} already exists, archival skipped.`); 401 | } 402 | } 403 | 404 | }; 405 | 406 | // This creates an icon in the left ribbon. 407 | // This function is called when the user clicks the icon. 408 | const ribbonIconEl = this.addRibbonIcon('carrot', `Open today's obligator note`, run_obligator); 409 | 410 | // Add the command to open today's Obligator note 411 | this.addCommand({ 412 | id: "obligator-run", 413 | name: "Open today's note", 414 | callback: run_obligator 415 | }); 416 | 417 | this.app.workspace.onLayoutReady(async () => { 418 | // Run obligator if the user has enabled the startup setting. 419 | if (this.settings.run_on_startup) { 420 | run_obligator(); 421 | } 422 | }); 423 | 424 | // This adds a settings tab so the user can configure various aspects of the plugin 425 | this.addSettingTab(new ObligatorSettingTab(this.app, this)); 426 | 427 | } 428 | 429 | onunload() { 430 | 431 | } 432 | 433 | async loadSettings() { 434 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 435 | } 436 | 437 | async saveSettings() { 438 | await this.saveData(this.settings); 439 | } 440 | } 441 | 442 | class ObligatorSettingTab extends PluginSettingTab { 443 | plugin: Obligator; 444 | 445 | constructor(app: App, plugin: Obligator) { 446 | super(app, plugin); 447 | this.plugin = plugin; 448 | } 449 | async is_file(file_path:string) { 450 | let file = this.app.vault.getAbstractFileByPath(file_path); 451 | if (file === null) { 452 | file = this.app.vault.getAbstractFileByPath(`${file_path}.md`); 453 | } 454 | if (file === null || !(file instanceof TFile)) { 455 | return false; 456 | } 457 | return true; 458 | } 459 | 460 | async getHeadings(file_path:string) { 461 | let file = this.app.vault.getAbstractFileByPath(file_path); 462 | if (file === null) { 463 | file = this.app.vault.getAbstractFileByPath(`${file_path}.md`); 464 | } 465 | if (file === null || !(file instanceof TFile)) { 466 | return []; 467 | } 468 | const content = await this.app.vault.read(file); 469 | // TODO The +1 below is a hack to insert "" at index zero. I should 470 | // figure out a proper approach. 471 | let headings: {[index:string]:any} = Array.from(content.matchAll(HEADING_REGEX_GLOBAL)) 472 | .reduce((accumulator, [heading], index) => { 473 | return {...accumulator, [(index + 1).toString()]: heading}; 474 | }, {}); 475 | headings["0"] = ""; 476 | return headings; 477 | } 478 | 479 | async display(): Promise { 480 | const {containerEl} = this; 481 | 482 | containerEl.empty(); 483 | 484 | containerEl.createEl('h1', {text: 'Basic Settings'}); 485 | // -------------------------------------------------------------------- 486 | // New note file directory 487 | // -------------------------------------------------------------------- 488 | new Setting(containerEl) 489 | .setName("New file directory") 490 | .setDesc("New daily notes will be placed here.") 491 | .addText(text => { 492 | new FolderSuggest(this.app, text.inputEl); 493 | text.setValue(this.plugin.settings.note_path) 494 | .onChange(async value => { 495 | this.plugin.settings.note_path = value; 496 | await this.plugin.saveSettings(); 497 | }) 498 | }); 499 | 500 | // -------------------------------------------------------------------- 501 | // Template file 502 | // -------------------------------------------------------------------- 503 | new Setting(containerEl) 504 | .setName("Template file") 505 | .setDesc("New daily notes will utilize the template file specified.") 506 | .addText(text => { 507 | new FileSuggest(this.app, text.inputEl); 508 | text.setValue(this.plugin.settings.template_path) 509 | .onChange(async value => { 510 | const check = await this.is_file(value); 511 | if (check || value === "" ) { 512 | this.plugin.settings.template_path = value; 513 | await this.plugin.saveSettings(); 514 | if (check) { 515 | this.display(); 516 | } 517 | } 518 | }) 519 | }); 520 | 521 | // -------------------------------------------------------------------- 522 | // New note file date format (file name) 523 | // -------------------------------------------------------------------- 524 | let date_formatter: MomentFormatComponent; 525 | const setting_date_format = new Setting(containerEl) 526 | .setName("Date format") 527 | .addMomentFormat((format: MomentFormatComponent) => { 528 | date_formatter = format 529 | .setDefaultFormat(DEFAULT_SETTINGS.date_format) 530 | .setPlaceholder(DEFAULT_SETTINGS.date_format) 531 | .setValue(this.plugin.settings.date_format) 532 | .onChange(async (value) => { 533 | this.plugin.settings.date_format = value; 534 | await this.plugin.saveSettings(); 535 | }); 536 | }); 537 | 538 | const date_format_el = setting_date_format.descEl.createEl("b", { 539 | cls: "u-pop", 540 | text: "test" 541 | }); 542 | // @ts-ignore 543 | date_formatter.setSampleEl(date_format_el); 544 | setting_date_format.descEl.append( 545 | "For syntax information, refer to the ", 546 | setting_date_format.descEl.createEl("a", { 547 | href: "https://momentjs.com/docs/#/displaying/format/", 548 | text: "moment documentation" 549 | }), 550 | setting_date_format.descEl.createEl("br"), 551 | "Today's note would look like this: ", 552 | date_format_el 553 | ); 554 | 555 | // -------------------------------------------------------------------- 556 | // Create the dropdown options for the initial / terminal heading selection 557 | // -------------------------------------------------------------------- 558 | const headings = await this.getHeadings(this.plugin.settings.template_path); 559 | const terminal_value = Object.keys(headings) 560 | .find(key => headings[key] === this.plugin.settings.terminal) 561 | || ""; 562 | const initial_value = Object.keys(headings) 563 | .find(key => headings[key] === this.plugin.settings.initial) 564 | || ""; 565 | // -------------------------------------------------------------------- 566 | // The heading which initializes the to-do list items 567 | // -------------------------------------------------------------------- 568 | new Setting(containerEl) 569 | .setName('Initial Heading') 570 | .setDesc(`(Optional) The heading from the template which begins what Obligator will copy. Everything before this heading in your note will be ignored. If left blank, Obligator will copy from the beginning of the file.`) 571 | .addDropdown(dropdown => dropdown 572 | .addOptions(headings) 573 | .setValue(initial_value) 574 | .onChange(async value => { 575 | this.plugin.settings.initial = headings[value] || null; 576 | await this.plugin.saveSettings(); 577 | }) 578 | ); 579 | // -------------------------------------------------------------------- 580 | // The heading which terminates the to-do list items 581 | // -------------------------------------------------------------------- 582 | new Setting(containerEl) 583 | .setName('Terminal Heading') 584 | .setDesc(`(Optional) The heading from the template which terminates what Obligator will copy. Everything after this heading in your note will be ignored. If left blank, Obligator will copy to the end of the file.`) 585 | .addDropdown(dropdown => dropdown 586 | .addOptions(headings) 587 | .setValue(terminal_value) 588 | .onChange(async value => { 589 | this.plugin.settings.terminal = headings[value] || null; 590 | await this.plugin.saveSettings(); 591 | }) 592 | ); 593 | 594 | // -------------------------------------------------------------------- 595 | // Toggle for running obligator on startup 596 | // -------------------------------------------------------------------- 597 | new Setting(containerEl) 598 | .setName("Open Obligator note on startup") 599 | .setDesc(`Open your Obligator note automatically when you open this vault.`) 600 | .addToggle(toggle => { toggle 601 | .setValue(this.plugin.settings.run_on_startup) 602 | .onChange(async value => { 603 | this.plugin.settings.run_on_startup = value; 604 | await this.plugin.saveSettings(); 605 | }) 606 | }) 607 | 608 | containerEl.createEl('h1', {text: 'Archive Settings'}); 609 | // -------------------------------------------------------------------- 610 | // Toggle the archiving function 611 | // -------------------------------------------------------------------- 612 | new Setting(containerEl) 613 | .setName("Enable archival of old notes") 614 | .setDesc(`Enabling this will move the previous to-do note into the 615 | directory specified when a new note is created. The note 616 | will be renamed according to the date format specified.`) 617 | .addToggle(toggle => { toggle 618 | .setValue(this.plugin.settings.archive) 619 | .onChange(async value => { 620 | this.plugin.settings.archive = value; 621 | await this.plugin.saveSettings(); 622 | }) 623 | }); 624 | 625 | // -------------------------------------------------------------------- 626 | // Archive note file directory 627 | // -------------------------------------------------------------------- 628 | new Setting(containerEl) 629 | .setName("Archive directory") 630 | .setDesc(`Archived notes will be moved here.`) 631 | .addText(text => { 632 | new FolderSuggest(this.app, text.inputEl); 633 | text.setValue(this.plugin.settings.archive_path).onChange( 634 | async value => { 635 | this.plugin.settings.archive_path = value; 636 | await this.plugin.saveSettings(); 637 | }) 638 | }); 639 | 640 | // -------------------------------------------------------------------- 641 | // Archive note file date format (file name) 642 | // -------------------------------------------------------------------- 643 | let archive_date_formatter: MomentFormatComponent; 644 | const setting_archive_date_format = new Setting(containerEl) 645 | .setName("Archive date format") 646 | .addMomentFormat((format: MomentFormatComponent) => { 647 | archive_date_formatter = format 648 | .setDefaultFormat(DEFAULT_SETTINGS.archive_date_format) 649 | .setPlaceholder(DEFAULT_SETTINGS.archive_date_format) 650 | .setValue(this.plugin.settings.archive_date_format) 651 | .onChange(async (value) => { 652 | this.plugin.settings.archive_date_format = value; 653 | await this.plugin.saveSettings(); 654 | }); 655 | }); 656 | 657 | const archive_date_format_el = setting_archive_date_format.descEl.createEl("b", { 658 | cls: "u-pop", 659 | text: "test" 660 | }); 661 | // @ts-ignore 662 | archive_date_formatter.setSampleEl(archive_date_format_el); 663 | setting_archive_date_format.descEl.append( 664 | "For syntax information, refer to the ", 665 | setting_archive_date_format.descEl.createEl("a", { 666 | href: "https://momentjs.com/docs/#/displaying/format/", 667 | text: "moment documentation" 668 | }), 669 | setting_archive_date_format.descEl.createEl("br"), 670 | "The archival path for today's note would look like this: ", 671 | archive_date_format_el 672 | ); 673 | 674 | 675 | containerEl.createEl('h1', {text: 'Advanced Settings'}); 676 | 677 | let setting_keep_template_headings:Setting; 678 | let toggle_keep_template_headings:ToggleComponent; 679 | // -------------------------------------------------------------------- 680 | // Toggle for the setting to delete empty headings. 681 | // -------------------------------------------------------------------- 682 | new Setting(containerEl) 683 | .setName("Delete empty headings") 684 | .setDesc(`If this is enabled, obligator will automatically delete 685 | headings which don't have any non-whitespace children when 686 | you create a new daily note. Turning this off will leave 687 | all headings untouched, even if they have no contents.`) 688 | .addToggle(toggle => { toggle 689 | .setValue(this.plugin.settings.delete_empty_headings) 690 | .onChange(async value => { 691 | this.plugin.settings.delete_empty_headings = value; 692 | await this.plugin.saveSettings(); 693 | // Make sure the element has been created before using it 694 | if (setting_keep_template_headings instanceof Setting) { 695 | if (toggle_keep_template_headings instanceof ToggleComponent) { 696 | setting_keep_template_headings.setDisabled(!value); 697 | this.plugin.settings.keep_template_headings = value; 698 | toggle_keep_template_headings.setValue(value); 699 | await this.plugin.saveSettings(); 700 | } 701 | } 702 | }) 703 | }) 704 | // -------------------------------------------------------------------- 705 | // Toggle for the setting to delete headings 706 | // -------------------------------------------------------------------- 707 | setting_keep_template_headings = new Setting(containerEl) 708 | .setName("Don't delete headings from template") 709 | .setDesc(`This prevents the setting above from deleting any 710 | headings which are present in the template`) 711 | .addToggle(toggle => {toggle 712 | .setValue(this.plugin.settings.keep_template_headings) 713 | .onChange(async value => { 714 | this.plugin.settings.keep_template_headings = value; 715 | await this.plugin.saveSettings(); 716 | }) 717 | toggle_keep_template_headings = toggle; 718 | }).setDisabled(!this.plugin.settings.delete_empty_headings); 719 | 720 | // -------------------------------------------------------------------- 721 | // Toggle for the setting to keep children if the parent isn't complete 722 | // -------------------------------------------------------------------- 723 | setting_keep_template_headings = new Setting(containerEl) 724 | .setName("Only delete to-dos when parent is complete") 725 | .setDesc(`To-dos which are children of other to-dos will not be 726 | deleted unless the parent is checked, and all of its 727 | children are too. This setting would be used if you want 728 | to retain checked to-dos until the whole structure is 729 | checked.`) 730 | .addToggle(toggle => {toggle 731 | .setValue(this.plugin.settings.keep_until_parent_complete) 732 | .onChange(async value => { 733 | this.plugin.settings.keep_until_parent_complete = value; 734 | await this.plugin.saveSettings(); 735 | }) 736 | }); 737 | } 738 | } 739 | -------------------------------------------------------------------------------- /src/note_utils.ts: -------------------------------------------------------------------------------- 1 | export const HEADING_REGEX = /^#{1,7} +\S.*/m; 2 | export const HEADING_REGEX_GLOBAL = /^#{1,7} +\S.*/gm; 3 | 4 | // https://regex101.com/r/e7avuk/1 5 | export const CHECKBOX_REGEX = /^(\s*)-\s+\[(.)\].*$/m; 6 | export const UNCHECKEDBOX_REGEX = /^\s*-\s+\[[^xX]\].*$/m; 7 | export const CHECKEDBOX_REGEX = /^\s*-\s+\[[xX]\].*$/m; 8 | 9 | // https://regex101.com/r/adwhVh/1 10 | export const OBLIGATION_REGEX = /^\s*{{\s*obligate\s+([\*\-,\d]+)\s+([\*\-,\d]+)\s+([\*\-,\d]+)\s*}}\s*$/; 11 | 12 | export function get_heading_level(heading:string|null) { 13 | if (heading && HEADING_REGEX.test(heading)) { 14 | return heading.replace(/[^#]/g, "").length; 15 | } 16 | return 0; 17 | } 18 | 19 | export function get_checkbox_level(checkbox:string|null) { 20 | if (checkbox && CHECKBOX_REGEX.test(checkbox)) { 21 | // This is offset by 1 so that the logic matches the heading logic 22 | return checkbox.replace(CHECKBOX_REGEX, "$1").length + 1; 23 | } 24 | return 0; 25 | } 26 | 27 | interface Parent { 28 | text: string|null; 29 | children: (Parent|string)[]; 30 | total: number; 31 | } 32 | 33 | // Structurize takes a set of lines from a note file and structures them 34 | // hierarchically based on fold scope. 35 | export function structurize(lines:string[], text:string|null=null):Parent { 36 | const parent_heading_level = get_heading_level(text); 37 | const parent_checkbox_level = get_checkbox_level(text); 38 | let total = 0; 39 | let children = []; 40 | for (let i = 0; i < lines.length; i++) { 41 | const line = lines[i]; 42 | const is_heading = HEADING_REGEX.test(line); 43 | const is_checkbox = CHECKBOX_REGEX.test(line); 44 | 45 | // A heading cannot be the child of a checkbox. text is checked here 46 | // because it can be null when invoked 47 | if (is_heading && text && CHECKBOX_REGEX.test(text)) { 48 | break; 49 | } 50 | 51 | // A lower level means a greater scope. If this new heading has a 52 | // lesser or equal level, then there are no more children, we can 53 | // stop processing. 54 | if (is_heading && get_heading_level(line) <= parent_heading_level) { 55 | break; 56 | } 57 | if (is_checkbox && get_checkbox_level(line) <= parent_checkbox_level) { 58 | break; 59 | } 60 | 61 | // If this is a foldable, then we do the recursive step, otherwise we 62 | // can simply add the child to the list of children 63 | if (is_heading || is_checkbox) { 64 | const child = structurize(lines.slice(i+1), line); 65 | children.push(child); 66 | i += child.total; 67 | total += child.total; 68 | } else { 69 | // Since we increment the iterator above, this will always 70 | // be children on the same level, so they can be added. 71 | children.push(line) 72 | } 73 | total += 1; 74 | } 75 | return {text: text, children: children, total: total}; 76 | } 77 | 78 | // Flattens the heirarchy represented by the output of a call to structurize 79 | export function destructure(structure:Parent):string[] { 80 | let lines = []; 81 | if (structure.text) { 82 | lines.push(structure.text); 83 | } 84 | for (let i = 0; i < structure.children.length; i++) { 85 | const child = structure.children[i]; 86 | if (child instanceof Object) { 87 | lines.push(...destructure(child)) 88 | } else if (child !== undefined) { 89 | lines.push(child); 90 | } 91 | } 92 | return lines; 93 | } 94 | 95 | // Merges second into first 96 | // Limitation: Parent paths have to be unique within the structure. 97 | export function merge_structure (first:Parent, second:Parent) { 98 | // If we receive structures which don't have the same heading, the only 99 | // thing we can do is merge them under a new parent 100 | if (first.text !== second.text) { 101 | return { 102 | text: null, 103 | children: [first, second], 104 | total: first.total + second.total + 2 105 | } 106 | } 107 | 108 | // Go through each child in second, and check if it's in first 109 | second.children.forEach((child) => { 110 | if (child instanceof Object) { 111 | 112 | // Ignoring this type check because the code actually works fine. 113 | // @ts-ignore 114 | const first_child = first.children.find(c => c.text === child.text); 115 | if (first_child && first_child instanceof Object) { 116 | const old_total = first_child.total; 117 | merge_structure(first_child, child); 118 | first.total += first_child.total-old_total; 119 | } else { 120 | if (child.text && CHECKBOX_REGEX.test(child.text)) { 121 | const heading_index = first.children.findIndex(c => c instanceof Object && c.text && HEADING_REGEX.test(c.text)); 122 | if (heading_index > -1) { 123 | first.children.splice(heading_index, 0, child); 124 | } else { 125 | first.children.push(child); 126 | } 127 | } else { 128 | first.children.push(child); 129 | first.total += child.total + 1; 130 | } 131 | } 132 | } else { 133 | // Add missing children 134 | if (!first.children.contains(child)) { 135 | const heading_index = first.children.findIndex(c => c instanceof Object && c.text && HEADING_REGEX.test(c.text)); 136 | if (heading_index > -1) { 137 | first.children.splice(heading_index, 0, child); 138 | } else { 139 | first.children.push(child); 140 | } 141 | // Text children must come before any headings or else they'll 142 | // get wrapped into other fold scopes when heading children 143 | // are added. 144 | first.total += 1; 145 | } 146 | } 147 | }); 148 | } 149 | 150 | export function filter_structure(structure:Parent, 151 | delete_headings:boolean, 152 | keep_until_parent_complete:boolean) { 153 | for (let i = 0; i < structure.children.length; i++) { 154 | const child = structure.children[i]; 155 | if (typeof child === "object") { 156 | 157 | // child.text is checked here because it can be null when invoked. 158 | if (child.text) { 159 | if (CHECKBOX_REGEX.test(child.text) 160 | && !CHECKEDBOX_REGEX.test(child.text) 161 | && keep_until_parent_complete) { 162 | // Here we have a non-checked checkbox and the setting is not 163 | // to delete until everything is checked, so we should just 164 | // continue at this point. 165 | continue; 166 | } 167 | filter_structure(child, 168 | delete_headings, 169 | keep_until_parent_complete) 170 | if (HEADING_REGEX.test(child.text)) { 171 | if (delete_headings) { 172 | const non_empty = child.children.filter((element) => { 173 | if (typeof element === "object") { 174 | return true; 175 | } else { 176 | if (!/^\s*$/m.test(element)) { 177 | return true; 178 | } 179 | } 180 | return false; 181 | }); 182 | if (non_empty.length === 0) { 183 | delete structure.children[i]; 184 | structure.total = structure.total - child.children.length; 185 | } 186 | } 187 | } 188 | // Only delete checkedboxes if they have no unchecked children 189 | if (CHECKEDBOX_REGEX.test(child.text)) { 190 | // Count the number of unchecked children 191 | if (child.children.filter((element) => { 192 | if (typeof element == "object" && element.text !== null) { 193 | return CHECKBOX_REGEX.test(element.text) && !CHECKEDBOX_REGEX.test(element.text) 194 | } 195 | return false 196 | }).length === 0) { 197 | delete structure.children[i]; 198 | structure.total = structure.total - (1 + child.children.length); 199 | } 200 | } 201 | } 202 | } 203 | } 204 | structure.children = structure.children.filter((element) => element !== undefined); 205 | }; 206 | 207 | export function cron_segment_to_list(segment:string):number[] { 208 | let output:number[] = []; 209 | const RANGE_REGEX = /^(\d+)-(\d+)$/; 210 | for (let r of segment.split(',')) { 211 | if (RANGE_REGEX.test(r)) { 212 | // Non-null assertion operator in use here because it can't be null 213 | const start:number = parseInt(r.match(RANGE_REGEX)![1]); 214 | const end:number = parseInt(r.match(RANGE_REGEX)![2]); 215 | const expanded_range = Array.from({length: 1+end-start}, (_, i) => start + i); 216 | output = output.concat(expanded_range); 217 | } else { 218 | output.push(parseInt(r)); 219 | } 220 | } 221 | return output.sort((a,b) => a-b); 222 | } 223 | 224 | export function should_trigger_obligation(obligation_string:string, test_date:moment.Moment):boolean { 225 | // Parse the cron string 226 | // Non-null assertion operator in use here because it can't be null 227 | const day_months = cron_segment_to_list(obligation_string.match(OBLIGATION_REGEX)![1]); 228 | const month_years = cron_segment_to_list(obligation_string.match(OBLIGATION_REGEX)![2]); 229 | const day_weeks = cron_segment_to_list(obligation_string.match(OBLIGATION_REGEX)![3]); 230 | 231 | const test_day_month = parseInt(test_date.format('D')); 232 | const test_month_year = parseInt(test_date.format('M')); 233 | const test_day_week = test_date.day(); 234 | 235 | // includes(NaN) covers the * case. 236 | const matched_day_month = (day_months.includes(NaN) || (day_months.includes(test_day_month))); 237 | const matched_month_year = (month_years.includes(NaN) || (month_years.includes(test_month_year))); 238 | const matched_day_week = (day_weeks.includes(NaN) || (day_weeks.includes(test_day_week))) 239 | 240 | if (matched_day_month && matched_month_year && matched_day_week) { 241 | return true; 242 | } 243 | return false; 244 | }; 245 | 246 | export function strip_frontmatter(lines:string[]):string[] { 247 | if (lines.length == 0) { 248 | return lines 249 | } 250 | 251 | if (lines[0] === "---") { 252 | const end_index = lines.slice(1).indexOf("---"); 253 | if (end_index !== -1) { 254 | // +1 for the first slice above, 255 | // +1 to get above the index of the terminal 256 | return lines.slice(end_index + 2); 257 | } else { 258 | return lines 259 | } 260 | 261 | } 262 | 263 | return lines; 264 | } 265 | -------------------------------------------------------------------------------- /src/suggest.ts: -------------------------------------------------------------------------------- 1 | // Originally from https://github.com/mirnovov/obsidian-homepage/blob/main/src/suggest.ts 2 | import { App, ISuggestOwner, Scope } from "obsidian"; 3 | import { createPopper, Instance as PopperInstance } from "@popperjs/core"; 4 | import { wrapAround } from "./utils"; 5 | 6 | class Suggest { 7 | private owner: ISuggestOwner; 8 | private values: T[]; 9 | private suggestions: HTMLDivElement[]; 10 | private selectedItem: number; 11 | private containerEl: HTMLDivElement; 12 | 13 | constructor(owner: ISuggestOwner, containerEl: HTMLDivElement, scope: Scope) { 14 | this.owner = owner; 15 | this.containerEl = containerEl; 16 | 17 | containerEl.on( 18 | "click", 19 | ".suggestion-item", 20 | this.onSuggestionClick.bind(this) 21 | ); 22 | containerEl.on( 23 | "mousemove", 24 | ".suggestion-item", 25 | this.onSuggestionMouseover.bind(this) 26 | ); 27 | 28 | scope.register([], "ArrowUp", (event) => { 29 | if (!event.isComposing) { 30 | this.setSelectedItem(this.selectedItem - 1, true); 31 | return false; 32 | } 33 | }); 34 | 35 | scope.register([], "ArrowDown", (event) => { 36 | if (!event.isComposing) { 37 | this.setSelectedItem(this.selectedItem + 1, true); 38 | return false; 39 | } 40 | }); 41 | 42 | scope.register([], "Enter", (event) => { 43 | if (!event.isComposing) { 44 | this.useSelectedItem(event); 45 | return false; 46 | } 47 | }); 48 | } 49 | 50 | onSuggestionClick(event: MouseEvent, el: HTMLElement): void { 51 | event.preventDefault(); 52 | 53 | const item = this.suggestions.indexOf(el as HTMLDivElement); 54 | this.setSelectedItem(item, false); 55 | this.useSelectedItem(event); 56 | } 57 | 58 | onSuggestionMouseover(_event: MouseEvent, el: HTMLElement): void { 59 | const item = this.suggestions.indexOf(el as HTMLDivElement); 60 | this.setSelectedItem(item, false); 61 | } 62 | 63 | setSuggestions(values: T[]) { 64 | this.containerEl.empty(); 65 | const suggestionEls: HTMLDivElement[] = []; 66 | 67 | values.forEach((value) => { 68 | const suggestionEl = this.containerEl.createDiv("suggestion-item"); 69 | this.owner.renderSuggestion(value, suggestionEl); 70 | suggestionEls.push(suggestionEl); 71 | }); 72 | 73 | this.values = values; 74 | this.suggestions = suggestionEls; 75 | this.setSelectedItem(0, false); 76 | } 77 | 78 | useSelectedItem(event: MouseEvent | KeyboardEvent) { 79 | const currentValue = this.values[this.selectedItem]; 80 | if (currentValue) { 81 | this.owner.selectSuggestion(currentValue, event); 82 | } 83 | } 84 | 85 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { 86 | const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); 87 | const prevSelectedSuggestion = this.suggestions[this.selectedItem]; 88 | const selectedSuggestion = this.suggestions[normalizedIndex]; 89 | 90 | prevSelectedSuggestion?.removeClass("is-selected"); 91 | selectedSuggestion?.addClass("is-selected"); 92 | 93 | this.selectedItem = normalizedIndex; 94 | 95 | if (scrollIntoView) { 96 | selectedSuggestion.scrollIntoView(false); 97 | } 98 | } 99 | } 100 | 101 | export abstract class TextInputSuggest implements ISuggestOwner { 102 | protected app: App; 103 | protected inputEl: HTMLInputElement; 104 | 105 | private popper: PopperInstance; 106 | private scope: Scope; 107 | private suggestEl: HTMLElement; 108 | private suggest: Suggest; 109 | 110 | constructor(app: App, inputEl: HTMLInputElement) { 111 | this.app = app; 112 | this.inputEl = inputEl; 113 | this.scope = new Scope(); 114 | 115 | this.suggestEl = createDiv("suggestion-container"); 116 | const suggestion = this.suggestEl.createDiv("suggestion"); 117 | this.suggest = new Suggest(this, suggestion, this.scope); 118 | 119 | this.scope.register([], "Escape", this.close.bind(this)); 120 | 121 | this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); 122 | this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); 123 | this.inputEl.addEventListener("blur", this.close.bind(this)); 124 | this.suggestEl.on( 125 | "mousedown", 126 | ".suggestion-container", 127 | (event: MouseEvent) => { 128 | event.preventDefault(); 129 | } 130 | ); 131 | } 132 | 133 | onInputChanged(): void { 134 | const inputStr = this.inputEl.value; 135 | const suggestions = this.getSuggestions(inputStr); 136 | 137 | if (suggestions.length > 0) { 138 | this.suggest.setSuggestions(suggestions); 139 | this.open((this.app as any).dom.appContainerEl, this.inputEl); 140 | } 141 | } 142 | 143 | open(container: HTMLElement, inputEl: HTMLElement): void { 144 | (this.app as any).keymap.pushScope(this.scope); 145 | 146 | container.appendChild(this.suggestEl); 147 | this.popper = createPopper(inputEl, this.suggestEl, { 148 | placement: "bottom-start", 149 | modifiers: [ 150 | { 151 | name: "sameWidth", 152 | enabled: true, 153 | fn: ({ state, instance }) => { 154 | // Note: positioning needs to be calculated twice - 155 | // first pass - positioning it according to the width of the popper 156 | // second pass - position it with the width bound to the reference element 157 | // we need to early exit to avoid an infinite loop 158 | const targetWidth = `${state.rects.reference.width + 100}px`; 159 | if (state.styles.popper.width === targetWidth) { 160 | return; 161 | } 162 | state.styles.popper.width = targetWidth; 163 | instance.update(); 164 | }, 165 | phase: "beforeWrite", 166 | requires: ["computeStyles"], 167 | }, 168 | ], 169 | }); 170 | } 171 | 172 | close(): void { 173 | (this.app as any).keymap.popScope(this.scope); 174 | 175 | this.suggest.setSuggestions([]); 176 | if(this.popper) this.popper.destroy(); 177 | this.suggestEl.detach(); 178 | } 179 | 180 | abstract getSuggestions(inputStr: string): T[]; 181 | abstract renderSuggestion(item: T, el: HTMLElement): void; 182 | abstract selectSuggestion(item: T): void; 183 | } 184 | -------------------------------------------------------------------------------- /src/ui.ts: -------------------------------------------------------------------------------- 1 | // Originally from https://github.com/mirnovov/obsidian-homepage/blob/main/src/ui.ts 2 | import { App, FuzzySuggestModal, Notice, TAbstractFile, TFile, TFolder } from "obsidian"; 3 | import { TextInputSuggest } from "./suggest"; 4 | import { trimFile } from "./utils"; 5 | 6 | export class FileSuggest extends TextInputSuggest { 7 | getSuggestions(inputStr: string): TFile[] { 8 | const abstractFiles = this.app.vault.getAllLoadedFiles(); 9 | const files: TFile[] = []; 10 | const inputLower = inputStr.toLowerCase(); 11 | 12 | abstractFiles.forEach((file: TAbstractFile) => { 13 | if ( 14 | file instanceof TFile && "md" == file.extension && 15 | file.path.toLowerCase().contains(inputLower) 16 | ) { 17 | files.push(file); 18 | } 19 | }); 20 | 21 | return files; 22 | } 23 | 24 | renderSuggestion(file: TFile, el: HTMLElement) { 25 | if (file.extension == "md") { 26 | el.setText(trimFile(file)); 27 | } 28 | else { 29 | //we don't use trimFile here as the extension isn't displayed here 30 | el.setText(file.path.slice(0, -7)) 31 | el.insertAdjacentHTML( 32 | "beforeend", 33 | `` 34 | ); 35 | } 36 | } 37 | 38 | selectSuggestion(file: TFile) { 39 | this.inputEl.value = trimFile(file); 40 | this.inputEl.trigger("input"); 41 | this.close(); 42 | } 43 | } 44 | 45 | export class FolderSuggest extends TextInputSuggest { 46 | getSuggestions(inputStr: string): TFolder[] { 47 | const abstractFiles = this.app.vault.getAllLoadedFiles(); 48 | const folders: TFolder[] = []; 49 | const inputLower = inputStr.toLowerCase(); 50 | 51 | abstractFiles.forEach((file: TAbstractFile) => { 52 | if (file instanceof TFolder 53 | && file.path.toLowerCase().contains(inputLower)) { 54 | folders.push(file); 55 | } 56 | }); 57 | 58 | return folders; 59 | } 60 | 61 | renderSuggestion(folder: TFolder, el: HTMLElement) { 62 | // @ts-ignore 63 | el.setText(trimFile(folder)); 64 | } 65 | 66 | selectSuggestion(folder: TFolder) { 67 | // @ts-ignore 68 | this.inputEl.value = trimFile(folder); 69 | this.inputEl.trigger("input"); 70 | this.close(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Originally from https://github.com/mirnovov/obsidian-homepage/blob/main/src/utils.ts 2 | import { App, TFile, View as OView } from "obsidian"; 3 | 4 | export function trimFile(file: TFile): string { 5 | if (!file) return ""; 6 | return file.extension == "md" ? file.path.slice(0, -3): file.path; 7 | } 8 | 9 | export function untrimName(name: string): string { 10 | return name.endsWith(".canvas") ? name : `${name}.md`; 11 | } 12 | 13 | export function wrapAround(value: number, size: number): number { 14 | return ((value % size) + size) % size; 15 | }; 16 | 17 | export function randomFile(app: App): string | undefined { 18 | const files = app.vault.getFiles().filter( 19 | (f: TFile) => ["md", "canvas"].contains(f.extension) 20 | ); 21 | 22 | if (files.length) { 23 | const indice = Math.floor(Math.random() * files.length); 24 | return trimFile(files[indice]); 25 | } 26 | 27 | return undefined; 28 | } 29 | 30 | export function emptyActiveView(app: App): boolean { 31 | return app.workspace.getActiveViewOfType(OView)?.getViewType() == "empty"; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /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 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_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 | "2.2.0": "0.15.0", 4 | "2.3.0": "0.15.0", 5 | "2.4.0": "0.15.0", 6 | "2.5.0": "0.15.0", 7 | "2.6.0": "0.15.0", 8 | "3.0.0": "0.15.0", 9 | "3.1.0": "0.15.0", 10 | "3.2.0": "0.15.0", 11 | "3.3.0": "0.15.0", 12 | "3.4.0": "0.15.0", 13 | "3.4.1": "0.15.0", 14 | "3.4.2": "0.15.0", 15 | "3.4.3": "0.15.0", 16 | "3.5.0": "0.15.0", 17 | "3.6.0": "0.15.0", 18 | "3.7.0": "0.15.0", 19 | "3.8.0": "0.15.0", 20 | "3.9.0": "0.15.0", 21 | "3.10.0": "0.15.0", 22 | "3.11.0": "0.15.0", 23 | "3.12.0": "0.15.0", 24 | "3.13.0": "0.15.0", 25 | "3.14.0": "0.15.0", 26 | "3.15.0": "0.15.0", 27 | "3.16.0": "0.15.0", 28 | "3.17.0": "0.15.0", 29 | "3.18.0": "0.15.0", 30 | "3.19.0": "0.15.0", 31 | "3.20.0": "0.15.0", 32 | "3.20.1": "0.15.0", 33 | "3.20.2": "0.15.0", 34 | "4.0.0": "0.15.0", 35 | "4.1.0": "0.15.0", 36 | "5.0.0": "0.15.0", 37 | "5.1.0": "0.15.0", 38 | "5.2.0": "0.15.0" 39 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/android-arm64@0.17.3": 6 | version "0.17.3" 7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz#35d045f69c9b4cf3f8efcd1ced24a560213d3346" 8 | integrity sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ== 9 | 10 | "@esbuild/android-arm@0.17.3": 11 | version "0.17.3" 12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.3.tgz#4986d26306a7440078d42b3bf580d186ef714286" 13 | integrity sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg== 14 | 15 | "@esbuild/android-x64@0.17.3": 16 | version "0.17.3" 17 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.3.tgz#a1928cd681e4055103384103c8bd34df7b9c7b19" 18 | integrity sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A== 19 | 20 | "@esbuild/darwin-arm64@0.17.3": 21 | version "0.17.3" 22 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz#e4af2b392e5606a4808d3a78a99d38c27af39f1d" 23 | integrity sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw== 24 | 25 | "@esbuild/darwin-x64@0.17.3": 26 | version "0.17.3" 27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz#cbcbfb32c8d5c86953f215b48384287530c5a38e" 28 | integrity sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA== 29 | 30 | "@esbuild/freebsd-arm64@0.17.3": 31 | version "0.17.3" 32 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz#90ec1755abca4c3ffe1ad10819cd9d31deddcb89" 33 | integrity sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w== 34 | 35 | "@esbuild/freebsd-x64@0.17.3": 36 | version "0.17.3" 37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz#8760eedc466af253c3ed0dfa2940d0e59b8b0895" 38 | integrity sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg== 39 | 40 | "@esbuild/linux-arm64@0.17.3": 41 | version "0.17.3" 42 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz#13916fc8873115d7d546656e19037267b12d4567" 43 | integrity sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g== 44 | 45 | "@esbuild/linux-arm@0.17.3": 46 | version "0.17.3" 47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz#15f876d127b244635ddc09eaaa65ae97bc472a63" 48 | integrity sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ== 49 | 50 | "@esbuild/linux-ia32@0.17.3": 51 | version "0.17.3" 52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz#6691f02555d45b698195c81c9070ab4e521ef005" 53 | integrity sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA== 54 | 55 | "@esbuild/linux-loong64@0.17.3": 56 | version "0.17.3" 57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz#f77ef657f222d8b3a8fbd530a09e40976c458d48" 58 | integrity sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA== 59 | 60 | "@esbuild/linux-mips64el@0.17.3": 61 | version "0.17.3" 62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz#fa38833cfc8bfaadaa12b243257fe6d19d0f6f79" 63 | integrity sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ== 64 | 65 | "@esbuild/linux-ppc64@0.17.3": 66 | version "0.17.3" 67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz#c157a602b627c90d174743e4b0dfb7630b101dbf" 68 | integrity sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ== 69 | 70 | "@esbuild/linux-riscv64@0.17.3": 71 | version "0.17.3" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz#7bf79614bd544bd932839b1fcff6cf1f8f6bdf1a" 73 | integrity sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow== 74 | 75 | "@esbuild/linux-s390x@0.17.3": 76 | version "0.17.3" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz#6bb50c5a2613d31ce1137fe5c249ecadbecccdea" 78 | integrity sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug== 79 | 80 | "@esbuild/linux-x64@0.17.3": 81 | version "0.17.3" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz#aa140d99f0d9e0af388024823bfe4558d73fbbf9" 83 | integrity sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg== 84 | 85 | "@esbuild/netbsd-x64@0.17.3": 86 | version "0.17.3" 87 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz#b6ae9948b03e4c95dc581c68358fb61d9d12a625" 88 | integrity sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg== 89 | 90 | "@esbuild/openbsd-x64@0.17.3": 91 | version "0.17.3" 92 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz#cda007233e211fc9154324bfa460540cfc469408" 93 | integrity sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA== 94 | 95 | "@esbuild/sunos-x64@0.17.3": 96 | version "0.17.3" 97 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz#f1385b092000c662d360775f3fad80943d2169c4" 98 | integrity sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q== 99 | 100 | "@esbuild/win32-arm64@0.17.3": 101 | version "0.17.3" 102 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz#14e9dd9b1b55aa991f80c120fef0c4492d918801" 103 | integrity sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A== 104 | 105 | "@esbuild/win32-ia32@0.17.3": 106 | version "0.17.3" 107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz#de584423513d13304a6925e01233499a37a4e075" 108 | integrity sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg== 109 | 110 | "@esbuild/win32-x64@0.17.3": 111 | version "0.17.3" 112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz#2f69ea6b37031b0d1715dd2da832a8ae5eb36e74" 113 | integrity sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g== 114 | 115 | "@nodelib/fs.scandir@2.1.5": 116 | version "2.1.5" 117 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 118 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 119 | dependencies: 120 | "@nodelib/fs.stat" "2.0.5" 121 | run-parallel "^1.1.9" 122 | 123 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 124 | version "2.0.5" 125 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 126 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 127 | 128 | "@nodelib/fs.walk@^1.2.3": 129 | version "1.2.8" 130 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 131 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 132 | dependencies: 133 | "@nodelib/fs.scandir" "2.1.5" 134 | fastq "^1.6.0" 135 | 136 | "@popperjs/core@^2.11.8": 137 | version "2.11.8" 138 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" 139 | integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== 140 | 141 | "@types/codemirror@0.0.108": 142 | version "0.0.108" 143 | resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.108.tgz#e640422b666bf49251b384c390cdeb2362585bde" 144 | integrity sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw== 145 | dependencies: 146 | "@types/tern" "*" 147 | 148 | "@types/estree@*": 149 | version "1.0.1" 150 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" 151 | integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== 152 | 153 | "@types/json-schema@^7.0.9": 154 | version "7.0.12" 155 | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" 156 | integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== 157 | 158 | "@types/node@^16.11.6": 159 | version "16.18.34" 160 | resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.34.tgz#62d2099b30339dec4b1b04a14c96266459d7c8b2" 161 | integrity sha512-VmVm7gXwhkUimRfBwVI1CHhwp86jDWR04B5FGebMMyxV90SlCmFujwUHrxTD4oO+SOYU86SoxvhgeRQJY7iXFg== 162 | 163 | "@types/tern@*": 164 | version "0.23.4" 165 | resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb" 166 | integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg== 167 | dependencies: 168 | "@types/estree" "*" 169 | 170 | "@typescript-eslint/eslint-plugin@5.29.0": 171 | version "5.29.0" 172 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz#c67794d2b0fd0b4a47f50266088acdc52a08aab6" 173 | integrity sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w== 174 | dependencies: 175 | "@typescript-eslint/scope-manager" "5.29.0" 176 | "@typescript-eslint/type-utils" "5.29.0" 177 | "@typescript-eslint/utils" "5.29.0" 178 | debug "^4.3.4" 179 | functional-red-black-tree "^1.0.1" 180 | ignore "^5.2.0" 181 | regexpp "^3.2.0" 182 | semver "^7.3.7" 183 | tsutils "^3.21.0" 184 | 185 | "@typescript-eslint/parser@5.29.0": 186 | version "5.29.0" 187 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.29.0.tgz#41314b195b34d44ff38220caa55f3f93cfca43cf" 188 | integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw== 189 | dependencies: 190 | "@typescript-eslint/scope-manager" "5.29.0" 191 | "@typescript-eslint/types" "5.29.0" 192 | "@typescript-eslint/typescript-estree" "5.29.0" 193 | debug "^4.3.4" 194 | 195 | "@typescript-eslint/scope-manager@5.29.0": 196 | version "5.29.0" 197 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz#2a6a32e3416cb133e9af8dcf54bf077a916aeed3" 198 | integrity sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA== 199 | dependencies: 200 | "@typescript-eslint/types" "5.29.0" 201 | "@typescript-eslint/visitor-keys" "5.29.0" 202 | 203 | "@typescript-eslint/type-utils@5.29.0": 204 | version "5.29.0" 205 | resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz#241918001d164044020b37d26d5b9f4e37cc3d5d" 206 | integrity sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg== 207 | dependencies: 208 | "@typescript-eslint/utils" "5.29.0" 209 | debug "^4.3.4" 210 | tsutils "^3.21.0" 211 | 212 | "@typescript-eslint/types@5.29.0": 213 | version "5.29.0" 214 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.29.0.tgz#7861d3d288c031703b2d97bc113696b4d8c19aab" 215 | integrity sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg== 216 | 217 | "@typescript-eslint/typescript-estree@5.29.0": 218 | version "5.29.0" 219 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz#e83d19aa7fd2e74616aab2f25dfbe4de4f0b5577" 220 | integrity sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ== 221 | dependencies: 222 | "@typescript-eslint/types" "5.29.0" 223 | "@typescript-eslint/visitor-keys" "5.29.0" 224 | debug "^4.3.4" 225 | globby "^11.1.0" 226 | is-glob "^4.0.3" 227 | semver "^7.3.7" 228 | tsutils "^3.21.0" 229 | 230 | "@typescript-eslint/utils@5.29.0": 231 | version "5.29.0" 232 | resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.29.0.tgz#775046effd5019667bd086bcf326acbe32cd0082" 233 | integrity sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A== 234 | dependencies: 235 | "@types/json-schema" "^7.0.9" 236 | "@typescript-eslint/scope-manager" "5.29.0" 237 | "@typescript-eslint/types" "5.29.0" 238 | "@typescript-eslint/typescript-estree" "5.29.0" 239 | eslint-scope "^5.1.1" 240 | eslint-utils "^3.0.0" 241 | 242 | "@typescript-eslint/visitor-keys@5.29.0": 243 | version "5.29.0" 244 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz#7a4749fa7ef5160c44a451bf060ac1dc6dfb77ee" 245 | integrity sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ== 246 | dependencies: 247 | "@typescript-eslint/types" "5.29.0" 248 | eslint-visitor-keys "^3.3.0" 249 | 250 | array-union@^2.1.0: 251 | version "2.1.0" 252 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 253 | integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== 254 | 255 | braces@^3.0.2: 256 | version "3.0.2" 257 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 258 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 259 | dependencies: 260 | fill-range "^7.0.1" 261 | 262 | builtin-modules@3.3.0: 263 | version "3.3.0" 264 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" 265 | integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== 266 | 267 | debug@^4.3.4: 268 | version "4.3.4" 269 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 270 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 271 | dependencies: 272 | ms "2.1.2" 273 | 274 | dir-glob@^3.0.1: 275 | version "3.0.1" 276 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 277 | integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== 278 | dependencies: 279 | path-type "^4.0.0" 280 | 281 | esbuild@0.17.3: 282 | version "0.17.3" 283 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.3.tgz#d9aa02a3bc441ed35f9569cd9505812ae3fcae61" 284 | integrity sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g== 285 | optionalDependencies: 286 | "@esbuild/android-arm" "0.17.3" 287 | "@esbuild/android-arm64" "0.17.3" 288 | "@esbuild/android-x64" "0.17.3" 289 | "@esbuild/darwin-arm64" "0.17.3" 290 | "@esbuild/darwin-x64" "0.17.3" 291 | "@esbuild/freebsd-arm64" "0.17.3" 292 | "@esbuild/freebsd-x64" "0.17.3" 293 | "@esbuild/linux-arm" "0.17.3" 294 | "@esbuild/linux-arm64" "0.17.3" 295 | "@esbuild/linux-ia32" "0.17.3" 296 | "@esbuild/linux-loong64" "0.17.3" 297 | "@esbuild/linux-mips64el" "0.17.3" 298 | "@esbuild/linux-ppc64" "0.17.3" 299 | "@esbuild/linux-riscv64" "0.17.3" 300 | "@esbuild/linux-s390x" "0.17.3" 301 | "@esbuild/linux-x64" "0.17.3" 302 | "@esbuild/netbsd-x64" "0.17.3" 303 | "@esbuild/openbsd-x64" "0.17.3" 304 | "@esbuild/sunos-x64" "0.17.3" 305 | "@esbuild/win32-arm64" "0.17.3" 306 | "@esbuild/win32-ia32" "0.17.3" 307 | "@esbuild/win32-x64" "0.17.3" 308 | 309 | eslint-scope@^5.1.1: 310 | version "5.1.1" 311 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" 312 | integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== 313 | dependencies: 314 | esrecurse "^4.3.0" 315 | estraverse "^4.1.1" 316 | 317 | eslint-utils@^3.0.0: 318 | version "3.0.0" 319 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" 320 | integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== 321 | dependencies: 322 | eslint-visitor-keys "^2.0.0" 323 | 324 | eslint-visitor-keys@^2.0.0: 325 | version "2.1.0" 326 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" 327 | integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== 328 | 329 | eslint-visitor-keys@^3.3.0: 330 | version "3.4.1" 331 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" 332 | integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== 333 | 334 | esrecurse@^4.3.0: 335 | version "4.3.0" 336 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 337 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 338 | dependencies: 339 | estraverse "^5.2.0" 340 | 341 | estraverse@^4.1.1: 342 | version "4.3.0" 343 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 344 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 345 | 346 | estraverse@^5.2.0: 347 | version "5.3.0" 348 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 349 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 350 | 351 | fast-glob@^3.2.9: 352 | version "3.2.12" 353 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" 354 | integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== 355 | dependencies: 356 | "@nodelib/fs.stat" "^2.0.2" 357 | "@nodelib/fs.walk" "^1.2.3" 358 | glob-parent "^5.1.2" 359 | merge2 "^1.3.0" 360 | micromatch "^4.0.4" 361 | 362 | fastq@^1.6.0: 363 | version "1.15.0" 364 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" 365 | integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== 366 | dependencies: 367 | reusify "^1.0.4" 368 | 369 | fill-range@^7.0.1: 370 | version "7.0.1" 371 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 372 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 373 | dependencies: 374 | to-regex-range "^5.0.1" 375 | 376 | functional-red-black-tree@^1.0.1: 377 | version "1.0.1" 378 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 379 | integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== 380 | 381 | glob-parent@^5.1.2: 382 | version "5.1.2" 383 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 384 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 385 | dependencies: 386 | is-glob "^4.0.1" 387 | 388 | globby@^11.1.0: 389 | version "11.1.0" 390 | resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" 391 | integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== 392 | dependencies: 393 | array-union "^2.1.0" 394 | dir-glob "^3.0.1" 395 | fast-glob "^3.2.9" 396 | ignore "^5.2.0" 397 | merge2 "^1.4.1" 398 | slash "^3.0.0" 399 | 400 | ignore@^5.2.0: 401 | version "5.2.4" 402 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" 403 | integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== 404 | 405 | is-extglob@^2.1.1: 406 | version "2.1.1" 407 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 408 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 409 | 410 | is-glob@^4.0.1, is-glob@^4.0.3: 411 | version "4.0.3" 412 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 413 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 414 | dependencies: 415 | is-extglob "^2.1.1" 416 | 417 | is-number@^7.0.0: 418 | version "7.0.0" 419 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 420 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 421 | 422 | lru-cache@^6.0.0: 423 | version "6.0.0" 424 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 425 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 426 | dependencies: 427 | yallist "^4.0.0" 428 | 429 | merge2@^1.3.0, merge2@^1.4.1: 430 | version "1.4.1" 431 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 432 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 433 | 434 | micromatch@^4.0.4: 435 | version "4.0.5" 436 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 437 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 438 | dependencies: 439 | braces "^3.0.2" 440 | picomatch "^2.3.1" 441 | 442 | moment@2.29.4: 443 | version "2.29.4" 444 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" 445 | integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== 446 | 447 | ms@2.1.2: 448 | version "2.1.2" 449 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 450 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 451 | 452 | obsidian@latest: 453 | version "1.2.8" 454 | resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.2.8.tgz#294e76d9021cbf341ffd5d49e2bcfa15432da7d7" 455 | integrity sha512-HrC+feA8o0tXspj4lEAqxb1btwLwHD2oHXSwbbN+CdRHURqbCkuIDLld+nkuyJ1w1c9uvVDRVk8BoeOnWheOrQ== 456 | dependencies: 457 | "@types/codemirror" "0.0.108" 458 | moment "2.29.4" 459 | 460 | path-type@^4.0.0: 461 | version "4.0.0" 462 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 463 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 464 | 465 | picomatch@^2.3.1: 466 | version "2.3.1" 467 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 468 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 469 | 470 | queue-microtask@^1.2.2: 471 | version "1.2.3" 472 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 473 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 474 | 475 | regexpp@^3.2.0: 476 | version "3.2.0" 477 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" 478 | integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== 479 | 480 | reusify@^1.0.4: 481 | version "1.0.4" 482 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 483 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 484 | 485 | run-parallel@^1.1.9: 486 | version "1.2.0" 487 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 488 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 489 | dependencies: 490 | queue-microtask "^1.2.2" 491 | 492 | semver@^7.3.7: 493 | version "7.5.1" 494 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" 495 | integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== 496 | dependencies: 497 | lru-cache "^6.0.0" 498 | 499 | slash@^3.0.0: 500 | version "3.0.0" 501 | resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" 502 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 503 | 504 | to-regex-range@^5.0.1: 505 | version "5.0.1" 506 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 507 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 508 | dependencies: 509 | is-number "^7.0.0" 510 | 511 | tslib@2.4.0: 512 | version "2.4.0" 513 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" 514 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== 515 | 516 | tslib@^1.8.1: 517 | version "1.14.1" 518 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 519 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 520 | 521 | tsutils@^3.21.0: 522 | version "3.21.0" 523 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" 524 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== 525 | dependencies: 526 | tslib "^1.8.1" 527 | 528 | typescript@4.7.4: 529 | version "4.7.4" 530 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" 531 | integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== 532 | 533 | yallist@^4.0.0: 534 | version "4.0.0" 535 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 536 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 537 | --------------------------------------------------------------------------------