From d30595fead0e87f8f9a727625f30b973b2012ea3 Mon Sep 17 00:00:00 2001 From: Gilles VIGNER Date: Sun, 3 May 2026 16:46:18 +0200 Subject: [PATCH] Initial commit: properties-filename plugin v0.1.0 --- .gitignore | 6 + LICENSE | 21 ++ README.md | 46 ++++ esbuild.config.mjs | 47 ++++ manifest.json | 10 + package-lock.json | 611 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 23 ++ src/main.ts | 347 +++++++++++++++++++++++++ tsconfig.json | 18 ++ version-bump.mjs | 12 + versions.json | 3 + 11 files changed, 1144 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 esbuild.config.mjs create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 tsconfig.json create mode 100644 version-bump.mjs create mode 100644 versions.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d9db86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +main.js +main.js.map +.DS_Store +*.log +.env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..923ae25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 gilles6 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79d8a73 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Properties Filename + +Obsidian plugin that automatically renames notes based on frontmatter properties. + +When a note's properties change (e.g. `nom` and `prenom`), the file is renamed to match a configurable template (e.g. `{{nom}} {{prenom}}`). Wikilinks pointing to the file are updated automatically by Obsidian's rename API. + +## Use case + +You maintain a folder of contact/patient/client notes where the canonical identifier is built from properties — first name + last name, project code + title, etc. Editing the property updates the filename without manual renames. + +## Configuration + +Each rule has: + +- **Folder** — folder path the rule applies to (e.g. `Patients`). Subfolders included. +- **Template** — filename template using `{{property}}` placeholders. Example: `{{nom}} {{prenom}}` +- **Require type** *(optional)* — only rename if frontmatter `type` matches this value (e.g. `patient`). + +A rename only fires when: + +- All referenced properties are present and non-empty +- The new name differs from the current filename +- No other file already exists at the target path + +## Commands + +- **Properties Filename: Rename current file from properties** — manual trigger for the active note. +- **Properties Filename: Rename all matching files in vault** — bulk pass over every file matching a rule. + +## Auto-rename + +Auto-rename on property change is **off by default**. Enable it in settings if you want files to rename live as you type (debounced 800ms). + +## Install (manual) + +1. Download `main.js`, `manifest.json` from the latest release +2. Drop into `/.obsidian/plugins/properties-filename/` +3. Enable in Settings → Community plugins + +## Install via BRAT (beta) + +Add `gilles6/properties-filename` in the BRAT plugin. + +## License + +MIT diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..3720844 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,47 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = process.argv[2] === "production"; + +const ctx = await esbuild.context({ + banner: { js: banner }, + entryPoints: ["src/main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2020", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, +}); + +if (prod) { + await ctx.rebuild(); + process.exit(0); +} else { + await ctx.watch(); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..fcd4389 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "properties-filename", + "name": "Properties Filename", + "version": "0.1.0", + "minAppVersion": "1.4.0", + "description": "Automatically rename notes from frontmatter properties (e.g. nom + prenom) when they change.", + "author": "gilles6", + "authorUrl": "https://github.com/gilles6", + "isDesktopOnly": false +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d5751f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,611 @@ +{ + "name": "properties-filename", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "properties-filename", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.20.0", + "obsidian": "latest", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obsidian": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..48f9b7b --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "properties-filename", + "version": "0.1.0", + "description": "Obsidian plugin: rename notes from frontmatter properties.", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production", + "deploy": "npm run build && cp main.js manifest.json \"$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/Serge/.obsidian/plugins/properties-filename/\"", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": ["obsidian", "plugin", "frontmatter", "properties", "rename", "filename"], + "author": "gilles6", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.20.0", + "obsidian": "latest", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3a49e86 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,347 @@ +import { + App, + Notice, + Plugin, + PluginSettingTab, + Setting, + TAbstractFile, + TFile, + debounce, + normalizePath, +} from "obsidian"; + +interface RenameRule { + folder: string; + template: string; + requireType?: string; +} + +interface PropertiesFilenameSettings { + rules: RenameRule[]; + autoRename: boolean; + debounceMs: number; +} + +const DEFAULT_SETTINGS: PropertiesFilenameSettings = { + rules: [], + autoRename: false, + debounceMs: 800, +}; + +const ILLEGAL_FILENAME_CHARS = /[\\/:*?"<>|#^[\]]/g; +const TEMPLATE_TOKEN = /\{\{\s*([^}\s]+)\s*\}\}/g; + +export default class PropertiesFilenamePlugin extends Plugin { + settings!: PropertiesFilenameSettings; + private debouncedHandlers = new Map void>(); + + async onload() { + await this.loadSettings(); + + this.addSettingTab(new PropertiesFilenameSettingTab(this.app, this)); + + this.addCommand({ + id: "rename-active-file", + name: "Rename current file from properties", + checkCallback: (checking) => { + const file = this.app.workspace.getActiveFile(); + if (!file || file.extension !== "md") return false; + if (checking) return true; + void this.renameFile(file, { notify: true }); + return true; + }, + }); + + this.addCommand({ + id: "rename-all-files", + name: "Rename all matching files in vault", + callback: () => void this.renameAll(), + }); + + this.registerEvent( + this.app.metadataCache.on("changed", (file) => { + if (!this.settings.autoRename) return; + if (!(file instanceof TFile) || file.extension !== "md") return; + this.scheduleRename(file); + }) + ); + } + + onunload() { + this.debouncedHandlers.clear(); + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + private scheduleRename(file: TFile) { + let handler = this.debouncedHandlers.get(file.path); + if (!handler) { + handler = debounce( + () => { + this.debouncedHandlers.delete(file.path); + const current = this.app.vault.getAbstractFileByPath( + file.path + ); + if (current instanceof TFile) { + void this.renameFile(current, { notify: false }); + } + }, + this.settings.debounceMs, + true + ); + this.debouncedHandlers.set(file.path, handler); + } + handler(); + } + + async renameAll() { + const files = this.app.vault.getMarkdownFiles(); + let renamed = 0; + let skipped = 0; + for (const file of files) { + const result = await this.renameFile(file, { notify: false }); + if (result === "renamed") renamed++; + else if (result === "skipped-collision") skipped++; + } + new Notice( + `Properties Filename: ${renamed} renamed, ${skipped} collisions.` + ); + } + + async renameFile( + file: TFile, + opts: { notify: boolean } + ): Promise<"renamed" | "no-rule" | "no-change" | "missing-props" | "skipped-collision"> { + const rule = this.findRule(file); + if (!rule) { + if (opts.notify) new Notice("No matching rule for this file."); + return "no-rule"; + } + + const cache = this.app.metadataCache.getFileCache(file); + const frontmatter = cache?.frontmatter ?? {}; + + if (rule.requireType && frontmatter.type !== rule.requireType) { + if (opts.notify) new Notice("File type does not match rule."); + return "no-rule"; + } + + const rendered = applyTemplate(rule.template, frontmatter); + if (!rendered) { + if (opts.notify) + new Notice( + "Required properties are empty — file not renamed." + ); + return "missing-props"; + } + + const newBasename = sanitizeFilename(rendered); + if (!newBasename) { + if (opts.notify) + new Notice("Rendered name is empty after sanitization."); + return "missing-props"; + } + + if (newBasename === file.basename) return "no-change"; + + const folder = file.parent?.path ?? ""; + const newPath = normalizePath( + folder ? `${folder}/${newBasename}.md` : `${newBasename}.md` + ); + + const collision = this.app.vault.getAbstractFileByPath(newPath); + if (collision && collision !== file) { + if (opts.notify) + new Notice( + `Cannot rename: "${newPath}" already exists.` + ); + return "skipped-collision"; + } + + try { + await this.app.fileManager.renameFile(file, newPath); + if (opts.notify) new Notice(`Renamed to "${newBasename}".`); + return "renamed"; + } catch (e) { + console.error("Properties Filename: rename failed", e); + if (opts.notify) new Notice("Rename failed — see console."); + return "skipped-collision"; + } + } + + private findRule(file: TFile): RenameRule | null { + for (const rule of this.settings.rules) { + const folder = rule.folder.replace(/^\/+|\/+$/g, ""); + if (!folder) continue; + if (!rule.template?.trim()) continue; + if ( + file.path === `${folder}.md` || + file.path.startsWith(`${folder}/`) + ) { + return rule; + } + } + return null; + } +} + +export function applyTemplate( + template: string, + frontmatter: Record +): string | null { + let allPresent = true; + const result = template.replace(TEMPLATE_TOKEN, (_, key: string) => { + const raw = frontmatter[key]; + if (raw === undefined || raw === null || raw === "") { + allPresent = false; + return ""; + } + return String(raw).trim(); + }); + if (!allPresent) return null; + const collapsed = result.replace(/\s+/g, " ").trim(); + return collapsed || null; +} + +export function sanitizeFilename(name: string): string { + return name.replace(ILLEGAL_FILENAME_CHARS, "").trim(); +} + +class PropertiesFilenameSettingTab extends PluginSettingTab { + plugin: PropertiesFilenamePlugin; + + constructor(app: App, plugin: PropertiesFilenamePlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + new Setting(containerEl) + .setName("Auto-rename on property change") + .setDesc( + "Rename files automatically when their properties change (debounced). Off by default — use the command palette for manual renames." + ) + .addToggle((t) => + t + .setValue(this.plugin.settings.autoRename) + .onChange(async (v) => { + this.plugin.settings.autoRename = v; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Debounce (ms)") + .setDesc( + "Delay before auto-renaming after a property change. Avoids renaming on every keystroke." + ) + .addText((t) => + t + .setPlaceholder("800") + .setValue(String(this.plugin.settings.debounceMs)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 100 && n <= 10000) { + this.plugin.settings.debounceMs = n; + await this.plugin.saveSettings(); + } + }) + ); + + new Setting(containerEl).setName("Rules").setHeading(); + + containerEl.createEl("p", { + text: "Each rule applies to files in a folder (subfolders included). The template uses {{property}} placeholders that are replaced by frontmatter values. A file is renamed only when all referenced properties are non-empty.", + cls: "setting-item-description", + }); + + this.plugin.settings.rules.forEach((rule, index) => { + const ruleEl = containerEl.createDiv({ cls: "pf-rule" }); + ruleEl.style.border = "1px solid var(--background-modifier-border)"; + ruleEl.style.borderRadius = "6px"; + ruleEl.style.padding = "0.75em"; + ruleEl.style.marginBottom = "0.75em"; + + new Setting(ruleEl) + .setName(`Rule ${index + 1}`) + .addExtraButton((b) => + b + .setIcon("trash") + .setTooltip("Delete rule") + .onClick(async () => { + this.plugin.settings.rules.splice(index, 1); + await this.plugin.saveSettings(); + this.display(); + }) + ); + + new Setting(ruleEl) + .setName("Folder") + .setDesc("Folder path, e.g. Patients") + .addText((t) => + t + .setPlaceholder("Patients") + .setValue(rule.folder) + .onChange(async (v) => { + rule.folder = v; + await this.plugin.saveSettings(); + }) + ); + + new Setting(ruleEl) + .setName("Template") + .setDesc("e.g. {{nom}} {{prenom}}") + .addText((t) => + t + .setPlaceholder("{{nom}} {{prenom}}") + .setValue(rule.template) + .onChange(async (v) => { + rule.template = v; + await this.plugin.saveSettings(); + }) + ); + + new Setting(ruleEl) + .setName("Require type (optional)") + .setDesc( + "If set, only rename files whose frontmatter type matches this value." + ) + .addText((t) => + t + .setPlaceholder("patient") + .setValue(rule.requireType ?? "") + .onChange(async (v) => { + rule.requireType = v.trim() || undefined; + await this.plugin.saveSettings(); + }) + ); + }); + + new Setting(containerEl).addButton((b) => + b + .setButtonText("Add rule") + .setCta() + .onClick(async () => { + this.plugin.settings.rules.push({ + folder: "", + template: "", + }); + await this.plugin.saveSettings(); + this.display(); + }) + ); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e6d9f4a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES2020", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "esModuleInterop": true, + "lib": ["DOM", "ES2020"] + }, + "include": ["src/**/*.ts"] +} diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..c1d5c84 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,12 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +const versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..cdffaed --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "0.1.0": "1.4.0" +}