vault backup: 2024-01-02 14:46:20
This commit is contained in:
942
.obsidian/plugins/obsidian-paste-image-rename/main.js
vendored
Normal file
942
.obsidian/plugins/obsidian-paste-image-rename/main.js
vendored
Normal file
@@ -0,0 +1,942 @@
|
||||
/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD */
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defProps = Object.defineProperties;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __spreadValues = (a, b) => {
|
||||
for (var prop in b || (b = {}))
|
||||
if (__hasOwnProp.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
if (__getOwnPropSymbols)
|
||||
for (var prop of __getOwnPropSymbols(b)) {
|
||||
if (__propIsEnum.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// package.json
|
||||
var require_package = __commonJS({
|
||||
"package.json"(exports, module2) {
|
||||
module2.exports = {
|
||||
name: "obsidian-paste-image-rename",
|
||||
version: "1.6.1",
|
||||
main: "main.js",
|
||||
scripts: {
|
||||
start: "node esbuild.config.mjs",
|
||||
build: "tsc -noEmit -skipLibCheck && BUILD_ENV=production node esbuild.config.mjs && cp manifest.json build",
|
||||
version: "node version-bump.mjs && git add manifest.json versions.json",
|
||||
release: "npm run build && gh release create ${npm_package_version} build/*"
|
||||
},
|
||||
keywords: [],
|
||||
author: "Reorx",
|
||||
license: "MIT",
|
||||
devDependencies: {
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"builtin-modules": "^3.3.0",
|
||||
esbuild: "0.16.17",
|
||||
obsidian: "^1.1.1",
|
||||
tslib: "2.5.0",
|
||||
typescript: "4.9.4"
|
||||
},
|
||||
dependencies: {
|
||||
"cash-dom": "^8.1.2"
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// src/main.ts
|
||||
var main_exports = {};
|
||||
__export(main_exports, {
|
||||
default: () => PasteImageRenamePlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian2 = require("obsidian");
|
||||
|
||||
// src/batch.ts
|
||||
var import_obsidian = require("obsidian");
|
||||
|
||||
// src/utils.ts
|
||||
var DEBUG = false;
|
||||
if (DEBUG)
|
||||
console.log("DEBUG is enabled");
|
||||
function debugLog(...args) {
|
||||
if (DEBUG) {
|
||||
console.log(new Date().toISOString().slice(11, 23), ...args);
|
||||
}
|
||||
}
|
||||
function createElementTree(rootEl, opts) {
|
||||
const result = {
|
||||
el: rootEl.createEl(opts.tag, opts),
|
||||
children: []
|
||||
};
|
||||
const children = opts.children || [];
|
||||
for (const child of children) {
|
||||
result.children.push(createElementTree(result.el, child));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
var path = {
|
||||
// Credit: @creationix/path.js
|
||||
join(...partSegments) {
|
||||
let parts = [];
|
||||
for (let i = 0, l = partSegments.length; i < l; i++) {
|
||||
parts = parts.concat(partSegments[i].split("/"));
|
||||
}
|
||||
const newParts = [];
|
||||
for (let i = 0, l = parts.length; i < l; i++) {
|
||||
const part = parts[i];
|
||||
if (!part || part === ".")
|
||||
continue;
|
||||
else
|
||||
newParts.push(part);
|
||||
}
|
||||
if (parts[0] === "")
|
||||
newParts.unshift("");
|
||||
return newParts.join("/");
|
||||
},
|
||||
// returns the last part of a path, e.g. 'foo.jpg'
|
||||
basename(fullpath) {
|
||||
const sp = fullpath.split("/");
|
||||
return sp[sp.length - 1];
|
||||
},
|
||||
// return extension without dot, e.g. 'jpg'
|
||||
extension(fullpath) {
|
||||
const positions = [...fullpath.matchAll(new RegExp("\\.", "gi"))].map((a) => a.index);
|
||||
return fullpath.slice(positions[positions.length - 1] + 1);
|
||||
}
|
||||
};
|
||||
var filenameNotAllowedChars = /[^\p{L}0-9~`!@$&*()\-_=+{};'",<.>? ]/ug;
|
||||
var sanitizer = {
|
||||
filename(s) {
|
||||
return s.replace(filenameNotAllowedChars, "").trim();
|
||||
},
|
||||
delimiter(s) {
|
||||
s = this.filename(s);
|
||||
if (!s)
|
||||
s = "-";
|
||||
return s;
|
||||
}
|
||||
};
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
function lockInputMethodComposition(el) {
|
||||
const state = {
|
||||
lock: false
|
||||
};
|
||||
el.addEventListener("compositionstart", () => {
|
||||
state.lock = true;
|
||||
});
|
||||
el.addEventListener("compositionend", () => {
|
||||
state.lock = false;
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
// src/batch.ts
|
||||
var ImageBatchRenameModal = class extends import_obsidian.Modal {
|
||||
constructor(app, activeFile, renameFunc, onClose) {
|
||||
super(app);
|
||||
this.activeFile = activeFile;
|
||||
this.renameFunc = renameFunc;
|
||||
this.onCloseExtra = onClose;
|
||||
this.state = {
|
||||
namePattern: "",
|
||||
extPattern: "",
|
||||
nameReplace: "",
|
||||
renameTasks: []
|
||||
};
|
||||
}
|
||||
onOpen() {
|
||||
this.containerEl.addClass("image-rename-modal");
|
||||
const { contentEl, titleEl } = this;
|
||||
titleEl.setText("Batch rename embeded files");
|
||||
const namePatternSetting = new import_obsidian.Setting(contentEl).setName("Name pattern").setDesc("Please input the name pattern to match files (regex)").addText((text) => text.setValue(this.state.namePattern).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.state.namePattern = value;
|
||||
})
|
||||
));
|
||||
const npInputEl = namePatternSetting.controlEl.children[0];
|
||||
npInputEl.focus();
|
||||
const npInputState = lockInputMethodComposition(npInputEl);
|
||||
npInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
|
||||
if (e.key === "Enter" && !npInputState.lock) {
|
||||
e.preventDefault();
|
||||
if (!this.state.namePattern) {
|
||||
errorEl.innerText = 'Error: "Name pattern" could not be empty';
|
||||
errorEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
this.matchImageNames(tbodyEl);
|
||||
}
|
||||
}));
|
||||
const extPatternSetting = new import_obsidian.Setting(contentEl).setName("Extension pattern").setDesc("Please input the extension pattern to match files (regex)").addText((text) => text.setValue(this.state.extPattern).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.state.extPattern = value;
|
||||
})
|
||||
));
|
||||
const extInputEl = extPatternSetting.controlEl.children[0];
|
||||
extInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.matchImageNames(tbodyEl);
|
||||
}
|
||||
}));
|
||||
const nameReplaceSetting = new import_obsidian.Setting(contentEl).setName("Name replace").setDesc("Please input the string to replace the matched name (use $1, $2 for regex groups)").addText((text) => text.setValue(this.state.nameReplace).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.state.nameReplace = value;
|
||||
})
|
||||
));
|
||||
const nrInputEl = nameReplaceSetting.controlEl.children[0];
|
||||
const nrInputState = lockInputMethodComposition(nrInputEl);
|
||||
nrInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
|
||||
if (e.key === "Enter" && !nrInputState.lock) {
|
||||
e.preventDefault();
|
||||
this.matchImageNames(tbodyEl);
|
||||
}
|
||||
}));
|
||||
const matchedContainer = contentEl.createDiv({
|
||||
cls: "matched-container"
|
||||
});
|
||||
const tableET = createElementTree(matchedContainer, {
|
||||
tag: "table",
|
||||
children: [
|
||||
{
|
||||
tag: "thead",
|
||||
children: [
|
||||
{
|
||||
tag: "tr",
|
||||
children: [
|
||||
{
|
||||
tag: "td",
|
||||
text: "Original path"
|
||||
},
|
||||
{
|
||||
tag: "td",
|
||||
text: "Renamed Name"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
tag: "tbody"
|
||||
}
|
||||
]
|
||||
});
|
||||
const tbodyEl = tableET.children[1].el;
|
||||
const errorEl = contentEl.createDiv({
|
||||
cls: "error",
|
||||
attr: {
|
||||
style: "display: none;"
|
||||
}
|
||||
});
|
||||
new import_obsidian.Setting(contentEl).addButton((button) => {
|
||||
button.setButtonText("Rename all").setClass("mod-cta").onClick(() => {
|
||||
new ConfirmModal(
|
||||
this.app,
|
||||
"Confirm rename all",
|
||||
`Are you sure? This will rename all the ${this.state.renameTasks.length} images matched the pattern.`,
|
||||
() => {
|
||||
this.renameAll();
|
||||
this.close();
|
||||
}
|
||||
).open();
|
||||
});
|
||||
}).addButton((button) => {
|
||||
button.setButtonText("Cancel").onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseExtra();
|
||||
}
|
||||
renameAll() {
|
||||
return __async(this, null, function* () {
|
||||
debugLog("renameAll", this.state);
|
||||
for (const task of this.state.renameTasks) {
|
||||
yield this.renameFunc(task.file, task.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
matchImageNames(tbodyEl) {
|
||||
const { state } = this;
|
||||
const renameTasks = [];
|
||||
tbodyEl.empty();
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.activeFile);
|
||||
if (!fileCache || !fileCache.embeds)
|
||||
return;
|
||||
const namePatternRegex = new RegExp(state.namePattern, "g");
|
||||
const extPatternRegex = new RegExp(state.extPattern);
|
||||
fileCache.embeds.forEach((embed) => {
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, this.activeFile.path);
|
||||
if (!file) {
|
||||
console.warn("file not found", embed.link);
|
||||
return;
|
||||
}
|
||||
if (state.extPattern) {
|
||||
const m0 = extPatternRegex.exec(file.extension);
|
||||
if (!m0)
|
||||
return;
|
||||
}
|
||||
const stem = file.basename;
|
||||
namePatternRegex.lastIndex = 0;
|
||||
const m1 = namePatternRegex.exec(stem);
|
||||
if (!m1)
|
||||
return;
|
||||
let renamedName = file.name;
|
||||
if (state.nameReplace) {
|
||||
namePatternRegex.lastIndex = 0;
|
||||
renamedName = stem.replace(namePatternRegex, state.nameReplace);
|
||||
renamedName = `${renamedName}.${file.extension}`;
|
||||
}
|
||||
renameTasks.push({
|
||||
file,
|
||||
name: renamedName
|
||||
});
|
||||
createElementTree(tbodyEl, {
|
||||
tag: "tr",
|
||||
children: [
|
||||
{
|
||||
tag: "td",
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
text: file.name
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
text: file.path,
|
||||
attr: {
|
||||
class: "file-path"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
tag: "td",
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
text: renamedName
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
text: path.join(file.parent.path, renamedName),
|
||||
attr: {
|
||||
class: "file-path"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
debugLog("new renameTasks", renameTasks);
|
||||
state.renameTasks = renameTasks;
|
||||
}
|
||||
};
|
||||
var ConfirmModal = class extends import_obsidian.Modal {
|
||||
constructor(app, title, message, onConfirm) {
|
||||
super(app);
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl, titleEl } = this;
|
||||
titleEl.setText(this.title);
|
||||
contentEl.createEl("p", {
|
||||
text: this.message
|
||||
});
|
||||
new import_obsidian.Setting(contentEl).addButton((button) => {
|
||||
button.setButtonText("Yes").setClass("mod-warning").onClick(() => {
|
||||
this.onConfirm();
|
||||
this.close();
|
||||
});
|
||||
}).addButton((button) => {
|
||||
button.setButtonText("No").onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/template.ts
|
||||
var dateTmplRegex = /{{DATE:([^}]+)}}/gm;
|
||||
var frontmatterTmplRegex = /{{frontmatter:([^}]+)}}/gm;
|
||||
var replaceDateVar = (s, date) => {
|
||||
const m = dateTmplRegex.exec(s);
|
||||
if (!m)
|
||||
return s;
|
||||
return s.replace(m[0], date.format(m[1]));
|
||||
};
|
||||
var replaceFrontmatterVar = (s, frontmatter) => {
|
||||
if (!frontmatter)
|
||||
return s;
|
||||
const m = frontmatterTmplRegex.exec(s);
|
||||
if (!m)
|
||||
return s;
|
||||
return s.replace(m[0], frontmatter[m[1]] || "");
|
||||
};
|
||||
var renderTemplate = (tmpl, data, frontmatter) => {
|
||||
const now = window.moment();
|
||||
let text = tmpl;
|
||||
let newtext;
|
||||
while ((newtext = replaceDateVar(text, now)) != text) {
|
||||
text = newtext;
|
||||
}
|
||||
while ((newtext = replaceFrontmatterVar(text, frontmatter)) != text) {
|
||||
text = newtext;
|
||||
}
|
||||
text = text.replace(/{{imageNameKey}}/gm, data.imageNameKey).replace(/{{fileName}}/gm, data.fileName).replace(/{{dirName}}/gm, data.dirName).replace(/{{firstHeading}}/gm, data.firstHeading);
|
||||
return text;
|
||||
};
|
||||
|
||||
// src/main.ts
|
||||
var DEFAULT_SETTINGS = {
|
||||
imageNamePattern: "{{fileName}}",
|
||||
dupNumberAtStart: false,
|
||||
dupNumberDelimiter: "-",
|
||||
dupNumberAlways: false,
|
||||
autoRename: false,
|
||||
handleAllAttachments: false,
|
||||
excludeExtensionPattern: "",
|
||||
disableRenameNotice: false
|
||||
};
|
||||
var PASTED_IMAGE_PREFIX = "Pasted image ";
|
||||
var PasteImageRenamePlugin = class extends import_obsidian2.Plugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.modals = [];
|
||||
}
|
||||
onload() {
|
||||
return __async(this, null, function* () {
|
||||
const pkg = require_package();
|
||||
console.log(`Plugin loading: ${pkg.name} ${pkg.version} BUILD_ENV=${"production"}`);
|
||||
yield this.loadSettings();
|
||||
this.registerEvent(
|
||||
this.app.vault.on("create", (file) => {
|
||||
if (!(file instanceof import_obsidian2.TFile))
|
||||
return;
|
||||
const timeGapMs = new Date().getTime() - file.stat.ctime;
|
||||
if (timeGapMs > 1e3)
|
||||
return;
|
||||
if (isMarkdownFile(file))
|
||||
return;
|
||||
if (isPastedImage(file)) {
|
||||
debugLog("pasted image created", file);
|
||||
this.startRenameProcess(file, this.settings.autoRename);
|
||||
} else {
|
||||
if (this.settings.handleAllAttachments) {
|
||||
debugLog("handleAllAttachments for file", file);
|
||||
if (this.testExcludeExtension(file)) {
|
||||
debugLog("excluded file by ext", file);
|
||||
return;
|
||||
}
|
||||
this.startRenameProcess(file, this.settings.autoRename);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const startBatchRenameProcess = () => {
|
||||
this.openBatchRenameModal();
|
||||
};
|
||||
this.addCommand({
|
||||
id: "batch-rename-embeded-files",
|
||||
name: "Batch rename embeded files (in the current file)",
|
||||
callback: startBatchRenameProcess
|
||||
});
|
||||
if (DEBUG) {
|
||||
this.addRibbonIcon("wand-glyph", "Batch rename embeded files", startBatchRenameProcess);
|
||||
}
|
||||
const batchRenameAllImages = () => {
|
||||
this.batchRenameAllImages();
|
||||
};
|
||||
this.addCommand({
|
||||
id: "batch-rename-all-images",
|
||||
name: "Batch rename all images instantly (in the current file)",
|
||||
callback: batchRenameAllImages
|
||||
});
|
||||
if (DEBUG) {
|
||||
this.addRibbonIcon("wand-glyph", "Batch rename all images instantly (in the current file)", batchRenameAllImages);
|
||||
}
|
||||
this.addSettingTab(new SettingTab(this.app, this));
|
||||
});
|
||||
}
|
||||
startRenameProcess(file, autoRename = false) {
|
||||
return __async(this, null, function* () {
|
||||
const activeFile = this.getActiveFile();
|
||||
if (!activeFile) {
|
||||
new import_obsidian2.Notice("Error: No active file found.");
|
||||
return;
|
||||
}
|
||||
const { stem, newName, isMeaningful } = this.generateNewName(file, activeFile);
|
||||
debugLog("generated newName:", newName, isMeaningful);
|
||||
if (!isMeaningful || !autoRename) {
|
||||
this.openRenameModal(file, isMeaningful ? stem : "", activeFile.path);
|
||||
return;
|
||||
}
|
||||
this.renameFile(file, newName, activeFile.path, true);
|
||||
});
|
||||
}
|
||||
renameFile(file, inputNewName, sourcePath, replaceCurrentLine) {
|
||||
return __async(this, null, function* () {
|
||||
const { name: newName } = yield this.deduplicateNewName(inputNewName, file);
|
||||
debugLog("deduplicated newName:", newName);
|
||||
const originName = file.name;
|
||||
const linkText = this.app.fileManager.generateMarkdownLink(file, sourcePath);
|
||||
const newPath = path.join(file.parent.path, newName);
|
||||
try {
|
||||
yield this.app.fileManager.renameFile(file, newPath);
|
||||
} catch (err) {
|
||||
new import_obsidian2.Notice(`Failed to rename ${newName}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
if (!replaceCurrentLine) {
|
||||
return;
|
||||
}
|
||||
const newLinkText = this.app.fileManager.generateMarkdownLink(file, sourcePath);
|
||||
debugLog("replace text", linkText, newLinkText);
|
||||
const editor = this.getActiveEditor();
|
||||
if (!editor) {
|
||||
new import_obsidian2.Notice(`Failed to rename ${newName}: no active editor`);
|
||||
return;
|
||||
}
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
const replacedLine = line.replace(linkText, newLinkText);
|
||||
debugLog("current line -> replaced line", line, replacedLine);
|
||||
editor.transaction({
|
||||
changes: [
|
||||
{
|
||||
from: __spreadProps(__spreadValues({}, cursor), { ch: 0 }),
|
||||
to: __spreadProps(__spreadValues({}, cursor), { ch: line.length }),
|
||||
text: replacedLine
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!this.settings.disableRenameNotice) {
|
||||
new import_obsidian2.Notice(`Renamed ${originName} to ${newName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
openRenameModal(file, newName, sourcePath) {
|
||||
const modal = new ImageRenameModal(
|
||||
this.app,
|
||||
file,
|
||||
newName,
|
||||
(confirmedName) => {
|
||||
debugLog("confirmedName:", confirmedName);
|
||||
this.renameFile(file, confirmedName, sourcePath, true);
|
||||
},
|
||||
() => {
|
||||
this.modals.splice(this.modals.indexOf(modal), 1);
|
||||
}
|
||||
);
|
||||
this.modals.push(modal);
|
||||
modal.open();
|
||||
debugLog("modals count", this.modals.length);
|
||||
}
|
||||
openBatchRenameModal() {
|
||||
const activeFile = this.getActiveFile();
|
||||
const modal = new ImageBatchRenameModal(
|
||||
this.app,
|
||||
activeFile,
|
||||
(file, name) => __async(this, null, function* () {
|
||||
yield this.renameFile(file, name, activeFile.path);
|
||||
}),
|
||||
() => {
|
||||
this.modals.splice(this.modals.indexOf(modal), 1);
|
||||
}
|
||||
);
|
||||
this.modals.push(modal);
|
||||
modal.open();
|
||||
}
|
||||
batchRenameAllImages() {
|
||||
return __async(this, null, function* () {
|
||||
const activeFile = this.getActiveFile();
|
||||
const fileCache = this.app.metadataCache.getFileCache(activeFile);
|
||||
if (!fileCache || !fileCache.embeds)
|
||||
return;
|
||||
const extPatternRegex = /jpe?g|png|gif|tiff|webp/i;
|
||||
for (const embed of fileCache.embeds) {
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, activeFile.path);
|
||||
if (!file) {
|
||||
console.warn("file not found", embed.link);
|
||||
return;
|
||||
}
|
||||
const m0 = extPatternRegex.exec(file.extension);
|
||||
if (!m0)
|
||||
return;
|
||||
const { newName, isMeaningful } = this.generateNewName(file, activeFile);
|
||||
debugLog("generated newName:", newName, isMeaningful);
|
||||
if (!isMeaningful) {
|
||||
new import_obsidian2.Notice("Failed to batch rename images: the generated name is not meaningful");
|
||||
break;
|
||||
}
|
||||
yield this.renameFile(file, newName, activeFile.path, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
// returns a new name for the input file, with extension
|
||||
generateNewName(file, activeFile) {
|
||||
let imageNameKey = "";
|
||||
let firstHeading = "";
|
||||
let frontmatter;
|
||||
const fileCache = this.app.metadataCache.getFileCache(activeFile);
|
||||
if (fileCache) {
|
||||
debugLog("frontmatter", fileCache.frontmatter);
|
||||
frontmatter = fileCache.frontmatter;
|
||||
imageNameKey = (frontmatter == null ? void 0 : frontmatter.imageNameKey) || "";
|
||||
firstHeading = getFirstHeading(fileCache.headings);
|
||||
} else {
|
||||
console.warn("could not get file cache from active file", activeFile.name);
|
||||
}
|
||||
const stem = renderTemplate(
|
||||
this.settings.imageNamePattern,
|
||||
{
|
||||
imageNameKey,
|
||||
fileName: activeFile.basename,
|
||||
dirName: activeFile.parent.name,
|
||||
firstHeading
|
||||
},
|
||||
frontmatter
|
||||
);
|
||||
const meaninglessRegex = new RegExp(`[${this.settings.dupNumberDelimiter}\\s]`, "gm");
|
||||
return {
|
||||
stem,
|
||||
newName: stem + "." + file.extension,
|
||||
isMeaningful: stem.replace(meaninglessRegex, "") !== ""
|
||||
};
|
||||
}
|
||||
// newName: foo.ext
|
||||
deduplicateNewName(newName, file) {
|
||||
return __async(this, null, function* () {
|
||||
const dir = file.parent.path;
|
||||
const listed = yield this.app.vault.adapter.list(dir);
|
||||
debugLog("sibling files", listed);
|
||||
const newNameExt = path.extension(newName), newNameStem = newName.slice(0, newName.length - newNameExt.length - 1), newNameStemEscaped = escapeRegExp(newNameStem), delimiter = this.settings.dupNumberDelimiter, delimiterEscaped = escapeRegExp(delimiter);
|
||||
let dupNameRegex;
|
||||
if (this.settings.dupNumberAtStart) {
|
||||
dupNameRegex = new RegExp(
|
||||
`^(?<number>\\d+)${delimiterEscaped}(?<name>${newNameStemEscaped})\\.${newNameExt}$`
|
||||
);
|
||||
} else {
|
||||
dupNameRegex = new RegExp(
|
||||
`^(?<name>${newNameStemEscaped})${delimiterEscaped}(?<number>\\d+)\\.${newNameExt}$`
|
||||
);
|
||||
}
|
||||
debugLog("dupNameRegex", dupNameRegex);
|
||||
const dupNameNumbers = [];
|
||||
let isNewNameExist = false;
|
||||
for (let sibling of listed.files) {
|
||||
sibling = path.basename(sibling);
|
||||
if (sibling == newName) {
|
||||
isNewNameExist = true;
|
||||
continue;
|
||||
}
|
||||
const m = dupNameRegex.exec(sibling);
|
||||
if (!m)
|
||||
continue;
|
||||
dupNameNumbers.push(parseInt(m.groups.number));
|
||||
}
|
||||
if (isNewNameExist || this.settings.dupNumberAlways) {
|
||||
const newNumber = dupNameNumbers.length > 0 ? Math.max(...dupNameNumbers) + 1 : 1;
|
||||
if (this.settings.dupNumberAtStart) {
|
||||
newName = `${newNumber}${delimiter}${newNameStem}.${newNameExt}`;
|
||||
} else {
|
||||
newName = `${newNameStem}${delimiter}${newNumber}.${newNameExt}`;
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: newName,
|
||||
stem: newName.slice(0, newName.length - newNameExt.length - 1),
|
||||
extension: newNameExt
|
||||
};
|
||||
});
|
||||
}
|
||||
getActiveFile() {
|
||||
const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
|
||||
const file = view == null ? void 0 : view.file;
|
||||
debugLog("active file", file == null ? void 0 : file.path);
|
||||
return file;
|
||||
}
|
||||
getActiveEditor() {
|
||||
const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
|
||||
return view == null ? void 0 : view.editor;
|
||||
}
|
||||
onunload() {
|
||||
this.modals.map((modal) => modal.close());
|
||||
}
|
||||
testExcludeExtension(file) {
|
||||
const pattern = this.settings.excludeExtensionPattern;
|
||||
if (!pattern)
|
||||
return false;
|
||||
return new RegExp(pattern).test(file.extension);
|
||||
}
|
||||
loadSettings() {
|
||||
return __async(this, null, function* () {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
|
||||
});
|
||||
}
|
||||
saveSettings() {
|
||||
return __async(this, null, function* () {
|
||||
yield this.saveData(this.settings);
|
||||
});
|
||||
}
|
||||
};
|
||||
function getFirstHeading(headings) {
|
||||
if (headings && headings.length > 0) {
|
||||
for (const heading of headings) {
|
||||
if (heading.level === 1) {
|
||||
return heading.heading;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function isPastedImage(file) {
|
||||
if (file instanceof import_obsidian2.TFile) {
|
||||
if (file.name.startsWith(PASTED_IMAGE_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isMarkdownFile(file) {
|
||||
if (file instanceof import_obsidian2.TFile) {
|
||||
if (file.extension === "md") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var ImageRenameModal = class extends import_obsidian2.Modal {
|
||||
constructor(app, src, stem, renameFunc, onClose) {
|
||||
super(app);
|
||||
this.src = src;
|
||||
this.stem = stem;
|
||||
this.renameFunc = renameFunc;
|
||||
this.onCloseExtra = onClose;
|
||||
}
|
||||
onOpen() {
|
||||
this.containerEl.addClass("image-rename-modal");
|
||||
const { contentEl, titleEl } = this;
|
||||
titleEl.setText("Rename image");
|
||||
const imageContainer = contentEl.createDiv({
|
||||
cls: "image-container"
|
||||
});
|
||||
imageContainer.createEl("img", {
|
||||
attr: {
|
||||
src: this.app.vault.getResourcePath(this.src)
|
||||
}
|
||||
});
|
||||
let stem = this.stem;
|
||||
const ext = this.src.extension;
|
||||
const getNewName = (stem2) => stem2 + "." + ext;
|
||||
const getNewPath = (stem2) => path.join(this.src.parent.path, getNewName(stem2));
|
||||
const infoET = createElementTree(contentEl, {
|
||||
tag: "ul",
|
||||
cls: "info",
|
||||
children: [
|
||||
{
|
||||
tag: "li",
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
text: "Origin path"
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
text: this.src.path
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
tag: "li",
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
text: "New path"
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
text: getNewPath(stem)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
const doRename = () => __async(this, null, function* () {
|
||||
debugLog("doRename", `stem=${stem}`);
|
||||
this.renameFunc(getNewName(stem));
|
||||
});
|
||||
const nameSetting = new import_obsidian2.Setting(contentEl).setName("New name").setDesc("Please input the new name for the image (without extension)").addText((text) => text.setValue(stem).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
stem = sanitizer.filename(value);
|
||||
infoET.children[1].children[1].el.innerText = getNewPath(stem);
|
||||
})
|
||||
));
|
||||
const nameInputEl = nameSetting.controlEl.children[0];
|
||||
nameInputEl.focus();
|
||||
const nameInputState = lockInputMethodComposition(nameInputEl);
|
||||
nameInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
|
||||
if (e.key === "Enter" && !nameInputState.lock) {
|
||||
e.preventDefault();
|
||||
if (!stem) {
|
||||
errorEl.innerText = 'Error: "New name" could not be empty';
|
||||
errorEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
doRename();
|
||||
this.close();
|
||||
}
|
||||
}));
|
||||
const errorEl = contentEl.createDiv({
|
||||
cls: "error",
|
||||
attr: {
|
||||
style: "display: none;"
|
||||
}
|
||||
});
|
||||
new import_obsidian2.Setting(contentEl).addButton((button) => {
|
||||
button.setButtonText("Rename").onClick(() => {
|
||||
doRename();
|
||||
this.close();
|
||||
});
|
||||
}).addButton((button) => {
|
||||
button.setButtonText("Cancel").onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseExtra();
|
||||
}
|
||||
};
|
||||
var imageNamePatternDesc = `
|
||||
The pattern indicates how the new name should be generated.
|
||||
|
||||
Available variables:
|
||||
- {{fileName}}: name of the active file, without ".md" extension.
|
||||
- {{imageNameKey}}: this variable is read from the markdown file's frontmatter, from the same key "imageNameKey".
|
||||
- {{DATE:$FORMAT}}: use "$FORMAT" to format the current date, "$FORMAT" must be a Moment.js format string, e.g. {{DATE:YYYY-MM-DD}}.
|
||||
|
||||
Here are some examples from pattern to image names (repeat in sequence), variables: fileName = "My note", imageNameKey = "foo":
|
||||
- {{fileName}}: My note, My note-1, My note-2
|
||||
- {{imageNameKey}}: foo, foo-1, foo-2
|
||||
- {{imageNameKey}}-{{DATE:YYYYMMDD}}: foo-20220408, foo-20220408-1, foo-20220408-2
|
||||
`;
|
||||
var SettingTab = class extends import_obsidian2.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
new import_obsidian2.Setting(containerEl).setName("Image name pattern").setDesc(imageNamePatternDesc).setClass("long-description-setting-item").addText((text) => text.setPlaceholder("{{imageNameKey}}").setValue(this.plugin.settings.imageNamePattern).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.imageNamePattern = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Duplicate number at start (or end)").setDesc(`If enabled, duplicate number will be added at the start as prefix for the image name, otherwise it will be added at the end as suffix for the image name.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAtStart).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.dupNumberAtStart = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Duplicate number delimiter").setDesc(`The delimiter to generate the number prefix/suffix for duplicated names. For example, if the value is "-", the suffix will be like "-1", "-2", "-3", and the prefix will be like "1-", "2-", "3-". Only characters that are valid in file names are allowed.`).addText((text) => text.setValue(this.plugin.settings.dupNumberDelimiter).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.dupNumberDelimiter = sanitizer.delimiter(value);
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Always add duplicate number").setDesc(`If enabled, duplicate number will always be added to the image name. Otherwise, it will only be added when the name is duplicated.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAlways).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.dupNumberAlways = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Auto rename").setDesc(`By default, the rename modal will always be shown to confirm before renaming, if this option is set, the image will be auto renamed after pasting.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.autoRename).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.autoRename = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Handle all attachments").setDesc(`By default, the plugin only handles images that starts with "Pasted image " in name,
|
||||
which is the prefix Obsidian uses to create images from pasted content.
|
||||
If this option is set, the plugin will handle all attachments that are created in the vault.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.handleAllAttachments).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.handleAllAttachments = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Exclude extension pattern").setDesc(`This option is only useful when "Handle all attachments" is enabled.
|
||||
Write a Regex pattern to exclude certain extensions from being handled. Only the first line will be used.`).setClass("single-line-textarea").addTextArea((text) => text.setPlaceholder("docx?|xlsx?|pptx?|zip|rar").setValue(this.plugin.settings.excludeExtensionPattern).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.excludeExtensionPattern = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
new import_obsidian2.Setting(containerEl).setName("Disable rename notice").setDesc(`Turn off this option if you don't want to see the notice when renaming images.
|
||||
Note that Obsidian may display a notice when a link has changed, this option cannot disable that.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.disableRenameNotice).onChange(
|
||||
(value) => __async(this, null, function* () {
|
||||
this.plugin.settings.disableRenameNotice = value;
|
||||
yield this.plugin.saveSettings();
|
||||
})
|
||||
));
|
||||
}
|
||||
};
|
||||
10
.obsidian/plugins/obsidian-paste-image-rename/manifest.json
vendored
Normal file
10
.obsidian/plugins/obsidian-paste-image-rename/manifest.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "obsidian-paste-image-rename",
|
||||
"name": "Paste image rename",
|
||||
"version": "1.6.1",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "Rename pasted images and all the other attchments added to the vault",
|
||||
"author": "Reorx",
|
||||
"authorUrl": "https://github.com/reorx",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
79
.obsidian/plugins/obsidian-paste-image-rename/styles.css
vendored
Normal file
79
.obsidian/plugins/obsidian-paste-image-rename/styles.css
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
/* src/styles.css */
|
||||
:root {
|
||||
--shadow-color: 0deg 0% 0%;
|
||||
--shadow-elevation-medium:
|
||||
0.5px 0.5px 0.7px hsl(var(--shadow-color) / 0.14),
|
||||
1.1px 1.1px 1.5px -0.9px hsl(var(--shadow-color) / 0.12),
|
||||
2.4px 2.5px 3.3px -1.8px hsl(var(--shadow-color) / 0.1),
|
||||
5.3px 5.6px 7.3px -2.7px hsl(var(--shadow-color) / 0.09),
|
||||
11px 11.4px 15.1px -3.6px hsl(var(--shadow-color) / 0.07);
|
||||
}
|
||||
.image-rename-modal .modal {
|
||||
width: 65%;
|
||||
min-width: 600px;
|
||||
}
|
||||
.image-rename-modal .modal-content {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
.image-rename-modal .image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-rename-modal .info {
|
||||
padding: 10px 0;
|
||||
color: var(--text-muted);
|
||||
user-select: text;
|
||||
}
|
||||
.image-rename-modal .info li > span:nth-of-type(1) {
|
||||
display: inline-block;
|
||||
width: 6em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
.image-rename-modal .info li > span:nth-of-type(1):after {
|
||||
content: ":";
|
||||
float: right;
|
||||
}
|
||||
.image-rename-modal .image-container img {
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
box-shadow: var(--shadow-elevation-medium);
|
||||
}
|
||||
.image-rename-modal .setting-item-control input {
|
||||
min-width: 300px;
|
||||
}
|
||||
.image-rename-modal .error {
|
||||
border: 1px solid rgb(201, 90, 90);
|
||||
color: rgb(134, 22, 22);
|
||||
padding: 10px;
|
||||
}
|
||||
.image-rename-modal table {
|
||||
font-size: .9em;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5em;
|
||||
user-select: text;
|
||||
}
|
||||
.image-rename-modal table td {
|
||||
padding-right: 1em;
|
||||
}
|
||||
.image-rename-modal table thead td {
|
||||
font-weight: 700;
|
||||
}
|
||||
.image-rename-modal table tbody td .file-path {
|
||||
font-size: .8em;
|
||||
color: var(--text-faint);
|
||||
line-height: 1;
|
||||
}
|
||||
.long-description-setting-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.long-description-setting-item .setting-item-description {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.long-description-setting-item .setting-item-control {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.long-description-setting-item .setting-item-control input {
|
||||
min-width: 300px;
|
||||
width: 50%;
|
||||
}
|
||||
Reference in New Issue
Block a user