├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.json ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── IMetaEditApi.ts ├── MetaEditApi.ts ├── Modals │ ├── AutoPropertiesSettingModal │ │ ├── AutoPropertiesModal.ts │ │ └── AutoPropertiesModalContent.svelte │ ├── GenericPrompt │ │ ├── GenericPrompt.ts │ │ ├── GenericPromptContent.svelte │ │ └── genericTextSuggester.ts │ ├── GenericSuggester │ │ └── GenericSuggester.ts │ ├── IgnoredPropertiesSettingModal │ │ ├── IgnoredPropertiesModal.ts │ │ └── IgnoredPropertiesModalContent.svelte │ ├── KanbanHelperSetting │ │ ├── KanbanHelperSettingContent.svelte │ │ └── KanbanHelperSettingSuggester.ts │ ├── LinkMenu.ts │ ├── ProgressPropertiesSettingModal │ │ ├── ProgressPropertiesModal.ts │ │ └── ProgressPropertiesModalContent.svelte │ ├── metaEditSuggester.ts │ └── shared │ │ └── SingleValueTableEditorContent.svelte ├── Settings │ ├── defaultSettings.ts │ ├── metaEditSettings.ts │ └── metaEditSettingsTab.ts ├── Types │ ├── autoProperty.ts │ ├── datedFileCacheItem.ts │ ├── editMode.ts │ ├── kanbanProperty.ts │ ├── metaType.ts │ ├── progressProperty.ts │ └── progressPropertyOptions.ts ├── automators │ ├── IAutomatorManager.ts │ ├── onFileModifyAutomatorManager.ts │ └── onFileModifyAutomators │ │ ├── IOnFileModifyAutomator.ts │ │ ├── kanbanHelper.ts │ │ ├── onFileModifyAutomator.ts │ │ ├── onModifyAutomatorType.ts │ │ └── progressPropertyHelper.ts ├── constants.ts ├── dataviewHelper.ts ├── logger │ ├── consoleErrorLogger.ts │ ├── errorLevel.ts │ ├── guiLogger.ts │ ├── ilogger.ts │ ├── logManager.ts │ ├── logger.ts │ └── metaEditError.ts ├── main.ts ├── metaController.ts ├── parser.ts ├── suggest.ts ├── tests │ └── uniqueQueue.test.ts ├── uniqueQueue.ts ├── updatedFileCache.ts └── utility.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: chhoumann 2 | custom: https://www.buymeacoffee.com/chhoumann -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - uses: pnpm/action-setup@v2 14 | with: 15 | version: 7 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'pnpm' 21 | - name: Install dependencies 22 | run: pnpm install 23 | - name: Build 24 | run: pnpm build 25 | - name: Run tests 26 | run: | 27 | pnpm test 28 | - name: Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: pnpm semantic-release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Intellij 4 | *.iml 5 | .idea 6 | 7 | # npm 8 | node_modules 9 | package-lock.json 10 | 11 | # build 12 | main.js 13 | *.js.map 14 | 15 | # obsidian 16 | data.json 17 | /coverage/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MetaEdit for Obsidian 2 | 3 | ![v554FnTthq](https://user-images.githubusercontent.com/29108628/118363633-9933de80-b595-11eb-9603-31a3be0e0ccc.gif) 4 | 5 | ## Features 6 | - Add or update Yaml properties and Dataview fields easily 7 | - Ignore properties to hide them from the menu 8 | - Auto Properties that have customizable, pre-defined values selectable through a suggester 9 | - Multi-Value Mode that allows you to detect and vectorize/create arrays from your values 10 | - Progress Properties that automatically update properties/fields 11 | - Works with total task, completed task, and incomplete task counts. Mark a task as completed (from anywhere), and the file will be updated with the new count. 12 | - Transform properties between YAML and Dataview 13 | - Delete properties easily 14 | - Auto update properties in files linked to from Kanban boards on lane change 15 | - Edit metadata through a filemenu 16 | - Edit last value in tags - works with [Obsidian Tracker](https://github.com/pyrochlore/obsidian-tracker), too. 17 | - API to use in other plugins and Templater templates. 18 | 19 | ## Installation 20 | This plugin is in the community plugin browser in Obsidian. Search for MetaEdit and you can install it from there. 21 | 22 | ### Manual Installation 23 | 1. Go to [Releases](https://github.com/chhoumann/MetaEdit/releases) and download the ZIP file from the latest release. 24 | 2. This ZIP file should be extracted in your Obsidian plugins folder. If you don't know where that is, you can go to `Community Plugins` inside Obsidian. There is a folder icon on the right of `Installed Plugins`. Click that and it opens your plugins folder. 25 | 3. Extract the contents of the ZIP file there. 26 | 4. Now you should have a folder in plugins called 'metaedit' containing a `main.js` file, `manifest.json` file, and a `styles.css` file. 27 | 28 | https://user-images.githubusercontent.com/29108628/119513092-3223e000-bd74-11eb-9060-3e0cae4dbef3.mp4 29 | 30 | ## Guides 31 | ### Kanban Helper Guide 32 | https://user-images.githubusercontent.com/29108628/121333246-ebf48200-c918-11eb-889b-23b9a80299b2.mp4 33 | 34 | ## API 35 | You can access the API by using `app.plugins.plugins["metaedit"].api`. 36 | 37 | I recommend destructuring the API, like so: 38 | ```js 39 | const {autoprop} = this.app.plugins.plugins["metaedit"].api; 40 | ``` 41 | 42 | ### `autoprop(propertyName: string)` 43 | Takes a string containing a property name. Looks for the property in user settings and will open a suggester with possible values for that property. 44 | 45 | Returns the selected value. If no value was selected, or if the property was not found in settings, it returns `null`. 46 | 47 | This is an asynchronous function, so you should `await` it. 48 | 49 | ### `update(propertyName: string, propertyValue: string, file: TFile | string)` 50 | Updates a property with the given name to the given value in the given file. 51 | 52 | If the file is a string, it should be the file path. Otherwise, a `TFile` is fine. 53 | 54 | This is an asynchronous function, so you should `await` it. 55 | 56 | ### `getPropertyValue(propertyName: string, file: TFile | string)` 57 | Gets the value of the given property in the given file. 58 | 59 | If the file is a string, it should be the file path. Otherwise, a `TFile` is fine. 60 | 61 | This is an asynchronous function, so you should `await` it. 62 | 63 | ### API Examples 64 | #### New Task template (requires [Templater](https://github.com/SilentVoid13/Templater)) 65 | ``` 66 | <%* 67 | const {autoprop} = this.app.plugins.plugins["metaedit"].api; 68 | _%> 69 | #tasks 70 | Complete:: 0 71 | Project:: 72 | Status:: <% await autoprop("Status") %> 73 | Priority:: <% await autoprop("Priority") %> 74 | Due Date:: 75 | 76 | Complete:: 0 77 | Energy:: 78 | Estimated Time:: 79 | 80 | Total:: 1 81 | Complete:: 0 82 | Incomplete:: 1 83 | 84 | --- 85 | 86 | - [ ] <% tp.file.cursor() %> 87 | ``` 88 | ![3EfcPLYkj6](https://user-images.githubusercontent.com/29108628/119262986-85175f00-bbdd-11eb-8073-424fe9ec93c2.gif) 89 | #### Complete Task in Dataview Table (Buttons version) 90 | Requires [Dataview](https://github.com/blacksmithgu/obsidian-dataview) and [Buttons](https://github.com/shabegom/buttons/). 91 | ```` 92 | ```dataviewjs 93 | const {update} = this.app.plugins.plugins["metaedit"].api 94 | const {createButton} = app.plugins.plugins["buttons"] 95 | 96 | dv.table(["Name", "Status", "Project", "Due Date", ""], dv.pages("#tasks") 97 | .sort(t => t["due-date"], 'desc') 98 | .where(t => t.status != "Completed") 99 | .map(t => [t.file.link, t.status, t.project, t["due-date"], 100 | createButton({app, el: this.container, args: {name: "Done!"}, clickOverride: {click: update, params: ['Status', 'Completed', t.file.path]}})]) 101 | ) 102 | ``` 103 | ```` 104 | ![CBrFA0qHr4](https://user-images.githubusercontent.com/29108628/119342641-ab003a80-bc95-11eb-8f0a-15a6ced6b36d.gif) 105 | 106 | 107 | #### Complete Task in Dataview Table (HTML buttons version) 108 | Requires [Dataview](https://github.com/blacksmithgu/obsidian-dataview). 109 | ```` 110 | ```dataviewjs 111 | const {update} = this.app.plugins.plugins["metaedit"].api; 112 | const buttonMaker = (pn, pv, fpath) => { 113 | const btn = this.container.createEl('button', {"text": "Done!"}); 114 | const file = this.app.vault.getAbstractFileByPath(fpath) 115 | btn.addEventListener('click', async (evt) => { 116 | evt.preventDefault(); 117 | await update(pn, pv, file); 118 | }); 119 | return btn; 120 | } 121 | dv.table(["Name", "Status", "Project", "Due Date", ""], dv.pages("#tasks") 122 | .sort(t => t["due-date"], 'desc') 123 | .where(t => t.status != "Completed") 124 | .map(t => [t.file.link, t.status, t.project, t["due-date"], 125 | buttonMaker('Status', 'Completed', t.file.path)]) 126 | ) 127 | ``` 128 | ```` 129 | ![BnAVIV4XCM](https://user-images.githubusercontent.com/29108628/119342519-7d1af600-bc95-11eb-8ff8-09f19027131e.gif) 130 | 131 | --- 132 | ### Dev Info 133 | Made by Christian B. B. Houmann 134 | Discord: Chhrriissyy#6548 135 | Twitter: [https://twitter.com/chrisbbh](https://twitter.com/chrisbbh) 136 | Feel free to @ me if you have any questions. 137 | 138 | 139 | Also from dev: [NoteTweet: Post tweets directly from Obsidian.](https://github.com/chhoumann/notetweet_obsidian) 140 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', {targets: {node: 'current'}}], 5 | '@babel/preset-typescript', 6 | ], 7 | }; -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clearMocks": true, 3 | "testEnvironment": "node", 4 | "maxConcurrency": 1, 5 | "maxWorkers": 1 6 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "metaedit", 3 | "name": "MetaEdit", 4 | "version": "1.8.2", 5 | "minAppVersion": "1.4.1", 6 | "description": "MetaEdit helps you manage your metadata.", 7 | "author": "Christian B. B. Houmann", 8 | "authorUrl": "https://bagerbach.com", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metaedit", 3 | "version": "1.8.2", 4 | "description": "MetaEdit helps you manage your metadata.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production", 9 | "test": "jest", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json", 11 | "semantic-release": "semantic-release" 12 | }, 13 | "keywords": [], 14 | "author": "Christian B. B. Houmann", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/core": "7.14.3", 18 | "@babel/preset-env": "7.14.2", 19 | "@babel/preset-typescript": "7.13.0", 20 | "@rollup/plugin-commonjs": "^18.0.0", 21 | "@rollup/plugin-node-resolve": "^11.2.1", 22 | "@rollup/plugin-typescript": "^8.2.1", 23 | "@semantic-release/git": "^10.0.1", 24 | "@tsconfig/svelte": "1.0.10", 25 | "@types/jest": "26.0.23", 26 | "@types/node": "14.17.1", 27 | "babel-core": "6.26.3", 28 | "babel-jest": "27.0.1", 29 | "cz-conventional-changelog": "^3.3.0", 30 | "jest": "27.0.1", 31 | "jest-environment-node": "27.0.1", 32 | "obsidian": "^0.13.20", 33 | "rollup": "^2.32.1", 34 | "rollup-plugin-strip-code": "0.2.7", 35 | "rollup-plugin-svelte": "^7.1.0", 36 | "semantic-release": "^20.1.1", 37 | "svelte": "^3.37.0", 38 | "svelte-check": "^1.3.0", 39 | "svelte-preprocess": "^4.7.0", 40 | "ts-jest": "27.0.0", 41 | "tslib": "^2.2.0", 42 | "typescript": "^4.2.4" 43 | }, 44 | "dependencies": { 45 | "@popperjs/core": "^2.9.2" 46 | }, 47 | "config": { 48 | "commitizen": { 49 | "path": "./node_modules/cz-conventional-changelog" 50 | } 51 | }, 52 | "release": { 53 | "tagFormat": "${version}", 54 | "plugins": [ 55 | [ 56 | "@semantic-release/commit-analyzer", 57 | { 58 | "releaseRules": [ 59 | { 60 | "type": "chore", 61 | "release": "patch" 62 | } 63 | ] 64 | } 65 | ], 66 | "@semantic-release/release-notes-generator", 67 | [ 68 | "@semantic-release/npm", 69 | { 70 | "npmPublish": false 71 | } 72 | ], 73 | [ 74 | "@semantic-release/git", 75 | { 76 | "assets": [ 77 | "package.json", 78 | "package-lock.json", 79 | "manifest.json", 80 | "versions.json" 81 | ], 82 | "message": "release(version): Release ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 83 | } 84 | ], 85 | [ 86 | "@semantic-release/github", 87 | { 88 | "assets": [ 89 | { 90 | "path": "main.js", 91 | "label": "main.js" 92 | }, 93 | { 94 | "path": "manifest.json", 95 | "label": "manifest.json" 96 | }, 97 | { 98 | "path": "styles.css", 99 | "label": "styles.css" 100 | } 101 | ] 102 | } 103 | ] 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import svelte from "rollup-plugin-svelte"; 5 | import autoPreprocess from "svelte-preprocess"; 6 | import stripCode from "rollup-plugin-strip-code"; 7 | 8 | export default { 9 | input: 'src/main.ts', 10 | output: { 11 | dir: '.', 12 | format: 'cjs', 13 | exports: 'default', 14 | }, 15 | external: ['obsidian'], 16 | plugins: [ 17 | typescript(), 18 | nodeResolve({ browser: true, dedupe: ["svelte"] }), 19 | commonjs({ include: "node_modules/**" }), 20 | svelte({ 21 | emitCss: false, 22 | preprocess: autoPreprocess(), 23 | }), 24 | process.env["BUILD"] ? stripCode({ 25 | start_comment: 'START.DEVCMD', 26 | end_comment: 'END.DEVCMD' 27 | }) : null 28 | ] 29 | }; -------------------------------------------------------------------------------- /src/IMetaEditApi.ts: -------------------------------------------------------------------------------- 1 | import type {TFile} from "obsidian"; 2 | import type {Property} from "./parser"; 3 | 4 | export interface IMetaEditApi { 5 | autoprop: (propertyName: string) => void; 6 | update: (propertyName: string, propertyValue: string, file: TFile | string) => Promise; 7 | getPropertyValue: (propertyName: string, file: (TFile | string)) => Promise; 8 | getFilesWithProperty: (propertyName: string) => TFile[]; 9 | createYamlProperty: (propertyName: string, propertyValue: string, file: TFile | string) => Promise; 10 | getPropertiesInFile: (file: TFile | string) => Promise; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/MetaEditApi.ts: -------------------------------------------------------------------------------- 1 | import type MetaEdit from "./main"; 2 | import MetaController from "./metaController"; 3 | import type {IMetaEditApi} from "./IMetaEditApi"; 4 | import type {Property} from "./parser"; 5 | import {TFile} from "obsidian"; 6 | 7 | export class MetaEditApi { 8 | constructor(private plugin: MetaEdit) { 9 | } 10 | 11 | public make(): IMetaEditApi { 12 | return { 13 | autoprop: this.getAutopropFunction(), 14 | update: this.getUpdateFunction(), 15 | getPropertyValue: this.getGetPropertyValueFunction(), 16 | getFilesWithProperty: this.getGetFilesWithPropertyFunction(), 17 | createYamlProperty: this.getCreateYamlPropertyFunction(), 18 | getPropertiesInFile: this.getGetPropertiesInFile(), 19 | }; 20 | } 21 | 22 | private getAutopropFunction() { 23 | return (propertyName: string) => new MetaController(this.plugin.app, this.plugin).handleAutoProperties(propertyName); 24 | } 25 | 26 | private getUpdateFunction(): (propertyName: string, propertyValue: string, file: (TFile | string)) => Promise { 27 | return async (propertyName: string, propertyValue: string, file: TFile | string) => { 28 | const targetFile = this.getFileFromTFileOrPath(file); 29 | if (!targetFile) return; 30 | 31 | const controller: MetaController = new MetaController(this.plugin.app, this.plugin); 32 | const propsInFile: Property[] = await controller.getPropertiesInFile(targetFile); 33 | 34 | const targetProperty = propsInFile.find(prop => prop.key === propertyName); 35 | if (!targetProperty) return; 36 | 37 | return controller.updatePropertyInFile(targetProperty, propertyValue, targetFile); 38 | } 39 | } 40 | 41 | private getFileFromTFileOrPath(file: TFile | string) { 42 | let targetFile: TFile; 43 | 44 | if (file instanceof TFile) 45 | targetFile = file; 46 | 47 | if (typeof file === "string") { 48 | const abstractFile = this.plugin.app.vault.getAbstractFileByPath(file); 49 | if (abstractFile instanceof TFile) { 50 | targetFile = abstractFile; 51 | } 52 | } 53 | 54 | return targetFile; 55 | } 56 | 57 | private getGetPropertyValueFunction(): (propertyName: string, file: (TFile | string)) => Promise { 58 | return async (propertyName: string, file: TFile | string) => { 59 | const targetFile = this.getFileFromTFileOrPath(file); 60 | if (!targetFile) return; 61 | 62 | const controller: MetaController = new MetaController(this.plugin.app, this.plugin); 63 | const propsInFile: Property[] = await controller.getPropertiesInFile(targetFile); 64 | 65 | const targetProperty = propsInFile.find(prop => prop.key === propertyName); 66 | if (!targetProperty) return; 67 | 68 | return targetProperty.content; 69 | } 70 | } 71 | 72 | private getGetFilesWithPropertyFunction() { 73 | return (propertyName: string): TFile[] => { 74 | return this.plugin.getFilesWithProperty(propertyName); 75 | } 76 | } 77 | 78 | private getCreateYamlPropertyFunction() { 79 | return async (propertyName: string, propertyValue: string, file: TFile | string) => { 80 | const targetFile = this.getFileFromTFileOrPath(file); 81 | if (!targetFile) return; 82 | 83 | const controller: MetaController = new MetaController(this.plugin.app, this.plugin); 84 | await controller.addYamlProp(propertyName, propertyValue, targetFile); 85 | } 86 | } 87 | 88 | private getGetPropertiesInFile() { 89 | return async (file: TFile | string): Promise => { 90 | const targetFile = this.getFileFromTFileOrPath(file); 91 | if (!targetFile) return; 92 | 93 | const controller: MetaController = new MetaController(this.plugin.app, this.plugin); 94 | return await controller.getPropertiesInFile(targetFile); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/Modals/AutoPropertiesSettingModal/AutoPropertiesModal.ts: -------------------------------------------------------------------------------- 1 | import {App, Modal} from "obsidian"; 2 | import type MetaEdit from "../../main"; 3 | import AutoPropertiesModalContent from "./AutoPropertiesModalContent.svelte"; 4 | import type {AutoProperty} from "../../Types/autoProperty"; 5 | 6 | export default class AutoPropertiesModal extends Modal { 7 | public waitForResolve: Promise; 8 | private plugin: MetaEdit; 9 | private content: AutoPropertiesModalContent; 10 | private resolvePromise: (autoProperties: AutoProperty[]) => void; 11 | private autoProperties: AutoProperty[]; 12 | 13 | constructor(app: App, plugin: MetaEdit, autoProperties: AutoProperty[]) { 14 | super(app); 15 | this.plugin = plugin; 16 | this.autoProperties = autoProperties; 17 | 18 | this.waitForResolve = new Promise( 19 | (resolve) => (this.resolvePromise = resolve) 20 | ); 21 | 22 | this.content = new AutoPropertiesModalContent({ 23 | target: this.contentEl, 24 | props: { 25 | save: (autoProperties: AutoProperty[]) => this.save(autoProperties), 26 | autoProperties 27 | }, 28 | }); 29 | 30 | this.open(); 31 | } 32 | 33 | save(autoProperties: AutoProperty[]) { 34 | this.autoProperties = autoProperties; 35 | this.close(); 36 | } 37 | 38 | onClose() { 39 | super.onClose(); 40 | this.content.$destroy(); 41 | this.resolvePromise(this.autoProperties); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Modals/AutoPropertiesSettingModal/AutoPropertiesModalContent.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {#each autoProperties as property} 50 | 51 | 54 | 57 | 65 | 70 | 71 |
72 | {/each} 73 |
NameValues
52 | removeProperty(property)}/> 53 | 55 | save(autoProperties)} type="text" placeholder="Property name" bind:value={property.name}> 56 | 58 | {#each property.choices as choice, i} 59 |
60 | save(autoProperties)} type="text" bind:value={choice} /> 61 | removeChoice(property, i)}> 62 |
63 | {/each} 64 |
66 |
67 | addChoice(property)}> 68 |
69 |
74 | 75 |
76 | 77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /src/Modals/GenericPrompt/GenericPrompt.ts: -------------------------------------------------------------------------------- 1 | import {App, Modal} from "obsidian"; 2 | import GenericPromptContent from "./GenericPromptContent.svelte" 3 | 4 | export default class GenericPrompt extends Modal { 5 | private modalContent: GenericPromptContent; 6 | private resolvePromise: (input: string) => void; 7 | private input: string; 8 | public waitForClose: Promise; 9 | private rejectPromise: (reason?: any) => void; 10 | private didSubmit: boolean = false; 11 | 12 | public static Prompt(app: App, header: string, placeholder?: string, value?: string, suggestValues?: string[]): Promise { 13 | const newPromptModal = new GenericPrompt(app, header, placeholder, value, suggestValues); 14 | return newPromptModal.waitForClose; 15 | } 16 | 17 | private constructor(app: App, header: string, placeholder?: string, value?: string, suggestValues?: string[]) { 18 | super(app); 19 | 20 | this.modalContent = new GenericPromptContent({ 21 | target: this.contentEl, 22 | props: { 23 | app, 24 | header, 25 | placeholder, 26 | value, 27 | suggestValues, 28 | onSubmit: (input: string) => { 29 | this.input = input; 30 | this.didSubmit = true; 31 | this.close(); 32 | } 33 | } 34 | }); 35 | 36 | this.waitForClose = new Promise( 37 | (resolve, reject) => { 38 | this.resolvePromise = resolve; 39 | this.rejectPromise = reject; 40 | } 41 | ); 42 | 43 | this.open(); 44 | } 45 | 46 | onOpen() { 47 | super.onOpen(); 48 | 49 | const modalPrompt: HTMLElement = document.querySelector('.metaEditPrompt'); 50 | const modalInput: any = modalPrompt.querySelector('.metaEditPromptInput'); 51 | modalInput.focus(); 52 | modalInput.select(); 53 | } 54 | 55 | onClose() { 56 | super.onClose(); 57 | this.modalContent.$destroy(); 58 | 59 | if(!this.didSubmit) this.rejectPromise("No input given."); 60 | else this.resolvePromise(this.input); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Modals/GenericPrompt/GenericPromptContent.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |

{header}

33 | 40 |
-------------------------------------------------------------------------------- /src/Modals/GenericPrompt/genericTextSuggester.ts: -------------------------------------------------------------------------------- 1 | import {TextInputSuggest} from "../../suggest"; 2 | import type {App} from "obsidian"; 3 | 4 | export class GenericTextSuggester extends TextInputSuggest { 5 | 6 | constructor(public app: App, public inputEl: HTMLInputElement, private items: string[]) { 7 | super(app, inputEl); 8 | } 9 | 10 | getSuggestions(inputStr: string): string[] { 11 | const inputLowerCase: string = inputStr.toLowerCase(); 12 | const filtered = this.items.filter(item => { 13 | if (item.toLowerCase().contains(inputLowerCase)) 14 | return item; 15 | }); 16 | 17 | if (!filtered) this.close(); 18 | if (filtered?.length === 1) return [...filtered, inputStr]; 19 | if (filtered?.length > 1) return filtered; 20 | } 21 | 22 | selectSuggestion(item: string): void { 23 | this.inputEl.value = item; 24 | this.inputEl.trigger("input"); 25 | this.close(); 26 | } 27 | 28 | renderSuggestion(value: string, el: HTMLElement): void { 29 | if (value) 30 | el.setText(value); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Modals/GenericSuggester/GenericSuggester.ts: -------------------------------------------------------------------------------- 1 | import {App, FuzzySuggestModal} from "obsidian"; 2 | 3 | export default class GenericSuggester extends FuzzySuggestModal{ 4 | private resolvePromise: (value: string) => void; 5 | private promise: Promise; 6 | 7 | public static Suggest(app: App, displayItems: string[], items: string[]) { 8 | const newSuggester = new GenericSuggester(app, displayItems, items); 9 | return newSuggester.promise; 10 | } 11 | 12 | private constructor(app: App, private displayItems: string[], private items: string[]) { 13 | super(app); 14 | 15 | this.promise = new Promise( 16 | (resolve) => (this.resolvePromise = resolve) 17 | ); 18 | 19 | this.open(); 20 | } 21 | 22 | getItemText(item: string): string { 23 | return this.displayItems[this.items.indexOf(item)]; 24 | } 25 | 26 | getItems(): string[] { 27 | return this.items; 28 | } 29 | 30 | onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void { 31 | this.resolvePromise(item); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/Modals/IgnoredPropertiesSettingModal/IgnoredPropertiesModal.ts: -------------------------------------------------------------------------------- 1 | import {App, Modal} from "obsidian"; 2 | import type MetaEdit from "../../main"; 3 | import IgnoredPropertiesModalContent from "./IgnoredPropertiesModalContent.svelte"; 4 | 5 | export default class IgnoredPropertiesModal extends Modal{ 6 | public waitForResolve: Promise; 7 | private plugin: MetaEdit; 8 | private content: IgnoredPropertiesModalContent; 9 | private resolvePromise: (ignoredProperties: string[]) => void; 10 | private ignoredProperties: string[]; 11 | 12 | constructor(app: App, plugin: MetaEdit, ignoredProperties: string[]) { 13 | super(app); 14 | this.plugin = plugin; 15 | this.ignoredProperties = ignoredProperties; 16 | 17 | this.waitForResolve = new Promise( 18 | (resolve) => (this.resolvePromise = resolve) 19 | ); 20 | 21 | this.content = new IgnoredPropertiesModalContent({ 22 | target: this.contentEl, 23 | props: { 24 | ignoredProperties, 25 | save: (ignoredProperties: string[]) => { 26 | this.ignoredProperties = ignoredProperties; 27 | this.close(); 28 | } 29 | }, 30 | }); 31 | 32 | this.open(); 33 | } 34 | 35 | onClose() { 36 | super.onClose(); 37 | this.content.$destroy(); 38 | this.resolvePromise(this.ignoredProperties); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Modals/IgnoredPropertiesSettingModal/IgnoredPropertiesModalContent.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {#each ignoredProperties as property, i} 27 | 28 | 31 | 34 | 35 | {/each} 36 |
Property
29 | removeProperty(i)}/> 30 | 32 | save(ignoredProperties)} style="width: 100%;" type="text" placeholder="Property name" bind:value={property}> 33 |
37 | 38 |
39 | 40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /src/Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {#each kanbanProperties as kanbanProperty, i} 65 | 66 | 69 | 72 | 75 | 78 | 79 |
80 | {/each} 81 |
BoardProperty in linkPossible values
67 | removeProperty(i)}/> 68 | 70 | {kanbanProperty.boardName} 71 | 73 | save(kanbanProperties)} type="text" placeholder="Property name" bind:value={kanbanProperty.property}> 74 | 76 | {getHeadingsInBoard(kanbanProperty.boardName)} 77 |
82 | 83 | 84 |
85 | 86 |
87 |
88 | 89 | -------------------------------------------------------------------------------- /src/Modals/KanbanHelperSetting/KanbanHelperSettingSuggester.ts: -------------------------------------------------------------------------------- 1 | import {TextInputSuggest} from "../../suggest"; 2 | import type {App} from "obsidian"; 3 | import type {TFile} from "obsidian"; 4 | 5 | export class KanbanHelperSettingSuggester extends TextInputSuggest { 6 | public app: App; 7 | public inputEl: HTMLInputElement; 8 | private boards: TFile[]; 9 | 10 | constructor(app: App, inputEl: HTMLInputElement, boards: TFile[]) { 11 | super(app, inputEl); 12 | this.app = app; 13 | this.inputEl = inputEl; 14 | this.boards = boards; 15 | } 16 | 17 | getSuggestions(inputStr: string): TFile[] { 18 | const inputLowerCase: string = inputStr.toLowerCase(); 19 | return this.boards.map(board => { 20 | if (board.basename.toLowerCase().contains(inputLowerCase)) 21 | return board; 22 | }); 23 | } 24 | 25 | selectSuggestion(item: TFile): void { 26 | this.inputEl.value = item.basename; 27 | this.inputEl.trigger("input"); 28 | this.close(); 29 | } 30 | 31 | renderSuggestion(value: TFile, el: HTMLElement): void { 32 | if (value) 33 | el.setText(value.basename); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Modals/LinkMenu.ts: -------------------------------------------------------------------------------- 1 | import type MetaEdit from "../main"; 2 | import {EventRef, Menu, TAbstractFile, TFile, TFolder} from "obsidian"; 3 | 4 | export class LinkMenu { 5 | private targetFile: TFile; 6 | private targetFolder: TFolder; 7 | private eventRef: EventRef; 8 | 9 | constructor(private plugin: MetaEdit) {} 10 | 11 | public registerEvent(): void { 12 | this.eventRef = this.plugin.app.workspace.on('file-menu', 13 | (menu, file, source) => this.onMenuOpenCallback(menu, file, source)); 14 | this.plugin.registerEvent(this.eventRef); 15 | } 16 | 17 | public unregisterEvent(): void { 18 | if (this.eventRef){ 19 | this.plugin.app.workspace.offref(this.eventRef); 20 | } 21 | } 22 | 23 | private onMenuOpenCallback(menu: Menu, file: TAbstractFile, source: string) { 24 | const bCorrectSource: boolean = (source === "link-context-menu" || 25 | source === "calendar-context-menu" || 26 | source =="file-explorer-context-menu"); 27 | if (bCorrectSource) 28 | { 29 | if (file instanceof TFile && file.extension === "md") { 30 | this.targetFile = file; 31 | this.addFileOptions(menu); 32 | } 33 | if (file instanceof TFolder && file.children && file.children.some(f => f instanceof TFile && f.extension === "md")) { 34 | this.targetFolder = file; 35 | this.addFolderOptions(menu); 36 | } 37 | } 38 | } 39 | 40 | private addFileOptions(menu: Menu) { 41 | menu.addItem(item => { 42 | item.setIcon('pencil'); 43 | item.setTitle("Edit Meta"); 44 | item.onClick(async evt => { 45 | await this.plugin.runMetaEditForFile(this.targetFile); 46 | }) 47 | }) 48 | } 49 | 50 | private addFolderOptions(menu: Menu) { 51 | menu.addItem(item => { 52 | item.setIcon('pencil'); 53 | item.setTitle("Add YAML property to all files in this folder (and subfolders)"); 54 | item.onClick(async evt => { 55 | await this.plugin.runMetaEditForFolder(this.targetFolder); 56 | }) 57 | }) 58 | } 59 | } -------------------------------------------------------------------------------- /src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModal.ts: -------------------------------------------------------------------------------- 1 | import {App, Modal} from "obsidian"; 2 | import type MetaEdit from "../../main"; 3 | import ProgressPropertiesModalContent from "./ProgressPropertiesModalContent.svelte"; 4 | import type {ProgressProperty} from "../../Types/progressProperty"; 5 | 6 | export default class ProgressPropertiesModal extends Modal { 7 | public waitForResolve: Promise; 8 | private plugin: MetaEdit; 9 | private content: ProgressPropertiesModalContent; 10 | private resolvePromise: (properties: ProgressProperty[]) => void; 11 | private properties: ProgressProperty[]; 12 | 13 | constructor(app: App, plugin: MetaEdit, properties: ProgressProperty[]) { 14 | super(app); 15 | this.plugin = plugin; 16 | if (properties.length > 0) 17 | this.properties = properties; 18 | else 19 | this.properties = []; 20 | 21 | this.waitForResolve = new Promise( 22 | (resolve) => (this.resolvePromise = resolve) 23 | ); 24 | 25 | this.content = new ProgressPropertiesModalContent({ 26 | target: this.contentEl, 27 | props: { 28 | properties: this.properties, 29 | save: (properties: ProgressProperty[]) => { 30 | this.properties = properties; 31 | this.close(); 32 | } 33 | }, 34 | }); 35 | 36 | this.open(); 37 | } 38 | 39 | onClose() { 40 | super.onClose(); 41 | this.content.$destroy(); 42 | this.resolvePromise(this.properties); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {#each properties as property} 32 | 33 | 36 | 43 | 46 | 47 | {/each} 48 |
NameType
34 | save(properties)}> 35 | 37 | 42 | 44 | removeProperty(property)} value="❌"/> 45 |
49 | 50 |
51 | 52 |
53 |
54 | 55 | -------------------------------------------------------------------------------- /src/Modals/metaEditSuggester.ts: -------------------------------------------------------------------------------- 1 | import {App, FuzzyMatch, FuzzySuggestModal, TFile} from "obsidian"; 2 | import type MetaEdit from "../main"; 3 | import type MetaController from "../metaController"; 4 | import type {Property} from "../parser"; 5 | import {MAIN_SUGGESTER_OPTIONS, newDataView, newYaml} from "../constants"; 6 | import {MetaType} from "../Types/metaType"; 7 | import {concat} from "svelte-preprocess/dist/modules/utils"; 8 | import type {AutoProperty} from "../Types/autoProperty"; 9 | 10 | export default class MetaEditSuggester extends FuzzySuggestModal { 11 | public app: App; 12 | private readonly file: TFile; 13 | private plugin: MetaEdit; 14 | private readonly data: Property[]; 15 | private readonly options: Property[]; 16 | private controller: MetaController; 17 | private suggestValues: string[]; 18 | 19 | constructor(app: App, plugin: MetaEdit, data: Property[], file: TFile, controller: MetaController) { 20 | super(app); 21 | this.file = file; 22 | this.app = app; 23 | this.plugin = plugin; 24 | this.data = this.removeIgnored(data); 25 | this.controller = controller; 26 | this.options = MAIN_SUGGESTER_OPTIONS; 27 | 28 | this.setSuggestValues(); 29 | 30 | this.setInstructions([ 31 | {command: "❌", purpose: "Delete property"}, 32 | {command: "🔃", purpose: "Transform to YAML/Dataview"} 33 | ]) 34 | } 35 | 36 | renderSuggestion(item: FuzzyMatch, el: HTMLElement) { 37 | super.renderSuggestion(item, el); 38 | 39 | if (Object.values(this.options).find(v => v === item.item)) { 40 | el.style.fontWeight = "bold"; 41 | } else { 42 | this.createButton(el,"❌", this.deleteItem(item)); 43 | this.createButton(el, "🔃", this.transformProperty(item)) 44 | } 45 | } 46 | 47 | getItemText(item: Property): string { 48 | return item.key; 49 | } 50 | 51 | getItems(): Property[] { 52 | return concat(this.options, this.data); 53 | } 54 | 55 | async onChooseItem(item: Property, evt: MouseEvent | KeyboardEvent): Promise { 56 | if (item.content === newYaml) { 57 | const newProperty = await this.controller.createNewProperty(this.suggestValues); 58 | if (!newProperty) return null; 59 | 60 | const {propName, propValue} = newProperty; 61 | await this.controller.addYamlProp(propName, propValue, this.file); 62 | return; 63 | } 64 | 65 | if (item.content === newDataView) { 66 | const newProperty = await this.controller.createNewProperty(this.suggestValues); 67 | if (!newProperty) return null; 68 | 69 | const {propName, propValue} = newProperty; 70 | await this.controller.addDataviewField(propName, propValue, this.file); 71 | return; 72 | } 73 | 74 | await this.controller.editMetaElement(item, this.data, this.file); 75 | } 76 | 77 | private deleteItem(item: FuzzyMatch) { 78 | return async (evt: MouseEvent) => { 79 | evt.stopPropagation(); 80 | await this.controller.deleteProperty(item.item, this.file); 81 | this.close(); 82 | }; 83 | } 84 | 85 | private transformProperty(item: FuzzyMatch) { 86 | return async (evt: MouseEvent | KeyboardEvent) => { 87 | evt.stopPropagation(); 88 | const {item: property} = item; 89 | if (property.type === MetaType.YAML) { 90 | await this.toDataview(property); 91 | } else { 92 | await this.toYaml(property); 93 | } 94 | 95 | this.close(); 96 | } 97 | } 98 | 99 | private async toYaml(property: Property) { 100 | await this.controller.deleteProperty(property, this.file); 101 | await this.controller.addYamlProp(property.key, property.content, this.file); 102 | } 103 | 104 | private async toDataview(property: Property) { 105 | await this.controller.deleteProperty(property, this.file); 106 | await this.controller.addDataviewField(property.key, property.content, this.file); 107 | } 108 | 109 | private createButton(el: HTMLElement, content: string, callback: (evt: MouseEvent) => void) { 110 | const itemButton = el.createEl("button"); 111 | itemButton.textContent = content; 112 | itemButton.classList.add("not-a-button"); 113 | itemButton.style.float = "right"; 114 | itemButton.style.marginRight = "4px"; 115 | itemButton.addEventListener("click", callback); 116 | } 117 | 118 | private removeIgnored(data: Property[]): Property[] { 119 | const ignored = this.plugin.settings.IgnoredProperties.properties; 120 | let purged: Property[] = []; 121 | 122 | for (let item in data) { 123 | if (!ignored.contains(data[item].key)) 124 | purged.push(data[item]); 125 | } 126 | 127 | return purged; 128 | } 129 | 130 | private setSuggestValues() { 131 | const autoProps = this.plugin.settings.AutoProperties.properties; 132 | 133 | this.suggestValues = autoProps.reduce((arr: string[], val: AutoProperty) => { 134 | if (!this.data.find(prop => val.name === prop.key || val.name.startsWith('#'))) { 135 | arr.push(val.name); 136 | } 137 | 138 | return arr; 139 | }, []); 140 | } 141 | } -------------------------------------------------------------------------------- /src/Modals/shared/SingleValueTableEditorContent.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {#each properties as property, i} 27 | 28 | 31 | 34 | 35 | {/each} 36 |
Property
29 | removeProperty(i)}/> 30 | 32 | save(properties)} style="width: 100%;" type="text" placeholder="Property name" bind:value={property}> 33 |
37 | 38 |
39 | 40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /src/Settings/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import type {MetaEditSettings} from "./metaEditSettings"; 2 | import {EditMode} from "../Types/editMode"; 3 | 4 | export const DEFAULT_SETTINGS: MetaEditSettings = Object.freeze({ 5 | ProgressProperties: { 6 | enabled: false, 7 | properties: [] 8 | }, 9 | IgnoredProperties: { 10 | enabled: false, 11 | properties: [] 12 | }, 13 | AutoProperties: { 14 | enabled: false, 15 | properties: [] 16 | }, 17 | EditMode: { 18 | mode: EditMode.AllSingle, 19 | properties: [], 20 | }, 21 | KanbanHelper: { 22 | enabled: false, 23 | boards: [] 24 | }, 25 | UIElements: { 26 | enabled: true 27 | } 28 | }); -------------------------------------------------------------------------------- /src/Settings/metaEditSettings.ts: -------------------------------------------------------------------------------- 1 | import type {EditMode} from "../Types/editMode"; 2 | import type {ProgressProperty} from "../Types/progressProperty"; 3 | import type {AutoProperty} from "../Types/autoProperty"; 4 | import type {KanbanProperty} from "../Types/kanbanProperty"; 5 | 6 | export interface MetaEditSettings { 7 | ProgressProperties: { 8 | enabled: boolean, 9 | properties: ProgressProperty[] 10 | }, 11 | IgnoredProperties: { 12 | enabled: boolean, 13 | properties: string[] 14 | }, 15 | AutoProperties: { 16 | enabled: boolean, 17 | properties: AutoProperty[] 18 | }, 19 | EditMode: { 20 | mode: EditMode, 21 | properties: string[], 22 | }, 23 | KanbanHelper: { 24 | enabled: boolean, 25 | boards: KanbanProperty[] 26 | } 27 | UIElements: { 28 | enabled: boolean 29 | } 30 | } -------------------------------------------------------------------------------- /src/Settings/metaEditSettingsTab.ts: -------------------------------------------------------------------------------- 1 | import {App, PluginSettingTab, Setting} from "obsidian"; 2 | import type MetaEdit from "../main"; 3 | import {EditMode} from "../Types/editMode"; 4 | import ProgressPropertiesModalContent 5 | from "../Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte"; 6 | import AutoPropertiesModalContent from "../Modals/AutoPropertiesSettingModal/AutoPropertiesModalContent.svelte"; 7 | import KanbanHelperSettingContent from "../Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte"; 8 | import SingleValueTableEditorContent from "../Modals/shared/SingleValueTableEditorContent.svelte"; 9 | import type {ProgressProperty} from "../Types/progressProperty"; 10 | import type {AutoProperty} from "../Types/autoProperty"; 11 | import type {KanbanProperty} from "../Types/kanbanProperty"; 12 | 13 | function toggleHiddenEl(el: HTMLElement, bShow: boolean) { 14 | if (el && !bShow) { 15 | el.style.display = "none"; 16 | return true; 17 | } else if (el && bShow) { 18 | el.style.display = "block"; 19 | return false; 20 | } 21 | return bShow; 22 | } 23 | 24 | export class MetaEditSettingsTab extends PluginSettingTab { 25 | plugin: MetaEdit; 26 | private svelteElements: (SingleValueTableEditorContent | AutoPropertiesModalContent | ProgressPropertiesModalContent)[] = []; 27 | 28 | constructor(app: App, plugin: MetaEdit) { 29 | super(app, plugin); 30 | this.plugin = plugin; 31 | } 32 | 33 | display(): void { 34 | let {containerEl} = this; 35 | 36 | containerEl.empty(); 37 | 38 | containerEl.createEl('h2', {text: 'MetaEdit Settings'}); 39 | 40 | this.addProgressPropertiesSetting(containerEl); 41 | this.addAutoPropertiesSetting(containerEl); 42 | this.addIgnorePropertiesSetting(containerEl); 43 | this.addEditModeSetting(containerEl); 44 | this.addKanbanHelperSetting(containerEl); 45 | this.addUIElementsSetting(containerEl); 46 | } 47 | 48 | private addProgressPropertiesSetting(containerEl: HTMLElement) { 49 | let modal: ProgressPropertiesModalContent, div: HTMLDivElement, hidden: boolean = true; 50 | const setting = new Setting(containerEl) 51 | .setName("Progress Properties") 52 | .setDesc("Update properties automatically.") 53 | .addToggle(toggle => { 54 | toggle 55 | .setTooltip("Toggle Progress Properties") 56 | .setValue(this.plugin.settings.ProgressProperties.enabled) 57 | .onChange(async value => { 58 | if (value === this.plugin.settings.ProgressProperties.enabled) return; 59 | 60 | this.plugin.settings.ProgressProperties.enabled = value; 61 | this.plugin.toggleAutomators(); 62 | 63 | await this.plugin.saveSettings(); 64 | }); 65 | }) 66 | .addExtraButton(button => button.onClick(() => hidden = toggleHiddenEl(div, hidden))) 67 | 68 | div = setting.settingEl.createDiv(); 69 | setting.settingEl.style.display = "block"; 70 | div.style.display = "none"; 71 | 72 | modal = new ProgressPropertiesModalContent({ 73 | target: div, 74 | props: { 75 | properties: this.plugin.settings.ProgressProperties.properties, 76 | save: async (progressProperties: ProgressProperty[]) => { 77 | this.plugin.settings.ProgressProperties.properties = progressProperties; 78 | await this.plugin.saveSettings(); 79 | } 80 | }, 81 | }); 82 | 83 | this.svelteElements.push(modal); 84 | } 85 | 86 | private addAutoPropertiesSetting(containerEl: HTMLElement) { 87 | let modal: AutoPropertiesModalContent, div: HTMLDivElement, hidden: boolean = true; 88 | const setting = new Setting(containerEl) 89 | .setName("Auto Properties") 90 | .setDesc("Quick switch for values you know the value of.") 91 | .addToggle(toggle => { 92 | toggle 93 | .setTooltip("Toggle Auto Properties") 94 | .setValue(this.plugin.settings.AutoProperties.enabled) 95 | .onChange(async value => { 96 | if (value === this.plugin.settings.AutoProperties.enabled) return; 97 | 98 | this.plugin.settings.AutoProperties.enabled = value; 99 | 100 | await this.plugin.saveSettings(); 101 | }); 102 | }) 103 | .addExtraButton(b => b.onClick(() => hidden = toggleHiddenEl(div, hidden))); 104 | 105 | div = setting.settingEl.createDiv(); 106 | setting.settingEl.style.display = "block"; 107 | div.style.display = "none"; 108 | 109 | modal = new AutoPropertiesModalContent({ 110 | target: div, 111 | props: { 112 | autoProperties: this.plugin.settings.AutoProperties.properties, 113 | save: async (autoProperties: AutoProperty[]) => { 114 | this.plugin.settings.AutoProperties.properties = autoProperties; 115 | await this.plugin.saveSettings(); 116 | } 117 | }, 118 | }); 119 | 120 | this.svelteElements.push(modal); 121 | } 122 | 123 | private addIgnorePropertiesSetting(containerEl: HTMLElement) { 124 | let modal: SingleValueTableEditorContent, div: HTMLDivElement, hidden = true; 125 | const setting = new Setting(containerEl) 126 | .setName("Ignore Properties") 127 | .setDesc("Hide these properties from the menu.") 128 | .addToggle(toggle => { 129 | toggle 130 | .setTooltip("Toggle Ignored Properties") 131 | .setValue(this.plugin.settings.IgnoredProperties.enabled) 132 | .onChange(async value => { 133 | if (value === this.plugin.settings.IgnoredProperties.enabled) return; 134 | 135 | this.plugin.settings.IgnoredProperties.enabled = value; 136 | 137 | await this.plugin.saveSettings(); 138 | this.display(); 139 | }); 140 | }).addExtraButton(b => b.onClick(() => hidden = toggleHiddenEl(div, hidden))) 141 | 142 | if (this.plugin.settings.IgnoredProperties.enabled) { 143 | div = setting.settingEl.createDiv(); 144 | setting.settingEl.style.display = "block"; 145 | div.style.display = "none"; 146 | 147 | modal = new SingleValueTableEditorContent({ 148 | target: div, 149 | props: { 150 | properties: this.plugin.settings.IgnoredProperties.properties, 151 | save: async (ignoredProperties: string[]) => { 152 | this.plugin.settings.IgnoredProperties.properties = ignoredProperties; 153 | await this.plugin.saveSettings(); 154 | } 155 | }, 156 | }); 157 | 158 | this.svelteElements.push(modal); 159 | } 160 | } 161 | 162 | private addEditModeSetting(containerEl: HTMLElement) { 163 | let modal: any, div: HTMLDivElement, bDivToggle: boolean = true, extraButtonEl, bExtraButtonToggle: boolean = true; 164 | 165 | // For linebreaks 166 | const df = new DocumentFragment(); 167 | df.createEl('p', {text: "Single: property values are just one value. "}); 168 | df.createEl('p', {text: "Multi: properties are arrays. "}) 169 | df.createEl('p', {text: "Some Multi: all options are single, except those specified in the settings (click button)."}); 170 | 171 | const setting = new Setting(containerEl) 172 | .setName("Edit Mode") 173 | .setDesc(df) 174 | .addDropdown(dropdown => { 175 | dropdown 176 | .addOption(EditMode.AllSingle, EditMode.AllSingle) 177 | .addOption(EditMode.AllMulti, EditMode.AllMulti) 178 | .addOption(EditMode.SomeMulti, EditMode.SomeMulti) 179 | .setValue(this.plugin.settings.EditMode.mode) 180 | .onChange(async value => { 181 | switch (value) { 182 | case EditMode.AllMulti: 183 | this.plugin.settings.EditMode.mode = EditMode.AllMulti; 184 | bExtraButtonToggle = toggleHiddenEl(extraButtonEl, false); 185 | bDivToggle = toggleHiddenEl(div, false); 186 | break; 187 | case EditMode.AllSingle: 188 | this.plugin.settings.EditMode.mode = EditMode.AllSingle; 189 | bExtraButtonToggle = toggleHiddenEl(extraButtonEl, false); 190 | bDivToggle = toggleHiddenEl(div, false); 191 | break; 192 | case EditMode.SomeMulti: 193 | this.plugin.settings.EditMode.mode = EditMode.SomeMulti; 194 | bExtraButtonToggle = toggleHiddenEl(extraButtonEl, true); 195 | break; 196 | } 197 | 198 | await this.plugin.saveSettings(); 199 | }) 200 | }) 201 | .addExtraButton(b => { 202 | extraButtonEl = b.extraSettingsEl; 203 | b.setTooltip("Configure which properties are Multi.") 204 | return b.onClick(() => bDivToggle = toggleHiddenEl(div, bDivToggle)); 205 | }); 206 | 207 | if (this.plugin.settings.EditMode.mode != EditMode.SomeMulti) { 208 | bExtraButtonToggle = toggleHiddenEl(extraButtonEl, false); 209 | } 210 | 211 | div = setting.settingEl.createDiv(); 212 | setting.settingEl.style.display = "block"; 213 | div.style.display = "none"; 214 | 215 | modal = new SingleValueTableEditorContent({ 216 | target: div, 217 | props: { 218 | properties: this.plugin.settings.EditMode.properties, 219 | save: async (properties: string[]) => { 220 | this.plugin.settings.EditMode.properties = properties; 221 | await this.plugin.saveSettings(); 222 | } 223 | }, 224 | }); 225 | 226 | this.svelteElements.push(modal); 227 | } 228 | 229 | hide(): any { 230 | this.svelteElements.forEach(el => el.$destroy()); 231 | return super.hide(); 232 | } 233 | 234 | private addKanbanHelperSetting(containerEl: HTMLElement) { 235 | let modal: ProgressPropertiesModalContent, div: HTMLDivElement, hidden: boolean = true; 236 | const setting = new Setting(containerEl) 237 | .setName("Kanban Board Helper") 238 | .setDesc("Update properties in links in kanban boards automatically when a card is moved to a new lane.") 239 | .addToggle(toggle => { 240 | toggle 241 | .setTooltip("Toggle Kanban Helper") 242 | .setValue(this.plugin.settings.KanbanHelper.enabled) 243 | .onChange(async value => { 244 | if (value === this.plugin.settings.KanbanHelper.enabled) return; 245 | 246 | this.plugin.settings.KanbanHelper.enabled = value; 247 | this.plugin.toggleAutomators(); 248 | 249 | await this.plugin.saveSettings(); 250 | }); 251 | }) 252 | .addExtraButton(button => button.onClick(() => hidden = toggleHiddenEl(div, hidden))) 253 | 254 | div = setting.settingEl.createDiv(); 255 | setting.settingEl.style.display = "block"; 256 | div.style.display = "none"; 257 | 258 | modal = new KanbanHelperSettingContent({ 259 | target: div, 260 | props: { 261 | kanbanProperties: this.plugin.settings.KanbanHelper.boards, 262 | boards: this.plugin.getFilesWithProperty("kanban-plugin"), 263 | app: this.app, 264 | save: async (kanbanProperties: KanbanProperty[]) => { 265 | this.plugin.settings.KanbanHelper.boards = kanbanProperties; 266 | await this.plugin.saveSettings(); 267 | } 268 | }, 269 | }); 270 | 271 | this.svelteElements.push(modal); 272 | } 273 | 274 | private addUIElementsSetting(containerEl: HTMLElement) { 275 | new Setting(containerEl) 276 | .setName("UI Elements") 277 | .setDesc("Toggle UI elements: the 'Edit Meta' right-click menu option.") 278 | .addToggle(toggle => { 279 | toggle 280 | .setTooltip("Toggle UI elements") 281 | .setValue(this.plugin.settings.UIElements.enabled) 282 | .onChange(async value => { 283 | if (value === this.plugin.settings.UIElements.enabled) return; 284 | 285 | this.plugin.settings.UIElements.enabled = value; 286 | value ? this.plugin.linkMenu.registerEvent() : this.plugin.linkMenu.unregisterEvent(); 287 | 288 | await this.plugin.saveSettings(); 289 | }); 290 | }) 291 | } 292 | } -------------------------------------------------------------------------------- /src/Types/autoProperty.ts: -------------------------------------------------------------------------------- 1 | export interface AutoProperty { 2 | name: string, 3 | choices: string[] 4 | } -------------------------------------------------------------------------------- /src/Types/datedFileCacheItem.ts: -------------------------------------------------------------------------------- 1 | export interface DatedFileCacheItem { 2 | content: string, 3 | updateTime: number 4 | } -------------------------------------------------------------------------------- /src/Types/editMode.ts: -------------------------------------------------------------------------------- 1 | export enum EditMode { 2 | AllSingle = "All Single", 3 | AllMulti = "All Multi", 4 | SomeMulti = "Some Multi", 5 | } -------------------------------------------------------------------------------- /src/Types/kanbanProperty.ts: -------------------------------------------------------------------------------- 1 | export interface KanbanProperty { 2 | boardName: string, 3 | property: string 4 | } -------------------------------------------------------------------------------- /src/Types/metaType.ts: -------------------------------------------------------------------------------- 1 | export enum MetaType { 2 | YAML, Dataview, Tag, Option 3 | } -------------------------------------------------------------------------------- /src/Types/progressProperty.ts: -------------------------------------------------------------------------------- 1 | import type {ProgressPropertyOptions} from "./progressPropertyOptions"; 2 | 3 | export interface ProgressProperty { 4 | name: string; 5 | type: ProgressPropertyOptions 6 | } -------------------------------------------------------------------------------- /src/Types/progressPropertyOptions.ts: -------------------------------------------------------------------------------- 1 | export enum ProgressPropertyOptions { 2 | TaskTotal = "Total Tasks", 3 | TaskComplete = "Completed Tasks", 4 | TaskIncomplete = "Incomplete Tasks" 5 | } -------------------------------------------------------------------------------- /src/automators/IAutomatorManager.ts: -------------------------------------------------------------------------------- 1 | import type {IOnFileModifyAutomator} from "./onFileModifyAutomators/IOnFileModifyAutomator"; 2 | import type {OnModifyAutomatorType} from "./onFileModifyAutomators/onModifyAutomatorType"; 3 | 4 | export interface IAutomatorManager { 5 | attach(automator: IOnFileModifyAutomator): IAutomatorManager; 6 | detach(automatorType: OnModifyAutomatorType): IAutomatorManager; 7 | startAutomators(): IAutomatorManager; 8 | } -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomatorManager.ts: -------------------------------------------------------------------------------- 1 | import {UniqueQueue} from "../uniqueQueue"; 2 | import type {App, TAbstractFile, TFile} from "obsidian"; 3 | import {debounce} from "obsidian"; 4 | import {UpdatedFileCache} from "../updatedFileCache"; 5 | import type {IOnFileModifyAutomator} from "./onFileModifyAutomators/IOnFileModifyAutomator"; 6 | import {log} from "../logger/logManager"; 7 | import type MetaEdit from "../main"; 8 | import {abstractFileToMarkdownTFile} from "../utility"; 9 | import type {IAutomatorManager} from "./IAutomatorManager"; 10 | import type {OnModifyAutomatorType} from "./onFileModifyAutomators/onModifyAutomatorType"; 11 | 12 | export class OnFileModifyAutomatorManager implements IAutomatorManager { 13 | private plugin: MetaEdit; 14 | private app: App; 15 | 16 | private updateFileQueue: UniqueQueue = new UniqueQueue(); 17 | private updatedFileCache: UpdatedFileCache= new UpdatedFileCache(); 18 | private automators: IOnFileModifyAutomator[] = []; 19 | private readonly notifyDelay: number = 5000; 20 | 21 | constructor(plugin: MetaEdit) { 22 | this.plugin = plugin; 23 | this.app = plugin.app; 24 | } 25 | 26 | startAutomators(): IAutomatorManager { 27 | this.plugin.registerEvent( 28 | this.plugin.app.vault.on("modify", (file) => this.onFileModify(file)) 29 | ); 30 | 31 | return this; 32 | } 33 | 34 | attach(automator: IOnFileModifyAutomator): IAutomatorManager { 35 | const isExist = this.automators.some(tAuto => tAuto.type === automator.type); 36 | if (isExist) { 37 | log.logWarning(`a ${automator.type} automator is already attached.`); 38 | return this; 39 | } 40 | 41 | this.automators.push(automator); 42 | return this; 43 | } 44 | 45 | detach(automatorType: OnModifyAutomatorType): IAutomatorManager { 46 | const automatorIndex = this.automators.findIndex(automator => automator.type === automatorType); 47 | if (automatorIndex === -1) { 48 | log.logMessage(`automator of type '${automatorType}' does not exist.`); 49 | return this; 50 | } 51 | 52 | this.automators.splice(automatorIndex, 1); 53 | return this; 54 | } 55 | 56 | private async onFileModify(file: TAbstractFile): Promise { 57 | const outfile: TFile = abstractFileToMarkdownTFile(file); 58 | if (!outfile) return; 59 | 60 | // Return on Excalidraw files to prevent conflict with its auto-save feature. 61 | const metadata = await this.app.metadataCache.getFileCache(outfile); 62 | if (metadata.frontmatter != null) { // Don't try to use frontmatter if it doesn't exist. 63 | const keys = Object.keys(metadata?.frontmatter); 64 | if (keys && keys.some(key => key.toLowerCase().contains("excalidraw"))) { 65 | return; 66 | } 67 | 68 | const fileContent: string = await this.app.vault.cachedRead(outfile); 69 | if (!this.updatedFileCache.set(outfile.path, fileContent)) return; 70 | 71 | if (this.updateFileQueue.enqueue(outfile)) { 72 | this.notifyAutomators(); 73 | } 74 | } 75 | } 76 | 77 | private notifyAutomators = debounce(async () => { 78 | while (!this.updateFileQueue.isEmpty()) { 79 | const file = this.updateFileQueue.dequeue(); 80 | 81 | for (const automator of this.automators) { 82 | await automator.onFileModify(file); 83 | } 84 | } 85 | }, this.notifyDelay, true); 86 | } -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomators/IOnFileModifyAutomator.ts: -------------------------------------------------------------------------------- 1 | import type {TFile} from "obsidian"; 2 | import type {OnModifyAutomatorType} from "./onModifyAutomatorType"; 3 | 4 | export interface IOnFileModifyAutomator { 5 | onFileModify(file: TFile): Promise; 6 | type: OnModifyAutomatorType; 7 | } -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomators/kanbanHelper.ts: -------------------------------------------------------------------------------- 1 | import {CachedMetadata, LinkCache, Notice, TFile} from "obsidian"; 2 | import type MetaEdit from "../../main"; 3 | import type {KanbanProperty} from "../../Types/kanbanProperty"; 4 | import {abstractFileToMarkdownTFile} from "../../utility"; 5 | import {log} from "../../logger/logManager"; 6 | import type {Property} from "../../parser"; 7 | import {OnFileModifyAutomator} from "./onFileModifyAutomator"; 8 | import {OnModifyAutomatorType} from "./onModifyAutomatorType"; 9 | 10 | export class KanbanHelper extends OnFileModifyAutomator { 11 | private get boards(): KanbanProperty[] { return this.plugin.settings.KanbanHelper.boards } 12 | 13 | constructor(plugin: MetaEdit) { 14 | super(plugin, OnModifyAutomatorType.KanbanHelper); 15 | } 16 | 17 | public async onFileModify(file: TFile): Promise { 18 | const kanbanBoardFileContent: string = await this.app.vault.cachedRead(file); 19 | const kanbanBoardFileCache: CachedMetadata = this.app.metadataCache.getFileCache(file); 20 | const targetBoard = this.findBoardByName(file.basename); 21 | if (!targetBoard || !kanbanBoardFileCache) return; 22 | 23 | const {links} = kanbanBoardFileCache; 24 | if (!links) return; 25 | 26 | await this.updateFilesInBoard(links, targetBoard, kanbanBoardFileContent); 27 | } 28 | 29 | private findBoardByName(boardName: string): KanbanProperty { 30 | return this.boards.find(board => board.boardName === boardName); 31 | } 32 | 33 | private getLinkFile(link: LinkCache): TFile { 34 | const markdownFiles: TFile[] = this.app.vault.getMarkdownFiles(); 35 | return markdownFiles.find(f => f.path.endsWith(`/${link.link}.md`) || f.path === `${link.link}.md`); 36 | } 37 | 38 | private async updateFilesInBoard(links: LinkCache[], board: KanbanProperty, kanbanBoardFileContent: string) { 39 | for (const link of links) { 40 | const linkFile: TFile = this.getLinkFile(link); 41 | const linkIsMarkdownFile: boolean = !!abstractFileToMarkdownTFile(linkFile); 42 | if (!linkFile || !linkIsMarkdownFile) { 43 | log.logMessage(`${link.link} is not updatable for the KanbanHelper.`); 44 | continue; 45 | } 46 | 47 | await this.updateFileInBoard(link, linkFile, board, kanbanBoardFileContent); 48 | } 49 | } 50 | 51 | private async updateFileInBoard(link: LinkCache, linkFile: TFile, board: KanbanProperty, kanbanBoardFileContent: string) 52 | : Promise 53 | { 54 | const heading: string = this.getTaskHeading(link.original, kanbanBoardFileContent); 55 | if (!heading) { 56 | log.logMessage(`found linked file ${link.link} but could not get heading for task.`); 57 | return; 58 | } 59 | 60 | const fileProperties: Property[] = await this.plugin.controller.getPropertiesInFile(linkFile); 61 | if (!fileProperties) { 62 | log.logWarning(`No properties found in '${board.boardName}', cannot update '${board.property}'.`) 63 | return; 64 | } 65 | const targetProperty = fileProperties.find(prop => prop.key === board.property); 66 | if (!targetProperty) { 67 | log.logWarning(`'${board.property} not found in ${board.boardName} for file "${linkFile.name}".'`); 68 | new Notice(`'${board.property} not found in ${board.boardName} for file "${linkFile.name}".'`); // This notice will help users debug "Property not found in board" errors. 69 | return; 70 | } 71 | 72 | const propertyHasChanged = (targetProperty.content != heading); // Kanban Helper will check if the file's property is different from its current heading in the kanban and will only make changes to the file if there's a difference 73 | if (propertyHasChanged) { 74 | console.debug("Updating " + targetProperty.key + " of file " + linkFile.name + " to " + heading); 75 | await this.plugin.controller.updatePropertyInFile(targetProperty, heading, linkFile); 76 | } 77 | } 78 | 79 | private getTaskHeading(targetTaskContent: string, fileContent: string): string | null { 80 | const MARKDOWN_HEADING = new RegExp(/#+\s+(.+)/); 81 | const TASK_REGEX = new RegExp(/(\s*)-\s*\[([ Xx\.]?)\]\s*(.+)/, "i"); 82 | 83 | let lastHeading: string = ""; 84 | const contentLines = fileContent.split("\n"); 85 | for (const line of contentLines) { 86 | const headingMatch = MARKDOWN_HEADING.exec(line); 87 | 88 | if (headingMatch) { 89 | const headingText = headingMatch[1]; 90 | lastHeading = headingText; 91 | } 92 | 93 | const taskMatch = TASK_REGEX.exec(line); 94 | if (taskMatch) { 95 | const taskContent = taskMatch[3]; 96 | 97 | if (taskContent.includes(targetTaskContent)) { 98 | return lastHeading; 99 | } 100 | } 101 | } 102 | 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomators/onFileModifyAutomator.ts: -------------------------------------------------------------------------------- 1 | import type {IOnFileModifyAutomator} from "./IOnFileModifyAutomator"; 2 | import type MetaEdit from "../../main"; 3 | import type {App, TFile} from "obsidian"; 4 | import type {OnModifyAutomatorType} from "./onModifyAutomatorType"; 5 | 6 | export abstract class OnFileModifyAutomator implements IOnFileModifyAutomator { 7 | protected plugin: MetaEdit; 8 | protected app: App; 9 | public type: OnModifyAutomatorType; 10 | 11 | protected constructor(plugin: MetaEdit, type: OnModifyAutomatorType) { 12 | this.plugin = plugin; 13 | this.app = plugin.app; 14 | this.type = type; 15 | } 16 | 17 | public abstract onFileModify(file: TFile): Promise; 18 | } -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomators/onModifyAutomatorType.ts: -------------------------------------------------------------------------------- 1 | export enum OnModifyAutomatorType { 2 | KanbanHelper = "KanbanHelper", ProgressProperties = "ProgressProperties" 3 | } -------------------------------------------------------------------------------- /src/automators/onFileModifyAutomators/progressPropertyHelper.ts: -------------------------------------------------------------------------------- 1 | import type MetaEdit from "../../main"; 2 | import type {TFile} from "obsidian"; 3 | import {OnFileModifyAutomator} from "./onFileModifyAutomator"; 4 | import {OnModifyAutomatorType} from "./onModifyAutomatorType"; 5 | 6 | export class ProgressPropertyHelper extends OnFileModifyAutomator { 7 | constructor(plugin: MetaEdit) { 8 | super(plugin, OnModifyAutomatorType.ProgressProperties); 9 | } 10 | 11 | async onFileModify(file: TFile): Promise { 12 | const data = await this.plugin.controller.getPropertiesInFile(file); 13 | if (!data) return; 14 | 15 | await this.plugin.controller.handleProgressProps(data, file); 16 | } 17 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type {Property} from "./parser"; 2 | import {MetaType} from "./Types/metaType"; 3 | 4 | export const ADD_FIRST_ELEMENT: string = "cmd:addfirst"; 5 | export const ADD_TO_BEGINNING: string = "cmd:beg"; 6 | export const ADD_TO_END: string = "cmd:end"; 7 | 8 | export const newDataView: string = "New Dataview field"; 9 | export const newYaml: string = "New YAML property"; 10 | export const MAIN_SUGGESTER_OPTIONS: Property[] = [ 11 | {key: newYaml, content: newYaml, type: MetaType.Option}, 12 | {key: newDataView, content: newDataView, type: MetaType.Option} 13 | ] -------------------------------------------------------------------------------- /src/dataviewHelper.ts: -------------------------------------------------------------------------------- 1 | import type {TFile, WorkspaceLeaf} from "obsidian"; 2 | import type MetaEdit from "./main"; 3 | 4 | export class DataviewHelper { 5 | private selectedValues: {[key: string]: string}[] = []; 6 | 7 | private activeLeaves: {[p: string]: { 8 | leaf: WorkspaceLeaf, 9 | addedCheckboxes: boolean, 10 | file: TFile 11 | }} = {}; 12 | 13 | constructor(private plugin: MetaEdit) { } 14 | 15 | public trigger(): { [p: string]: string }[] { 16 | return this.selectedValues; 17 | } 18 | 19 | public start(): DataviewHelper { 20 | return this; 21 | } 22 | 23 | private handleCache() { 24 | const currentLeaves: WorkspaceLeaf[] = this.plugin.app.workspace.getLeavesOfType("markdown"); 25 | 26 | Object.keys(this.activeLeaves).map(activeLeaf => { 27 | if (Object.keys(this.activeLeaves).length === 0 || 28 | !currentLeaves.find(leaf => this.activeLeaves[activeLeaf]?.leaf === leaf)) { 29 | delete this.activeLeaves[activeLeaf]; 30 | } 31 | }); 32 | 33 | currentLeaves.forEach(leaf => { 34 | const {state} = leaf.getViewState(); 35 | if (state.file && !this.activeLeaves[state.file]) 36 | this.activeLeaves[state.file] = {leaf, addedCheckboxes: false, file: null} 37 | }); 38 | } 39 | 40 | private addCheckboxes(item: { leaf: WorkspaceLeaf; addedCheckboxes: boolean; file: TFile }): void { 41 | if (item.addedCheckboxes) return; 42 | item.addedCheckboxes = true; 43 | 44 | const dvJS = document.getElementsByClassName('block-language-dataviewjs'); 45 | const dv = document.getElementsByClassName('block-language-dataview'); 46 | 47 | const dataviewBlocks: HTMLElement[] = [ 48 | ...Array.prototype.slice.call(dvJS), 49 | ...Array.prototype.slice.call(dv), 50 | ]; 51 | 52 | dataviewBlocks.forEach(block => { 53 | const tables = block.querySelectorAll("table"); 54 | tables.forEach(table => { 55 | let headers: string[] = []; 56 | 57 | for (let h = 0; h < table.tHead.rows[0].cells.length; h++) { 58 | headers.push(table.tHead.rows[0].cells[h].textContent); 59 | } 60 | 61 | for (let i = 0; i < table.rows.length; i++) { 62 | const item = table.rows.item(i); 63 | const newCell = item.insertCell(0); 64 | 65 | let valuesInRow: {[key: string]: string} = {}; 66 | for (let h = 0; h < item.cells.length; h++) { 67 | valuesInRow[headers[h - 1]] = item.cells[h].textContent; 68 | } 69 | 70 | if (i > 0) { 71 | const checkBox = newCell.createEl('input', {type: "checkbox"}); 72 | checkBox.addEventListener('change', (evt: MouseEvent) => { 73 | evt.preventDefault(); 74 | this.selectedValues.push(valuesInRow); 75 | }); 76 | } else { 77 | newCell.style.border = "none"; 78 | } 79 | } 80 | }) 81 | }) 82 | } 83 | } -------------------------------------------------------------------------------- /src/logger/consoleErrorLogger.ts: -------------------------------------------------------------------------------- 1 | import {ErrorLevel} from "./errorLevel"; 2 | import {MetaEditLogger} from "./logger"; 3 | import type {MetaEditError} from "./metaEditError"; 4 | 5 | export class ConsoleErrorLogger extends MetaEditLogger { 6 | public ErrorLog: MetaEditError[] = []; 7 | 8 | public logError(errorMsg: string) { 9 | const error = this.getMetaEditError(errorMsg, ErrorLevel.Error); 10 | this.addMessageToErrorLog(error); 11 | 12 | console.error(this.formatOutputString(error)); 13 | } 14 | 15 | public logWarning(warningMsg: string) { 16 | const warning = this.getMetaEditError(warningMsg, ErrorLevel.Warning); 17 | this.addMessageToErrorLog(warning); 18 | 19 | console.warn(this.formatOutputString(warning)); 20 | } 21 | 22 | public logMessage(logMsg: string) { 23 | const log = this.getMetaEditError(logMsg, ErrorLevel.Log); 24 | this.addMessageToErrorLog(log); 25 | 26 | console.log(this.formatOutputString(log)); 27 | } 28 | 29 | private addMessageToErrorLog(error: MetaEditError): void { 30 | this.ErrorLog.push(error); 31 | } 32 | } -------------------------------------------------------------------------------- /src/logger/errorLevel.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorLevel { Error = "ERROR", Warning = "WARNING", Log = "LOG"} 2 | -------------------------------------------------------------------------------- /src/logger/guiLogger.ts: -------------------------------------------------------------------------------- 1 | import {Notice} from "obsidian"; 2 | import type QuickAdd from "../main"; 3 | import {ErrorLevel} from "./errorLevel"; 4 | import {MetaEditLogger} from "./logger"; 5 | 6 | export class GuiLogger extends MetaEditLogger { 7 | constructor(private plugin: QuickAdd) { 8 | super(); 9 | } 10 | 11 | logError(msg: string): void { 12 | const error = this.getMetaEditError(msg, ErrorLevel.Error); 13 | new Notice(this.formatOutputString(error)); 14 | } 15 | 16 | logWarning(msg: string): void { 17 | const warning = this.getMetaEditError(msg, ErrorLevel.Warning); 18 | new Notice(this.formatOutputString(warning)); 19 | } 20 | 21 | logMessage(msg: string): void {} 22 | } -------------------------------------------------------------------------------- /src/logger/ilogger.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | logError(msg: string): void; 3 | 4 | logWarning(msg: string): void; 5 | 6 | logMessage(msg: string): void; 7 | } -------------------------------------------------------------------------------- /src/logger/logManager.ts: -------------------------------------------------------------------------------- 1 | import type {ILogger} from "./ilogger"; 2 | 3 | class LogManager { 4 | public static loggers: ILogger[] = []; 5 | 6 | public register(logger: ILogger): LogManager { 7 | LogManager.loggers.push(logger); 8 | 9 | return this; 10 | } 11 | 12 | logError(message: string) { 13 | LogManager.loggers.forEach(logger => logger.logError(message)); 14 | throw new Error(); 15 | } 16 | 17 | logWarning(message: string) { 18 | LogManager.loggers.forEach(logger => logger.logError(message)); 19 | } 20 | 21 | logMessage(message: string) { 22 | LogManager.loggers.forEach(logger => logger.logMessage(message)); 23 | } 24 | } 25 | 26 | export const log = new LogManager(); -------------------------------------------------------------------------------- /src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import type {ILogger} from "./ilogger"; 2 | import type {ErrorLevel} from "./errorLevel"; 3 | import type {MetaEditError} from "./metaEditError"; 4 | 5 | export abstract class MetaEditLogger implements ILogger{ 6 | abstract logError(msg: string): void; 7 | 8 | abstract logMessage(msg: string): void; 9 | 10 | abstract logWarning(msg: string): void; 11 | 12 | protected formatOutputString(error: MetaEditError): string { 13 | return `MetaEdit: (${error.level}) ${error.message}`; 14 | } 15 | 16 | protected getMetaEditError(message: string, level: ErrorLevel): MetaEditError { 17 | return {message, level, time: Date.now()}; 18 | } 19 | } -------------------------------------------------------------------------------- /src/logger/metaEditError.ts: -------------------------------------------------------------------------------- 1 | import type {ErrorLevel} from "./errorLevel"; 2 | 3 | export interface MetaEditError { 4 | message: string, 5 | level: ErrorLevel, 6 | time: number 7 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {Plugin, TFile, TFolder} from 'obsidian'; 2 | import {MetaEditSettingsTab} from "./Settings/metaEditSettingsTab"; 3 | import MetaEditSuggester from "./Modals/metaEditSuggester"; 4 | import MetaController from "./metaController"; 5 | import type {MetaEditSettings} from "./Settings/metaEditSettings"; 6 | import {DEFAULT_SETTINGS} from "./Settings/defaultSettings"; 7 | import {LinkMenu} from "./Modals/LinkMenu"; 8 | import type {Property} from "./parser"; 9 | import type {IMetaEditApi} from "./IMetaEditApi"; 10 | import {MetaEditApi} from "./MetaEditApi"; 11 | import GenericPrompt from "./Modals/GenericPrompt/GenericPrompt"; 12 | import {getActiveMarkdownFile} from "./utility"; 13 | import {ConsoleErrorLogger} from "./logger/consoleErrorLogger"; 14 | import {GuiLogger} from "./logger/guiLogger"; 15 | import {log} from "./logger/logManager"; 16 | import {OnFileModifyAutomatorManager} from "./automators/onFileModifyAutomatorManager"; 17 | import type {IAutomatorManager} from "./automators/IAutomatorManager"; 18 | import {KanbanHelper} from "./automators/onFileModifyAutomators/kanbanHelper"; 19 | import {ProgressPropertyHelper} from "./automators/onFileModifyAutomators/progressPropertyHelper"; 20 | import {OnModifyAutomatorType} from "./automators/onFileModifyAutomators/onModifyAutomatorType"; 21 | 22 | export default class MetaEdit extends Plugin { 23 | public settings: MetaEditSettings; 24 | public linkMenu: LinkMenu; 25 | public api: IMetaEditApi; 26 | public controller: MetaController; 27 | private automatorManager: IAutomatorManager; 28 | 29 | async onload() { 30 | console.log('Loading MetaEdit'); 31 | 32 | this.controller = new MetaController(this.app, this); 33 | 34 | await this.loadSettings(); 35 | 36 | /*START.DEVCMD*/ 37 | this.addCommand({ 38 | id: 'reloadMetaEdit', 39 | name: 'Reload MetaEdit (dev)', 40 | callback: () => { // @ts-ignore - for this.app.plugins 41 | const id: string = this.manifest.id, plugins = this.app.plugins; 42 | plugins.disablePlugin(id).then(() => plugins.enablePlugin(id)); 43 | }, 44 | }); 45 | /*END.DEVCMD*/ 46 | 47 | this.addCommand({ 48 | id: 'metaEditRun', 49 | name: 'Run MetaEdit', 50 | callback: async () => { 51 | const file: TFile = getActiveMarkdownFile(this.app); 52 | if (!file) return; 53 | 54 | await this.runMetaEditForFile(file); 55 | } 56 | }); 57 | 58 | this.addSettingTab(new MetaEditSettingsTab(this.app, this)); 59 | this.linkMenu = new LinkMenu(this); 60 | 61 | if (this.settings.UIElements.enabled) { 62 | this.linkMenu.registerEvent(); 63 | } 64 | 65 | this.api = new MetaEditApi(this).make(); 66 | 67 | log.register(new ConsoleErrorLogger()) 68 | .register(new GuiLogger(this)); 69 | 70 | this.automatorManager = new OnFileModifyAutomatorManager(this).startAutomators(); 71 | this.toggleAutomators(); 72 | } 73 | 74 | public toggleAutomators() { 75 | if (this.settings.KanbanHelper.enabled) 76 | this.automatorManager.attach(new KanbanHelper(this)); 77 | else 78 | this.automatorManager.detach(OnModifyAutomatorType.KanbanHelper); 79 | 80 | if (this.settings.ProgressProperties.enabled) 81 | this.automatorManager.attach(new ProgressPropertyHelper(this)); 82 | else 83 | this.automatorManager.detach(OnModifyAutomatorType.ProgressProperties); 84 | } 85 | 86 | public async runMetaEditForFile(file: TFile) { 87 | const data: Property[] = await this.controller.getPropertiesInFile(file); 88 | if (!data) return; 89 | 90 | const suggester: MetaEditSuggester = new MetaEditSuggester(this.app, this, data, file, this.controller); 91 | suggester.open(); 92 | } 93 | 94 | onunload() { 95 | console.log('Unloading MetaEdit'); 96 | this.linkMenu.unregisterEvent(); 97 | } 98 | 99 | async loadSettings() { 100 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 101 | } 102 | 103 | async saveSettings() { 104 | await this.saveData(this.settings); 105 | } 106 | 107 | public getFilesWithProperty(property: string): TFile[] { 108 | const markdownFiles = this.app.vault.getMarkdownFiles(); 109 | let files: TFile[] = []; 110 | 111 | markdownFiles.forEach(file => { 112 | const fileCache = this.app.metadataCache.getFileCache(file); 113 | 114 | if (fileCache) { 115 | const fileFrontmatter = fileCache.frontmatter; 116 | 117 | if (fileFrontmatter && fileFrontmatter[property]) { 118 | files.push(file); 119 | } 120 | } 121 | }); 122 | 123 | return files; 124 | } 125 | 126 | public async runMetaEditForFolder(targetFolder: TFolder) { 127 | const pName = await GenericPrompt.Prompt(this.app, `Add a new property to all files in ${targetFolder.name} (and subfolders)`); 128 | if (!pName) return; 129 | 130 | const pVal = await GenericPrompt.Prompt(this.app, "Enter a value"); 131 | if (!pVal) return; 132 | 133 | const updateFilesInFolder = async (targetFolder: TFolder, propertyName: string, propertyValue: string) => { 134 | for (const child of targetFolder.children) { 135 | if (child instanceof TFile && child.extension == "md") 136 | await this.controller.addYamlProp(pName, pVal, child); 137 | 138 | if (child instanceof TFolder) 139 | await updateFilesInFolder(child, propertyName, propertyValue); 140 | } 141 | } 142 | 143 | await updateFilesInFolder(targetFolder, pName, pVal); 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /src/metaController.ts: -------------------------------------------------------------------------------- 1 | import MetaEditParser, {Property} from "./parser"; 2 | import type {App, TFile} from "obsidian"; 3 | import type MetaEdit from "./main"; 4 | import GenericPrompt from "./Modals/GenericPrompt/GenericPrompt"; 5 | import {EditMode} from "./Types/editMode"; 6 | import GenericSuggester from "./Modals/GenericSuggester/GenericSuggester"; 7 | import type {MetaEditSettings} from "./Settings/metaEditSettings"; 8 | import {ADD_FIRST_ELEMENT, ADD_TO_BEGINNING, ADD_TO_END} from "./constants"; 9 | import type {ProgressProperty} from "./Types/progressProperty"; 10 | import {ProgressPropertyOptions} from "./Types/progressPropertyOptions"; 11 | import {MetaType} from "./Types/metaType"; 12 | import {Notice, stringifyYaml} from "obsidian"; 13 | import {log} from "./logger/logManager"; 14 | 15 | export default class MetaController { 16 | private parser: MetaEditParser; 17 | private readonly app: App; 18 | private plugin: MetaEdit; 19 | private readonly hasTrackerPlugin: boolean = false; 20 | private useTrackerPlugin: boolean = false; 21 | 22 | constructor(app: App, plugin: MetaEdit) { 23 | this.app = app; 24 | this.parser = new MetaEditParser(app); 25 | this.plugin = plugin; 26 | // @ts-ignore 27 | this.hasTrackerPlugin = !!this.app.plugins.plugins["obsidian-tracker"]; 28 | } 29 | 30 | public async getPropertiesInFile(file: TFile): Promise { 31 | const yaml = await this.parser.parseFrontmatter(file); 32 | const inlineFields = await this.parser.parseInlineFields(file); 33 | const tags = await this.parser.getTagsForFile(file); 34 | 35 | return [...tags, ...yaml, ...inlineFields]; 36 | } 37 | 38 | public async addYamlProp(propName: string, propValue: string, file: TFile): Promise { 39 | const fileContent: string = await this.app.vault.read(file); 40 | const frontmatter: Property[] = await this.parser.parseFrontmatter(file); 41 | const isYamlEmpty: boolean = ((!frontmatter || frontmatter.length === 0) && !fileContent.match(/^-{3}\s*\n*\r*-{3}/)); 42 | 43 | if (frontmatter.some(value => value.key === propName)) { 44 | new Notice(`Frontmatter in file '${file.name}' already has property '${propName}. Will not add.'`); 45 | return; 46 | } 47 | 48 | const settings = this.plugin.settings; 49 | if (settings.EditMode.mode === EditMode.AllMulti || 50 | (settings.EditMode.mode === EditMode.SomeMulti && settings.EditMode.properties.contains(propName))) { 51 | propValue = `[${propValue}]`; 52 | } 53 | 54 | let splitContent = fileContent.split("\n"); 55 | if (isYamlEmpty) { 56 | splitContent.unshift("---"); 57 | splitContent.unshift(`${propName}: ${propValue}`); 58 | splitContent.unshift("---"); 59 | } 60 | else { 61 | splitContent.splice(1, 0, `${propName}: ${propValue}`); 62 | } 63 | 64 | const newFileContent = splitContent.join("\n"); 65 | await this.app.vault.modify(file, newFileContent); 66 | } 67 | 68 | public async addDataviewField(propName: string, propValue: string, file: TFile): Promise { 69 | const fileContent: string = await this.app.vault.read(file); 70 | let lines = fileContent.split("\n").reduce((obj: {[key: string]: string}, line: string, idx: number) => { 71 | obj[idx] = !!line ? line : ""; 72 | return obj; 73 | }, {}); 74 | 75 | let appendAfter: string = await GenericSuggester.Suggest(this.app, Object.values(lines), Object.keys(lines)); 76 | if (!appendAfter) return; 77 | 78 | let splitContent: string[] = fileContent.split("\n"); 79 | if (typeof appendAfter === "number" || parseInt(appendAfter)) { 80 | splitContent.splice(parseInt(appendAfter), 0, `${propName}:: ${propValue}`); 81 | } 82 | const newFileContent = splitContent.join("\n"); 83 | 84 | await this.app.vault.modify(file, newFileContent); 85 | } 86 | 87 | public async editMetaElement(property: Property, meta: Property[], file: TFile): Promise { 88 | const mode: EditMode = this.plugin.settings.EditMode.mode; 89 | 90 | if (property.type === MetaType.Tag) 91 | await this.editTag(property, file); 92 | else if (mode === EditMode.AllMulti || mode === EditMode.SomeMulti) 93 | await this.multiValueMode(property, file); 94 | else 95 | await this.standardMode(property, file); 96 | } 97 | 98 | private async editTag(property: Property, file: TFile) { 99 | const splitTag: string[] = property.key.split("/"); 100 | const allButLast: string = splitTag.slice(0, splitTag.length - 1).join("/"); 101 | const trackerPluginMethod = "Use Tracker", metaEditMethod = "Use MetaEdit", choices = [trackerPluginMethod, metaEditMethod]; 102 | let newValue: string; 103 | let method: string = metaEditMethod; 104 | 105 | if (this.hasTrackerPlugin) 106 | method = await GenericSuggester.Suggest(this.app, choices, choices); 107 | 108 | if (!method) return; 109 | 110 | if (method === trackerPluginMethod) { 111 | newValue = await GenericPrompt.Prompt(this.app, `Enter a new value for ${property.key}`) 112 | this.useTrackerPlugin = true; 113 | } else if (method === metaEditMethod) { 114 | const autoProp = await this.handleAutoProperties(allButLast); 115 | 116 | if (autoProp) 117 | newValue = autoProp; 118 | else 119 | newValue = await GenericPrompt.Prompt(this.app, `Enter a new value for ${property.key}`); 120 | } 121 | 122 | if (newValue) { 123 | await this.updatePropertyInFile(property, newValue, file); 124 | } 125 | } 126 | 127 | public async handleProgressProps(meta: Property[], file: TFile): Promise { 128 | try { 129 | const {enabled, properties} = this.plugin.settings.ProgressProperties; 130 | if (!enabled) return; 131 | 132 | const tasks = this.app.metadataCache.getFileCache(file)?.listItems?.filter(li => li.task); 133 | if (!tasks) return; 134 | let total: number = 0, complete: number = 0, incomplete: number = 0; 135 | 136 | total = tasks.length; 137 | complete = tasks.filter(i => i.task != " ").length; 138 | incomplete = total - complete; 139 | 140 | const props = await this.progressPropHelper(properties, meta, {total, complete, incomplete}); 141 | await this.updateMultipleInFile(props, file); 142 | } 143 | catch (e) { 144 | log.logError(e); 145 | } 146 | } 147 | 148 | public async createNewProperty(suggestValues?: string[]) { 149 | let propName = await GenericPrompt.Prompt(this.app, "Enter a property name", "Property", "", suggestValues); 150 | if (!propName) return null; 151 | 152 | let propValue: string; 153 | const autoProp = await this.handleAutoProperties(propName); 154 | 155 | if (autoProp) { 156 | propValue = autoProp; 157 | } else { 158 | propValue = await GenericPrompt.Prompt(this.app, "Enter a property value", "Value") 159 | .catch(() => null); 160 | } 161 | 162 | if (propValue === null) return null; 163 | 164 | return {propName, propValue: propValue.trim()}; 165 | } 166 | 167 | public async deleteProperty(property: Property, file: TFile): Promise { 168 | const fileContent = await this.app.vault.read(file); 169 | const splitContent = fileContent.split("\n"); 170 | const regexp = new RegExp(`^\s*${property.key}:`); 171 | 172 | const idx = splitContent.findIndex(s => s.match(regexp)); 173 | const newFileContent = splitContent.filter((v, i) => { 174 | if (i != idx) return true; 175 | }).join("\n"); 176 | 177 | await this.app.vault.modify(file, newFileContent); 178 | } 179 | 180 | private async progressPropHelper(progressProps: ProgressProperty[], meta: Property[], counts: {total: number, complete: number, incomplete: number}) { 181 | return progressProps.reduce((obj: Property[], el) => { 182 | const property = meta.find(prop => prop.key === el.name); 183 | if (property) { 184 | switch (el.type) { 185 | case ProgressPropertyOptions.TaskComplete: 186 | obj.push({...property, content: counts.complete.toString()}); 187 | break; 188 | case ProgressPropertyOptions.TaskIncomplete: 189 | obj.push({...property, content: counts.incomplete.toString()}); 190 | break; 191 | case ProgressPropertyOptions.TaskTotal: 192 | obj.push({...property, content: counts.total.toString()}); 193 | break; 194 | default: break; 195 | } 196 | } 197 | 198 | return obj; 199 | }, []) 200 | } 201 | 202 | private async standardMode(property: Property, file: TFile): Promise { 203 | const autoProp = await this.handleAutoProperties(property.key); 204 | let newValue; 205 | 206 | if (autoProp) 207 | newValue = autoProp; 208 | else 209 | newValue = await GenericPrompt.Prompt(this.app, `Enter a new value for ${property.key}`, property.content, property.content); 210 | 211 | if (newValue) { 212 | await this.updatePropertyInFile(property, newValue, file); 213 | } 214 | } 215 | 216 | private async multiValueMode(property: Property, file: TFile): Promise { 217 | const settings: MetaEditSettings = this.plugin.settings; 218 | let newValue: string; 219 | 220 | 221 | if (settings.EditMode.mode == EditMode.SomeMulti && !settings.EditMode.properties.includes(property.key)) { 222 | await this.standardMode(property, file); 223 | return false; 224 | } 225 | 226 | let selectedOption: string, tempValue: string, splitValues: string[]; 227 | let currentPropValue: string = property.content; 228 | 229 | if (currentPropValue !== null) 230 | currentPropValue = currentPropValue.toString(); 231 | else 232 | currentPropValue = ""; 233 | 234 | if (property.type === MetaType.YAML) { 235 | splitValues = currentPropValue.split('').filter(c => !c.includes("[]")).join('').split(","); 236 | } else { 237 | splitValues = currentPropValue.split(",").map(prop => prop.trim()); 238 | } 239 | 240 | if (splitValues.length == 0 || (splitValues.length == 1 && splitValues[0] == "")) { 241 | const options = ["Add new value"]; 242 | selectedOption = await GenericSuggester.Suggest(this.app, options, [ADD_FIRST_ELEMENT]); 243 | } 244 | else if (splitValues.length == 1) { 245 | const options = [splitValues[0], "Add to end", "Add to beginning"]; 246 | selectedOption = await GenericSuggester.Suggest(this.app, options, [splitValues[0], ADD_TO_END, ADD_TO_BEGINNING]); 247 | } else { 248 | const options = ["Add to end", ...splitValues, "Add to beginning"]; 249 | selectedOption = await GenericSuggester.Suggest(this.app, options, [ADD_TO_END, ...splitValues, ADD_TO_BEGINNING]); 250 | } 251 | 252 | if (!selectedOption) return; 253 | let selectedIndex; 254 | 255 | const autoProp = await this.handleAutoProperties(property.key); 256 | if (autoProp) { 257 | tempValue = autoProp; 258 | } else if (selectedOption.includes("cmd")) { 259 | tempValue = await GenericPrompt.Prompt(this.app, "Enter a new value"); 260 | } else { 261 | selectedIndex = splitValues.findIndex(el => el == selectedOption); 262 | tempValue = await GenericPrompt.Prompt(this.app, `Change ${selectedOption} to`, selectedOption); 263 | } 264 | 265 | if (!tempValue) return; 266 | switch(selectedOption) { 267 | case ADD_FIRST_ELEMENT: 268 | newValue = `${tempValue}`; 269 | break; 270 | case ADD_TO_BEGINNING: 271 | newValue = `${[tempValue, ...splitValues].join(", ")}`; 272 | break; 273 | case ADD_TO_END: 274 | newValue = `${[...splitValues, tempValue].join(", ")}`; 275 | break; 276 | default: 277 | if (selectedIndex) 278 | splitValues[selectedIndex] = tempValue; 279 | else 280 | splitValues = [tempValue]; 281 | newValue = `${splitValues.join(", ")}`; 282 | break; 283 | } 284 | 285 | if (property.type === MetaType.YAML) 286 | newValue = `[${newValue}]`; 287 | 288 | if (newValue) { 289 | await this.updatePropertyInFile(property, newValue, file); 290 | return true; 291 | } 292 | 293 | return false; 294 | } 295 | 296 | public async handleAutoProperties(propertyName: string): Promise { 297 | const autoProp = this.plugin.settings.AutoProperties.properties.find(a => a.name === propertyName); 298 | 299 | if (this.plugin.settings.AutoProperties.enabled && autoProp) { 300 | const options = autoProp.choices; 301 | return await GenericPrompt.Prompt(this.app, `Enter a new value for ${propertyName}`, '', '', options); 302 | } 303 | 304 | return null; 305 | } 306 | 307 | private updateYamlProperty(property: Partial, newValue: string, file: TFile): string { 308 | const fileCache = this.app.metadataCache.getFileCache(file); 309 | const frontMatter = fileCache.frontmatter; 310 | frontMatter[property.key] = newValue; 311 | return stringifyYaml(frontMatter); 312 | } 313 | 314 | public async updatePropertyInFile(property: Partial, newValue: string, file: TFile): Promise { 315 | // I'm aware this is hacky. Didn't want to spend a bunch of time rewriting old logic. 316 | // This uses the new frontmatter API to update the frontmatter. Later TODO: rewrite old logic to just do this & clean. 317 | if (property.type === MetaType.YAML) { 318 | const updatedMetaData = `---\n${this.updateYamlProperty(property, newValue, file)}\n---`; 319 | //@ts-ignore 320 | const frontmatterPosition = this.app.metadataCache.getFileCache(file).frontmatterPosition; 321 | const fileContents = await this.app.vault.read(file); 322 | 323 | const deleteFrom = frontmatterPosition.start.offset; 324 | const deleteTo = frontmatterPosition.end.offset; 325 | 326 | const newFileContents = fileContents.substring(0, deleteFrom) + updatedMetaData + fileContents.substring(deleteTo); 327 | 328 | await this.app.vault.modify(file, newFileContents); 329 | return; 330 | } 331 | 332 | const fileContent = await this.app.vault.read(file); 333 | 334 | const newFileContent = fileContent.split("\n").map(line => { 335 | if (this.lineMatch(property, line)) { 336 | return this.updatePropertyLine(property, newValue, line); 337 | } 338 | 339 | return line; 340 | }).join("\n"); 341 | 342 | await this.app.vault.modify(file, newFileContent); 343 | } 344 | 345 | private escapeSpecialCharacters(text: string): string{ 346 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 347 | } 348 | 349 | private lineMatch(property: Partial, line: string): boolean { 350 | const propertyRegex = new RegExp(`${this.escapeSpecialCharacters(property.key)}\:{1,2}`); 351 | const tagRegex = new RegExp(`^\s*${this.escapeSpecialCharacters(property.key)}`); 352 | 353 | if (property.key.contains('#')) { 354 | return tagRegex.test(line); 355 | } 356 | 357 | return propertyRegex.test(line); 358 | } 359 | 360 | private updatePropertyLine(property: Partial, newValue: string, line: string) { 361 | let newLine: string; 362 | switch (property.type) { 363 | case MetaType.Dataview: 364 | const propertyRegex = new RegExp(`([\\(\\[]?)${this.escapeSpecialCharacters(property.key)}::[ ]*[^\\)\\]\n\r]*(?:\\]\])?([\\]\\)]?)`, 'g'); 365 | newLine = line.replace(propertyRegex, `$1${property.key}:: ${newValue}$2`); 366 | break; 367 | case MetaType.YAML: 368 | newLine = `${property.key}: ${newValue}`; 369 | break; 370 | case MetaType.Tag: 371 | if (this.useTrackerPlugin) { 372 | newLine = `${property.key}:${newValue}`; 373 | } else { 374 | const splitTag: string[] = property.key.split("/"); 375 | if (splitTag.length === 1) 376 | newLine = `${splitTag[0]}/${newValue}`; 377 | else if (splitTag.length > 1) { 378 | const allButLast: string = splitTag.slice(0, splitTag.length - 1).join("/"); 379 | newLine = `${allButLast}/${newValue}`; 380 | } else 381 | newLine = property.key; 382 | } 383 | break; 384 | default: 385 | newLine = property.key; 386 | break; 387 | } 388 | 389 | return newLine; 390 | } 391 | 392 | private async updateMultipleInFile(properties: Property[], file: TFile): Promise { 393 | let fileContent = (await this.app.vault.read(file)).split("\n"); 394 | 395 | for (const prop of properties) { 396 | fileContent = fileContent.map(line => { 397 | 398 | if (this.lineMatch(prop, line)) { 399 | return this.updatePropertyLine(prop, prop.content, line) 400 | } 401 | 402 | return line; 403 | }); 404 | } 405 | const newFileContent = fileContent.join("\n"); 406 | 407 | await this.app.vault.modify(file, newFileContent); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import type {App, TFile} from "obsidian"; 2 | import {parseYaml} from "obsidian"; 3 | import {MetaType} from "./Types/metaType"; 4 | 5 | export type Property = {key: string, content: any, type: MetaType}; 6 | 7 | export default class MetaEditParser { 8 | private app: App; 9 | 10 | constructor(app: App) { 11 | this.app = app; 12 | } 13 | 14 | public async getTagsForFile(file: TFile): Promise { 15 | const cache = this.app.metadataCache.getFileCache(file); 16 | if (!cache) return []; 17 | const tags = cache.tags; 18 | if (!tags) return []; 19 | 20 | let mTags: Property[] = []; 21 | tags.forEach(tag => mTags.push({key: tag.tag, content: tag.tag, type: MetaType.Tag})); 22 | return mTags; 23 | } 24 | 25 | public async parseFrontmatter(file: TFile): Promise { 26 | const fileCache = this.app.metadataCache.getFileCache(file); 27 | const frontmatter = fileCache?.frontmatter; 28 | if (!frontmatter) return []; 29 | 30 | //@ts-ignore - this is part of the new Obsidian API as of v1.4.1 31 | const {start, end} = fileCache?.frontmatterPosition; 32 | const filecontent = await this.app.vault.cachedRead(file); 33 | 34 | const yamlContent: string = filecontent.split("\n").slice(start.line, end.line).join("\n"); 35 | const parsedYaml = parseYaml(yamlContent); 36 | 37 | let metaYaml: Property[] = []; 38 | 39 | for (const key in parsedYaml) { 40 | metaYaml.push({key, content: parsedYaml[key], type: MetaType.YAML}); 41 | } 42 | 43 | return metaYaml; 44 | } 45 | 46 | public async parseInlineFields(file: TFile): Promise { 47 | const content = await this.app.vault.cachedRead(file); 48 | const regex = /[\[\(]?([^\n\r\(\[]*)::[ ]*([^\)\]\n\r]*)[\]\)]?/g; 49 | const properties: Property[] = []; 50 | 51 | let match; 52 | while ((match = regex.exec(content)) !== null) { 53 | const key: string = match[1].trim(); 54 | const value: string = match[2].trim(); 55 | 56 | properties.push({key, content: value, type: MetaType.Dataview}); 57 | } 58 | 59 | return properties; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/suggest.ts: -------------------------------------------------------------------------------- 1 | // Sam stole all this from Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes 2 | // And then I stole it from Sam's Buttons: https://github.com/shabegom/buttons 3 | 4 | import { App, ISuggestOwner, Scope, TFile, TAbstractFile } from "obsidian"; 5 | import { createPopper, Instance as PopperInstance } from "@popperjs/core"; 6 | 7 | const wrapAround = (value: number, size: number): number => { 8 | return ((value % size) + size) % size; 9 | }; 10 | 11 | class Suggest { 12 | private owner: ISuggestOwner; 13 | private values: T[]; 14 | private suggestions: HTMLDivElement[]; 15 | private selectedItem: number; 16 | private containerEl: HTMLElement; 17 | 18 | constructor(owner: ISuggestOwner, containerEl: HTMLElement, scope: Scope) { 19 | this.owner = owner; 20 | this.containerEl = containerEl; 21 | 22 | containerEl.on( 23 | "click", 24 | ".suggestion-item", 25 | this.onSuggestionClick.bind(this) 26 | ); 27 | containerEl.on( 28 | "mousemove", 29 | ".suggestion-item", 30 | this.onSuggestionMouseover.bind(this) 31 | ); 32 | 33 | scope.register([], "ArrowUp", (event) => { 34 | if (!event.isComposing) { 35 | this.setSelectedItem(this.selectedItem - 1, true); 36 | return false; 37 | } 38 | }); 39 | 40 | scope.register([], "ArrowDown", (event) => { 41 | if (!event.isComposing) { 42 | this.setSelectedItem(this.selectedItem + 1, true); 43 | return false; 44 | } 45 | }); 46 | 47 | scope.register([], "Enter", (event) => { 48 | if (!event.isComposing) { 49 | this.useSelectedItem(event); 50 | return false; 51 | } 52 | }); 53 | } 54 | 55 | onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { 56 | event.preventDefault(); 57 | 58 | const item = this.suggestions.indexOf(el); 59 | this.setSelectedItem(item, false); 60 | this.useSelectedItem(event); 61 | } 62 | 63 | onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { 64 | const item = this.suggestions.indexOf(el); 65 | this.setSelectedItem(item, false); 66 | } 67 | 68 | setSuggestions(values: T[]) { 69 | this.containerEl.empty(); 70 | const suggestionEls: HTMLDivElement[] = []; 71 | 72 | values.forEach((value) => { 73 | const suggestionEl = this.containerEl.createDiv("suggestion-item"); 74 | this.owner.renderSuggestion(value, suggestionEl); 75 | suggestionEls.push(suggestionEl); 76 | }); 77 | 78 | this.values = values; 79 | this.suggestions = suggestionEls; 80 | this.setSelectedItem(0, false); 81 | } 82 | 83 | useSelectedItem(event: MouseEvent | KeyboardEvent) { 84 | const currentValue = this.values[this.selectedItem]; 85 | if (currentValue) { 86 | this.owner.selectSuggestion(currentValue, event); 87 | } 88 | } 89 | 90 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { 91 | const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); 92 | const prevSelectedSuggestion = this.suggestions[this.selectedItem]; 93 | const selectedSuggestion = this.suggestions[normalizedIndex]; 94 | 95 | prevSelectedSuggestion?.removeClass("is-selected"); 96 | selectedSuggestion?.addClass("is-selected"); 97 | 98 | this.selectedItem = normalizedIndex; 99 | 100 | if (scrollIntoView) { 101 | selectedSuggestion.scrollIntoView(false); 102 | } 103 | } 104 | } 105 | 106 | export abstract class TextInputSuggest implements ISuggestOwner { 107 | protected app: App; 108 | protected inputEl: HTMLInputElement; 109 | 110 | private popper: PopperInstance; 111 | private scope: Scope; 112 | private suggestEl: HTMLElement; 113 | private suggest: Suggest; 114 | 115 | constructor(app: App, inputEl: HTMLInputElement) { 116 | this.app = app; 117 | this.inputEl = inputEl; 118 | this.scope = new Scope(); 119 | 120 | this.suggestEl = createDiv("suggestion-container"); 121 | const suggestion = this.suggestEl.createDiv("suggestion"); 122 | this.suggest = new Suggest(this, suggestion, this.scope); 123 | 124 | this.scope.register([], "Escape", this.close.bind(this)); 125 | 126 | this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); 127 | this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); 128 | this.inputEl.addEventListener("blur", this.close.bind(this)); 129 | this.suggestEl.on( 130 | "mousedown", 131 | ".suggestion-container", 132 | (event: MouseEvent) => { 133 | event.preventDefault(); 134 | } 135 | ); 136 | } 137 | 138 | onInputChanged(): void { 139 | const inputStr = this.inputEl.value; 140 | const suggestions = this.getSuggestions(inputStr); 141 | 142 | if (!suggestions) { 143 | this.close(); 144 | return; 145 | } 146 | 147 | if (suggestions.length > 0) { 148 | this.suggest.setSuggestions(suggestions); 149 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 150 | this.open((this.app).dom.appContainerEl, this.inputEl); 151 | } else { 152 | this.close() 153 | } 154 | } 155 | 156 | open(container: HTMLElement, inputEl: HTMLElement): void { 157 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 158 | (this.app).keymap.pushScope(this.scope); 159 | 160 | container.appendChild(this.suggestEl); 161 | this.popper = createPopper(inputEl, this.suggestEl, { 162 | placement: "bottom-start", 163 | modifiers: [ 164 | { 165 | name: "sameWidth", 166 | enabled: true, 167 | fn: ({ state, instance }) => { 168 | // Note: positioning needs to be calculated twice - 169 | // first pass - positioning it according to the width of the popper 170 | // second pass - position it with the width bound to the reference element 171 | // we need to early exit to avoid an infinite loop 172 | const targetWidth = `${state.rects.reference.width}px`; 173 | if (state.styles.popper.width === targetWidth) { 174 | return; 175 | } 176 | state.styles.popper.width = targetWidth; 177 | instance.update(); 178 | }, 179 | phase: "beforeWrite", 180 | requires: ["computeStyles"], 181 | }, 182 | ], 183 | }); 184 | } 185 | 186 | close(): void { 187 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 188 | (this.app).keymap.popScope(this.scope); 189 | 190 | this.suggest.setSuggestions([]); 191 | this.popper.destroy(); 192 | this.suggestEl.detach(); 193 | } 194 | 195 | abstract getSuggestions(inputStr: string): T[]; 196 | abstract renderSuggestion(item: T, el: HTMLElement): void; 197 | abstract selectSuggestion(item: T): void; 198 | } -------------------------------------------------------------------------------- /src/tests/uniqueQueue.test.ts: -------------------------------------------------------------------------------- 1 | import {UniqueQueue} from "../uniqueQueue"; 2 | 3 | export {} 4 | 5 | test("UniqueQueue_enqueue_isEmptyFalseOnAdd", () => { 6 | const queue = new UniqueQueue(); 7 | queue.enqueue(1); 8 | 9 | expect(queue.isEmpty()).toBe(false); 10 | }) 11 | 12 | test("UniqueQueue_isEmpty_TrueOnEmpty", () => { 13 | const queue = new UniqueQueue(); 14 | 15 | expect(queue.isEmpty()).toBe(true); 16 | }) 17 | 18 | test("UniqueQueue_enqueue_OnlyUniqueItemsInQueue", () => { 19 | const queue = new UniqueQueue(); 20 | queue.enqueue(1); 21 | queue.enqueue(1); 22 | queue.enqueue(1); 23 | queue.enqueue(1); 24 | 25 | expect(queue.length()).toBe(1); 26 | }) 27 | 28 | test("UniqueQueue_dequeue_RemoveItemFromQueue", () => { 29 | const queue = new UniqueQueue(); 30 | queue.enqueue(1); 31 | 32 | expect(queue.length()).toBe(1); 33 | expect(queue.dequeue()).toBe(1); 34 | expect(queue.length()).toBe(0); 35 | }) 36 | 37 | test("UniqueQueue_dequeue_RemoveItemFromEmptyQueue", () => { 38 | const queue = new UniqueQueue(); 39 | 40 | expect(queue.isEmpty()).toBe(true); 41 | expect(queue.dequeue()).toBe(undefined); 42 | }) 43 | 44 | test("UniqueQueue_peek_ShowFrontOfQueue", () => { 45 | const queue = new UniqueQueue(); 46 | expect(queue.peek()).toBe(undefined) 47 | 48 | queue.enqueue(1); 49 | 50 | expect(queue.peek()).toBe(1); 51 | }) 52 | 53 | test("UniqueQueue_peek_UndefinedOnNoQueue", () => { 54 | const queue = new UniqueQueue(); 55 | 56 | expect(queue.peek()).toBe(undefined) 57 | }) -------------------------------------------------------------------------------- /src/uniqueQueue.ts: -------------------------------------------------------------------------------- 1 | export class UniqueQueue { 2 | private readonly elements: T[]; 3 | 4 | constructor() { 5 | this.elements = []; 6 | } 7 | 8 | public enqueue(item: T): boolean { 9 | if (this.elements.find(i => i === item)) { 10 | return false; 11 | } 12 | 13 | this.elements.push(item); 14 | return true; 15 | } 16 | 17 | public dequeue(): T | undefined { 18 | return this.elements.shift(); 19 | } 20 | 21 | public peek(): T | undefined{ 22 | return this.elements[0]; 23 | } 24 | 25 | public isEmpty(): boolean { 26 | return this.elements.length === 0; 27 | } 28 | 29 | public length(): number { 30 | return this.elements.length; 31 | } 32 | } -------------------------------------------------------------------------------- /src/updatedFileCache.ts: -------------------------------------------------------------------------------- 1 | import type {DatedFileCacheItem} from "./Types/datedFileCacheItem"; 2 | 3 | export class UpdatedFileCache { 4 | private map: Map; 5 | 6 | constructor() { 7 | this.map = new Map(); 8 | } 9 | 10 | public get(key: string): DatedFileCacheItem | undefined { 11 | return this.map.get(key); 12 | } 13 | 14 | public set(key: string, content: string): boolean { 15 | if (this.map.has(key) && this.map.get(key).content === content) 16 | return false; 17 | 18 | this.map.set(key, {content, updateTime: Date.now()}); 19 | this.clean(); 20 | 21 | return true; 22 | } 23 | 24 | public delete(key: string) { 25 | this.map.delete(key); 26 | } 27 | 28 | private clean() { 29 | const five_minutes: number = 300_000; 30 | 31 | this.map.forEach((item, key) => { 32 | if (item.updateTime < Date.now() - five_minutes) { 33 | this.delete(key); 34 | } 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /src/utility.ts: -------------------------------------------------------------------------------- 1 | import {App, TAbstractFile, TFile} from "obsidian"; 2 | 3 | export function getActiveMarkdownFile(app: App): TFile { 4 | const activeFile: TFile = app.workspace.getActiveFile(); 5 | const activeMarkdownFile = abstractFileToMarkdownTFile(activeFile); 6 | 7 | if (!activeMarkdownFile) { 8 | this.logError("could not get current file."); 9 | return null; 10 | } 11 | 12 | return activeMarkdownFile; 13 | } 14 | 15 | export function abstractFileToMarkdownTFile(file: TAbstractFile): TFile { 16 | if (file instanceof TFile && file.extension === "md") 17 | return file; 18 | 19 | return null; 20 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .centerSettingContent { 2 | display: grid; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .not-a-button { 8 | background: none; 9 | color: inherit; 10 | border: none; 11 | padding: 0; 12 | font: inherit; 13 | cursor: pointer; 14 | outline: inherit; 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*"], 5 | "exclude": ["node_modules/*"], 6 | "compilerOptions": { 7 | "types": ["node", "svelte", "jest"], 8 | "baseUrl": ".", 9 | "paths": { 10 | "src": ["src/*"] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /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.7.2": "0.12.0", 3 | "1.8.0": "0.12.0", 4 | "1.8.1": "1.4.1", 5 | "1.8.2": "1.4.1" 6 | } --------------------------------------------------------------------------------